commit 720a35af3401caf403034234852f586eae947903 Author: Lumerel Deploy Date: Sun Mar 22 22:36:40 2026 +0000 Deploy from Lumerel diff --git a/.docker/99-migrate.sh b/.docker/99-migrate.sh new file mode 100644 index 0000000..66b83d4 --- /dev/null +++ b/.docker/99-migrate.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# IMPORTANT: This script is sourced (not executed) by the webdevops entrypoint. +# Never use "exit" — it would kill the parent entrypoint and prevent supervisord from starting. +# Use "return" instead to leave this script without affecting the parent. + +echo ">>> Waiting for database connection..." +echo ">>> DB_HOST=${DB_HOST:-NOT SET}" +echo ">>> DB_PORT=${DB_PORT:-NOT SET}" +echo ">>> DB_DATABASE=${DB_DATABASE:-NOT SET}" +echo ">>> DB_USERNAME=${DB_USERNAME:-NOT SET}" +echo ">>> DB_PASSWORD is $([ -n "$DB_PASSWORD" ] && echo 'set' || echo 'NOT SET')" + +if [ -z "$DB_HOST" ] || [ -z "$DB_USERNAME" ]; then + echo ">>> ERROR: DB_HOST or DB_USERNAME not set. Skipping migrations." + return 0 2>/dev/null || true +fi + +_migrate_max_retries=30 +_migrate_count=0 +_migrate_done=false + +while [ $_migrate_count -lt $_migrate_max_retries ]; do + if php -r ' + try { + $host = getenv("DB_HOST"); + $port = getenv("DB_PORT") ?: "3306"; + $user = getenv("DB_USERNAME"); + $pass = getenv("DB_PASSWORD"); + new PDO("mysql:host=$host;port=$port", $user, $pass, [PDO::ATTR_TIMEOUT => 3]); + exit(0); + } catch (Throwable $e) { + fwrite(STDERR, "PDO error: " . $e->getMessage() . "\n"); + exit(1); + } + ' 2>&1; then + echo ">>> Database is reachable. Running migrations..." + if php /app/migrate.php; then + echo ">>> Migrations completed successfully." + else + echo ">>> WARNING: migrate.php exited with code $?" + fi + _migrate_done=true + break + fi + + _migrate_count=$((_migrate_count + 1)) + echo ">>> Waiting for database... attempt $_migrate_count/$_migrate_max_retries" + sleep 2 +done + +if [ "$_migrate_done" = false ]; then + echo ">>> WARNING: Could not connect to database after $_migrate_max_retries attempts. Skipping migrations." +fi \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17896fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c9ce0f1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM webdevops/php-nginx:8.3-alpine +ENV WEB_DOCUMENT_ROOT=/app +ARG CACHE_BUST=1774219000 +COPY . /app +RUN echo "index index.php index.html index.htm;" > /opt/docker/etc/nginx/vhost.common.d/01-index.conf \ + && echo "add_header Cache-Control 'no-cache, no-store, must-revalidate';" > /opt/docker/etc/nginx/vhost.common.d/02-no-cache.conf +RUN set -e; if [ -f /app/composer.json ]; then \ + echo ">>> composer.json found, installing dependencies..."; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + cd /app && composer install --no-dev --no-interaction --optimize-autoloader; \ + ls -la /app/vendor/autoload.php; \ + else \ + echo ">>> No composer.json found, skipping composer install"; \ + fi +RUN set -e; if [ -f /app/package.json ]; then \ + echo ">>> package.json found, installing node dependencies..."; \ + apk add --no-cache nodejs npm; \ + cd /app && npm install --production; \ + else \ + echo ">>> No package.json found, skipping npm install"; \ + fi +COPY --chmod=755 .docker/99-migrate.sh /opt/docker/provision/entrypoint.d/99-migrate.sh +RUN chown -R application:application /app \ No newline at end of file diff --git a/api.php b/api.php new file mode 100644 index 0000000..9438523 --- /dev/null +++ b/api.php @@ -0,0 +1,189 @@ +prepare($sql); + $stmt->execute($params); + $items = $stmt->fetchAll(); + + $stats = $pdo->query(" + SELECT + COUNT(*) AS total, + SUM(watched) AS watched, + COUNT(*) - SUM(watched) AS unwatched + FROM watchlist + ")->fetch(); + + $services = $pdo->query(" + SELECT streaming_service, COUNT(*) AS cnt + FROM watchlist + WHERE streaming_service IS NOT NULL AND streaming_service != '' + GROUP BY streaming_service + ORDER BY cnt DESC + ")->fetchAll(); + + echo json_encode([ + 'success' => true, + 'items' => $items, + 'stats' => $stats, + 'services' => $services, + ]); + break; + + case 'add': + $title = trim($input['title'] ?? ''); + $type = $input['type'] ?? 'movie'; + $service = trim($input['streaming_service'] ?? ''); + $genre = trim($input['genre'] ?? ''); + $notes = trim($input['notes'] ?? ''); + + if ($title === '') { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Title is required']); + exit; + } + + $stmt = $pdo->prepare(" + INSERT INTO watchlist (title, type, streaming_service, genre, notes) + VALUES (?, ?, ?, ?, ?) + "); + $stmt->execute([$title, $type, $service ?: null, $genre ?: null, $notes ?: null]); + $id = $pdo->lastInsertId(); + + $item = $pdo->prepare("SELECT * FROM watchlist WHERE id = ?"); + $item->execute([$id]); + + echo json_encode(['success' => true, 'item' => $item->fetch()]); + break; + + case 'toggle': + $id = (int)($input['id'] ?? 0); + if (!$id) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'ID required']); + exit; + } + + $current = $pdo->prepare("SELECT watched FROM watchlist WHERE id = ?"); + $current->execute([$id]); + $row = $current->fetch(); + + if (!$row) { + http_response_code(404); + echo json_encode(['success' => false, 'error' => 'Not found']); + exit; + } + + $newWatched = $row['watched'] ? 0 : 1; + $watchedAt = $newWatched ? date('Y-m-d H:i:s') : null; + + $stmt = $pdo->prepare("UPDATE watchlist SET watched = ?, watched_at = ? WHERE id = ?"); + $stmt->execute([$newWatched, $watchedAt, $id]); + + $item = $pdo->prepare("SELECT * FROM watchlist WHERE id = ?"); + $item->execute([$id]); + + echo json_encode(['success' => true, 'item' => $item->fetch()]); + break; + + case 'update': + $id = (int)($input['id'] ?? 0); + $title = trim($input['title'] ?? ''); + $type = $input['type'] ?? 'movie'; + $service = trim($input['streaming_service'] ?? ''); + $genre = trim($input['genre'] ?? ''); + $notes = trim($input['notes'] ?? ''); + + if (!$id || $title === '') { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'ID and title required']); + exit; + } + + $stmt = $pdo->prepare(" + UPDATE watchlist SET title = ?, type = ?, streaming_service = ?, genre = ?, notes = ? + WHERE id = ? + "); + $stmt->execute([$title, $type, $service ?: null, $genre ?: null, $notes ?: null, $id]); + + $item = $pdo->prepare("SELECT * FROM watchlist WHERE id = ?"); + $item->execute([$id]); + + echo json_encode(['success' => true, 'item' => $item->fetch()]); + break; + + case 'delete': + $id = (int)($input['id'] ?? 0); + if (!$id) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'ID required']); + exit; + } + + $stmt = $pdo->prepare("DELETE FROM watchlist WHERE id = ?"); + $stmt->execute([$id]); + + echo json_encode(['success' => true]); + break; + + default: + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Unknown action']); + } + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/db.php b/db.php new file mode 100644 index 0000000..68a8498 --- /dev/null +++ b/db.php @@ -0,0 +1,14 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); diff --git a/index.php b/index.php new file mode 100644 index 0000000..f4a1514 --- /dev/null +++ b/index.php @@ -0,0 +1,178 @@ + + + + + + Watchlist ✨ + + + + + + + + +
+
+
+
+
+ +
+ + +
+
+ 🎬 +
+

