add media service

This commit is contained in:
Lukas Kreussel
2025-09-21 17:50:43 +02:00
parent 6cef24e002
commit 520b91a501
12 changed files with 679 additions and 772 deletions

1
dev/.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

View File

@@ -1,6 +1,6 @@
# Jellyfin Development Environment
A complete Docker Compose setup for testing Jellyswarrm with two preconfigured Jellyfin server instances and legally downloadable content.
A complete Docker Compose setup for testing Jellyswarrm with three preconfigured Jellyfin servers (Movies, TV Shows, Music) and legally downloadable content.
## 🚀 Quick Start
@@ -9,238 +9,159 @@ cd dev
docker-compose up -d
```
That's it! Docker Compose will:
1. Download legal content automatically
2. Start two preconfigured Jellyfin servers
3. Set up libraries automatically
What happens:
- Downloads legal sample content automatically
- Starts three Jellyfin servers (movies, tv, music)
- Initializes each server (skips wizard, creates library, ready to browse)
Then access:
- **Movies Server**: http://localhost:8096 (movies only)
- **TV Shows Server**: http://localhost:8097 (TV series only)
- Movies: http://localhost:8096
- TV Shows: http://localhost:8097
- Music: http://localhost:8098
## 👥 Preconfigured Users
## 👥 Users and libraries
Perfect! I've successfully created a development environment with preconfigured users and different passwords for each server:
- Each server creates an admin user automatically:
- Admin: `admin` / `password`
- Libraries are created via API and point to:
- Movies → `/media/movies`
- TV Shows → `/media/tv-shows`
- Music → `/media/music`
## ✅ What You Get
Note: Additional non-admin users are not created by default in this setup.
**🎬 Movies Server (localhost:8096)** - Dedicated movie library
- **Admin**: `admin` / `password`
- **User**: `user` / `movies`
## 🧩 Services
**📺 TV Shows Server (localhost:8097)** - Dedicated TV series library
- **Admin**: `admin` / `password`
- **User**: `user` / `shows`
From `docker-compose.yml`:
## 🚀 How to Use
- content-downloader
- Image: ghcr.io/astral-sh/uv:python3.11-alpine
- Runs `scripts/download-content.py` and writes into `./data/media` mounted as `/downloads`
The environment is completely automated:
- jellyfin-movies (http://localhost:8096)
- Image: jellyfin/jellyfin:latest
- Mounts `./data/media``/media` (read-only)
- Persists config in `./data/jellyfin-movies/{config,cache}`
1. **Start with progress visible**: `docker-compose up` (without -d to see download progress)
2. **Or start in background**: `docker-compose up -d` (then use `docker-compose logs -f content-downloader` to see progress)
3. **Wait for download**: Content downloads automatically (takes a few minutes)
4. **Initialize servers**: `docker-compose --profile init up` (sets up users and libraries)
5. **Access servers**: Both servers start with users already configured
6. **Log in**: Use the credentials above - no setup wizard needed!
- jellyfin-movies-init
- Image: ghcr.io/astral-sh/uv:python3.11-alpine
- Runs `scripts/init-jellyfin.py` with:
- URL=http://jellyfin-movies:8096
- COLLECTION_NAME=Movies
- COLLECTION_PATH=/media/movies
- COLLECTION_TYPE=movies
The content downloader will download several legally free movies and organize them into appropriate libraries automatically. Both Jellyfin servers are preconfigured to skip the setup wizard and have their libraries ready to go.
- jellyfin-tvshows (http://localhost:8097)
- Image: jellyfin/jellyfin:latest
- Mounts `./data/media``/media` (read-only)
- Persists config in `./data/jellyfin-tvshows/{config,cache}`
## 📁 What's Included
- jellyfin-tvshows-init
- Image: ghcr.io/astral-sh/uv:python3.11-alpine
- Runs `scripts/init-jellyfin.py` with:
- URL=http://jellyfin-tvshows:8096
- COLLECTION_NAME=Shows
- COLLECTION_PATH=/media/tv-shows
- COLLECTION_TYPE=tvshows
### Content Sources
All content is legally downloadable from:
- **Internet Archive**: Public domain movies and shows
- **Blender Foundation**: Creative Commons licensed films
- **Google Sample Videos**: Test content
- jellyfin-music (http://localhost:8098)
- Image: jellyfin/jellyfin:latest
- Mounts `./data/media``/media` (read-only)
- Persists config in `./data/jellyfin-music/{config,cache}`
### Movies (Public Domain & Creative Commons)
- Night of the Living Dead (1968) - Classic horror, public domain
- Plan 9 from Outer Space (1959) - Sci-fi B-movie, public domain
- The Cabinet of Dr. Caligari (1920) - German expressionist film, public domain
- Big Buck Bunny (2008) - Blender Foundation, CC license
- Sintel (2010) - Blender Foundation, CC license
- Tears of Steel (2012) - Blender Foundation, CC license
- Elephant's Dream (2006) - Blender Foundation, CC license
- jellyfin-music-init
- Image: ghcr.io/astral-sh/uv:python3.11-alpine
- Runs `scripts/init-jellyfin.py` with:
- URL=http://jellyfin-music:8096
- COLLECTION_NAME=Music
- COLLECTION_PATH=/media/music
- COLLECTION_TYPE=music
### TV Shows
- Blender Open Movies - Collection organized as TV series episodes
## 📁 Downloaded content
## 🛠️ Manual Commands
All content is legally downloadable. Current script includes:
### Start the environment with visible progress
```bash
docker-compose up
- Movies
- Night of the Living Dead (1968) — Internet Archive (Public Domain)
- Plan 9 from Outer Space (1959) — Internet Archive (Public Domain)
- Big Buck Bunny (2008) — Blender Foundation (CC)
- TV Shows
- The Cisco Kid (1950) — S01E01, S01E02 — Internet Archive (Public Domain)
- Music
- Kimiko Ishizaka — The Open Goldberg Variations (2012) — OGG — Internet Archive (CC0/PD)
- Kevin MacLeod — Royalty Free (2017) — MP3 — Internet Archive (CC-BY 3.0; attribution required)
- Josh Woodward — Breadcrumbs (Instrumental Version) — OGG — Internet Archive Jamendo mirror (CC)
Content is placed under `./data/media/` on the host:
```
data/media/
├── movies/
├── tv-shows/
└── music/
```
### Start the environment in background
```bash
docker-compose up -d
```
## 🛠️ Useful commands
### Initialize servers (after first startup)
```bash
docker-compose --profile init up
```
- Start with visible logs
```bash
docker-compose up
```
### View download progress (if running in background)
```bash
docker-compose logs -f content-downloader
```
### View initialization progress
```bash
docker-compose --profile init logs -f
```
### Stop the environment
```bash
docker-compose down
```
### View logs
```bash
docker-compose logs -f
```
### Restart services
```bash
docker-compose restart
```
### Clean restart (removes volumes)
```bash
docker-compose down -v
docker-compose up -d
```
## 📋 Setup Instructions
1. **Start everything**:
- Start in background
```bash
docker-compose up -d
```
2. **Access the servers**:
- **Movies**: http://localhost:8096 (preconfigured with movie library)
- **TV Shows**: http://localhost:8097 (preconfigured with TV series library)
3. **Initialize the servers**:
- Watch content download logs
```bash
docker-compose --profile init up
docker-compose logs -f content-downloader
```
4. **Both servers are fully configured**:
- Setup wizard is skipped
- Libraries are automatically created via API
- Content is downloaded and ready to browse
- Users are created automatically
- Stop everything
```bash
docker-compose down
```
No manual configuration needed!
- Restart services
```bash
docker-compose restart
```
## 🏗️ Architecture
- Clean restart (removes data in named directories)
```bash
docker-compose down -v
docker-compose up -d
```
## 🏗️ Layout
```
dev/
├── docker-compose.yml # Main compose file with all services
├── docker-compose.yml
├── scripts/
│ ├── download-content.sh # Content download script
── init-movies-server.sh # Movies server API initialization
│ └── init-tvshows-server.sh # TV shows server API initialization
│ ├── download-content.py
── init-jellyfin.py
├── data/
│ └── media/ # Downloaded media files (local folder)
│ └── media/
│ ├── movies/
│ ├── tv-shows/
│ └── CONTENT_SUMMARY.txt
└── README.md # This file
Docker Volumes:
├── jellyfin-movies-config # Movie server configuration
├── jellyfin-movies-cache # Movie server cache
├── jellyfin-tvshows-config # TV server configuration
└── jellyfin-tvshows-cache # TV server cache
│ └── music/
└── README.md
```
## 🔧 Configuration
## <EFBFBD> Permissions and environment
### Servers
- **Movies Server** (port 8096): Preconfigured with movie library pointing to `/media/movies`
- **TV Shows Server** (port 8097): Preconfigured with TV series library pointing to `/media/tv-shows`
- Containers run with `PUID=1000`, `PGID=1000`, `TZ=UTC` for predictable file ownership and timestamps.
- Media is mounted read-only to Jellyfin servers to avoid accidental writes by the apps.
### Users
Each server has two preconfigured users:
- **admin/password**: Administrator with full access
- **user/movies** (movies server) or **user/shows** (TV server): Regular user access
## 📜 Licenses and attribution
### Volumes
- Jellyfin configurations and caches are stored in Docker volumes
- **Media content is stored in `./data/media/`** (local folder on host)
- Content is automatically downloaded on first startup and accessible from host system
- Configuration is done via Jellyfin's REST API (no static config files needed)
### Environment Variables
- `PUID=1000` - User ID for file permissions
- `PGID=1000` - Group ID for file permissions
- `TZ=UTC` - Timezone
## 🧪 Testing Jellyswarrm
This environment is perfect for testing Jellyswarrm features:
1. **Multiple Server Support**: Two independent Jellyfin instances with different content types
2. **Real Content**: Actual video files with metadata
3. **Specialized Libraries**: One server for movies, one for TV shows
4. **Isolated Environment**: Fully contained in Docker with automatic setup
5. **No Manual Configuration**: Everything is preconfigured and ready to use
## 🐛 Troubleshooting
### Services won't start
```bash
# Check Docker is running
docker info
# Check logs
docker-compose logs
```
### Content download fails
```bash
# Check content downloader logs
docker-compose logs content-downloader
# Retry content download
docker-compose up content-downloader --force-recreate
# Check available space
df -h
```
### Permission issues
```bash
# Check Docker volume permissions
docker-compose exec jellyfin-movies ls -la /config
docker-compose exec jellyfin-tvshows ls -la /config
```
### Port conflicts
If ports 8096 or 8097 are in use, edit `docker-compose.yml`:
```yaml
ports:
- "8098:8096" # Change to available port
```
## 📜 Legal Notice
All included content is either:
- **Public Domain**: No copyright restrictions
- **Creative Commons**: Freely redistributable under CC licenses
- **Open Source**: Blender Foundation open movie projects
- Public domain items can be used freely.
- CC-BY items (e.g., Kevin MacLeod) require attribution if used or redistributed publicly. Keep attribution in your app/docs if you publish content beyond local testing.
Sources:
- [Internet Archive](https://archive.org/)
- [Blender Foundation](https://www.blender.org/about/projects/)
- [Google Sample Videos](https://goo.gl/A3JoZX)
## 🤝 Contributing
Feel free to add more legal content sources or improve the setup scripts!
- Internet Archivehttps://archive.org/
- Blender Foundationhttps://www.blender.org/about/projects/

View File

@@ -1,27 +1,10 @@
services:
content-downloader:
image: alpine:latest
container jellyfin-tvshows-init:
image: ghcr.io/astral-sh/uv:python3.11-alpine
container_name: jellyfin-tvshows-init
volumes:
- ./scripts/init-tvshows-server.py:/scripts/init-tvshows-server.py:ro
working_dir: /scripts
command: ["uv", "run", "--with", "jellyfin-apiclient-python", "/scripts/init-tvshows-server.py"]
networks:
- jellyfin-dev-net
depends_on:
jellyfin-tvshows:
condition: service_started
profiles:
- initn-content-downloader
volumes:
- ./data/media:/downloads
- ./scripts/download-content.sh:/scripts/download-content.sh:ro
working_dir: /downloads
command: sh -c "apk add --no-cache wget curl bash && sh /scripts/download-content.sh"
networks:
- jellyfin-dev-net
- ./scripts/download-content.py:/scripts/download-content.py:ro
command: ["uv", "run", "/scripts/download-content.py"]
tty: true
jellyfin-movies:
@@ -33,8 +16,8 @@ services:
- TZ=UTC
- JELLYFIN_PublishedServerUrl=http://localhost:8096
volumes:
- jellyfin-movies-config:/config
- jellyfin-movies-cache:/cache
- ./data/jellyfin-movies/config:/config
- ./data/jellyfin-movies/cache:/cache
- ./data/media:/media:ro
ports:
- "8096:8096"
@@ -49,16 +32,22 @@ services:
image: ghcr.io/astral-sh/uv:python3.11-alpine
container_name: jellyfin-movies-init
volumes:
- ./scripts/init-movies-server.py:/scripts/init-movies-server.py:ro
- ./scripts/init-jellyfin.py:/scripts/init-jellyfin.py:ro
working_dir: /scripts
command: ["uv", "run", "--with", "jellyfin-apiclient-python", "/scripts/init-movies-server.py"]
command: ["uv", "run", "/scripts/init-jellyfin.py"]
environment:
- URL=http://jellyfin-movies:8096
- USERNAME=admin
- PASSWORD=movies
- COLLECTION_NAME=Movies
- COLLECTION_PATH=/media/movies
- COLLECTION_TYPE=movies
networks:
- jellyfin-dev-net
depends_on:
jellyfin-movies:
condition: service_started
profiles:
- init
jellyfin-tvshows:
image: jellyfin/jellyfin:latest
@@ -69,8 +58,8 @@ services:
- TZ=UTC
- JELLYFIN_PublishedServerUrl=http://localhost:8097
volumes:
- jellyfin-tvshows-config:/config
- jellyfin-tvshows-cache:/cache
- ./data/jellyfin-tvshows/config:/config
- ./data/jellyfin-tvshows/cache:/cache
- ./data/media:/media:ro
ports:
- "8097:8096"
@@ -82,30 +71,66 @@ services:
condition: service_completed_successfully
jellyfin-tvshows-init:
image: python:3.11-alpine
image: ghcr.io/astral-sh/uv:python3.11-alpine
container_name: jellyfin-tvshows-init
volumes:
- ./scripts/init-tvshows-server.py:/scripts/init-tvshows-server.py:ro
- ./scripts/init-jellyfin.py:/scripts/init-jellyfin.py:ro
working_dir: /scripts
command: sh -c "pip install uv && uvx --with jellyfin-apiclient-python python /scripts/init-tvshows-server.py"
command: ["uv", "run", "/scripts/init-jellyfin.py"]
environment:
- URL=http://jellyfin-tvshows:8096
- USERNAME=admin
- PASSWORD=shows
- COLLECTION_NAME=Shows
- COLLECTION_PATH=/media/tv-shows
- COLLECTION_TYPE=tvshows
networks:
- jellyfin-dev-net
depends_on:
jellyfin-tvshows:
condition: service_started
profiles:
- init
jellyfin-music:
image: jellyfin/jellyfin:latest
container_name: jellyfin-music
environment:
- PUID=1000
- PGID=1000
- TZ=UTC
- JELLYFIN_PublishedServerUrl=http://localhost:8098
volumes:
- ./data/jellyfin-music/config:/config
- ./data/jellyfin-music/cache:/cache
- ./data/media:/media:ro
ports:
- "8098:8096"
restart: unless-stopped
networks:
- jellyfin-dev-net
depends_on:
content-downloader:
condition: service_completed_successfully
jellyfin-music-init:
image: ghcr.io/astral-sh/uv:python3.11-alpine
container_name: jellyfin-music-init
volumes:
- ./scripts/init-jellyfin.py:/scripts/init-jellyfin.py:ro
working_dir: /scripts
command: ["uv", "run", "/scripts/init-jellyfin.py"]
environment:
- URL=http://jellyfin-music:8096
- USERNAME=admin
- PASSWORD=music
- COLLECTION_NAME=Music
- COLLECTION_PATH=/media/music
- COLLECTION_TYPE=music
networks:
- jellyfin-dev-net
depends_on:
jellyfin-music:
condition: service_started
networks:
jellyfin-dev-net:
driver: bridge
volumes:
jellyfin-movies-config:
driver: local
jellyfin-movies-cache:
driver: local
jellyfin-tvshows-config:
driver: local
jellyfin-tvshows-cache:
driver: local
driver: bridge

10
dev/pyproject.toml Normal file
View File

@@ -0,0 +1,10 @@
[project]
name = "dev"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.28.1",
"jellyfin-apiclient-python>=1.11.0",
]

View File

@@ -0,0 +1,170 @@
import os
import urllib.request
import urllib.error
from pathlib import Path
def download_with_progress(url, filepath):
"""Download a file with progress indication"""
try:
print(f" 📥 Downloading to {filepath}...")
urllib.request.urlretrieve(url, filepath)
return True
except urllib.error.URLError as e:
print(f" ⚠️ Failed to download: {e}")
return False
def ensure_directory(path):
"""Create directory if it doesn't exist"""
Path(path).mkdir(parents=True, exist_ok=True)
def main():
print("🎬 Starting content download for Jellyfin development servers...")
# Create directory structure
print("📁 Creating directory structure...")
downloads_base = Path("/downloads")
movies_dir = downloads_base / "movies"
tv_shows_dir = downloads_base / "tv-shows"
music_dir = downloads_base / "music"
ensure_directory(movies_dir)
ensure_directory(tv_shows_dir)
ensure_directory(music_dir)
print("🎭 Downloading public domain movies from Internet Archive...")
# Night of the Living Dead (1968) - Public Domain
print(" 📥 Night of the Living Dead (1968)...")
night_dir = movies_dir / "Night of the Living Dead (1968)"
night_file = night_dir / "Night of the Living Dead (1968).mp4"
ensure_directory(night_dir)
if not night_file.exists():
download_with_progress(
"https://archive.org/download/night_of_the_living_dead_dvd/Night.mp4",
night_file
)
else:
print(" ✅ Night of the Living Dead already exists, skipping download")
# Plan 9 from Outer Space (1959) - Public Domain
print(" 📥 Plan 9 from Outer Space (1959)...")
plan9_dir = movies_dir / "Plan 9 from Outer Space (1959)"
plan9_file = plan9_dir / "Plan 9 from Outer Space (1959).mp4"
ensure_directory(plan9_dir)
if not plan9_file.exists():
download_with_progress(
"https://archive.org/download/plan-9-from-outer-space-1959_ed-wood/PLAN%209%20FROM%20OUTER%20SPACE%201959.ia.mp4",
plan9_file
)
else:
print(" ✅ Plan 9 from Outer Space already exists, skipping download")
print("🎨 Downloading Creative Commons content...")
# Big Buck Bunny
print(" 📥 Big Buck Bunny...")
bunny_dir = movies_dir / "Big Buck Bunny (2008)"
bunny_file = bunny_dir / "Big Buck Bunny (2008).mp4"
ensure_directory(bunny_dir)
if not bunny_file.exists():
download_with_progress(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
bunny_file
)
else:
print(" ✅ Big Buck Bunny already exists, skipping download")
print("📺 Downloading public domain TV series...")
# The Cisco Kid - Public Domain Western Series
print(" 📥 The Cisco Kid (1950-1956)...")
cisco_dir = tv_shows_dir / "The Cisco Kid (1950)" / "Season 01"
ensure_directory(cisco_dir)
# Episode 1
ep1_file = cisco_dir / "The Cisco Kid - S01E01 - The Gay Caballero.mp4"
if not ep1_file.exists():
download_with_progress(
"https://archive.org/download/TheCiscoKidpublicdomain/The_Cisco_Kid_s01e01.mp4",
ep1_file
)
else:
print(" ✅ Cisco Kid S01E01 already exists, skipping download")
# Episode 2
ep2_file = cisco_dir / "The Cisco Kid - S01E02 - Boomerang.mp4"
if not ep2_file.exists():
download_with_progress(
"https://archive.org/download/TheCiscoKidpublicdomain/The_Cisco_Kid_s01e02.mp4",
ep2_file
)
else:
print(" ✅ Cisco Kid S01E02 already exists, skipping download")
print("🎵 Downloading royalty-free and freely-copiable music albums...")
# Album 1: The Open Goldberg Variations (2012) — Kimiko Ishizaka (CC0/Public Domain)
# Source: https://archive.org/details/The_Open_Goldberg_Variations-11823
print(" 🎹 Downloading 'The Open Goldberg Variations' (Kimiko Ishizaka)...")
ogv_dir = music_dir / "Kimiko Ishizaka" / "The Open Goldberg Variations (2012)"
ensure_directory(ogv_dir)
ogv_tracks = [
("01 - Aria.ogg", "Kimiko_Ishizaka_-_01_-_Aria.ogg"),
("02 - Variatio 1 a 1 Clav.ogg", "Kimiko_Ishizaka_-_02_-_Variatio_1_a_1_Clav.ogg"),
("03 - Variatio 2 a 1 Clav.ogg", "Kimiko_Ishizaka_-_03_-_Variatio_2_a_1_Clav.ogg"),
("04 - Variatio 3 a 1 Clav. Canone all'Unisuono.ogg", "Kimiko_Ishizaka_-_04_-_Variatio_3_a_1_Clav_Canone_allUnisuono.ogg"),
]
for display_name, src_name in ogv_tracks:
target = ogv_dir / display_name
if not target.exists():
download_with_progress(
f"https://archive.org/download/The_Open_Goldberg_Variations-11823/{src_name}",
target
)
else:
print(f"{display_name} already exists, skipping")
# Album 2: Kevin MacLeod — Royalty Free (2017) (CC-BY 3.0 — attribution required)
# Source: https://archive.org/details/Kevin-MacLeod_Royalty-Free_2017_FullAlbum
print(" 🎼 Downloading 'Kevin MacLeod: Royalty Free (2017)'...")
kml_dir = music_dir / "Kevin MacLeod" / "Royalty Free (2017)"
ensure_directory(kml_dir)
# Filenames on IA are simple track names under VBR MP3; no "Kevin MacLeod - 00 -" prefix
kml_tracks = [
("01 - Achaidh Cheide.mp3", "Achaidh%20Cheide.mp3"),
("02 - Achilles.mp3", "Achilles.mp3"),
]
for display_name, src_name in kml_tracks:
target = kml_dir / display_name
if not target.exists():
download_with_progress(
f"https://archive.org/download/Kevin-MacLeod_Royalty-Free_2017_FullAlbum/{src_name}",
target
)
else:
print(f"{display_name} already exists, skipping")
# Album 3: Josh Woodward — Breadcrumbs (Instrumental Version) (CC — Jamendo archive)
# Source: https://archive.org/details/jamendo-089689
print(" 🎵 Downloading 'Josh Woodward: Breadcrumbs (Instrumental Version)'...")
jw_dir = music_dir / "Josh Woodward" / "Breadcrumbs (Instrumental Version)"
ensure_directory(jw_dir)
# We'll fetch first three tracks to keep it small
for idx in [1, 2, 3]:
src = f"https://archive.org/download/jamendo-089689/{idx:02}.ogg"
target = jw_dir / f"{idx:02}.ogg"
if not target.exists():
download_with_progress(src, target)
else:
print(f" ✅ Track {idx:02}.ogg already exists, skipping")
print("🎉 Content download completed!")
if __name__ == "__main__":
main()

View File

@@ -1,68 +0,0 @@
#!/bin/bash
set -e
echo "🎬 Starting content download for Jellyfin development servers..."
# Create directory structure
echo "📁 Creating directory structure..."
mkdir -p /downloads/movies
mkdir -p /downloads/tv-shows
echo "🎭 Downloading public domain movies from Internet Archive..."
# Night of the Living Dead (1968) - Public Domain - Updated URL
echo " 📥 Night of the Living Dead (1968)..."
mkdir -p "/downloads/movies/Night of the Living Dead (1968)"
if [ ! -f "/downloads/movies/Night of the Living Dead (1968)/Night of the Living Dead (1968).mp4" ]; then
wget --progress=bar:force -O "/downloads/movies/Night of the Living Dead (1968)/Night of the Living Dead (1968).mp4" \
"https://archive.org/download/night_of_the_living_dead_dvd/Night.mp4" || echo " ⚠️ Failed to download Night of the Living Dead"
else
echo " ✅ Night of the Living Dead already exists, skipping download"
fi
# Plan 9 from Outer Space (1959) - Public Domain - Updated URL
echo " 📥 Plan 9 from Outer Space (1959)..."
mkdir -p "/downloads/movies/Plan 9 from Outer Space (1959)"
if [ ! -f "/downloads/movies/Plan 9 from Outer Space (1959)/Plan 9 from Outer Space (1959).mp4" ]; then
wget --progress=bar:force -O "/downloads/movies/Plan 9 from Outer Space (1959)/Plan 9 from Outer Space (1959).mp4" \
"https://archive.org/download/plan-9-from-outer-space-1959_ed-wood/PLAN%209%20FROM%20OUTER%20SPACE%201959.ia.mp4" || echo " ⚠️ Failed to download Plan 9 from Outer Space"
else
echo " ✅ Plan 9 from Outer Space already exists, skipping download"
fi
echo "🎨 Downloading Creative Commons content..."
# Big Buck Bunny
echo " 📥 Big Buck Bunny..."
mkdir -p "/downloads/movies/Big Buck Bunny (2008)"
if [ ! -f "/downloads/movies/Big Buck Bunny (2008)/Big Buck Bunny (2008).mp4" ]; then
wget --progress=bar:force -O "/downloads/movies/Big Buck Bunny (2008)/Big Buck Bunny (2008).mp4" \
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" || echo " ⚠️ Failed to download Big Buck Bunny"
else
echo " ✅ Big Buck Bunny already exists, skipping download"
fi
echo "📺 Downloading public domain TV series..."
# The Cisco Kid - Public Domain Western Series
echo " 📥 The Cisco Kid (1950-1956)..."
mkdir -p "/downloads/tv-shows/The Cisco Kid (1950)/Season 01"
if [ ! -f "/downloads/tv-shows/The Cisco Kid (1950)/Season 01/The Cisco Kid - S01E01 - The Gay Caballero.mp4" ]; then
wget --progress=bar:force -O "/downloads/tv-shows/The Cisco Kid (1950)/Season 01/The Cisco Kid - S01E01 - The Gay Caballero.mp4" \
"https://archive.org/download/CiscoKid_201611/The%20Cisco%20Kid%20-%20The%20Gay%20Caballero.mp4" || echo " ⚠️ Failed to download Cisco Kid S01E01"
else
echo " ✅ Cisco Kid S01E01 already exists, skipping download"
fi
if [ ! -f "/downloads/tv-shows/The Cisco Kid (1950)/Season 01/The Cisco Kid - S01E02 - Boomerang.mp4" ]; then
wget --progress=bar:force -O "/downloads/tv-shows/The Cisco Kid (1950)/Season 01/The Cisco Kid - S01E02 - Boomerang.mp4" \
"https://archive.org/download/CiscoKid_201611/The%20Cisco%20Kid%20-%20Boomerang.mp4" || echo " ⚠️ Failed to download Cisco Kid S01E02"
else
echo " ✅ Cisco Kid S01E02 already exists, skipping download"
fi
echo "<22>🔧 Setting permissions..."
chmod -R 755 /downloads

View File

@@ -0,0 +1,111 @@
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx",
# "jellyfin-apiclient-python",
# ]
# ///
import httpx
import os
from jellyfin_apiclient_python import JellyfinClient
import time
AUTHORIZATION_HEADER = 'MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjoxNDIuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xNDIuMHwxNzU4NDQ4NDAzOTk5", Version="10.10.7"'
AUTHORIZATION = {"Authorization": AUTHORIZATION_HEADER}
SERVER_URL = os.environ.get("URL", "http://localhost:8096")
ADMIN_PASSWORD = "password"
ADMIN_USER = "admin"
USERNAME = os.environ.get("USERNAME","user")
PASSWORD = os.environ.get("PASSWORD","password")
COLLECTION_NAME = os.environ.get("COLLECTION_NAME", "Movies")
COLLECTION_PATH = os.environ.get("COLLECTION_PATH","/media/movies")
COLLECTION_TYPE = os.environ.get("COLLECTION_TYPE", "movies")
def initialize_server():
with httpx.Client(headers=AUTHORIZATION, base_url=SERVER_URL) as client:
# Retry logic for getting system info until we get a response
max_retries = 10
retry_delay = 5 # seconds
for attempt in range(max_retries):
try:
info = client.get("/System/Info/Public").json()
break # Success, exit loop
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
else:
raise # Re-raise after max retries
if info and info.get("Version"):
print(f" Jellyfin version: {info['Version']}")
if info.get("StartupWizardCompleted"):
print(" Setup wizard already completed, skipping initialization")
return
default_user = client.get("/Startup/User")
default_user.raise_for_status()
print("✅ Retrieved default user: ", default_user.json())
client.post("/Startup/User", json={"Name": ADMIN_USER,"Password": ADMIN_PASSWORD}).raise_for_status()
print(f"✅ Created user '{ADMIN_USER}' with password '{ADMIN_PASSWORD}'")
client.post("/Startup/Configuration", json={"UICulture": "en-US","MetadataCountryCode": "US","PreferredMetadataLanguage": "en"}).raise_for_status()
print("✅ Configured server settings")
client.post("/Startup/RemoteAccess", json={"EnableRemoteAccess": True,"EnableAutomaticPortMapping": True}).raise_for_status()
print("✅ Enabled remote access and automatic port mapping")
client.post("/Startup/Complete").raise_for_status()
print("✅ Completed setup wizard")
def create_users(client: JellyfinClient):
try:
users = client.jellyfin.get_users()
for user in users:
if user['Name'] == USERNAME:
print(f"User '{USERNAME}' already exists, skipping creation")
return
client.jellyfin.new_user(name=USERNAME, pw=PASSWORD)
print(f"✅ Created user '{USERNAME}' with password '{PASSWORD}'")
except Exception as e:
print(f"Failed to create user '{USERNAME}'. It might already exist. Error: {e}")
def create_library(client: JellyfinClient):
try:
folders = client.jellyfin.get_media_folders()
for folder in folders['Items']:
if folder['Name'] == COLLECTION_NAME:
print(f"Library '{COLLECTION_NAME}' already exists, skipping creation")
return
client.jellyfin.add_media_library(
name=COLLECTION_NAME,
collectionType=COLLECTION_TYPE,
paths=[COLLECTION_PATH],
)
print(f"✅ Created library '{COLLECTION_NAME}'")
except Exception as e:
print(f"❌ Failed to create library: {e}")
client.jellyfin.refresh_library()
if __name__ == "__main__":
initialize_server()
client = JellyfinClient()
client.config.app('auto-init', '0.0.1', 'foo', 'bar')
client.config.data["auth.ssl"] = False
client.auth.connect_to_address(SERVER_URL)
user = client.auth.login(SERVER_URL, username=ADMIN_USER, password=ADMIN_PASSWORD)
print(f"✅ Authenticated as '{user['User']['Name']}'")
create_users(client)
create_library(client)

View File

@@ -1,127 +0,0 @@
#!/usr/bin/env python3
import time
import sys
import json
from jellyfin_apiclient_python import JellyfinClient
def wait_for_jellyfin(server_url, max_retries=30):
"""Wait for Jellyfin server to be ready"""
print("🔧 Initializing Jellyfin Movies Server...")
for i in range(max_retries):
try:
client = JellyfinClient()
client.config.app('Jellyfin Init Script', '1.0.0', 'init-script-movies', 'init-movies-container')
client.config.data["auth.ssl"] = False
# Try to connect to check if server is ready
client.auth.connect_to_address(server_url)
print("✅ Jellyfin is running, setting up initial configuration...")
return client
except Exception as e:
print(f"⏳ Waiting for Jellyfin to start... (attempt {i+1}/{max_retries})")
time.sleep(5)
print("❌ Jellyfin server failed to start within timeout")
sys.exit(1)
def initialize_server():
server_url = "http://jellyfin-movies:8096"
# Wait for server to be ready
client = wait_for_jellyfin(server_url)
try:
# Complete initial setup wizard
print("🔧 Completing initial setup wizard...")
# Set up initial configuration
setup_data = {
"UICulture": "en-US",
"MetadataCountryCode": "US",
"PreferredMetadataLanguage": "en"
}
# Create admin user during setup
user_data = {
"Name": "admin",
"Password": "password"
}
# The jellyfin-apiclient-python handles the setup process
# We'll use direct authentication since the server should be in setup mode
client.auth.login(server_url, "admin", "password")
print("🔑 Admin user authenticated successfully")
# Create regular user
print("👤 Creating regular user...")
# Get the jellyfin API object
api = client.jellyfin
# Create regular user
regular_user_data = {
"Name": "user",
"Password": "movies"
}
try:
# Note: The exact API call may need adjustment based on server state
result = api.create_user_by_name(regular_user_data)
print("👤 Created regular user successfully")
except Exception as e:
print(f"⚠️ Regular user creation may have failed: {e}")
# Add Movies library
print("🎬 Creating Movies library...")
library_options = {
"Name": "Movies",
"CollectionType": "movies",
"PathInfos": [{"Path": "/media/movies"}],
"LibraryOptions": {
"EnablePhotos": True,
"EnableRealtimeMonitor": True,
"EnableChapterImageExtraction": False,
"ExtractChapterImagesDuringLibraryScan": False,
"SaveLocalMetadata": False,
"EnableInternetProviders": True,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "US"
}
}
try:
# Create virtual folder (library)
result = api.add_virtual_folder(
name="Movies",
collection_type="movies",
paths=["/media/movies"]
)
print("🎬 Created Movies library successfully")
except Exception as e:
print(f"⚠️ Movies library creation may have failed: {e}")
# Trigger library scan
print("🔍 Triggering library scan...")
try:
api.refresh_library()
print("🔍 Library scan triggered successfully")
except Exception as e:
print(f"⚠️ Library scan trigger may have failed: {e}")
print("✅ Movies server initialization complete!")
except Exception as e:
print(f"❌ Error during server initialization: {e}")
# Print more details for debugging
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
# Initial delay to let Jellyfin fully start
time.sleep(10)
initialize_server()

View File

@@ -1,107 +0,0 @@
#!/bin/bash
# Wait for Jellyfin to start
sleep 10
JELLYFIN_URL="http://jellyfin-movies:8096"
echo "🔧 Initializing Jellyfin Movies Server..."
# Check if Jellyfin is running
until curl -s "$JELLYFIN_URL/health" > /dev/null; do
echo "⏳ Waiting for Jellyfin to start..."
sleep 5
done
echo "✅ Jellyfin is running, setting up initial configuration..."
# Complete initial setup (skip wizard)
curl -X POST "$JELLYFIN_URL/Startup/Configuration" \
-H "Content-Type: application/json" \
-d '{
"UICulture": "en-US",
"MetadataCountryCode": "US",
"PreferredMetadataLanguage": "en"
}' > /dev/null 2>&1
sleep 2
# Create admin user
curl -X POST "$JELLYFIN_URL/Startup/User" \
-H "Content-Type: application/json" \
-d '{
"Name": "admin",
"Password": "password"
}' > /dev/null 2>&1
sleep 2
# Complete startup
curl -X POST "$JELLYFIN_URL/Startup/Complete" > /dev/null 2>&1
sleep 5
# Get auth token by logging in as admin
AUTH_RESPONSE=$(curl -s -X POST "$JELLYFIN_URL/Users/AuthenticateByName" \
-H "Content-Type: application/json" \
-d '{
"Username": "admin",
"Pw": "password",
"App": "Jellyfin Init Script",
"AppVersion": "1.0.0",
"DeviceId": "init-script-movies",
"DeviceName": "Init Script"
}')
TOKEN=$(echo "$AUTH_RESPONSE" | grep -o '"AccessToken":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "❌ Failed to get auth token"
echo "Auth response: $AUTH_RESPONSE"
exit 1
fi
echo "🔑 Got authentication token"
# Create regular user
curl -s -X POST "$JELLYFIN_URL/Users/New" \
-H "Content-Type: application/json" \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
-d '{
"Name": "user",
"Password": "movies"
}' > /dev/null
echo "👤 Created users"
# Add movie library
curl -s -X POST "$JELLYFIN_URL/Library/VirtualFolders?collectionType=movies&refreshLibrary=true&name=Movies" \
-H "Content-Type: application/json" \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
-d '{
"LibraryOptions": {
"EnablePhotos": true,
"EnableRealtimeMonitor": true,
"EnableChapterImageExtraction": false,
"ExtractChapterImagesDuringLibraryScan": false,
"PathInfos": [
{
"Path": "/media/movies",
"NetworkPath": ""
}
],
"SaveLocalMetadata": false,
"EnableInternetProviders": true,
"EnableAutomaticSeriesGrouping": false,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "US"
}
}' > /dev/null
echo "🎬 Created Movies library"
# Trigger library scan
curl -s -X POST "$JELLYFIN_URL/Library/Refresh" \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" > /dev/null
echo "🔍 Triggered library scan"
echo "✅ Movies server initialization complete!"

