diff --git a/.coveragerc b/.coveragerc index 243bf14c..03e5f372 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [run] branch = True -source = backend +source = src [report] omit = diff --git a/.github/workflows/backend-battery.yml b/.github/workflows/backend-battery.yml index c68978c9..e1336705 100644 --- a/.github/workflows/backend-battery.yml +++ b/.github/workflows/backend-battery.yml @@ -34,14 +34,14 @@ jobs: # - name: Ruff & Isort Check # run: | - # poetry run ruff check ./backend - # poetry run isort --check-only ./backend + # poetry run ruff check ./src + # poetry run isort --check-only ./src # - name: Type check # run: poetry run pyright # - name: Run Tests & Coverage - # run: poetry run pytest --cov=./backend --cov-report=xml + # run: poetry run pytest --cov=./src --cov-report=xml # - name: Upload Coverage Report to Codecov # uses: codecov/codecov-action@v4.1.1 diff --git a/.gitignore b/.gitignore index 3202d92a..80e556ba 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,10 @@ env/ venv/ ENV/ env.bak/ -venv.bak/ \ No newline at end of file +venv.bak/ + +# Rider IDE +**/.idea/ + +# MacOs +**/.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4fb888b9..0a0523fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,14 +24,6 @@ COPY pyproject.toml poetry.lock ./ RUN touch README.md RUN poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR -# Frontend Builder -FROM node:20-alpine AS frontend -WORKDIR /app -COPY frontend/package*.json ./ -RUN npm install -g pnpm && pnpm install -COPY frontend/ . -RUN pnpm run build && pnpm prune --prod - # Final Image FROM python:3.11-alpine LABEL name="Riven" \ @@ -42,33 +34,18 @@ LABEL name="Riven" \ ENV PYTHONUNBUFFERED=1 RUN apk add --no-cache \ curl \ - fish \ shadow \ - nodejs \ - npm \ rclone \ - fontconfig \ unzip \ gcc \ musl-dev \ libffi-dev \ - python3-dev && \ - npm install -g pnpm - -# Install Nerd Fonts -RUN mkdir -p /usr/share/fonts/nerd-fonts && \ - curl -fLo "/usr/share/fonts/nerd-fonts/FiraCode.zip" \ - https://github.com/ryanoasis/nerd-fonts/releases/download/v2.1.0/FiraCode.zip && \ - unzip /usr/share/fonts/nerd-fonts/FiraCode.zip -d /usr/share/fonts/nerd-fonts && \ - rm /usr/share/fonts/nerd-fonts/FiraCode.zip && \ - fc-cache -fv + python3-dev \ + libpq-dev # Install Poetry RUN pip install poetry==1.8.3 -# Create fish config directory -RUN mkdir -p /home/riven/.config/fish - # Set environment variable to force color output ENV FORCE_COLOR=1 ENV TERM=xterm-256color @@ -76,25 +53,17 @@ ENV TERM=xterm-256color # Set working directory WORKDIR /riven -# Copy frontend build from the previous stage -COPY --from=frontend /app/build /riven/frontend/build -COPY --from=frontend /app/node_modules /riven/frontend/node_modules -COPY --from=frontend /app/package.json /riven/frontend/package.json - # Copy the virtual environment from the builder stage COPY --from=builder /app/.venv /app/.venv ENV VIRTUAL_ENV=/app/.venv ENV PATH="/app/.venv/bin:$PATH" # Copy the rest of the application code -COPY backend/ /riven/backend -COPY pyproject.toml poetry.lock /riven/backend/ +COPY src/ /riven/src +COPY pyproject.toml poetry.lock /riven/src/ COPY VERSION entrypoint.sh /riven/ # Ensure entrypoint script is executable RUN chmod +x /riven/entrypoint.sh -# Switch to fish shell -SHELL ["fish", "--login"] - -ENTRYPOINT ["fish", "/riven/entrypoint.sh"] +ENTRYPOINT ["/riven/entrypoint.sh"] diff --git a/Dockerfile.slim b/Dockerfile.slim index df67cb51..9a18c103 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,4 +1,4 @@ -# Riven Backend Builder +# Riven src Builder FROM python:3.11.9-alpine3.19 as Base LABEL name="Riven" \ @@ -36,16 +36,16 @@ COPY pyproject.toml poetry.lock* /riven/ # Install Python dependencies RUN poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR -# Copy backend code and other necessary files -COPY backend/ /riven/backend +# Copy src code and other necessary files +COPY src/ /riven/src COPY VERSION entrypoint.sh /riven/ -RUN cd /riven/backend && poetry add nuitka && \ +RUN cd /riven/src && poetry add nuitka && \ poetry run python3 -m nuitka --standalone --onefile --onefile-tempdir-spec=/onefile_%PID%_%TIME% --python-flag=nosite,-O --nofollow-import-to=pytest --clang --warn-implicit-exceptions --warn-unusual-code --prefer-source-code main.py FROM scratch -COPY --from=Base /riven/backend/main.bin /main.bin +COPY --from=Base /riven/src/main.bin /main.bin COPY VERSION / VOLUME /data COPY --from=Base /lib/ /lib/ diff --git a/VERSION b/VERSION index 4d01880a..8adc70fd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.6 \ No newline at end of file +0.8.0 \ No newline at end of file diff --git a/backend/program/cache.py b/backend/program/cache.py deleted file mode 100644 index 12d566a1..00000000 --- a/backend/program/cache.py +++ /dev/null @@ -1,83 +0,0 @@ -import threading -from datetime import datetime -from typing import Iterator - -from cachetools import TTLCache -from utils.logger import logger - - -class HashCache: - """A class for caching hashes with additional metadata and a time-to-live (TTL) mechanism.""" - - def __init__(self, ttl: int = 900, maxsize: int = 10000): - """ - Initializes the HashCache with a specified TTL and maximum size. - - Args: - ttl (int): The time-to-live for each cache entry in seconds. - maxsize (int): The maximum size of the cache. - """ - self.cache = TTLCache(maxsize=maxsize, ttl=ttl) - self.lock = threading.RLock() - - def __contains__(self, infohash: str) -> bool: - """Check if a hash is in the cache.""" - with self.lock: - return infohash in self.cache - - def __iter__(self) -> Iterator[str]: - """Iterate over the cache.""" - with self.lock: - for infohash in self.cache: - yield infohash - - def is_blacklisted(self, infohash: str) -> bool: - """Check if a hash is blacklisted.""" - with self.lock: - return self._get_cache_entry(infohash).get("blacklisted", False) - - def is_downloaded(self, infohash: str) -> bool: - """Check if a hash is marked as downloaded.""" - with self.lock: - return self._get_cache_entry(infohash).get("downloaded", False) - - def blacklist(self, infohash: str) -> None: - """Blacklist a hash.""" - if not infohash: - raise ValueError("Infohash is required") - - with self.lock: - entry = self._get_cache_entry(infohash) - entry["blacklisted"] = True - self.cache[infohash] = entry - - def mark_downloaded(self, infohash: str) -> None: - """Mark a hash as downloaded.""" - if not self.is_downloaded(infohash): - with self.lock: - entry = self._get_cache_entry(infohash) - entry["downloaded"] = True - self.cache[infohash] = entry - logger.log("CACHE", f"Marked hash {infohash} as downloaded") - - def remove(self, infohash: str) -> None: - """Remove a hash from the blacklist.""" - if not infohash: - raise ValueError("Infohash is required") - - with self.lock: - if infohash in self.cache: - del self.cache[infohash] - logger.log("CACHE", f"Removed hash {infohash}") - - def clear_cache(self) -> None: - """Clear the cache.""" - with self.lock: - self.cache.clear() - - def _get_cache_entry(self, infohash: str) -> dict: - """Helper function to get a cache entry or create a new one if it doesn't exist.""" - return self.cache.get(infohash, {"blacklisted": False, "downloaded": False, "added_at": datetime.now()}) - - -hash_cache = HashCache() diff --git a/backend/program/downloaders/__init__.py b/backend/program/downloaders/__init__.py deleted file mode 100644 index 97de516e..00000000 --- a/backend/program/downloaders/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .realdebrid import Debrid -from .torbox import TorBoxDownloader \ No newline at end of file diff --git a/backend/program/downloaders/torbox.py b/backend/program/downloaders/torbox.py deleted file mode 100644 index d889a2b1..00000000 --- a/backend/program/downloaders/torbox.py +++ /dev/null @@ -1,100 +0,0 @@ -import contextlib -import time -from datetime import datetime -from typing import Generator - -from program.media.item import MediaItem, Movie -from program.settings.manager import settings_manager -from RTN import parse -from RTN.exceptions import GarbageTorrent -from utils.logger import logger -from utils.request import get, post - - -class TorBoxDownloader: - """TorBox Downloader""" - - def __init__(self, hash_cache): - self.key = "torbox_downloader" - self.settings = settings_manager.settings.downloaders.torbox - self.api_key = self.settings.api_key - self.base_url = "https://api.torbox.app/v1/api" - self.headers = {"Authorization": f"Bearer {self.api_key}"} - self.initialized = self.validate() - if not self.initialized: - return - self.hash_cache = hash_cache - logger.success("TorBox Downloader initialized!") - - def validate(self) -> bool: - """Validate the TorBox Downloader as a service""" - if not self.settings.enabled: - logger.warning("Torbox downloader is set to disabled.") - return False - if not self.settings.api_key: - logger.error("Torbox API key is not set") - try: - return self.get_expiry_date() > datetime.now() - except: - return False - - def run(self, item: MediaItem) -> Generator[MediaItem, None, None]: - """Download media item from TorBox""" - logger.info(f"Downloading {item.log_string} from TorBox") - if self.is_cached(item): - self.download(item) - yield item - - - def is_cached(self, item: MediaItem): - streams = [hash for hash in item.streams] - data = self.get_web_download_cached(streams) - for hash in data: - item.active_stream=data[hash] - return True - - def download(self, item: MediaItem): - if item.type == "movie": - exists = False - torrent_list = self.get_torrent_list() - for torrent in torrent_list: - if item.active_stream["hash"] == torrent["hash"]: - id = torrent["id"] - exists = True - break - if not exists: - id = self.create_torrent(item.active_stream["hash"]) - for torrent in torrent_list: - if torrent["id"] == id: - with contextlib.suppress(GarbageTorrent, TypeError): - for file in torrent["files"]: - if file["size"] > 10000: - parsed_file = parse(file["short_name"]) - if parsed_file.type == "movie": - item.set("folder", ".") - item.set("alternative_folder", ".") - item.set("file", file["short_name"]) - return True - - def get_expiry_date(self): - expiry = datetime.fromisoformat(self.get_user_data().premium_expires_at) - expiry = expiry.replace(tzinfo=None) - return expiry - - def get_web_download_cached(self, hash_list): - hash_string = ",".join(hash_list) - response = get(f"{self.base_url}/webdl/checkcached?hash={hash_string}", additional_headers=self.headers, response_type=dict) - return response.data["data"] - - def get_user_data(self): - response = get(f"{self.base_url}/user/me", additional_headers=self.headers, retry_if_failed=False) - return response.data.data - - def create_torrent(self, hash) -> int: - magnet_url = f"magnet:?xt=urn:btih:{hash}&dn=&tr=" - response = post(f"{self.base_url}/torrents/createtorrent", data={"magnet": magnet_url}, additional_headers=self.headers) - return response.data.data.torrent_id - - def get_torrent_list(self) -> list: - response = get(f"{self.base_url}/torrents/mylist", additional_headers=self.headers, response_type=dict) - return response.data["data"] diff --git a/backend/program/media/container.py b/backend/program/media/container.py deleted file mode 100644 index ad1f0cbb..00000000 --- a/backend/program/media/container.py +++ /dev/null @@ -1,303 +0,0 @@ -import os -import shutil -import tempfile -import threading -from copy import copy -from pickle import UnpicklingError -from typing import Dict, Generator, List, Optional - -import dill -from program.media.item import Episode, ItemId, MediaItem, Movie, Season, Show -from program.media.state import States -from utils.logger import logger - - -class ReadWriteLock: - def __init__(self): - self._read_ready = threading.Condition(threading.Lock()) - self._readers = 0 - - def acquire_read(self): - with self._read_ready: - self._readers += 1 - - def release_read(self): - with self._read_ready: - self._readers -= 1 - if self._readers == 0: - self._read_ready.notify_all() - - def acquire_write(self): - self._read_ready.acquire() - while self._readers > 0: - self._read_ready.wait() - - def release_write(self): - self._read_ready.release() - - def __enter__(self): - self.acquire_write() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.release_write() - - -class MediaItemContainer: - """A container to store media items.""" - - def __init__(self): - self._items: Dict[ItemId, MediaItem] = {} - self._shows: Dict[ItemId, Show] = {} - self._seasons: Dict[ItemId, Season] = {} - self._episodes: Dict[ItemId, Episode] = {} - self._movies: Dict[ItemId, Movie] = {} - self.library_file: str = "media.pkl" - self.lock: ReadWriteLock = ReadWriteLock() - - def __iter__(self) -> Generator[MediaItem, None, None]: - self.lock.acquire_read() - try: - for item in self._items.values(): - yield item - finally: - self.lock.release_read() - - def __contains__(self, item) -> bool: - self.lock.acquire_read() - try: - return item in self._items - finally: - self.lock.release_read() - - def __len__(self) -> int: - self.lock.acquire_read() - try: - return len(self._items) - finally: - self.lock.release_read() - - def __getitem__(self, item_id: ItemId) -> MediaItem: - self.lock.acquire_read() - try: - return self._items[item_id] - finally: - self.lock.release_read() - - def get(self, key, default=None) -> MediaItem: - self.lock.acquire_read() - try: - return self._items.get(key, default) - finally: - self.lock.release_read() - - @property - def seasons(self) -> dict[ItemId, Season]: - return copy(self._seasons) - - @property - def episodes(self) -> dict[ItemId, Episode]: - return copy(self._episodes) - - @property - def shows(self) -> dict[ItemId, Show]: - return copy(self._shows) - - @property - def movies(self) -> dict[ItemId, Movie]: - return copy(self._movies) - - def get_items_with_state(self, state) -> dict[ItemId, MediaItem]: - """Get items with the specified state""" - return { - item_id: self[item_id] - for item_id, item in self._items.items() - if item.state == state - } - - def get_incomplete_items(self) -> dict[ItemId, MediaItem]: - """Get items that are not completed.""" - self.lock.acquire_read() - try: - incomplete_items = {} - items_copy = list(self._items.items()) # Create a copy of the dictionary items - for item_id, item in items_copy: - if isinstance(item, Show): - if item.state not in [States.Completed]: - incomplete_items[item_id] = item - elif isinstance(item, Movie): - if item.state not in [States.Completed, States.PartiallyCompleted]: - incomplete_items[item_id] = item - return incomplete_items - finally: - self.lock.release_read() - - def get_item(self, identifier: str | ItemId) -> Optional[MediaItem]: - """Retrieve an item by its IMDb ID or item ID from the container.""" - self.lock.acquire_read() - try: - if isinstance(identifier, str) and identifier.startswith("tt"): - return self._items.get(ItemId(identifier)) - if isinstance(identifier, ItemId): - return self._items.get(identifier) - return None - finally: - self.lock.release_read() - - def get_episodes(self, show_id: ItemId) -> List[MediaItem]: - """Get all episodes for a show.""" - self.lock.acquire_read() - try: - return self.shows[show_id].episodes - finally: - self.lock.release_read() - - def upsert(self, item: MediaItem) -> None: - """Iterate through the input item and upsert all parents and children.""" - if not item: - logger.error(f"Item is None: {item}") - return - - self._items[item.item_id] = item - detatched = item.item_id.parent_id is None or item.parent is None - if isinstance(item, (Season, Episode)) and detatched: - if not item or not getattr(item, 'log_string', None): - logger.error(f"Detached item cannot be upserted into the database") - else: - logger.error( - f"{item.__class__.__name__} item {item.log_string} is detatched " + - "and not associated with a parent, and thus" + - " it cannot be upserted into the database" - ) - del self._items[item.item_id] - return - if isinstance(item, Show): - self._shows[item.item_id] = item - for season in item.seasons: - season.parent = item - self._items[season.item_id] = season - self._seasons[season.item_id] = season - for episode in season.episodes: - episode.parent = season - self._items[episode.item_id] = episode - self._episodes[episode.item_id] = episode - if isinstance(item, Season): - self._seasons[item.item_id] = item - # update children - for episode in item.episodes: - episode.parent = item - self._items[episode.item_id] = episode - self._episodes[episode.item_id] = episode - # Ensure the parent Show is updated in the container - container_show: Show = self._items[item.item_id.parent_id] - parent_index = container_show.get_season_index_by_id(item.item_id) - if parent_index is not None: - container_show.seasons[parent_index] = item - elif isinstance(item, Episode): - self._episodes[item.item_id] = item - # Ensure the parent Season is updated in the container - container_season: Season = self._items[item.item_id.parent_id] - parent_index = container_season.get_episode_index_by_id(item.item_id) - if parent_index is not None: - container_season.episodes[parent_index] = item - elif isinstance(item, Movie): - self._movies[item.item_id] = item - - def _index_item(self, item: MediaItem): - """Index the item and its children in the appropriate dictionaries.""" - self._items[item.item_id] = item - if isinstance(item, Show): - for season in item.seasons: - season.parent = item - season.item_id.parent_id = item.item_id - self._index_item(season) - elif isinstance(item, Season): - for episode in item.episodes: - episode.parent = item - episode.item_id.parent_id = item.item_id - self._index_item(episode) - - def remove(self, items): - """Remove a list of items, which could be movies, shows, seasons, or episodes.""" - self.lock.acquire_write() - try: - for item in items: - self._remove_item(item) - logger.debug(f"Removed items: {[item.log_string for item in items]}") - except Exception as e: - logger.error(f"Unexpected error occurred while removing items: {e}") - finally: - self.lock.release_write() - - def _remove_item(self, item): - """Helper method to remove an item from the container.""" - item_id = item.item_id - if item_id in self._items: - del self._items[item_id] - logger.debug(f"Successfully removed item with ID: {item_id}") - else: - logger.error(f"Item ID {item_id} not found in _items.") - - def count(self, state) -> int: - """Count items with given state in container""" - return len(self.get_items_with_state(state)) - - def save(self, filename: str = "media.pkl") -> None: - """Save the container to a file.""" - with self.lock, tempfile.NamedTemporaryFile(delete=False, mode="wb") as temp_file: - try: - dill.dump(self, temp_file, dill.HIGHEST_PROTOCOL) - temp_file.flush() - os.fsync(temp_file.fileno()) - except Exception as e: - logger.error(f"Failed to serialize data: {e}") - return - - try: - backup_filename = filename + ".bak" - if os.path.exists(filename): - shutil.copyfile(filename, backup_filename) - shutil.move(temp_file.name, filename) - except Exception as e: - logger.error(f"Failed to replace old file with new file: {e}") - try: - os.remove(temp_file.name) - except OSError as remove_error: - logger.error(f"Failed to remove temporary file: {remove_error}") - - def load(self, filename: str = "media.pkl") -> None: - """Load the container from a file.""" - try: - with open(filename, "rb") as file: - from_disk = dill.load(file) - self._items = from_disk._items - self._movies = from_disk._movies - self._shows = from_disk._shows - self._seasons = from_disk._seasons - self._episodes = from_disk._episodes - except FileNotFoundError: - pass - except (EOFError, UnpicklingError): - logger.error(f"Failed to unpickle media data at {filename}, wiping cached data") - os.remove(filename) - self._items = {} - self._movies = {} - self._shows = {} - self._seasons = {} - self._episodes = {} - - def log(self): - """Log the items in the container.""" - movies_symlinks = self._count_symlinks(self._movies) - episodes_symlinks = self._count_symlinks(self._episodes) - total_symlinks = movies_symlinks + episodes_symlinks - - logger.log("ITEM", f"Movies: {len(self._movies)} (Symlinks: {movies_symlinks})") - logger.log("ITEM", f"Shows: {len(self._shows)}") - logger.log("ITEM", f"Seasons: {len(self._seasons)}") - logger.log("ITEM", f"Episodes: {len(self._episodes)} (Symlinks: {episodes_symlinks})") - logger.log("ITEM", f"Total Items: {len(self._items)} (Symlinks: {total_symlinks})") - - def _count_symlinks(self, items): - """Count the number of symlinks in the given items.""" - return sum(1 for item in items.values() if item.symlinked) \ No newline at end of file diff --git a/backend/program/pickly.py b/backend/program/pickly.py deleted file mode 100644 index 54d78cad..00000000 --- a/backend/program/pickly.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import threading - -from program.media.container import MediaItemContainer - - -class Pickly(threading.Thread): - def __init__(self, media_items: MediaItemContainer, data_path: str): - super().__init__(name="Pickly") - self.media_items = media_items - self.data_path = data_path - self.media_file = os.path.join(self.data_path, "media.pkl") - self.running = False - self._stop_event = threading.Event() - - def start(self) -> None: - self.load() - self.running = True - super().start() - - def stop(self) -> None: - self.running = False - self._stop_event.set() - self.save() - - def join(self, timeout=None) -> None: - self.stop() - super().join(timeout) - - def load(self) -> None: - self.media_items.load(self.media_file) - - def save(self) -> None: - self.media_items.save(self.media_file) - - def run(self): - while self.running: - self.save() - self._stop_event.wait(60) diff --git a/backend/tests/test_items.py b/backend/tests/test_items.py deleted file mode 100644 index afecd193..00000000 --- a/backend/tests/test_items.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest.mock import MagicMock - -from controllers import items -from fastapi import FastAPI -from program.media.container import MediaItemContainer -from program.media.state import States -from starlette.testclient import TestClient - -app = FastAPI() -app.include_router(items.router) -app.program = MagicMock() -app.program.media_items = MediaItemContainer() - -client = TestClient(app) - - -def test_get_states(): - response = client.get("/items/states") - assert response.status_code == 200 - assert response.json() == { - "success": True, - "states": [state.value for state in States], - } - - -def test_get_items(): - response = client.get("/items/") - assert response.status_code == 200 - assert isinstance(response.json(), dict) - assert response.json()["success"] is True - assert isinstance(response.json()["items"], list) diff --git a/data/.gitkeep b/data/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 5ddc1ade..42afb1bc 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -9,11 +9,11 @@ services: network_mode: host tty: true environment: - # Docker - PUID=1000 - PGID=1000 - ORIGIN=${RIVEN_ORIGIN:-http://localhost:8080} - - BACKEND_URL=${RIVEN_BACKEND_URL:-http:// + # - RIVEN_FORCE_ENV=true + # - RIVEN_DATABASE_HOST=sqlite:////riven/data/media.db - TZ=UTC volumes: - ./data:/riven/data diff --git a/docker-compose.yml b/docker-compose.yml index 59e7f8fb..06dd1aa7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,19 @@ services: - ORIGIN=http://localhost:3000 - BACKEND_URL=http://127.0.0.1:8080 - TZ=America/New_York + - RIVEN_DATABASE_HOST=postgresql+psycopg2://postgres:postgres@riven_postgres/riven volumes: - ./data:/riven/data - /mnt:/mnt ports: - "3000:3000" + + riven_postgres: + image: postgres:16.3-alpine3.20 + container_name: riven-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: riven + networks: + - postgres-internal \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 503df733..ac33c49d 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,87 +1,80 @@ -#!/usr/bin/fish -set PUID (set -q PUID; and echo $PUID; or echo 1000) -set PGID (set -q PGID; and echo $PGID; or echo 1000) +#!/bin/sh + +# Default PUID and PGID to 1000 if not set +PUID=${PUID:-1000} +PGID=${PGID:-1000} echo "Starting Container with $PUID:$PGID permissions..." -if [ "$PUID" = "0" ] - echo running as root user - set USER_HOME "/home/$USERNAME" - mkdir -p $USER_HOME +if [ "$PUID" = "0" ]; then + echo "Running as root user" + USER_HOME="/root" + mkdir -p "$USER_HOME" else - if not echo $PUID | grep -qE '^[0-9]+$' + # Validate PUID and PGID are integers + if ! echo "$PUID" | grep -qE '^[0-9]+$'; then echo "PUID is not a valid integer. Exiting..." exit 1 - end + fi - if not echo $PGID | grep -qE '^[0-9]+$' + if ! echo "$PGID" | grep -qE '^[0-9]+$'; then echo "PGID is not a valid integer. Exiting..." exit 1 - end + fi - set -q USERNAME; or set USERNAME riven - set -q GROUPNAME; or set GROUPNAME riven + # Default USERNAME and GROUPNAME if not set + USERNAME=${USERNAME:-riven} + GROUPNAME=${GROUPNAME:-riven} - if not getent group $PGID > /dev/null - addgroup -g $PGID $GROUPNAME - if test $status -ne 0 - echo "Failed to create group. Exiting..." - exit 1 - end + # Create group if it doesn't exist + if ! getent group "$PGID" > /dev/null; then + addgroup --gid "$PGID" "$GROUPNAME" + if [ $? -ne 0 ]; then + echo "Failed to create group. Exiting..." + exit 1 + fi else - set GROUPNAME (getent group $PGID | cut -d: -f1) - end + GROUPNAME=$(getent group "$PGID" | cut -d: -f1) + fi - if not getent passwd $USERNAME > /dev/null - adduser -D -u $PUID -G $GROUPNAME $USERNAME - if test $status -ne 0 - echo "Failed to create user. Exiting..." - exit 1 - end + # Create user if it doesn't exist + if ! getent passwd "$USERNAME" > /dev/null; then + adduser -D -h "$USER_HOME" -u "$PUID" -G "$GROUPNAME" "$USERNAME" + if [ $? -ne 0 ]; then + echo "Failed to create user. Exiting..." + exit 1 + fi else - if test $PUID -ne 0 - usermod -u $PUID -g $PGID $USERNAME - if test $status -ne 0 - echo "Failed to modify user UID/GID. Exiting..." - exit 1 - end + if [ "$PUID" -ne 0 ]; then + usermod -u "$PUID" -g "$PGID" "$USERNAME" + if [ $? -ne 0 ]; then + echo "Failed to modify user UID/GID. Exiting..." + exit 1 + fi else - echo "Skipping usermod for root user." - end - end + echo "Skipping usermod for root user." + fi + fi - set USER_HOME "/home/$USERNAME" - mkdir -p $USER_HOME - chown -R $PUID:$PGID $USER_HOME - chown -R $PUID:$PGID /riven/data -end + USER_HOME="/home/$USERNAME" + mkdir -p "$USER_HOME" + chown -R "$PUID:$PGID" "$USER_HOME" + chown -R "$PUID:$PGID" /riven/data +fi umask 002 -set -x XDG_CONFIG_HOME "$USER_HOME/.config" -set -x XDG_DATA_HOME "$USER_HOME/.local/share" -set -x POETRY_CACHE_DIR "$USER_HOME/.cache/pypoetry" -set -x HOME $USER_HOME +export XDG_CONFIG_HOME="$USER_HOME/.config" +export XDG_DATA_HOME="$USER_HOME/.local/share" +export POETRY_CACHE_DIR="$USER_HOME/.cache/pypoetry" +export HOME="$USER_HOME" # Ensure poetry is in the PATH -set -x PATH $PATH /app/.venv/bin - -su -m $USERNAME -c "poetry config virtualenvs.create false" -set -q ORIGIN; or set ORIGIN "http://localhost:3000" -set -q BACKEND_URL; or set BACKEND_URL "http://127.0.0.1:8080" +export PATH="$PATH:/app/.venv/bin" +su -m "$USERNAME" -c "poetry config virtualenvs.create false" echo "Container Initialization complete." -# Start rclone in the background -# echo "Starting rclone..." -# rclone rcd --rc-web-gui --rc-addr 0.0.0.0:5572 --rc-no-auth --log-level ERROR &> /dev/null & -# set rclone_pid (jobs -p %1) - # Start the backend -echo "Starting backend..." -su -m $USERNAME -c "fish -c 'cd /riven/backend; and poetry run python3 main.py'" & -set backend_pid (jobs -p %1) - -# Start the frontend -echo "Starting frontend..." -exec su -m $USERNAME -c "fish -c 'ORIGIN=$ORIGIN BACKEND_URL=$BACKEND_URL node /riven/frontend/build'" +echo "Starting Riven (Backend)..." +su -m "$USERNAME" -c "cd /riven/src && poetry run python3 main.py" diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 6635cf55..00000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc deleted file mode 100644 index b6f27f13..00000000 --- a/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index cc41cea9..00000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 8bc6e864..00000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] -} diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 5ce67661..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# create-svelte - -Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npm create svelte@latest - -# create a new project in my-app -npm create svelte@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/frontend/components.json b/frontend/components.json deleted file mode 100644 index 7461b70e..00000000 --- a/frontend/components.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://shadcn-svelte.com/schema.json", - "style": "default", - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/app.css", - "baseColor": "slate" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils" - }, - "typescript": true -} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index e4bd6ce8..00000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,42 +0,0 @@ -import js from '@eslint/js'; -import ts from 'typescript-eslint'; -import svelte from 'eslint-plugin-svelte'; -import prettier from 'eslint-config-prettier'; -import globals from 'globals'; - -/** @type {import('eslint').Linter.FlatConfig[]} */ -export default [ - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs['flat/recommended'], - prettier, - ...svelte.configs['flat/prettier'], - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.node - } - } - }, - { - files: ['**/*.svelte'], - languageOptions: { - parserOptions: { - parser: ts.parser - } - }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^\\$\\$(Props|Events|Slots|Generic)$' - } - ] - } - }, - { - ignores: ['build/', '.svelte-kit/', 'dist/'] - } -]; diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 70f0ab31..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "test": "vitest", - "lint": "prettier --check . && eslint .", - "format": "prettier --write ." - }, - "devDependencies": { - "@sveltejs/adapter-node": "^5.0.1", - "@sveltejs/kit": "^2.5.10", - "@sveltejs/vite-plugin-svelte": "^3.1.1", - "@tailwindcss/typography": "^0.5.13", - "@types/eslint": "^8.56.10", - "@types/luxon": "^3.4.2", - "@types/nprogress": "^0.2.3", - "@types/uuid": "^9.0.8", - "autoprefixer": "^10.4.19", - "eslint": "^9.4.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.39.0", - "globals": "^15.4.0", - "postcss": "^8.4.38", - "prettier": "^3.3.1", - "prettier-plugin-svelte": "^3.2.4", - "prettier-plugin-tailwindcss": "^0.5.14", - "svelte": "^4.2.18", - "svelte-check": "^3.8.0", - "tailwindcss": "^3.4.4", - "tslib": "^2.6.3", - "typescript": "^5.4.5", - "typescript-eslint": "8.0.0-alpha.28", - "vite": "^5.2.12", - "vitest": "^2.0.1" - }, - "type": "module", - "dependencies": { - "bits-ui": "^0.21.10", - "clsx": "^2.1.1", - "cmdk-sv": "^0.0.17", - "embla-carousel-autoplay": "^8.1.5", - "embla-carousel-svelte": "^8.1.5", - "formsnap": "^1.0.0", - "lucide-svelte": "^0.390.0", - "luxon": "^3.4.4", - "mode-watcher": "^0.3.0", - "motion": "^10.18.0", - "nprogress": "^0.2.0", - "svelte-sonner": "^0.3.24", - "sveltekit-superforms": "^2.14.0", - "tailwind-merge": "^2.3.0", - "tailwind-variants": "^0.2.1", - "uuid": "^9.0.1", - "vaul-svelte": "^0.3.2", - "zod": "^3.23.8" - } -} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml deleted file mode 100644 index 48e9cc6a..00000000 --- a/frontend/pnpm-lock.yaml +++ /dev/null @@ -1,4135 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - bits-ui: - specifier: ^0.21.10 - version: 0.21.10(svelte@4.2.18) - clsx: - specifier: ^2.1.1 - version: 2.1.1 - cmdk-sv: - specifier: ^0.0.17 - version: 0.0.17(svelte@4.2.18) - embla-carousel-autoplay: - specifier: ^8.1.5 - version: 8.1.5(embla-carousel@8.1.5) - embla-carousel-svelte: - specifier: ^8.1.5 - version: 8.1.5(svelte@4.2.18) - formsnap: - specifier: ^1.0.0 - version: 1.0.1(svelte@4.2.18)(sveltekit-superforms@2.15.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)) - lucide-svelte: - specifier: ^0.390.0 - version: 0.390.0(svelte@4.2.18) - luxon: - specifier: ^3.4.4 - version: 3.4.4 - mode-watcher: - specifier: ^0.3.0 - version: 0.3.1(svelte@4.2.18) - motion: - specifier: ^10.18.0 - version: 10.18.0 - nprogress: - specifier: ^0.2.0 - version: 0.2.0 - svelte-sonner: - specifier: ^0.3.24 - version: 0.3.24(svelte@4.2.18) - sveltekit-superforms: - specifier: ^2.14.0 - version: 2.15.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18) - tailwind-merge: - specifier: ^2.3.0 - version: 2.3.0 - tailwind-variants: - specifier: ^0.2.1 - version: 0.2.1(tailwindcss@3.4.4) - uuid: - specifier: ^9.0.1 - version: 9.0.1 - vaul-svelte: - specifier: ^0.3.2 - version: 0.3.2(svelte@4.2.18) - zod: - specifier: ^3.23.8 - version: 3.23.8 - devDependencies: - '@sveltejs/adapter-node': - specifier: ^5.0.1 - version: 5.2.0(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1)) - '@sveltejs/kit': - specifier: ^2.5.10 - version: 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1) - '@sveltejs/vite-plugin-svelte': - specifier: ^3.1.1 - version: 3.1.1(svelte@4.2.18)(vite@5.3.1) - '@tailwindcss/typography': - specifier: ^0.5.13 - version: 0.5.13(tailwindcss@3.4.4) - '@types/eslint': - specifier: ^8.56.10 - version: 8.56.10 - '@types/luxon': - specifier: ^3.4.2 - version: 3.4.2 - '@types/nprogress': - specifier: ^0.2.3 - version: 0.2.3 - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 - autoprefixer: - specifier: ^10.4.19 - version: 10.4.19(postcss@8.4.38) - eslint: - specifier: ^9.4.0 - version: 9.5.0 - eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@9.5.0) - eslint-plugin-svelte: - specifier: ^2.39.0 - version: 2.40.0(eslint@9.5.0)(svelte@4.2.18) - globals: - specifier: ^15.4.0 - version: 15.6.0 - postcss: - specifier: ^8.4.38 - version: 8.4.38 - prettier: - specifier: ^3.3.1 - version: 3.3.2 - prettier-plugin-svelte: - specifier: ^3.2.4 - version: 3.2.4(prettier@3.3.2)(svelte@4.2.18) - prettier-plugin-tailwindcss: - specifier: ^0.5.14 - version: 0.5.14(prettier-plugin-svelte@3.2.4(prettier@3.3.2)(svelte@4.2.18))(prettier@3.3.2) - svelte: - specifier: ^4.2.18 - version: 4.2.18 - svelte-check: - specifier: ^3.8.0 - version: 3.8.1(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.18) - tailwindcss: - specifier: ^3.4.4 - version: 3.4.4 - tslib: - specifier: ^2.6.3 - version: 2.6.3 - typescript: - specifier: ^5.4.5 - version: 5.4.5 - typescript-eslint: - specifier: 8.0.0-alpha.28 - version: 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - vite: - specifier: ^5.2.12 - version: 5.3.1 - vitest: - specifier: ^2.0.1 - version: 2.0.1 - -packages: - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@arktype/schema@0.1.13': - resolution: {integrity: sha512-qZjtCAKrnhsixDWsEGJtosWfi4bLpAg4OnnICVYTer/6v5hwlhsdYpYobTSJUc5eiBoI5Ai/kcNfYaQISshY2g==} - - '@arktype/util@0.0.48': - resolution: {integrity: sha512-U5FO5EUAJ4LoYtLSyAMmTf6CEVgslObfSQuua2zoK5Tv2FB3aESVQ3rdLfhuz+coRhlzlynbkmimyoQWwQT+aQ==} - - '@babel/runtime@7.24.7': - resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} - engines: {node: '>=6.9.0'} - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.10.1': - resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.16.0': - resolution: {integrity: sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.5.0': - resolution: {integrity: sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@exodus/schemasafe@1.3.0': - resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - - '@floating-ui/core@1.6.2': - resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} - - '@floating-ui/dom@1.6.5': - resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} - - '@floating-ui/utils@0.2.2': - resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} - - '@gcornut/valibot-json-schema@0.31.0': - resolution: {integrity: sha512-3xGptCurm23e7nuPQkdrE5rEs1FeTPHhAUsBuwwqG4/YeZLwJOoYZv+fmsppUEfo5y9lzUwNQrNqLS/q7HMc7g==} - hasBin: true - - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} - engines: {node: '>=18.18'} - - '@internationalized/date@3.5.4': - resolution: {integrity: sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@melt-ui/svelte@0.61.2': - resolution: {integrity: sha512-BHkD9G31zQBToA4euDRBgTQRvWxT9scufOVCXgDO6HKTvyxFspbWT2bgiSFqAK4BbAGDn9Ao36Q8F9O71KN4OQ==} - peerDependencies: - svelte: '>=3 <5' - - '@melt-ui/svelte@0.76.2': - resolution: {integrity: sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==} - peerDependencies: - svelte: '>=3 <5' - - '@motionone/animation@10.18.0': - resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==} - - '@motionone/dom@10.18.0': - resolution: {integrity: sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==} - - '@motionone/easing@10.18.0': - resolution: {integrity: sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==} - - '@motionone/generators@10.18.0': - resolution: {integrity: sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==} - - '@motionone/types@10.17.1': - resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==} - - '@motionone/utils@10.18.0': - resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@polka/url@1.0.0-next.25': - resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} - - '@poppinss/macroable@1.0.2': - resolution: {integrity: sha512-xhhEcEvhQC8mP5oOr5hbE4CmUgmw/IPV1jhpGg2xSkzoFrt9i8YVqBQt9744EFesi5F7pBheWozg63RUBM/5JA==} - engines: {node: '>=18.16.0'} - - '@rollup/plugin-commonjs@26.0.1': - resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@15.2.3': - resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@5.1.0': - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.18.0': - resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.18.0': - resolution: {integrity: sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.18.0': - resolution: {integrity: sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.18.0': - resolution: {integrity: sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-linux-arm-gnueabihf@4.18.0': - resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.18.0': - resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.18.0': - resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.18.0': - resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': - resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.18.0': - resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.18.0': - resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.18.0': - resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.18.0': - resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.18.0': - resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.18.0': - resolution: {integrity: sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.18.0': - resolution: {integrity: sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==} - cpu: [x64] - os: [win32] - - '@sideway/address@4.1.5': - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@sinclair/typebox@0.32.33': - resolution: {integrity: sha512-jM50BfkKA0fwfj0uRRO6asfNfbU0oZipJIb/bL2+BUH/THjuEf2BMiqBOvKfBji5Z9t59NboZQGNfKZbdV50Iw==} - - '@sodaru/yup-to-json-schema@2.0.1': - resolution: {integrity: sha512-lWb0Wiz8KZ9ip/dY1eUqt7fhTPmL24p6Hmv5Fd9pzlzAdw/YNcWZr+tiCT4oZ4Zyxzi9+1X4zv82o7jYvcFxYA==} - - '@sveltejs/adapter-node@5.2.0': - resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==} - peerDependencies: - '@sveltejs/kit': ^2.4.0 - - '@sveltejs/kit@2.5.17': - resolution: {integrity: sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==} - engines: {node: '>=18.13'} - hasBin: true - peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 - svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.3 - - '@sveltejs/vite-plugin-svelte-inspector@2.1.0': - resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==} - engines: {node: ^18.0.0 || >=20} - peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 - svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.0 - - '@sveltejs/vite-plugin-svelte@3.1.1': - resolution: {integrity: sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==} - engines: {node: ^18.0.0 || >=20} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.0 - - '@swc/helpers@0.5.11': - resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} - - '@tailwindcss/typography@0.5.13': - resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' - - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - - '@types/eslint@8.56.10': - resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} - - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/luxon@3.4.2': - resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - - '@types/nprogress@0.2.3': - resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} - - '@types/pug@2.0.10': - resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@types/uuid@9.0.8': - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - - '@types/validator@13.12.0': - resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==} - - '@typescript-eslint/eslint-plugin@8.0.0-alpha.28': - resolution: {integrity: sha512-LsTbTdXiF7rP/H/ni85ia+01WmLBkkOq5sAi60OqHRa7mv7FTbwmiPwQ3hcHmXWxZFkHVy7hXXfBhwZzbbsaIA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@8.0.0-alpha.28': - resolution: {integrity: sha512-44N1MlwmhMUJ0jyrGuWe093wywcCIVu7I7Ge1H15HXCRIB7ptpSf3GJZRCDpumuEUKkVxoL8rdLixMnVDWyT2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@8.0.0-alpha.28': - resolution: {integrity: sha512-Iq8QFmJ2DH2tx7jfOraMZM1Y1axRfWh4t29JXRgbzvgiDQ2uHRHcaXqTulqsZXzJ0+vERNvNkOIPcQYGsNeGVQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/type-utils@8.0.0-alpha.28': - resolution: {integrity: sha512-Ta7vf3DtAakaeLEFGEnMGcfRtFV38PSLD+PvLT2xkof3vz5exKv4KYMyjAlNJmrhLzHwGWvHbt6IN8HoWwYnCw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@8.0.0-alpha.28': - resolution: {integrity: sha512-HYg+e0EWVShx0FEX0MAjDinYLmd+wD6nGMpbaddB1iACYwqaJFbf7vw0l+hdLTJvQC6UY8ndRkaEsL68QEoIZQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.0.0-alpha.28': - resolution: {integrity: sha512-I/5ODd4XJ+TO0XrKwDaB4tVGVi6kz2LAlN3WPd7mZVVtW21HHByCILRhOF9RbC69gJQ/TGHFpWCmAcsq2RZisg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@8.0.0-alpha.28': - resolution: {integrity: sha512-PnIz94+nbyjJisMI+KZqXMfw0wfIHvbyh0MGEx2M314wqm6SUWcxB5I8zduGQgJbRB0YFnboPS+MeSlBYPWrBQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - - '@typescript-eslint/visitor-keys@8.0.0-alpha.28': - resolution: {integrity: sha512-+ewAOeKDycydKMlnfmW8zAURTA8PR5Csyvxy6PJt4XRYjoquode9/eWaMt9Sp4Rz1FGMSVU9KxDRR83ASH/xkQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@vinejs/compiler@2.5.0': - resolution: {integrity: sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==} - engines: {node: '>=18.0.0'} - - '@vinejs/vine@1.8.0': - resolution: {integrity: sha512-Qq3XxbA26jzqS9ICifkqzT399lMQZ2fWtqeV3luI2as+UIK7qDifJFU2Q4W3q3IB5VXoWxgwAZSZEO0em9I/qQ==} - engines: {node: '>=18.16.0'} - - '@vitest/expect@2.0.1': - resolution: {integrity: sha512-yw70WL3ZwzbI2O3MOXYP2Shf4vqVkS3q5FckLJ6lhT9VMMtDyWdofD53COZcoeuHwsBymdOZp99r5bOr5g+oeA==} - - '@vitest/runner@2.0.1': - resolution: {integrity: sha512-XfcSXOGGxgR2dQ466ZYqf0ZtDLLDx9mZeQcKjQDLQ9y6Cmk2Wl7wxMuhiYK4Fo1VxCtLcFEGW2XpcfMuiD1Maw==} - - '@vitest/snapshot@2.0.1': - resolution: {integrity: sha512-rst79a4Q+J5vrvHRapdfK4BdqpMH0eF58jVY1vYeBo/1be+nkyenGI5SCSohmjf6MkCkI20/yo5oG+0R8qrAnA==} - - '@vitest/spy@2.0.1': - resolution: {integrity: sha512-NLkdxbSefAtJN56GtCNcB4GiHFb5i9q1uh4V229lrlTZt2fnwsTyjLuWIli1xwK2fQspJJmHXHyWx0Of3KTXWA==} - - '@vitest/utils@2.0.1': - resolution: {integrity: sha512-STH+2fHZxlveh1mpU4tKzNgRk7RZJyr6kFGJYCI5vocdfqfPsQrgVC6k7dBWHfin5QNB4TLvRS0Ckly3Dt1uWw==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.12.0: - resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - arktype@2.0.0-dev.21: - resolution: {integrity: sha512-dgHCjb3FK4BGvG2LuXqgdWXstbFmiYowSy0jiKnyk4KVcMT5DyIJ9d1nbQM3ztiAL3hIPmPdkmpfxUqR+BoOBQ==} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - autoprefixer@10.4.19: - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - axobject-query@4.0.0: - resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - bits-ui@0.21.10: - resolution: {integrity: sha512-KuweEOKO0Rr8XX87dQh46G9mG0bZSmTqNxj5qBazz4OTQC+oPKui04/wP/ISsCOSGFomaRydTULqh4p+nsyc2g==} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.118 - - bits-ui@0.9.9: - resolution: {integrity: sha512-LkdkyTtpXdkjBzPZJVJgpcre4fut6DONoprMfadHFo82HNUhph+02CxDjYEcZcThb5z4YjSxMlCYvQPZm+YtfQ==} - peerDependencies: - svelte: ^4.0.0 - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.23.1: - resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - - caniuse-lite@1.0.30001636: - resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} - - chai@5.1.1: - resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} - engines: {node: '>=12'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - cmdk-sv@0.0.17: - resolution: {integrity: sha512-28QTrK1tT1TSNoGq9MVnzjeLNNjCgjmsM8c2HJfDpRt9t+GD+9m3wX/WdAPaP9jdoNYU0SSdZVdgsGgpaSQOYQ==} - peerDependencies: - svelte: ^4.0.0 - - code-red@1.0.4: - resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - dayjs@1.11.11: - resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} - - debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - devalue@5.0.0: - resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} - - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.4.806: - resolution: {integrity: sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==} - - embla-carousel-autoplay@8.1.5: - resolution: {integrity: sha512-9pHIezRqiO9yism7wRFwmM1j/W9c9hBNX4hq3F4aTPbK84M0M57ryG8QUwhbsTGJbDoBBVTm//YEd0T6i/5YgA==} - peerDependencies: - embla-carousel: 8.1.5 - - embla-carousel-reactive-utils@8.1.5: - resolution: {integrity: sha512-76uZTrSaEGGta+qpiGkMFlLK0I7N04TdjZ2obrBhyggYIFDWlxk1CriIEmt2lisLNsa1IYXM85kr863JoCMSyg==} - peerDependencies: - embla-carousel: 8.1.5 - - embla-carousel-svelte@8.1.5: - resolution: {integrity: sha512-3XEIqfALTwmA73narVxMo6vNDqy+ezFgn5LyywYBfoq/azh7AHbNGpfG/2mZQBttJcI3JBswYwQoZklajhil1Q==} - peerDependencies: - svelte: ^3.49.0 || ^4.0.0 - - embla-carousel@8.1.5: - resolution: {integrity: sha512-R6xTf7cNdR2UTNM6/yUPZlJFRmZSogMiRjJ5vXHO65II5MoUlrVYUAP0fHQei/py82Vf15lj+WI+QdhnzBxA2g==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - es6-promise@3.3.1: - resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - - esbuild-runner@2.2.2: - resolution: {integrity: sha512-fRFVXcmYVmSmtYm2mL8RlUASt2TDkGh3uRcvHFOKNr/T58VrfVeKD9uT9nlgxk96u0LS0ehS/GY7Da/bXWKkhw==} - hasBin: true - peerDependencies: - esbuild: '*' - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-compat-utils@0.5.1: - resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - - eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-svelte@2.40.0: - resolution: {integrity: sha512-JuOzmfVaMeEkBASL7smHu3tIU4D9rWkHuRNV+zm/5zgAwiZVvxrXM7TcfIOS+U7VXOr4uCZuE+kZTVTzS0IE+Q==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.155 - peerDependenciesMeta: - svelte: - optional: true - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-scope@8.0.1: - resolution: {integrity: sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.5.0: - resolution: {integrity: sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - - esm-env@1.0.0: - resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} - - espree@10.1.0: - resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - - focus-trap@7.5.4: - resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} - - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} - engines: {node: '>=14'} - - formsnap@1.0.1: - resolution: {integrity: sha512-TvU9CoLSiacW1c7wXhLiyVpyy/LBfG0CEFDbs3M3jrsxBSrkTpsuhbQ8JYKY3CNCmIhZlgxCH+Vqr7RBF9G53w==} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.1 - sveltekit-superforms: ^2.3.0 - - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@10.4.2: - resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} - engines: {node: '>=16 || 14 >=14.18'} - hasBin: true - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@15.6.0: - resolution: {integrity: sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==} - engines: {node: '>=18'} - - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hey-listen@1.0.8: - resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} - - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - - is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} - - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@3.4.0: - resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} - engines: {node: '>=14'} - - jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} - hasBin: true - - joi@17.13.1: - resolution: {integrity: sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-to-ts@3.1.0: - resolution: {integrity: sha512-UeVN/ery4/JeXI8h4rM8yZPxsH+KqPi/84qFxHfTGHZnWnK9D0UU9ZGYO+6XAaJLqCWMiks+ARuFOKAiSxJCHA==} - engines: {node: '>=16'} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - just-clone@6.2.0: - resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - - known-css-properties@0.32.0: - resolution: {integrity: sha512-PXuex21brpp7qENI143ZL5cWQcMR4IZVeeZv9ew6dg+bZX2xRUu/NzGKudZJY5DO4APiMkNPYIF8VGIdY08Tdw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - loupe@3.1.1: - resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} - - lru-cache@10.2.2: - resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} - engines: {node: 14 || >=16.14} - - lucide-svelte@0.390.0: - resolution: {integrity: sha512-zaHTuk1KB2PqYrF/UY87g8DkforXEj7sYOo2Ml3LokNzx35dRJMHZHsuEXXTj9gqB76Da0o6Tud15bmtNBX1/Q==} - peerDependencies: - svelte: ^3 || ^4 || ^5.0.0-next.42 - - luxon@3.4.4: - resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} - engines: {node: '>=12'} - - magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - - mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - - memoize-weak@1.0.2: - resolution: {integrity: sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} - - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - mode-watcher@0.3.1: - resolution: {integrity: sha512-inghSVSXpEENV0SHM98+AvjDmTOjh9EJIrJXMScK42GNkDm5QJL7a/nZQ8M8i8RK7loFHqsQKBKdlXfw5e5C4w==} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.1 - - motion@10.18.0: - resolution: {integrity: sha512-MVAZZmwM/cp77BrNe1TxTMldxRPjwBNHheU5aPToqT4rJdZxLiADk58H+a0al5jKLxkB0OdgNq6DiVn11cjvIQ==} - - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - - mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} - engines: {node: '>=10'} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@4.0.2: - resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} - engines: {node: ^14 || ^16 || >=18} - hasBin: true - - nanoid@5.0.7: - resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} - engines: {node: ^18 || >=20} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-url@8.0.1: - resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} - engines: {node: '>=14.16'} - - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - nprogress@0.2.0: - resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - package-json-from-dist@1.0.0: - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-nested@6.0.1: - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - - postcss-safe-parser@6.0.0: - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.3.3 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - - postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} - - postcss-selector-parser@6.1.0: - resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-plugin-svelte@3.2.4: - resolution: {integrity: sha512-tZv+ADfeOWFNQkXkRh6zUXE16w3Vla8x2Ug0B/EnSmjR4EnwdwZbGgL/liSwR1kcEALU5mAAyua98HBxheCxgg==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - - prettier-plugin-tailwindcss@0.5.14: - resolution: {integrity: sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==} - engines: {node: '>=14.21.3'} - peerDependencies: - '@ianvs/prettier-plugin-sort-imports': '*' - '@prettier/plugin-pug': '*' - '@shopify/prettier-plugin-liquid': '*' - '@trivago/prettier-plugin-sort-imports': '*' - '@zackad/prettier-plugin-twig-melody': '*' - prettier: ^3.0 - prettier-plugin-astro: '*' - prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' - prettier-plugin-jsdoc: '*' - prettier-plugin-marko: '*' - prettier-plugin-organize-attributes: '*' - prettier-plugin-organize-imports: '*' - prettier-plugin-sort-imports: '*' - prettier-plugin-style-order: '*' - prettier-plugin-svelte: '*' - peerDependenciesMeta: - '@ianvs/prettier-plugin-sort-imports': - optional: true - '@prettier/plugin-pug': - optional: true - '@shopify/prettier-plugin-liquid': - optional: true - '@trivago/prettier-plugin-sort-imports': - optional: true - '@zackad/prettier-plugin-twig-melody': - optional: true - prettier-plugin-astro: - optional: true - prettier-plugin-css-order: - optional: true - prettier-plugin-import-sort: - optional: true - prettier-plugin-jsdoc: - optional: true - prettier-plugin-marko: - optional: true - prettier-plugin-organize-attributes: - optional: true - prettier-plugin-organize-imports: - optional: true - prettier-plugin-sort-imports: - optional: true - prettier-plugin-style-order: - optional: true - prettier-plugin-svelte: - optional: true - - prettier@3.3.2: - resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} - engines: {node: '>=14'} - hasBin: true - - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - property-expr@2.0.6: - resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rollup@4.18.0: - resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - - sander@0.5.1: - resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} - engines: {node: '>=10'} - hasBin: true - - set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - sirv@2.0.4: - resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} - engines: {node: '>= 10'} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - sorcery@0.11.1: - resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} - hasBin: true - - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - superstruct@1.0.4: - resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} - engines: {node: '>=14.0.0'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svelte-check@3.8.1: - resolution: {integrity: sha512-KlQ0TRVe01mdvh49Ylkr9FQxO/UWbQOtaIrccl3gjgkvby1TxY41VkT7ijCl6i29FjaJPE4m6YGmhdqov0MfkA==} - hasBin: true - peerDependencies: - svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - - svelte-eslint-parser@0.39.1: - resolution: {integrity: sha512-0VR9gq2TOdSrJW94Qf2F3XrzXRQomXQtRZGFS3FEUr3G4J8DcpqXfBF1HJyOa3dACyGsKiBbOPF56pBgYaqXBA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.115 - peerDependenciesMeta: - svelte: - optional: true - - svelte-hmr@0.16.0: - resolution: {integrity: sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==} - engines: {node: ^12.20 || ^14.13.1 || >= 16} - peerDependencies: - svelte: ^3.19.0 || ^4.0.0 - - svelte-preprocess@5.1.4: - resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==} - engines: {node: '>= 16.0.0'} - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - - svelte-sonner@0.3.24: - resolution: {integrity: sha512-txuL0JBUs0v6qGrr0PGCsbXmKHuthdrAkfISYi8umuveF7+gINb6EXl6VmKY9aHhyxCqvVgqd6yophQNrnor4w==} - peerDependencies: - svelte: '>=3 <5' - - svelte@4.2.18: - resolution: {integrity: sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==} - engines: {node: '>=16'} - - sveltekit-superforms@2.15.1: - resolution: {integrity: sha512-rLzcJTGEzt2oFC1fNYn+ddM25uoCawudHBU7qoLo5gp/JLMRNhtX9gbBMt8imMLo4VcB8339VtxBRcWiMV1faQ==} - peerDependencies: - '@sveltejs/kit': 1.x || 2.x - svelte: 3.x || 4.x || >=5.0.0-next.51 - - tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - - tailwind-merge@2.3.0: - resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} - - tailwind-variants@0.2.1: - resolution: {integrity: sha512-2xmhAf4UIc3PijOUcJPA1LP4AbxhpcHuHM2C26xM0k81r0maAO6uoUSHl3APmvHZcY5cZCY/bYuJdfFa4eGoaw==} - engines: {node: '>=16.x', pnpm: '>=7.x'} - peerDependencies: - tailwindcss: '*' - - tailwindcss@3.4.4: - resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} - engines: {node: '>=14.0.0'} - hasBin: true - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tiny-case@1.0.3: - resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} - - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - - tinybench@2.8.0: - resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} - - tinypool@1.0.0: - resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyspy@3.0.0: - resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} - engines: {node: '>=14.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toposort@2.0.2: - resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} - - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - - ts-api-utils@1.3.0: - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - - ts-deepmerge@7.0.0: - resolution: {integrity: sha512-WZ/iAJrKDhdINv1WG6KZIGHrZDar6VfhftG1QJFpVbOYZMYJLJOvZOo1amictRXVdBXZIgBHKswMTXzElngprA==} - engines: {node: '>=14.13.1'} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - - typescript-eslint@8.0.0-alpha.28: - resolution: {integrity: sha512-hCJMQp3fOwt7o3V8ETQoMK+6yctfN++Rq9rQ8cukTfu/oCLkUycuez2TqBbkkkGxT/prqRNteU72RxiregqgWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - - update-browserslist-db@1.0.16: - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - - valibot@0.31.1: - resolution: {integrity: sha512-2YYIhPrnVSz/gfT2/iXVTrSj92HwchCt9Cga/6hX4B26iCz9zkIsGTS0HjDYTZfTi1Un0X6aRvhBi1cfqs/i0Q==} - - validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} - engines: {node: '>= 0.10'} - - vaul-svelte@0.3.2: - resolution: {integrity: sha512-X4OGWttSTVUl417qGDsSFgOvIx24DoiMRY/jaP9z0v9FL8LQQJ0RQ1ZM0QpdyQPRlNd24ewjNQHh5EgYDtfNpw==} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.1 - - vite-node@2.0.1: - resolution: {integrity: sha512-nVd6kyhPAql0s+xIVJzuF+RSRH8ZimNrm6U8ZvTA4MXv8CHI17TFaQwRaFiK75YX6XeFqZD4IoAaAfi9OR1XvQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite@5.3.1: - resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitefu@0.2.5: - resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - vite: - optional: true - - vitest@2.0.1: - resolution: {integrity: sha512-PBPvNXRJiywtI9NmbnEqHIhcXlk8mB0aKf6REQIaYGY4JtWF1Pg8Am+N0vAuxdg/wUSlxPSVJr8QdjwcVxc2Hg==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.0.1 - '@vitest/ui': 2.0.1 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yaml@2.4.5: - resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} - engines: {node: '>= 14'} - hasBin: true - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - yup@1.4.0: - resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==} - - zod-to-json-schema@3.23.1: - resolution: {integrity: sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==} - peerDependencies: - zod: ^3.23.3 - - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - -snapshots: - - '@alloc/quick-lru@5.2.0': {} - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@arktype/schema@0.1.13': - dependencies: - '@arktype/util': 0.0.48 - optional: true - - '@arktype/util@0.0.48': - optional: true - - '@babel/runtime@7.24.7': - dependencies: - regenerator-runtime: 0.14.1 - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@eslint-community/eslint-utils@4.4.0(eslint@9.5.0)': - dependencies: - eslint: 9.5.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.10.1': {} - - '@eslint/config-array@0.16.0': - dependencies: - '@eslint/object-schema': 2.1.4 - debug: 4.3.5 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/eslintrc@3.1.0': - dependencies: - ajv: 6.12.6 - debug: 4.3.5 - espree: 10.1.0 - globals: 14.0.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.5.0': {} - - '@eslint/object-schema@2.1.4': {} - - '@exodus/schemasafe@1.3.0': - optional: true - - '@floating-ui/core@1.6.2': - dependencies: - '@floating-ui/utils': 0.2.2 - - '@floating-ui/dom@1.6.5': - dependencies: - '@floating-ui/core': 1.6.2 - '@floating-ui/utils': 0.2.2 - - '@floating-ui/utils@0.2.2': {} - - '@gcornut/valibot-json-schema@0.31.0': - dependencies: - valibot: 0.31.1 - optionalDependencies: - '@types/json-schema': 7.0.15 - esbuild: 0.21.5 - esbuild-runner: 2.2.2(esbuild@0.21.5) - optional: true - - '@hapi/hoek@9.3.0': - optional: true - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - optional: true - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.3.0': {} - - '@internationalized/date@3.5.4': - dependencies: - '@swc/helpers': 0.5.11 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.4.15': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - - '@melt-ui/svelte@0.61.2(svelte@4.2.18)': - dependencies: - '@floating-ui/core': 1.6.2 - '@floating-ui/dom': 1.6.5 - '@internationalized/date': 3.5.4 - dequal: 2.0.3 - focus-trap: 7.5.4 - nanoid: 4.0.2 - svelte: 4.2.18 - - '@melt-ui/svelte@0.76.2(svelte@4.2.18)': - dependencies: - '@floating-ui/core': 1.6.2 - '@floating-ui/dom': 1.6.5 - '@internationalized/date': 3.5.4 - dequal: 2.0.3 - focus-trap: 7.5.4 - nanoid: 5.0.7 - svelte: 4.2.18 - - '@motionone/animation@10.18.0': - dependencies: - '@motionone/easing': 10.18.0 - '@motionone/types': 10.17.1 - '@motionone/utils': 10.18.0 - tslib: 2.6.3 - - '@motionone/dom@10.18.0': - dependencies: - '@motionone/animation': 10.18.0 - '@motionone/generators': 10.18.0 - '@motionone/types': 10.17.1 - '@motionone/utils': 10.18.0 - hey-listen: 1.0.8 - tslib: 2.6.3 - - '@motionone/easing@10.18.0': - dependencies: - '@motionone/utils': 10.18.0 - tslib: 2.6.3 - - '@motionone/generators@10.18.0': - dependencies: - '@motionone/types': 10.17.1 - '@motionone/utils': 10.18.0 - tslib: 2.6.3 - - '@motionone/types@10.17.1': {} - - '@motionone/utils@10.18.0': - dependencies: - '@motionone/types': 10.17.1 - hey-listen: 1.0.8 - tslib: 2.6.3 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@polka/url@1.0.0-next.25': {} - - '@poppinss/macroable@1.0.2': - optional: true - - '@rollup/plugin-commonjs@26.0.1(rollup@4.18.0)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.18.0) - commondir: 1.0.1 - estree-walker: 2.0.2 - glob: 10.4.2 - is-reference: 1.2.1 - magic-string: 0.30.10 - optionalDependencies: - rollup: 4.18.0 - - '@rollup/plugin-json@6.1.0(rollup@4.18.0)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.18.0) - optionalDependencies: - rollup: 4.18.0 - - '@rollup/plugin-node-resolve@15.2.3(rollup@4.18.0)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.18.0) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-builtin-module: 3.2.1 - is-module: 1.0.0 - resolve: 1.22.8 - optionalDependencies: - rollup: 4.18.0 - - '@rollup/pluginutils@5.1.0(rollup@4.18.0)': - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 4.18.0 - - '@rollup/rollup-android-arm-eabi@4.18.0': - optional: true - - '@rollup/rollup-android-arm64@4.18.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.18.0': - optional: true - - '@rollup/rollup-darwin-x64@4.18.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.18.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.18.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.18.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.18.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.18.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.18.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.18.0': - optional: true - - '@sideway/address@4.1.5': - dependencies: - '@hapi/hoek': 9.3.0 - optional: true - - '@sideway/formula@3.0.1': - optional: true - - '@sideway/pinpoint@2.0.0': - optional: true - - '@sinclair/typebox@0.27.8': {} - - '@sinclair/typebox@0.32.33': - optional: true - - '@sodaru/yup-to-json-schema@2.0.1': - optional: true - - '@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1))': - dependencies: - '@rollup/plugin-commonjs': 26.0.1(rollup@4.18.0) - '@rollup/plugin-json': 6.1.0(rollup@4.18.0) - '@rollup/plugin-node-resolve': 15.2.3(rollup@4.18.0) - '@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1) - rollup: 4.18.0 - - '@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1)': - dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.18)(vite@5.3.1) - '@types/cookie': 0.6.0 - cookie: 0.6.0 - devalue: 5.0.0 - esm-env: 1.0.0 - import-meta-resolve: 4.1.0 - kleur: 4.1.5 - magic-string: 0.30.10 - mrmime: 2.0.0 - sade: 1.8.1 - set-cookie-parser: 2.6.0 - sirv: 2.0.4 - svelte: 4.2.18 - tiny-glob: 0.2.9 - vite: 5.3.1 - - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1)': - dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.18)(vite@5.3.1) - debug: 4.3.5 - svelte: 4.2.18 - vite: 5.3.1 - transitivePeerDependencies: - - supports-color - - '@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1)': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1) - debug: 4.3.5 - deepmerge: 4.3.1 - kleur: 4.1.5 - magic-string: 0.30.10 - svelte: 4.2.18 - svelte-hmr: 0.16.0(svelte@4.2.18) - vite: 5.3.1 - vitefu: 0.2.5(vite@5.3.1) - transitivePeerDependencies: - - supports-color - - '@swc/helpers@0.5.11': - dependencies: - tslib: 2.6.3 - - '@tailwindcss/typography@0.5.13(tailwindcss@3.4.4)': - dependencies: - lodash.castarray: 4.4.0 - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.4 - - '@types/cookie@0.6.0': {} - - '@types/eslint@8.56.10': - dependencies: - '@types/estree': 1.0.5 - '@types/json-schema': 7.0.15 - - '@types/estree@1.0.5': {} - - '@types/json-schema@7.0.15': {} - - '@types/luxon@3.4.2': {} - - '@types/nprogress@0.2.3': {} - - '@types/pug@2.0.10': {} - - '@types/resolve@1.20.2': {} - - '@types/uuid@9.0.8': {} - - '@types/validator@13.12.0': - optional: true - - '@typescript-eslint/eslint-plugin@8.0.0-alpha.28(@typescript-eslint/parser@8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5)': - dependencies: - '@eslint-community/regexpp': 4.10.1 - '@typescript-eslint/parser': 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 8.0.0-alpha.28 - '@typescript-eslint/type-utils': 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/utils': 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 8.0.0-alpha.28 - eslint: 9.5.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5)': - dependencies: - '@typescript-eslint/scope-manager': 8.0.0-alpha.28 - '@typescript-eslint/types': 8.0.0-alpha.28 - '@typescript-eslint/typescript-estree': 8.0.0-alpha.28(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 8.0.0-alpha.28 - debug: 4.3.5 - eslint: 9.5.0 - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.0.0-alpha.28': - dependencies: - '@typescript-eslint/types': 8.0.0-alpha.28 - '@typescript-eslint/visitor-keys': 8.0.0-alpha.28 - - '@typescript-eslint/type-utils@8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5)': - dependencies: - '@typescript-eslint/typescript-estree': 8.0.0-alpha.28(typescript@5.4.5) - '@typescript-eslint/utils': 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - debug: 4.3.5 - ts-api-utils: 1.3.0(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - eslint - - supports-color - - '@typescript-eslint/types@8.0.0-alpha.28': {} - - '@typescript-eslint/typescript-estree@8.0.0-alpha.28(typescript@5.4.5)': - dependencies: - '@typescript-eslint/types': 8.0.0-alpha.28 - '@typescript-eslint/visitor-keys': 8.0.0-alpha.28 - debug: 4.3.5 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.2 - ts-api-utils: 1.3.0(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) - '@typescript-eslint/scope-manager': 8.0.0-alpha.28 - '@typescript-eslint/types': 8.0.0-alpha.28 - '@typescript-eslint/typescript-estree': 8.0.0-alpha.28(typescript@5.4.5) - eslint: 9.5.0 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@8.0.0-alpha.28': - dependencies: - '@typescript-eslint/types': 8.0.0-alpha.28 - eslint-visitor-keys: 3.4.3 - - '@vinejs/compiler@2.5.0': - optional: true - - '@vinejs/vine@1.8.0': - dependencies: - '@poppinss/macroable': 1.0.2 - '@types/validator': 13.12.0 - '@vinejs/compiler': 2.5.0 - camelcase: 8.0.0 - dayjs: 1.11.11 - dlv: 1.1.3 - normalize-url: 8.0.1 - validator: 13.12.0 - optional: true - - '@vitest/expect@2.0.1': - dependencies: - '@vitest/spy': 2.0.1 - '@vitest/utils': 2.0.1 - chai: 5.1.1 - - '@vitest/runner@2.0.1': - dependencies: - '@vitest/utils': 2.0.1 - pathe: 1.1.2 - - '@vitest/snapshot@2.0.1': - dependencies: - magic-string: 0.30.10 - pathe: 1.1.2 - pretty-format: 29.7.0 - - '@vitest/spy@2.0.1': - dependencies: - tinyspy: 3.0.0 - - '@vitest/utils@2.0.1': - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 3.1.1 - pretty-format: 29.7.0 - - acorn-jsx@5.3.2(acorn@8.12.0): - dependencies: - acorn: 8.12.0 - - acorn@8.12.0: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-regex@6.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - - argparse@2.0.1: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - arktype@2.0.0-dev.21: - dependencies: - '@arktype/schema': 0.1.13 - '@arktype/util': 0.0.48 - optional: true - - array-union@2.1.0: {} - - assertion-error@2.0.1: {} - - autoprefixer@10.4.19(postcss@8.4.38): - dependencies: - browserslist: 4.23.1 - caniuse-lite: 1.0.30001636 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.38 - postcss-value-parser: 4.2.0 - - axobject-query@4.0.0: - dependencies: - dequal: 2.0.3 - - balanced-match@1.0.2: {} - - binary-extensions@2.3.0: {} - - bits-ui@0.21.10(svelte@4.2.18): - dependencies: - '@internationalized/date': 3.5.4 - '@melt-ui/svelte': 0.76.2(svelte@4.2.18) - nanoid: 5.0.7 - svelte: 4.2.18 - - bits-ui@0.9.9(svelte@4.2.18): - dependencies: - '@melt-ui/svelte': 0.61.2(svelte@4.2.18) - nanoid: 5.0.7 - svelte: 4.2.18 - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.23.1: - dependencies: - caniuse-lite: 1.0.30001636 - electron-to-chromium: 1.4.806 - node-releases: 2.0.14 - update-browserslist-db: 1.0.16(browserslist@4.23.1) - - buffer-crc32@1.0.0: {} - - buffer-from@1.1.2: - optional: true - - builtin-modules@3.3.0: {} - - cac@6.7.14: {} - - callsites@3.1.0: {} - - camelcase-css@2.0.1: {} - - camelcase@8.0.0: - optional: true - - caniuse-lite@1.0.30001636: {} - - chai@5.1.1: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.1 - pathval: 2.0.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - check-error@2.1.1: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - clsx@2.1.1: {} - - cmdk-sv@0.0.17(svelte@4.2.18): - dependencies: - bits-ui: 0.9.9(svelte@4.2.18) - nanoid: 5.0.7 - svelte: 4.2.18 - - code-red@1.0.4: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - '@types/estree': 1.0.5 - acorn: 8.12.0 - estree-walker: 3.0.3 - periscopic: 3.1.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@4.1.1: {} - - commondir@1.0.1: {} - - concat-map@0.0.1: {} - - cookie@0.6.0: {} - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css-tree@2.3.1: - dependencies: - mdn-data: 2.0.30 - source-map-js: 1.2.0 - - cssesc@3.0.0: {} - - dayjs@1.11.11: - optional: true - - debug@4.3.5: - dependencies: - ms: 2.1.2 - - deep-eql@5.0.2: {} - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - dequal@2.0.3: {} - - detect-indent@6.1.0: {} - - devalue@5.0.0: {} - - didyoumean@1.2.2: {} - - diff-sequences@29.6.3: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - dlv@1.1.3: {} - - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.4.806: {} - - embla-carousel-autoplay@8.1.5(embla-carousel@8.1.5): - dependencies: - embla-carousel: 8.1.5 - - embla-carousel-reactive-utils@8.1.5(embla-carousel@8.1.5): - dependencies: - embla-carousel: 8.1.5 - - embla-carousel-svelte@8.1.5(svelte@4.2.18): - dependencies: - embla-carousel: 8.1.5 - embla-carousel-reactive-utils: 8.1.5(embla-carousel@8.1.5) - svelte: 4.2.18 - - embla-carousel@8.1.5: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - es6-promise@3.3.1: {} - - esbuild-runner@2.2.2(esbuild@0.21.5): - dependencies: - esbuild: 0.21.5 - source-map-support: 0.5.21 - tslib: 2.4.0 - optional: true - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - escalade@3.1.2: {} - - escape-string-regexp@4.0.0: {} - - eslint-compat-utils@0.5.1(eslint@9.5.0): - dependencies: - eslint: 9.5.0 - semver: 7.6.2 - - eslint-config-prettier@9.1.0(eslint@9.5.0): - dependencies: - eslint: 9.5.0 - - eslint-plugin-svelte@2.40.0(eslint@9.5.0)(svelte@4.2.18): - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) - '@jridgewell/sourcemap-codec': 1.4.15 - eslint: 9.5.0 - eslint-compat-utils: 0.5.1(eslint@9.5.0) - esutils: 2.0.3 - known-css-properties: 0.32.0 - postcss: 8.4.38 - postcss-load-config: 3.1.4(postcss@8.4.38) - postcss-safe-parser: 6.0.0(postcss@8.4.38) - postcss-selector-parser: 6.1.0 - semver: 7.6.2 - svelte-eslint-parser: 0.39.1(svelte@4.2.18) - optionalDependencies: - svelte: 4.2.18 - transitivePeerDependencies: - - ts-node - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-scope@8.0.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.0.0: {} - - eslint@9.5.0: - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) - '@eslint-community/regexpp': 4.10.1 - '@eslint/config-array': 0.16.0 - '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.5.0 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.5 - escape-string-regexp: 4.0.0 - eslint-scope: 8.0.1 - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - esm-env@1.0.0: {} - - espree@10.1.0: - dependencies: - acorn: 8.12.0 - acorn-jsx: 5.3.2(acorn@8.12.0) - eslint-visitor-keys: 4.0.0 - - espree@9.6.1: - dependencies: - acorn: 8.12.0 - acorn-jsx: 5.3.2(acorn@8.12.0) - eslint-visitor-keys: 3.4.3 - - esquery@1.5.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.5 - - esutils@2.0.3: {} - - execa@8.0.1: - dependencies: - cross-spawn: 7.0.3 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.7 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - - flatted@3.3.1: {} - - focus-trap@7.5.4: - dependencies: - tabbable: 6.2.0 - - foreground-child@3.2.1: - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - - formsnap@1.0.1(svelte@4.2.18)(sveltekit-superforms@2.15.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)): - dependencies: - nanoid: 5.0.7 - svelte: 4.2.18 - sveltekit-superforms: 2.15.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18) - - fraction.js@4.3.7: {} - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - get-func-name@2.0.2: {} - - get-stream@8.0.1: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@10.4.2: - dependencies: - foreground-child: 3.2.1 - jackspeak: 3.4.0 - minimatch: 9.0.4 - minipass: 7.1.2 - package-json-from-dist: 1.0.0 - path-scurry: 1.11.1 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@14.0.0: {} - - globals@15.6.0: {} - - globalyzer@0.1.0: {} - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 3.0.0 - - globrex@0.1.2: {} - - graceful-fs@4.2.11: {} - - graphemer@1.4.0: {} - - has-flag@4.0.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hey-listen@1.0.8: {} - - human-signals@5.0.0: {} - - ignore@5.3.1: {} - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-meta-resolve@4.1.0: {} - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-builtin-module@3.2.1: - dependencies: - builtin-modules: 3.3.0 - - is-core-module@2.13.1: - dependencies: - hasown: 2.0.2 - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-module@1.0.0: {} - - is-number@7.0.0: {} - - is-path-inside@3.0.3: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.5 - - is-reference@3.0.2: - dependencies: - '@types/estree': 1.0.5 - - is-stream@3.0.0: {} - - isexe@2.0.0: {} - - jackspeak@3.4.0: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jiti@1.21.6: {} - - joi@17.13.1: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - optional: true - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-to-ts@3.1.0: - dependencies: - '@babel/runtime': 7.24.7 - ts-algebra: 2.0.0 - optional: true - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - just-clone@6.2.0: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kleur@4.1.5: {} - - known-css-properties@0.32.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@2.1.0: {} - - lilconfig@3.1.2: {} - - lines-and-columns@1.2.4: {} - - locate-character@3.0.0: {} - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.castarray@4.4.0: {} - - lodash.isplainobject@4.0.6: {} - - lodash.merge@4.6.2: {} - - loupe@3.1.1: - dependencies: - get-func-name: 2.0.2 - - lru-cache@10.2.2: {} - - lucide-svelte@0.390.0(svelte@4.2.18): - dependencies: - svelte: 4.2.18 - - luxon@3.4.4: {} - - magic-string@0.30.10: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - - mdn-data@2.0.30: {} - - memoize-weak@1.0.2: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.7: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mimic-fn@4.0.0: {} - - min-indent@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@9.0.4: - dependencies: - brace-expansion: 2.0.1 - - minimist@1.2.8: {} - - minipass@7.1.2: {} - - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - mode-watcher@0.3.1(svelte@4.2.18): - dependencies: - svelte: 4.2.18 - - motion@10.18.0: - dependencies: - '@motionone/animation': 10.18.0 - '@motionone/dom': 10.18.0 - '@motionone/types': 10.17.1 - '@motionone/utils': 10.18.0 - - mri@1.2.0: {} - - mrmime@2.0.0: {} - - ms@2.1.2: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.7: {} - - nanoid@4.0.2: {} - - nanoid@5.0.7: {} - - natural-compare@1.4.0: {} - - node-releases@2.0.14: {} - - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - normalize-url@8.0.1: - optional: true - - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - nprogress@0.2.0: {} - - object-assign@4.1.1: {} - - object-hash@3.0.0: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - package-json-from-dist@1.0.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-key@4.0.0: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.2.2 - minipass: 7.1.2 - - path-type@4.0.0: {} - - pathe@1.1.2: {} - - pathval@2.0.0: {} - - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.5 - estree-walker: 3.0.3 - is-reference: 3.0.2 - - picocolors@1.0.1: {} - - picomatch@2.3.1: {} - - pify@2.3.0: {} - - pirates@4.0.6: {} - - postcss-import@15.1.0(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - - postcss-js@4.0.1(postcss@8.4.38): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.38 - - postcss-load-config@3.1.4(postcss@8.4.38): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 - optionalDependencies: - postcss: 8.4.38 - - postcss-load-config@4.0.2(postcss@8.4.38): - dependencies: - lilconfig: 3.1.2 - yaml: 2.4.5 - optionalDependencies: - postcss: 8.4.38 - - postcss-nested@6.0.1(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - postcss-selector-parser: 6.1.0 - - postcss-safe-parser@6.0.0(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - - postcss-scss@4.0.9(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - - postcss-selector-parser@6.0.10: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-selector-parser@6.1.0: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - - postcss@8.4.38: - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - - prelude-ls@1.2.1: {} - - prettier-plugin-svelte@3.2.4(prettier@3.3.2)(svelte@4.2.18): - dependencies: - prettier: 3.3.2 - svelte: 4.2.18 - - prettier-plugin-tailwindcss@0.5.14(prettier-plugin-svelte@3.2.4(prettier@3.3.2)(svelte@4.2.18))(prettier@3.3.2): - dependencies: - prettier: 3.3.2 - optionalDependencies: - prettier-plugin-svelte: 3.2.4(prettier@3.3.2)(svelte@4.2.18) - - prettier@3.3.2: {} - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - property-expr@2.0.6: - optional: true - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - react-is@18.3.1: {} - - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - regenerator-runtime@0.14.1: {} - - resolve-from@4.0.0: {} - - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.0.4: {} - - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - - rollup@4.18.0: - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.18.0 - '@rollup/rollup-android-arm64': 4.18.0 - '@rollup/rollup-darwin-arm64': 4.18.0 - '@rollup/rollup-darwin-x64': 4.18.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.18.0 - '@rollup/rollup-linux-arm-musleabihf': 4.18.0 - '@rollup/rollup-linux-arm64-gnu': 4.18.0 - '@rollup/rollup-linux-arm64-musl': 4.18.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.18.0 - '@rollup/rollup-linux-riscv64-gnu': 4.18.0 - '@rollup/rollup-linux-s390x-gnu': 4.18.0 - '@rollup/rollup-linux-x64-gnu': 4.18.0 - '@rollup/rollup-linux-x64-musl': 4.18.0 - '@rollup/rollup-win32-arm64-msvc': 4.18.0 - '@rollup/rollup-win32-ia32-msvc': 4.18.0 - '@rollup/rollup-win32-x64-msvc': 4.18.0 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - sade@1.8.1: - dependencies: - mri: 1.2.0 - - sander@0.5.1: - dependencies: - es6-promise: 3.3.1 - graceful-fs: 4.2.11 - mkdirp: 0.5.6 - rimraf: 2.7.1 - - semver@7.6.2: {} - - set-cookie-parser@2.6.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - siginfo@2.0.0: {} - - signal-exit@4.1.0: {} - - sirv@2.0.4: - dependencies: - '@polka/url': 1.0.0-next.25 - mrmime: 2.0.0 - totalist: 3.0.1 - - slash@3.0.0: {} - - sorcery@0.11.1: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - buffer-crc32: 1.0.0 - minimist: 1.2.8 - sander: 0.5.1 - - source-map-js@1.2.0: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - optional: true - - source-map@0.6.1: - optional: true - - stackback@0.0.2: {} - - std-env@3.7.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - - strip-final-newline@3.0.0: {} - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - strip-json-comments@3.1.1: {} - - sucrase@3.35.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - commander: 4.1.1 - glob: 10.4.2 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - - superstruct@1.0.4: - optional: true - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svelte-check@3.8.1(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.18): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - chokidar: 3.6.0 - fast-glob: 3.3.2 - import-fresh: 3.3.0 - picocolors: 1.0.1 - sade: 1.8.1 - svelte: 4.2.18 - svelte-preprocess: 5.1.4(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.18)(typescript@5.4.5) - typescript: 5.4.5 - transitivePeerDependencies: - - '@babel/core' - - coffeescript - - less - - postcss - - postcss-load-config - - pug - - sass - - stylus - - sugarss - - svelte-eslint-parser@0.39.1(svelte@4.2.18): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.4.38 - postcss-scss: 4.0.9(postcss@8.4.38) - optionalDependencies: - svelte: 4.2.18 - - svelte-hmr@0.16.0(svelte@4.2.18): - dependencies: - svelte: 4.2.18 - - svelte-preprocess@5.1.4(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.18)(typescript@5.4.5): - dependencies: - '@types/pug': 2.0.10 - detect-indent: 6.1.0 - magic-string: 0.30.10 - sorcery: 0.11.1 - strip-indent: 3.0.0 - svelte: 4.2.18 - optionalDependencies: - postcss: 8.4.38 - postcss-load-config: 4.0.2(postcss@8.4.38) - typescript: 5.4.5 - - svelte-sonner@0.3.24(svelte@4.2.18): - dependencies: - svelte: 4.2.18 - - svelte@4.2.18: - dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - '@types/estree': 1.0.5 - acorn: 8.12.0 - aria-query: 5.3.0 - axobject-query: 4.0.0 - code-red: 1.0.4 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.2 - locate-character: 3.0.0 - magic-string: 0.30.10 - periscopic: 3.1.0 - - sveltekit-superforms@2.15.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18): - dependencies: - '@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1))(svelte@4.2.18)(vite@5.3.1) - devalue: 5.0.0 - just-clone: 6.2.0 - memoize-weak: 1.0.2 - svelte: 4.2.18 - ts-deepmerge: 7.0.0 - optionalDependencies: - '@exodus/schemasafe': 1.3.0 - '@gcornut/valibot-json-schema': 0.31.0 - '@sinclair/typebox': 0.32.33 - '@sodaru/yup-to-json-schema': 2.0.1 - '@vinejs/vine': 1.8.0 - arktype: 2.0.0-dev.21 - joi: 17.13.1 - json-schema-to-ts: 3.1.0 - superstruct: 1.0.4 - valibot: 0.31.1 - yup: 1.4.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.1(zod@3.23.8) - - tabbable@6.2.0: {} - - tailwind-merge@2.3.0: - dependencies: - '@babel/runtime': 7.24.7 - - tailwind-variants@0.2.1(tailwindcss@3.4.4): - dependencies: - tailwind-merge: 2.3.0 - tailwindcss: 3.4.4 - - tailwindcss@3.4.4: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.6 - lilconfig: 2.1.0 - micromatch: 4.0.7 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.38 - postcss-import: 15.1.0(postcss@8.4.38) - postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38) - postcss-nested: 6.0.1(postcss@8.4.38) - postcss-selector-parser: 6.1.0 - resolve: 1.22.8 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node - - text-table@0.2.0: {} - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tiny-case@1.0.3: - optional: true - - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - - tinybench@2.8.0: {} - - tinypool@1.0.0: {} - - tinyspy@3.0.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toposort@2.0.2: - optional: true - - totalist@3.0.1: {} - - ts-algebra@2.0.0: - optional: true - - ts-api-utils@1.3.0(typescript@5.4.5): - dependencies: - typescript: 5.4.5 - - ts-deepmerge@7.0.0: {} - - ts-interface-checker@0.1.13: {} - - tslib@2.4.0: - optional: true - - tslib@2.6.3: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@2.19.0: - optional: true - - typescript-eslint@8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5): - dependencies: - '@typescript-eslint/eslint-plugin': 8.0.0-alpha.28(@typescript-eslint/parser@8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/parser': 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/utils': 8.0.0-alpha.28(eslint@9.5.0)(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - eslint - - supports-color - - typescript@5.4.5: {} - - update-browserslist-db@1.0.16(browserslist@4.23.1): - dependencies: - browserslist: 4.23.1 - escalade: 3.1.2 - picocolors: 1.0.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - util-deprecate@1.0.2: {} - - uuid@9.0.1: {} - - valibot@0.31.1: - optional: true - - validator@13.12.0: - optional: true - - vaul-svelte@0.3.2(svelte@4.2.18): - dependencies: - bits-ui: 0.21.10(svelte@4.2.18) - svelte: 4.2.18 - - vite-node@2.0.1: - dependencies: - cac: 6.7.14 - debug: 4.3.5 - pathe: 1.1.2 - picocolors: 1.0.1 - vite: 5.3.1 - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - - vite@5.3.1: - dependencies: - esbuild: 0.21.5 - postcss: 8.4.38 - rollup: 4.18.0 - optionalDependencies: - fsevents: 2.3.3 - - vitefu@0.2.5(vite@5.3.1): - optionalDependencies: - vite: 5.3.1 - - vitest@2.0.1: - dependencies: - '@ampproject/remapping': 2.3.0 - '@vitest/expect': 2.0.1 - '@vitest/runner': 2.0.1 - '@vitest/snapshot': 2.0.1 - '@vitest/spy': 2.0.1 - '@vitest/utils': 2.0.1 - chai: 5.1.1 - debug: 4.3.5 - execa: 8.0.1 - magic-string: 0.30.10 - pathe: 1.1.2 - picocolors: 1.0.1 - std-env: 3.7.0 - tinybench: 2.8.0 - tinypool: 1.0.0 - vite: 5.3.1 - vite-node: 2.0.1 - why-is-node-running: 2.2.2 - transitivePeerDependencies: - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.2.2: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - wrappy@1.0.2: {} - - yaml@1.10.2: {} - - yaml@2.4.5: {} - - yocto-queue@0.1.0: {} - - yup@1.4.0: - dependencies: - property-expr: 2.0.6 - tiny-case: 1.0.3 - toposort: 2.0.2 - type-fest: 2.19.0 - optional: true - - zod-to-json-schema@3.23.1(zod@3.23.8): - dependencies: - zod: 3.23.8 - optional: true - - zod@3.23.8: {} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 0f772168..00000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } -}; diff --git a/frontend/src/app.css b/frontend/src/app.css deleted file mode 100644 index 6741ba88..00000000 --- a/frontend/src/app.css +++ /dev/null @@ -1,65 +0,0 @@ -@import 'nprogress.css'; - -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 204 60% 98%; - --foreground: 204 70% 4%; - --muted: 204 21% 89%; - --muted-foreground: 204 15% 40%; - --popover: 204 60% 98%; - --popover-foreground: 204 70% 4%; - --card: 204 60% 97%; - --card-foreground: 204 70% 3%; - --border: 220 13% 91%; - --input: 220 13% 91%; - --primary: 181.13 45.2% 59.29%; - --primary-foreground: 0 0% 100%; - --secondary: 204 4% 92%; - --secondary-foreground: 204 4% 32%; - --accent: 204 13% 83%; - --accent-foreground: 204 13% 23%; - --destructive: 0 75% 36.08%; - --destructive-foreground: 0 0% 100%; - --ring: 185.66 41.87% 53.69%; - --radius: 0.5rem; - } - - .dark { - --background: 215.29 48.57% 6.86%; - --foreground: 204 31% 99%; - --muted: 204 21% 11%; - --muted-foreground: 204 15% 60%; - --popover: 204 43% 4%; - --popover-foreground: 204 31% 99%; - --card: 204 43% 5%; - --card-foreground: 0 0% 100%; - --border: 215 27.9% 16.9%; - --input: 215 27.9% 16.9%; - --primary: 204.78 44.23% 20.39%; - --primary-foreground: 0 0% 100%; - --secondary: 204 3% 18%; - --secondary-foreground: 204 3% 78%; - --accent: 204 9% 28%; - --accent-foreground: 204 9% 88%; - --destructive: 0 74.71% 36.1%; - --destructive-foreground: 0 0% 100%; - --ring: 29.43 92.41% 32.78%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - -.no-scrollbar::-webkit-scrollbar { - display: none; -} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts deleted file mode 100644 index 743f07b2..00000000 --- a/frontend/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index 47c907ad..00000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts deleted file mode 100644 index 78e9438c..00000000 --- a/frontend/src/hooks.server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; -import { redirect, error } from '@sveltejs/kit'; -import { sequence } from '@sveltejs/kit/hooks'; -import { env } from '$env/dynamic/private'; - -const onboarding: Handle = async ({ event, resolve }) => { - if (!event.url.pathname.startsWith('/onboarding') && event.request.method === 'GET') { - const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - const res = await event.fetch(`${env.BACKEND_URL}/services`); - const data = await res.json(); - if (!data.success || !data.data) { - error(500, 'API Error'); - } - const toCheck = ['symlink', 'symlinklibrary']; - const allServicesTrue: boolean = toCheck.every((service) => data.data[service] === true); - if (!allServicesTrue) { - redirect(302, '/onboarding'); - } - } - - return resolve(event); -}; - -export const handle = sequence(onboarding); diff --git a/frontend/src/index.test.ts b/frontend/src/index.test.ts deleted file mode 100644 index e07cbbd7..00000000 --- a/frontend/src/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/frontend/src/lib/components/header-item.svelte b/frontend/src/lib/components/header-item.svelte deleted file mode 100644 index 8fe80932..00000000 --- a/frontend/src/lib/components/header-item.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if $page.url.pathname === navItem.path} - -
- {navItem.name} -
-
-{:else} - -
- {navItem.name} -
-
-{/if} diff --git a/frontend/src/lib/components/header.svelte b/frontend/src/lib/components/header.svelte deleted file mode 100644 index aed4dae3..00000000 --- a/frontend/src/lib/components/header.svelte +++ /dev/null @@ -1,122 +0,0 @@ - - - diff --git a/frontend/src/lib/components/home-items.svelte b/frontend/src/lib/components/home-items.svelte deleted file mode 100644 index eb1e0a4f..00000000 --- a/frontend/src/lib/components/home-items.svelte +++ /dev/null @@ -1,113 +0,0 @@ - - -
-
-
-
-
-
- -
-

Trending {name}

-
- - - -
- -
- {#each data.results as item, i} - -
- {item.title -
- - - {roundOff(item.vote_average)} - -
-
-
- {/each} -
-
- - -
-
diff --git a/frontend/src/lib/components/media-item.svelte b/frontend/src/lib/components/media-item.svelte deleted file mode 100644 index 57984437..00000000 --- a/frontend/src/lib/components/media-item.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - -
- - {data.title} - -
-
- {data.type} - • - - {data.state === 'PartiallyCompleted' ? 'Partial' : statesName[data.state]} - -
-
{data.title}
-
-
-
diff --git a/frontend/src/lib/components/theme-switcher.svelte b/frontend/src/lib/components/theme-switcher.svelte deleted file mode 100644 index 85ba7d2a..00000000 --- a/frontend/src/lib/components/theme-switcher.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - -{#if $mode === 'light'} - - - - - -

Dark mode

-
-
-{:else} - - - - - -

Light mode

-
-
-{/if} diff --git a/frontend/src/lib/components/ui/accordion/accordion-content.svelte b/frontend/src/lib/components/ui/accordion/accordion-content.svelte deleted file mode 100644 index 7943a40d..00000000 --- a/frontend/src/lib/components/ui/accordion/accordion-content.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - -
- -
-
diff --git a/frontend/src/lib/components/ui/accordion/accordion-item.svelte b/frontend/src/lib/components/ui/accordion/accordion-item.svelte deleted file mode 100644 index 21a3be62..00000000 --- a/frontend/src/lib/components/ui/accordion/accordion-item.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte b/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte deleted file mode 100644 index b1fb046a..00000000 --- a/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - svg]:rotate-180', - className - )} - {...$$restProps} - on:click - > - - - - diff --git a/frontend/src/lib/components/ui/accordion/index.ts b/frontend/src/lib/components/ui/accordion/index.ts deleted file mode 100644 index 0734d4e3..00000000 --- a/frontend/src/lib/components/ui/accordion/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Accordion as AccordionPrimitive } from 'bits-ui'; -import Content from './accordion-content.svelte'; -import Item from './accordion-item.svelte'; -import Trigger from './accordion-trigger.svelte'; -const Root = AccordionPrimitive.Root; - -export { - Root, - Content, - Item, - Trigger, - // - Root as Accordion, - Content as AccordionContent, - Item as AccordionItem, - Trigger as AccordionTrigger -}; diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte deleted file mode 100644 index 2c5ea4c0..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte deleted file mode 100644 index edf6d78d..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte deleted file mode 100644 index 970a3430..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte deleted file mode 100644 index c77c6840..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte deleted file mode 100644 index 329304a8..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
- -
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte deleted file mode 100644 index 434c29c9..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
- -
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte deleted file mode 100644 index 8b1969fe..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte deleted file mode 100644 index 347119a2..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte deleted file mode 100644 index 75287f36..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/index.ts b/frontend/src/lib/components/ui/alert-dialog/index.ts deleted file mode 100644 index fe3ff29c..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; - -import Title from './alert-dialog-title.svelte'; -import Action from './alert-dialog-action.svelte'; -import Cancel from './alert-dialog-cancel.svelte'; -import Portal from './alert-dialog-portal.svelte'; -import Footer from './alert-dialog-footer.svelte'; -import Header from './alert-dialog-header.svelte'; -import Overlay from './alert-dialog-overlay.svelte'; -import Content from './alert-dialog-content.svelte'; -import Description from './alert-dialog-description.svelte'; - -const Root = AlertDialogPrimitive.Root; -const Trigger = AlertDialogPrimitive.Trigger; - -export { - Root, - Title, - Action, - Cancel, - Portal, - Footer, - Header, - Trigger, - Overlay, - Content, - Description, - // - Root as AlertDialog, - Title as AlertDialogTitle, - Action as AlertDialogAction, - Cancel as AlertDialogCancel, - Portal as AlertDialogPortal, - Footer as AlertDialogFooter, - Header as AlertDialogHeader, - Trigger as AlertDialogTrigger, - Overlay as AlertDialogOverlay, - Content as AlertDialogContent, - Description as AlertDialogDescription -}; diff --git a/frontend/src/lib/components/ui/alert/alert-description.svelte b/frontend/src/lib/components/ui/alert/alert-description.svelte deleted file mode 100644 index 06d344cc..00000000 --- a/frontend/src/lib/components/ui/alert/alert-description.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
- -
diff --git a/frontend/src/lib/components/ui/alert/alert-title.svelte b/frontend/src/lib/components/ui/alert/alert-title.svelte deleted file mode 100644 index c63089bd..00000000 --- a/frontend/src/lib/components/ui/alert/alert-title.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/alert/alert.svelte b/frontend/src/lib/components/ui/alert/alert.svelte deleted file mode 100644 index 0bf6eec7..00000000 --- a/frontend/src/lib/components/ui/alert/alert.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert/index.ts b/frontend/src/lib/components/ui/alert/index.ts deleted file mode 100644 index 87f42b7a..00000000 --- a/frontend/src/lib/components/ui/alert/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { type VariantProps, tv } from 'tailwind-variants'; - -import Root from './alert.svelte'; -import Description from './alert-description.svelte'; -import Title from './alert-title.svelte'; - -export const alertVariants = tv({ - base: 'relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', - - variants: { - variant: { - default: 'bg-background text-foreground', - destructive: - 'border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive' - } - }, - defaultVariants: { - variant: 'default' - } -}); - -export type Variant = VariantProps['variant']; -export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; - -export { - Root, - Description, - Title, - // - Root as Alert, - Description as AlertDescription, - Title as AlertTitle -}; diff --git a/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte b/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte deleted file mode 100644 index 0870de93..00000000 --- a/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/aspect-ratio/index.ts b/frontend/src/lib/components/ui/aspect-ratio/index.ts deleted file mode 100644 index 1d9517e1..00000000 --- a/frontend/src/lib/components/ui/aspect-ratio/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Root from './aspect-ratio.svelte'; - -export { Root, Root as AspectRatio }; diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte deleted file mode 100644 index ba65f8c8..00000000 --- a/frontend/src/lib/components/ui/badge/badge.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/badge/index.ts b/frontend/src/lib/components/ui/badge/index.ts deleted file mode 100644 index 1fad281f..00000000 --- a/frontend/src/lib/components/ui/badge/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type VariantProps, tv } from 'tailwind-variants'; -export { default as Badge } from './badge.svelte'; - -export const badgeVariants = tv({ - base: 'inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - variants: { - variant: { - default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', - destructive: - 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', - outline: 'text-foreground' - } - }, - defaultVariants: { - variant: 'default' - } -}); - -export type Variant = VariantProps['variant']; diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte deleted file mode 100644 index 1aad3bbb..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte deleted file mode 100644 index ca5edc13..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
  • - -
  • diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte deleted file mode 100644 index 27e6ecd3..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -{#if asChild} - -{:else} - - - -{/if} diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte deleted file mode 100644 index e8b586a1..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
      - -
    diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte deleted file mode 100644 index a9d6d193..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte deleted file mode 100644 index 2717ebfc..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte deleted file mode 100644 index aa2803eb..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/breadcrumb/index.ts b/frontend/src/lib/components/ui/breadcrumb/index.ts deleted file mode 100644 index 773fd604..00000000 --- a/frontend/src/lib/components/ui/breadcrumb/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Root from './breadcrumb.svelte'; -import Ellipsis from './breadcrumb-ellipsis.svelte'; -import Item from './breadcrumb-item.svelte'; -import Separator from './breadcrumb-separator.svelte'; -import Link from './breadcrumb-link.svelte'; -import List from './breadcrumb-list.svelte'; -import Page from './breadcrumb-page.svelte'; - -export { - Root, - Ellipsis, - Item, - Separator, - Link, - List, - Page, - // - Root as Breadcrumb, - Ellipsis as BreadcrumbEllipsis, - Item as BreadcrumbItem, - Separator as BreadcrumbSeparator, - Link as BreadcrumbLink, - List as BreadcrumbList, - Page as BreadcrumbPage -}; diff --git a/frontend/src/lib/components/ui/button/button.svelte b/frontend/src/lib/components/ui/button/button.svelte deleted file mode 100644 index f578a575..00000000 --- a/frontend/src/lib/components/ui/button/button.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/button/index.ts b/frontend/src/lib/components/ui/button/index.ts deleted file mode 100644 index 0f43874b..00000000 --- a/frontend/src/lib/components/ui/button/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { type VariantProps, tv } from 'tailwind-variants'; -import type { Button as ButtonPrimitive } from 'bits-ui'; -import Root from './button.svelte'; - -const buttonVariants = tv({ - base: 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline' - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10' - } - }, - defaultVariants: { - variant: 'default', - size: 'default' - } -}); - -type Variant = VariantProps['variant']; -type Size = VariantProps['size']; - -type Props = ButtonPrimitive.Props & { - variant?: Variant; - size?: Size; -}; - -type Events = ButtonPrimitive.Events; - -export { - Root, - type Props, - type Events, - // - Root as Button, - type Props as ButtonProps, - type Events as ButtonEvents, - buttonVariants -}; diff --git a/frontend/src/lib/components/ui/card/card-content.svelte b/frontend/src/lib/components/ui/card/card-content.svelte deleted file mode 100644 index 52776fa9..00000000 --- a/frontend/src/lib/components/ui/card/card-content.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/card/card-description.svelte b/frontend/src/lib/components/ui/card/card-description.svelte deleted file mode 100644 index 33d29308..00000000 --- a/frontend/src/lib/components/ui/card/card-description.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -

    - -

    diff --git a/frontend/src/lib/components/ui/card/card-footer.svelte b/frontend/src/lib/components/ui/card/card-footer.svelte deleted file mode 100644 index c97fd438..00000000 --- a/frontend/src/lib/components/ui/card/card-footer.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/card/card-header.svelte b/frontend/src/lib/components/ui/card/card-header.svelte deleted file mode 100644 index 43be1c6e..00000000 --- a/frontend/src/lib/components/ui/card/card-header.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/card/card-title.svelte b/frontend/src/lib/components/ui/card/card-title.svelte deleted file mode 100644 index 5520e72c..00000000 --- a/frontend/src/lib/components/ui/card/card-title.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/card/card.svelte b/frontend/src/lib/components/ui/card/card.svelte deleted file mode 100644 index 78fe4ff5..00000000 --- a/frontend/src/lib/components/ui/card/card.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/card/index.ts b/frontend/src/lib/components/ui/card/index.ts deleted file mode 100644 index 86c54082..00000000 --- a/frontend/src/lib/components/ui/card/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Root from './card.svelte'; -import Content from './card-content.svelte'; -import Description from './card-description.svelte'; -import Footer from './card-footer.svelte'; -import Header from './card-header.svelte'; -import Title from './card-title.svelte'; - -export { - Root, - Content, - Description, - Footer, - Header, - Title, - // - Root as Card, - Content as CardContent, - Description as CardDescription, - Footer as CardFooter, - Header as CardHeader, - Title as CardTitle -}; - -export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; diff --git a/frontend/src/lib/components/ui/carousel/carousel-content.svelte b/frontend/src/lib/components/ui/carousel/carousel-content.svelte deleted file mode 100644 index d11117a4..00000000 --- a/frontend/src/lib/components/ui/carousel/carousel-content.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
    -
    - -
    -
    diff --git a/frontend/src/lib/components/ui/carousel/carousel-item.svelte b/frontend/src/lib/components/ui/carousel/carousel-item.svelte deleted file mode 100644 index 3f0262a8..00000000 --- a/frontend/src/lib/components/ui/carousel/carousel-item.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/carousel/carousel-next.svelte b/frontend/src/lib/components/ui/carousel/carousel-next.svelte deleted file mode 100644 index 51f3ecc7..00000000 --- a/frontend/src/lib/components/ui/carousel/carousel-next.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/carousel/carousel-previous.svelte b/frontend/src/lib/components/ui/carousel/carousel-previous.svelte deleted file mode 100644 index c5d1b1c4..00000000 --- a/frontend/src/lib/components/ui/carousel/carousel-previous.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/carousel/carousel.svelte b/frontend/src/lib/components/ui/carousel/carousel.svelte deleted file mode 100644 index 4241103d..00000000 --- a/frontend/src/lib/components/ui/carousel/carousel.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/carousel/context.ts b/frontend/src/lib/components/ui/carousel/context.ts deleted file mode 100644 index bfb65c6f..00000000 --- a/frontend/src/lib/components/ui/carousel/context.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { EmblaCarouselSvelteType } from 'embla-carousel-svelte'; -import type emblaCarouselSvelte from 'embla-carousel-svelte'; -import { getContext, hasContext, setContext } from 'svelte'; -import type { HTMLAttributes } from 'svelte/elements'; -import type { Readable, Writable } from 'svelte/store'; - -export type CarouselAPI = - NonNullable['on:emblaInit']> extends ( - evt: CustomEvent - ) => void - ? CarouselAPI - : never; - -type EmblaCarouselConfig = NonNullable[1]>; - -export type CarouselOptions = EmblaCarouselConfig['options']; -export type CarouselPlugins = EmblaCarouselConfig['plugins']; - -//// - -export type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugins; - api?: CarouselAPI; - orientation?: 'horizontal' | 'vertical'; -} & HTMLAttributes; - -const EMBLA_CAROUSEL_CONTEXT = Symbol('EMBLA_CAROUSEL_CONTEXT'); - -type EmblaContext = { - api: Writable; - orientation: Writable<'horizontal' | 'vertical'>; - scrollNext: () => void; - scrollPrev: () => void; - canScrollNext: Readable; - canScrollPrev: Readable; - handleKeyDown: (e: KeyboardEvent) => void; - options: Writable; - plugins: Writable; - onInit: (e: CustomEvent) => void; - scrollTo: (index: number, jump?: boolean) => void; - scrollSnaps: Readable; - selectedIndex: Readable; -}; - -export function setEmblaContext(config: EmblaContext): EmblaContext { - setContext(EMBLA_CAROUSEL_CONTEXT, config); - return config; -} - -export function getEmblaContext(name = 'This component') { - if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) { - throw new Error(`${name} must be used within a component`); - } - return getContext>(EMBLA_CAROUSEL_CONTEXT); -} diff --git a/frontend/src/lib/components/ui/carousel/index.ts b/frontend/src/lib/components/ui/carousel/index.ts deleted file mode 100644 index 0ae50aa8..00000000 --- a/frontend/src/lib/components/ui/carousel/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default as Root } from './carousel.svelte'; -export { default as Content } from './carousel-content.svelte'; -export { default as Item } from './carousel-item.svelte'; -export { default as Previous } from './carousel-previous.svelte'; -export { default as Next } from './carousel-next.svelte'; diff --git a/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/frontend/src/lib/components/ui/checkbox/checkbox.svelte deleted file mode 100644 index ad672cd2..00000000 --- a/frontend/src/lib/components/ui/checkbox/checkbox.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - {#if isChecked} - - {:else if isIndeterminate} - - {/if} - - diff --git a/frontend/src/lib/components/ui/checkbox/index.ts b/frontend/src/lib/components/ui/checkbox/index.ts deleted file mode 100644 index 5c276719..00000000 --- a/frontend/src/lib/components/ui/checkbox/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Root from './checkbox.svelte'; -export { - Root, - // - Root as Checkbox -}; diff --git a/frontend/src/lib/components/ui/command/command-dialog.svelte b/frontend/src/lib/components/ui/command/command-dialog.svelte deleted file mode 100644 index bc705219..00000000 --- a/frontend/src/lib/components/ui/command/command-dialog.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - diff --git a/frontend/src/lib/components/ui/command/command-empty.svelte b/frontend/src/lib/components/ui/command/command-empty.svelte deleted file mode 100644 index 3c87766d..00000000 --- a/frontend/src/lib/components/ui/command/command-empty.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/command/command-group.svelte b/frontend/src/lib/components/ui/command/command-group.svelte deleted file mode 100644 index bc5c6910..00000000 --- a/frontend/src/lib/components/ui/command/command-group.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/command/command-input.svelte b/frontend/src/lib/components/ui/command/command-input.svelte deleted file mode 100644 index 41cec2f9..00000000 --- a/frontend/src/lib/components/ui/command/command-input.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    - - -
    diff --git a/frontend/src/lib/components/ui/command/command-item.svelte b/frontend/src/lib/components/ui/command/command-item.svelte deleted file mode 100644 index 5f691e62..00000000 --- a/frontend/src/lib/components/ui/command/command-item.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/command/command-list.svelte b/frontend/src/lib/components/ui/command/command-list.svelte deleted file mode 100644 index cbe76d23..00000000 --- a/frontend/src/lib/components/ui/command/command-list.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/command/command-separator.svelte b/frontend/src/lib/components/ui/command/command-separator.svelte deleted file mode 100644 index 93bb3d5a..00000000 --- a/frontend/src/lib/components/ui/command/command-separator.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/command/command-shortcut.svelte b/frontend/src/lib/components/ui/command/command-shortcut.svelte deleted file mode 100644 index 9987b93e..00000000 --- a/frontend/src/lib/components/ui/command/command-shortcut.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/command/command.svelte b/frontend/src/lib/components/ui/command/command.svelte deleted file mode 100644 index b8cba5df..00000000 --- a/frontend/src/lib/components/ui/command/command.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/command/index.ts b/frontend/src/lib/components/ui/command/index.ts deleted file mode 100644 index 03922d5d..00000000 --- a/frontend/src/lib/components/ui/command/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Command as CommandPrimitive } from 'cmdk-sv'; - -import Root from './command.svelte'; -import Dialog from './command-dialog.svelte'; -import Empty from './command-empty.svelte'; -import Group from './command-group.svelte'; -import Item from './command-item.svelte'; -import Input from './command-input.svelte'; -import List from './command-list.svelte'; -import Separator from './command-separator.svelte'; -import Shortcut from './command-shortcut.svelte'; - -const Loading = CommandPrimitive.Loading; - -export { - Root, - Dialog, - Empty, - Group, - Item, - Input, - List, - Separator, - Shortcut, - Loading, - // - Root as Command, - Dialog as CommandDialog, - Empty as CommandEmpty, - Group as CommandGroup, - Item as CommandItem, - Input as CommandInput, - List as CommandList, - Separator as CommandSeparator, - Shortcut as CommandShortcut, - Loading as CommandLoading -}; diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte deleted file mode 100644 index 491bb1d7..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-content.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - Close - - - diff --git a/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/frontend/src/lib/components/ui/dialog/dialog-description.svelte deleted file mode 100644 index e8d11b88..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-description.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte deleted file mode 100644 index 329304a8..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-footer.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/frontend/src/lib/components/ui/dialog/dialog-header.svelte deleted file mode 100644 index ac663bc3..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-header.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte deleted file mode 100644 index c490ab29..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dialog/dialog-portal.svelte b/frontend/src/lib/components/ui/dialog/dialog-portal.svelte deleted file mode 100644 index 00774dd4..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-portal.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/frontend/src/lib/components/ui/dialog/dialog-title.svelte deleted file mode 100644 index e111dd7a..00000000 --- a/frontend/src/lib/components/ui/dialog/dialog-title.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dialog/index.ts b/frontend/src/lib/components/ui/dialog/index.ts deleted file mode 100644 index 6cbc5a7f..00000000 --- a/frontend/src/lib/components/ui/dialog/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Dialog as DialogPrimitive } from 'bits-ui'; - -import Title from './dialog-title.svelte'; -import Portal from './dialog-portal.svelte'; -import Footer from './dialog-footer.svelte'; -import Header from './dialog-header.svelte'; -import Overlay from './dialog-overlay.svelte'; -import Content from './dialog-content.svelte'; -import Description from './dialog-description.svelte'; - -const Root = DialogPrimitive.Root; -const Trigger = DialogPrimitive.Trigger; -const Close = DialogPrimitive.Close; - -export { - Root, - Title, - Portal, - Footer, - Header, - Trigger, - Overlay, - Content, - Description, - Close, - // - Root as Dialog, - Title as DialogTitle, - Portal as DialogPortal, - Footer as DialogFooter, - Header as DialogHeader, - Trigger as DialogTrigger, - Overlay as DialogOverlay, - Content as DialogContent, - Description as DialogDescription, - Close as DialogClose -}; diff --git a/frontend/src/lib/components/ui/drawer/drawer-content.svelte b/frontend/src/lib/components/ui/drawer/drawer-content.svelte deleted file mode 100644 index e03e5bbd..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-content.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - -
    - -
    -
    diff --git a/frontend/src/lib/components/ui/drawer/drawer-description.svelte b/frontend/src/lib/components/ui/drawer/drawer-description.svelte deleted file mode 100644 index cc092ee9..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-description.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/drawer/drawer-footer.svelte b/frontend/src/lib/components/ui/drawer/drawer-footer.svelte deleted file mode 100644 index da35d000..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-footer.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/drawer/drawer-header.svelte b/frontend/src/lib/components/ui/drawer/drawer-header.svelte deleted file mode 100644 index 1c3aa361..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-header.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/drawer/drawer-nested.svelte b/frontend/src/lib/components/ui/drawer/drawer-nested.svelte deleted file mode 100644 index de5e96ac..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-nested.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte b/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte deleted file mode 100644 index c7221098..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/drawer/drawer-title.svelte b/frontend/src/lib/components/ui/drawer/drawer-title.svelte deleted file mode 100644 index 95299866..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer-title.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/drawer/drawer.svelte b/frontend/src/lib/components/ui/drawer/drawer.svelte deleted file mode 100644 index 2ac8ce5e..00000000 --- a/frontend/src/lib/components/ui/drawer/drawer.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/drawer/index.ts b/frontend/src/lib/components/ui/drawer/index.ts deleted file mode 100644 index dd39b2a5..00000000 --- a/frontend/src/lib/components/ui/drawer/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Drawer as DrawerPrimitive } from 'vaul-svelte'; - -import Root from './drawer.svelte'; -import Content from './drawer-content.svelte'; -import Description from './drawer-description.svelte'; -import Overlay from './drawer-overlay.svelte'; -import Footer from './drawer-footer.svelte'; -import Header from './drawer-header.svelte'; -import Title from './drawer-title.svelte'; -import NestedRoot from './drawer-nested.svelte'; - -const Trigger = DrawerPrimitive.Trigger; -const Portal = DrawerPrimitive.Portal; -const Close = DrawerPrimitive.Close; - -export { - Root, - NestedRoot, - Content, - Description, - Overlay, - Footer, - Header, - Title, - Trigger, - Portal, - Close, - - // - Root as Drawer, - NestedRoot as DrawerNestedRoot, - Content as DrawerContent, - Description as DrawerDescription, - Overlay as DrawerOverlay, - Footer as DrawerFooter, - Header as DrawerHeader, - Title as DrawerTitle, - Trigger as DrawerTrigger, - Portal as DrawerPortal, - Close as DrawerClose -}; diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte deleted file mode 100644 index 729503da..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte deleted file mode 100644 index f1054415..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte deleted file mode 100644 index 475a0779..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte deleted file mode 100644 index 670a6861..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte deleted file mode 100644 index dd67d440..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte deleted file mode 100644 index 58d8ab6e..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte deleted file mode 100644 index cc0197e2..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte deleted file mode 100644 index eeeb8f7e..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte deleted file mode 100644 index f7f05d84..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte deleted file mode 100644 index 2f459f86..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts deleted file mode 100644 index 52c1f3c3..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; -import Item from './dropdown-menu-item.svelte'; -import Label from './dropdown-menu-label.svelte'; -import Content from './dropdown-menu-content.svelte'; -import Shortcut from './dropdown-menu-shortcut.svelte'; -import RadioItem from './dropdown-menu-radio-item.svelte'; -import Separator from './dropdown-menu-separator.svelte'; -import RadioGroup from './dropdown-menu-radio-group.svelte'; -import SubContent from './dropdown-menu-sub-content.svelte'; -import SubTrigger from './dropdown-menu-sub-trigger.svelte'; -import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; - -const Sub = DropdownMenuPrimitive.Sub; -const Root = DropdownMenuPrimitive.Root; -const Trigger = DropdownMenuPrimitive.Trigger; -const Group = DropdownMenuPrimitive.Group; - -export { - Sub, - Root, - Item, - Label, - Group, - Trigger, - Content, - Shortcut, - Separator, - RadioItem, - SubContent, - SubTrigger, - RadioGroup, - CheckboxItem, - // - Root as DropdownMenu, - Sub as DropdownMenuSub, - Item as DropdownMenuItem, - Label as DropdownMenuLabel, - Group as DropdownMenuGroup, - Content as DropdownMenuContent, - Trigger as DropdownMenuTrigger, - Shortcut as DropdownMenuShortcut, - RadioItem as DropdownMenuRadioItem, - Separator as DropdownMenuSeparator, - RadioGroup as DropdownMenuRadioGroup, - SubContent as DropdownMenuSubContent, - SubTrigger as DropdownMenuSubTrigger, - CheckboxItem as DropdownMenuCheckboxItem -}; diff --git a/frontend/src/lib/components/ui/form/form-button.svelte b/frontend/src/lib/components/ui/form/form-button.svelte deleted file mode 100644 index 8a148843..00000000 --- a/frontend/src/lib/components/ui/form/form-button.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/form/form-description.svelte b/frontend/src/lib/components/ui/form/form-description.svelte deleted file mode 100644 index 7d3aa490..00000000 --- a/frontend/src/lib/components/ui/form/form-description.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/form/form-element-field.svelte b/frontend/src/lib/components/ui/form/form-element-field.svelte deleted file mode 100644 index a9ab42fd..00000000 --- a/frontend/src/lib/components/ui/form/form-element-field.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - -
    - -
    -
    diff --git a/frontend/src/lib/components/ui/form/form-field-errors.svelte b/frontend/src/lib/components/ui/form/form-field-errors.svelte deleted file mode 100644 index ae9212c8..00000000 --- a/frontend/src/lib/components/ui/form/form-field-errors.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - - {#each errors as error} -
    {error}
    - {/each} -
    -
    diff --git a/frontend/src/lib/components/ui/form/form-field.svelte b/frontend/src/lib/components/ui/form/form-field.svelte deleted file mode 100644 index a0d5e0bd..00000000 --- a/frontend/src/lib/components/ui/form/form-field.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - -
    - -
    -
    diff --git a/frontend/src/lib/components/ui/form/form-fieldset.svelte b/frontend/src/lib/components/ui/form/form-fieldset.svelte deleted file mode 100644 index ff51b1a8..00000000 --- a/frontend/src/lib/components/ui/form/form-fieldset.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/ui/form/form-label.svelte b/frontend/src/lib/components/ui/form/form-label.svelte deleted file mode 100644 index 977ae25a..00000000 --- a/frontend/src/lib/components/ui/form/form-label.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/form/form-legend.svelte b/frontend/src/lib/components/ui/form/form-legend.svelte deleted file mode 100644 index f8adaab5..00000000 --- a/frontend/src/lib/components/ui/form/form-legend.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/form/index.ts b/frontend/src/lib/components/ui/form/index.ts deleted file mode 100644 index 2d129aad..00000000 --- a/frontend/src/lib/components/ui/form/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as FormPrimitive from 'formsnap'; -import Description from './form-description.svelte'; -import Label from './form-label.svelte'; -import FieldErrors from './form-field-errors.svelte'; -import Field from './form-field.svelte'; -import Fieldset from './form-fieldset.svelte'; -import Legend from './form-legend.svelte'; -import ElementField from './form-element-field.svelte'; -import Button from './form-button.svelte'; - -const Control = FormPrimitive.Control; - -export { - Field, - Control, - Label, - Button, - FieldErrors, - Description, - Fieldset, - Legend, - ElementField, - // - Field as FormField, - Control as FormControl, - Description as FormDescription, - Label as FormLabel, - FieldErrors as FormFieldErrors, - Fieldset as FormFieldset, - Legend as FormLegend, - ElementField as FormElementField, - Button as FormButton -}; diff --git a/frontend/src/lib/components/ui/input/index.ts b/frontend/src/lib/components/ui/input/index.ts deleted file mode 100644 index 1c8bcb70..00000000 --- a/frontend/src/lib/components/ui/input/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Root from './input.svelte'; - -export type FormInputEvent = T & { - currentTarget: EventTarget & HTMLInputElement; -}; -export type InputEvents = { - blur: FormInputEvent; - change: FormInputEvent; - click: FormInputEvent; - focus: FormInputEvent; - focusin: FormInputEvent; - focusout: FormInputEvent; - keydown: FormInputEvent; - keypress: FormInputEvent; - keyup: FormInputEvent; - mouseover: FormInputEvent; - mouseenter: FormInputEvent; - mouseleave: FormInputEvent; - mousemove: FormInputEvent; - paste: FormInputEvent; - input: FormInputEvent; - wheel: FormInputEvent; -}; - -export { - Root, - // - Root as Input -}; diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte deleted file mode 100644 index 7e8f33c2..00000000 --- a/frontend/src/lib/components/ui/input/input.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/label/index.ts b/frontend/src/lib/components/ui/label/index.ts deleted file mode 100644 index 808d1415..00000000 --- a/frontend/src/lib/components/ui/label/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from './label.svelte'; - -export { - Root, - // - Root as Label -}; diff --git a/frontend/src/lib/components/ui/label/label.svelte b/frontend/src/lib/components/ui/label/label.svelte deleted file mode 100644 index ca4b30d5..00000000 --- a/frontend/src/lib/components/ui/label/label.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/pagination/index.ts b/frontend/src/lib/components/ui/pagination/index.ts deleted file mode 100644 index 2f5532a2..00000000 --- a/frontend/src/lib/components/ui/pagination/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Root from './pagination.svelte'; -import Content from './pagination-content.svelte'; -import Item from './pagination-item.svelte'; -import Link from './pagination-link.svelte'; -import PrevButton from './pagination-prev-button.svelte'; -import NextButton from './pagination-next-button.svelte'; -import Ellipsis from './pagination-ellipsis.svelte'; - -export { - Root, - Content, - Item, - Link, - PrevButton, - NextButton, - Ellipsis, - // - Root as Pagination, - Content as PaginationContent, - Item as PaginationItem, - Link as PaginationLink, - PrevButton as PaginationPrevButton, - NextButton as PaginationNextButton, - Ellipsis as PaginationEllipsis -}; diff --git a/frontend/src/lib/components/ui/pagination/pagination-content.svelte b/frontend/src/lib/components/ui/pagination/pagination-content.svelte deleted file mode 100644 index e49b2696..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination-content.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
      - -
    diff --git a/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte b/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte deleted file mode 100644 index 6a92b3b4..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - More pages - diff --git a/frontend/src/lib/components/ui/pagination/pagination-item.svelte b/frontend/src/lib/components/ui/pagination/pagination-item.svelte deleted file mode 100644 index 28432214..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination-item.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
  • - -
  • diff --git a/frontend/src/lib/components/ui/pagination/pagination-link.svelte b/frontend/src/lib/components/ui/pagination/pagination-link.svelte deleted file mode 100644 index 64b30713..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination-link.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - - {page.value} - diff --git a/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte b/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte deleted file mode 100644 index d7453950..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte b/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte deleted file mode 100644 index def814cd..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/pagination/pagination.svelte b/frontend/src/lib/components/ui/pagination/pagination.svelte deleted file mode 100644 index 9e999b62..00000000 --- a/frontend/src/lib/components/ui/pagination/pagination.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/progress/index.ts b/frontend/src/lib/components/ui/progress/index.ts deleted file mode 100644 index 97f57fcd..00000000 --- a/frontend/src/lib/components/ui/progress/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from './progress.svelte'; - -export { - Root, - // - Root as Progress -}; diff --git a/frontend/src/lib/components/ui/progress/progress.svelte b/frontend/src/lib/components/ui/progress/progress.svelte deleted file mode 100644 index 270a370f..00000000 --- a/frontend/src/lib/components/ui/progress/progress.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - -
    -
    diff --git a/frontend/src/lib/components/ui/radio-group/index.ts b/frontend/src/lib/components/ui/radio-group/index.ts deleted file mode 100644 index 7d71e248..00000000 --- a/frontend/src/lib/components/ui/radio-group/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'; - -import Root from './radio-group.svelte'; -import Item from './radio-group-item.svelte'; -const Input = RadioGroupPrimitive.Input; - -export { - Root, - Input, - Item, - // - Root as RadioGroup, - Input as RadioGroupInput, - Item as RadioGroupItem -}; diff --git a/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte b/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte deleted file mode 100644 index 795017cc..00000000 --- a/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - -
    - - - -
    -
    diff --git a/frontend/src/lib/components/ui/radio-group/radio-group.svelte b/frontend/src/lib/components/ui/radio-group/radio-group.svelte deleted file mode 100644 index 8ae8bf72..00000000 --- a/frontend/src/lib/components/ui/radio-group/radio-group.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts deleted file mode 100644 index 6b2ec200..00000000 --- a/frontend/src/lib/components/ui/select/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Select as SelectPrimitive } from 'bits-ui'; - -import Label from './select-label.svelte'; -import Item from './select-item.svelte'; -import Content from './select-content.svelte'; -import Trigger from './select-trigger.svelte'; -import Separator from './select-separator.svelte'; - -const Root = SelectPrimitive.Root; -const Group = SelectPrimitive.Group; -const Input = SelectPrimitive.Input; -const Value = SelectPrimitive.Value; - -export { - Root, - Group, - Input, - Label, - Item, - Value, - Content, - Trigger, - Separator, - // - Root as Select, - Group as SelectGroup, - Input as SelectInput, - Label as SelectLabel, - Item as SelectItem, - Value as SelectValue, - Content as SelectContent, - Trigger as SelectTrigger, - Separator as SelectSeparator -}; diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte deleted file mode 100644 index ff135c33..00000000 --- a/frontend/src/lib/components/ui/select/select-content.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - -
    - -
    -
    diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte deleted file mode 100644 index 42c9f8e8..00000000 --- a/frontend/src/lib/components/ui/select/select-item.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - {label || value} - - diff --git a/frontend/src/lib/components/ui/select/select-label.svelte b/frontend/src/lib/components/ui/select/select-label.svelte deleted file mode 100644 index 424bc646..00000000 --- a/frontend/src/lib/components/ui/select/select-label.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/select/select-separator.svelte b/frontend/src/lib/components/ui/select/select-separator.svelte deleted file mode 100644 index 64d1886f..00000000 --- a/frontend/src/lib/components/ui/select/select-separator.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/select/select-trigger.svelte b/frontend/src/lib/components/ui/select/select-trigger.svelte deleted file mode 100644 index a5ead7d5..00000000 --- a/frontend/src/lib/components/ui/select/select-trigger.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - -span]:line-clamp-1', - className - )} - {...$$restProps} - let:builder - on:click - on:keydown -> - -
    - -
    -
    diff --git a/frontend/src/lib/components/ui/separator/index.ts b/frontend/src/lib/components/ui/separator/index.ts deleted file mode 100644 index 768efac9..00000000 --- a/frontend/src/lib/components/ui/separator/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from './separator.svelte'; - -export { - Root, - // - Root as Separator -}; diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte deleted file mode 100644 index 3160e6a2..00000000 --- a/frontend/src/lib/components/ui/separator/separator.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/sheet/index.ts b/frontend/src/lib/components/ui/sheet/index.ts deleted file mode 100644 index 88f11805..00000000 --- a/frontend/src/lib/components/ui/sheet/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Dialog as SheetPrimitive } from 'bits-ui'; -import { type VariantProps, tv } from 'tailwind-variants'; - -import Portal from './sheet-portal.svelte'; -import Overlay from './sheet-overlay.svelte'; -import Content from './sheet-content.svelte'; -import Header from './sheet-header.svelte'; -import Footer from './sheet-footer.svelte'; -import Title from './sheet-title.svelte'; -import Description from './sheet-description.svelte'; - -const Root = SheetPrimitive.Root; -const Close = SheetPrimitive.Close; -const Trigger = SheetPrimitive.Trigger; - -export { - Root, - Close, - Trigger, - Portal, - Overlay, - Content, - Header, - Footer, - Title, - Description, - // - Root as Sheet, - Close as SheetClose, - Trigger as SheetTrigger, - Portal as SheetPortal, - Overlay as SheetOverlay, - Content as SheetContent, - Header as SheetHeader, - Footer as SheetFooter, - Title as SheetTitle, - Description as SheetDescription -}; - -export const sheetVariants = tv({ - base: 'fixed z-50 gap-4 bg-background p-6 shadow-lg', - variants: { - side: { - top: 'inset-x-0 top-0 border-b', - bottom: 'inset-x-0 bottom-0 border-t', - left: 'inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm', - right: 'inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm' - } - }, - defaultVariants: { - side: 'right' - } -}); - -export const sheetTransitions = { - top: { - in: { - y: '-100%', - duration: 500, - opacity: 1 - }, - out: { - y: '-100%', - duration: 300, - opacity: 1 - } - }, - bottom: { - in: { - y: '100%', - duration: 500, - opacity: 1 - }, - out: { - y: '100%', - duration: 300, - opacity: 1 - } - }, - left: { - in: { - x: '-100%', - duration: 500, - opacity: 1 - }, - out: { - x: '-100%', - duration: 300, - opacity: 1 - } - }, - right: { - in: { - x: '100%', - duration: 500, - opacity: 1 - }, - out: { - x: '100%', - duration: 300, - opacity: 1 - } - } -}; - -export type Side = VariantProps['side']; diff --git a/frontend/src/lib/components/ui/sheet/sheet-content.svelte b/frontend/src/lib/components/ui/sheet/sheet-content.svelte deleted file mode 100644 index dd6ba018..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-content.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - Close - - - diff --git a/frontend/src/lib/components/ui/sheet/sheet-description.svelte b/frontend/src/lib/components/ui/sheet/sheet-description.svelte deleted file mode 100644 index 44184db9..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-description.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/sheet/sheet-footer.svelte b/frontend/src/lib/components/ui/sheet/sheet-footer.svelte deleted file mode 100644 index 329304a8..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-footer.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/sheet/sheet-header.svelte b/frontend/src/lib/components/ui/sheet/sheet-header.svelte deleted file mode 100644 index 434c29c9..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-header.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    - -
    diff --git a/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte b/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte deleted file mode 100644 index d9e38593..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/sheet/sheet-portal.svelte b/frontend/src/lib/components/ui/sheet/sheet-portal.svelte deleted file mode 100644 index 7f62f709..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-portal.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/sheet/sheet-title.svelte b/frontend/src/lib/components/ui/sheet/sheet-title.svelte deleted file mode 100644 index e670e397..00000000 --- a/frontend/src/lib/components/ui/sheet/sheet-title.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/skeleton/index.ts b/frontend/src/lib/components/ui/skeleton/index.ts deleted file mode 100644 index 3120ce12..00000000 --- a/frontend/src/lib/components/ui/skeleton/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from './skeleton.svelte'; - -export { - Root, - // - Root as Skeleton -}; diff --git a/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/frontend/src/lib/components/ui/skeleton/skeleton.svelte deleted file mode 100644 index 387b378b..00000000 --- a/frontend/src/lib/components/ui/skeleton/skeleton.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -
    diff --git a/frontend/src/lib/components/ui/sonner/index.ts b/frontend/src/lib/components/ui/sonner/index.ts deleted file mode 100644 index fcaf06bf..00000000 --- a/frontend/src/lib/components/ui/sonner/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Toaster } from './sonner.svelte'; diff --git a/frontend/src/lib/components/ui/sonner/sonner.svelte b/frontend/src/lib/components/ui/sonner/sonner.svelte deleted file mode 100644 index f1d41c58..00000000 --- a/frontend/src/lib/components/ui/sonner/sonner.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/switch/index.ts b/frontend/src/lib/components/ui/switch/index.ts deleted file mode 100644 index 129f8f5c..00000000 --- a/frontend/src/lib/components/ui/switch/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from './switch.svelte'; - -export { - Root, - // - Root as Switch -}; diff --git a/frontend/src/lib/components/ui/switch/switch.svelte b/frontend/src/lib/components/ui/switch/switch.svelte deleted file mode 100644 index ae1be09c..00000000 --- a/frontend/src/lib/components/ui/switch/switch.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/index.ts b/frontend/src/lib/components/ui/table/index.ts deleted file mode 100644 index 99239aee..00000000 --- a/frontend/src/lib/components/ui/table/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Root from './table.svelte'; -import Body from './table-body.svelte'; -import Caption from './table-caption.svelte'; -import Cell from './table-cell.svelte'; -import Footer from './table-footer.svelte'; -import Head from './table-head.svelte'; -import Header from './table-header.svelte'; -import Row from './table-row.svelte'; - -export { - Root, - Body, - Caption, - Cell, - Footer, - Head, - Header, - Row, - // - Root as Table, - Body as TableBody, - Caption as TableCaption, - Cell as TableCell, - Footer as TableFooter, - Head as TableHead, - Header as TableHeader, - Row as TableRow -}; diff --git a/frontend/src/lib/components/ui/table/table-body.svelte b/frontend/src/lib/components/ui/table/table-body.svelte deleted file mode 100644 index 47bf50e8..00000000 --- a/frontend/src/lib/components/ui/table/table-body.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/table-caption.svelte b/frontend/src/lib/components/ui/table/table-caption.svelte deleted file mode 100644 index ea4c654b..00000000 --- a/frontend/src/lib/components/ui/table/table-caption.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/table-cell.svelte b/frontend/src/lib/components/ui/table/table-cell.svelte deleted file mode 100644 index 901963b6..00000000 --- a/frontend/src/lib/components/ui/table/table-cell.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/table-footer.svelte b/frontend/src/lib/components/ui/table/table-footer.svelte deleted file mode 100644 index ebac5ab1..00000000 --- a/frontend/src/lib/components/ui/table/table-footer.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/table-head.svelte b/frontend/src/lib/components/ui/table/table-head.svelte deleted file mode 100644 index 0f2acd8a..00000000 --- a/frontend/src/lib/components/ui/table/table-head.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/table-header.svelte b/frontend/src/lib/components/ui/table/table-header.svelte deleted file mode 100644 index 980b2a74..00000000 --- a/frontend/src/lib/components/ui/table/table-header.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/frontend/src/lib/components/ui/table/table-row.svelte b/frontend/src/lib/components/ui/table/table-row.svelte deleted file mode 100644 index 234f3257..00000000 --- a/frontend/src/lib/components/ui/table/table-row.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/table/table.svelte b/frontend/src/lib/components/ui/table/table.svelte deleted file mode 100644 index 7b4cee5e..00000000 --- a/frontend/src/lib/components/ui/table/table.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -
    - - -
    -
    diff --git a/frontend/src/lib/components/ui/tabs/index.ts b/frontend/src/lib/components/ui/tabs/index.ts deleted file mode 100644 index 00ecd770..00000000 --- a/frontend/src/lib/components/ui/tabs/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Tabs as TabsPrimitive } from 'bits-ui'; -import Content from './tabs-content.svelte'; -import List from './tabs-list.svelte'; -import Trigger from './tabs-trigger.svelte'; - -const Root = TabsPrimitive.Root; - -export { - Root, - Content, - List, - Trigger, - // - Root as Tabs, - Content as TabsContent, - List as TabsList, - Trigger as TabsTrigger -}; diff --git a/frontend/src/lib/components/ui/tabs/tabs-content.svelte b/frontend/src/lib/components/ui/tabs/tabs-content.svelte deleted file mode 100644 index 6d4c10b0..00000000 --- a/frontend/src/lib/components/ui/tabs/tabs-content.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/tabs/tabs-list.svelte b/frontend/src/lib/components/ui/tabs/tabs-list.svelte deleted file mode 100644 index 37729a6d..00000000 --- a/frontend/src/lib/components/ui/tabs/tabs-list.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte deleted file mode 100644 index 9fe6d52b..00000000 --- a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/textarea/index.ts b/frontend/src/lib/components/ui/textarea/index.ts deleted file mode 100644 index eac4cc5e..00000000 --- a/frontend/src/lib/components/ui/textarea/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Root from './textarea.svelte'; - -type FormTextareaEvent = T & { - currentTarget: EventTarget & HTMLTextAreaElement; -}; - -type TextareaEvents = { - blur: FormTextareaEvent; - change: FormTextareaEvent; - click: FormTextareaEvent; - focus: FormTextareaEvent; - keydown: FormTextareaEvent; - keypress: FormTextareaEvent; - keyup: FormTextareaEvent; - mouseover: FormTextareaEvent; - mouseenter: FormTextareaEvent; - mouseleave: FormTextareaEvent; - paste: FormTextareaEvent; - input: FormTextareaEvent; -}; - -export { - Root, - // - Root as Textarea, - type TextareaEvents, - type FormTextareaEvent -}; diff --git a/frontend/src/lib/components/ui/textarea/textarea.svelte b/frontend/src/lib/components/ui/textarea/textarea.svelte deleted file mode 100644 index 85b4194e..00000000 --- a/frontend/src/lib/components/ui/textarea/textarea.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/tooltip/index.ts b/frontend/src/lib/components/ui/tooltip/index.ts deleted file mode 100644 index 243cd027..00000000 --- a/frontend/src/lib/components/ui/tooltip/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Tooltip as TooltipPrimitive } from 'bits-ui'; -import Content from './tooltip-content.svelte'; - -const Root = TooltipPrimitive.Root; -const Trigger = TooltipPrimitive.Trigger; - -export { - Root, - Trigger, - Content, - // - Root as Tooltip, - Content as TooltipContent, - Trigger as TooltipTrigger -}; diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte deleted file mode 100644 index 905013fe..00000000 --- a/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts deleted file mode 100644 index 27abe01b..00000000 --- a/frontend/src/lib/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const TMDB_IMAGES_ORIGINAL = 'https://www.themoviedb.org/t/p/original'; -export const TMDB_BACKDROP_SMALL = 'https://www.themoviedb.org/t/p/w1280'; -export const TMDB_POSTER_SMALL = 'https://www.themoviedb.org/t/p/w342'; -export const TMDB_PROFILE_SMALL = 'https://www.themoviedb.org/t/p/w185'; -export const TMDB_PROFILE_LARGE = 'https://www.themoviedb.org/t/p/h632'; diff --git a/frontend/src/lib/forms/components/array-field.svelte b/frontend/src/lib/forms/components/array-field.svelte deleted file mode 100644 index e73deea1..00000000 --- a/frontend/src/lib/forms/components/array-field.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - - - - -
    -
    - {legend} - {#if fieldDescription} -

    {fieldDescription}

    - {/if} -
    - -
    - -
    - - -
    -
    diff --git a/frontend/src/lib/forms/components/checkbox-field.svelte b/frontend/src/lib/forms/components/checkbox-field.svelte deleted file mode 100644 index 586088e1..00000000 --- a/frontend/src/lib/forms/components/checkbox-field.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - - - -{#if isForGroup} - - -
    - {label} - -
    - -
    -
    -{:else} - - -
    -
    - {label} - {#if fieldDescription} -

    {fieldDescription}

    - {/if} -
    - -
    - -
    - - -
    -{/if} diff --git a/frontend/src/lib/forms/components/group-checkbox-field.svelte b/frontend/src/lib/forms/components/group-checkbox-field.svelte deleted file mode 100644 index 27f12c8f..00000000 --- a/frontend/src/lib/forms/components/group-checkbox-field.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
    -
    -

    {fieldTitle}

    - {#if fieldDescription} -

    {fieldDescription}

    - {/if} -
    - -
    - -
    -
    diff --git a/frontend/src/lib/forms/components/number-field.svelte b/frontend/src/lib/forms/components/number-field.svelte deleted file mode 100644 index 67694b36..00000000 --- a/frontend/src/lib/forms/components/number-field.svelte +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - -
    -
    - {label} - {#if fieldDescription} -

    {fieldDescription}

    - {/if} -
    - -
    -
    - - -
    diff --git a/frontend/src/lib/forms/components/text-field.svelte b/frontend/src/lib/forms/components/text-field.svelte deleted file mode 100644 index 7334fb78..00000000 --- a/frontend/src/lib/forms/components/text-field.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - -
    -
    - {label} - {#if fieldDescription} -

    {fieldDescription}

    - {/if} -
    - {#if isProtected} - 0 - })} - type="text" - spellcheck="false" - autocomplete="false" - bind:value={$formData[name]} - /> - {:else} - - {/if} -
    -
    - - -
    diff --git a/frontend/src/lib/forms/content-form.svelte b/frontend/src/lib/forms/content-form.svelte deleted file mode 100644 index 25e24080..00000000 --- a/frontend/src/lib/forms/content-form.svelte +++ /dev/null @@ -1,487 +0,0 @@ - - -
    - - - - - - - - - - - {#if $formData.overseerr_enabled} -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.mdblist_enabled} -
    - -
    - -
    - - {#each $formData.mdblist_lists as _, i} - - -
    - - -
    - { - removeField('mdblist_lists', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add MDB Lists

    - { - addField('mdblist_lists'); - }} - > - - -
    -
    -
    - -
    - -
    - {/if} - - {#if $formData.plex_watchlist_enabled} -
    - - {#each $formData.plex_watchlist_rss as _, i} - - -
    - - -
    - { - removeField('plex_watchlist_rss', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add Plex Watchlist RSS

    - { - addField('plex_watchlist_rss'); - }} - > - - -
    -
    -
    - -
    - -
    - {/if} - - {#if $formData.listrr_enabled} -
    - -
    - -
    - -
    - -
    - - {#each $formData.listrr_movie_lists as _, i} - - -
    - - -
    - { - removeField('listrr_movie_lists', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add Listrr movie lists

    - { - addField('listrr_movie_lists'); - }} - > - - -
    -
    -
    - -
    - - {#each $formData.listrr_show_lists as _, i} - - -
    - - -
    - { - removeField('listrr_show_lists', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add Listrr shows lists

    - { - addField('listrr_show_lists'); - }} - > - - -
    -
    -
    - {/if} - - {#if $formData.trakt_enabled} -
    - -
    - -
    - -
    - -
    - - {#each $formData.trakt_watchlist as _, i} - - -
    - - -
    - { - removeField('trakt_watchlist', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add Trakt watchlist

    - { - addField('trakt_watchlist'); - }} - > - - -
    -
    -
    - -
    - - {#each $formData.trakt_user_lists as _, i} - - -
    - - -
    - { - removeField('trakt_user_lists', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add Trakt user watchlists

    - { - addField('trakt_user_lists'); - }} - > - - -
    -
    -
    - -
    - - {#each $formData.trakt_collection as _, i} - - -
    - - -
    - { - removeField('trakt_collection', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add Trakt collections

    - { - addField('trakt_collection'); - }} - > - - -
    -
    -
    - -
    - -
    - - {#if $formData.trakt_fetch_trending} -
    - -
    - {/if} - -
    - -
    - - {#if $formData.trakt_fetch_popular} -
    - -
    - {/if} - {/if} - - -
    - - {#if $delayed} - - {/if} - Save changes - and continue - -
    - - -{#if formDebug} - -{/if} diff --git a/frontend/src/lib/forms/general-form.svelte b/frontend/src/lib/forms/general-form.svelte deleted file mode 100644 index 329d3c7c..00000000 --- a/frontend/src/lib/forms/general-form.svelte +++ /dev/null @@ -1,140 +0,0 @@ - - -
    - - - - - - - - - - - - - - - - - - {#if $formData.realdebrid_enabled} -
    - -
    - -
    - -
    - - {#if $formData.realdebrid_proxy_enabled} -
    - -
    - {/if} - {/if} - - {#if $formData.torbox_enabled} -
    - -
    - {/if} - - -
    - - {#if $delayed} - - {/if} - Save changes - and continue - -
    - - -{#if formDebug} - -{/if} diff --git a/frontend/src/lib/forms/helpers.server.ts b/frontend/src/lib/forms/helpers.server.ts deleted file mode 100644 index fe6b04d1..00000000 --- a/frontend/src/lib/forms/helpers.server.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -// TODO: Add toCheck -export async function setSettings(fetch: any, toSet: any) { - const settings = await fetch(`${BACKEND_URL}/settings/set`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(toSet) - }); - const settingsData = await settings.json(); - - return { - data: settingsData - }; -} - -export async function saveSettings(fetch: any) { - const data = await fetch(`${BACKEND_URL}/settings/save`, { - method: 'POST' - }); - const response = await data.json(); - - return { - data: response - }; -} - -export async function loadSettings(fetch: any) { - const data = await fetch(`${BACKEND_URL}/settings/load`, { - method: 'GET' - }); - const response = await data.json(); - - return { - data: response - }; -} diff --git a/frontend/src/lib/forms/helpers.ts b/frontend/src/lib/forms/helpers.ts deleted file mode 100644 index 93493f4c..00000000 --- a/frontend/src/lib/forms/helpers.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { type SuperValidated, type Infer } from 'sveltekit-superforms'; -import { z } from 'zod'; - -// General Settings ----------------------------------------------------------------------------------- -export const generalSettingsToGet: string[] = ['debug', 'log', 'symlink', 'downloaders']; - -export const generalSettingsSchema = z.object({ - debug: z.boolean().default(true), - log: z.boolean().default(true), - rclone_path: z.string().min(1), - library_path: z.string().min(1), - separate_anime_dirs: z.boolean().default(false), - movie_filesize_min: z.coerce.number().gte(0).int().optional().default(200), - movie_filesize_max: z.coerce.number().gte(-1).int().optional().default(-1), - episode_filesize_min: z.coerce.number().gte(0).int().optional().default(40), - episode_filesize_max: z.coerce.number().gte(-1).int().optional().default(-1), - realdebrid_enabled: z.boolean().default(false), - realdebrid_api_key: z.string().optional().default(''), - realdebrid_proxy_enabled: z.boolean().default(false), - realdebrid_proxy_url: z.string().optional().default(''), - torbox_enabled: z.boolean().default(false), - torbox_api_key: z.string().optional().default('') -}); -export type GeneralSettingsSchema = typeof generalSettingsSchema; - -export function generalSettingsToPass(data: any) { - return { - debug: data.data.debug, - log: data.data.log, - rclone_path: data.data.symlink.rclone_path, - library_path: data.data.symlink.library_path, - separate_anime_dirs: data.data.symlink.separate_anime_dirs, - movie_filesize_min: data.data.downloaders.movie_filesize_min, - movie_filesize_max: data.data.downloaders.movie_filesize_max, - episode_filesize_min: data.data.downloaders.episode_filesize_min, - episode_filesize_max: data.data.downloaders.episode_filesize_max, - realdebrid_enabled: data.data.downloaders.real_debrid.enabled, - realdebrid_api_key: data.data.downloaders.real_debrid?.api_key || '', - realdebrid_proxy_enabled: data.data.downloaders.real_debrid?.proxy_enabled || false, - realdebrid_proxy_url: data.data.downloaders.real_debrid?.proxy_url || '', - torbox_enabled: data.data.downloaders.torbox.enabled, - torbox_api_key: data.data.downloaders.torbox?.api_key || '' - }; -} - -export function generalSettingsToSet(form: SuperValidated>) { - return [ - { - key: 'debug', - value: form.data.debug - }, - { - key: 'log', - value: form.data.log - }, - { - key: 'symlink', - value: { - rclone_path: form.data.rclone_path, - library_path: form.data.library_path, - separate_anime_dirs: form.data.separate_anime_dirs - } - }, - { - key: 'downloaders', - value: { - movie_filesize_min: form.data.movie_filesize_min, - movie_filesize_max: form.data.movie_filesize_max, - episode_filesize_min: form.data.episode_filesize_min, - episode_filesize_max: form.data.episode_filesize_max, - real_debrid: { - enabled: form.data.realdebrid_enabled, - api_key: form.data.realdebrid_api_key, - proxy_enabled: form.data.realdebrid_proxy_enabled, - proxy_url: form.data.realdebrid_proxy_url - }, - torbox: { - enabled: form.data.torbox_enabled, - api_key: form.data.torbox_api_key - } - } - } - ]; -} - -// Media Server Settings ----------------------------------------------------------------------------------- - -export const mediaServerSettingsToGet: string[] = ['updaters']; - -export const mediaServerSettingsSchema = z.object({ - // update_interval: z.number().nonnegative().int().optional().default(120), // Moved to coerce due to https://github.com/huntabyte/shadcn-svelte/issues/574 - update_interval: z.coerce.number().gte(0).int().optional().default(120), - local_enabled: z.boolean().default(false), - plex_enabled: z.boolean().default(false), - plex_token: z.string().optional().default(''), - plex_url: z.string().optional().default('') -}); -export type MediaServerSettingsSchema = typeof mediaServerSettingsSchema; - -export function mediaServerSettingsToPass(data: any) { - return { - update_interval: data.data.updaters.update_interval, - plex_token: data.data.updaters.plex.token, - plex_url: data.data.updaters.plex.url, - plex_enabled: data.data.updaters.plex.enabled, - local_enabled: data.data.updaters.local.enabled - }; -} - -export function mediaServerSettingsToSet(form: SuperValidated>) { - return [ - { - key: 'updaters', - value: { - update_interval: form.data.update_interval, - local: { - enabled: form.data.local_enabled - }, - plex: { - enabled: form.data.plex_enabled, - token: form.data.plex_token, - url: form.data.plex_url - } - } - } - ]; -} - -// Scrapers Settings ----------------------------------------------------------------------------------- - -export const scrapersSettingsToGet: string[] = ['scraping']; - -export const scrapersSettingsSchema = z.object({ - after_2: z.coerce.number().gte(0).int().default(0.5), - after_5: z.coerce.number().gte(0).int().default(2), - after_10: z.coerce.number().gte(0).int().default(24), - torrentio_enabled: z.boolean().default(false), - torrentio_url: z.string().optional().default('https://torrentio.strem.fun'), - torrentio_timeout: z.coerce.number().gte(0).int().optional().default(30), - torrentio_ratelimit: z.boolean().default(true), - torrentio_filter: z - .string() - .optional() - .default('sort=qualitysize%7Cqualityfilter=480p,scr,cam,unknown'), - knightcrawler_enabled: z.boolean().default(false), - knightcrawler_url: z.string().optional().default('https://knightcrawler.elfhosted.com/'), - knightcrawler_timeout: z.coerce.number().gte(0).int().optional().default(30), - knightcrawler_ratelimit: z.boolean().default(true), - knightcrawler_filter: z - .string() - .optional() - .default('sort=qualitysize%7Cqualityfilter=480p,scr,cam,unknown'), - annatar_enabled: z.boolean().default(false), - annatar_url: z.string().optional().default('https://annatar.elfhosted.com'), - annatar_timeout: z.coerce.number().gte(0).int().optional().default(10), - annatar_ratelimit: z.boolean().default(true), - annatar_limit: z.coerce.number().gte(0).int().optional().default(2000), - orionoid_enabled: z.boolean().default(false), - orionoid_api_key: z.string().optional().default(''), - orionoid_timeout: z.coerce.number().gte(0).int().optional().default(10), - orionoid_ratelimit: z.boolean().default(true), - orionoid_limitcount: z.coerce.number().gte(0).int().optional().default(5), - jackett_enabled: z.boolean().default(false), - jackett_url: z.string().optional().default('http://localhost:9117'), - jackett_timeout: z.coerce.number().gte(0).int().optional().default(10), - jackett_ratelimit: z.boolean().default(true), - jackett_api_key: z.string().optional().default(''), - mediafusion_enabled: z.boolean().default(false), - mediafusion_url: z.string().optional().default('https://mediafusion.elfhosted.com'), - mediafusion_timeout: z.coerce.number().gte(0).int().optional().default(10), - mediafusion_ratelimit: z.boolean().default(true), - mediafusion_catalogs: z.array(z.string()).optional().default([]), - prowlarr_enabled: z.boolean().default(false), - prowlarr_url: z.string().optional().default('http://localhost:9696'), - prowlarr_timeout: z.coerce.number().gte(0).int().optional().default(10), - prowlarr_ratelimit: z.boolean().default(true), - prowlarr_limiter_seconds: z.coerce.number().gte(0).int().optional().default(60), - prowlarr_api_key: z.string().optional().default(''), - torbox_scraper_enabled: z.boolean().default(false), - torbox_scraper_timeout: z.coerce.number().gte(0).int().optional().default(30), - torbox_scraper_ratelimit: z.boolean().default(true), - zilean_enabled: z.boolean().default(false), - zilean_url: z.string().optional().default('http://localhost:8181'), - zilean_timeout: z.coerce.number().gte(0).int().optional().default(30), - zilean_ratelimit: z.boolean().default(true) -}); -export type ScrapersSettingsSchema = typeof scrapersSettingsSchema; - -export function scrapersSettingsToPass(data: any) { - return { - after_2: data.data.scraping.after_2, - after_5: data.data.scraping.after_5, - after_10: data.data.scraping.after_10, - torrentio_url: data.data.scraping.torrentio?.url || 'https://torrentio.strem.fun', - torrentio_enabled: data.data.scraping.torrentio.enabled, - torrentio_filter: data.data.scraping.torrentio?.filter || '', - torrentio_timeout: data.data.scraping.torrentio?.timeout || 30, - torrentio_ratelimit: data.data.scraping.torrentio?.ratelimit || true, - knightcrawler_url: - data.data.scraping.knightcrawler?.url || 'https://knightcrawler.elfhosted.com/', - knightcrawler_enabled: data.data.scraping.knightcrawler.enabled, - knightcrawler_filter: data.data.scraping.knightcrawler?.filter || '', - knightcrawler_timeout: data.data.scraping.knightcrawler?.timeout || 30, - knightcrawler_ratelimit: data.data.scraping.knightcrawler?.ratelimit || true, - annatar_url: data.data.scraping.annatar?.url || 'https://annatar.elfhosted.com', - annatar_enabled: data.data.scraping.annatar.enabled, - annatar_limit: data.data.scraping.annatar?.limit || 2000, - annatar_timeout: data.data.scraping.annatar?.timeout || 10, - annatar_ratelimit: data.data.scraping.annatar?.ratelimit || true, - orionoid_enabled: data.data.scraping.orionoid.enabled, - orionoid_api_key: data.data.scraping.orionoid?.api_key || '', - orionoid_limitcount: data.data.scraping.orionoid?.limitcount || 5, - orionoid_timeout: data.data.scraping.orionoid?.timeout || 10, - orionoid_ratelimit: data.data.scraping.orionoid?.ratelimit || true, - jackett_enabled: data.data.scraping.jackett.enabled, - jackett_url: data.data.scraping.jackett?.url || '', - jackett_api_key: data.data.scraping.jackett?.api_key || '', - jackett_timeout: data.data.scraping.jackett?.timeout || 10, - jackett_ratelimit: data.data.scraping.jackett?.ratelimit || true, - mediafusion_url: data.data.scraping.mediafusion?.url || 'https://mediafusion.elfhosted.com/', - mediafusion_enabled: data.data.scraping.mediafusion.enabled, - mediafusion_catalogs: data.data.scraping.mediafusion.catalogs || [ - 'prowlarr_streams', - 'torrentio_streams' - ], - mediafusion_timeout: data.data.scraping.mediafusion?.timeout || 10, - mediafusion_ratelimit: data.data.scraping.mediafusion?.ratelimit || true, - prowlarr_enabled: data.data.scraping.prowlarr?.enabled || false, - prowlarr_url: data.data.scraping.prowlarr?.url || 'http://localhost:9696', - prowlarr_api_key: data.data.scraping.prowlarr?.api_key || '', - prowlarr_timeout: data.data.scraping.prowlarr?.timeout || 10, - prowlarr_ratelimit: data.data.scraping.prowlarr?.ratelimit || true, - prowlarr_limiter_seconds: data.data.scraping.prowlarr?.limiter_seconds || 60, - torbox_scraper_enabled: data.data.scraping.torbox_scraper?.enabled || false, - torbox_scraper_timeout: data.data.scraping.torbox_scraper?.timeout || 30, - torbox_scraper_ratelimit: data.data.scraping.torbox_scraper?.ratelimit || true, - zilean_enabled: data.data.scraping.zilean?.enabled || false, - zilean_url: data.data.scraping.zilean?.url || 'http://localhost:8181', - zilean_timeout: data.data.scraping.zilean?.timeout || 30, - zilean_ratelimit: data.data.scraping.zilean?.ratelimit || true - }; -} - -export function scrapersSettingsToSet(form: SuperValidated>) { - return [ - { - key: 'scraping', - value: { - after_2: form.data.after_2, - after_5: form.data.after_5, - after_10: form.data.after_10, - torrentio: { - enabled: form.data.torrentio_enabled, - url: form.data.torrentio_url, - filter: form.data.torrentio_filter, - timeout: form.data.torrentio_timeout, - ratelimit: form.data.torrentio_ratelimit - }, - knightcrawler: { - enabled: form.data.knightcrawler_enabled, - url: form.data.knightcrawler_url, - filter: form.data.knightcrawler_filter, - timeout: form.data.knightcrawler_timeout, - ratelimit: form.data.knightcrawler_ratelimit - }, - annatar: { - enabled: form.data.annatar_enabled, - url: form.data.annatar_url, - limit: form.data.annatar_limit, - timeout: form.data.annatar_timeout, - ratelimit: form.data.annatar_ratelimit - }, - orionoid: { - enabled: form.data.orionoid_enabled, - api_key: form.data.orionoid_api_key, - limitcount: form.data.orionoid_limitcount, - timeout: form.data.orionoid_timeout, - ratelimit: form.data.orionoid_ratelimit - }, - jackett: { - enabled: form.data.jackett_enabled, - url: form.data.jackett_url, - api_key: form.data.jackett_api_key, - timeout: form.data.jackett_timeout, - ratelimit: form.data.jackett_ratelimit - }, - mediafusion: { - enabled: form.data.mediafusion_enabled, - url: form.data.mediafusion_url, - catalogs: form.data.mediafusion_catalogs, - timeout: form.data.mediafusion_timeout, - ratelimit: form.data.mediafusion_ratelimit - }, - prowlarr: { - enabled: form.data.prowlarr_enabled, - url: form.data.prowlarr_url, - api_key: form.data.prowlarr_api_key, - timeout: form.data.prowlarr_timeout, - ratelimit: form.data.prowlarr_ratelimit, - limiter_seconds: form.data.prowlarr_limiter_seconds - }, - torbox_scraper: { - enabled: form.data.torbox_scraper_enabled, - timeout: form.data.torbox_scraper_timeout, - ratelimit: form.data.torbox_scraper_ratelimit - }, - zilean: { - enabled: form.data.zilean_enabled, - url: form.data.zilean_url, - timeout: form.data.zilean_timeout, - ratelimit: form.data.zilean_ratelimit - } - } - } - ]; -} - -// Content Settings ----------------------------------------------------------------------------------- - -export const contentSettingsToGet: string[] = ['content']; - -export const contentSettingsSchema = z.object({ - overseerr_enabled: z.boolean().default(false), - overseerr_url: z.string().optional().default(''), - overseerr_api_key: z.string().optional().default(''), - overseerr_update_interval: z.coerce.number().gte(0).int().optional().default(30), - overseerr_use_webhook: z.boolean().optional().default(false), - mdblist_enabled: z.boolean().default(false), - mdblist_api_key: z.string().optional().default(''), - mdblist_update_interval: z.coerce.number().gte(0).int().optional().default(300), - mdblist_lists: z.string().array().optional().default([]), - plex_watchlist_enabled: z.boolean().default(false), - plex_watchlist_rss: z.array(z.string()).optional().default([]), - plex_watchlist_update_interval: z.coerce.number().gte(0).int().optional().default(60), - listrr_enabled: z.boolean().default(false), - listrr_api_key: z.string().optional().default(''), - listrr_update_interval: z.coerce.number().gte(0).int().optional().default(300), - listrr_movie_lists: z.string().array().optional().default([]), - listrr_show_lists: z.string().array().optional().default([]), - trakt_enabled: z.boolean().default(false), - trakt_api_key: z.string().optional().default(''), - trakt_update_interval: z.coerce.number().gte(0).int().optional().default(300), - trakt_watchlist: z.array(z.string()).optional().default([]), - trakt_user_lists: z.array(z.string()).optional().default([]), - trakt_collection: z.array(z.string()).optional().default([]), - trakt_fetch_trending: z.boolean().default(false), - trakt_fetch_popular: z.boolean().default(false), - trakt_trending_count: z.coerce.number().gte(0).int().optional().default(10), - trakt_popular_count: z.coerce.number().gte(0).int().optional().default(10) -}); -export type ContentSettingsSchema = typeof contentSettingsSchema; - -export function contentSettingsToPass(data: any) { - return { - overseerr_enabled: data.data.content.overseerr.enabled, - overseerr_url: data.data.content.overseerr?.url || '', - overseerr_api_key: data.data.content.overseerr?.api_key || '', - overseerr_update_interval: data.data.content.overseerr?.update_interval || 30, - overseerr_use_webhook: data.data.content.overseerr?.use_webhook || false, - mdblist_enabled: data.data.content.mdblist.enabled, - mdblist_api_key: data.data.content.mdblist?.api_key || '', - mdblist_update_interval: data.data.content.mdblist?.update_interval || 300, - mdblist_lists: data.data.content.mdblist?.lists || [], - plex_watchlist_enabled: data.data.content.plex_watchlist.enabled, - plex_watchlist_rss: data.data.content.plex_watchlist?.rss || [], - plex_watchlist_update_interval: data.data.content.plex_watchlist?.update_interval || 60, - listrr_enabled: data.data.content.listrr.enabled, - listrr_api_key: data.data.content.listrr?.api_key || '', - listrr_update_interval: data.data.content.listrr?.update_interval || 300, - listrr_movie_lists: data.data.content.listrr?.movie_lists || [], - listrr_show_lists: data.data.content.listrr?.show_lists || [], - trakt_enabled: data.data.content.trakt.enabled, - trakt_api_key: data.data.content.trakt?.api_key || '', - trakt_update_interval: data.data.content.trakt?.update_interval || 300, - trakt_watchlist: data.data.content.trakt?.watchlist || [], - trakt_user_lists: data.data.content.trakt?.user_lists || [], - trakt_collection: data.data.content.trakt?.collection || [], - trakt_fetch_trending: data.data.content.trakt?.fetch_trending || false, - trakt_fetch_popular: data.data.content.trakt?.fetch_popular || false, - trakt_trending_count: data.data.content.trakt?.fetch_trending_count || 10, - trakt_popular_count: data.data.content.trakt?.fetch_popular_count || 10 - }; -} - -export function contentSettingsToSet(form: SuperValidated>) { - return [ - { - key: 'content', - value: { - overseerr: { - enabled: form.data.overseerr_enabled, - url: form.data.overseerr_url, - api_key: form.data.overseerr_api_key, - update_interval: form.data.overseerr_update_interval, - use_webhook: form.data.overseerr_use_webhook - }, - mdblist: { - enabled: form.data.mdblist_enabled, - api_key: form.data.mdblist_api_key, - update_interval: form.data.mdblist_update_interval, - lists: form.data.mdblist_lists - }, - plex_watchlist: { - enabled: form.data.plex_watchlist_enabled, - rss: form.data.plex_watchlist_rss, - update_interval: form.data.plex_watchlist_update_interval - }, - listrr: { - enabled: form.data.listrr_enabled, - api_key: form.data.listrr_api_key, - update_interval: form.data.listrr_update_interval, - movie_lists: form.data.listrr_movie_lists, - show_lists: form.data.listrr_show_lists - }, - trakt: { - enabled: form.data.trakt_enabled, - api_key: form.data.trakt_api_key, - update_interval: form.data.trakt_update_interval, - watchlist: form.data.trakt_watchlist, - user_lists: form.data.trakt_user_lists, - collection: form.data.trakt_collection, - fetch_trending: form.data.trakt_fetch_trending, - fetch_popular: form.data.trakt_fetch_popular, - trending_count: form.data.trakt_trending_count, - popular_count: form.data.trakt_popular_count - } - } - } - ]; -} diff --git a/frontend/src/lib/forms/media-server-form.svelte b/frontend/src/lib/forms/media-server-form.svelte deleted file mode 100644 index 4e15dc45..00000000 --- a/frontend/src/lib/forms/media-server-form.svelte +++ /dev/null @@ -1,182 +0,0 @@ - - -
    - - - - - - - - {#if $formData.plex_enabled} -
    - -
    - -
    - - -
    -
    - Plex Token -

    - Click the button to generate a new Plex token -

    -
    - - -
    -
    -
    -
    - {/if} - - -
    - - {#if $delayed} - - {/if} - Save changes - and continue - -
    - - -{#if formDebug} - -{/if} diff --git a/frontend/src/lib/forms/scrapers-form.svelte b/frontend/src/lib/forms/scrapers-form.svelte deleted file mode 100644 index e2de473e..00000000 --- a/frontend/src/lib/forms/scrapers-form.svelte +++ /dev/null @@ -1,386 +0,0 @@ - - -
    - - - - - - - - - - - - - - - - - {#if $formData.torrentio_enabled} -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.knightcrawler_enabled} -
    - -
    -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.annatar_enabled} -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.orionoid_enabled} -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.jackett_enabled} -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.mediafusion_enabled} -
    - -
    - -
    - - {#each $formData.mediafusion_catalogs as _, i} - - -
    - - -
    - { - removeField('mediafusion_catalogs', i); - }} - > - - -
    -
    -
    -
    - {/each} - -
    -

    Add catalogs

    - { - addField('mediafusion_catalogs'); - }} - > - - -
    -
    -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.prowlarr_enabled} -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - {#if $formData.torbox_scraper_enabled} -
    - -
    - -
    - -
    - {/if} - - {#if $formData.zilean_enabled} -
    - -
    - -
    - -
    - -
    - -
    - {/if} - - -
    - - {#if $delayed} - - {/if} - Save changes - and continue - -
    - - -{#if formDebug} - -{/if} diff --git a/frontend/src/lib/helpers.ts b/frontend/src/lib/helpers.ts deleted file mode 100644 index 87e0937f..00000000 --- a/frontend/src/lib/helpers.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { DateTime } from 'luxon'; -import type { RivenItem } from '$lib/types'; - -// only works with real-debrid dates because of CET format provided by RD -export function formatRDDate(inputDate: string, format: string = 'long'): string { - let cetDate = DateTime.fromISO(inputDate, { zone: 'Europe/Paris' }); - cetDate = cetDate.setZone('utc'); - - const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - cetDate = cetDate.setZone(userTimeZone); - - let formattedDate; - if (format === 'short') { - formattedDate = cetDate.toLocaleString({ - year: 'numeric', - month: 'short', - day: 'numeric' - }); - } else if (format === 'left') { - const now = DateTime.now(); - const diff = cetDate.diff(now, 'days').toObject(); - const days = Math.round(diff.days ?? 0); - if (days > 0) { - formattedDate = `${days} days left`; - } else if (days < 0) { - formattedDate = `${Math.abs(days)} days ago`; - } else { - formattedDate = 'Today'; - } - } else { - formattedDate = cetDate.toLocaleString(DateTime.DATETIME_FULL); - } - - return formattedDate; -} - -export function formatDate( - inputDate: string, - format: string = 'long', - relative: boolean = false -): string { - let date = DateTime.fromISO(inputDate); - date = date.setZone('local'); - - let formattedDate; - - if (relative) { - formattedDate = date.toRelative() || ''; - } else { - if (format === 'short') { - formattedDate = date.toLocaleString({ - year: 'numeric', - month: 'short', - day: 'numeric' - }); - } else if (format === 'year') { - formattedDate = date.toLocaleString({ - year: 'numeric' - }); - } else if (format === 'left') { - const now = DateTime.now(); - const diff = date.diff(now, 'days').toObject(); - const days = Math.round(diff.days ?? 0); - if (days > 0) { - formattedDate = `${days} days left`; - } else if (days < 0) { - formattedDate = `${Math.abs(days)} days ago`; - } else { - formattedDate = 'Today'; - } - } else { - formattedDate = date.toLocaleString(DateTime.DATETIME_FULL); - } - } - - return formattedDate; -} - -export function formatWords(words: string) { - return words - .split('_') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' '); -} - -export function roundOff(num: number, decimalPlaces: number = 1) { - return Math.round(num * 10 ** decimalPlaces) / 10 ** decimalPlaces; -} - -export function convertIcebergItemsToObject(items: RivenItem[]) { - const result: { [key: string]: RivenItem[] } = {}; - - for (const item of items) { - if (!result[item.state]) { - result[item.state] = []; - } - result[item.state].push(item); - } - - return result; -} - -export function createQueryString(params: Record): string { - const queryString = Object.entries(params) - .filter(([_, value]) => value !== null && value !== undefined && value.toString().trim() !== '') - .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) - .join('&'); - return `?${queryString}`; -} diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6c..00000000 --- a/frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/tmdb.ts b/frontend/src/lib/tmdb.ts deleted file mode 100644 index 7e425a83..00000000 --- a/frontend/src/lib/tmdb.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Todo - -class TmdbApi { - private apiKey: string; - private baseUrl: string; - - constructor(apiKey: string) { - this.apiKey = apiKey; - this.baseUrl = 'https://api.themoviedb.org/3'; - } - - async getPopularMovies() { - const response = await fetch(`${this.baseUrl}/movie/popular?api_key=${this.apiKey}`); - const data = await response.json(); - return data.results; - } - - async getMovieDetails(id: number) { - const response = await fetch(`${this.baseUrl}/movie/${id}?api_key=${this.apiKey}`); - return response.json(); - } -} - -export default TmdbApi; diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts deleted file mode 100644 index 567e3e75..00000000 --- a/frontend/src/lib/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -export interface NavItem { - name: string; - path: string; -} - -export interface RDData { - id: number; - username: string; - email: string; - points: number; - locale: string; - avatar: string; - type: string; - premium: number; - expiration: string; -} - -export interface RDUserResponse { - success: boolean; - data: RDData; - downloader: string; -} -interface TorboxData { - id: number; - created_at: string; - updated_at: string; - email: string; - plan: number; - total_downloaded: number; - customer: string; - server: number; - is_subscribed: boolean; - premium_expires_at: string; - cooldown_until: string; - auth_id: string; - user_referral: string; - base_email: string; -} - -export interface TorboxUserResponse { - success: boolean; - detail: string; - data: TorboxData; - downloader: string; -} - -export interface RivenItem { - item_id: number; - title: string; - type: string; - imdb_id: string | null; - tvdb_id: number | null; - tmdb_id: number | null; - state: string; - imdb_link: string; - aired_at: string; - genres: string[]; - is_anime: boolean; - guid: string | null; - requested_at: string; - requested_by: string; - scraped_at: string | null; - scraped_times: number | null; -} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts deleted file mode 100644 index eba19d86..00000000 --- a/frontend/src/lib/utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; -import { cubicOut } from 'svelte/easing'; -import type { TransitionConfig } from 'svelte/transition'; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -type FlyAndScaleParams = { - y?: number; - x?: number; - start?: number; - duration?: number; -}; - -export const flyAndScale = ( - node: Element, - params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } -): TransitionConfig => { - const style = getComputedStyle(node); - const transform = style.transform === 'none' ? '' : style.transform; - - const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => { - const [minA, maxA] = scaleA; - const [minB, maxB] = scaleB; - - const percentage = (valueA - minA) / (maxA - minA); - const valueB = percentage * (maxB - minB) + minB; - - return valueB; - }; - - const styleToString = (style: Record): string => { - return Object.keys(style).reduce((str, key) => { - if (style[key] === undefined) return str; - return str + `${key}:${style[key]};`; - }, ''); - }; - - return { - duration: params.duration ?? 200, - delay: 0, - css: (t) => { - const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); - const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); - const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); - - return styleToString({ - transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, - opacity: t - }); - }, - easing: cubicOut - }; -}; diff --git a/frontend/src/nprogress.css b/frontend/src/nprogress.css deleted file mode 100644 index 5e41a27b..00000000 --- a/frontend/src/nprogress.css +++ /dev/null @@ -1,83 +0,0 @@ -/* Make clicks pass-through */ -#nprogress { - pointer-events: none; -} - -#nprogress .bar { - background: red; - - position: fixed; - z-index: 1031; - top: 0; - left: 0; - - width: 100%; - height: 3px; -} - -/* Fancy blur effect */ -#nprogress .peg { - display: block; - position: absolute; - right: 0px; - width: 100px; - height: 100%; - box-shadow: - 0 0 10px red, - 0 0 5px red; - opacity: 1; - - -webkit-transform: rotate(3deg) translate(0px, -4px); - -ms-transform: rotate(3deg) translate(0px, -4px); - transform: rotate(3deg) translate(0px, -4px); -} - -/* Remove these to get rid of the spinner */ -#nprogress .spinner { - display: block; - position: fixed; - z-index: 1031; - top: 15px; - right: 15px; -} - -#nprogress .spinner-icon { - width: 18px; - height: 18px; - box-sizing: border-box; - - border: solid 2px transparent; - border-top-color: red; - border-left-color: red; - border-radius: 50%; - - -webkit-animation: nprogress-spinner 400ms linear infinite; - animation: nprogress-spinner 400ms linear infinite; -} - -.nprogress-custom-parent { - overflow: hidden; - position: relative; -} - -.nprogress-custom-parent #nprogress .spinner, -.nprogress-custom-parent #nprogress .bar { - position: absolute; -} - -@-webkit-keyframes nprogress-spinner { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@keyframes nprogress-spinner { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte deleted file mode 100644 index 08e3c312..00000000 --- a/frontend/src/routes/+error.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -
    -

    Something went wrong

    -

    Error code: {$page.status}

    -

    Error message: {$page.error?.message}

    -
    diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte deleted file mode 100644 index 2a6ea28a..00000000 --- a/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - -
    - - - -
    diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts deleted file mode 100644 index 70ccc675..00000000 --- a/frontend/src/routes/+page.server.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { error } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getNowPlaying() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/movie/now_playing`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch now playing data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch now playing data. Server error or API is down.'); - } - } - - async function getTrendingAll() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/trending/all/day`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch trending data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch trending data. Server error or API is down.'); - } - } - - async function getTrendingMoviesWeek() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/trending/movie/week`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch trending movies data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch trending movies data. Server error or API is down.'); - } - } - - async function getTrendingShowsWeek() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/trending/tv/week`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch trending shows data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch trending shows data. Server error or API is down.'); - } - } - - async function getMoviesPopular() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/movie/popular`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch popular movies data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch popular movies data. Server error or API is down.'); - } - } - - async function getMoviesTopRated() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/movie/top_rated`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch top rated movies data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch top rated movies data. Server error or API is down.'); - } - } - - async function getShowsPopular() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/tv/popular`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch popular shows data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch popular shows data. Server error or API is down.'); - } - } - - async function getShowsTopRated() { - try { - const res = await fetch(`${BACKEND_URL}/tmdb/tv/top_rated`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch top rated shows data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch top rated shows data. Server error or API is down.'); - } - } - - return { - nowPlaying: await getNowPlaying(), - trendingAll: await getTrendingAll(), - trendingMovies: await getTrendingMoviesWeek(), - trendingShows: await getTrendingShowsWeek(), - moviesPopular: await getMoviesPopular(), - moviesTopRated: await getMoviesTopRated(), - showsPopular: await getShowsPopular(), - showsTopRated: await getShowsTopRated() - }; -}; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte deleted file mode 100644 index c532af6e..00000000 --- a/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,295 +0,0 @@ - - - - Riven | Home - - - -
    - - {#each data.nowPlaying.data.results as nowPlaying, i} - {#if i <= 9} - -
    - {nowPlaying.title} -
    -
    -
    -
    -

    - {nowPlaying.title} -

    -
    -
    -
    - -

    {roundOff(nowPlaying.vote_average)}

    -
    -
    - -

    {nowPlaying.release_date}

    -
    -
    - -

    {nowPlaying.original_language}

    -
    -
    -
    -

    {nowPlaying.overview}

    -
    -
    - - -
    -
    -
    -
    -
    - {/if} - {/each} -
    - - -
    -
    -
    - -
    -

    What's Trending Today

    -
    - - - {#each data.trendingAll.data.results as trendingAll, i} - {#if trendingAll.media_type !== 'person'} - {@const mediaType = trendingAll.media_type} - - - - {/if} - {/each} - - -
    - - - - - -
    -
    -
    -
    - -
    -

    Top Rated

    -
    - { - curTopRatedType = String(selected?.value); - }} - > - - - - - Movies - Shows - - -
    - -
    - - - {@const topRatedData = - curTopRatedType === 'movie' ? data.moviesTopRated : data.showsTopRated} - {#each topRatedData.data.results as showsTopRated, i} - -
    - {showsTopRated.title -
    -
    -

    - {showsTopRated.title || showsTopRated.name} -

    -
    - - -

    {roundOff(showsTopRated.vote_average)}

    -
    - tv - - {showsTopRated.release_date || showsTopRated.first_air_date} - - - {showsTopRated.original_language} - -
    -
    - {showsTopRated.overview} -
    -
    - - -
    -
    -
    - {showsTopRated.title -
    -
    - {/each} -
    -
    -
    -
    diff --git a/frontend/src/routes/[type]/[id]/+page.server.ts b/frontend/src/routes/[type]/[id]/+page.server.ts deleted file mode 100644 index 335e7bc7..00000000 --- a/frontend/src/routes/[type]/[id]/+page.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PageServerLoad } from './$types'; - -export const load = (async () => { - return {}; -}) satisfies PageServerLoad; diff --git a/frontend/src/routes/[type]/[id]/+page.svelte b/frontend/src/routes/[type]/[id]/+page.svelte deleted file mode 100644 index abe9cc81..00000000 --- a/frontend/src/routes/[type]/[id]/+page.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -
    - -

    Soon...

    diff --git a/frontend/src/routes/library/+page.server.ts b/frontend/src/routes/library/+page.server.ts deleted file mode 100644 index 69c85e96..00000000 --- a/frontend/src/routes/library/+page.server.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { error } from '@sveltejs/kit'; -import { createQueryString } from '$lib/helpers'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load = (async ({ fetch, url }) => { - const params = { - limit: Number(url.searchParams.get('limit')) || 100, - page: Number(url.searchParams.get('page')) || 1, - type: url.searchParams.get('type') || 'Movie', - search: url.searchParams.get('search') || '', - state: url.searchParams.get('state') || '' - }; - - const queryString = createQueryString(params); - - async function getItems() { - try { - const res = await fetch(`${BACKEND_URL}/items${queryString}`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch items data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch items data. Server error or API is down.'); - } - } - - return { - items: await getItems() - }; -}) satisfies PageServerLoad; diff --git a/frontend/src/routes/library/+page.svelte b/frontend/src/routes/library/+page.svelte deleted file mode 100644 index 0e860a4b..00000000 --- a/frontend/src/routes/library/+page.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
    - -

    Soon™

    diff --git a/frontend/src/routes/onboarding/+layout.svelte b/frontend/src/routes/onboarding/+layout.svelte deleted file mode 100644 index 17562056..00000000 --- a/frontend/src/routes/onboarding/+layout.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - Onboarding | Riven - - - diff --git a/frontend/src/routes/onboarding/+page.svelte b/frontend/src/routes/onboarding/+page.svelte deleted file mode 100644 index dae2a6d1..00000000 --- a/frontend/src/routes/onboarding/+page.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - -
    -
    -
    - -
    - - - -

    Welcome to Riven!

    -

    - Before you can start using Riven, you need to configure some services first. -

    - - -
    -
    diff --git a/frontend/src/routes/onboarding/1/+page.server.ts b/frontend/src/routes/onboarding/1/+page.server.ts deleted file mode 100644 index a15d4d98..00000000 --- a/frontend/src/routes/onboarding/1/+page.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { superValidate } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { error } from '@sveltejs/kit'; -import { - generalSettingsSchema, - generalSettingsToGet, - generalSettingsToPass -} from '$lib/forms/helpers'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch(`${BACKEND_URL}/settings/get/${generalSettingsToGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = generalSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(generalSettingsSchema)) - }; -}; diff --git a/frontend/src/routes/onboarding/1/+page.svelte b/frontend/src/routes/onboarding/1/+page.svelte deleted file mode 100644 index efab430c..00000000 --- a/frontend/src/routes/onboarding/1/+page.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -
    - -

    Step 1/4

    -

    Let's get started by configuring your general settings.

    -
    -
    - - -
    -
    diff --git a/frontend/src/routes/onboarding/2/+page.server.ts b/frontend/src/routes/onboarding/2/+page.server.ts deleted file mode 100644 index 1bab133a..00000000 --- a/frontend/src/routes/onboarding/2/+page.server.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { superValidate } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { error } from '@sveltejs/kit'; -import { - mediaServerSettingsSchema, - mediaServerSettingsToGet, - mediaServerSettingsToPass -} from '$lib/forms/helpers'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch( - `${BACKEND_URL}/settings/get/${mediaServerSettingsToGet.join(',')}` - ); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = mediaServerSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(mediaServerSettingsSchema)) - }; -}; diff --git a/frontend/src/routes/onboarding/2/+page.svelte b/frontend/src/routes/onboarding/2/+page.svelte deleted file mode 100644 index 5cb5ad8f..00000000 --- a/frontend/src/routes/onboarding/2/+page.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -
    - -

    Step 2/4

    -

    Time to configure your media server.

    -
    -
    - - -
    -
    diff --git a/frontend/src/routes/onboarding/3/+page.server.ts b/frontend/src/routes/onboarding/3/+page.server.ts deleted file mode 100644 index f0936e53..00000000 --- a/frontend/src/routes/onboarding/3/+page.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { superValidate } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { error } from '@sveltejs/kit'; -import { - contentSettingsSchema, - contentSettingsToGet, - contentSettingsToPass -} from '$lib/forms/helpers'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch(`${BACKEND_URL}/settings/get/${contentSettingsToGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = contentSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(contentSettingsSchema)) - }; -}; diff --git a/frontend/src/routes/onboarding/3/+page.svelte b/frontend/src/routes/onboarding/3/+page.svelte deleted file mode 100644 index 23017ec1..00000000 --- a/frontend/src/routes/onboarding/3/+page.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -
    - -

    Step 3/4

    -

    Services to request content from.

    -
    -
    - - -
    -
    diff --git a/frontend/src/routes/onboarding/4/+page.server.ts b/frontend/src/routes/onboarding/4/+page.server.ts deleted file mode 100644 index a27668e4..00000000 --- a/frontend/src/routes/onboarding/4/+page.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { superValidate } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { error } from '@sveltejs/kit'; -import { - scrapersSettingsSchema, - scrapersSettingsToGet, - scrapersSettingsToPass -} from '$lib/forms/helpers'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch(`${BACKEND_URL}/settings/get/${scrapersSettingsToGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = scrapersSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(scrapersSettingsSchema)) - }; -}; diff --git a/frontend/src/routes/onboarding/4/+page.svelte b/frontend/src/routes/onboarding/4/+page.svelte deleted file mode 100644 index 6cf9dbbb..00000000 --- a/frontend/src/routes/onboarding/4/+page.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -
    - -

    Step 4/4

    -

    Configure where to scrape content from.

    -
    -
    - - -
    -
    diff --git a/frontend/src/routes/settings/+layout.svelte b/frontend/src/routes/settings/+layout.svelte deleted file mode 100644 index 98d2b0d9..00000000 --- a/frontend/src/routes/settings/+layout.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - - - Settings | General - - -
    - -
    - { - goto(String(selected?.value)); - }} - selected={{ - value: $page.url.pathname, - label: - (settingsItems.find((item) => item.path === $page.url.pathname) || {}).name || 'Not found' - }} - > - - - - - {#each settingsItems as item} - {item.name} - {/each} - - - - - - - - -
    diff --git a/frontend/src/routes/settings/+page.ts b/frontend/src/routes/settings/+page.ts deleted file mode 100644 index 26d43df7..00000000 --- a/frontend/src/routes/settings/+page.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { PageLoad } from './$types'; -import { redirect } from '@sveltejs/kit'; - -export const load: PageLoad = async ({}) => { - redirect(302, '/settings/general'); -}; diff --git a/frontend/src/routes/settings/about/+page.server.ts b/frontend/src/routes/settings/about/+page.server.ts deleted file mode 100644 index ef2ec792..00000000 --- a/frontend/src/routes/settings/about/+page.server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { error } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getAboutInfo() { - try { - const toGet = ['version', 'symlink']; - const results = await fetch(`${BACKEND_URL}/settings/get/${toGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - return { settings: await getAboutInfo() }; -}; diff --git a/frontend/src/routes/settings/about/+page.svelte b/frontend/src/routes/settings/about/+page.svelte deleted file mode 100644 index 8b4ecde7..00000000 --- a/frontend/src/routes/settings/about/+page.svelte +++ /dev/null @@ -1,141 +0,0 @@ - - - - Settings | About - - -
    -

    About

    -

    - Know what you're running and how to get help. -

    -
    -
    -

    Version

    -
    -

    - {version} -

    - -
    -
    - {#each Object.keys(aboutData) as key} - -
    -

    - {formatWords(key)} -

    -
    -

    - {aboutData[key]} -

    -
    -
    - {/each} -
    - -

    Support

    -

    - Need help? Reach out to the Riven community or report an issue on GitHub. -

    -
    - {#each Object.keys(supportData) as key} - -
    -

    - {formatWords(key)} -

    - -
    - {/each} -
    - -

    Contributors

    -

    - Thanks to the following people for their contributions to Riven -

    - contributors -
    diff --git a/frontend/src/routes/settings/content/+page.server.ts b/frontend/src/routes/settings/content/+page.server.ts deleted file mode 100644 index 509c93cf..00000000 --- a/frontend/src/routes/settings/content/+page.server.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { PageServerLoad, Actions } from './$types'; -import { superValidate, message } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { fail, error, redirect } from '@sveltejs/kit'; -import { - contentSettingsSchema, - contentSettingsToGet, - contentSettingsToPass, - contentSettingsToSet -} from '$lib/forms/helpers'; -import { setSettings, saveSettings, loadSettings } from '$lib/forms/helpers.server'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch(`${BACKEND_URL}/settings/get/${contentSettingsToGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = contentSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(contentSettingsSchema)) - }; -}; - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(contentSettingsSchema)); - - if (!form.valid) { - console.log('form not valid'); - return fail(400, { - form - }); - } - const toSet = contentSettingsToSet(form); - - try { - const data = await setSettings(event.fetch, toSet); - console.log(data); - if (!data.data.success) { - return message(form, `Service(s) failed to initialize. Please check your settings.`, { - status: 400 - }); - } - const save = await saveSettings(event.fetch); - const load = await loadSettings(event.fetch); - } catch (e) { - console.error(e); - return message(form, 'Unable to save settings. API is down.', { - status: 400 - }); - } - - if (event.url.searchParams.get('onboarding') === 'true') { - redirect(302, '/onboarding/4'); - } - - return message(form, 'Settings saved!'); - } -}; diff --git a/frontend/src/routes/settings/content/+page.svelte b/frontend/src/routes/settings/content/+page.svelte deleted file mode 100644 index 5480a219..00000000 --- a/frontend/src/routes/settings/content/+page.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    -

    Content Settings

    -

    Configure content providers for Riven.

    - - -
    diff --git a/frontend/src/routes/settings/general/+page.server.ts b/frontend/src/routes/settings/general/+page.server.ts deleted file mode 100644 index 7ad6a314..00000000 --- a/frontend/src/routes/settings/general/+page.server.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { PageServerLoad, Actions } from './$types'; -import { superValidate, message } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { fail, error, redirect } from '@sveltejs/kit'; -import { - generalSettingsSchema, - generalSettingsToGet, - generalSettingsToPass, - generalSettingsToSet -} from '$lib/forms/helpers'; -import { setSettings, saveSettings, loadSettings } from '$lib/forms/helpers.server'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch(`${BACKEND_URL}/settings/get/${generalSettingsToGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = generalSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(generalSettingsSchema)) - }; -}; - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(generalSettingsSchema)); - - if (!form.valid) { - console.log('form not valid'); - return fail(400, { - form - }); - } - const toSet = generalSettingsToSet(form); - - try { - const data = await setSettings(event.fetch, toSet); - if (!data.data.success) { - return message(form, `Service(s) failed to initialize. Please check your settings.`, { - status: 400 - }); - } - const save = await saveSettings(event.fetch); - const load = await loadSettings(event.fetch); - } catch (e) { - console.error(e); - return message(form, 'Unable to save settings. API is down.', { - status: 400 - }); - } - - if (event.url.searchParams.get('onboarding') === 'true') { - redirect(302, '/onboarding/2'); - } - - return message(form, 'Settings saved!'); - } -}; diff --git a/frontend/src/routes/settings/general/+page.svelte b/frontend/src/routes/settings/general/+page.svelte deleted file mode 100644 index 050ff502..00000000 --- a/frontend/src/routes/settings/general/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -
    -

    General Settings

    -

    - Configure global and default settings for Riven. -

    - - -
    diff --git a/frontend/src/routes/settings/mediaserver/+page.server.ts b/frontend/src/routes/settings/mediaserver/+page.server.ts deleted file mode 100644 index 73a012e4..00000000 --- a/frontend/src/routes/settings/mediaserver/+page.server.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { PageServerLoad, Actions } from './$types'; -import { superValidate, message } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { fail, error, redirect } from '@sveltejs/kit'; -import { - mediaServerSettingsSchema, - mediaServerSettingsToGet, - mediaServerSettingsToPass, - mediaServerSettingsToSet -} from '$lib/forms/helpers'; -import { setSettings, saveSettings, loadSettings } from '$lib/forms/helpers.server'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch( - `${BACKEND_URL}/settings/get/${mediaServerSettingsToGet.join(',')}` - ); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = mediaServerSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(mediaServerSettingsSchema)) - }; -}; - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(mediaServerSettingsSchema)); - - if (!form.valid) { - console.log('form not valid'); - return fail(400, { - form - }); - } - const toSet = mediaServerSettingsToSet(form); - - try { - const data = await setSettings(event.fetch, toSet); - if (!data.data.success) { - return message(form, `Service(s) failed to initialize. Please check your settings.`, { - status: 400 - }); - } - const save = await saveSettings(event.fetch); - const load = await loadSettings(event.fetch); - } catch (e) { - console.error(e); - return message(form, 'Unable to save settings. API is down.', { - status: 400 - }); - } - - if (event.url.searchParams.get('onboarding') === 'true') { - redirect(302, '/onboarding/3'); - } - - return message(form, 'Settings saved!'); - } -}; diff --git a/frontend/src/routes/settings/mediaserver/+page.svelte b/frontend/src/routes/settings/mediaserver/+page.svelte deleted file mode 100644 index 12f44c21..00000000 --- a/frontend/src/routes/settings/mediaserver/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -
    -

    Media Server Settings

    -

    - Configure media server settings for Riven. -

    - - -
    diff --git a/frontend/src/routes/settings/scrapers/+page.server.ts b/frontend/src/routes/settings/scrapers/+page.server.ts deleted file mode 100644 index c9b7f6d3..00000000 --- a/frontend/src/routes/settings/scrapers/+page.server.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { PageServerLoad, Actions } from './$types'; -import { superValidate, message } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { fail, error, redirect } from '@sveltejs/kit'; -import { - scrapersSettingsSchema, - scrapersSettingsToGet, - scrapersSettingsToPass, - scrapersSettingsToSet -} from '$lib/forms/helpers'; -import { setSettings, saveSettings, loadSettings } from '$lib/forms/helpers.server'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - - -export const load: PageServerLoad = async ({ fetch }) => { - async function getPartialSettings() { - try { - const results = await fetch(`${BACKEND_URL}/settings/get/${scrapersSettingsToGet.join(',')}`); - return await results.json(); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch settings data. API is down.'); - } - } - - let data: any = await getPartialSettings(); - let toPassToSchema = scrapersSettingsToPass(data); - - return { - form: await superValidate(toPassToSchema, zod(scrapersSettingsSchema)) - }; -}; - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(scrapersSettingsSchema)); - - if (!form.valid) { - console.log('form not valid'); - return fail(400, { - form - }); - } - const toSet = scrapersSettingsToSet(form); - - try { - const data = await setSettings(event.fetch, toSet); - if (!data.data.success) { - return message(form, `Service(s) failed to initialize. Please check your settings.`, { - status: 400 - }); - } - const save = await saveSettings(event.fetch); - const load = await loadSettings(event.fetch); - } catch (e) { - console.error(e); - return message(form, 'Unable to save settings. API is down.', { - status: 400 - }); - } - - if (event.url.searchParams.get('onboarding') === 'true') { - redirect(302, '/?onboarding=true'); - } - - return message(form, 'Settings saved!'); - } -}; diff --git a/frontend/src/routes/settings/scrapers/+page.svelte b/frontend/src/routes/settings/scrapers/+page.svelte deleted file mode 100644 index 6d418ff2..00000000 --- a/frontend/src/routes/settings/scrapers/+page.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
    -

    Scraper Settings

    -

    Configure scraper settings for Riven

    - - -
    diff --git a/frontend/src/routes/summary/+page.server.ts b/frontend/src/routes/summary/+page.server.ts deleted file mode 100644 index 202bc491..00000000 --- a/frontend/src/routes/summary/+page.server.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { error } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; -const BACKEND_URL = env.BACKEND_URL || 'http://127.0.0.1:8080'; - -export const load = (async () => { - async function getStats() { - try { - const res = await fetch(`${BACKEND_URL}/stats`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch stats data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch stats data. Server error or API is down.'); - } - } - - async function getIncompleteItems() { - try { - const res = await fetch(`${BACKEND_URL}/items/incomplete`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch incomplete items data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch incomplete items data. Server error or API is down.'); - } - } - - async function getServices() { - try { - const res = await fetch(`${BACKEND_URL}/services`); - if (res.ok) { - return await res.json(); - } - error(400, `Unable to fetch services data: ${res.status} ${res.statusText}`); - } catch (e) { - console.error(e); - error(503, 'Unable to fetch services data. Server error or API is down.'); - } - } - - return { - stats: await getStats(), - incompleteItems: await getIncompleteItems(), - services: await getServices() - }; -}) satisfies PageServerLoad; diff --git a/frontend/src/routes/summary/+page.svelte b/frontend/src/routes/summary/+page.svelte deleted file mode 100644 index 9070a224..00000000 --- a/frontend/src/routes/summary/+page.svelte +++ /dev/null @@ -1,340 +0,0 @@ - - -
    - -
    -

    Statistics

    -

    Statistics of the library

    -
    - {#each statsData as stat} - - - {stat.title} - - - {#if stat.title === 'Total Shows'} -

    {stat.value}

    -

    - {data.stats.data.total_seasons} Seasons -

    -

    - {data.stats.data.total_episodes} Episodes -

    - {:else} -

    {stat.value}

    - {/if} -
    -
    - {/each} -
    - -

    Services

    -

    Tells the current status of the services

    -
    - {#each servicesStatus as service} - - - {service.name} - - - {#each service.services as status} -
    - -

    {status.name}

    -
    - {/each} -
    -
    - {/each} -
    - -

    States

    -

    - Tells the current state of the items in the library -

    - - - Know more about states - - - - What are these states? - -

    - Riven has items, which are movies/shows/season/episode. These items go through different - states. -

    -

    - States represent how the items are processed in the library. Each state represents a - different stage of the item in the library. Items start Requested and end up in - Completed state. Sometimes due to ongoing series, no streams or some error, they can end - up in Incomplete or Failed state. Rarely items end up in Unknown state. -

    -
    -
    -
    -
    -
    - {#each Object.keys(data.stats.data.states) as state} - - - {statesName[state]} - - -

    {data.stats.data.states[state]}

    -
    -
    - {/each} -
    - -
    -

    Incomplete Items

    -

    Items whose state is not Completed

    -
    - {#if totalIncompleteItems} -

    - Showing {start + 1} to {end > totalIncompleteItems ? totalIncompleteItems : end} of{' '} - {totalIncompleteItems} items -

    - {:else} -

    No incomplete items found

    - {/if} -
    -
    - {#each incompleteItems as item, id (item.imdb_id)} - - - {/each} -
    - {#if totalIncompleteItems} - (curPage = page)} - > - - - - - {#each pages as page (page.key)} - {#if page.type === 'ellipsis'} - - - - {:else} - - - {page.value} - - - {/if} - {/each} - - - - - - {/if} -
    -
    diff --git a/frontend/static/android-chrome-192x192.png b/frontend/static/android-chrome-192x192.png deleted file mode 100644 index 47daf2d1..00000000 Binary files a/frontend/static/android-chrome-192x192.png and /dev/null differ diff --git a/frontend/static/android-chrome-512x512.png b/frontend/static/android-chrome-512x512.png deleted file mode 100644 index f1c4d0c1..00000000 Binary files a/frontend/static/android-chrome-512x512.png and /dev/null differ diff --git a/frontend/static/apple-touch-icon.png b/frontend/static/apple-touch-icon.png deleted file mode 100644 index 05d3d7b6..00000000 Binary files a/frontend/static/apple-touch-icon.png and /dev/null differ diff --git a/frontend/static/favicon-16x16.png b/frontend/static/favicon-16x16.png deleted file mode 100644 index ed65810f..00000000 Binary files a/frontend/static/favicon-16x16.png and /dev/null differ diff --git a/frontend/static/favicon-32x32.png b/frontend/static/favicon-32x32.png deleted file mode 100644 index 6286c92f..00000000 Binary files a/frontend/static/favicon-32x32.png and /dev/null differ diff --git a/frontend/static/favicon.ico b/frontend/static/favicon.ico deleted file mode 100644 index dfdd6b2a..00000000 Binary files a/frontend/static/favicon.ico and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Black.woff b/frontend/static/fonts/Satoshi-Black.woff deleted file mode 100644 index 466e622b..00000000 Binary files a/frontend/static/fonts/Satoshi-Black.woff and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Black.woff2 b/frontend/static/fonts/Satoshi-Black.woff2 deleted file mode 100644 index 69d40fe5..00000000 Binary files a/frontend/static/fonts/Satoshi-Black.woff2 and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Bold.woff b/frontend/static/fonts/Satoshi-Bold.woff deleted file mode 100644 index 82222ae4..00000000 Binary files a/frontend/static/fonts/Satoshi-Bold.woff and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Bold.woff2 b/frontend/static/fonts/Satoshi-Bold.woff2 deleted file mode 100644 index 332e2426..00000000 Binary files a/frontend/static/fonts/Satoshi-Bold.woff2 and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Light.woff b/frontend/static/fonts/Satoshi-Light.woff deleted file mode 100644 index 16e6e85a..00000000 Binary files a/frontend/static/fonts/Satoshi-Light.woff and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Light.woff2 b/frontend/static/fonts/Satoshi-Light.woff2 deleted file mode 100644 index f16557bb..00000000 Binary files a/frontend/static/fonts/Satoshi-Light.woff2 and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Medium.woff b/frontend/static/fonts/Satoshi-Medium.woff deleted file mode 100644 index 7b6c089f..00000000 Binary files a/frontend/static/fonts/Satoshi-Medium.woff and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Medium.woff2 b/frontend/static/fonts/Satoshi-Medium.woff2 deleted file mode 100644 index 4a923ecf..00000000 Binary files a/frontend/static/fonts/Satoshi-Medium.woff2 and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Regular.woff b/frontend/static/fonts/Satoshi-Regular.woff deleted file mode 100644 index 5f5d47b9..00000000 Binary files a/frontend/static/fonts/Satoshi-Regular.woff and /dev/null differ diff --git a/frontend/static/fonts/Satoshi-Regular.woff2 b/frontend/static/fonts/Satoshi-Regular.woff2 deleted file mode 100644 index 76bfba6c..00000000 Binary files a/frontend/static/fonts/Satoshi-Regular.woff2 and /dev/null differ diff --git a/frontend/static/site.webmanifest b/frontend/static/site.webmanifest deleted file mode 100644 index 52a2fe3f..00000000 --- a/frontend/static/site.webmanifest +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js deleted file mode 100644 index fffb8494..00000000 --- a/frontend/svelte.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() - } -}; - -export default config; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts deleted file mode 100644 index a2b88446..00000000 --- a/frontend/tailwind.config.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { fontFamily } from 'tailwindcss/defaultTheme'; -import type { Config } from 'tailwindcss'; - -const config: Config = { - darkMode: ['class'], - content: ['./src/**/*.{html,js,svelte,ts}'], - safelist: ['dark'], - theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px' - } - }, - extend: { - colors: { - border: 'hsl(var(--border) / )', - input: 'hsl(var(--input) / )', - ring: 'hsl(var(--ring) / )', - background: 'hsl(var(--background) / )', - foreground: 'hsl(var(--foreground) / )', - primary: { - DEFAULT: 'hsl(var(--primary) / )', - foreground: 'hsl(var(--primary-foreground) / )' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary) / )', - foreground: 'hsl(var(--secondary-foreground) / )' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive) / )', - foreground: 'hsl(var(--destructive-foreground) / )' - }, - muted: { - DEFAULT: 'hsl(var(--muted) / )', - foreground: 'hsl(var(--muted-foreground) / )' - }, - accent: { - DEFAULT: 'hsl(var(--accent) / )', - foreground: 'hsl(var(--accent-foreground) / )' - }, - popover: { - DEFAULT: 'hsl(var(--popover) / )', - foreground: 'hsl(var(--popover-foreground) / )' - }, - card: { - DEFAULT: 'hsl(var(--card) / )', - foreground: 'hsl(var(--card-foreground) / )' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - fontFamily: { - sans: [...fontFamily.sans], - primary: ['Satoshi', ...fontFamily.sans] - } - } - } -}; - -export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index fc93cbd9..00000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias - // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index 6331932b..00000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - plugins: [sveltekit()], - test: { - include: ['src/**/*.{test,spec}.{js,ts}'] - }, - define: { - SUPERFORMS_LEGACY: true - } -}); diff --git a/makefile b/makefile index 14be25dd..fc52c09d 100644 --- a/makefile +++ b/makefile @@ -13,7 +13,7 @@ help: @echo "Riven Local Development Environment" @echo "-------------------------------------------------------------------------" @echo "install : Install the required packages" - @echo "run : Run the Riven backend" + @echo "run : Run the Riven src" @echo "start : Build and run the Riven container (requires Docker)" @echo "start-dev : Build and run the Riven container in development mode (requires Docker)" @echo "stop : Stop and remove the Riven container (requires Docker)" @@ -32,12 +32,14 @@ start: stop @docker compose -f docker-compose.yml up --build -d --force-recreate --remove-orphans @docker compose -f docker-compose.yml logs -f -start-dev: stop +start-dev: stop-dev @docker compose -f docker-compose-dev.yml up --build -d --force-recreate --remove-orphans @docker compose -f docker-compose-dev.yml logs -f stop: @docker compose -f docker-compose.yml down + +stop-dev: @docker compose -f docker-compose-dev.yml down restart: @@ -75,34 +77,35 @@ clean: @find . -type d -name '__pycache__' -exec rm -rf {} + @find . -type d -name '.pytest_cache' -exec rm -rf {} + @find . -type d -name '.ruff_cache' -exec rm -rf {} + - @rm -rf data/media.pkl data/media.pkl.bak + @rm -rf data/alembic/ + @rm -rf data/*.db install: @poetry install --with dev # Run the application run: - @poetry run python backend/main.py + @poetry run python src/main.py # Code quality commands format: - @poetry run isort backend + @poetry run isort src check: @poetry run pyright lint: - @poetry run ruff check backend - @poetry run isort --check-only backend + @poetry run ruff check src + @poetry run isort --check-only src sort: - @poetry run isort backend + @poetry run isort src test: - @poetry run pytest backend + @poetry run pytest src coverage: clean - @poetry run pytest backend --cov=backend --cov-report=xml --cov-report=term + @poetry run pytest src --cov=src --cov-report=xml --cov-report=term # Run the linter and tests pr-ready: clean lint test diff --git a/poetry.lock b/poetry.lock index a13f80cb..d666b7ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,24 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "alembic" +version = "1.13.2" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -80,24 +99,24 @@ test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock [[package]] name = "cachetools" -version = "5.3.3" +version = "5.4.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -241,63 +260,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.4" +version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, - {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, - {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, - {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, - {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, - {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, - {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, - {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, - {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, - {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, - {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, - {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, - {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, - {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, + {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, + {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, + {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, + {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, + {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, + {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, + {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, + {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, + {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, + {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, + {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, + {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, ] [package.extras] @@ -354,6 +373,77 @@ typing-extensions = ">=4.8.0" [package.extras] all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "h11" version = "0.14.0" @@ -791,6 +881,25 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.10)"] +[[package]] +name = "mako" +version = "1.3.5" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, + {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -815,6 +924,75 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -938,13 +1116,13 @@ regex = ">=2023.12.25,<2024.0.0" [[package]] name = "plexapi" -version = "4.15.14" +version = "4.15.15" description = "Python bindings for the Plex API." optional = false python-versions = ">=3.8" files = [ - {file = "PlexAPI-4.15.14-py3-none-any.whl", hash = "sha256:97330c16efa9be39a2eca35f186be3c0bc849fc5865f38882aa9dba21bd86846"}, - {file = "PlexAPI-4.15.14.tar.gz", hash = "sha256:c0729e66dc0640467ef0edddbf0810ee05f96ea53cc3954dee5783d5d033e3f4"}, + {file = "PlexAPI-4.15.15-py3-none-any.whl", hash = "sha256:5c96cac901f144848c5d64f703e270597fcd95aad48d0e68b72bcd0a0bcdeba7"}, + {file = "PlexAPI-4.15.15.tar.gz", hash = "sha256:af85e0425685fe4b62fa26e03502d98c97350cecc8bdbaa5aea32698667614a4"}, ] [package.dependencies] @@ -1011,20 +1189,101 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, +] + [[package]] name = "pydantic" -version = "2.8.0" +version = "2.8.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.0-py3-none-any.whl", hash = "sha256:ead4f3a1e92386a734ca1411cb25d94147cf8778ed5be6b56749047676d6364e"}, - {file = "pydantic-2.8.0.tar.gz", hash = "sha256:d970ffb9d030b710795878940bd0489842c638e7252fc4a19c3ae2f7da4d6141"}, + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.20.0" +pydantic-core = "2.20.1" typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, @@ -1035,99 +1294,100 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.20.0" +version = "2.20.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e9dcd7fb34f7bfb239b5fa420033642fff0ad676b765559c3737b91f664d4fa9"}, - {file = "pydantic_core-2.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:649a764d9b0da29816889424697b2a3746963ad36d3e0968784ceed6e40c6355"}, - {file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7701df088d0b05f3460f7ba15aec81ac8b0fb5690367dfd072a6c38cf5b7fdb5"}, - {file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab760f17c3e792225cdaef31ca23c0aea45c14ce80d8eff62503f86a5ab76bff"}, - {file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1ad5b4d73cde784cf64580166568074f5ccd2548d765e690546cff3d80937d"}, - {file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b81ec2efc04fc1dbf400647d4357d64fb25543bae38d2d19787d69360aad21c9"}, - {file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4a9732a5cad764ba37f3aa873dccb41b584f69c347a57323eda0930deec8e10"}, - {file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6dc85b9e10cc21d9c1055f15684f76fa4facadddcb6cd63abab702eb93c98943"}, - {file = "pydantic_core-2.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:21d9f7e24f63fdc7118e6cc49defaab8c1d27570782f7e5256169d77498cf7c7"}, - {file = "pydantic_core-2.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8b315685832ab9287e6124b5d74fc12dda31e6421d7f6b08525791452844bc2d"}, - {file = "pydantic_core-2.20.0-cp310-none-win32.whl", hash = "sha256:c3dc8ec8b87c7ad534c75b8855168a08a7036fdb9deeeed5705ba9410721c84d"}, - {file = "pydantic_core-2.20.0-cp310-none-win_amd64.whl", hash = "sha256:85770b4b37bb36ef93a6122601795231225641003e0318d23c6233c59b424279"}, - {file = "pydantic_core-2.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:58e251bb5a5998f7226dc90b0b753eeffa720bd66664eba51927c2a7a2d5f32c"}, - {file = "pydantic_core-2.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:78d584caac52c24240ef9ecd75de64c760bbd0e20dbf6973631815e3ef16ef8b"}, - {file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5084ec9721f82bef5ff7c4d1ee65e1626783abb585f8c0993833490b63fe1792"}, - {file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d0f52684868db7c218437d260e14d37948b094493f2646f22d3dda7229bbe3f"}, - {file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1def125d59a87fe451212a72ab9ed34c118ff771e5473fef4f2f95d8ede26d75"}, - {file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34480fd6778ab356abf1e9086a4ced95002a1e195e8d2fd182b0def9d944d11"}, - {file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42669d319db366cb567c3b444f43caa7ffb779bf9530692c6f244fc635a41eb"}, - {file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53b06aea7a48919a254b32107647be9128c066aaa6ee6d5d08222325f25ef175"}, - {file = "pydantic_core-2.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1f038156b696a1c39d763b2080aeefa87ddb4162c10aa9fabfefffc3dd8180fa"}, - {file = "pydantic_core-2.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3f0f3a4a23717280a5ee3ac4fb1f81d6fde604c9ec5100f7f6f987716bb8c137"}, - {file = "pydantic_core-2.20.0-cp311-none-win32.whl", hash = "sha256:316fe7c3fec017affd916a0c83d6f1ec697cbbbdf1124769fa73328e7907cc2e"}, - {file = "pydantic_core-2.20.0-cp311-none-win_amd64.whl", hash = "sha256:2d06a7fa437f93782e3f32d739c3ec189f82fca74336c08255f9e20cea1ed378"}, - {file = "pydantic_core-2.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d6f8c49657f3eb7720ed4c9b26624063da14937fc94d1812f1e04a2204db3e17"}, - {file = "pydantic_core-2.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad1bd2f377f56fec11d5cfd0977c30061cd19f4fa199bf138b200ec0d5e27eeb"}, - {file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed741183719a5271f97d93bbcc45ed64619fa38068aaa6e90027d1d17e30dc8d"}, - {file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d82e5ed3a05f2dcb89c6ead2fd0dbff7ac09bc02c1b4028ece2d3a3854d049ce"}, - {file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2ba34a099576234671f2e4274e5bc6813b22e28778c216d680eabd0db3f7dad"}, - {file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:879ae6bb08a063b3e1b7ac8c860096d8fd6b48dd9b2690b7f2738b8c835e744b"}, - {file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b0eefc7633a04c0694340aad91fbfd1986fe1a1e0c63a22793ba40a18fcbdc8"}, - {file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73deadd6fd8a23e2f40b412b3ac617a112143c8989a4fe265050fd91ba5c0608"}, - {file = "pydantic_core-2.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:35681445dc85446fb105943d81ae7569aa7e89de80d1ca4ac3229e05c311bdb1"}, - {file = "pydantic_core-2.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0f6dd3612a3b9f91f2e63924ea18a4476656c6d01843ca20a4c09e00422195af"}, - {file = "pydantic_core-2.20.0-cp312-none-win32.whl", hash = "sha256:7e37b6bb6e90c2b8412b06373c6978d9d81e7199a40e24a6ef480e8acdeaf918"}, - {file = "pydantic_core-2.20.0-cp312-none-win_amd64.whl", hash = "sha256:7d4df13d1c55e84351fab51383520b84f490740a9f1fec905362aa64590b7a5d"}, - {file = "pydantic_core-2.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d43e7ab3b65e4dc35a7612cfff7b0fd62dce5bc11a7cd198310b57f39847fd6c"}, - {file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6a24d7b5893392f2b8e3b7a0031ae3b14c6c1942a4615f0d8794fdeeefb08b"}, - {file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2f13c3e955a087c3ec86f97661d9f72a76e221281b2262956af381224cfc243"}, - {file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72432fd6e868c8d0a6849869e004b8bcae233a3c56383954c228316694920b38"}, - {file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d70a8ff2d4953afb4cbe6211f17268ad29c0b47e73d3372f40e7775904bc28fc"}, - {file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e49524917b8d3c2f42cd0d2df61178e08e50f5f029f9af1f402b3ee64574392"}, - {file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4f0f71653b1c1bad0350bc0b4cc057ab87b438ff18fa6392533811ebd01439c"}, - {file = "pydantic_core-2.20.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:16197e6f4fdecb9892ed2436e507e44f0a1aa2cff3b9306d1c879ea2f9200997"}, - {file = "pydantic_core-2.20.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:763602504bf640b3ded3bba3f8ed8a1cc2fc6a87b8d55c1c5689f428c49c947e"}, - {file = "pydantic_core-2.20.0-cp313-none-win32.whl", hash = "sha256:a3f243f318bd9523277fa123b3163f4c005a3e8619d4b867064de02f287a564d"}, - {file = "pydantic_core-2.20.0-cp313-none-win_amd64.whl", hash = "sha256:03aceaf6a5adaad3bec2233edc5a7905026553916615888e53154807e404545c"}, - {file = "pydantic_core-2.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d6f2d8b8da1f03f577243b07bbdd3412eee3d37d1f2fd71d1513cbc76a8c1239"}, - {file = "pydantic_core-2.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a272785a226869416c6b3c1b7e450506152d3844207331f02f27173562c917e0"}, - {file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efbb412d55a4ffe73963fed95c09ccb83647ec63b711c4b3752be10a56f0090b"}, - {file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e4f46189d8740561b43655263a41aac75ff0388febcb2c9ec4f1b60a0ec12f3"}, - {file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3df115f4a3c8c5e4d5acf067d399c6466d7e604fc9ee9acbe6f0c88a0c3cf"}, - {file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a340d2bdebe819d08f605e9705ed551c3feb97e4fd71822d7147c1e4bdbb9508"}, - {file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:616b9c2f882393d422ba11b40e72382fe975e806ad693095e9a3b67c59ea6150"}, - {file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25c46bb2ff6084859bbcfdf4f1a63004b98e88b6d04053e8bf324e115398e9e7"}, - {file = "pydantic_core-2.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:23425eccef8f2c342f78d3a238c824623836c6c874d93c726673dbf7e56c78c0"}, - {file = "pydantic_core-2.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:52527e8f223ba29608d999d65b204676398009725007c9336651c2ec2d93cffc"}, - {file = "pydantic_core-2.20.0-cp38-none-win32.whl", hash = "sha256:1c3c5b7f70dd19a6845292b0775295ea81c61540f68671ae06bfe4421b3222c2"}, - {file = "pydantic_core-2.20.0-cp38-none-win_amd64.whl", hash = "sha256:8093473d7b9e908af1cef30025609afc8f5fd2a16ff07f97440fd911421e4432"}, - {file = "pydantic_core-2.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ee7785938e407418795e4399b2bf5b5f3cf6cf728077a7f26973220d58d885cf"}, - {file = "pydantic_core-2.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e75794883d635071cf6b4ed2a5d7a1e50672ab7a051454c76446ef1ebcdcc91"}, - {file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:344e352c96e53b4f56b53d24728217c69399b8129c16789f70236083c6ceb2ac"}, - {file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:978d4123ad1e605daf1ba5e01d4f235bcf7b6e340ef07e7122e8e9cfe3eb61ab"}, - {file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c05eaf6c863781eb834ab41f5963604ab92855822a2062897958089d1335dad"}, - {file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc7e43b4a528ffca8c9151b6a2ca34482c2fdc05e6aa24a84b7f475c896fc51d"}, - {file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658287a29351166510ebbe0a75c373600cc4367a3d9337b964dada8d38bcc0f4"}, - {file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1dacf660d6de692fe351e8c806e7efccf09ee5184865893afbe8e59be4920b4a"}, - {file = "pydantic_core-2.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3e147fc6e27b9a487320d78515c5f29798b539179f7777018cedf51b7749e4f4"}, - {file = "pydantic_core-2.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c867230d715a3dd1d962c8d9bef0d3168994ed663e21bf748b6e3a529a129aab"}, - {file = "pydantic_core-2.20.0-cp39-none-win32.whl", hash = "sha256:22b813baf0dbf612752d8143a2dbf8e33ccb850656b7850e009bad2e101fc377"}, - {file = "pydantic_core-2.20.0-cp39-none-win_amd64.whl", hash = "sha256:3a7235b46c1bbe201f09b6f0f5e6c36b16bad3d0532a10493742f91fbdc8035f"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cafde15a6f7feaec2f570646e2ffc5b73412295d29134a29067e70740ec6ee20"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2aec8eeea0b08fd6bc2213d8e86811a07491849fd3d79955b62d83e32fa2ad5f"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:840200827984f1c4e114008abc2f5ede362d6e11ed0b5931681884dd41852ff1"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ea1d8b7df522e5ced34993c423c3bf3735c53df8b2a15688a2f03a7d678800"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5b8376a867047bf08910573deb95d3c8dfb976eb014ee24f3b5a61ccc5bee1b"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d08264b4460326cefacc179fc1411304d5af388a79910832835e6f641512358b"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7a3639011c2e8a9628466f616ed7fb413f30032b891898e10895a0a8b5857d6c"}, - {file = "pydantic_core-2.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05e83ce2f7eba29e627dd8066aa6c4c0269b2d4f889c0eba157233a353053cea"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:603a843fea76a595c8f661cd4da4d2281dff1e38c4a836a928eac1a2f8fe88e4"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac76f30d5d3454f4c28826d891fe74d25121a346c69523c9810ebba43f3b1cec"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e3b1d4b1b3f6082849f9b28427ef147a5b46a6132a3dbaf9ca1baa40c88609"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2761f71faed820e25ec62eacba670d1b5c2709bb131a19fcdbfbb09884593e5a"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a0586cddbf4380e24569b8a05f234e7305717cc8323f50114dfb2051fcbce2a3"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b8c46a8cf53e849eea7090f331ae2202cd0f1ceb090b00f5902c423bd1e11805"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b4a085bd04af7245e140d1b95619fe8abb445a3d7fdf219b3f80c940853268ef"}, - {file = "pydantic_core-2.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:116b326ac82c8b315e7348390f6d30bcfe6e688a7d3f1de50ff7bcc2042a23c2"}, - {file = "pydantic_core-2.20.0.tar.gz", hash = "sha256:366be8e64e0cb63d87cf79b4e1765c0703dd6313c729b22e7b9e378db6b96877"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [package.dependencies] @@ -1135,13 +1395,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyfakefs" -version = "5.5.0" +version = "5.6.0" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" files = [ - {file = "pyfakefs-5.5.0-py3-none-any.whl", hash = "sha256:8dbf203ab7bef1529f11f7d41b9478b898e95bf9f3b71262163aac07a518cd76"}, - {file = "pyfakefs-5.5.0.tar.gz", hash = "sha256:7448aaa07142f892d0a4eb52a5ed3206a9f02c6599e686cd97d624c18979c154"}, + {file = "pyfakefs-5.6.0-py3-none-any.whl", hash = "sha256:1a45bba8615323ec29d65929d32dc66d7b59a1e60a02109950440edb0486c539"}, + {file = "pyfakefs-5.6.0.tar.gz", hash = "sha256:7a549b32865aa97d8ba6538285a93816941d9b7359be2954ac60ec36b277e879"}, ] [[package]] @@ -1177,13 +1437,13 @@ dev = ["importlib-metadata", "tox"] [[package]] name = "pyright" -version = "1.1.369" +version = "1.1.371" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.369-py3-none-any.whl", hash = "sha256:06d5167a8d7be62523ced0265c5d2f1e022e110caf57a25d92f50fb2d07bcda0"}, - {file = "pyright-1.1.369.tar.gz", hash = "sha256:ad290710072d021e213b98cc7a2f90ae3a48609ef5b978f749346d1a47eb9af8"}, + {file = "pyright-1.1.371-py3-none-any.whl", hash = "sha256:cce52e42ff73943243e7e5e24f2a59dee81b97d99f4e3cf97370b27e8a1858cd"}, + {file = "pyright-1.1.371.tar.gz", hash = "sha256:777b508b92dda2db476214c400ce043aad8d8f3dd0e10d284c96e79f298308b5"}, ] [package.dependencies] @@ -1213,24 +1473,6 @@ pluggy = ">=1.5,<2.0" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1350,104 +1592,104 @@ regex = ">=2023.12.25,<2024.0.0" [[package]] name = "rapidfuzz" -version = "3.9.3" +version = "3.9.4" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.8" files = [ - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdb8c5b8e29238ec80727c2ba3b301efd45aa30c6a7001123a6647b8e6f77ea4"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3bd0d9632088c63a241f217742b1cf86e2e8ae573e01354775bd5016d12138c"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153f23c03d4917f6a1fc2fb56d279cc6537d1929237ff08ee7429d0e40464a18"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96c5225e840f1587f1bac8fa6f67562b38e095341576e82b728a82021f26d62"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b777cd910ceecd738adc58593d6ed42e73f60ad04ecdb4a841ae410b51c92e0e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53e06e4b81f552da04940aa41fc556ba39dee5513d1861144300c36c33265b76"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7ca5b6050f18fdcacdada2dc5fb7619ff998cd9aba82aed2414eee74ebe6cd"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:87bb8d84cb41446a808c4b5f746e29d8a53499381ed72f6c4e456fe0f81c80a8"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:959a15186d18425d19811bea86a8ffbe19fd48644004d29008e636631420a9b7"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a24603dd05fb4e3c09d636b881ce347e5f55f925a6b1b4115527308a323b9f8e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d055da0e801c71dd74ba81d72d41b2fa32afa182b9fea6b4b199d2ce937450d"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:875b581afb29a7213cf9d98cb0f98df862f1020bce9d9b2e6199b60e78a41d14"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win32.whl", hash = "sha256:6073a46f61479a89802e3f04655267caa6c14eb8ac9d81a635a13805f735ebc1"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:119c010e20e561249b99ca2627f769fdc8305b07193f63dbc07bca0a6c27e892"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:790b0b244f3213581d42baa2fed8875f9ee2b2f9b91f94f100ec80d15b140ba9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f57e8305c281e8c8bc720515540e0580355100c0a7a541105c6cafc5de71daae"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4fc7b784cf987dbddc300cef70e09a92ed1bce136f7bb723ea79d7e297fe76d"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b422c0a6fe139d5447a0766268e68e6a2a8c2611519f894b1f31f0a392b9167"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f50fed4a9b0c9825ff37cf0bccafd51ff5792090618f7846a7650f21f85579c9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b80eb7cbe62348c61d3e67e17057cddfd6defab168863028146e07d5a8b24a89"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f45be77ec82da32ce5709a362e236ccf801615cc7163b136d1778cf9e31b14"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd84b7f652a5610733400307dc732f57c4a907080bef9520412e6d9b55bc9adc"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e6d27dad8c990218b8cd4a5c99cbc8834f82bb46ab965a7265d5aa69fc7ced7"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05ee0696ebf0dfe8f7c17f364d70617616afc7dafe366532730ca34056065b8a"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2bc8391749e5022cd9e514ede5316f86e332ffd3cfceeabdc0b17b7e45198a8c"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93981895602cf5944d89d317ae3b1b4cc684d175a8ae2a80ce5b65615e72ddd0"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:754b719a4990735f66653c9e9261dcf52fd4d925597e43d6b9069afcae700d21"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win32.whl", hash = "sha256:14c9f268ade4c88cf77ab007ad0fdf63699af071ee69378de89fff7aa3cae134"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc1991b4cde6c9d3c0bbcb83d5581dc7621bec8c666c095c65b4277233265a82"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:0c34139df09a61b1b557ab65782ada971b4a3bce7081d1b2bee45b0a52231adb"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d6a210347d6e71234af5c76d55eeb0348b026c9bb98fe7c1cca89bac50fb734"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b300708c917ce52f6075bdc6e05b07c51a085733650f14b732c087dc26e0aaad"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83ea7ca577d76778250421de61fb55a719e45b841deb769351fc2b1740763050"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8319838fb5b7b5f088d12187d91d152b9386ce3979ed7660daa0ed1bff953791"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:505d99131afd21529293a9a7b91dfc661b7e889680b95534756134dc1cc2cd86"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c52970f7784518d7c82b07a62a26e345d2de8c2bd8ed4774e13342e4b3ff4200"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:143caf7247449055ecc3c1e874b69e42f403dfc049fc2f3d5f70e1daf21c1318"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b8ab0fa653d9225195a8ff924f992f4249c1e6fa0aea563f685e71b81b9fcccf"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57e7c5bf7b61c7320cfa5dde1e60e678d954ede9bb7da8e763959b2138391401"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51fa1ba84653ab480a2e2044e2277bd7f0123d6693051729755addc0d015c44f"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:17ff7f7eecdb169f9236e3b872c96dbbaf116f7787f4d490abd34b0116e3e9c8"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afe7c72d3f917b066257f7ff48562e5d462d865a25fbcabf40fca303a9fa8d35"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win32.whl", hash = "sha256:e53ed2e9b32674ce96eed80b3b572db9fd87aae6742941fb8e4705e541d861ce"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:35b7286f177e4d8ba1e48b03612f928a3c4bdac78e5651379cec59f95d8651e6"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:e6e4b9380ed4758d0cb578b0d1970c3f32dd9e87119378729a5340cb3169f879"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a39890013f6d5b056cc4bfdedc093e322462ece1027a57ef0c636537bdde7531"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b5bc0fdbf419493163c5c9cb147c5fbe95b8e25844a74a8807dcb1a125e630cf"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe6e200a75a792d37b960457904c4fce7c928a96ae9e5d21d2bd382fe39066e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de077c468c225d4c18f7188c47d955a16d65f21aab121cbdd98e3e2011002c37"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f917eaadf5388466a95f6a236f678a1588d231e52eda85374077101842e794e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858ba57c05afd720db8088a8707079e8d024afe4644001fe0dbd26ef7ca74a65"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36447d21b05f90282a6f98c5a33771805f9222e5d0441d03eb8824e33e5bbb4"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:acbe4b6f1ccd5b90c29d428e849aa4242e51bb6cab0448d5f3c022eb9a25f7b1"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:53c7f27cdf899e94712972237bda48cfd427646aa6f5d939bf45d084780e4c16"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6175682a829c6dea4d35ed707f1dadc16513270ef64436568d03b81ccb6bdb74"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5276df395bd8497397197fca2b5c85f052d2e6a66ffc3eb0544dd9664d661f95"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:77b5c4f3e72924d7845f0e189c304270066d0f49635cf8a3938e122c437e58de"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win32.whl", hash = "sha256:8add34061e5cd561c72ed4febb5c15969e7b25bda2bb5102d02afc3abc1f52d0"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:604e0502a39cf8e67fa9ad239394dddad4cdef6d7008fdb037553817d420e108"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21047f55d674614eb4b0ab34e35c3dc66f36403b9fbfae645199c4a19d4ed447"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a56da3aff97cb56fe85d9ca957d1f55dbac7c27da927a86a2a86d8a7e17f80aa"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c08481aec2fe574f0062e342924db2c6b321391aeb73d68853ed42420fd6d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e2b827258beefbe5d3f958243caa5a44cf46187eff0c20e0b2ab62d1550327a"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6e65a301fcd19fbfbee3a514cc0014ff3f3b254b9fd65886e8a9d6957fb7bca"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe93ba1725a8d47d2b9dca6c1f435174859427fbc054d83de52aea5adc65729"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca21c0a34adee582775da997a600283e012a608a107398d80a42f9a57ad323d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:256e07d3465173b2a91c35715a2277b1ee3ae0b9bbab4e519df6af78570741d0"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:802ca2cc8aa6b8b34c6fdafb9e32540c1ba05fca7ad60b3bbd7ec89ed1797a87"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:dd789100fc852cffac1449f82af0da139d36d84fd9faa4f79fc4140a88778343"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5d0abbacdb06e27ff803d7ae0bd0624020096802758068ebdcab9bd49cf53115"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:378d1744828e27490a823fc6fe6ebfb98c15228d54826bf4e49e4b76eb5f5579"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win32.whl", hash = "sha256:5d0cb272d43e6d3c0dedefdcd9d00007471f77b52d2787a4695e9dd319bb39d2"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:15e4158ac4b3fb58108072ec35b8a69165f651ba1c8f43559a36d518dbf9fb3f"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_arm64.whl", hash = "sha256:58c6a4936190c558d5626b79fc9e16497e5df7098589a7e80d8bff68148ff096"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5410dc848c947a603792f4f51b904a3331cf1dc60621586bfbe7a6de72da1091"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:282d55700a1a3d3a7980746eb2fcd48c9bbc1572ebe0840d0340d548a54d01fe"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc1037507810833646481f5729901a154523f98cbebb1157ba3a821012e16402"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e33f779391caedcba2ba3089fb6e8e557feab540e9149a5c3f7fea7a3a7df37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41a81a9f311dc83d22661f9b1a1de983b201322df0c4554042ffffd0f2040c37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a93250bd8fae996350c251e1752f2c03335bb8a0a5b0c7e910a593849121a435"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3617d1aa7716c57d120b6adc8f7c989f2d65bc2b0cbd5f9288f1fc7bf469da11"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad04a3f5384b82933213bba2459f6424decc2823df40098920856bdee5fd6e88"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8709918da8a88ad73c9d4dd0ecf24179a4f0ceba0bee21efc6ea21a8b5290349"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b770f85eab24034e6ef7df04b2bfd9a45048e24f8a808e903441aa5abde8ecdd"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930b4e6fdb4d914390141a2b99a6f77a52beacf1d06aa4e170cba3a98e24c1bc"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c8444e921bfc3757c475c4f4d7416a7aa69b2d992d5114fe55af21411187ab0d"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c1d3ef3878f871abe6826e386c3d61b5292ef5f7946fe646f4206b85836b5da"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d861bf326ee7dabc35c532a40384541578cd1ec1e1b7db9f9ecbba56eb76ca22"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cde6b9d9ba5007077ee321ec722fa714ebc0cbd9a32ccf0f4dd3cc3f20952d71"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb6546e7b6bed1aefbe24f68a5fb9b891cc5aef61bca6c1a7b1054b7f0359bb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8a57261ef7996d5ced7c8cba9189ada3fbeffd1815f70f635e4558d93766cb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:67201c02efc596923ad950519e0b75ceb78d524177ea557134d6567b9ac2c283"}, - {file = "rapidfuzz-3.9.3.tar.gz", hash = "sha256:b398ea66e8ed50451bce5997c430197d5e4b06ac4aa74602717f792d8d8d06e2"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b9793c19bdf38656c8eaefbcf4549d798572dadd70581379e666035c9df781"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:015b5080b999404fe06ec2cb4f40b0be62f0710c926ab41e82dfbc28e80675b4"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc5ceca9c1e1663f3e6c23fb89a311f69b7615a40ddd7645e3435bf3082688a"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1424e238bc3f20e1759db1e0afb48a988a9ece183724bef91ea2a291c0b92a95"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed01378f605aa1f449bee82cd9c83772883120d6483e90aa6c5a4ce95dc5c3aa"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb26d412271e5a76cdee1c2d6bf9881310665d3fe43b882d0ed24edfcb891a84"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f37e9e1f17be193c41a31c864ad4cd3ebd2b40780db11cd5c04abf2bcf4201b"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d070ec5cf96b927c4dc5133c598c7ff6db3b833b363b2919b13417f1002560bc"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:10e61bb7bc807968cef09a0e32ce253711a2d450a4dce7841d21d45330ffdb24"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:31a2fc60bb2c7face4140010a7aeeafed18b4f9cdfa495cc644a68a8c60d1ff7"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fbebf1791a71a2e89f5c12b78abddc018354d5859e305ec3372fdae14f80a826"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aee9fc9e3bb488d040afc590c0a7904597bf4ccd50d1491c3f4a5e7e67e6cd2c"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win32.whl", hash = "sha256:005a02688a51c7d2451a2d41c79d737aa326ff54167211b78a383fc2aace2c2c"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:3a2e75e41ee3274754d3b2163cc6c82cd95b892a85ab031f57112e09da36455f"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win_arm64.whl", hash = "sha256:2c99d355f37f2b289e978e761f2f8efeedc2b14f4751d9ff7ee344a9a5ca98d9"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:07141aa6099e39d48637ce72a25b893fc1e433c50b3e837c75d8edf99e0c63e1"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db1664eaff5d7d0f2542dd9c25d272478deaf2c8412e4ad93770e2e2d828e175"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc01a223f6605737bec3202e94dcb1a449b6c76d46082cfc4aa980f2a60fd40e"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1869c42e73e2a8910b479be204fa736418741b63ea2325f9cc583c30f2ded41a"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62ea7007941fb2795fff305ac858f3521ec694c829d5126e8f52a3e92ae75526"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:698e992436bf7f0afc750690c301215a36ff952a6dcd62882ec13b9a1ebf7a39"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b76f611935f15a209d3730c360c56b6df8911a9e81e6a38022efbfb96e433bab"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129627d730db2e11f76169344a032f4e3883d34f20829419916df31d6d1338b1"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:90a82143c14e9a14b723a118c9ef8d1bbc0c5a16b1ac622a1e6c916caff44dd8"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ded58612fe3b0e0d06e935eaeaf5a9fd27da8ba9ed3e2596307f40351923bf72"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f16f5d1c4f02fab18366f2d703391fcdbd87c944ea10736ca1dc3d70d8bd2d8b"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:26aa7eece23e0df55fb75fbc2a8fb678322e07c77d1fd0e9540496e6e2b5f03e"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win32.whl", hash = "sha256:f187a9c3b940ce1ee324710626daf72c05599946bd6748abe9e289f1daa9a077"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8e9130fe5d7c9182990b366ad78fd632f744097e753e08ace573877d67c32f8"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win_arm64.whl", hash = "sha256:40419e98b10cd6a00ce26e4837a67362f658fc3cd7a71bd8bd25c99f7ee8fea5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b5d5072b548db1b313a07d62d88fe0b037bd2783c16607c647e01b070f6cf9e5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf5bcf22e1f0fd273354462631d443ef78d677f7d2fc292de2aec72ae1473e66"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8fc973adde8ed52810f590410e03fb6f0b541bbaeb04c38d77e63442b2df4c"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2464bb120f135293e9a712e342c43695d3d83168907df05f8c4ead1612310c7"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d9d58689aca22057cf1a5851677b8a3ccc9b535ca008c7ed06dc6e1899f7844"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167e745f98baa0f3034c13583e6302fb69249a01239f1483d68c27abb841e0a1"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0bf0663b4b6da1507869722420ea9356b6195aa907228d6201303e69837af9"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd6ac61b74fdb9e23f04d5f068e6cf554f47e77228ca28aa2347a6ca8903972f"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:60ff67c690acecf381759c16cb06c878328fe2361ddf77b25d0e434ea48a29da"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cb934363380c60f3a57d14af94325125cd8cded9822611a9f78220444034e36e"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fe833493fb5cc5682c823ea3e2f7066b07612ee8f61ecdf03e1268f262106cdd"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2797fb847d89e04040d281cb1902cbeffbc4b5131a5c53fc0db490fd76b2a547"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win32.whl", hash = "sha256:52e3d89377744dae68ed7c84ad0ddd3f5e891c82d48d26423b9e066fc835cc7c"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:c76da20481c906e08400ee9be230f9e611d5931a33707d9df40337c2655c84b5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win_arm64.whl", hash = "sha256:f2d2846f3980445864c7e8b8818a29707fcaff2f0261159ef6b7bd27ba139296"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:355fc4a268ffa07bab88d9adee173783ec8d20136059e028d2a9135c623c44e6"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d81a78f90269190b568a8353d4ea86015289c36d7e525cd4d43176c88eff429"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e618625ffc4660b26dc8e56225f8b966d5842fa190e70c60db6cd393e25b86e"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b712336ad6f2bacdbc9f1452556e8942269ef71f60a9e6883ef1726b52d9228a"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc1ee19fdad05770c897e793836c002344524301501d71ef2e832847425707"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1950f8597890c0c707cb7e0416c62a1cf03dcdb0384bc0b2dbda7e05efe738ec"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6c35f272ec9c430568dc8c1c30cb873f6bc96be2c79795e0bce6db4e0e101d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1df0f9e9239132a231c86ae4f545ec2b55409fa44470692fcfb36b1bd00157ad"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d2c51955329bfccf99ae26f63d5928bf5be9fcfcd9f458f6847fd4b7e2b8986c"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:3c522f462d9fc504f2ea8d82e44aa580e60566acc754422c829ad75c752fbf8d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:d8a52fc50ded60d81117d7647f262c529659fb21d23e14ebfd0b35efa4f1b83d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:04dbdfb0f0bfd3f99cf1e9e24fadc6ded2736d7933f32f1151b0f2abb38f9a25"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-win32.whl", hash = "sha256:4968c8bd1df84b42f382549e6226710ad3476f976389839168db3e68fd373298"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:3fe4545f89f8d6c27b6bbbabfe40839624873c08bd6700f63ac36970a179f8f5"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f256c8fb8f3125574c8c0c919ab0a1f75d7cba4d053dda2e762dcc36357969d"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fdc09cf6e9d8eac3ce48a4615b3a3ee332ea84ac9657dbbefef913b13e632f"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d395d46b80063d3b5d13c0af43d2c2cedf3ab48c6a0c2aeec715aa5455b0c632"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa714fb96ce9e70c37e64c83b62fe8307030081a0bfae74a76fac7ba0f91715"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc1a0f29f9119be7a8d3c720f1d2068317ae532e39e4f7f948607c3a6de8396"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6022674aa1747d6300f699cd7c54d7dae89bfe1f84556de699c4ac5df0838082"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb72e5f9762fd469701a7e12e94b924af9004954f8c739f925cb19c00862e38"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad04ae301129f0eb5b350a333accd375ce155a0c1cec85ab0ec01f770214e2e4"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f46a22506f17c0433e349f2d1dc11907c393d9b3601b91d4e334fa9a439a6a4d"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:01b42a8728c36011718da409aa86b84984396bf0ca3bfb6e62624f2014f6022c"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e590d5d5443cf56f83a51d3c4867bd1f6be8ef8cfcc44279522bcef3845b2a51"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c72078b5fdce34ba5753f9299ae304e282420e6455e043ad08e4488ca13a2b0"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win32.whl", hash = "sha256:f75639277304e9b75e6a7b3c07042d2264e16740a11e449645689ed28e9c2124"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:e81e27e8c32a1e1278a4bb1ce31401bfaa8c2cc697a053b985a6f8d013df83ec"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win_arm64.whl", hash = "sha256:15bc397ee9a3ed1210b629b9f5f1da809244adc51ce620c504138c6e7095b7bd"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:20488ade4e1ddba3cfad04f400da7a9c1b91eff5b7bd3d1c50b385d78b587f4f"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e61b03509b1a6eb31bc5582694f6df837d340535da7eba7bedb8ae42a2fcd0b9"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098d231d4e51644d421a641f4a5f2f151f856f53c252b03516e01389b2bfef99"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17ab8b7d10fde8dd763ad428aa961c0f30a1b44426e675186af8903b5d134fb0"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e272df61bee0a056a3daf99f9b1bd82cf73ace7d668894788139c868fdf37d6f"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d6481e099ff8c4edda85b8b9b5174c200540fd23c8f38120016c765a86fa01f5"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ad61676e9bdae677d577fe80ec1c2cea1d150c86be647e652551dcfe505b1113"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:af65020c0dd48d0d8ae405e7e69b9d8ae306eb9b6249ca8bf511a13f465fad85"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d38b4e026fcd580e0bda6c0ae941e0e9a52c6bc66cdce0b8b0da61e1959f5f8"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f74ed072c2b9dc6743fb19994319d443a4330b0e64aeba0aa9105406c7c5b9c2"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee5f6b8321f90615c184bd8a4c676e9becda69b8e4e451a90923db719d6857c"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a555e3c841d6efa350f862204bb0a3fea0c006b8acc9b152b374fa36518a1c6"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0772150d37bf018110351c01d032bf9ab25127b966a29830faa8ad69b7e2f651"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:addcdd3c3deef1bd54075bd7aba0a6ea9f1d01764a08620074b7a7b1e5447cb9"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fe86b82b776554add8f900b6af202b74eb5efe8f25acdb8680a5c977608727f"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0fc91ac59f4414d8542454dfd6287a154b8e6f1256718c898f695bdbb993467"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a944e546a296a5fdcaabb537b01459f1b14d66f74e584cb2a91448bffadc3c1"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fb96ba96d58c668a17a06b5b5e8340fedc26188e87b0d229d38104556f30cd8"}, + {file = "rapidfuzz-3.9.4.tar.gz", hash = "sha256:366bf8947b84e37f2f4cf31aaf5f37c39f620d8c0eddb8b633e6ba0129ca4a0a"}, ] [package.extras] @@ -1642,6 +1884,108 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sqla-wrapper" +version = "6.0.0" +description = "A framework-independent modern wrapper for SQLAlchemy & Alembic" +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "sqla_wrapper-6.0.0-py3-none-any.whl", hash = "sha256:39665c54c310a40832bf79db36c6c4230d5654a73b109847203cd6b98c93af95"}, + {file = "sqla_wrapper-6.0.0.tar.gz", hash = "sha256:4159aed7aa391d5a664574230fc2c9f6185eb7f1d10852495aadecff55864ac7"}, +] + +[package.dependencies] +alembic = ">=1.9,<2.0" +sqlalchemy = ">=2.0,<3.0" + +[[package]] +name = "sqlalchemy" +version = "2.0.31" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"}, + {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, + {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "starlette" version = "0.37.2" @@ -1709,17 +2053,6 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] -[[package]] -name = "unidecode" -version = "1.3.8" -description = "ASCII transliterations of Unicode text" -optional = false -python-versions = ">=3.5" -files = [ - {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, - {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, -] - [[package]] name = "urllib3" version = "2.2.2" @@ -2096,4 +2429,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "ec310c9e9c651dde48ebfba5bfb8af0edf289060db25ff3396aebaef9dee7227" +content-hash = "1c7b41c0c73f6581860846426d0f7900103ba2817046f6df40d5866be59bd9b4" diff --git a/pyproject.toml b/pyproject.toml index 45a735d8..8d3b891f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,19 +27,27 @@ opentelemetry-api = "^1.25.0" opentelemetry-sdk = "^1.25.0" opentelemetry-exporter-prometheus = "^0.46b0" prometheus-client = "^0.20.0" -unidecode = "^1.3.8" +sqlalchemy = "^2.0.31" +sqla-wrapper = "^6.0.0" +alembic = "^1.13.2" +psycopg2-binary = "^2.9.9" [tool.poetry.group.dev.dependencies] pyright = "^1.1.352" pyperf = "^2.2.0" +pytest = "^8.2.2" +pyfakefs = "^5.4.1" ruff = "^0.3.0" isort = "^5.10.1" codecov = "^2.1.13" -pytest = "^8.1.1" -pytest-cov = "^5.0.0" -pyfakefs = "^5.4.1" httpx = "^0.27.0" +[tool.poetry.group.test] +optional = true + +[tool.poetry.group.test.dependencies] +pytest= "*" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" @@ -74,7 +82,7 @@ ignore = [ "S108", # flake8-bandit: Probable insecure usage of temp file/directory "S311", # flake8-bandit: Standard pseudo-random generators are not suitable for security/cryptographic purposes "S101", # ruff: Ignore assert warnings on tests - "RET505", # + "RET505", # "RET503", # ruff: Ignore required explicit returns (is this desired?) "SLF001" # private member accessing from pickle ] diff --git a/backend/.gitignore b/src/.gitignore similarity index 100% rename from backend/.gitignore rename to src/.gitignore diff --git a/backend/__init__.py b/src/__init__.py similarity index 100% rename from backend/__init__.py rename to src/__init__.py diff --git a/backend/controllers/default.py b/src/controllers/default.py similarity index 50% rename from backend/controllers/default.py rename to src/controllers/default.py index 7676d801..b2de087f 100644 --- a/backend/controllers/default.py +++ b/src/controllers/default.py @@ -2,10 +2,15 @@ import requests from fastapi import APIRouter, HTTPException, Request +from program.media.item import MediaItem from program.content.trakt import TraktContent from program.media.state import States from program.scrapers import Scraping from program.settings.manager import settings_manager +from program.media.item import Episode, MediaItem, Movie, Season, Show +from program.db.db import db +from sqlalchemy import select, func +import program.db.db_functions as DB router = APIRouter( responses={404: {"description": "Not found"}}, @@ -16,7 +21,7 @@ async def root(): return { "success": True, - "message": "Iceburg is running!", + "message": "Riven is running!", "version": settings_manager.settings.version, } @@ -99,55 +104,61 @@ async def trakt_oauth_callback(code: str, request: Request): @router.get("/stats") async def get_stats(request: Request): payload = {} - - total_items = len(request.app.program.media_items._items) - total_movies = len(request.app.program.media_items._movies) - total_shows = len(request.app.program.media_items._shows) - total_seasons = len(request.app.program.media_items._seasons) - total_episodes = len(request.app.program.media_items._episodes) - - _incomplete_items = request.app.program.media_items.get_incomplete_items() - - incomplete_retries = {} - for _, item in _incomplete_items.items(): - incomplete_retries[item.log_string] = item.scraped_times - - states = {} - for state in States: - states[state] = request.app.program.media_items.count(state) - - payload["total_items"] = total_items - payload["total_movies"] = total_movies - payload["total_shows"] = total_shows - payload["total_seasons"] = total_seasons - payload["total_episodes"] = total_episodes - payload["incomplete_items"] = len(_incomplete_items) - payload["incomplete_retries"] = incomplete_retries - payload["states"] = states - - return {"success": True, "data": payload} + with db.Session() as session: + + movies_symlinks = session.execute(select(func.count(Movie._id)).where(Movie.symlinked == True)).scalar_one() + episodes_symlinks = session.execute(select(func.count(Episode._id)).where(Episode.symlinked == True)).scalar_one() + total_symlinks = movies_symlinks + episodes_symlinks + + total_movies = session.execute(select(func.count(Movie._id))).scalar_one() + total_shows = session.execute(select(func.count(Show._id))).scalar_one() + total_seasons = session.execute(select(func.count(Season._id))).scalar_one() + total_episodes = session.execute(select(func.count(Episode._id))).scalar_one() + total_items = session.execute(select(func.count(MediaItem._id))).scalar_one() + _incomplete_items = session.execute(select(MediaItem).where(MediaItem.last_state != "Completed")).unique().scalars().all() + + incomplete_retries = {} + for item in _incomplete_items: + incomplete_retries[item.log_string] = item.scraped_times + + states = {} + for state in States: + states[state] = session.execute(select(func.count(MediaItem._id)).where(MediaItem.last_state == state.value)).scalar_one() + + payload["total_items"] = total_items + payload["total_movies"] = total_movies + payload["total_shows"] = total_shows + payload["total_seasons"] = total_seasons + payload["total_episodes"] = total_episodes + payload["total_symlinks"] = total_symlinks + payload["incomplete_items"] = len(_incomplete_items) + payload["incomplete_retries"] = incomplete_retries + payload["states"] = states + + return {"success": True, "data": payload} @router.get("/scrape/{item_id:path}") async def scrape_item(item_id: str, request: Request): - item = request.app.program.media_items.get_item(item_id) - if item is None: - raise HTTPException(status_code=404, detail="Item not found") - - scraper = request.app.program.services.get(Scraping) - if scraper is None: - raise HTTPException(status_code=404, detail="Scraping service not found") - - time_now = time.time() - scraped_results = scraper.scrape(item, log=False) - time_end = time.time() - duration = time_end - time_now - - results = {} - for hash, torrent in scraped_results.items(): - results[hash] = { - "title": torrent.data.parsed_title, - "raw_title": torrent.raw_title, - "rank": torrent.rank, - } - - return {"success": True, "total": len(results), "duration": round(duration, 3), "results": results} \ No newline at end of file + with db.Session() as session: + item = DB._get_item_from_db(session, MediaItem({"imdb_id":str(item_id)})) + if item is None: + raise HTTPException(status_code=404, detail="Item not found") + + scraper = request.app.program.services.get(Scraping) + if scraper is None: + raise HTTPException(status_code=404, detail="Scraping service not found") + + time_now = time.time() + scraped_results = scraper.scrape(item, log=False) + time_end = time.time() + duration = time_end - time_now + + results = {} + for hash, torrent in scraped_results.items(): + results[hash] = { + "title": torrent.data.parsed_title, + "raw_title": torrent.raw_title, + "rank": torrent.rank, + } + + return {"success": True, "total": len(results), "duration": round(duration, 3), "results": results} \ No newline at end of file diff --git a/backend/controllers/items.py b/src/controllers/items.py similarity index 55% rename from backend/controllers/items.py rename to src/controllers/items.py index ddf604c7..f9b9672b 100644 --- a/backend/controllers/items.py +++ b/src/controllers/items.py @@ -1,12 +1,11 @@ -from datetime import datetime -from enum import Enum from typing import List, Optional import Levenshtein from fastapi import APIRouter, HTTPException, Request -from program.content.overseerr import Overseerr -from program.media.container import MediaItemContainer -from program.media.item import Episode, ItemId, MediaItem, Movie, Season, Show +from program.db.db import db +from sqlalchemy import select, func +import program.db.db_functions as DB +from program.media.item import Episode, MediaItem, Movie, Season, Show from program.media.state import States from program.symlink import Symlinker from pydantic import BaseModel @@ -51,46 +50,29 @@ async def get_items( if limit < 1: raise HTTPException(status_code=400, detail="Limit must be 1 or greater.") - items = list(request.app.program.media_items._items.values()) - total_items = len(items) + query = select(MediaItem) - if search: # TODO: fix for search + if search: search_lower = search.lower() - filtered_items = [] if search_lower.startswith("tt"): - item = request.app.program.media_items.get_item(ItemId(search_lower)) - if item: - filtered_items.append(item) - else: - raise HTTPException(status_code=404, detail="Item not found.") + query = query.where(MediaItem.imdb_id == search_lower) else: - for item in items: - if isinstance(item, MediaItem): - title_match = ( - item.title - and Levenshtein.distance(search_lower, item.title.lower()) - <= 0.90 - ) - imdb_match = ( - item.imdb_id - and Levenshtein.distance(search_lower, item.imdb_id.lower()) - <= 1 - ) - if title_match or imdb_match: - filtered_items.append(item) - items = filtered_items + query = query.where( + (func.lower(MediaItem.title).like(f"%{search_lower}%")) | + (func.lower(MediaItem.imdb_id).like(f"%{search_lower}%")) + ) if state: filter_lower = state.lower() filter_state = None - for state in States: - if Levenshtein.distance(filter_lower, state.name.lower()) <= 0.82: - filter_state = state + for state_enum in States: + if Levenshtein.distance(filter_lower, state_enum.name.lower()) <= 0.82: + filter_state = state_enum break if filter_state: - items = [item for item in items if item.state == filter_state] + query = query.where(MediaItem.state == filter_state) else: - valid_states = [state.name for state in States] + valid_states = [state_enum.name for state_enum in States] raise HTTPException( status_code=400, detail=f"Invalid filter state: {state}. Valid states are: {valid_states}", @@ -98,62 +80,47 @@ async def get_items( if type: type_lower = type.lower() - if type_lower == "movie": - items = list(request.app.program.media_items.movies.values()) - total_items = len(items) - elif type_lower == "show": - items = list(request.app.program.media_items.shows.values()) - total_items = len(items) - elif type_lower == "season": - items = list(request.app.program.media_items.seasons.values()) - total_items = len(items) - elif type_lower == "episode": - items = list(request.app.program.media_items.episodes.values()) - total_items = len(items) - else: + if type_lower not in ["movie", "show", "season", "episode"]: raise HTTPException( status_code=400, detail=f"Invalid type: {type}. Valid types are: ['movie', 'show', 'season', 'episode']", ) + query = query.where(MediaItem.type == type_lower) - if ( - sort and not search - ): # we don't want to sort search results as they are already sorted by relevance + if sort and not search: if sort.lower() == "asc": - items = sorted(items, key=lambda x: x.requested_at) + query = query.order_by(MediaItem.requested_at.asc()) elif sort.lower() == "desc": - items = sorted(items, key=lambda x: x.requested_at, reverse=True) + query = query.order_by(MediaItem.requested_at.desc()) else: raise HTTPException( status_code=400, detail=f"Invalid sort: {sort}. Valid sorts are: ['asc', 'desc']", ) - start = (page - 1) * limit - end = start + limit - items = items[start:end] - total_pages = (total_items + limit - 1) // limit + with db.Session() as session: + total_items = session.execute(select(func.count()).select_from(query.subquery())).scalar_one() + items = session.execute(query.offset((page - 1) * limit).limit(limit)).unique().scalars().all() - return { - "success": True, - "items": [item.to_dict() for item in items], - "page": page, - "limit": limit, - "total_items": total_items, - "total_pages": total_pages, - } + total_pages = (total_items + limit - 1) // limit + + return { + "success": True, + "items": [item.to_dict() for item in items], + "page": page, + "limit": limit, + "total_items": total_items, + "total_pages": total_pages, + } @router.get("/extended/{item_id}") async def get_extended_item_info(request: Request, item_id: str): - mic: MediaItemContainer = request.app.program.media_items - item = mic.get(item_id) - if item is None: - raise HTTPException(status_code=404, detail="Item not found") - return { - "success": True, - "item": item.to_extended_dict(), - } + with db.Session() as session: + item = DB._get_item_from_db(session, MediaItem({"imdb_id":str(item_id)})) + if item is None: + raise HTTPException(status_code=404, detail="Item not found") + return {"success": True, "item": item.to_extended_dict()} @router.post("/add/imdb/{imdb_id}") @@ -190,7 +157,7 @@ async def remove_item( request: Request, item_id: Optional[str] = None, imdb_id: Optional[str] = None ): if item_id: - item = request.app.program.media_items.get(ItemId(item_id)) + item = request.app.program.media_items.get(item_id) id_type = "ID" elif imdb_id: item = next( @@ -246,32 +213,48 @@ async def get_imdb_info( Get the item with the given IMDb ID. If the season and episode are provided, get the item with the given season and episode. """ - item_id = ItemId(imdb_id) - if season is not None: - item_id = ItemId(str(season), parent_id=item_id) - if episode is not None: - item_id = ItemId(str(episode), parent_id=item_id) + with db.Session() as session: + if season is not None and episode is not None: + item = session.execute( + select(Episode).where( + (Episode.imdb_id == imdb_id) & + (Episode.season_number == season) & + (Episode.episode_number == episode) + ) + ).scalar_one_or_none() + elif season is not None: + item = session.execute( + select(Season).where( + (Season.imdb_id == imdb_id) & + (Season.season_number == season) + ) + ).scalar_one_or_none() + else: + item = session.execute( + select(MediaItem).where(MediaItem.imdb_id == imdb_id) + ).scalar_one_or_none() - item = request.app.program.media_items.get_item(item_id) - if item is None: - raise HTTPException(status_code=404, detail="Item not found") + if item is None: + raise HTTPException(status_code=404, detail="Item not found") - return {"success": True, "item": item.to_extended_dict()} + return {"success": True, "item": item.to_extended_dict()} @router.get("/incomplete") async def get_incomplete_items(request: Request): - if not hasattr(request.app, "program") or not hasattr( - request.app.program, "media_items" - ): - logger.error("Program or media_items not found in the request app") + if not hasattr(request.app, "program"): + logger.error("Program not found in the request app") raise HTTPException(status_code=500, detail="Internal server error") - incomplete_items = request.app.program.media_items.get_incomplete_items() - if not incomplete_items: - return {"success": True, "incomplete_items": []} + with db.Session() as session: + incomplete_items = session.execute( + select(MediaItem).where(MediaItem.last_state != "Completed") + ).unique().scalars().all() - return { - "success": True, - "incomplete_items": [item.to_dict() for item in incomplete_items.values()], - } + if not incomplete_items: + return {"success": True, "incomplete_items": []} + + return { + "success": True, + "incomplete_items": [item.to_dict() for item in incomplete_items], + } diff --git a/backend/controllers/metrics.py b/src/controllers/metrics.py similarity index 100% rename from backend/controllers/metrics.py rename to src/controllers/metrics.py diff --git a/backend/controllers/models/overseerr.py b/src/controllers/models/overseerr.py similarity index 100% rename from backend/controllers/models/overseerr.py rename to src/controllers/models/overseerr.py diff --git a/backend/controllers/models/plex.py b/src/controllers/models/plex.py similarity index 100% rename from backend/controllers/models/plex.py rename to src/controllers/models/plex.py diff --git a/backend/controllers/settings.py b/src/controllers/settings.py similarity index 100% rename from backend/controllers/settings.py rename to src/controllers/settings.py diff --git a/backend/controllers/tmdb.py b/src/controllers/tmdb.py similarity index 100% rename from backend/controllers/tmdb.py rename to src/controllers/tmdb.py diff --git a/backend/controllers/webhooks.py b/src/controllers/webhooks.py similarity index 100% rename from backend/controllers/webhooks.py rename to src/controllers/webhooks.py diff --git a/backend/main.py b/src/main.py similarity index 100% rename from backend/main.py rename to src/main.py diff --git a/backend/program/__init__.py b/src/program/__init__.py similarity index 65% rename from backend/program/__init__.py rename to src/program/__init__.py index c86af528..8621dc2b 100644 --- a/backend/program/__init__.py +++ b/src/program/__init__.py @@ -1,5 +1,4 @@ """Program main module""" -from program.media.container import MediaItemContainer # noqa: F401 from program.media.item import MediaItem # noqa: F401 from program.program import Event, Program # noqa: F401 diff --git a/backend/program/content/__init__.py b/src/program/content/__init__.py similarity index 100% rename from backend/program/content/__init__.py rename to src/program/content/__init__.py diff --git a/backend/program/content/listrr.py b/src/program/content/listrr.py similarity index 94% rename from backend/program/content/listrr.py rename to src/program/content/listrr.py index 6fd73933..c66b77de 100644 --- a/backend/program/content/listrr.py +++ b/src/program/content/listrr.py @@ -49,11 +49,11 @@ def validate(self) -> bool: return False try: response = ping("https://listrr.pro/", additional_headers=self.headers) - if not response.ok: + if not response.is_ok: logger.error( - f"Listrr ping failed - Status Code: {response.status_code}, Reason: {response.reason}", + f"Listrr ping failed - Status Code: {response.status_code}, Reason: {response.response.reason}", ) - return response.ok + return response.is_ok except Exception as e: logger.error(f"Listrr ping exception: {e}") return False @@ -102,4 +102,4 @@ def _get_items_from_Listrr(self, content_type, content_lists) -> list[MediaItem] logger.error(f"An error occurred: {e}") break page += 1 - return list(unique_ids) + return list(unique_ids) \ No newline at end of file diff --git a/backend/program/content/mdblist.py b/src/program/content/mdblist.py similarity index 94% rename from backend/program/content/mdblist.py rename to src/program/content/mdblist.py index 63b3ef5f..ff1e9c7d 100644 --- a/backend/program/content/mdblist.py +++ b/src/program/content/mdblist.py @@ -5,7 +5,8 @@ from program.media.item import MediaItem from program.settings.manager import settings_manager from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get, ping +from utils.request import get, ping +from utils.ratelimiter import RateLimiter, RateLimitExceeded class Mdblist: @@ -33,7 +34,7 @@ def validate(self): logger.error("Mdblist is enabled, but list is empty.") return False response = ping(f"https://mdblist.com/api/user?apikey={self.settings.api_key}") - if "Invalid API key!" in response.text: + if "Invalid API key!" in response.response.text: logger.error("Mdblist api key is invalid.") return False return True @@ -91,4 +92,4 @@ def list_items_by_url(url: str, api_key: str): url = url if url.endswith("/") else f"{url}/" url = url if url.endswith("json/") else f"{url}json/" response = get(url, params={"apikey": api_key}) - return response.data + return response.data \ No newline at end of file diff --git a/backend/program/content/overseerr.py b/src/program/content/overseerr.py similarity index 94% rename from backend/program/content/overseerr.py rename to src/program/content/overseerr.py index 7715323e..4f923ed6 100644 --- a/backend/program/content/overseerr.py +++ b/src/program/content/overseerr.py @@ -40,10 +40,10 @@ def validate(self) -> bool: ) if response.status_code >= 201: logger.error( - f"Overseerr ping failed - Status Code: {response.status_code}, Reason: {response.reason}" + f"Overseerr ping failed - Status Code: {response.status_code}, Reason: {response.response.reason}" ) return False - return response.ok + return response.is_ok except (ConnectionError, RetryError, MaxRetryError, NewConnectionError) as e: logger.error(f"Overseerr URL is not reachable, or it timed out") return False @@ -71,7 +71,7 @@ def run(self): logger.error(f"Unexpected error during fetching requests: {str(e)}") return - if not response.is_ok or response.data.pageInfo.results == 0: + if not response.is_ok or not hasattr(response.data, 'pageInfo') or getattr(response.data.pageInfo, 'results', 0) == 0: return # Lets look at approved items only that are only in the pending state @@ -223,4 +223,4 @@ def mark_completed(mediaId: int) -> bool: # 2 = PENDING, # 3 = PROCESSING, # 4 = PARTIALLY_AVAILABLE, -# 5 = AVAILABLE +# 5 = AVAILABLE \ No newline at end of file diff --git a/backend/program/content/plex_watchlist.py b/src/program/content/plex_watchlist.py similarity index 85% rename from backend/program/content/plex_watchlist.py rename to src/program/content/plex_watchlist.py index 2ea24676..b622c059 100644 --- a/backend/program/content/plex_watchlist.py +++ b/src/program/content/plex_watchlist.py @@ -34,7 +34,7 @@ def validate(self): for rss_url in self.settings.rss: try: response = ping(rss_url) - response.raise_for_status() + response.response.raise_for_status() self.rss_enabled = True return True except HTTPError as e: @@ -61,16 +61,16 @@ def run(self) -> Generator[Union[Movie, Show, Season, Episode], None, None]: new_items = watchlist_items | rss_items - items = [ - MediaItem({"imdb_id": imdb_id, "requested_by": self.key}) - for imdb_id in new_items if imdb_id not in self.recurring_items - ] for imdb_id in new_items: - if imdb_id in self.recurring_items: + if not imdb_id or imdb_id in self.recurring_items: continue self.recurring_items.add(imdb_id) + media_item = MediaItem({"imdb_id": imdb_id, "requested_by": self.key}) + if media_item: + yield media_item + else: + logger.log("NOT_FOUND", f"Failed to create media item for {imdb_id}") - yield items def _get_items_from_rss(self) -> Generator[MediaItem, None, None]: """Fetch media from Plex RSS Feeds.""" @@ -80,7 +80,12 @@ def _get_items_from_rss(self) -> Generator[MediaItem, None, None]: if not response.is_ok: logger.error(f"Failed to fetch Plex RSS feed from {rss_url}: HTTP {response.status_code}") continue - yield self._extract_imdb_ids(response.data.channel.item.guid) + if not hasattr(response.data, "items"): + logger.error("Invalid response or missing items attribute in response data.") + continue + results = response.data.items + for item in results: + yield self._extract_imdb_ids(item.guids) except Exception as e: logger.error(f"An unexpected error occurred while fetching Plex RSS feed from {rss_url}: {e}") @@ -101,6 +106,8 @@ def _get_items_from_watchlist(self) -> Generator[MediaItem, None, None]: imdb_id = self._ratingkey_to_imdbid(item.ratingKey) if imdb_id: yield imdb_id + else: + logger.log("NOT_FOUND", f"{item.title} ({item.year}) is missing ratingKey attribute from Plex") @staticmethod def _ratingkey_to_imdbid(ratingKey: str) -> str: @@ -121,4 +128,4 @@ def _extract_imdb_ids(self, guids): if guid.startswith("imdb://"): imdb_id = guid.split("//")[-1] if imdb_id: - yield imdb_id + return imdb_id \ No newline at end of file diff --git a/backend/program/content/trakt.py b/src/program/content/trakt.py similarity index 99% rename from backend/program/content/trakt.py rename to src/program/content/trakt.py index e29fc2f7..c78d7510 100644 --- a/backend/program/content/trakt.py +++ b/src/program/content/trakt.py @@ -9,7 +9,8 @@ from program.settings.manager import settings_manager from requests import RequestException from utils.logger import logger -from utils.request import RateLimiter, get, post +from utils.request import get, post +from utils.ratelimiter import RateLimiter class TraktContent: @@ -359,4 +360,4 @@ def _resolve_short_url(short_url) -> str or None: patterns: dict[str, re.Pattern] = { "user_list": re.compile(r'https://trakt.tv/users/([^/]+)/lists/([^/]+)'), "short_list": re.compile(r'https://trakt.tv/lists/\d+') -} +} \ No newline at end of file diff --git a/src/program/db/__init__.py b/src/program/db/__init__.py new file mode 100644 index 00000000..15bd15ce --- /dev/null +++ b/src/program/db/__init__.py @@ -0,0 +1 @@ +from .db import db \ No newline at end of file diff --git a/src/program/db/db.py b/src/program/db/db.py new file mode 100644 index 00000000..85dbb6af --- /dev/null +++ b/src/program/db/db.py @@ -0,0 +1,33 @@ +from sqla_wrapper import Alembic, SQLAlchemy +from program.settings.manager import settings_manager +from utils import data_dir_path + +db = SQLAlchemy(settings_manager.settings.database.host) + +script_location = data_dir_path / "alembic/" + +import os +if not os.path.exists(script_location): + os.makedirs(script_location) + +alembic = Alembic(db, script_location) +alembic.init(script_location) + +from alembic.autogenerate import compare_metadata +from alembic.runtime.migration import MigrationContext + +# https://stackoverflow.com/questions/61374525/how-do-i-check-if-alembic-migrations-need-to-be-generated +def need_upgrade_check() -> bool: + diff = [] + with db.engine.connect() as connection: + mc = MigrationContext.configure(connection) + diff = compare_metadata(mc, db.Model.metadata) + return diff != [] + +def run_migrations() -> None: + try: + if need_upgrade_check(): + alembic.revision("auto-upg") + alembic.upgrade() + except: + alembic.upgrade() \ No newline at end of file diff --git a/src/program/db/db_functions.py b/src/program/db/db_functions.py new file mode 100644 index 00000000..7e4f74c9 --- /dev/null +++ b/src/program/db/db_functions.py @@ -0,0 +1,133 @@ +import os +from program.media.item import Episode, MediaItem, Movie, Season, Show +from sqlalchemy import select, func +from sqlalchemy.orm import joinedload +from utils.logger import logger +from program.types import Event +from .db import db + + +def _ensure_item_exists_in_db(item:MediaItem) -> bool: + if isinstance(item, (Movie, Show)): + with db.Session() as session: + return session.execute(select(func.count(MediaItem._id)).where(MediaItem.imdb_id==item.imdb_id)).scalar_one() != 0 + return item._id is not None + +def _get_item_type_from_db(item: MediaItem) -> str: + with db.Session() as session: + if item._id is None: + return session.execute(select(MediaItem.type).where( (MediaItem.imdb_id==item.imdb_id ) & ( (MediaItem.type == 'show') | (MediaItem.type == 'movie') ) )).scalar_one() + return session.execute(select(MediaItem.type).where(MediaItem._id==item._id)).scalar_one() + +def _store_item(item: MediaItem): + if isinstance(item, (Movie, Show, Season, Episode)) and item._id is not None: + with db.Session() as session: + session.merge(item) + session.commit() + else: + with db.Session() as session: + _check_for_and_run_insertion_required(session, item) + +def _get_item_from_db(session, item: MediaItem): + if not _ensure_item_exists_in_db(item): + return None + type = _get_item_type_from_db(item) + session.expire_on_commit = False + match type: + case "movie": + r = session.execute(select(Movie).where(MediaItem.imdb_id==item.imdb_id).options(joinedload("*"))).unique().scalar_one() + r.set("streams", item.get("streams", {})) + return r + case "show": + r = session.execute(select(Show).where(MediaItem.imdb_id==item.imdb_id).options(joinedload("*"))).unique().scalar_one() + r.set("streams", item.get("streams", {})) + return r + case "season": + r = session.execute(select(Season).where(Season._id==item._id).options(joinedload("*"))).unique().scalar_one() + r.set("streams", item.get("streams", {})) + return r + case "episode": + r = session.execute(select(Episode).where(Episode._id==item._id).options(joinedload("*"))).unique().scalar_one() + r.set("streams", item.get("streams", {})) + return r + case _: + logger.error(f"_get_item_from_db Failed to create item from type: {type}") + return None + +def _check_for_and_run_insertion_required(session, item: MediaItem) -> None: + if _ensure_item_exists_in_db(item) == False: + if isinstance(item, (Show, Movie, Season, Episode)): + item.store_state() + session.add(item) + session.commit() + logger.log("PROGRAM", f"{item.log_string} Inserted into the database.") + return True + return False + +def _run_thread_with_db_item(fn, service, program, input_item: MediaItem | None): + if input_item is not None: + with db.Session() as session: + if isinstance(input_item, (Movie, Show, Season, Episode)): + item = input_item + if not _check_for_and_run_insertion_required(session, item): + pass + item = _get_item_from_db(session, item) + + #session.merge(item) + for res in fn(item): + if isinstance(res, list): + all_media_items = True + for i in res: + if not isinstance(i, MediaItem): + all_media_items = False + + program._remove_from_running_items(item, service.__name__) + if all_media_items == True: + for i in res: + program._push_event_queue(Event(emitted_by="_run_thread_with_db_item", item=i)) + session.commit() + return + elif not isinstance(res, MediaItem): + logger.log("PROGRAM", f"Service {service.__name__} emitted {res} from input item {item} of type {type(res).__name__}, backing off.") + program._remove_from_running_items(item, service.__name__) + if res is not None and isinstance(res, MediaItem): + program._push_event_queue(Event(emitted_by=service, item=res)) + # self._check_for_and_run_insertion_required(item) + + item.store_state() + session.commit() + + session.expunge_all() + return res + for i in fn(input_item): + if isinstance(i, (Show, Movie, Season, Episode)): + with db.Session() as session: + _check_for_and_run_insertion_required(session, i) + program._push_event_queue(Event(emitted_by=service, item=i)) + yield i + return + else: + for i in fn(): + if isinstance(i, (Show, Movie, Season, Episode)): + with db.Session() as session: + _check_for_and_run_insertion_required(session, i) + program._push_event_queue(Event(emitted_by=service, item=i)) + else: + program._push_event_queue(Event(emitted_by=service, item=i)) + return + +reset = os.getenv("HARD_RESET", None) +if reset is not None and reset.lower() in ["true","1"]: + print("Hard reset detected, dropping all tables") # Logging isn't initialized here yet. + def run_delete(_type): + with db.Session() as session: + all = session.execute(select(_type).options(joinedload("*"))).unique().scalars().all() + for i in all: + session.delete(i) + session.commit() + run_delete(Episode) + run_delete(Season) + run_delete(Show) + run_delete(Movie) + run_delete(MediaItem) + diff --git a/src/program/downloaders/__init__.py b/src/program/downloaders/__init__.py new file mode 100644 index 00000000..fe060767 --- /dev/null +++ b/src/program/downloaders/__init__.py @@ -0,0 +1,29 @@ +from .realdebrid import RealDebridDownloader +from .alldebrid import AllDebridDownloader +from .torbox import TorBoxDownloader +from program.media.item import MediaItem +from utils.logger import logger + + +class Downloader: + def __init__(self): + self.key = "downloader" + self.initialized = False + self.services = { + RealDebridDownloader: RealDebridDownloader(), + TorBoxDownloader: TorBoxDownloader(), + AllDebridDownloader: AllDebridDownloader(), + } + self.initialized = self.validate() + + def validate(self): + initialized_services = [service for service in self.services.values() if service.initialized] + if len(initialized_services) > 1: + logger.error("More than one downloader service is initialized. Only one downloader can be initialized at a time.") + return False + return len(initialized_services) == 1 + + def run(self, item: MediaItem): + for service in self.services.values(): + if service.initialized: + return service.run(item) \ No newline at end of file diff --git a/src/program/downloaders/alldebrid.py b/src/program/downloaders/alldebrid.py new file mode 100644 index 00000000..f0cbc45a --- /dev/null +++ b/src/program/downloaders/alldebrid.py @@ -0,0 +1,653 @@ +import contextlib +import time +from datetime import datetime +from os.path import splitext +from pathlib import Path +from types import SimpleNamespace +from typing import Generator, List + +from program.media.item import Episode, MediaItem, Movie, Season, Show +from program.media.state import States +from program.settings.manager import settings_manager +from requests import ConnectTimeout +from RTN.exceptions import GarbageTorrent +from RTN.parser import parse +from RTN.patterns import extract_episodes +from utils.logger import logger +from utils.request import get, ping, post +from utils.ratelimiter import RateLimiter + +WANTED_FORMATS = {".mkv", ".mp4", ".avi"} +AD_BASE_URL = "https://api.alldebrid.com/v4" +AD_AGENT = "Riven" +AD_PARAM_AGENT = f"agent={AD_AGENT}" + +class AllDebridDownloader: + """All-Debrid API Wrapper""" + + def __init__(self): + self.rate_limiter = None + self.key = "alldebrid" + self.settings = settings_manager.settings.downloaders.all_debrid + self.download_settings = settings_manager.settings.downloaders + self.auth_headers = {"Authorization": f"Bearer {self.settings.api_key}"} + self.proxy = self.settings.proxy_url if self.settings.proxy_enabled else None + self.inner_rate_limit = RateLimiter(12, 1) # 12 requests per second + self.overall_rate_limiter = RateLimiter(600, 60) # 600 requests per minute + self.initialized = self.validate() + if not self.initialized: + return + logger.success("AllDebrid initialized!") + + def validate(self) -> bool: + """Validate All-Debrid settings and API key""" + if not self.settings.enabled: + logger.warning("All-Debrid is set to disabled") + return False + if not self.settings.api_key: + logger.warning("All-Debrid API key is not set") + return False + if not isinstance(self.download_settings.movie_filesize_min, int) or self.download_settings.movie_filesize_min < -1: + logger.error("All-Debrid movie filesize min is not set or invalid.") + return False + if not isinstance(self.download_settings.movie_filesize_max, int) or self.download_settings.movie_filesize_max < -1: + logger.error("All-Debrid movie filesize max is not set or invalid.") + return False + if not isinstance(self.download_settings.episode_filesize_min, int) or self.download_settings.episode_filesize_min < -1: + logger.error("All-Debrid episode filesize min is not set or invalid.") + return False + if not isinstance(self.download_settings.episode_filesize_max, int) or self.download_settings.episode_filesize_max < -1: + logger.error("All-Debrid episode filesize max is not set or invalid.") + return False + if self.settings.proxy_enabled and not self.settings.proxy_url: + logger.error("Proxy is enabled but no proxy URL is provided.") + return False + try: + response = ping( + f"{AD_BASE_URL}/user?{AD_PARAM_AGENT}", + additional_headers=self.auth_headers, + proxies=self.proxy, + specific_rate_limiter=self.inner_rate_limit, + overall_rate_limiter=self.overall_rate_limiter) + if response.is_ok: + user_info = response.data.data.user + expiration = user_info.premiumUntil or 0 + expiration_datetime = datetime.utcfromtimestamp(expiration) + time_left = expiration_datetime - datetime.utcnow() + days_left = time_left.days + hours_left, minutes_left = divmod(time_left.seconds // 3600, 60) + expiration_message = "" + + if days_left > 0: + expiration_message = f"Your account expires in {days_left} days." + elif hours_left > 0: + expiration_message = f"Your account expires in {hours_left} hours and {minutes_left} minutes." + else: + expiration_message = "Your account expires soon." + + if not user_info.isPremium or False: + logger.error("You are not a premium member.") + return False + else: + logger.log("DEBRID", expiration_message) + + return user_info.isPremium or False + except ConnectTimeout: + logger.error("Connection to All-Debrid timed out.") + except Exception as e: + logger.exception(f"Failed to validate All-Debrid settings: {e}") + return False + + def run(self, item: MediaItem) -> Generator[MediaItem, None, None]: + """Download media item from all-debrid.com""" + if (item.file and item.folder): + yield None + return + if not self.is_cached(item): + if isinstance(item, Season): + res = [e for e in item.episodes] + yield res + return + if isinstance(item, Show): + res = [s for s in item.seasons] + yield res + return + yield None + return + if not self._is_downloaded(item): + self._download_item(item) + self.log_item(item) + yield item + + @staticmethod + def log_item(item: MediaItem) -> None: + """Log only the files downloaded for the item based on its type.""" + if isinstance(item, Movie): + if item.file and item.folder: + logger.log("DEBRID", f"Downloaded {item.log_string} with file: {item.file}") + else: + logger.debug(f"Movie item missing file or folder: {item.log_string}") + elif isinstance(item, Episode): + if item.file and item.folder: + logger.log("DEBRID", f"Downloaded {item.log_string} with file: {item.file}") + else: + logger.debug(f"Episode item missing file or folder: {item.log_string}") + elif isinstance(item, Season): + for episode in item.episodes: + if episode.file and episode.folder: + logger.log("DEBRID", f"Downloaded {episode.log_string} with file: {episode.file}") + elif not episode.file: + logger.debug(f"Episode item missing file: {episode.log_string}") + elif not episode.folder: + logger.debug(f"Episode item missing folder: {episode.log_string}") + elif isinstance(item, Show): + for season in item.seasons: + for episode in season.episodes: + if episode.file and episode.folder: + logger.log("DEBRID", f"Downloaded {episode.log_string} with file: {episode.file}") + elif not episode.file: + logger.debug(f"Episode item missing file or folder: {episode.log_string}") + elif not episode.folder: + logger.debug(f"Episode item missing folder: {episode.log_string}") + else: + logger.debug(f"Unknown item type: {item.log_string}") + + def is_cached(self, item: MediaItem) -> bool: + """Check if item is cached on all-debrid.com""" + if not item.get("streams", {}): + return False + + def _chunked(lst: List, n: int) -> Generator[List, None, None]: + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(lst), n): + yield lst[i:i + n] + + logger.log("DEBRID", f"Processing {len(item.streams)} streams for {item.log_string}") + + processed_stream_hashes = set() + filtered_streams = [hash for hash in item.streams if hash and hash not in processed_stream_hashes] + if not filtered_streams: + logger.log("NOT_FOUND", f"No streams found from filtering: {item.log_string}") + return False + + for stream_chunk in _chunked(filtered_streams, 5): + try: + params = {'agent': AD_AGENT} + for i, magnet in enumerate(stream_chunk): + params[f'magnets[{i}]'] = magnet + + response = get(f"{AD_BASE_URL}/magnet/instant", params=params, additional_headers=self.auth_headers, proxies=self.proxy, response_type=dict, specific_rate_limiter=self.inner_rate_limit, overall_rate_limiter=self.overall_rate_limiter) + if response.is_ok and self._evaluate_stream_response(response.data, processed_stream_hashes, item): + return True + except Exception as e: + logger.error(f"Error checking cache for streams: {str(e)}", exc_info=True) + + item.set("streams", {}) + logger.log("NOT_FOUND", f"No wanted cached streams found for {item.log_string} out of {len(filtered_streams)}") + return False + + def _evaluate_stream_response(self, data, processed_stream_hashes, item): + """Evaluate the response data from the stream availability check.""" + if data.get("status") != "success": + logger.error("Failed to get a successful response") + return False + + magnets = data.get("data", {}).get("magnets", []) + for magnet in magnets: + stream_hash = magnet.get("hash") + if not stream_hash or stream_hash in processed_stream_hashes: + continue + + if not magnet.get("instant", False): + continue + + processed_stream_hashes.add(stream_hash) + if self._process_providers(item, magnet, stream_hash): + return True + return False + + def _process_providers(self, item: MediaItem, magnet: dict, stream_hash: str) -> bool: + """Process providers for an item""" + if not magnet or not stream_hash: + return False + + sorted_files = sorted( + (file for file in magnet.get("files", [])), + key=lambda file: file.get("s", 0), + reverse=True + ) + + if isinstance(item, Movie): + for file in sorted_files: + if self._is_wanted_movie(file, item): + item.set("active_stream", {"hash": stream_hash, "files": magnet["files"], "id": None}) + return True + elif isinstance(item, Show): + for file in sorted_files: + if self._is_wanted_show(file, item): + item.set("active_stream", {"hash": stream_hash, "files": magnet["files"], "id": None}) + return True + elif isinstance(item, Season): + other_containers = [ + s for s in item.parent.seasons + if s != item and s.active_stream + and s.state not in (States.Indexed, States.Unknown) + ] + for c in other_containers: + if self._is_wanted_season(c.active_stream["files"], item): + item.set("active_stream", {"hash": c.active_stream["hash"], "files": c.active_stream["files"], "id": None}) + return True + for file in sorted_files: + if self._is_wanted_season(file, item): + item.set("active_stream", {"hash": stream_hash, "files": magnet["files"], "id": None}) + return True + elif isinstance(item, Episode): + for file in sorted_files: + if self._is_wanted_episode(file, item): + item.set("active_stream", {"hash": stream_hash, "files": magnet["files"], "id": None}) + return True + return False + + def _is_wanted_movie(self, file: dict, item: Movie) -> bool: + """Check if file is wanted for a movie""" + if not isinstance(item, Movie): + logger.error(f"Item is not a Movie instance: {item.log_string}") + return False + + min_size = self.download_settings.movie_filesize_min * 1_000_000 + max_size = self.download_settings.movie_filesize_max * 1_000_000 if self.download_settings.movie_filesize_max != -1 else float('inf') + + if not isinstance(file, dict) or file.get("s", 0) < min_size or file.get("s", 0) > max_size or splitext(file.get("n", "").lower())[1] not in WANTED_FORMATS: + return False + + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["n"], remove_trash=True) + if parsed_file and parsed_file.type == "movie": + item.set("folder", item.active_stream.get("name")) + item.set("alternative_folder", item.active_stream.get("alternative_name", None)) + item.set("file", file["n"]) + return True + return False + + def _is_wanted_episode(self, file: dict, item: Episode) -> bool: + """Check if file is wanted for an episode""" + if not isinstance(item, Episode): + logger.error(f"Item is not an Episode instance: {item.log_string}") + return False + + min_size = self.download_settings.episode_filesize_min * 1_000_000 + max_size = self.download_settings.episode_filesize_max * 1_000_000 if self.download_settings.episode_filesize_max != -1 else float('inf') + + if not isinstance(file, dict) or file.get("s", 0) < min_size or file.get("s", 0) > max_size or splitext(file.get("n", "").lower())[1] not in WANTED_FORMATS: + return False + + one_season = len(item.parent.parent.seasons) == 1 + + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["n"], remove_trash=True) + if parsed_file and item.number in parsed_file.episode and (item.parent.number in parsed_file.season or one_season): + item.set("folder", item.active_stream.get("name")) + item.set("alternative_folder", item.active_stream.get("alternative_name")) + item.set("file", file["n"]) + return True + return False + + def _is_wanted_season(self, files: list, item: Season) -> bool: + """Check if files are wanted for a season""" + if not isinstance(item, Season): + logger.error(f"Item is not a Season instance: {item.log_string}") + return False + + min_size = self.download_settings.episode_filesize_min * 1_000_000 + max_size = self.download_settings.episode_filesize_max * 1_000_000 if self.download_settings.episode_filesize_max != -1 else float('inf') + + filenames = [ + file for file in files + if isinstance(file, dict) and file.get("s", 0) > min_size + and file.get("s", 0) < max_size + and splitext(file.get("n", "").lower())[1] in WANTED_FORMATS + ] + + if not filenames: + return False + + needed_episodes = {episode.number: episode for episode in item.episodes if episode.state in [States.Indexed, States.Scraped, States.Unknown, States.Failed]} + one_season = len(item.parent.seasons) == 1 + + matched_files = {} + season_num = item.number + + for file in filenames: + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["n"], remove_trash=True) + if parsed_file and (season_num in parsed_file.season or one_season): + for ep_num in parsed_file.episode: + if ep_num in needed_episodes: + matched_files[ep_num] = file["n"] + + if not matched_files: + return False + + if needed_episodes.keys() == matched_files.keys(): + for ep_num, filename in matched_files.items(): + ep = needed_episodes[ep_num] + ep.set("folder", item.active_stream.get("name")) + ep.set("alternative_folder", item.active_stream.get("alternative_name")) + ep.set("file", filename) + return True + return False + + def _is_wanted_show(self, files: list, item: Show) -> bool: + """Check if files are wanted for a show""" + if not isinstance(item, Show): + logger.error(f"Item is not a Show instance: {item.log_string}") + return False + + min_size = self.download_settings.episode_filesize_min * 1_000_000 + max_size = self.download_settings.episode_filesize_max * 1_000_000 if self.download_settings.episode_filesize_max != -1 else float('inf') + + filenames = [ + file for file in files + if isinstance(file, dict) and file.get("s", 0) > min_size + and file.get("s", 0) < max_size + and splitext(file.get("n", "").lower())[1] in WANTED_FORMATS + ] + + if not filenames: + return False + + needed_episodes = {} + acceptable_states = [States.Indexed, States.Scraped, States.Unknown, States.Failed] + + for season in item.seasons: + if season.state in acceptable_states and season.is_released_nolog: + needed_episode_numbers = {episode.number for episode in season.episodes if episode.state in acceptable_states and episode.is_released_nolog} + if needed_episode_numbers: + needed_episodes[season.number] = needed_episode_numbers + if not needed_episodes: + return False + + matched_files = {} + for file in filenames: + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["n"], remove_trash=True) + if parsed_file: + for season_number, episodes in needed_episodes.items(): + if season_number in parsed_file.season: + for episode_number in list(episodes): + if episode_number in parsed_file.episode: + matched_files[(season_number, episode_number)] = file + episodes.remove(episode_number) + + if not matched_files: + return False + + all_found = all(len(episodes) == 0 for episodes in needed_episodes.values()) + + if all_found: + for (season_number, episode_number), file in matched_files.items(): + season = next(season for season in item.seasons if season.number == season_number) + episode = next(episode for episode in season.episodes if episode.number == episode_number) + episode.set("folder", item.active_stream.get("name")) + episode.set("alternative_folder", item.active_stream.get("alternative_name", None)) + episode.set("file", file["n"]) + return True + return False + + def _is_downloaded(self, item: MediaItem) -> bool: + """Check if item is already downloaded after checking if it was cached""" + hash_key = item.active_stream.get("hash", None) + if not hash_key: + logger.log("DEBRID", f"Item missing hash, skipping check: {item.log_string}") + return False + + logger.debug(f"Checking if torrent is already downloaded for item: {item.log_string}") + torrent = self.get_torrent(hash_key) + + if not torrent: + logger.debug(f"No matching torrent found for hash: {hash_key}") + return False + + if item.active_stream.get("id", None): + logger.debug(f"Item already has an active stream ID: {item.active_stream.get('id')}") + return True + + info = self.get_torrent_info(torrent.id) + if not info or not hasattr(info, "links"): + logger.debug(f"Failed to get torrent info for ID: {torrent.id}") + return False + + if not self._matches_item(info, item): + return False + + # Cache this as downloaded + logger.debug(f"Marking torrent as downloaded for hash: {torrent.hash}") + item.set("active_stream.id", torrent.id) + self.set_active_files(item) + logger.debug(f"Set active files for item: {item.log_string} with {len(item.active_stream.get('files', {}))} total files") + return True + + def _download_item(self, item: MediaItem): + """Download item from all-debrid.com""" + logger.debug(f"Starting download for item: {item.log_string}") + request_id = self.add_magnet(item) + logger.debug(f"Magnet added to All-Debrid, request ID: {request_id} for {item.log_string}") + item.set("active_stream.id", request_id) + self.set_active_files(item) + logger.debug(f"Active files set for item: {item.log_string} with {len(item.active_stream.get('files', {}))} total files") + time.sleep(0.5) + logger.debug(f"Item marked as downloaded: {item.log_string}") + + def set_active_files(self, item: MediaItem) -> None: + """Set active files for item from all-debrid.com""" + active_stream = item.get("active_stream") + if not active_stream or "id" not in active_stream: + logger.error(f"Invalid active stream data for item: {item.log_string}") + return + + info = self.get_torrent_info(active_stream["id"]) + magnet_info = info.data.magnets + if not info or not magnet_info or not magnet_info.filename: + logger.error(f"Failed to get torrent info for item: {item.log_string}") + return + + item.active_stream["alternative_name"] = magnet_info.filename + item.active_stream["name"] = magnet_info.filename + + if not item.folder or not item.alternative_folder: + item.set("folder", item.active_stream.get("name")) + item.set("alternative_folder", item.active_stream.get("alternative_name")) + + # Ensure that the folder and file attributes are set + if isinstance(item, (Movie, Episode)): + if not item.file: + for link in magnet_info.links: + if hasattr(link, "files"): + for file in link.files: + if isinstance(file, SimpleNamespace) and hasattr(file, "e"): + for subfile in file.e: + if isinstance(item, Movie) and self._is_wanted_movie(subfile, item): + item.set("file", subfile.n) + break + elif isinstance(item, Episode) and self._is_wanted_episode(subfile, item): + item.set("file", subfile.n) + break + if not item.folder or not item.alternative_folder or not item.file: + logger.error(f"Missing folder or alternative_folder or file for item: {item.log_string}") + return + + if isinstance(item, Season) and item.folder: + for episode in item.episodes: + if episode.file and not episode.folder: + episode.set("folder", item.folder) + + if isinstance(item, Show) and item.folder: + for season in item.seasons: + for episode in season.episodes: + if episode.file and not episode.folder: + episode.set("folder", item.folder) + + # Handle nested files in the links + for link in magnet_info.links: + if hasattr(link, "files"): + for file in link.files: + if isinstance(file, SimpleNamespace) and hasattr(file, "e"): + for subfile in file.e: + if isinstance(item, Season) and self._is_wanted_season(link.files, item): + break + elif isinstance(item, Show) and self._is_wanted_show(link.files, item): + break + + if isinstance(item, Season) and item.folder: + for episode in item.episodes: + if episode.file and not episode.folder: + episode.set("folder", item.folder) + + if isinstance(item, Show) and item.folder: + for season in item.seasons: + for episode in season.episodes: + if episode.file and not episode.folder: + episode.set("folder", item.folder) + + # Handle nested files in the links + for link in magnet_info.links: + if hasattr(link, "files"): + for file in link.files: + if isinstance(file, SimpleNamespace) and hasattr(file, "e"): + for subfile in file.e: + if isinstance(item, Season) and self._is_wanted_season(link.files, item): + break + elif isinstance(item, Show) and self._is_wanted_show(link.files, item): + break + + ### API Methods for All-Debrid below + def add_magnet(self, item: MediaItem) -> str: + """Add magnet link to All-Debrid""" + if not item.active_stream.get("hash"): + logger.error(f"No active stream or hash found for {item.log_string}") + return None + + try: + hash = item.active_stream.get("hash") + params = {"agent": AD_AGENT} + params[f'magnets[0]'] = hash + response = post( + f"{AD_BASE_URL}/magnet/upload", + params=params, + additional_headers=self.auth_headers, + proxies=self.proxy, + specific_rate_limiter=self.inner_rate_limit, + overall_rate_limiter=self.overall_rate_limiter + ) + if response.is_ok: + data = response.data.data + magnets = data.magnets or [] + if magnets: + return magnets[0].id + logger.error(f"Failed to add magnet: {response.data}") + except Exception as e: + logger.error(f"Error adding magnet for {item.log_string}: {e}") + return None + + def get_torrent_info(self, request_id: str) -> SimpleNamespace: + """Get torrent info from All-Debrid""" + if not request_id: + logger.error("No request ID found") + return SimpleNamespace() + + try: + response = get( + f"{AD_BASE_URL}/magnet/status?{AD_PARAM_AGENT}&id={request_id}", + additional_headers=self.auth_headers, + proxies=self.proxy, + specific_rate_limiter=self.inner_rate_limit, + overall_rate_limiter=self.overall_rate_limiter + ) + if response.is_ok: + return response.data + except Exception as e: + logger.error(f"Error getting torrent info for {request_id or 'UNKNOWN'}: {e}") + return SimpleNamespace() + + def get_torrent(self, hash_key: str) -> dict[str, SimpleNamespace]: + """Get torrents from All-Debrid""" + try: + response = get( + f"{AD_BASE_URL}/magnet/status?{AD_PARAM_AGENT}&id={hash_key}", + additional_headers=self.auth_headers, + proxies=self.proxy, + specific_rate_limiter=self.inner_rate_limit, + overall_rate_limiter=self.overall_rate_limiter + ) + if response.is_ok and response.data: + magnets = getattr(response.data, "magnets", []) + return {magnet.hash: SimpleNamespace(**magnet) for magnet in magnets} + except Exception as e: + logger.error(f"Error getting torrents from All-Debrid: {e}") + return {} + + def _matches_item(torrent_info: SimpleNamespace, item: MediaItem) -> bool: + """Check if the downloaded torrent matches the item specifics.""" + logger.debug(f"Checking if torrent matches item: {item.log_string}") + + if not hasattr(torrent_info, "files"): + logger.error(f"Torrent info for {item.log_string} does not have files attribute: {torrent_info}") + return False + + def check_movie(): + for file in torrent_info.files: + if file["selected"] == 1 and file["size"] > 200_000_000: + file_size_mb = file["size"] / (1024 * 1024) + if file_size_mb >= 1024: + file_size_gb = file_size_mb / 1024 + logger.debug(f"Selected file: {Path(file['path']).name} with size: {file_size_gb:.2f} GB") + else: + logger.debug(f"Selected file: {Path(file['path']).name} with size: {file_size_mb:.2f} MB") + return True + return False + + def check_episode(): + one_season = len(item.parent.parent.seasons) == 1 + item_number = item.number + parent_number = item.parent.number + for file in torrent_info.files: + if file["selected"] == 1: + file_episodes = extract_episodes(Path(file["path"]).name) + if (item_number in file_episodes and parent_number in file_episodes) or (one_season and item_number in file_episodes): + logger.debug(f"File {Path(file['path']).name} selected for episode {item_number} in season {parent_number}") + return True + return False + + def check_season(season): + season_number = season.number + episodes_in_season = {episode.number for episode in season.episodes} + matched_episodes = set() + one_season = len(season.parent.seasons) == 1 + for file in torrent_info.files: + if file["selected"] == 1: + file_episodes = extract_episodes(Path(file["path"]).name) + if season_number in file_episodes: + matched_episodes.update(file_episodes) + elif one_season and file_episodes: + matched_episodes.update(file_episodes) + return len(matched_episodes) >= len(episodes_in_season) // 2 + + if isinstance(item, Movie): + if check_movie(): + logger.info(f"{item.log_string} already exists in All-Debrid account.") + return True + elif isinstance(item, Show): + if all(check_season(season) for season in item.seasons): + logger.info(f"{item.log_string} already exists in All-Debrid account.") + return True + elif isinstance(item, Season): + if check_season(item): + logger.info(f"{item.log_string} already exists in All-Debrid account.") + return True + elif isinstance(item, Episode): + if check_episode(): + logger.info(f"{item.log_string} already exists in All-Debrid account.") + return True + + logger.debug(f"No matching item found for {item.log_string}") + return False \ No newline at end of file diff --git a/backend/program/downloaders/realdebrid.py b/src/program/downloaders/realdebrid.py similarity index 79% rename from backend/program/downloaders/realdebrid.py rename to src/program/downloaders/realdebrid.py index 648d1cce..7eef22e0 100644 --- a/backend/program/downloaders/realdebrid.py +++ b/src/program/downloaders/realdebrid.py @@ -17,24 +17,27 @@ from RTN.patterns import extract_episodes from utils.logger import logger from utils.request import get, ping, post +from utils.ratelimiter import RateLimiter WANTED_FORMATS = {".mkv", ".mp4", ".avi"} RD_BASE_URL = "https://api.real-debrid.com/rest/1.0" -class Debrid: +class RealDebridDownloader: """Real-Debrid API Wrapper""" - def __init__(self, hash_cache): + def __init__(self): + self.rate_limiter = None self.key = "realdebrid" self.settings = settings_manager.settings.downloaders.real_debrid self.download_settings = settings_manager.settings.downloaders self.auth_headers = {"Authorization": f"Bearer {self.settings.api_key}"} self.proxy = self.settings.proxy_url if self.settings.proxy_enabled else None + self.torrents_rate_limiter = RateLimiter(1, 1) + self.overall_rate_limiter = RateLimiter(60, 60) self.initialized = self.validate() if not self.initialized: return - self.hash_cache = hash_cache logger.success("Real Debrid initialized!") def validate(self) -> bool: @@ -61,9 +64,13 @@ def validate(self) -> bool: logger.error("Proxy is enabled but no proxy URL is provided.") return False try: - response = ping(f"{RD_BASE_URL}/user", additional_headers=self.auth_headers, proxies=self.proxy) - if response.ok: - user_info = response.json() + response = ping( + f"{RD_BASE_URL}/user", + additional_headers=self.auth_headers, + proxies=self.proxy, + overall_rate_limiter=self.overall_rate_limiter) + if response.is_ok: + user_info = response.response.json() expiration = user_info.get("expiration", "") expiration_datetime = datetime.fromisoformat(expiration.replace('Z', '+00:00')).replace(tzinfo=None) time_left = expiration_datetime - datetime.utcnow().replace(tzinfo=None) @@ -119,30 +126,18 @@ def log_item(item: MediaItem) -> None: if isinstance(item, Movie): if item.file and item.folder: logger.log("DEBRID", f"Downloaded {item.log_string} with file: {item.file}") - else: - logger.debug(f"Movie item missing file or folder: {item.log_string}") elif isinstance(item, Episode): if item.file and item.folder: logger.log("DEBRID", f"Downloaded {item.log_string} with file: {item.file}") - else: - logger.debug(f"Episode item missing file or folder: {item.log_string}") elif isinstance(item, Season): for episode in item.episodes: if episode.file and episode.folder: logger.log("DEBRID", f"Downloaded {episode.log_string} with file: {episode.file}") - elif not episode.file: - logger.debug(f"Episode item missing file: {episode.log_string}") - elif not episode.folder: - logger.debug(f"Episode item missing folder: {episode.log_string}") elif isinstance(item, Show): for season in item.seasons: for episode in season.episodes: if episode.file and episode.folder: logger.log("DEBRID", f"Downloaded {episode.log_string} with file: {episode.file}") - elif not episode.file: - logger.debug(f"Episode item missing file or folder: {episode.log_string}") - elif not episode.folder: - logger.debug(f"Episode item missing folder: {episode.log_string}") else: logger.debug(f"Unknown item type: {item.log_string}") @@ -167,11 +162,14 @@ def _chunked(lst: List, n: int) -> Generator[List, None, None]: for stream_chunk in _chunked(filtered_streams, 5): streams = "/".join(stream_chunk) try: - response = get(f"{RD_BASE_URL}/torrents/instantAvailability/{streams}/", additional_headers=self.auth_headers, proxies=self.proxy, response_type=dict) - if response.is_ok and self._evaluate_stream_response(response.data, processed_stream_hashes, item): - return True + response = get(f"{RD_BASE_URL}/torrents/instantAvailability/{streams}/", additional_headers=self.auth_headers, proxies=self.proxy, response_type=dict, specific_rate_limiter=self.torrents_rate_limiter, overall_rate_limiter=self.overall_rate_limiter) + if response.is_ok and response.data and isinstance(response.data, dict): + if self._evaluate_stream_response(response.data, processed_stream_hashes, item): + item.set("streams", {}) + return True except Exception as e: - logger.error(f"Error checking cache for streams: {str(e)}", exc_info=True) + logger.exception(f"Error checking cache for streams: {str(e)}", exc_info=True) + continue item.set("streams", {}) logger.log("NOT_FOUND", f"No wanted cached streams found for {item.log_string} out of {len(filtered_streams)}") @@ -180,15 +178,14 @@ def _chunked(lst: List, n: int) -> Generator[List, None, None]: def _evaluate_stream_response(self, data, processed_stream_hashes, item): """Evaluate the response data from the stream availability check.""" for stream_hash, provider_list in data.items(): - if stream_hash in processed_stream_hashes or self.hash_cache.is_blacklisted(stream_hash): + if stream_hash in processed_stream_hashes: continue if not provider_list or not provider_list.get("rd"): - self.hash_cache.blacklist(stream_hash) continue processed_stream_hashes.add(stream_hash) if self._process_providers(item, provider_list, stream_hash): + logger.debug(f"Finished processing providers - selecting {stream_hash} for downloading") return True - self.hash_cache.blacklist(stream_hash) return False def _process_providers(self, item: MediaItem, provider_list: dict, stream_hash: str) -> bool: @@ -206,30 +203,35 @@ def _process_providers(self, item: MediaItem, provider_list: dict, stream_hash: for container in sorted_containers: if self._is_wanted_movie(container, item): item.set("active_stream", {"hash": stream_hash, "files": container, "id": None}) + logger.debug(f"Found wanted files for {item.log_string} in {stream_hash}") return True elif isinstance(item, Show): for container in sorted_containers: if self._is_wanted_show(container, item): item.set("active_stream", {"hash": stream_hash, "files": container, "id": None}) + logger.debug(f"Found wanted files for {item.log_string} in {stream_hash}") return True elif isinstance(item, Season): other_containers = [ - s for s in item.parent.seasons + s for s in item.parent.seasons if s != item and s.active_stream and s.state not in (States.Indexed, States.Unknown) ] for c in other_containers: if self._is_wanted_season(c.active_stream["files"], item): item.set("active_stream", {"hash": c.active_stream["hash"], "files": c.active_stream["files"], "id": None}) + logger.debug(f"Found wanted files for {item.log_string} in {c.active_stream['hash']}") return True for container in sorted_containers: if self._is_wanted_season(container, item): item.set("active_stream", {"hash": stream_hash, "files": container, "id": None}) + logger.debug(f"Found wanted files for {item.log_string} in {stream_hash}") return True elif isinstance(item, Episode): for container in sorted_containers: if self._is_wanted_episode(container, item): item.set("active_stream", {"hash": stream_hash, "files": container, "id": None}) + logger.debug(f"Found wanted files for {item.log_string} in {stream_hash}") return True # False if no cached files in containers (provider_list) return False @@ -244,7 +246,7 @@ def _is_wanted_movie(self, container: dict, item: Movie) -> bool: max_size = self.download_settings.movie_filesize_max * 1_000_000 if self.download_settings.movie_filesize_max != -1 else float('inf') filenames = sorted( - (file for file in container.values() if file and file["filesize"] > min_size + (file for file in container.values() if file and file["filesize"] > min_size and file["filesize"] < max_size and splitext(file["filename"].lower())[1] in WANTED_FORMATS), key=lambda file: file["filesize"], reverse=True @@ -254,17 +256,16 @@ def _is_wanted_movie(self, container: dict, item: Movie) -> bool: return False for file in filenames: - if not file or not file.get("filename"): + if not file or "sample" in file["filename"].lower(): continue with contextlib.suppress(GarbageTorrent, TypeError): parsed_file = parse(file["filename"], remove_trash=True) if not parsed_file or not parsed_file.parsed_title: continue - if parsed_file.type == "movie": - item.set("folder", item.active_stream.get("name")) - item.set("alternative_folder", item.active_stream.get("alternative_name", None)) - item.set("file", file["filename"]) - return True + item.set("folder", item.active_stream.get("name")) + item.set("alternative_folder", item.active_stream.get("alternative_name", None)) + item.set("file", file["filename"]) + return True return False def _is_wanted_episode(self, container: dict, item: Episode) -> bool: @@ -327,41 +328,41 @@ def _is_wanted_season(self, container: dict, item: Season) -> bool: if not filenames: return False - needed_episodes = {episode.number: episode for episode in item.episodes if episode.state in [States.Indexed, States.Scraped, States.Unknown, States.Failed]} - one_season = len(item.parent.seasons) == 1 + acceptable_states = [States.Indexed, States.Scraped, States.Unknown, States.Failed, States.PartiallyCompleted] + + needed_episodes = [] + for episode in item.episodes: + if episode.state in acceptable_states and episode.is_released_nolog: + needed_episodes.append(episode.number) + + if not needed_episodes: + return False # Dictionary to hold the matched files for each episode matched_files = {} - season_num = item.number + one_season = len(item.parent.seasons) == 1 # Parse files once and assign to episodes for file in filenames: - if not file or not file.get("filename"): - continue with contextlib.suppress(GarbageTorrent, TypeError): parsed_file = parse(file["filename"], remove_trash=True) if not parsed_file or not parsed_file.episode or 0 in parsed_file.season: continue - # Check if the file's season matches the item's season or if there's only one season - if season_num in parsed_file.season: - for ep_num in parsed_file.episode: - if ep_num in needed_episodes: - matched_files[ep_num] = file["filename"] - elif one_season: - for ep_num in parsed_file.episode: - if ep_num in needed_episodes: - matched_files[ep_num] = file["filename"] - if not matched_files: - return False - # Check if all needed episodes are captured (or atleast half) - if (needed_episodes.keys() == matched_files.keys()): - # Set the necessary attributes for each episode - for ep_num, filename in matched_files.items(): - ep = needed_episodes[ep_num] - ep.set("folder", item.active_stream.get("name")) - ep.set("alternative_folder", item.active_stream.get("alternative_name")) - ep.set("file", filename) + if one_season or item.number in parsed_file.season: + for episode_number in parsed_file.episode: + if episode_number in needed_episodes: + matched_files.setdefault(episode_number, []).append(file["filename"]) + + if any(matched_files.values()): + for ep_num, filenames in matched_files.items(): + for filename in filenames: + if not filename or "sample" in filename.lower(): + continue + ep = next(episode for episode in item.episodes if episode.number == ep_num) + ep.set("folder", item.active_stream.get("name")) + ep.set("alternative_folder", item.active_stream.get("alternative_name")) + ep.set("file", filename) return True return False @@ -377,8 +378,7 @@ def _is_wanted_show(self, container: dict, item: Show) -> bool: # Filter and sort files once to improve performance filenames = [ file for file in container.values() - if file and file["filesize"] > min_size - and file["filesize"] < max_size + if file and min_size < file["filesize"] < max_size and splitext(file["filename"].lower())[1] in WANTED_FORMATS ] @@ -387,44 +387,51 @@ def _is_wanted_show(self, container: dict, item: Show) -> bool: # Create a dictionary to map seasons and episodes needed needed_episodes = {} - acceptable_states = [States.Indexed, States.Scraped, States.Unknown, States.Failed] + acceptable_states = [States.Indexed, States.Scraped, States.Unknown, States.Failed, States.PartiallyCompleted] for season in item.seasons: if season.state in acceptable_states and season.is_released_nolog: needed_episode_numbers = {episode.number for episode in season.episodes if episode.state in acceptable_states and episode.is_released_nolog} if needed_episode_numbers: needed_episodes[season.number] = needed_episode_numbers - if not needed_episodes: + + if not any(needed_episodes.values()): return False + # logger.debug(f"Checking {len(filenames)} files in container for {item.log_string}") + # for file in filenames: + # logger.debug(f"Looking at file: {file['filename']} with size: {file['filesize']}") + # Iterate over each file to check if it matches # the season and episode within the show matched_files = {} + one_season = len(item.seasons) == 1 + for file in filenames: with contextlib.suppress(GarbageTorrent, TypeError): parsed_file = parse(file["filename"], remove_trash=True) - if not parsed_file or not parsed_file.parsed_title or 0 in parsed_file.season: + if not parsed_file or not parsed_file.episode or 0 in parsed_file.season: continue + # Check each season and episode to find a match for season_number, episodes in needed_episodes.items(): - if season_number in parsed_file.season: - for episode_number in list(episodes): - if episode_number in parsed_file.episode: + if one_season or season_number in parsed_file.season: + for episode_number in parsed_file.episode: + if episode_number in episodes: # Store the matched file for this episode - matched_files[(season_number, episode_number)] = file - episodes.remove(episode_number) - if not matched_files: - return False - - all_found = all(len(episodes) == 0 for episodes in needed_episodes.values()) - - if all_found: - for (season_number, episode_number), file in matched_files.items(): - season = next(season for season in item.seasons if season.number == season_number) - episode = next(episode for episode in season.episodes if episode.number == episode_number) - episode.set("folder", item.active_stream.get("name")) - episode.set("alternative_folder", item.active_stream.get("alternative_name", None)) - episode.set("file", file.get("filename")) + matched_files.setdefault((season_number, episode_number), []).append(file["filename"]) + + if any(matched_files.values()): + for key, filenames in matched_files.items(): + season_number, episode_number = key + for filename in filenames: + if not filename or "sample" in filename.lower(): + continue + season = next(season for season in item.seasons if season.number == season_number) + episode = next(episode for episode in season.episodes if episode.number == episode_number) + episode.set("folder", item.active_stream.get("name")) + episode.set("alternative_folder", item.active_stream.get("alternative_name", None)) + episode.set("file", filename) return True return False @@ -435,14 +442,6 @@ def _is_downloaded(self, item: MediaItem) -> bool: logger.log("DEBRID", f"Item missing hash, skipping check: {item.log_string}") return False - if self.hash_cache.is_blacklisted(hash_key): - logger.log("DEBRID", f"Skipping download check for blacklisted hash: {hash_key}") - return False - - if self.hash_cache.is_downloaded(hash_key) and item.active_stream.get("id", None): - logger.log("DEBRID", f"Item already downloaded for hash: {hash_key}") - return True - logger.debug(f"Checking if torrent is already downloaded for item: {item.log_string}") torrents = self.get_torrents(1000) torrent = torrents.get(hash_key) @@ -458,16 +457,12 @@ def _is_downloaded(self, item: MediaItem) -> bool: info = self.get_torrent_info(torrent.id) if not info or not hasattr(info, "files"): logger.debug(f"Failed to get torrent info for ID: {torrent.id}") - self.hash_cache.blacklist(torrent.hash) return False if not _matches_item(info, item): - self.hash_cache.blacklist(torrent.hash) return False - # Cache this as downloaded logger.debug(f"Marking torrent as downloaded for hash: {torrent.hash}") - self.hash_cache.mark_downloaded(torrent.hash) item.set("active_stream.id", torrent.id) self.set_active_files(item) logger.debug(f"Set active files for item: {item.log_string} with {len(item.active_stream.get('files', {}))} total files") @@ -484,7 +479,6 @@ def _download_item(self, item: MediaItem): time.sleep(0.5) self.select_files(request_id, item) logger.debug(f"Files selected for request ID: {request_id} for {item.log_string}") - self.hash_cache.mark_downloaded(item.active_stream["hash"]) logger.debug(f"Item marked as downloaded: {item.log_string}") def set_active_files(self, item: MediaItem) -> None: @@ -511,32 +505,18 @@ def set_active_files(self, item: MediaItem) -> None: if not item.folder or not item.alternative_folder or not item.file: logger.error(f"Missing folder or alternative_folder or file for item: {item.log_string}") return - + if isinstance(item, Season) and item.folder: for episode in item.episodes: if episode.file and not episode.folder: episode.set("folder", item.folder) - + if isinstance(item, Show) and item.folder: for season in item.seasons: for episode in season.episodes: if episode.file and not episode.folder: episode.set("folder", item.folder) - def _is_wanted_item(self, item: Union[Movie, Episode, Season]) -> bool: - """Check if item is wanted""" - if isinstance(item, Movie): - return self._is_wanted_movie(item.active_stream.get("files", {}), item) - elif isinstance(item, Show): - return self._is_wanted_show(item.active_stream.get("files", {}), item) - elif isinstance(item, Season): - return self._is_wanted_season(item.active_stream.get("files", {}), item) - elif isinstance(item, Episode): - return self._is_wanted_episode(item.active_stream.get("files", {}), item) - else: - logger.error(f"Unsupported item type: {type(item).__name__}") - return False - ### API Methods for Real-Debrid below @@ -552,7 +532,9 @@ def add_magnet(self, item: MediaItem) -> str: f"{RD_BASE_URL}/torrents/addMagnet", {"magnet": f"magnet:?xt=urn:btih:{hash}&dn=&tr="}, additional_headers=self.auth_headers, - proxies=self.proxy + proxies=self.proxy, + specific_rate_limiter=self.torrents_rate_limiter, + overall_rate_limiter=self.overall_rate_limiter ) if response.is_ok: return response.data.id @@ -569,9 +551,11 @@ def get_torrent_info(self, request_id: str) -> dict: try: response = get( - f"{RD_BASE_URL}/torrents/info/{request_id}", + f"{RD_BASE_URL}/torrents/info/{request_id}", additional_headers=self.auth_headers, - proxies=self.proxy + proxies=self.proxy, + specific_rate_limiter=self.torrents_rate_limiter, + overall_rate_limiter=self.overall_rate_limiter ) if response.is_ok: return response.data @@ -594,25 +578,26 @@ def select_files(self, request_id: str, item: MediaItem) -> bool: f"{RD_BASE_URL}/torrents/selectFiles/{request_id}", {"files": ",".join(files.keys())}, additional_headers=self.auth_headers, - proxies=self.proxy + proxies=self.proxy, + specific_rate_limiter=self.torrents_rate_limiter, + overall_rate_limiter=self.overall_rate_limiter ) return response.is_ok except Exception as e: logger.error(f"Error selecting files for {item.log_string}: {e}") return False - def get_torrents(self, limit: int) -> dict[str, SimpleNamespace]: """Get torrents from real-debrid.com""" try: response = get( f"{RD_BASE_URL}/torrents?limit={str(limit)}", additional_headers=self.auth_headers, - proxies=self.proxy + proxies=self.proxy, + specific_rate_limiter=self.torrents_rate_limiter, + overall_rate_limiter=self.overall_rate_limiter ) if response.is_ok and response.data: - # Example response.data: - # namespace(id='JXQWAQ5GPXJWG', filename='Kill Bill - The Whole Bloody Affair (2011) Reconstruction (1080p BluRay HEVC x265 10bit AC3 5.1)[DHB].mkv', hash='5336e4e408378d70593f3ec7ed7abf15480acedb', bytes=17253577745, host='real-debrid.com', split=2000, progress=100, status='downloaded', added='2024-06-01T15:18:44.000Z', links=['https://real-debrid.com/d/TWJXDFV2XS2T737NMKH4HISF24'], ended='2023-05-21T15:52:34.000Z') return {torrent.hash: torrent for torrent in response.data} except Exception as e: logger.error(f"Error getting torrents from Real-Debrid: {e}") @@ -686,4 +671,4 @@ def check_season(season): return True logger.debug(f"No matching item found for {item.log_string}") - return False + return False \ No newline at end of file diff --git a/src/program/downloaders/torbox.py b/src/program/downloaders/torbox.py new file mode 100644 index 00000000..30f9b10f --- /dev/null +++ b/src/program/downloaders/torbox.py @@ -0,0 +1,290 @@ +import contextlib +from datetime import datetime +from posixpath import splitext +from typing import Generator +from pathlib import Path +from RTN import parse +from RTN.exceptions import GarbageTorrent +from requests import ConnectTimeout + +from program.media.state import States +from program.media.item import MediaItem +from program.settings.manager import settings_manager +from RTN import parse +from RTN.exceptions import GarbageTorrent +from utils.logger import logger +from utils.request import get, post + +API_URL = "https://api.torbox.app/v1/api" +WANTED_FORMATS = {".mkv", ".mp4", ".avi"} + + +class TorBoxDownloader: + """TorBox Downloader""" + + def __init__(self): + self.key = "torbox_downloader" + self.settings = settings_manager.settings.downloaders.torbox + self.api_key = self.settings.api_key + self.base_url = "https://api.torbox.app/v1/api" + self.headers = {"Authorization": f"Bearer {self.api_key}"} + self.initialized = self.validate() + if not self.initialized: + return + logger.success("TorBox Downloader initialized!") + + def validate(self) -> bool: + """Validate the TorBox Downloader as a service""" + if not self.settings.enabled: + logger.info("Torbox downloader is not enabled") + return False + if not self.settings.api_key: + logger.error("Torbox API key is not set") + try: + response = get(f"{self.base_url}/user/me", additional_headers=self.headers) + if response.is_ok: + user_info = response.data.data + expiration = user_info.premium_expires_at + expiration_date_time = datetime.fromisoformat(expiration) + expiration_date_time.replace(tzinfo=None) + delta = expiration_date_time - datetime.now().replace( + tzinfo=expiration_date_time.tzinfo + ) + + if delta.days > 0: + expiration_message = f"Your account expires in {delta.days} days." + else: + expiration_message = "Your account expires soon." + + if user_info.plan == 0: + logger.error("You are not a premium member.") + return False + else: + logger.log("DEBRID", expiration_message) + + return user_info.plan != 0 + except ConnectTimeout: + logger.error("Connection to Torbox timed out.") + except Exception as e: + logger.exception(f"Failed to validate Torbox settings: {e}") + return False + + def run(self, item: MediaItem) -> Generator[MediaItem, None, None]: + """Download media item from torbox.app""" + cached_hashes = self.get_torrent_cached([hash for hash in item.streams]) + if cached_hashes: + for cache in cached_hashes.values(): + item.active_stream = cache + if self.find_required_files(item, cache["files"]): + logger.log( + "DEBRID", f"Item is cached, proceeding with: {item.log_string}" + ) + item.set( + "active_stream", + {"hash": cache["hash"], "files": cache["files"], "id": None}, + ) + self.download(item) + break + else: + logger.log("DEBRID", f"Item is not cached: {item.log_string}") + for hash in item.streams: + logger.log( + "DEBUG", f"Blacklisting hash ({hash}) for item: {item.log_string}" + ) + item.streams = {} + yield item + + def find_required_files(self, item, container): + + files = [ + file + for file in container + if file + and file["size"] > 10000 + and splitext(file["name"].lower())[1] in WANTED_FORMATS + ] + + if item.type == "movie": + for file in files: + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["name"], remove_trash=True) + if parsed_file.type == "movie": + return [file] + if item.type == "show": + # Create a dictionary to map seasons and episodes needed + needed_episodes = {} + acceptable_states = [ + States.Indexed, + States.Scraped, + States.Unknown, + States.Failed, + ] + + for season in item.seasons: + if season.state in acceptable_states and season.is_released_nolog: + needed_episode_numbers = { + episode.number + for episode in season.episodes + if episode.state in acceptable_states + and episode.is_released_nolog + } + if needed_episode_numbers: + needed_episodes[season.number] = needed_episode_numbers + if not needed_episodes: + return False + + # Iterate over each file to check if it matches + # the season and episode within the show + matched_files = [] + for file in files: + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["name"], remove_trash=True) + if ( + not parsed_file + or not parsed_file.parsed_title + or 0 in parsed_file.season + ): + continue + # Check each season and episode to find a match + for season_number, episodes in needed_episodes.items(): + if season_number in parsed_file.season: + for episode_number in list(episodes): + if episode_number in parsed_file.episode: + # Store the matched file for this episode + matched_files.append(file) + episodes.remove(episode_number) + if not matched_files: + return False + + if all(len(episodes) == 0 for episodes in needed_episodes.values()): + return matched_files + if item.type == "season": + needed_episodes = { + episode.number: episode + for episode in item.episodes + if episode.state + in [States.Indexed, States.Scraped, States.Unknown, States.Failed] + } + one_season = len(item.parent.seasons) == 1 + + # Dictionary to hold the matched files for each episode + matched_files = [] + season_num = item.number + + # Parse files once and assign to episodes + for file in files: + if not file or not file.get("name"): + continue + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["name"], remove_trash=True) + if ( + not parsed_file + or not parsed_file.episode + or 0 in parsed_file.season + ): + continue + # Check if the file's season matches the item's season or if there's only one season + if season_num in parsed_file.season: + for ep_num in parsed_file.episode: + if ep_num in needed_episodes: + matched_files.append(file) + elif one_season: + for ep_num in parsed_file.episode: + if ep_num in needed_episodes: + matched_files.append(file) + if not matched_files: + return False + + # Check if all needed episodes are captured (or atleast half) + if len(needed_episodes) == len(matched_files): + return matched_files + if item.type == "episode": + for file in files: + if not file or not file.get("name"): + continue + with contextlib.suppress(GarbageTorrent, TypeError): + parsed_file = parse(file["name"], remove_trash=True) + if ( + item.number in parsed_file.episode + and item.parent.number in parsed_file.season + ): + return [file] + + return [] + + def download(self, item: MediaItem): + # Check if the torrent already exists + exists = False + torrent_list = self.get_torrent_list() + for torrent in torrent_list: + if item.active_stream["hash"] == torrent["hash"]: + id = torrent["id"] + exists = True + break + + # If it doesnt, lets download it and refresh the torrent_list + if not exists: + id = self.create_torrent(item.active_stream["hash"]) + torrent_list = self.get_torrent_list() + + # Find the torrent, correct file and we gucci + for torrent in torrent_list: + if torrent["id"] == id: + if item.type == "movie": + file = self.find_required_files(item, item.active_stream["files"])[ + 0 + ] + _file_path = Path(file["name"]) + item.set("folder", _file_path.parent.name) + item.set("alternative_folder", ".") + item.set("file", _file_path.name) + if item.type == "show": + files = self.find_required_files(item, item.active_stream["files"]) + for season in item.seasons: + for episode in season.episodes: + file = self.find_required_files(episode, files)[0] + _file_path = Path(file["name"]) + episode.set("folder", _file_path.parent.name) + episode.set("alternative_folder", ".") + episode.set("file", _file_path.name) + if item.type == "season": + files = self.find_required_files(item, item.active_stream["files"]) + for episode in item.episodes: + file = self.find_required_files(episode, files)[0] + _file_path = Path(file["name"]) + episode.set("folder", _file_path.parent.name) + episode.set("alternative_folder", ".") + episode.set("file", _file_path.name) + if item.type == "episode": + file = self.find_required_files(episode, files)[0] + _file_path = Path(file["name"]) + item.set("folder", _file_path.parent.name) + item.set("alternative_folder", ".") + item.set("file", _file_path.name) + logger.log("DEBRID", f"Downloaded {item.log_string}") + + def get_torrent_cached(self, hash_list): + hash_string = ",".join(hash_list) + response = get( + f"{self.base_url}/torrents/checkcached?hash={hash_string}&list_files=True", + additional_headers=self.headers, + response_type=dict, + ) + return response.data["data"] + + def create_torrent(self, hash) -> int: + magnet_url = f"magnet:?xt=urn:btih:{hash}" + response = post( + f"{self.base_url}/torrents/createtorrent", + data={"magnet": magnet_url, "seed": 1, "allow_zip": False}, + additional_headers=self.headers, + ) + return response.data.data.torrent_id + + def get_torrent_list(self) -> list: + response = get( + f"{self.base_url}/torrents/mylist?bypass_cache=true", + additional_headers=self.headers, + response_type=dict, + ) + return response.data["data"] diff --git a/backend/program/indexers/__init__.py b/src/program/indexers/__init__.py similarity index 100% rename from backend/program/indexers/__init__.py rename to src/program/indexers/__init__.py diff --git a/backend/program/indexers/tmdb.py b/src/program/indexers/tmdb.py similarity index 100% rename from backend/program/indexers/tmdb.py rename to src/program/indexers/tmdb.py diff --git a/backend/program/indexers/trakt.py b/src/program/indexers/trakt.py similarity index 93% rename from backend/program/indexers/trakt.py rename to src/program/indexers/trakt.py index 5af9e184..445c0c2e 100644 --- a/backend/program/indexers/trakt.py +++ b/src/program/indexers/trakt.py @@ -118,8 +118,9 @@ def _map_item_from_data(data, item_type: str, show_genres: List[str] = None) -> } item["is_anime"] = ( - ("anime" in genres or "animation" in genres) if genres - and item["country"] in ("jp", "kr") + ("anime" in genres) + or ("animation" in genres and (item["country"] in ("jp", "kr")or item["language"] == "ja")) + if genres else False ) @@ -170,10 +171,8 @@ def find_first(preferred_types, data): return d return None - data = find_first(["show", "movie", "season", "episode"], response.data) - if data: - return _map_item_from_data(getattr(data, data.type), data.type) - return None + data = next((d for d in response.data if d.type in ["show", "movie", "season"]), None) + return _map_item_from_data(getattr(data, data.type), data.type) if data else None def get_imdbid_from_tmdb(tmdb_id: str) -> Optional[str]: """Wrapper for trakt.tv API search method.""" diff --git a/backend/program/libraries/__init__.py b/src/program/libraries/__init__.py similarity index 100% rename from backend/program/libraries/__init__.py rename to src/program/libraries/__init__.py diff --git a/backend/program/libraries/symlink.py b/src/program/libraries/symlink.py similarity index 89% rename from backend/program/libraries/symlink.py rename to src/program/libraries/symlink.py index 44d27306..52c7bf4f 100644 --- a/backend/program/libraries/symlink.py +++ b/src/program/libraries/symlink.py @@ -40,15 +40,15 @@ def run(self) -> MediaItem: Create a library from the symlink paths. Return stub items that should be fed into an Indexer to have the rest of the metadata filled in. """ - for directory, item_type, is_anime in [("movies", "movie", False), ("anime_movies", "anime movie", True)]: + for directory, item_type, is_anime in [("shows", "show", False), ("anime_shows", "anime show", True)]: if not self.settings.separate_anime_dirs and is_anime: continue - yield from process_items(self.settings.library_path / directory, Movie, item_type, is_anime) + yield from process_shows(self.settings.library_path / directory, item_type, is_anime) - for directory, item_type, is_anime in [("shows", "show", False), ("anime_shows", "anime show", True)]: + for directory, item_type, is_anime in [("movies", "movie", False), ("anime_movies", "anime movie", True)]: if not self.settings.separate_anime_dirs and is_anime: continue - yield from process_shows(self.settings.library_path / directory, item_type, is_anime) + yield from process_items(self.settings.library_path / directory, Movie, item_type, is_anime) def process_items(directory: Path, item_class, item_type: str, is_anime: bool = False): @@ -88,11 +88,13 @@ def process_shows(directory: Path, item_type: str, is_anime: bool = False) -> Sh show_item = Show({'imdb_id': imdb_id.group(), 'title': title.group(1)}) if is_anime: show_item.is_anime = True + seasons = {} for season in os.listdir(directory / show): if not (season_number := re.search(r'(\d+)', season)): logger.log("NOT_FOUND", f"Can't extract season number at path {directory / show / season}") continue season_item = Season({'number': int(season_number.group())}) + episodes = {} for episode in os.listdir(directory / show / season): if not (episode_number := re.search(r's\d+e(\d+)', episode)): logger.log("NOT_FOUND", f"Can't extract episode number at path {directory / show / season / episode}") @@ -108,6 +110,13 @@ def process_shows(directory: Path, item_type: str, is_anime: bool = False) -> Sh episode_item.set("update_folder", "updated") if is_anime: episode_item.is_anime = True - season_item.add_episode(episode_item) - show_item.add_season(season_item) - yield show_item + #season_item.add_episode(episode_item) + episodes[int(episode_number.group(1))] = episode_item + if len(episodes) > 0: + for i in range(1, max(episodes.keys())+1): + season_item.add_episode(episodes.get(i, Episode({'number': i}))) + seasons[int(season_number.group())] = season_item + if len(seasons) > 0: + for i in range(1, max(seasons.keys())+1): + show_item.add_season(seasons.get(i, Season({'number': i}))) + yield show_item \ No newline at end of file diff --git a/backend/program/media/__init__.py b/src/program/media/__init__.py similarity index 66% rename from backend/program/media/__init__.py rename to src/program/media/__init__.py index 23adad03..271ab569 100644 --- a/backend/program/media/__init__.py +++ b/src/program/media/__init__.py @@ -1,3 +1,2 @@ -from .container import MediaItemContainer # noqa from .item import Episode, MediaItem, Movie, Season, Show # noqa from .state import States # noqa diff --git a/backend/program/media/item.py b/src/program/media/item.py similarity index 59% rename from backend/program/media/item.py rename to src/program/media/item.py index e5320375..470d2fdc 100644 --- a/backend/program/media/item.py +++ b/src/program/media/item.py @@ -1,78 +1,118 @@ -from dataclasses import dataclass -from datetime import datetime, timedelta +"""MediaItem class""" +from datetime import datetime from typing import List, Optional, Self from program.media.state import States -from RTN import Torrent -from RTN.patterns import extract_episodes +from RTN import Torrent, parse +# from RTN.patterns import extract_episodes from utils.logger import logger +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship +import sqlalchemy +from sqlalchemy import orm -@dataclass -class ItemId: - value: str - parent_id: Optional[Self] = None +from program.db.db import db - def __repr__(self): - if not self.parent_id: - return str(self.value) - return f"{self.parent_id}/{self.value}" - - def __hash__(self): - return hash(repr(self)) - - def __eq__(self, other): - if isinstance(other, ItemId): - return repr(self) == repr(other) - return False - - -class MediaItem: +class MediaItem(db.Model): """MediaItem class""" - + __tablename__ = "MediaItem" + _id: Mapped[int] = mapped_column(primary_key=True) + item_id: Mapped[str] = mapped_column(sqlalchemy.String, nullable=False) + number: Mapped[Optional[int]] = mapped_column(sqlalchemy.Integer, nullable=True) + type: Mapped[str] = mapped_column(sqlalchemy.String, nullable=False) + requested_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, default=datetime.now()) + requested_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + indexed_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True) + scraped_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True) + scraped_times: Mapped[Optional[int]] = mapped_column(sqlalchemy.Integer, default=0) + active_stream: Mapped[Optional[dict[str, str]]] = mapped_column(sqlalchemy.JSON, nullable=True) + symlinked: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False) + symlinked_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True) + symlinked_times: Mapped[Optional[int]] = mapped_column(sqlalchemy.Integer, default=0) + file: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + folder: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + alternative_folder: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + is_anime: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False) + title: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + imdb_id: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + tvdb_id: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + tmdb_id: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + network: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + country: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + language: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + aired_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True) + year: Mapped[Optional[int]] = mapped_column(sqlalchemy.Integer, nullable=True) + genres: Mapped[Optional[List[str]]] = mapped_column(sqlalchemy.JSON, nullable=True) + key: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + guid: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + update_folder: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True) + overseerr_id: Mapped[Optional[int]] = mapped_column(sqlalchemy.Integer, nullable=True) + last_state: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, default="Unknown") + + __mapper_args__ = { + "polymorphic_identity": "mediaitem", + "polymorphic_on":"type", + "with_polymorphic":"*", + } + @orm.reconstructor + def init_on_load(self): + self.streams: Optional[dict[str, Torrent]] = {} def __init__(self, item: dict) -> None: - self.requested_at: Optional[datetime] = item.get("requested_at", datetime.now()) - self.requested_by: Optional[str] = item.get("requested_by", None) - - self.indexed_at: Optional[datetime] = None - - self.scraped_at: Optional[datetime] = None - self.scraped_times: Optional[int] = 0 - self.active_stream: Optional[dict[str, str]] = item.get("active_stream", {}) + # id: Mapped[int] = mapped_column(primary_key=True) + # name: Mapped[str] = mapped_column(String(30)) + # fullname: Mapped[Optional[str]] + # addresses: Mapped[List["Address"]] = relationship(lazy=False, + # back_populates="user", cascade="all, delete-orphan" + # ) + # user_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("user_account.id")) + # user: Mapped["User"] = relationship(lazy=False, back_populates="addresses") + self.requested_at = item.get("requested_at", datetime.now()) + self.requested_by = item.get("requested_by", None) + + self.indexed_at = None + + self.scraped_at = None + self.scraped_times = 0 + self.active_stream = item.get("active_stream", {}) self.streams: Optional[dict[str, Torrent]] = {} - self.symlinked: Optional[bool] = False - self.symlinked_at: Optional[datetime] = None - self.symlinked_times: Optional[int] = 0 + self.symlinked = False + self.symlinked_at = None + self.symlinked_times = 0 - self.file: Optional[str] = None - self.folder: Optional[str] = None - self.is_anime: Optional[bool] = item.get("is_anime", False) - self.parent: Optional[Self] = None + self.file = None + self.folder = None + self.is_anime = item.get("is_anime", False) # Media related - self.title: Optional[str] = item.get("title", None) - self.imdb_id: Optional[str] = item.get("imdb_id", None) + self.title = item.get("title", None) + self.imdb_id = item.get("imdb_id", None) if self.imdb_id: - self.imdb_link: Optional[str] = f"https://www.imdb.com/title/{self.imdb_id}/" + self.imdb_link = f"https://www.imdb.com/title/{self.imdb_id}/" if not hasattr(self, "item_id"): - self.item_id: ItemId = ItemId(self.imdb_id) - self.tvdb_id: Optional[str] = item.get("tvdb_id", None) - self.tmdb_id: Optional[str] = item.get("tmdb_id", None) - self.network: Optional[str] = item.get("network", None) - self.country: Optional[str] = item.get("country", None) - self.language: Optional[str] = item.get("language", None) - self.aired_at: Optional[datetime] = item.get("aired_at", None) - self.genres: Optional[List[str]] = item.get("genres", []) + self.item_id = self.imdb_id + self.tvdb_id = item.get("tvdb_id", None) + self.tmdb_id = item.get("tmdb_id", None) + self.network = item.get("network", None) + self.country = item.get("country", None) + self.language = item.get("language", None) + self.aired_at = item.get("aired_at", None) + self.year = item.get("year" , None) + self.genres = item.get("genres", []) # Plex related - self.key: Optional[str] = item.get("key", None) - self.guid: Optional[str] = item.get("guid", None) - self.update_folder: Optional[str] = item.get("update_folder", None) + self.key = item.get("key", None) + self.guid = item.get("guid", None) + self.update_folder = item.get("update_folder", None) # Overseerr related - self.overseerr_id: Optional[int] = item.get("overseerr_id", None) + self.overseerr_id = item.get("overseerr_id", None) + def store_state(self) -> None: + self.last_state = self._determine_state().name + @property def is_released(self) -> bool: """Check if an item has been released.""" @@ -245,12 +285,21 @@ def collection(self): class Movie(MediaItem): """Movie class""" - + __tablename__ = "Movie" + _id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("MediaItem._id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "movie", + "polymorphic_load": "inline", + } + @orm.reconstructor + def init_on_load(self): + self.streams: Optional[dict[str, Torrent]] = {} + def __init__(self, item): self.type = "movie" self.file = item.get("file", None) super().__init__(item) - self.item_id = ItemId(self.imdb_id) + self.item_id = self.imdb_id def __repr__(self): return f"Movie:{self.log_string}:{self.state.name}" @@ -258,78 +307,31 @@ def __repr__(self): def __hash__(self): return super().__hash__() -class Show(MediaItem): - """Show class""" - - def __init__(self, item): - super().__init__(item) - self.type = "show" - self.locations = item.get("locations", []) - self.seasons: list[Season] = item.get("seasons", []) - self.item_id = ItemId(self.imdb_id) - - def get_season_index_by_id(self, item_id): - """Find the index of an season by its item_id.""" - for i, season in enumerate(self.seasons): - if season.item_id == item_id: - return i - return None - - def _determine_state(self): - if all(season.state == States.Completed for season in self.seasons): - return States.Completed - if any( - season.state in (States.Completed, States.PartiallyCompleted) - for season in self.seasons - ): - return States.PartiallyCompleted - if all(season.state == States.Symlinked for season in self.seasons): - return States.Symlinked - if all(season.state == States.Downloaded for season in self.seasons): - return States.Downloaded - if self.is_scraped(): - return States.Scraped - if any(season.state == States.Indexed for season in self.seasons): - return States.Indexed - if any(season.state == States.Requested for season in self.seasons): - return States.Requested - return States.Unknown - - def __repr__(self): - return f"Show:{self.log_string}:{self.state.name}" - - def __hash__(self): - return super().__hash__() - - def fill_in_missing_children(self, other: Self): - existing_seasons = [s.number for s in self.seasons] - for s in other.seasons: - if s.number not in existing_seasons: - self.add_season(s) - else: - existing_season = next( - es for es in self.seasons if s.number == es.number - ) - existing_season.fill_in_missing_children(s) - - def add_season(self, season): - """Add season to show""" - if season.number not in [s.number for s in self.seasons]: - season.is_anime = self.is_anime - self.seasons.append(season) - season.parent = self - season.item_id.parent_id = self.item_id - self.seasons = sorted(self.seasons, key=lambda s: s.number) - - class Season(MediaItem): """Season class""" + __tablename__ = "Season" + _id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("MediaItem._id"), primary_key=True) + parent_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("Show._id"), use_existing_column=True) + parent: Mapped["Show"] = relationship(lazy=False, back_populates="seasons", foreign_keys="Season.parent_id") + episodes: Mapped[List["Episode"]] = relationship(lazy=False, back_populates="parent", single_parent=True, cascade="all, delete-orphan", foreign_keys="Episode.parent_id") + @orm.reconstructor + def init_on_load(self): + self.streams: Optional[dict[str, Torrent]] = {} + __mapper_args__ = { + "polymorphic_identity": "season", + "polymorphic_load": "inline", + } + def store_state(self) -> None: + for episode in self.episodes: + episode.store_state() + self.last_state = self._determine_state().name def __init__(self, item): self.type = "season" self.number = item.get("number", None) self.episodes: list[Episode] = item.get("episodes", []) - self.item_id = ItemId(self.number, parent_id=item.get("parent_id")) + self.item_id = self.number + self.parent = item.get("parent", None) super().__init__(item) if self.parent and isinstance(self.parent, Show): self.is_anime = self.parent.is_anime @@ -338,14 +340,14 @@ def _determine_state(self): if len(self.episodes) > 0: if all(episode.state == States.Completed for episode in self.episodes): return States.Completed - if any(episode.state == States.Completed for episode in self.episodes): - return States.PartiallyCompleted if all(episode.state == States.Symlinked for episode in self.episodes): return States.Symlinked if all(episode.file and episode.folder for episode in self.episodes): return States.Downloaded if self.is_scraped(): return States.Scraped + if any(episode.state == States.Completed for episode in self.episodes): + return States.PartiallyCompleted if any(episode.state == States.Indexed for episode in self.episodes): return States.Indexed if any(episode.state == States.Requested for episode in self.episodes): @@ -359,7 +361,7 @@ def is_released(self) -> bool: def __eq__(self, other): if ( type(self) == type(other) - and self.item_id.parent_id == other.item_id.parent_id + and self.parent_id == other.parent_id ): return self.number == other.get("number", None) @@ -393,7 +395,6 @@ def add_episode(self, episode): episode.is_anime = self.is_anime self.episodes.append(episode) episode.parent = self - episode.item_id.parent_id = self.item_id self.episodes = sorted(self.episodes, key=lambda e: e.number) @property @@ -405,12 +406,24 @@ def get_top_title(self) -> str: class Episode(MediaItem): """Episode class""" + __tablename__ = "Episode" + _id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("MediaItem._id"), primary_key=True) + parent_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("Season._id"), use_existing_column=True) + parent: Mapped["Season"] = relationship(lazy=False, back_populates='episodes', foreign_keys="Episode.parent_id") + @orm.reconstructor + def init_on_load(self): + self.streams: Optional[dict[str, Torrent]] = {} + + __mapper_args__ = { + "polymorphic_identity": "episode", + "polymorphic_load": "inline", + } def __init__(self, item): self.type = "episode" self.number = item.get("number", None) self.file = item.get("file", None) - self.item_id = ItemId(self.number, parent_id=item.get("parent_id")) + self.item_id = self.number# , parent_id=item.get("parent_id")) super().__init__(item) if self.parent and isinstance(self.parent, Season): self.is_anime = self.parent.parent.is_anime @@ -418,7 +431,8 @@ def __init__(self, item): def __eq__(self, other): if ( type(self) == type(other) - and self.item_id.parent_id == other.item_id.parent_id + and self.item_id == other.item_id + and self.parent.parent.item_id == other.parent.parent.item_id ): return self.number == other.get("number", None) @@ -431,7 +445,8 @@ def __hash__(self): def get_file_episodes(self) -> List[int]: if not self.file or not isinstance(self.file, str): raise ValueError("The file attribute must be a non-empty string.") - return extract_episodes(self.file) + # return list of episodes + return parse(self.file).episode @property def log_string(self): @@ -440,6 +455,110 @@ def log_string(self): def get_top_title(self) -> str: return self.parent.parent.title + def get_top_year(self) -> Optional[int]: + return self.parent.parent.year + + def get_season_year(self) -> Optional[int]: + return self.parent.year + +class Show(MediaItem): + """Show class""" + __tablename__ = "Show" + _id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("MediaItem._id"), primary_key=True) + seasons: Mapped[List["Season"]] = relationship(lazy=False, back_populates="parent", single_parent=True, cascade="all, delete-orphan", foreign_keys="Season.parent_id") + @orm.reconstructor + def init_on_load(self): + self.streams: Optional[dict[str, Torrent]] = {} + + __mapper_args__ = { + "polymorphic_identity": "show", + "polymorphic_load": "inline", + } + + def __init__(self, item): + super().__init__(item) + self.type = "show" + self.locations = item.get("locations", []) + self.seasons: list[Season] = item.get("seasons", []) + self.item_id = item.get("imdb_id") + self.propagate_attributes_to_childs() + + def get_season_index_by_id(self, item_id): + """Find the index of an season by its item_id.""" + for i, season in enumerate(self.seasons): + if season.item_id == item_id: + return i + return None + + def _determine_state(self): + if all(season.state == States.Completed for season in self.seasons): + return States.Completed + if all(season.state == States.Symlinked for season in self.seasons): + return States.Symlinked + if all(season.state == States.Downloaded for season in self.seasons): + return States.Downloaded + if self.is_scraped(): + return States.Scraped + if any( + season.state in (States.Completed, States.PartiallyCompleted) + for season in self.seasons + ): + return States.PartiallyCompleted + if any(season.state == States.Indexed for season in self.seasons): + return States.Indexed + if any(season.state == States.Requested for season in self.seasons): + return States.Requested + return States.Unknown + def store_state(self) -> None: + for season in self.seasons: + season.store_state() + self.last_state = self._determine_state().name + def __repr__(self): + return f"Show:{self.log_string}:{self.state.name}" + + def __hash__(self): + return super().__hash__() + + def fill_in_missing_children(self, other: Self): + existing_seasons = [s.number for s in self.seasons] + for s in other.seasons: + if s.number not in existing_seasons: + self.add_season(s) + else: + existing_season = next( + es for es in self.seasons if s.number == es.number + ) + existing_season.fill_in_missing_children(s) + + def add_season(self, season): + """Add season to show""" + if season.number not in [s.number for s in self.seasons]: + season.is_anime = self.is_anime + self.seasons.append(season) + season.parent = self + #season.item_id.parent_id = self.item_id + self.seasons = sorted(self.seasons, key=lambda s: s.number) + + def propagate_attributes_to_childs(self): + """Propagate show attributes to seasons and episodes if they are empty or do not match.""" + # Important attributes that need to be connected. + attributes = ["genres", "country", "network", "language", "is_anime"] + + def propagate(target, source): + for attr in attributes: + source_value = getattr(source, attr, None) + target_value = getattr(target, attr, None) + # Check if the attribute source is not falsy (none, false, 0, []) + # and if the target is not None we set the source to the target + if (not target_value) and source_value is not None: + setattr(target, attr, source_value) + + for season in self.seasons: + propagate(season, self) + for episode in season.episodes: + propagate(episode, self) + + def _set_nested_attr(obj, key, value): if "." in key: diff --git a/backend/program/media/state.py b/src/program/media/state.py similarity index 100% rename from backend/program/media/state.py rename to src/program/media/state.py diff --git a/backend/program/program.py b/src/program/program.py similarity index 64% rename from backend/program/program.py rename to src/program/program.py index 96c402a9..362a051e 100644 --- a/backend/program/program.py +++ b/src/program/program.py @@ -10,11 +10,9 @@ from apscheduler.schedulers.background import BackgroundScheduler from program.content import Listrr, Mdblist, Overseerr, PlexWatchlist, TraktContent -from program.downloaders.realdebrid import Debrid -from program.downloaders.torbox import TorBoxDownloader +from program.downloaders import Downloader from program.indexers.trakt import TraktIndexer from program.libraries import SymlinkLibrary -from program.media.container import MediaItemContainer from program.media.item import Episode, MediaItem, Movie, Season, Show from program.media.state import States from program.scrapers import Scraping @@ -24,15 +22,19 @@ from utils import data_dir_path from utils.logger import logger, scrub_logs -from .cache import hash_cache -from .pickly import Pickly from .state_transition import process_event from .symlink import Symlinker from .types import Event, Service + if settings_manager.settings.tracemalloc: import tracemalloc +from program.db.db import db, alembic, run_migrations +from sqlalchemy import select, func +from sqlalchemy.orm import joinedload +import program.db.db_functions as DB + class Program(threading.Thread): """Program class""" @@ -42,12 +44,12 @@ def __init__(self, args): self.startup_args = args self.initialized = False self.event_queue = Queue() - self.media_items = MediaItemContainer() self.services = {} self.queued_items = [] self.running_items = [] self.mutex = Lock() self.enable_trace = settings_manager.settings.tracemalloc + self.sql_Session = db.Session if self.enable_trace: tracemalloc.start() self.malloc_time = time.monotonic()-50 @@ -64,12 +66,9 @@ def initialize_services(self): self.indexing_services = {TraktIndexer: TraktIndexer()} self.processing_services = { Scraping: Scraping(), - Symlinker: Symlinker(self.media_items), + Symlinker: Symlinker(), Updater: Updater(), - } - self.downloader_services = { - Debrid: Debrid(hash_cache), - TorBoxDownloader: TorBoxDownloader(hash_cache), + Downloader: Downloader(), } # Depends on Symlinker having created the file structure so needs # to run after it @@ -77,18 +76,19 @@ def initialize_services(self): SymlinkLibrary: SymlinkLibrary(), } if not any(s.initialized for s in self.requesting_services.values()): - logger.error("No Requesting service initialized, you must select at least one.") - if not any(s.initialized for s in self.downloader_services.values()): - logger.error("No Downloader service initialized, you must select at least one.") + logger.error("No Requesting service initialized, you must enable at least one.") if not self.processing_services.get(Scraping).initialized: - logger.error("No Scraping service initialized, you must select at least one.") + logger.error("No Scraping service initialized, you must enable at least one.") + if not self.processing_services.get(Downloader).initialized: + logger.error("No Downloader service initialized, you must enable at least one.") + if not self.processing_services.get(Updater).initialized: + logger.error("No Updater service initialized, you must enable at least one.") self.services = { **self.library_services, **self.indexing_services, **self.requesting_services, **self.processing_services, - **self.downloader_services, } if self.enable_trace: @@ -102,18 +102,12 @@ def validate(self) -> bool: any(s.initialized for s in self.library_services.values()), any(s.initialized for s in self.indexing_services.values()), all(s.initialized for s in self.processing_services.values()), - any(s.initialized for s in self.downloader_services.values()), ) ) def start(self): latest_version = get_version() - user_version = settings_manager.settings.version - - if latest_version != user_version: - logger.log("PROGRAM", f"Riven v{user_version} starting! (Update Available: v{latest_version})") - else: - logger.log("PROGRAM", f"Riven v{user_version} starting!") + logger.log("PROGRAM", f"Riven v{latest_version} starting!") settings_manager.register_observer(self.initialize_services) os.makedirs(data_dir_path, exist_ok=True) @@ -143,22 +137,42 @@ def start(self): self.initialized = True logger.log("PROGRAM", "Riven started!") - if not self.startup_args.ignore_cache: - self.pickly = Pickly(self.media_items, data_dir_path) - self.pickly.start() - - if not len(self.media_items): - # Seed initial MIC with Library State - for item in self.services[SymlinkLibrary].run(): - if settings_manager.settings.map_metadata: - if isinstance(item, (Movie, Show)): - item = next(self.services[TraktIndexer].run(item)) - logger.debug(f"Mapped metadata to {item.type.title()}: {item.log_string}") - self.media_items.upsert(item) - self.media_items.save(str(data_dir_path / "media.pkl")) - - if len(self.media_items): - self.media_items.log() + run_migrations() + with db.Session() as session: + res = session.execute(select(func.count(MediaItem._id))).scalar_one() + added = [] + if res == 0: + for item in self.services[SymlinkLibrary].run(): + if settings_manager.settings.map_metadata: + if isinstance(item, (Movie, Show)): + try: + item = next(self.services[TraktIndexer].run(item)) + except StopIteration: + logger.error(f"Failed to enhance metadata for {item.title} ({item.item_id}): {e}") + continue + if item.item_id in added: + logger.error(f"Cannot enhance metadata, {item.title} ({item.item_id}) contains multiple folders. Manual resolution required.") + exit(0) + added.append(item.item_id) + item.store_state() + session.add(item) + logger.debug(f"Mapped metadata to {item.type.title()}: {item.log_string}") + session.commit() + + movies_symlinks = session.execute(select(func.count(Movie._id)).where(Movie.symlinked == True)).scalar_one() + episodes_symlinks = session.execute(select(func.count(Episode._id)).where(Episode.symlinked == True)).scalar_one() + total_symlinks = movies_symlinks + episodes_symlinks + total_movies = session.execute(select(func.count(Movie._id))).scalar_one() + total_shows = session.execute(select(func.count(Show._id))).scalar_one() + total_seasons = session.execute(select(func.count(Season._id))).scalar_one() + total_episodes = session.execute(select(func.count(Episode._id))).scalar_one() + total_items = session.execute(select(func.count(MediaItem._id))).scalar_one() + + logger.log("ITEM", f"Movies: {total_movies} (Symlinks: {movies_symlinks})") + logger.log("ITEM", f"Shows: {total_shows}") + logger.log("ITEM", f"Seasons: {total_seasons}") + logger.log("ITEM", f"Episodes: {total_episodes} (Symlinks: {episodes_symlinks})") + logger.log("ITEM", f"Total Items: {total_items} (Symlinks: {total_symlinks})") self.executors = [] self.scheduler = BackgroundScheduler() @@ -172,12 +186,12 @@ def start(self): def _retry_library(self) -> None: """Retry any items that are in an incomplete state.""" - items_to_submit = [ - item for item in self.media_items.get_incomplete_items().values() - ] - logger.log("PROGRAM", f"Found {len(items_to_submit)} items to retry") - for item in items_to_submit: - self._push_event_queue(Event(emitted_by=self.__class__, item=item)) + with db.Session() as session: + items_to_submit = session.execute(select(Show).where(MediaItem.last_state!="Completed" ).order_by(MediaItem.scraped_at.desc())).unique().scalars().all() + items_to_submit = items_to_submit + session.execute(select(Movie).where(MediaItem.last_state!="Completed" ).order_by(MediaItem.scraped_at.desc())).unique().scalars().all() + logger.log("PROGRAM", f"Found {len(items_to_submit)} items to retry") + for item in items_to_submit: + self._push_event_queue(Event(emitted_by=self.__class__, item=item)) def _schedule_functions(self) -> None: """Schedule each service based on its update interval.""" @@ -220,24 +234,38 @@ def _schedule_services(self) -> None: ) logger.log("PROGRAM", f"Scheduled {service_cls.__name__} to run every {update_interval} seconds.") + def _id_in_queue(self, id): + for i in self.queued_items: + if i._id == id: + return True + return False + + def _id_in_running_items(self, id): + for i in self.running_items: + if i._id == id: + return True + return False + def _push_event_queue(self, event): with self.mutex: if( not event.item in self.queued_items and not event.item in self.running_items): - if ( isinstance(event.item, Show) - and (any( [s for s in event.item.seasons if s in self.queued_items or s in self.running_items]) - or any([e for e in [s.episodes for s in event.item.seasons] if e in self.queued_items or e in self.running_items]) ) - ): - return - if isinstance(event.item, Season) and any( [e for e in event.item.episodes if e in self.queued_items or e in self.running_items] ): - return - if hasattr(event.item, "parent") and event.item.parent in self.queued_items : - return - if hasattr(event.item, "parent") and hasattr(event.item.parent, "parent") and event.item.parent.parent and event.item.parent.parent in self.queued_items : - return - if hasattr(event.item, "parent") and event.item.parent in self.running_items : - return - if hasattr(event.item, "parent") and hasattr(event.item.parent, "parent") and event.item.parent.parent and event.item.parent.parent in self.running_items : - return + if hasattr(event.item, "_id"): + if isinstance(event.item, Show): + for s in event.item.seasons: + if self._id_in_queue(s._id) or self._id_in_running_items(s._id): + return + for e in s.episodes: + if self._id_in_queue(e._id) or self._id_in_running_items(e._id): + return + + if isinstance(event.item, Season): + for e in event.item.episodes: + if self._id_in_queue(e._id) or self._id_in_running_items(e._id): + return + if hasattr(event.item, "parent") and ( self._id_in_queue(event.item.parent._id) or self._id_in_running_items(event.item.parent._id) ): + return + if hasattr(event.item, "parent") and hasattr(event.item.parent, "parent") and event.item.parent.parent and ( self._id_in_queue(event.item.parent.parent._id) or self._id_in_running_items(event.item.parent.parent._id)): + return self.queued_items.append(event.item) self.event_queue.put(event) if not isinstance(event.item, (Show, Movie, Episode, Season)): @@ -250,6 +278,7 @@ def _push_event_queue(self, event): def _pop_event_queue(self, event): with self.mutex: + DB._store_item(event.item) self.queued_items.remove(event.item) def _remove_from_running_items(self, item, service_name=""): @@ -259,36 +288,24 @@ def _remove_from_running_items(self, item, service_name=""): logger.log("PROGRAM", f"Item {item.log_string} finished running section {service_name}" ) def add_to_running(self, item, service_name): + if item is None: + return if item not in self.running_items: - self.running_items.append(item) + if isinstance(item, MediaItem) and not self._id_in_running_items(item._id): + self.running_items.append(item) + elif not isinstance(item, MediaItem): + self.running_items.append(item) logger.log("PROGRAM", f"Item {item.log_string} started running section {service_name}" ) def _process_future_item(self, future: Future, service: Service, orig_item: MediaItem) -> None: """Callback to add the results from a future emitted by a service to the event queue.""" try: - timeout_seconds = int( - os.environ[service.__name__.upper() +"_WORKER_TIMEOUT"] - ) if service.__name__.upper() + "_WORKER_TIMEOUT" in os.environ else 60 * 3 - fut_except = future.exception() - if fut_except != None: - logger.error(f"{fut_except}") - self._remove_from_running_items(item) - for item in future.result(timeout=timeout_seconds): - if isinstance(item, list): - all_media_items = True - for i in item: - if not isinstance(i, MediaItem): - all_media_items = False - self._remove_from_running_items(orig_item, service.__name__) - if all_media_items == True: - for i in item: - self._push_event_queue(Event(emitted_by=self.__class__, item=i)) - return - elif not isinstance(item, MediaItem): - logger.log("PROGRAM", f"Service {service.__name__} emitted item {item} of type {item.__class__.__name__}, skipping") - self._remove_from_running_items(orig_item, service.__name__) - if item is not None and isinstance(item, MediaItem): - self._push_event_queue(Event(emitted_by=service, item=item)) + for item in future.result(): + pass + if orig_item is not None: + logger.log("PROGRAM", f"Service {service.__name__} finished running on {orig_item.log_string}") + else: + logger.log("PROGRAM", f"Service {service.__name__} finished running.") except TimeoutError: logger.debug('Service {service.__name__} timeout waiting for result on {orig_item.log_string}') self._remove_from_running_items(orig_item, service.__name__) @@ -305,9 +322,9 @@ def _submit_job(self, service: Service, item: MediaItem | None) -> None: # Check if the executor has been shut down if not self.running: - logger.error("Cannot submit job, executor is shut down.") + logger.log("PROGRAM", "Waiting for executor to start before submitting jobs") return - + # Instead of using the one executor, loop over the list of self.executors, if one is found with the service.__name__ then use that one # If one is not found with the service.__name__ then create a new one and append it to the list # This will allow for multiple services to run at the same time @@ -324,13 +341,10 @@ def _submit_job(self, service: Service, item: MediaItem | None) -> None: new_executor = ThreadPoolExecutor(thread_name_prefix=f"Worker_{service.__name__}", max_workers=max_workers ) self.executors.append({ "_name_prefix": service.__name__, "_executor": new_executor }) cur_executor = new_executor - - try: - func = self.services[service].run - future = cur_executor.submit(func) if item is None else cur_executor.submit(func, item) - future.add_done_callback(lambda f: self._process_future_item(f, service, item)) - except RuntimeError as e: - logger.error(f"Failed to submit job: {e}") + fn = self.services[service].run + func = DB._run_thread_with_db_item #func = self.services[service].run # self._run_thread_with_db_item + future = cur_executor.submit(func, fn, service, self, item) #cur_executor.submit(func) if item is None else cur_executor.submit(func, item) + future.add_done_callback(lambda f: self._process_future_item(f, service, item)) def display_top_allocators(self, snapshot, key_type='lineno', limit=10): top_stats = snapshot.compare_to(self.last_snapshot, 'lineno') @@ -374,24 +388,24 @@ def run(self): except Empty: self.dump_tracemalloc() continue + with db.Session() as session: + existing_item = DB._get_item_from_db(session, event.item) + updated_item, next_service, items_to_submit = process_event( + existing_item, event.emitted_by, existing_item if existing_item is not None else event.item + ) - existing_item = self.media_items.get(event.item.item_id, None) - updated_item, next_service, items_to_submit = process_event( - existing_item, event.emitted_by, event.item - ) - - if updated_item and isinstance(existing_item, (Movie, Show)) and updated_item.state == States.Symlinked: - logger.success(f"Item has been completed: {updated_item.log_string}") - - if updated_item: - self.media_items.upsert(updated_item) + if updated_item and isinstance(existing_item, (Movie, Show)) and updated_item.state == States.Symlinked: + logger.success(f"Item has been completed: {updated_item.log_string}") - self._remove_from_running_items(event.item, "program.run") + self._remove_from_running_items(event.item, "program.run") - if items_to_submit: - for item_to_submit in items_to_submit: - self.add_to_running(item_to_submit, next_service.__name__) - self._submit_job(next_service, item_to_submit) + if items_to_submit: + for item_to_submit in items_to_submit: + self.add_to_running(item_to_submit, next_service.__name__) + self._submit_job(next_service, item_to_submit) + if isinstance(existing_item, MediaItem): + existing_item.store_state() + session.commit() def stop(self): if not self.running: @@ -405,8 +419,6 @@ def stop(self): executor["_executor"].shutdown(wait=False) if hasattr(self, "scheduler") and getattr(self.scheduler, 'running', False): self.scheduler.shutdown(wait=False) - if hasattr(self, "pickly") and getattr(self.pickly, 'running', False): - self.pickly.stop() logger.log("PROGRAM", "Riven has been stopped.") def add_to_queue(self, item: MediaItem) -> bool: @@ -422,4 +434,4 @@ def clear_queue(self): self.event_queue.task_done() except Empty: break - logger.log("PROGRAM", "Cleared the event queue") + logger.log("PROGRAM", "Cleared the event queue") \ No newline at end of file diff --git a/backend/program/scrapers/__init__.py b/src/program/scrapers/__init__.py similarity index 82% rename from backend/program/scrapers/__init__.py rename to src/program/scrapers/__init__.py index 81263fd9..04cf2db4 100644 --- a/backend/program/scrapers/__init__.py +++ b/src/program/scrapers/__init__.py @@ -3,7 +3,6 @@ from datetime import datetime from typing import Dict, Generator, List, Union -from program.cache import HashCache from program.media.item import Episode, MediaItem, Movie, Season, Show from program.media.state import States from program.scrapers.annatar import Annatar @@ -16,6 +15,7 @@ from program.scrapers.torbox import TorBoxScraper from program.scrapers.torrentio import Torrentio from program.scrapers.zilean import Zilean +from program.scrapers.comet import Comet from program.settings.manager import settings_manager from RTN import Torrent from utils.logger import logger @@ -35,7 +35,8 @@ def __init__(self): TorBoxScraper: TorBoxScraper(), Mediafusion: Mediafusion(), Prowlarr: Prowlarr(), - Zilean: Zilean() + Zilean: Zilean(), + Comet: Comet() } self.initialized = self.validate() if not self.initialized: @@ -54,7 +55,7 @@ def yield_incomplete_children(self, item: MediaItem) -> Union[List[Season], List return None def partial_state(self, item: MediaItem) -> bool: - if item.state != States.PartiallyCompleted: + if item.state != States.PartiallyCompleted or self.can_we_scrape(item): return False if isinstance(item, Show): sres = [s for s in item.seasons if s.state != States.Completed and s.is_released and self.should_submit(s)] @@ -98,34 +99,40 @@ def scrape(self, item: MediaItem, log = True) -> Dict[str, Torrent]: """Scrape an item.""" threads: List[threading.Thread] = [] results: Dict[str, str] = {} - results_lock = threading.Lock() + total_results = 0 + results_lock = threading.RLock() - item_copy = copy(item) - - def run_service(service, item_copy): - service_results = service.run(item_copy) + def run_service(service, item,): + nonlocal total_results + service_results = service.run(item) with results_lock: results.update(service_results) + total_results += len(service_results) for service_name, service in self.services.items(): if service.initialized: - thread = threading.Thread(target=run_service, args=(service, item_copy), name=service_name.__name__) + thread = threading.Thread(target=run_service, args=(service, item), name=service_name.__name__) threads.append(thread) thread.start() for thread in threads: thread.join() - sorted_streams: Dict[str, Torrent] = _parse_results(item_copy, results) + if total_results != len(results): + logger.debug(f"Scraped {item.log_string} with {total_results} results, removed {total_results - len(results)} duplicate hashes") + + sorted_streams: Dict[str, Torrent] = _parse_results(item, results) if sorted_streams and (log and settings_manager.settings.debug): item_type = item.type.title() top_results = sorted(sorted_streams.values(), key=lambda x: x.rank, reverse=True)[:10] for sorted_tor in top_results: - if isinstance(item, (Season, Episode)): - logger.debug(f"[{item_type} {item.number}] Parsed '{sorted_tor.data.parsed_title}' with rank {sorted_tor.rank} and ratio {sorted_tor.lev_ratio:.2f}: '{sorted_tor.raw_title}'") - else: + if isinstance(item, (Movie, Show)): logger.debug(f"[{item_type}] Parsed '{sorted_tor.data.parsed_title}' with rank {sorted_tor.rank} and ratio {sorted_tor.lev_ratio:.2f}: '{sorted_tor.raw_title}'") + if isinstance(item, Season): + logger.debug(f"[{item_type} {item.number}] Parsed '{sorted_tor.data.parsed_title}' with rank {sorted_tor.rank} and ratio {sorted_tor.lev_ratio:.2f}: '{sorted_tor.raw_title}'") + elif isinstance(item, Episode): + logger.debug(f"[{item_type} {item.parent.number}:{item.number}] Parsed '{sorted_tor.data.parsed_title}' with rank {sorted_tor.rank} and ratio {sorted_tor.lev_ratio:.2f}: '{sorted_tor.raw_title}'") return sorted_streams @classmethod diff --git a/backend/program/scrapers/annatar.py b/src/program/scrapers/annatar.py similarity index 95% rename from backend/program/scrapers/annatar.py rename to src/program/scrapers/annatar.py index 3ba1ab99..d699d7c9 100644 --- a/backend/program/scrapers/annatar.py +++ b/src/program/scrapers/annatar.py @@ -6,7 +6,8 @@ from requests import ConnectTimeout, ReadTimeout from requests.exceptions import RequestException from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get +from utils.request import get +from utils.ratelimiter import RateLimiter, RateLimitExceeded class Annatar: @@ -42,7 +43,7 @@ def validate(self) -> bool: logger.error("Annatar ratelimit must be a valid boolean.") return False try: - response = get("https://annatar.elfhosted.com/manifest.json", timeout=15) + response = get(f"{self.settings.url}/manifest.json", timeout=15) if not response.is_ok: return False return True @@ -129,4 +130,4 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: torrents[stream.hash] = stream.title - return torrents, len(response.data.media) + return torrents, len(response.data.media) \ No newline at end of file diff --git a/src/program/scrapers/comet.py b/src/program/scrapers/comet.py new file mode 100644 index 00000000..26b1a2f0 --- /dev/null +++ b/src/program/scrapers/comet.py @@ -0,0 +1,154 @@ +""" Comet scraper module """ +from typing import Dict, Union +import base64 +import json +from urllib.parse import quote + +from program.media.item import Episode, MediaItem, Movie, Season, Show +from program.settings.manager import settings_manager +from program.settings.models import CometConfig +from requests import ConnectTimeout, ReadTimeout +from requests.exceptions import RequestException +from utils.logger import logger +from utils.request import RateLimiter, RateLimitExceeded, get, ping + + +class Comet: + """Scraper for `Comet`""" + + def __init__(self): + self.key = "comet" + self.settings = settings_manager.settings.scraping.comet + self.timeout = self.settings.timeout + self.encoded_string = base64.b64encode(json.dumps({ + "indexers": self.settings.indexers, + "maxResults":0, + "filterTitles":False, + "resolutions":["All"], + "languages":["All"], + "debridService":"realdebrid", + "debridApiKey": settings_manager.settings.downloaders.real_debrid.api_key, + "debridStreamProxyPassword":"" + }).encode('utf-8')).decode('utf-8') + self.initialized = self.validate() + if not self.initialized: + return + self.second_limiter = RateLimiter(max_calls=1, period=5) if self.settings.ratelimit else None + logger.success("Comet initialized!") + + def validate(self) -> bool: + """Validate the Comet settings.""" + if not self.settings.enabled: + logger.warning("Comet is set to disabled.") + return False + if not self.settings.url: + logger.error("Comet URL is not configured and will not be used.") + return False + if not isinstance(self.timeout, int) or self.timeout <= 0: + logger.error("Comet timeout is not set or invalid.") + return False + if not isinstance(self.settings.ratelimit, bool): + logger.error("Comet ratelimit must be a valid boolean.") + return False + try: + url = f"{self.settings.url}/manifest.json" + response = ping(url=url, timeout=self.timeout) + if response.ok: + return True + except Exception as e: + logger.error(f"Comet failed to initialize: {e}", ) + return False + return True + + def run(self, item: MediaItem) -> Dict[str, str]: + """Scrape the comet site for the given media items + and update the object with scraped streams""" + if not item: + return {} + + try: + # Returns a dict of {infoHash: raw_title} + return self.scrape(item) + except RateLimitExceeded: + if self.hour_limiter: + self.hour_limiter.limit_hit() + else: + logger.warning(f"Comet ratelimit exceeded for item: {item.log_string}") + except ConnectTimeout: + logger.warning(f"Comet connection timeout for item: {item.log_string}") + except ReadTimeout: + logger.warning(f"Comet read timeout for item: {item.log_string}") + except RequestException as e: + logger.error(f"Comet request exception: {str(e)}") + except Exception as e: + logger.error(f"Comet exception thrown: {str(e)}") + return {} + + def scrape(self, item: MediaItem) -> Dict[str, str]: + """Scrape the given media item""" + data, stream_count = self.api_scrape(item) + if data: + logger.log("SCRAPER", f"Found {len(data)} streams out of {stream_count} for {item.log_string}") + else: + logger.log("NOT_FOUND", f"No streams found for {item.log_string}") + return data + + + def _determine_scrape(self, item: Union[Show, Season, Episode, Movie]) -> tuple[str, str, str]: + """Determine the scrape type and identifier for the given media item""" + try: + if isinstance(item, Show): + identifier, scrape_type, imdb_id = f":{item.seasons[0].number}:1", "series", item.imdb_id + elif isinstance(item, Season): + identifier, scrape_type, imdb_id = f":{item.number}:1", "series", item.parent.imdb_id + elif isinstance(item, Episode): + identifier, scrape_type, imdb_id = f":{item.parent.number}:{item.number}", "series", item.parent.parent.imdb_id + elif isinstance(item, Movie): + identifier, scrape_type, imdb_id = None, "movie", item.imdb_id + else: + logger.error(f"Invalid media item type") + return None, None, None + return identifier, scrape_type, imdb_id + except Exception as e: + logger.warning(f"Failed to determine scrape type or identifier for {item.log_string}: {e}") + return None, None, None + + def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: + """Wrapper for `Comet` scrape method""" + identifier, scrape_type, imdb_id = self._determine_scrape(item) + if not imdb_id: + return {}, 0 + + url = f"{self.settings.url}/{self.encoded_string}/stream/{scrape_type}/{imdb_id}{identifier or ''}.json" + + if self.second_limiter: + with self.second_limiter: + response = get(url, timeout=self.timeout) + else: + response = get(url, timeout=self.timeout) + + if not response.is_ok or not response.data.streams: + return {}, 0 + + torrents: Dict[str, str] = {} + for stream in response.data.streams: + # Split the URL by '/playback/' and then split the remaining part by '/' + + url_parts = stream.url.split('/playback/') + + if len(url_parts) != 2: + logger.warning(f'Comet Playback url can\'t be parsed: {stream.url}') + + end_parts = url_parts[1].split('/') + + if len(end_parts) != 2: + logger.warning(f'End part of Comet Playback url can\'t be parsed ({end_parts}): {stream.url}') + + hash = end_parts[0] + + if not hash: + continue + + torrents[hash] = stream.title + + return torrents, len(response.data.streams) diff --git a/backend/program/scrapers/jackett.py b/src/program/scrapers/jackett.py similarity index 97% rename from backend/program/scrapers/jackett.py rename to src/program/scrapers/jackett.py index c63d0398..69ea4e78 100644 --- a/backend/program/scrapers/jackett.py +++ b/src/program/scrapers/jackett.py @@ -12,7 +12,7 @@ from pydantic import BaseModel from requests import HTTPError, ReadTimeout, RequestException, Timeout from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded +from utils.ratelimiter import RateLimiter, RateLimitExceeded class JackettIndexer(BaseModel): @@ -281,4 +281,4 @@ def _log_indexers(self) -> None: if not indexer.movie_search_capabilities: logger.debug(f"Movie search not available for {indexer.title}") if not indexer.tv_search_capabilities: - logger.debug(f"TV search not available for {indexer.title}") + logger.debug(f"TV search not available for {indexer.title}") \ No newline at end of file diff --git a/backend/program/scrapers/knightcrawler.py b/src/program/scrapers/knightcrawler.py similarity index 96% rename from backend/program/scrapers/knightcrawler.py rename to src/program/scrapers/knightcrawler.py index e24fdddf..dd0b61b1 100644 --- a/backend/program/scrapers/knightcrawler.py +++ b/src/program/scrapers/knightcrawler.py @@ -7,7 +7,8 @@ from requests import ConnectTimeout, ReadTimeout from requests.exceptions import RequestException from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get, ping +from utils.request import get, ping +from utils.ratelimiter import RateLimiter, RateLimitExceeded class Knightcrawler: @@ -40,7 +41,7 @@ def validate(self) -> bool: try: url = f"{self.settings.url}/{self.settings.filter}/manifest.json" response = ping(url=url, timeout=self.timeout) - if response.ok: + if response.is_ok: return True except Exception as e: logger.error(f"Knightcrawler failed to initialize: {e}", ) @@ -114,4 +115,4 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: torrents[stream.infoHash] = raw_title - return torrents, len(response.data.streams) + return torrents, len(response.data.streams) \ No newline at end of file diff --git a/backend/program/scrapers/mediafusion.py b/src/program/scrapers/mediafusion.py similarity index 97% rename from backend/program/scrapers/mediafusion.py rename to src/program/scrapers/mediafusion.py index 92207d93..832e6a49 100644 --- a/backend/program/scrapers/mediafusion.py +++ b/src/program/scrapers/mediafusion.py @@ -10,7 +10,8 @@ from requests import ConnectTimeout, ReadTimeout from requests.exceptions import RequestException from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get, ping +from utils.request import get, ping +from utils.ratelimiter import RateLimiter, RateLimitExceeded class Mediafusion: @@ -87,7 +88,7 @@ def validate(self) -> bool: try: url = f"{self.settings.url}/manifest.json" response = ping(url=url, timeout=self.timeout) - return response.ok + return response.is_ok except Exception as e: logger.error(f"Mediafusion failed to initialize: {e}") return False @@ -151,4 +152,4 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: torrents[info_hash] = raw_title - return torrents, len(response.data.streams) + return torrents, len(response.data.streams) \ No newline at end of file diff --git a/backend/program/scrapers/orionoid.py b/src/program/scrapers/orionoid.py similarity index 95% rename from backend/program/scrapers/orionoid.py rename to src/program/scrapers/orionoid.py index f7d3b191..e64ba5a4 100644 --- a/backend/program/scrapers/orionoid.py +++ b/src/program/scrapers/orionoid.py @@ -7,7 +7,8 @@ from requests import ConnectTimeout, ReadTimeout from requests.exceptions import RequestException from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get +from utils.request import get +from utils.ratelimiter import RateLimiter, RateLimitExceeded KEY_APP = "D3CH6HMX9KD9EMD68RXRCDUNBDJV5HRR" @@ -188,4 +189,4 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict, int]: torrents[stream.file.hash] = stream.file.name - return torrents, len(response.data.data.streams) + return torrents, len(response.data.data.streams) \ No newline at end of file diff --git a/backend/program/scrapers/prowlarr.py b/src/program/scrapers/prowlarr.py similarity index 97% rename from backend/program/scrapers/prowlarr.py rename to src/program/scrapers/prowlarr.py index 957030d2..622df41c 100644 --- a/backend/program/scrapers/prowlarr.py +++ b/src/program/scrapers/prowlarr.py @@ -13,7 +13,7 @@ from pydantic import BaseModel from requests import HTTPError, ReadTimeout, RequestException, Timeout from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded +from utils.ratelimiter import RateLimiter, RateLimitExceeded class ProwlarrIndexer(BaseModel): @@ -284,4 +284,4 @@ def _log_indexers(self) -> None: if not indexer.movie_search_capabilities: logger.debug(f"Movie search not available for {indexer.title}") if not indexer.tv_search_capabilities: - logger.debug(f"TV search not available for {indexer.title}") + logger.debug(f"TV search not available for {indexer.title}") \ No newline at end of file diff --git a/backend/program/scrapers/shared.py b/src/program/scrapers/shared.py similarity index 94% rename from backend/program/scrapers/shared.py rename to src/program/scrapers/shared.py index e268eb21..4ad4f43d 100644 --- a/backend/program/scrapers/shared.py +++ b/src/program/scrapers/shared.py @@ -3,7 +3,6 @@ from datetime import datetime from typing import Dict, Set -from program.cache import hash_cache from program.media.item import Episode, MediaItem, Movie, Season, Show from program.settings.manager import settings_manager from program.settings.versions import models @@ -13,7 +12,7 @@ settings_model = settings_manager.settings.ranking ranking_model = models.get(settings_model.profile) -rtn = RTN(settings_model, ranking_model) +rtn = RTN(settings_model, ranking_model, 0.821) def _get_stremio_identifier(item: MediaItem) -> str: @@ -43,7 +42,7 @@ def _parse_results(item: MediaItem, results: Dict[str, str]) -> Dict[str, Torren needed_seasons = [season.number for season in item.seasons] for infohash, raw_title in results.items(): - if infohash in processed_infohashes or hash_cache.is_blacklisted(infohash): + if infohash in processed_infohashes: continue try: @@ -107,9 +106,10 @@ def _parse_results(item: MediaItem, results: Dict[str, str]) -> Dict[str, Torren processed_infohashes.add(infohash) except (ValueError, AttributeError) as e: - logger.error(f"Failed to parse {raw_title}: {e}") + # logger.error(f"Failed to parse: '{raw_title}' - {e}") continue - except GarbageTorrent: + except GarbageTorrent as e: + # logger.debug(f"Trashing torrent {infohash}: '{raw_title}'") continue if torrents: diff --git a/backend/program/scrapers/torbox.py b/src/program/scrapers/torbox.py similarity index 96% rename from backend/program/scrapers/torbox.py rename to src/program/scrapers/torbox.py index 290aa167..329fe005 100644 --- a/backend/program/scrapers/torbox.py +++ b/src/program/scrapers/torbox.py @@ -8,7 +8,8 @@ from RTN import RTN, Torrent, sort_torrents from RTN.exceptions import GarbageTorrent from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get, ping +from utils.request import get, ping +from utils.ratelimiter import RateLimiter, RateLimitExceeded class TorBoxScraper: @@ -38,7 +39,7 @@ def validate(self) -> bool: try: response = ping(f"{self.base_url}/torrents/imdb:tt0944947?metadata=false&season=1&episode=1", timeout=self.timeout) - return response.ok + return response.is_ok except Exception as e: logger.exception(f"Error validating TorBox Scraper: {e}") return False @@ -118,4 +119,4 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: torrents[info_hash] = raw_title - return torrents, len(response.data.data.torrents) + return torrents, len(response.data.data.torrents) \ No newline at end of file diff --git a/backend/program/scrapers/torrentio.py b/src/program/scrapers/torrentio.py similarity index 94% rename from backend/program/scrapers/torrentio.py rename to src/program/scrapers/torrentio.py index 7f7600d2..fffff7c2 100644 --- a/backend/program/scrapers/torrentio.py +++ b/src/program/scrapers/torrentio.py @@ -7,7 +7,8 @@ from requests import ConnectTimeout, ReadTimeout from requests.exceptions import RequestException from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, get, ping +from utils.request import get, ping +from utils.ratelimiter import RateLimiter, RateLimitExceeded class Torrentio: @@ -42,7 +43,7 @@ def validate(self) -> bool: try: url = f"{self.settings.url}/{self.settings.filter}/manifest.json" response = ping(url=url, timeout=10) - if response.ok: + if response.is_ok: return True except Exception as e: logger.error(f"Torrentio failed to initialize: {e}", ) @@ -130,4 +131,4 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: raw_title = stream_title.split("\n")[-1].split("/")[-1] if isinstance(item, Episode) else stream_title.split("\n")[0] torrents[stream.infoHash] = raw_title - return torrents, len(response.data.streams) + return torrents, len(response.data.streams) \ No newline at end of file diff --git a/backend/program/scrapers/zilean.py b/src/program/scrapers/zilean.py similarity index 73% rename from backend/program/scrapers/zilean.py rename to src/program/scrapers/zilean.py index e4cdd0b8..a82d827e 100644 --- a/backend/program/scrapers/zilean.py +++ b/src/program/scrapers/zilean.py @@ -8,7 +8,8 @@ from requests import ConnectTimeout, ReadTimeout from requests.exceptions import RequestException from utils.logger import logger -from utils.request import RateLimiter, RateLimitExceeded, ping, post +from utils.request import ping, get +from utils.ratelimiter import RateLimiter, RateLimitExceeded class Zilean: @@ -18,13 +19,15 @@ def __init__(self): self.key = "zilean" self.api_key = None self.downloader = None + self.rate_limiter = None self.app_settings: AppModel = settings_manager.settings self.settings = self.app_settings.scraping.zilean self.timeout = self.settings.timeout self.initialized = self.validate() if not self.initialized: return - self.second_limiter = RateLimiter(max_calls=1, period=2) if self.settings.ratelimit else None + if self.settings.ratelimit: + self.rate_limiter = RateLimiter(max_calls=1, period=2) logger.success("Zilean initialized!") def validate(self) -> bool: @@ -44,8 +47,8 @@ def validate(self) -> bool: try: url = f"{self.settings.url}/healthchecks/ping" - response = ping(url=url, timeout=self.timeout) - return response.ok + response = ping(url=url, timeout=self.timeout, specific_rate_limiter=self.rate_limiter) + return response.is_ok except Exception as e: logger.error(f"Zilean failed to initialize: {e}") return False @@ -58,8 +61,8 @@ def run(self, item: MediaItem) -> Dict[str, str]: try: return self.scrape(item) except RateLimitExceeded: - if self.second_limiter: - self.second_limiter.limit_hit() + if self.rate_limiter: + self.rate_limiter.limit_hit() else: logger.warning(f"Zilean ratelimit exceeded for item: {item.log_string}") except ConnectTimeout: @@ -87,24 +90,31 @@ def api_scrape(self, item: MediaItem) -> tuple[Dict[str, str], int]: if not title: return {}, 0 - url = f"{self.settings.url}/dmm/search" - payload = {"queryText": title} + url = f"{self.settings.url}/dmm/filtered" + params = {"Query": title} - if self.second_limiter: - with self.second_limiter: - response = post(url, json=payload, timeout=self.timeout) - else: - response = post(url, json=payload, timeout=self.timeout) + if isinstance(item, MediaItem) and hasattr(item, 'year'): + params["Year"] = item.year + + if isinstance(item, Show): + params["Season"] = 1 + elif isinstance(item, Season): + params["Season"] = item.number + elif isinstance(item, Episode): + params["Season"] = item.parent.number + params["Episode"] = item.number + + response = get(url, params=params, timeout=self.timeout, specific_rate_limiter=self.rate_limiter) if not response.is_ok or not response.data: return {}, 0 torrents: Dict[str, str] = {} - + for result in response.data: - if not result.filename or not result.infoHash: + if not result.rawTitle or not result.infoHash: continue - torrents[result.infoHash] = result.filename + torrents[result.infoHash] = result.rawTitle - return torrents, len(response.data) + return torrents, len(response.data) \ No newline at end of file diff --git a/backend/program/settings/__init__.py b/src/program/settings/__init__.py similarity index 100% rename from backend/program/settings/__init__.py rename to src/program/settings/__init__.py diff --git a/backend/program/settings/manager.py b/src/program/settings/manager.py similarity index 99% rename from backend/program/settings/manager.py rename to src/program/settings/manager.py index ca1b7c01..3850d691 100644 --- a/backend/program/settings/manager.py +++ b/src/program/settings/manager.py @@ -32,7 +32,7 @@ def register_observer(self, observer): def notify_observers(self): for observer in self.observers: observer() - + def check_environment(self, settings, prefix="", seperator="_"): checked_settings = {} for key, value in settings.items(): @@ -56,7 +56,7 @@ def check_environment(self, settings, prefix="", seperator="_"): else: checked_settings[key] = value return checked_settings - + def load(self, settings_dict: dict | None = None): """Load settings from file, validating against the AppModel schema.""" try: @@ -66,6 +66,7 @@ def load(self, settings_dict: dict | None = None): if os.environ.get("RIVEN_FORCE_ENV", "false").lower() == "true": settings_dict = self.check_environment(settings_dict, "RIVEN") self.settings = AppModel.model_validate(settings_dict) + self.save() except ValidationError as e: logger.error(f"Error validating settings: {e}") raise diff --git a/src/program/settings/migratable.py b/src/program/settings/migratable.py new file mode 100644 index 00000000..999a5ea9 --- /dev/null +++ b/src/program/settings/migratable.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class MigratableBaseModel(BaseModel): + def __init__(self, **data): + for field_name, field in self.model_fields.items(): + if field_name not in data: + default_value = field.default if field.default is not None else None + data[field_name] = default_value + super().__init__(**data) + diff --git a/backend/program/settings/models.py b/src/program/settings/models.py similarity index 83% rename from backend/program/settings/models.py rename to src/program/settings/models.py index 9ac80406..72d1b72f 100644 --- a/backend/program/settings/models.py +++ b/src/program/settings/models.py @@ -1,13 +1,15 @@ """Riven settings models""" from pathlib import Path -from typing import Callable, Dict, List +from typing import Callable, Dict, List, Any from pydantic import BaseModel, field_validator from RTN.models import CustomRank, SettingsModel + +from program.settings.migratable import MigratableBaseModel from utils import version_file_path -class Observable(BaseModel): +class Observable(MigratableBaseModel): class Config: arbitrary_types_allowed = True @@ -23,14 +25,32 @@ def __setattr__(self, name, value): with self._notify_observers_context(): self.__class__._notify_observers() + @staticmethod + def _notify_observers_context(): + class NotifyContextManager: + def __enter__(self_): + pass + + def __exit__(self_, exc_type, exc_value, traceback): + pass + + return NotifyContextManager() + # Download Services -class DebridModel(Observable): +class RealDebridModel(Observable): enabled: bool = False api_key: str = "" - proxy_enabled: bool = False - proxy_url: str = "" + proxy_enabled: bool = False + proxy_url: str = "" + + +class AllDebridModel(Observable): + enabled: bool = False + api_key: str = "" + proxy_enabled: bool = False + proxy_url: str = "" class TorboxModel(Observable): @@ -39,11 +59,12 @@ class TorboxModel(Observable): class DownloadersModel(Observable): - movie_filesize_min: int = 200 # MB - movie_filesize_max: int = -1 # MB (-1 is no limit) + movie_filesize_min: int = 200 # MB + movie_filesize_max: int = -1 # MB (-1 is no limit) episode_filesize_min: int = 40 # MB episode_filesize_max: int = -1 # MB (-1 is no limit) - real_debrid: DebridModel = DebridModel() + real_debrid: RealDebridModel = RealDebridModel() + all_debrid: AllDebridModel = AllDebridModel() torbox: TorboxModel = TorboxModel() @@ -168,6 +189,21 @@ class KnightcrawlerConfig(Observable): timeout: int = 30 ratelimit: bool = True + +class CometConfig(Observable): + enabled: bool = False + url: str = "http://localhost:8000" + indexers: List[str] = [ + "bitsearch", + "eztv", + "thepiratebay", + "therarbg", + "yts" + ] + timeout: int = 30 + ratelimit: bool = True + + class ZileanConfig(Observable): enabled: bool = False url: str = "http://localhost:8181" @@ -238,6 +274,7 @@ class ScraperModel(Observable): torbox_scraper: TorBoxScraperConfig = TorBoxScraperConfig() mediafusion: MediafusionConfig = MediafusionConfig() zilean: ZileanConfig = ZileanConfig() + comet: CometConfig = CometConfig() # Version Ranking Model (set application defaults here!) @@ -291,6 +328,12 @@ def get_version() -> str: with open(version_file_path.resolve()) as file: return file.read() or "x.x.x" +class LoggingModel(Observable): + ... + +class DatabaseModel(Observable): + host: str = "postgresql+psycopg2://postgres:postgres@localhost/riven" + class AppModel(Observable): version: str = get_version() @@ -306,3 +349,11 @@ class AppModel(Observable): scraping: ScraperModel = ScraperModel() ranking: RTNSettingsModel = RTNSettingsModel() indexer: IndexerModel = IndexerModel() + database: DatabaseModel = DatabaseModel() + + def __init__(self, **data: Any): + current_version = get_version() + existing_version = data.get('version', current_version) + super().__init__(**data) + if existing_version < current_version: + self.version = current_version diff --git a/backend/program/settings/versions.py b/src/program/settings/versions.py similarity index 99% rename from backend/program/settings/versions.py rename to src/program/settings/versions.py index 8a28e3c6..6b35d8ce 100644 --- a/backend/program/settings/versions.py +++ b/src/program/settings/versions.py @@ -107,8 +107,8 @@ class AnimeRanking(BaseRankingModel): class AllRanking(BaseRankingModel): - uhd: int = 2 - fhd: int = 3 + uhd: int = 1 + fhd: int = 1 hd: int = 1 sd: int = 1 dolby_video: int = 1 diff --git a/backend/program/state_transition.py b/src/program/state_transition.py similarity index 95% rename from backend/program/state_transition.py rename to src/program/state_transition.py index 6b6efc22..a0cccf41 100644 --- a/backend/program/state_transition.py +++ b/src/program/state_transition.py @@ -1,7 +1,6 @@ from program.content import Listrr, Mdblist, Overseerr, PlexWatchlist from program.content.trakt import TraktContent -from program.downloaders.realdebrid import Debrid -from program.downloaders.torbox import TorBoxDownloader +from program.downloaders import Downloader from program.indexers.trakt import TraktIndexer from program.libraries import SymlinkLibrary from program.media import Episode, MediaItem, Movie, Season, Show, States @@ -43,7 +42,7 @@ def process_event(existing_item: MediaItem | None, emitted_by: Service, item: Me items_to_submit = [item] if Scraping.can_we_scrape(item) else [] elif item.state == States.Scraped: - next_service = Debrid or TorBoxDownloader + next_service = Downloader items_to_submit = [item] elif item.state == States.Downloaded: diff --git a/backend/program/symlink.py b/src/program/symlink.py similarity index 86% rename from backend/program/symlink.py rename to src/program/symlink.py index acd0b612..46acb6ad 100644 --- a/backend/program/symlink.py +++ b/src/program/symlink.py @@ -5,16 +5,12 @@ import time from datetime import datetime from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union -from program.media.container import MediaItemContainer from program.media.item import Episode, Movie, Season, Show from program.settings.manager import settings_manager -from utils import data_dir_path from utils.logger import logger -from .cache import hash_cache - class Symlinker: """ @@ -25,10 +21,9 @@ class Symlinker: library_path (str): The absolute path of the location we will create our symlinks that point to the rclone_path. """ - def __init__(self, media_items: MediaItemContainer): + def __init__(self, media_items=None): self.key = "symlink" self.settings = settings_manager.settings.symlink - self.media_items = media_items self.rclone_path = self.settings.rclone_path self.initialized = self.validate() if not self.initialized: @@ -104,7 +99,8 @@ def run(self, item: Union[Movie, Show, Season, Episode]): logger.error(f"Exception thrown when creating symlink for {item.log_string}: {e}") item.set("symlinked_times", item.symlinked_times + 1) - yield item + if self.should_submit(item): + yield item @staticmethod def should_submit(item: Union[Movie, Show, Season, Episode]) -> bool: @@ -119,7 +115,6 @@ def should_submit(item: Union[Movie, Show, Season, Episode]) -> bool: for episode in season.episodes: if not episode.file or not episode.folder or episode.file == "None.mkv": logger.warning(f"Cannot submit {episode.log_string} for symlink: Invalid file or folder. Needs to be rescraped.") - blacklist_item(episode) all_episodes_ready = False elif not quick_file_check(episode): logger.debug(f"File not found for {episode.log_string} at the moment, waiting for it to become available") @@ -127,9 +122,6 @@ def should_submit(item: Union[Movie, Show, Season, Episode]) -> bool: all_episodes_ready = False break # Give up on the whole season if one episode is not found in 90 seconds if not all_episodes_ready: - for season in item.seasons: - for episode in season.episodes: - blacklist_item(episode) logger.warning(f"Cannot submit show {item.log_string} for symlink: One or more episodes need to be rescraped.") return all_episodes_ready @@ -138,7 +130,6 @@ def should_submit(item: Union[Movie, Show, Season, Episode]) -> bool: for episode in item.episodes: if not episode.file or not episode.folder or episode.file == "None.mkv": logger.warning(f"Cannot submit {episode.log_string} for symlink: Invalid file or folder. Needs to be rescraped.") - blacklist_item(episode) all_episodes_ready = False elif not quick_file_check(episode): logger.debug(f"File not found for {episode.log_string} at the moment, waiting for it to become available") @@ -146,15 +137,12 @@ def should_submit(item: Union[Movie, Show, Season, Episode]) -> bool: all_episodes_ready = False break # Give up on the whole season if one episode is not found in 90 seconds if not all_episodes_ready: - for episode in item.episodes: - blacklist_item(episode) logger.warning(f"Cannot submit season {item.log_string} for symlink: One or more episodes need to be rescraped.") return all_episodes_ready if isinstance(item, (Movie, Episode)): if not item.file or not item.folder or item.file == "None.mkv": logger.warning(f"Cannot submit {item.log_string} for symlink: Invalid file or folder. Needs to be rescraped.") - blacklist_item(item) return False if item.symlinked_times < 3: @@ -175,8 +163,7 @@ def should_submit(item: Union[Movie, Show, Season, Episode]) -> bool: logger.log("SYMLINKER", f"File found for {item.log_string}, creating symlink") return True else: - logger.log("SYMLINKER", f"File not found for {item.log_string} after 3 attempts, blacklisting") - blacklist_item(item) + logger.log("SYMLINKER", f"File not found for {item.log_string} after 3 attempts, skipping") return False logger.debug(f"Item {item.log_string} not submitted for symlink, file not found yet") @@ -226,8 +213,6 @@ def _symlink_single(self, item: Union[Movie, Episode]): if not item.symlinked and item.file and item.folder: if self._symlink(item): logger.log("SYMLINKER", f"Symlink created for {item.log_string}") - else: - logger.error(f"Failed to create symlink for {item.log_string}") def _symlink(self, item: Union[Movie, Episode]) -> bool: """Create a symlink for the given media item if it does not already exist.""" @@ -243,15 +228,16 @@ def _symlink(self, item: Union[Movie, Episode]) -> bool: logger.error(f"Item folder is None for {item.log_string}, cannot create symlink.") return False + filename = self._determine_file_name(item) + if not filename: + logger.error(f"Symlink filename is None for {item.log_string}, cannot create symlink.") + return False + extension = os.path.splitext(item.file)[1][1:] - symlink_filename = f"{self._determine_file_name(item)}.{extension}" + symlink_filename = f"{filename}.{extension}" destination = self._create_item_folders(item, symlink_filename) source = os.path.join(self.rclone_path, item.folder, item.file) - if not os.path.exists(source): - logger.error(f"Source file does not exist: {source}") - return False - try: if os.path.islink(destination): os.remove(destination) @@ -275,7 +261,7 @@ def _symlink(self, item: Union[Movie, Episode]) -> bool: # Validate the symlink if not os.path.islink(destination) or not os.path.exists(destination): - logger.error(f"Symlink validation failed for {item.log_string}: {destination}") + logger.error(f"Symlink validation failed for {item.log_string} from source: {source} to destination: {destination}") return False return True @@ -326,23 +312,6 @@ def create_folder_path(base_path, *subfolders): destination_path = os.path.join(destination_folder, filename.replace("/", "-")) return destination_path - @classmethod - def save_and_reload_media_items(cls, media_items: MediaItemContainer): - """Save and reload the media items to ensure consistency.""" - # Acquire write lock for the duration of save and reload to ensure consistency - media_items.lock.acquire_write() - try: - media_file_path = str(data_dir_path / "media.pkl") - media_items.save(media_file_path) - logger.log("FILES", "Successfully saved updated media items to disk") - media_items.load(media_file_path) - logger.log("FILES", "Successfully reloaded media items from disk") - except Exception as e: - logger.error(f"Failed to save or reload media items: {e}") - # Consider what rollback or recovery actions might be appropriate here - finally: - media_items.lock.release_write() - def extract_imdb_id(self, path: Path) -> Optional[str]: """Extract IMDb ID from the file or folder name using regex.""" match = re.search(r'tt\d+', path.name) @@ -378,8 +347,8 @@ def _determine_file_name(self, item) -> str | None: filename = f"{showname} ({showyear}) - Season {str(item.number).zfill(2)}" elif isinstance(item, Episode): episode_string = "" - episode_number = item.get_file_episodes() - if episode_number and episode_number[0] == item.number: + episode_number: List[int] = item.get_file_episodes() + if episode_number and item.number in episode_number: if len(episode_number) > 1: episode_string = f"e{str(episode_number[0]).zfill(2)}-e{str(episode_number[-1]).zfill(2)}" else: @@ -387,7 +356,7 @@ def _determine_file_name(self, item) -> str | None: if episode_string != "": showname = item.parent.parent.title showyear = item.parent.parent.aired_at.year - filename = f"{showname} ({showyear}) - s{str(item.parent.number).zfill(2)}{episode_string} - {item.title}" + filename = f"{showname} ({showyear}) - s{str(item.parent.number).zfill(2)}{episode_string} - {item.title}" return filename def delete_item_symlinks(self, item: Union[Movie, Episode, Season, Show]): @@ -459,8 +428,7 @@ async def wait_for_file(item: Union[Movie, Episode], timeout: int = 90) -> bool: if search_file(rclone_path, item): logger.log("SYMLINKER", f"File found for {item.log_string} after searching") return True - logger.log("SYMLINKER", f"File not found for {item.log_string} after waiting for {timeout} seconds, blacklisting") - blacklist_item(item) + logger.log("SYMLINKER", f"File not found for {item.log_string} after waiting for {timeout} seconds, skipping") return False def quick_file_check(item: Union[Movie, Episode]) -> bool: @@ -505,15 +473,6 @@ def search_file(rclone_path: Path, item: Union[Movie, Episode]) -> bool: logger.error(f"Error occurred while searching for file {filename} in {rclone_path}: {e}") return False -def blacklist_item(item): - """Blacklist the item and reset its attributes to be rescraped.""" - infohash = get_infohash(item) - reset_item(item) - if infohash: - hash_cache.blacklist(infohash) - else: - logger.error(f"Failed to retrieve hash for {item.log_string}, unable to blacklist") - def reset_item(item: Union[Movie, Show, Season, Episode], reset_times: bool = True) -> None: """Reset item attributes for rescraping.""" item.set("file", None) @@ -524,12 +483,3 @@ def reset_item(item: Union[Movie, Show, Season, Episode], reset_times: bool = Tr item.set("symlinked_times", 0) item.set("scraped_times", 0) logger.debug(f"Item {item.log_string} reset for rescraping") - -def get_infohash(item): - """Retrieve the infohash from the item or its parent.""" - infohash = item.active_stream.get("hash") - if isinstance(item, Episode) and not infohash: - infohash = item.parent.active_stream.get("hash") - if isinstance(item, Movie) and not infohash: - logger.error(f"Failed to retrieve hash for {item.log_string}, unable to blacklist") - return infohash diff --git a/backend/program/types.py b/src/program/types.py similarity index 83% rename from backend/program/types.py rename to src/program/types.py index 0dc8d352..087845c3 100644 --- a/backend/program/types.py +++ b/src/program/types.py @@ -2,7 +2,7 @@ from typing import Generator, Union from program.content import Listrr, Mdblist, Overseerr, PlexWatchlist, TraktContent -from program.downloaders import Debrid, TorBoxDownloader +from program.downloaders import RealDebridDownloader, TorBoxDownloader, AllDebridDownloader from program.libraries import SymlinkLibrary from program.media.item import MediaItem from program.scrapers import ( @@ -22,7 +22,7 @@ # Typehint classes Scraper = Union[Scraping, Torrentio, Knightcrawler, Mediafusion, Orionoid, Jackett, Annatar, TorBoxScraper, Zilean] Content = Union[Overseerr, PlexWatchlist, Listrr, Mdblist, TraktContent] -Downloader = Union[Debrid, TorBoxDownloader] +Downloader = Union[RealDebridDownloader, TorBoxDownloader, AllDebridDownloader] Service = Union[Content, SymlinkLibrary, Scraper, Downloader, Symlinker, Updater] MediaItemGenerator = Generator[MediaItem, None, MediaItem | None] ProcessedEvent = (MediaItem, Service, list[MediaItem]) @@ -31,4 +31,4 @@ @dataclass class Event: emitted_by: Service - item: MediaItem + item: MediaItem \ No newline at end of file diff --git a/backend/program/updaters/__init__.py b/src/program/updaters/__init__.py similarity index 100% rename from backend/program/updaters/__init__.py rename to src/program/updaters/__init__.py diff --git a/backend/program/updaters/local.py b/src/program/updaters/local.py similarity index 100% rename from backend/program/updaters/local.py rename to src/program/updaters/local.py diff --git a/backend/program/updaters/plex.py b/src/program/updaters/plex.py similarity index 100% rename from backend/program/updaters/plex.py rename to src/program/updaters/plex.py diff --git a/backend/pytest.ini b/src/pytest.ini similarity index 100% rename from backend/pytest.ini rename to src/pytest.ini diff --git a/backend/tests/test_container.py b/src/tests/test_container.py similarity index 100% rename from backend/tests/test_container.py rename to src/tests/test_container.py diff --git a/backend/tests/test_debrid_matching.py b/src/tests/test_debrid_matching.py similarity index 100% rename from backend/tests/test_debrid_matching.py rename to src/tests/test_debrid_matching.py diff --git a/src/tests/test_ranking.py b/src/tests/test_ranking.py new file mode 100644 index 00000000..79284c55 --- /dev/null +++ b/src/tests/test_ranking.py @@ -0,0 +1,26 @@ +import pytest +from RTN import RTN, SettingsModel, Torrent, DefaultRanking + + +@pytest.fixture +def settings_model(): + return SettingsModel() + +@pytest.fixture +def ranking_model(): + return DefaultRanking() + +# basic implementation for testing ranking +def test_manual_fetch_check_from_user(settings_model, ranking_model): + rtn = RTN(settings_model, ranking_model, lev_threshold=0.9) + + item: Torrent = rtn.rank( + "Swamp People Serpent Invasion S03E05 720p WEB h264-KOGi[eztv re] mkv", + "c08a9ee8ce3a5c2c08865e2b05406273cabc97e7", + correct_title="Swamp People", + remove_trash=False, + threshold=0.9 + ) + + assert item.fetch is True, "Fetch should be True" + assert item.lev_ratio > 0, "Levenshtein ratio should be greater than 0" \ No newline at end of file diff --git a/src/tests/test_settings_migration.py b/src/tests/test_settings_migration.py new file mode 100644 index 00000000..4afc5b38 --- /dev/null +++ b/src/tests/test_settings_migration.py @@ -0,0 +1,69 @@ +import json +import os +from pathlib import Path +from program.settings.manager import SettingsManager + +TEST_VERSION = "9.9.9" +DATA_PATH = Path(os.curdir) / "data" + +# Sample old settings data +old_settings_data = { + "version": "0.7.5", + "debug": True, + "log": True, + "force_refresh": False, + "map_metadata": True, + "tracemalloc": False, + "downloaders": { + "movie_filesize_min": 200, + "movie_filesize_max": -1, + "episode_filesize_min": 40, + "episode_filesize_max": -1, + "real_debrid": { + "enabled": False, + "api_key": "", + "proxy_enabled": False, + "proxy_url": "" + }, + "all_debrid": { + "enabled": True, + "api_key": "12345678", + "proxy_enabled": False, + "proxy_url": "https://no_proxy.com" + }, + "torbox": { + "enabled": False, + "api_key": "" + } + }, +} + + +def test_load_and_migrate_settings(): + temp_settings_file = Path.joinpath(DATA_PATH, "settings.json") + version_file = Path.joinpath(DATA_PATH, "VERSION") + + try: + temp_settings_file.write_text(json.dumps(old_settings_data)) + version_file.write_text("9.9.9") + + import program.settings.models + program.settings.manager.data_dir_path = DATA_PATH + program.settings.models.version_file_path = version_file + settings_manager = SettingsManager() + + assert settings_manager.settings.debug == True + assert settings_manager.settings.log == True + assert settings_manager.settings.force_refresh == False + assert settings_manager.settings.map_metadata == True + assert settings_manager.settings.tracemalloc == False + assert settings_manager.settings.downloaders.movie_filesize_min == 200 + assert settings_manager.settings.downloaders.real_debrid.enabled == False + assert settings_manager.settings.downloaders.all_debrid.enabled == True + assert settings_manager.settings.downloaders.all_debrid.api_key == "12345678" + assert settings_manager.settings.downloaders.all_debrid.proxy_url == "https://no_proxy.com" + assert settings_manager.settings.database.host == "postgresql+psycopg2://postgres:postgres@localhost/riven" + assert settings_manager.settings.version == TEST_VERSION + finally: + temp_settings_file.unlink() + version_file.unlink() \ No newline at end of file diff --git a/backend/tests/test_states_processing.py b/src/tests/test_states_processing.py similarity index 95% rename from backend/tests/test_states_processing.py rename to src/tests/test_states_processing.py index fd7c6d35..c984e96f 100644 --- a/backend/tests/test_states_processing.py +++ b/src/tests/test_states_processing.py @@ -1,5 +1,5 @@ import pytest -from program.downloaders.realdebrid import Debrid +from program.downloaders.realdebrid import RealDebridDownloader from program.indexers.trakt import TraktIndexer from program.media.item import Episode, MediaItem, Movie, Season, Show from program.media.state import States @@ -126,8 +126,8 @@ def test_show_state_transitions(show): (States.Unknown, Program, TraktIndexer), # (States.Requested, TraktIndexer, TraktIndexer), (States.Indexed, TraktIndexer, Scraping), - (States.Scraped, Scraping, Debrid), - (States.Downloaded, Debrid, Symlinker), + (States.Scraped, Scraping, RealDebridDownloader), + (States.Downloaded, RealDebridDownloader, Symlinker), (States.Symlinked, Symlinker, PlexUpdater), (States.Completed, PlexUpdater, None) ]) @@ -150,8 +150,8 @@ def test_process_event_transitions_movie(state, service, next_service, movie): (States.Unknown, Program, TraktIndexer), # (States.Requested, TraktIndexer, TraktIndexer), (States.Indexed, TraktIndexer, Scraping), - (States.Scraped, Scraping, Debrid), - (States.Downloaded, Debrid, Symlinker), + (States.Scraped, Scraping, RealDebridDownloader), + (States.Downloaded, RealDebridDownloader, Symlinker), (States.Symlinked, Symlinker, PlexUpdater), (States.Completed, PlexUpdater, None) ]) @@ -181,8 +181,8 @@ def test_process_event_transition_shows(state, service, next_service, show): (States.Unknown, Program, TraktIndexer), # (States.Requested, TraktIndexer, TraktIndexer), (States.Indexed, TraktIndexer, Scraping), - (States.Scraped, Scraping, Debrid), - (States.Downloaded, Debrid, Symlinker), + (States.Scraped, Scraping, RealDebridDownloader), + (States.Downloaded, RealDebridDownloader, Symlinker), (States.Symlinked, Symlinker, PlexUpdater), (States.Completed, PlexUpdater, None) ]) @@ -225,4 +225,4 @@ def test_process_event_transitions_media_item_movie(state, service, next_service # if next_service is None: # assert next_service_result is None, f"Next service should be None for {service}" # else: -# assert next_service_result == next_service, f"Next service should be {next_service} for {service}" +# assert next_service_result == next_service, f"Next service should be {next_service} for {service}" \ No newline at end of file diff --git a/backend/tests/test_symlink_library.py b/src/tests/test_symlink_library.py similarity index 100% rename from backend/tests/test_symlink_library.py rename to src/tests/test_symlink_library.py diff --git a/backend/utils/__init__.py b/src/utils/__init__.py similarity index 100% rename from backend/utils/__init__.py rename to src/utils/__init__.py diff --git a/backend/utils/logger.py b/src/utils/logger.py similarity index 85% rename from backend/utils/logger.py rename to src/utils/logger.py index 192b95a6..5dcbeccc 100644 --- a/backend/utils/logger.py +++ b/src/utils/logger.py @@ -11,6 +11,8 @@ from utils import data_dir_path +LOG_ENABLED: bool = settings_manager.settings.log + def setup_logger(level): """Setup the logger""" logs_dir_path = data_dir_path / "logs" @@ -64,10 +66,25 @@ def get_log_settings(name, default_color, default_icon): "{module}.{function} - {message}" ) + # handlers = { + # "sink": log_filename, + # "level": level, + # "format": log_format, + # "rotation": "50 MB", + # "retention": "8 hours", + # "compression": None, + # "backtrace": False, + # "diagnose": True, + # "enqueue": True, + # } + + # if LOG_ENABLED: + # handlers.append(log_filename) + logger.configure(handlers=[ { "sink": sys.stderr, - "level": "DEBUG", + "level": level.upper() or "INFO", "format": log_format, "backtrace": False, "diagnose": False, @@ -75,7 +92,7 @@ def get_log_settings(name, default_color, default_icon): }, { "sink": log_filename, - "level": level, + "level": level.upper(), "format": log_format, "rotation": "50 MB", "retention": "8 hours", diff --git a/src/utils/ratelimiter.py b/src/utils/ratelimiter.py new file mode 100644 index 00000000..215d4172 --- /dev/null +++ b/src/utils/ratelimiter.py @@ -0,0 +1,43 @@ +import time +from multiprocessing import Lock +from requests import RequestException + + +class RateLimitExceeded(RequestException): + pass + + +class RateLimiter: + def __init__(self, max_calls, period, raise_on_limit=False): + self.max_calls = max_calls + self.period = period + self.tokens = max_calls + self.last_call = time.time() - period + self.lock = Lock() + self.raise_on_limit = raise_on_limit + + def limit_hit(self): + self.tokens = 0 + + def __enter__(self): + with self.lock: + current_time = time.time() + time_elapsed = current_time - self.last_call + + if time_elapsed >= self.period: + self.tokens = self.max_calls + + if self.tokens <= 0: + if self.raise_on_limit: + raise RateLimitExceeded("Rate limit exceeded") + time.sleep(self.period - time_elapsed) + self.last_call = time.time() + self.tokens = self.max_calls + else: + self.tokens -= 1 + + self.last_call = current_time + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass \ No newline at end of file diff --git a/backend/utils/request.py b/src/utils/request.py similarity index 53% rename from backend/utils/request.py rename to src/utils/request.py index 561c1baf..c7d99179 100644 --- a/backend/utils/request.py +++ b/src/utils/request.py @@ -1,8 +1,6 @@ -"""Requests wrapper""" import json import logging -import time -from multiprocessing import Lock +from contextlib import nullcontext from types import SimpleNamespace from typing import Optional @@ -12,6 +10,8 @@ from requests.exceptions import ConnectTimeout, RequestException from urllib3.util.retry import Retry from xmltodict import parse as parse_xml +from utils.useragents import user_agent_factory +from utils.ratelimiter import RateLimiter, RateLimitExceeded logger = logging.getLogger(__name__) @@ -36,11 +36,14 @@ def __init__(self, response: requests.Response, response_type=SimpleNamespace): def handle_response(self, response: requests.Response) -> dict: """Handle different types of responses.""" timeout_statuses = [408, 460, 504, 520, 524, 522, 598, 599] + rate_limit_statuses = [429] client_error_statuses = list(range(400, 451)) # 400-450 server_error_statuses = list(range(500, 512)) # 500-511 if self.status_code in timeout_statuses: raise ConnectTimeout(f"Connection timed out with status {self.status_code}", response=response) + if self.status_code in rate_limit_statuses: + raise RateLimitExceeded(f"Rate Limit Exceeded {self.status_code}", response=response) if self.status_code in client_error_statuses: raise RequestException(f"Client error with status {self.status_code}", response=response) if self.status_code in server_error_statuses: @@ -51,7 +54,7 @@ def handle_response(self, response: requests.Response) -> dict: content_type = response.headers.get("Content-Type", "") if not content_type or response.content == b"": return {} - + try: if "application/json" in content_type: if self.response_type == dict: @@ -75,29 +78,40 @@ def _handle_request_exception() -> SimpleNamespace: def _make_request( - method: str, - url: str, - data: dict = None, - params: dict = None, - timeout=5, - additional_headers=None, - retry_if_failed=True, - response_type=SimpleNamespace, - proxies=None, - json=None + method: str, + url: str, + data: dict = None, + params: dict = None, + timeout=5, + additional_headers=None, + retry_if_failed=True, + response_type=SimpleNamespace, + proxies=None, + json=None, + specific_rate_limiter: Optional[RateLimiter] = None, + overall_rate_limiter: Optional[RateLimiter] = None ) -> ResponseObject: session = requests.Session() if retry_if_failed: session.mount("http://", _adapter) session.mount("https://", _adapter) - headers = {"Content-Type": "application/json", "Accept": "application/json"} + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": user_agent_factory.get_random_user_agent() + } if additional_headers: headers.update(additional_headers) + specific_context = specific_rate_limiter if specific_rate_limiter else nullcontext() + overall_context = overall_rate_limiter if overall_rate_limiter else nullcontext() + try: - response = session.request( - method, url, headers=headers, data=data, params=params, timeout=timeout, proxies=proxies, json=json - ) + with overall_context: + with specific_context: + response = session.request( + method, url, headers=headers, data=data, params=params, timeout=timeout, proxies=proxies, json=json + ) except requests.exceptions.RequestException as e: logger.error(f"Request failed: {e}", exc_info=True) response = _handle_request_exception() @@ -107,20 +121,36 @@ def _make_request( return ResponseObject(response, response_type) -def ping(url: str, timeout=10, additional_headers=None, proxies=None): - return requests.Session().get(url, headers=additional_headers, timeout=timeout, proxies=proxies) +def ping( + url: str, + timeout=10, + additional_headers=None, + proxies=None, + params=None, + specific_rate_limiter: Optional[RateLimiter] = None, + overall_rate_limiter: Optional[RateLimiter] = None): + return get( + url, + additional_headers=additional_headers, + params=params, + timeout=timeout, + proxies=proxies, + specific_rate_limiter=specific_rate_limiter, + overall_rate_limiter=overall_rate_limiter) def get( - url: str, - timeout=10, - data=None, - params=None, - additional_headers=None, - retry_if_failed=True, - response_type=SimpleNamespace, - proxies=None, - json=None + url: str, + timeout=10, + data=None, + params=None, + additional_headers=None, + retry_if_failed=True, + response_type=SimpleNamespace, + proxies=None, + json=None, + specific_rate_limiter: Optional[RateLimiter] = None, + overall_rate_limiter: Optional[RateLimiter] = None ) -> ResponseObject: """Requests get wrapper""" return _make_request( @@ -133,19 +163,23 @@ def get( retry_if_failed=retry_if_failed, response_type=response_type, proxies=proxies, - json=json + json=json, + specific_rate_limiter=specific_rate_limiter, + overall_rate_limiter=overall_rate_limiter ) def post( - url: str, - data: Optional[dict] = None, - params: dict = None, - timeout=10, - additional_headers=None, - retry_if_failed=False, - proxies=None, - json: Optional[dict] = None + url: str, + data: Optional[dict] = None, + params: dict = None, + timeout=10, + additional_headers=None, + retry_if_failed=False, + proxies=None, + json: Optional[dict] = None, + specific_rate_limiter: Optional[RateLimiter] = None, + overall_rate_limiter: Optional[RateLimiter] = None ) -> ResponseObject: """Requests post wrapper""" return _make_request( @@ -157,18 +191,22 @@ def post( additional_headers=additional_headers, retry_if_failed=retry_if_failed, proxies=proxies, - json=json + json=json, + specific_rate_limiter=specific_rate_limiter, + overall_rate_limiter=overall_rate_limiter ) def put( - url: str, - data: dict = None, - timeout=10, - additional_headers=None, - retry_if_failed=False, - proxies=None, - json=None + url: str, + data: dict = None, + timeout=10, + additional_headers=None, + retry_if_failed=False, + proxies=None, + json=None, + specific_rate_limiter: Optional[RateLimiter] = None, + overall_rate_limiter: Optional[RateLimiter] = None ) -> ResponseObject: """Requests put wrapper""" return _make_request( @@ -179,18 +217,22 @@ def put( additional_headers=additional_headers, retry_if_failed=retry_if_failed, proxies=proxies, - json=json + json=json, + specific_rate_limiter=specific_rate_limiter, + overall_rate_limiter=overall_rate_limiter ) def delete( - url: str, - timeout=10, - data=None, - additional_headers=None, - retry_if_failed=False, - proxies=None, - json=None + url: str, + timeout=10, + data=None, + additional_headers=None, + retry_if_failed=False, + proxies=None, + json=None, + specific_rate_limiter: Optional[RateLimiter] = None, + overall_rate_limiter: Optional[RateLimiter] = None ) -> ResponseObject: """Requests delete wrapper""" return _make_request( @@ -201,7 +243,9 @@ def delete( additional_headers=additional_headers, retry_if_failed=retry_if_failed, proxies=proxies, - json=json + json=json, + specific_rate_limiter=specific_rate_limiter, + overall_rate_limiter=overall_rate_limiter ) @@ -216,77 +260,4 @@ def element_to_simplenamespace(element): attributes.update(children_as_ns) return SimpleNamespace(**attributes, text=element.text) - return element_to_simplenamespace(root) - -class RateLimitExceeded(Exception): - pass - - -class RateLimiter: - """ - A rate limiter class that limits the number of calls within a specified period. - - Args: - max_calls (int): The maximum number of calls allowed within the specified period. - period (float): The time period (in seconds) within which the calls are limited. - raise_on_limit (bool, optional): Whether to raise an exception when the rate limit is exceeded. - Defaults to False. - - Attributes: - max_calls (int): The maximum number of calls allowed within the specified period. - period (float): The time period (in seconds) within which the calls are limited. - tokens (int): The number of available tokens for making calls. - last_call (float): The timestamp of the last call made. - lock (threading.Lock): A lock used for thread-safety. - raise_on_limit (bool): Whether to raise an exception when the rate limit is exceeded. - - Methods: - limit_hit(): Resets the token count to 0, indicating that the rate limit has been hit. - __enter__(): Enters the rate limiter context and checks if a call can be made. - __exit__(): Exits the rate limiter context. - - Raises: - RateLimitExceeded: If the rate limit is exceeded and `raise_on_limit` is set to True. - """ - - def __init__(self, max_calls, period, raise_on_limit=False): - self.max_calls = max_calls - self.period = period - self.tokens = max_calls - self.last_call = time.time() - period - self.lock = Lock() - self.raise_on_limit = raise_on_limit - - def limit_hit(self): - """ - Resets the token count to 0, indicating that the rate limit has been hit. - """ - self.tokens = 0 - - def __enter__(self): - """ - Enters the rate limiter context and checks if a call can be made. - """ - with self.lock: - current_time = time.time() - time_elapsed = current_time - self.last_call - - if time_elapsed >= self.period: - self.tokens = self.max_calls - - if self.tokens <= 0: - if self.raise_on_limit: - raise RateLimitExceeded("Rate limit exceeded") - time.sleep(self.period - time_elapsed) - self.last_call = time.time() - self.tokens = self.max_calls - else: - self.tokens -= 1 - - self.last_call = current_time - return self - - def __exit__(self, exc_type, exc_value, traceback): - """ - Exits the rate limiter context. - """ + return element_to_simplenamespace(root) \ No newline at end of file diff --git a/src/utils/useragents.py b/src/utils/useragents.py new file mode 100644 index 00000000..99e33c6d --- /dev/null +++ b/src/utils/useragents.py @@ -0,0 +1,54 @@ +import random + + +class UserAgentFactory: + def __init__(self, user_agents: list): + self.user_agents = user_agents + + def get_random_user_agent(self): + return random.choice(self.user_agents) + + +# Sample user agents pool +user_agents_pool = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 5.1; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:27.0) Gecko/20100101 Firefox/27.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; AS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:30.0) Gecko/20100101 Firefox/30.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (X11; Linux i686; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:32.0) Gecko/20100101 Firefox/32.0", + "Mozilla/5.0 (Windows NT 6.1; rv:29.0) Gecko/20100101 Firefox/29.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0", + "curl/7.64.1", + "curl/7.58.0", + "curl/7.61.1", + "curl/7.55.1", + "curl/7.54.0", + "curl/7.65.3", + "curl/7.50.3", + "curl/7.67.0", + "curl/7.63.0", +] +user_agent_factory = UserAgentFactory(user_agents_pool) \ No newline at end of file