From f2e7467abf2742ecd9d70febb30d7a17f9eb64a7 Mon Sep 17 00:00:00 2001 From: vndangkhoa Date: Sun, 1 Feb 2026 19:13:15 +0700 Subject: [PATCH] feat: dockerize app, add brand assets, implement wake lock --- Dockerfile | 61 ++-- docker-compose.yml | 15 +- frontend-react/src/hooks/useWatchMovie.ts | 59 +++- frontend-react/src/themes/apple/WatchPage.tsx | 5 +- .../src/themes/default/WatchPage.tsx | 319 +++++++----------- .../src/themes/netflix/WatchPage.tsx | 298 +++++++--------- start-dev.ps1 | 51 +++ 7 files changed, 373 insertions(+), 435 deletions(-) create mode 100644 start-dev.ps1 diff --git a/Dockerfile b/Dockerfile index 8c58ef4..63db6ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,56 +1,49 @@ -# =========================== -# Stage 1: Frontend Build -# =========================== -FROM node:20-alpine AS frontend-builder +# Stage 1: Build Image (Frontend) +FROM node:18-alpine AS frontend-builder WORKDIR /app/frontend COPY frontend-react/package*.json ./ -RUN npm ci +RUN npm install COPY frontend-react/ . RUN npm run build -# =========================== -# Stage 2: Backend Build -# =========================== -FROM golang:1.22-alpine AS backend-builder +# Stage 2: Build Image (Backend) +FROM golang:1.23-alpine AS backend-builder WORKDIR /app/backend - -# Install necessary build tools for CGO (SQLite) +# Install build dependencies RUN apk add --no-cache gcc musl-dev COPY backend/go.mod backend/go.sum ./ RUN go mod download COPY backend/ . -# Build Linux binary -RUN CGO_ENABLED=1 GOOS=linux go build -o server ./cmd/server/main.go +# Build static binary for Linux amd64 +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server cmd/server/main.go -# =========================== -# Stage 3: Runtime -# =========================== -FROM python:3.11-alpine +# Stage 3: Final Image +FROM alpine:latest WORKDIR /app -# Install Runtime Dependencies -# - ffmpeg: for yt-dlp media handling -# - yt-dlp: via pip -# - ca-certificates: for HTTPS -RUN apk add --no-cache ffmpeg ca-certificates && \ - pip install --no-cache-dir yt-dlp +# Install runtime dependencies (sqlite) +RUN apk add --no-cache sqlite ca-certificates tzdata -# Create non-root user -RUN addgroup -S streamflow && adduser -S streamflow -G streamflow +# Copy backend binary +COPY --from=backend-builder /app/backend/server . -# Copy Frontend Build -COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist +# Copy frontend build to the expected static directory +# The backend expects ../frontend-react/dist relative to itself, or we configure it. +# Let's align with the standard deployment structure: /app/server and /app/dist +COPY --from=frontend-builder /app/frontend/dist ./dist -# Copy Backend Binary -COPY --from=backend-builder /app/backend/server /app/server +# Create data directory +RUN mkdir -p data -# Setup Permissions -RUN chown -R streamflow:streamflow /app - -USER streamflow +# Environment variables +ENV PORT=8000 +ENV DATABASE_URL=sqlite:///app/data/streamflow.db +ENV GIN_MODE=release +# Expose port EXPOSE 8000 -CMD ["/app/server"] +# Start server +CMD ["./server"] diff --git a/docker-compose.yml b/docker-compose.yml index 99e192d..d338f55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,19 @@ version: '3.8' services: - app: + streamflow: build: . - image: streamflow:v2 + image: streamflow:latest + container_name: streamflow + platform: linux/amd64 ports: - - "8000:8000" + - "3478:8000" environment: - - DATABASE_URL=sqlite:///./data/streamflow.db + - DATABASE_URL=sqlite:///app/data/streamflow.db - TMDB_API_KEY=${TMDB_API_KEY} volumes: - - streamflow_data:/app/data - - streamflow_cache:/app/cache - restart: unless-stopped + - ./data:/app/data + restart: always volumes: streamflow_data: diff --git a/frontend-react/src/hooks/useWatchMovie.ts b/frontend-react/src/hooks/useWatchMovie.ts index d906a13..7d20fee 100644 --- a/frontend-react/src/hooks/useWatchMovie.ts +++ b/frontend-react/src/hooks/useWatchMovie.ts @@ -70,18 +70,73 @@ export const useWatchMovie = (slug: string | undefined, episode: string | undefi hls.loadSource(source.stream_url); hls.attachMedia(videoRef.current); hls.on(Hls.Events.MANIFEST_PARSED, () => { - videoRef.current?.play(); + videoRef.current?.play().catch(() => { }); }); return () => { hls.destroy(); }; } else if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { videoRef.current.src = source.stream_url; - videoRef.current.play(); + videoRef.current.play().catch(() => { }); } } }, [source]); + // Wake Lock Logic (Prevent Screen Sleep) + useEffect(() => { + const video = videoRef.current; + let wakeLock: any = null; + + const requestWakeLock = async () => { + try { + if ('wakeLock' in navigator) { + wakeLock = await (navigator as any).wakeLock.request('screen'); + // console.log('Wake Lock active'); + } + } catch (err) { + console.warn('Wake Lock failed:', err); + } + }; + + const releaseWakeLock = async () => { + if (wakeLock) { + try { + await wakeLock.release(); + wakeLock = null; + // console.log('Wake Lock released'); + } catch (err) { + console.warn('Wake Lock release failed:', err); + } + } + }; + + if (video) { + const onPlay = () => requestWakeLock(); + const onPause = () => releaseWakeLock(); + const onEnded = () => releaseWakeLock(); + + video.addEventListener('play', onPlay); + video.addEventListener('pause', onPause); + video.addEventListener('ended', onEnded); + + // Re-acquire on visibility change if playing + const onVisibilityChange = () => { + if (document.visibilityState === 'visible' && !video.paused) { + requestWakeLock(); + } + }; + document.addEventListener('visibilitychange', onVisibilityChange); + + return () => { + video.removeEventListener('play', onPlay); + video.removeEventListener('pause', onPause); + video.removeEventListener('ended', onEnded); + document.removeEventListener('visibilitychange', onVisibilityChange); + releaseWakeLock(); + }; + } + }, [source]); // Re-run when source changes (new video loaded) + return { movie, source, diff --git a/frontend-react/src/themes/apple/WatchPage.tsx b/frontend-react/src/themes/apple/WatchPage.tsx index 844311c..f2df061 100644 --- a/frontend-react/src/themes/apple/WatchPage.tsx +++ b/frontend-react/src/themes/apple/WatchPage.tsx @@ -71,7 +71,8 @@ export const WatchPage = ({ slug, episode }: { slug: string, episode: string }) {/* Content Section - Scrolls over the bottom of the player if sticky, or just below */} -
+ {/* Content Section - Scrolls over the bottom of the player if sticky, or just below */} +
{/* Movie Info */}
@@ -93,7 +94,7 @@ export const WatchPage = ({ slug, episode }: { slug: string, episode: string }) {episodes.length} available
-
+
{(expanded ? episodes : episodes.slice(0, 20)).map((ep) => (
- {/* Main Layout Container */} -
- {/* Sidebar / Metadata Panel */} -
-
- {/* Movie Header */} -
-

{movie.title}

-
- - {movie.quality || 'HD'} - - {movie.year || '2024'} - - 98% Match - - {movie.original_title} -
-
- - {/* Description */} -
-

Synopsis

-
-
- - {/* Episodes Grid */} - {episodes.length > 0 && ( -
-
-

- - Episodes -

- - {episodes.length} Items - -
- -
- {visibleEpisodes.map((ep) => ( - - ))} -
- - {episodes.length > 20 && ( - - )} -
- )} - - {/* Related Content Section */} -
- {/* More Like This */} -
-

Có thể bạn sẽ thích

- -
- - {/* Trending */} -
-

Phim Mới Cập Nhật

- -
- - {/* Top Movies */} -
-

Top Phim Lẻ

- -
- - {/* Top Series */} -
-

Top Phim Bộ

- -
- - {/* Animation */} -
-

Hoạt Hình Hot

- -
- - {/* TV Shows */} -
-

TV Shows

- -
-
+ {/* 1. Cinema Player Section */} +
+ {loading && ( +
+
+ )} + {(() => { + const activeEpisode = movie.episodes?.find(e => e.number === currentEpisode); + if (!activeEpisode?.url) { + return ( +
+
+

Coming Soon

+

+ We're busy uploading the best quality version of this movie. +

+
+
+
+ ); + } + + return ( +
+ + {/* 2. Content Info & Rows */} + {/* 2. Content Info & Rows */} +
+ {/* Glass Info Card */} +
+

{movie.title}

+ + {/* Meta Tags */} +
+ + {movie.quality || 'HD'} + + {movie.year || '2024'} + 98% Match + {movie.original_title} +
+ +
- {/* Main Content Area (Player) */} -
- {/* Ambient Background Gradient behind player */} -
- - {loading && ( -
-
+ {/* Episodes Section - Compact Grid */} + {episodes.length > 0 && ( +
+
+

Episodes

+
{episodes.length} Items
- )} - {(() => { - const activeEpisode = movie.episodes?.find(e => e.number === currentEpisode); - if (!activeEpisode?.url) { - return ( -
-
-
- -
-

Coming Soon

-

- The server is currently processing the movie "{movie.title}". Please check back later for the upload. -

+
+ {visibleEpisodes.map((ep) => ( + + ))} +
- return ( -
+ )} + + {/* Related Content Section */} +
+ + + +
diff --git a/frontend-react/src/themes/netflix/WatchPage.tsx b/frontend-react/src/themes/netflix/WatchPage.tsx index bb6a216..4ac09cc 100644 --- a/frontend-react/src/themes/netflix/WatchPage.tsx +++ b/frontend-react/src/themes/netflix/WatchPage.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowLeft, Play, ChevronDown, ChevronUp } from 'lucide-react'; -import { Layout } from './Layout'; + import { useWatchMovie } from '../../hooks/useWatchMovie'; import MovieRow from '../../components/MovieRow'; @@ -16,205 +16,133 @@ export const WatchPage = ({ slug, episode }: { slug: string, episode: string }) const visibleEpisodes = expanded ? episodes : episodes.slice(0, 20); return ( - -
- {/* Back Button Overlay */} + +
+ {/* Back Navigation */} +
+
-
- {/* Sidebar / Metadata Panel */} -
-
- {/* Movie Header */} -
-

{movie.title}

-
- 98% Match - {movie.year || '2024'} - HD - {movie.original_title} + {/* 1. Cinema Player Section */} +
+ {loading && ( +
+
+
+ )} + {(() => { + const activeEpisode = movie.episodes?.find(e => e.number === currentEpisode); + if (!activeEpisode?.url) { + return ( +
+
+

Coming Soon

+

+ We're busy uploading the best quality version of this movie. +

+
+ ); + } -

- {movie.description} -

+ return ( +
- {/* Episodes Grid */} - {episodes.length > 0 && ( -
-
-

Episodes

- - {episodes.length} Episodes + {/* 2. Content Info & Rows */} +
+ {/* Glass Info Card */} +
+

{movie.title}

+ + {/* Meta Tags */} +
+ 98% Match + {movie.year || '2024'} + HD + {movie.original_title} +
+ +
+
+ + {/* Episodes Section - Compact Grid */} + {episodes.length > 0 && ( +
+
+

Episodes

+
{episodes.length} Items
+
+ +
+ {visibleEpisodes.map((ep) => ( + - ))} -
- - {episodes.length > 20 && ( - - )} -
- )} - - {/* Related Content Section */} -
- {/* More Like This */} -
-

More Like This

- -
- - {/* Trending */} -
-

New Releases

- -
- - {/* Top Movies */} -
-

Top Movies

- -
- - {/* Top Series */} -
-

Top Series

- -
- - {/* Animation */} -
-

Animation

- -
- - {/* TV Shows */} -
-

TV Shows

- -
-
-
-
- - {/* Main Content Area (Player) */} -
- {loading && ( -
-
-
- )} - {(() => { - const activeEpisode = movie.episodes?.find(e => e.number === currentEpisode); - if (!activeEpisode?.url) { - return ( -
-
-

Coming Soon

-

- We're busy uploading the best quality version of this movie. -

+ {currentEpisode === ep.number && ( +
+
-
-
- ); - } + )} + + ))} +
- return ( -
+ )} + + {/* Related Content Section */} +
+ + + +
- +
); }; diff --git a/start-dev.ps1 b/start-dev.ps1 new file mode 100644 index 0000000..1dca52d --- /dev/null +++ b/start-dev.ps1 @@ -0,0 +1,51 @@ +# Streamflow Dev Start Script (Auto-Restart) + +Write-Host "=============================" -ForegroundColor Cyan +Write-Host " Streamflow Dev Launcher " -ForegroundColor Cyan +Write-Host "=============================" -ForegroundColor Cyan + +$BackendPort = 8000 +$FrontendPort = 5173 + +# Helper function to kill processes on a port +function Kill-Port($port) { + echo "Checking port $port..." + $connection = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue + if ($connection) { + $pidNum = $connection.OwningProcess + Write-Host " -> Killing process $pidNum on port $port" -ForegroundColor Yellow + Stop-Process -Id $pidNum -Force -ErrorAction SilentlyContinue + } else { + Write-Host " -> Port $port is free." -ForegroundColor Green + } +} + +# 1. Cleanup +Write-Host "`n[1/4] Cleaning up existing processes..." -ForegroundColor White +Kill-Port $BackendPort +Kill-Port $FrontendPort + +# 2. Start Backend +Write-Host "`n[2/4] Starting Backend (Go)..." -ForegroundColor White +$backendProcess = Start-Process -FilePath "go" -ArgumentList "run cmd/server/main.go" -WorkingDirectory "$PSScriptRoot\backend" -PassThru -NoNewWindow:$false +Write-Host " -> Backend started (PID: $($backendProcess.Id))" -ForegroundColor Green + +# 3. Start Frontend +Write-Host "`n[3/4] Starting Frontend (Vite)..." -ForegroundColor White +# Use npm.cmd for Windows compatibility +$frontendProcess = Start-Process -FilePath "npm.cmd" -ArgumentList "run dev" -WorkingDirectory "$PSScriptRoot\frontend-react" -PassThru -NoNewWindow:$false +Write-Host " -> Frontend started (PID: $($frontendProcess.Id))" -ForegroundColor Green + +# 4. Launch Browser +Write-Host "`n[4/4] Waiting for services..." -ForegroundColor White +for ($i = 5; $i -gt 0; $i--) { + Write-Host " -> Launching in $i seconds..." -NoNewline + Start-Sleep -Seconds 1 + Write-Host "`r" -NoNewline +} + +Write-Host "`n -> Opening http://localhost:$FrontendPort" -ForegroundColor Cyan +Start-Process "http://localhost:$FrontendPort" + +Write-Host "`nAll systems go! Close the pop-up windows to stop the servers." -ForegroundColor Magenta +Start-Sleep -Seconds 3