View File

@@ -1,128 +0,0 @@
#!/usr/bin/env python3
import time
import sys
import json
from jellyfin_apiclient_python import JellyfinClient
def wait_for_jellyfin(server_url, max_retries=30):
"""Wait for Jellyfin server to be ready"""
print("🔧 Initializing Jellyfin TV Shows Server...")
for i in range(max_retries):
try:
client = JellyfinClient()
client.config.app('Jellyfin Init Script', '1.0.0', 'init-script-tvshows', 'init-tvshows-container')
client.config.data["auth.ssl"] = False
# Try to connect to check if server is ready
client.auth.connect_to_address(server_url)
print("✅ Jellyfin is running, setting up initial configuration...")
return client
except Exception as e:
print(f"⏳ Waiting for Jellyfin to start... (attempt {i+1}/{max_retries})")
time.sleep(5)
print("❌ Jellyfin server failed to start within timeout")
sys.exit(1)
def initialize_server():
server_url = "http://jellyfin-tvshows:8096"
# Wait for server to be ready
client = wait_for_jellyfin(server_url)
try:
# Complete initial setup wizard
print("🔧 Completing initial setup wizard...")
# Set up initial configuration
setup_data = {
"UICulture": "en-US",
"MetadataCountryCode": "US",
"PreferredMetadataLanguage": "en"
}
# Create admin user during setup
user_data = {
"Name": "admin",
"Password": "password"
}
# The jellyfin-apiclient-python handles the setup process
# We'll use direct authentication since the server should be in setup mode
client.auth.login(server_url, "admin", "password")
print("🔑 Admin user authenticated successfully")
# Create regular user
print("👤 Creating regular user...")
# Get the jellyfin API object
api = client.jellyfin
# Create regular user
regular_user_data = {
"Name": "user",
"Password": "shows"
}
try:
# Note: The exact API call may need adjustment based on server state
result = api.create_user_by_name(regular_user_data)
print("👤 Created regular user successfully")
except Exception as e:
print(f"⚠️ Regular user creation may have failed: {e}")
# Add TV Shows library
print("📺 Creating TV Shows library...")
library_options = {
"Name": "TV Shows",
"CollectionType": "tvshows",
"PathInfos": [{"Path": "/media/tv-shows"}],
"LibraryOptions": {
"EnablePhotos": True,
"EnableRealtimeMonitor": True,
"EnableChapterImageExtraction": False,
"ExtractChapterImagesDuringLibraryScan": False,
"SaveLocalMetadata": False,
"EnableInternetProviders": True,
"EnableAutomaticSeriesGrouping": True,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "US"
}
}
try:
# Create virtual folder (library)
result = api.add_virtual_folder(
name="TV Shows",
collection_type="tvshows",
paths=["/media/tv-shows"]
)
print("📺 Created TV Shows library successfully")
except Exception as e:
print(f"⚠️ TV Shows library creation may have failed: {e}")
# Trigger library scan
print("🔍 Triggering library scan...")
try:
api.refresh_library()
print("🔍 Library scan triggered successfully")
except Exception as e:
print(f"⚠️ Library scan trigger may have failed: {e}")
print("✅ TV Shows server initialization complete!")
except Exception as e:
print(f"❌ Error during server initialization: {e}")
# Print more details for debugging
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
# Initial delay to let Jellyfin fully start
time.sleep(10)
initialize_server()