Watchlist

+

Your personal streaming guide

+
+
+ +
+ + +
+
+ + Total +
+
+
+ + To Watch +
+
+
+ + Watched +
+
+
+
+
+
+ 0% complete +
+
+ + +
+
+ + + +
+
+ + + +
+ +
+ + +
+ + +
+
+
+

Loading…

+
+
+ + + + +
+ + + + + + + + +
+ + + + diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..bc86a3b --- /dev/null +++ b/migrate.php @@ -0,0 +1,25 @@ +exec("CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + migration VARCHAR(255) NOT NULL UNIQUE, + ran_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +)"); + +$ran = $pdo->query("SELECT migration FROM migrations")->fetchAll(PDO::FETCH_COLUMN); + +$files = glob(__DIR__ . '/migrations/*.php'); +sort($files); + +foreach ($files as $file) { + $name = basename($file); + if (!in_array($name, $ran)) { + require $file; + $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); + $stmt->execute([$name]); + echo "Ran: {$name}\n"; + } +} + +echo "Migrations complete.\n"; diff --git a/migrations/001_create_watchlist_table.php b/migrations/001_create_watchlist_table.php new file mode 100644 index 0000000..bf42b8d --- /dev/null +++ b/migrations/001_create_watchlist_table.php @@ -0,0 +1,13 @@ +exec("CREATE TABLE IF NOT EXISTS watchlist ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + type ENUM('movie', 'show') NOT NULL DEFAULT 'movie', + streaming_service VARCHAR(100) DEFAULT NULL, + genre VARCHAR(100) DEFAULT NULL, + notes TEXT DEFAULT NULL, + watched TINYINT(1) NOT NULL DEFAULT 0, + watched_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +)");