Compare commits
2 Commits
66f69bc90d
...
728ca893b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
728ca893b1 | ||
|
|
eeff824701 |
1112
API_DOCS.md
Normal file
1112
API_DOCS.md
Normal file
File diff suppressed because it is too large
Load Diff
331
README.md
Normal file
331
README.md
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
# Kyoo Backend
|
||||||
|
|
||||||
|
A RESTful API backend for media management, supporting movies, TV series, music albums, games, and cast information.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Media Management**: CRUD operations for movies, TV series, music albums, and games
|
||||||
|
- **Cast Management**: Manage actors, directors, and other staff with filmography tracking
|
||||||
|
- **Image Handling**: Automatic processing and storage of posters, banners, and cast photos
|
||||||
|
- **Search & Filtering**: Advanced search with pagination support
|
||||||
|
- **API Logging**: Built-in request/response logging for debugging
|
||||||
|
- **Docker Support**: Easy deployment with Docker Compose
|
||||||
|
- **Auto Documentation**: Self-documenting API endpoint
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **PHP 8.2** with Apache
|
||||||
|
- **MariaDB 10.11**
|
||||||
|
- **Docker & Docker Compose**
|
||||||
|
- **phpMyAdmin** for database management
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker installed
|
||||||
|
- Docker Compose installed
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd kyoobackend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the services:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access the API:
|
||||||
|
- API Base URL: `http://localhost:6400/api`
|
||||||
|
- phpMyAdmin: `http://localhost:6402`
|
||||||
|
|
||||||
|
### Database Credentials
|
||||||
|
|
||||||
|
- **Host**: `mariadb` (within Docker) or `localhost:6401` (from host)
|
||||||
|
- **Database**: `kyoo`
|
||||||
|
- **User**: `kyoo_user`
|
||||||
|
- **Password**: `kyoo_password`
|
||||||
|
- **Root Password**: `root_password`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
kyoobackend/
|
||||||
|
├── api/
|
||||||
|
│ ├── controllers/ # Request handlers
|
||||||
|
│ │ ├── MediaController.php
|
||||||
|
│ │ ├── CastController.php
|
||||||
|
│ │ ├── ImageController.php
|
||||||
|
│ │ └── SettingsController.php
|
||||||
|
│ ├── models/ # Data models
|
||||||
|
│ │ ├── Media.php
|
||||||
|
│ │ ├── Cast.php
|
||||||
|
│ │ ├── Game.php
|
||||||
|
│ │ ├── Series.php
|
||||||
|
│ │ ├── Music.php
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── services/ # Business logic
|
||||||
|
│ │ ├── ApiLogger.php
|
||||||
|
│ │ ├── ImageHandler.php
|
||||||
|
│ │ └── DocumentationService.php
|
||||||
|
│ ├── config.php # Configuration
|
||||||
|
│ ├── database.php # Database connection
|
||||||
|
│ ├── Router.php # API routing
|
||||||
|
│ └── index.php # Entry point
|
||||||
|
├── docker-compose.yml # Docker configuration
|
||||||
|
├── Dockerfile # PHP container build
|
||||||
|
└── php-custom.ini # PHP configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Root
|
||||||
|
- `GET /api` - API information and available endpoints
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- `GET /api/docs` - Auto-generated API documentation
|
||||||
|
|
||||||
|
### Media
|
||||||
|
- `GET /api/media` - Get all media (with pagination and filtering)
|
||||||
|
- `GET /api/media/:id` - Get specific media item
|
||||||
|
- `POST /api/media` - Create new media
|
||||||
|
- `PUT /api/media/:id` - Update media
|
||||||
|
- `DELETE /api/media/:id` - Delete media
|
||||||
|
|
||||||
|
### Series Episodes
|
||||||
|
- `GET /api/media/:id/episodes` - Get all episodes for a series
|
||||||
|
- `GET /api/media/:id/episodes/:episodeId` - Get specific episode
|
||||||
|
- `POST /api/media/:id/episodes` - Add episode to series
|
||||||
|
- `PUT /api/media/:id/episodes/:episodeId` - Update episode
|
||||||
|
- `DELETE /api/media/:id/episodes/:episodeId` - Delete episode
|
||||||
|
|
||||||
|
### Music Tracks
|
||||||
|
- `GET /api/media/:id/tracks` - Get all tracks for an album
|
||||||
|
- `GET /api/media/:id/tracks/:trackId` - Get specific track
|
||||||
|
- `POST /api/media/:id/tracks` - Add track to album
|
||||||
|
- `PUT /api/media/:id/tracks/:trackId` - Update track
|
||||||
|
- `DELETE /api/media/:id/tracks/:trackId` - Delete track
|
||||||
|
|
||||||
|
### Cast
|
||||||
|
- `GET /api/cast` - Get all cast members (with search)
|
||||||
|
- `GET /api/cast/:id` - Get specific cast member with filmography
|
||||||
|
- `GET /api/cast/:id/media` - Get all media for a cast member
|
||||||
|
- `POST /api/cast` - Create new cast member
|
||||||
|
- `PUT /api/cast/:id` - Update cast member
|
||||||
|
- `DELETE /api/cast/:id` - Delete cast member
|
||||||
|
|
||||||
|
### Adult Cast
|
||||||
|
- `GET /api/cast/adult` - Get all adult cast members (with filters)
|
||||||
|
- `GET /api/cast/adult/:id` - Get specific adult cast member
|
||||||
|
- `POST /api/cast/adult` - Create new adult cast member
|
||||||
|
- `PUT /api/cast/adult/:id` - Update adult cast member
|
||||||
|
- `DELETE /api/cast/adult/:id` - Delete adult cast member
|
||||||
|
|
||||||
|
### Images
|
||||||
|
- `GET /api/images/*` - Serve images directly
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
- `GET /api/settings` - Get application settings
|
||||||
|
- `PUT /api/settings` - Update settings
|
||||||
|
|
||||||
|
## Query Parameters
|
||||||
|
|
||||||
|
### Media List
|
||||||
|
- `page` - Page number (default: 1)
|
||||||
|
- `limit` - Items per page (default: 20)
|
||||||
|
- `category` - Filter by category (e.g., "movie", "tv", "music", "game")
|
||||||
|
- `type` - Filter by type (e.g., "Movie", "TV", "Album", "Game")
|
||||||
|
- `search` - Search in title and description
|
||||||
|
|
||||||
|
### Cast List
|
||||||
|
- `page` - Page number (default: 1)
|
||||||
|
- `limit` - Items per page (default: 20)
|
||||||
|
- `search` - Search by name
|
||||||
|
|
||||||
|
### Adult Cast List
|
||||||
|
- `page` - Page number (default: 1)
|
||||||
|
- `limit` - Items per page (default: 20)
|
||||||
|
- `search` - Search by name
|
||||||
|
- `ethnicity` - Filter by ethnicity
|
||||||
|
- `hair_color` - Filter by hair color
|
||||||
|
|
||||||
|
### Series Episodes
|
||||||
|
- `season` - Filter by season number
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Media
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Movie Title",
|
||||||
|
"cleanname": "movie-title",
|
||||||
|
"year": 2024,
|
||||||
|
"poster": "/images/movies/poster_xxx.webp",
|
||||||
|
"banner": "/images/movies/banner_xxx.webp",
|
||||||
|
"description": "Description text",
|
||||||
|
"rating": 8.5,
|
||||||
|
"category": "movie",
|
||||||
|
"type": "Movie",
|
||||||
|
"status": "released",
|
||||||
|
"aspectRatio": "16:9",
|
||||||
|
"runtime": 120,
|
||||||
|
"director": "Director Name",
|
||||||
|
"writer": "Writer Name",
|
||||||
|
"source": "netflix",
|
||||||
|
"releaseDate": "2024-01-01",
|
||||||
|
"genres": ["Action", "Drama"],
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"studios": ["Studio1"],
|
||||||
|
"staff": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Actor Name",
|
||||||
|
"role": "Actor",
|
||||||
|
"characterName": "Character",
|
||||||
|
"characterImage": "/images/characters/char_xxx.webp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2024-01-01 00:00:00",
|
||||||
|
"updatedAt": "2024-01-01 00:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cast
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Actor Name",
|
||||||
|
"cleanname": "actor-name",
|
||||||
|
"photo": "/images/cast/photo_xxx.webp",
|
||||||
|
"bio": "Biography text",
|
||||||
|
"birthDate": "1990-01-01",
|
||||||
|
"birthPlace": "City, Country",
|
||||||
|
"occupations": ["Actor", "Director"],
|
||||||
|
"filmography": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Movie Title",
|
||||||
|
"year": 2024,
|
||||||
|
"poster": "/images/movies/poster_xxx.webp",
|
||||||
|
"category": "movie",
|
||||||
|
"type": "Movie",
|
||||||
|
"role": "Actor",
|
||||||
|
"characterName": "Character"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2024-01-01 00:00:00",
|
||||||
|
"updatedAt": "2024-01-01 00:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
All responses follow this structure:
|
||||||
|
|
||||||
|
### Success
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Error message"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paginated List
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"items": [ ... ],
|
||||||
|
"total": 100,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 20,
|
||||||
|
"totalPages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables can be set in `docker-compose.yml` or as system environment variables:
|
||||||
|
|
||||||
|
- `DB_HOST` - Database host (default: `mariadb`)
|
||||||
|
- `DB_NAME` - Database name (default: `kyoo`)
|
||||||
|
- `DB_USER` - Database user (default: `kyoo_user`)
|
||||||
|
- `DB_PASS` - Database password (default: `kyoo_password`)
|
||||||
|
- `API_LOGGING_ENABLED` - Enable API logging (default: `false`)
|
||||||
|
|
||||||
|
## Docker Commands
|
||||||
|
|
||||||
|
### Start services
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop services
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
```bash
|
||||||
|
# All logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker-compose logs php
|
||||||
|
docker-compose logs mariadb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset database
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The API files are mounted as volumes, so changes are immediately reflected without rebuilding.
|
||||||
|
|
||||||
|
### API Logging
|
||||||
|
|
||||||
|
Logs are written to:
|
||||||
|
- API requests/responses: `/var/www/html/logs/api.log`
|
||||||
|
- PHP errors: `/var/www/html/logs/php_error.log`
|
||||||
|
|
||||||
|
Enable logging by setting `API_LOGGING_ENABLED=true`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### PHP container won't start
|
||||||
|
```bash
|
||||||
|
docker-compose logs php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database connection errors
|
||||||
|
Check if MariaDB is running:
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tables not created
|
||||||
|
Tables are auto-created on first API call. Check logs:
|
||||||
|
```bash
|
||||||
|
docker-compose logs php
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Add your license here]
|
||||||
@@ -279,16 +279,6 @@ class MediaController {
|
|||||||
|
|
||||||
$result = $this->media->search($filters, $page, $limit);
|
$result = $this->media->search($filters, $page, $limit);
|
||||||
|
|
||||||
// Game-spezifische Daten für Games laden
|
|
||||||
foreach ($result['items'] as &$item) {
|
|
||||||
if ($item['type'] === 'Game') {
|
|
||||||
$gameInfo = $this->game->getGameInfoForList($item['id']);
|
|
||||||
if ($gameInfo) {
|
|
||||||
$item = array_merge($item, $gameInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => true, 'data' => $result];
|
return ['success' => true, 'data' => $result];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ class Database {
|
|||||||
auto_play_trailers BOOLEAN DEFAULT 0,
|
auto_play_trailers BOOLEAN DEFAULT 0,
|
||||||
language TEXT DEFAULT 'en',
|
language TEXT DEFAULT 'en',
|
||||||
theme TEXT DEFAULT 'system',
|
theme TEXT DEFAULT 'system',
|
||||||
|
jellyfin_library_mappings JSON,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,15 +49,36 @@ class Cast extends BaseModel {
|
|||||||
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
||||||
$total = $this->count($conditions);
|
$total = $this->count($conditions);
|
||||||
|
|
||||||
|
// Load filmography for all cast members in a single query
|
||||||
|
if (!empty($items)) {
|
||||||
|
$castIds = array_column($items, 'id');
|
||||||
|
$placeholders = str_repeat('?,', count($castIds) - 1) . '?';
|
||||||
|
|
||||||
// Add filmography to each cast member
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT mc.cast_id, m.id, m.title, m.year, m.poster, m.category, m.type, mc.role, mc.characterName
|
||||||
|
FROM media m
|
||||||
|
INNER JOIN media_cast mc ON m.id = mc.media_id
|
||||||
|
WHERE mc.cast_id IN ($placeholders)
|
||||||
|
ORDER BY m.year DESC
|
||||||
|
");
|
||||||
|
$stmt->execute($castIds);
|
||||||
|
$allFilmography = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Group filmography by cast_id
|
||||||
|
$filmographyByCast = [];
|
||||||
|
foreach ($allFilmography as $film) {
|
||||||
|
$castId = $film['cast_id'];
|
||||||
|
unset($film['cast_id']);
|
||||||
|
$filmographyByCast[$castId][] = $film;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach filmography to each cast member
|
||||||
foreach ($items as &$item) {
|
foreach ($items as &$item) {
|
||||||
$item['filmography'] = $this->getMediaForCast($item['id']);
|
$item['filmography'] = $filmographyByCast[$item['id']] ?? [];
|
||||||
// Extract unique media types
|
|
||||||
$mediaTypes = array_unique(array_column($item['filmography'], 'category'));
|
$mediaTypes = array_unique(array_column($item['filmography'], 'category'));
|
||||||
$item['media_types'] = array_values($mediaTypes);
|
$item['media_types'] = array_values($mediaTypes);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
|
|||||||
@@ -65,11 +65,6 @@ class Media extends BaseModel {
|
|||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$items = $stmt->fetchAll();
|
$items = $stmt->fetchAll();
|
||||||
|
|
||||||
// Cast-Mitglieder für jedes Medium laden
|
|
||||||
foreach ($items as &$item) {
|
|
||||||
$item['staff'] = $this->getCastForMedia($item['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count Query
|
// Count Query
|
||||||
$countQuery = "SELECT COUNT(*) FROM {$this->table} WHERE 1=1";
|
$countQuery = "SELECT COUNT(*) FROM {$this->table} WHERE 1=1";
|
||||||
$countParams = [];
|
$countParams = [];
|
||||||
@@ -93,11 +88,6 @@ class Media extends BaseModel {
|
|||||||
} else {
|
} else {
|
||||||
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
||||||
$total = $this->count($conditions);
|
$total = $this->count($conditions);
|
||||||
|
|
||||||
// Cast-Mitglieder für jedes Medium laden
|
|
||||||
foreach ($items as &$item) {
|
|
||||||
$item['staff'] = $this->getCastForMedia($item['id']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class Settings extends BaseModel {
|
|||||||
$updateData['theme'] = $data['theme'];
|
$updateData['theme'] = $data['theme'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($data['jellyfin_library_mappings'])) {
|
||||||
|
$updateData['jellyfin_library_mappings'] = $data['jellyfin_library_mappings'];
|
||||||
|
}
|
||||||
|
|
||||||
// Check if settings row exists
|
// Check if settings row exists
|
||||||
$existing = $this->findById(1);
|
$existing = $this->findById(1);
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ class Settings extends BaseModel {
|
|||||||
'auto_play_trailers' => 0,
|
'auto_play_trailers' => 0,
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'theme' => 'system',
|
'theme' => 'system',
|
||||||
'theme' => 'system'
|
'jellyfin_library_mappings' => ''
|
||||||
];
|
];
|
||||||
$mergedData = array_merge($defaultData, $updateData);
|
$mergedData = array_merge($defaultData, $updateData);
|
||||||
$this->create($mergedData);
|
$this->create($mergedData);
|
||||||
|
|||||||
Reference in New Issue
Block a user