mirror of
https://github.com/LLukas22/Jellyswarrm.git
synced 2025-12-23 22:47:47 -05:00
add media service
This commit is contained in:
1
dev/.python-version
Normal file
1
dev/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11
|
||||
305
dev/README.md
305
dev/README.md
@@ -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 Archive — https://archive.org/
|
||||
- Blender Foundation — https://www.blender.org/about/projects/
|
||||
@@ -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
10
dev/pyproject.toml
Normal 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",
|
||||
]
|
||||
170
dev/scripts/download-content.py
Normal file
170
dev/scripts/download-content.py
Normal 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()
|
||||
@@ -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
|
||||
111
dev/scripts/init-jellyfin.py
Normal file
111
dev/scripts/init-jellyfin.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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!"
|
||||
@@ -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()
|
||||
@@ -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
206
dev/uv.lock
generated
Normal 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" },
|
||||
]
|
||||
Reference in New Issue
Block a user