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}
-
-
-
-
-
-
-
-
-
-
-
-
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.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'}
-
-
- {
- setMode('dark');
- }}
- class="max-w-max"
- >
-
-
-
-
- Dark mode
-
-
-{:else}
-
-
- {
- setMode('light');
- }}
- class="max-w-max"
- >
-
-
-
-
- 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 @@
-
-
-
-
- More
-
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 @@
-
-
-svg]:size-3.5', className)}
- bind:this={el}
- {...$$restProps}
->
-
-
-
-
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 @@
-
-
-
-
- Next slide
-
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 @@
-
-
-
-
- Previous slide
-
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 @@
-
-
-
-
-
- Next
-
-
-
-
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 @@
-
-
-
-
-
-
- Previous
-
-
-
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 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 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 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 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}
-
-
-
-
-
-
{roundOff(nowPlaying.vote_average)}
-
-
-
-
{nowPlaying.release_date}
-
-
-
-
{nowPlaying.original_language}
-
-
-
-
{nowPlaying.overview}
-
-
-
-
- Request
-
-
-
- Details
-
-
-
-
-
-
- {/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}
-
-
-
-
-
-
-
-
-
-
-
-
{
- curTopRatedType = String(selected?.value);
- }}
- >
-
-
-
-
- Movies
- Shows
-
-
-
-
-
-
-
- {@const topRatedData =
- curTopRatedType === 'movie' ? data.moviesTopRated : data.showsTopRated}
- {#each topRatedData.data.results as showsTopRated, i}
-
-
-
-
-
-
- {showsTopRated.title || showsTopRated.name}
-
-
-
-
- {roundOff(showsTopRated.vote_average)}
-
-
tv
-
- {showsTopRated.release_date || showsTopRated.first_air_date}
-
-
- {showsTopRated.original_language}
-
-
-
- {showsTopRated.overview}
-
-
-
-
- Request
-
-
-
- Details
-
-
-
-
-
-
-
- {/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.
-
-
-
- Let's go
-
-
-
-
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}
-
-
-
-
- {#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}
-
-
{
- await getLatestVersion();
- }}
- disabled={updateLoading}
- variant="outline"
- size="sm"
- >
- {#if updateLoading}
-
- {:else}
-
- {/if}
- Check for updates
-
-
-
- {#each Object.keys(aboutData) as key}
-
-
-
- {formatWords(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
-
-
-
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}
-
- {/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