View File

@@ -1,107 +0,0 @@
#!/bin/bash
# Wait for Jellyfin to start
sleep 10
JELLYFIN_URL="http://jellyfin-tvshows:8096"
echo "🔧 Initializing Jellyfin TV Shows Server..."
# Check if Jellyfin is running
until curl -s "$JELLYFIN_URL/health" > /dev/null; do
echo "⏳ Waiting for Jellyfin to start..."
sleep 5
done
echo "✅ Jellyfin is running, setting up initial configuration..."
# Complete initial setup (skip wizard)
curl -X POST "$JELLYFIN_URL/Startup/Configuration" \
-H "Content-Type: application/json" \
-d '{
"UICulture": "en-US",
"MetadataCountryCode": "US",
"PreferredMetadataLanguage": "en"
}' > /dev/null 2>&1
sleep 2
# Create admin user
curl -X POST "$JELLYFIN_URL/Startup/User" \
-H "Content-Type: application/json" \
-d '{
"Name": "admin",
"Password": "password"
}' > /dev/null 2>&1
sleep 2
# Complete startup
curl -X POST "$JELLYFIN_URL/Startup/Complete" > /dev/null 2>&1
sleep 5
# Get auth token by logging in as admin
AUTH_RESPONSE=$(curl -s -X POST "$JELLYFIN_URL/Users/AuthenticateByName" \
-H "Content-Type: application/json" \
-d '{
"Username": "admin",
"Pw": "password",
"App": "Jellyfin Init Script",
"AppVersion": "1.0.0",
"DeviceId": "init-script-tvshows",
"DeviceName": "Init Script"
}')
TOKEN=$(echo "$AUTH_RESPONSE" | grep -o '"AccessToken":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "❌ Failed to get auth token"
echo "Auth response: $AUTH_RESPONSE"
exit 1
fi
echo "🔑 Got authentication token"
# Create regular user
curl -s -X POST "$JELLYFIN_URL/Users/New" \
-H "Content-Type: application/json" \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
-d '{
"Name": "user",
"Password": "shows"
}' > /dev/null
echo "👤 Created users"
# Add TV shows library
curl -s -X POST "$JELLYFIN_URL/Library/VirtualFolders?collectionType=tvshows&refreshLibrary=true&name=TV%20Shows" \
-H "Content-Type: application/json" \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
-d '{
"LibraryOptions": {
"EnablePhotos": true,
"EnableRealtimeMonitor": true,
"EnableChapterImageExtraction": false,
"ExtractChapterImagesDuringLibraryScan": false,
"PathInfos": [
{
"Path": "/media/tv-shows",
"NetworkPath": ""
}
],
"SaveLocalMetadata": false,
"EnableInternetProviders": true,
"EnableAutomaticSeriesGrouping": true,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "US"
}
}' > /dev/null
echo "📺 Created TV Shows library"
# Trigger library scan
curl -s -X POST "$JELLYFIN_URL/Library/Refresh" \
-H "Authorization: MediaBrowser Token=\"$TOKEN\"" > /dev/null
echo "🔍 Triggered library scan"
echo "✅ TV Shows server initialization complete!"

206
dev/uv.lock generated Normal file
View File

@@ -0,0 +1,206 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "anyio"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
{ url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
{ url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
{ url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
{ url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
{ url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
{ url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
{ url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
{ url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
{ url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
{ url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
{ url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
{ url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
{ url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
{ url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
{ url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
{ url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
]
[[package]]
name = "dev"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "httpx" },
{ name = "jellyfin-apiclient-python" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "jellyfin-apiclient-python", specifier = ">=1.11.0" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "jellyfin-apiclient-python"
version = "1.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "requests" },
{ name = "urllib3" },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/b0/61cacc2261f6b1972e4a53d142857def8b4d3b6fbd1481a2257b26e31029/jellyfin_apiclient_python-1.11.0.tar.gz", hash = "sha256:f5e3dc4ea06a80d26859a62ace7c3ab26f762063a3032f9109f4cea2ed8ac5de", size = 62581, upload-time = "2025-03-17T23:11:29.289Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f3/c5d2ba45e432ed706c705e961c8e2a01c5600f93265279e8d61c61f544ae/jellyfin_apiclient_python-1.11.0-py3-none-any.whl", hash = "sha256:b666c8d175b36f2ce9e6020c13821eb1aa104a585bfbba58f6790fa15f358b40", size = 51475, upload-time = "2025-03-17T23:11:27.91Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "websocket-client"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" },
]