From 0ebea200b08ec5778c7cf18a1ea68b6afe599dfd Mon Sep 17 00:00:00 2001 From: KV-Tube Deployer Date: Thu, 1 Jan 2026 20:08:35 +0700 Subject: [PATCH] feat: Grid layout, feed freshness, and feature cleanup --- app.py | 81 +++++++++++++++-- deploy-docker.bat | 66 ++++++++++++++ deploy-docker.ps1 | 63 +++++++++++++ ...uct Requirements Document (PRD) - KV-Tube} | 90 +++++++++---------- requirements.txt | 11 +-- static/css/style.css | 49 +++++++--- static/js/main.js | 26 +++++- templates/index.html | 23 ++--- templates/watch.html | 24 ++--- 9 files changed, 332 insertions(+), 101 deletions(-) create mode 100644 deploy-docker.bat create mode 100644 deploy-docker.ps1 rename doc/{Product Requirements Document (PRD) - Khoavo-Tube => Product Requirements Document (PRD) - KV-Tube} (85%) diff --git a/app.py b/app.py index d2a7c64..41c6388 100644 --- a/app.py +++ b/app.py @@ -13,7 +13,15 @@ from functools import wraps from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled import re import heapq -# nltk removed to avoid SSL/download issues. Using regex instead. +import threading +import uuid +import datetime +import time + + + +# Fix for OMP: Error #15 +os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" app = Flask(__name__) app.secret_key = 'super_secret_key_change_this' # Required for sessions @@ -59,7 +67,11 @@ def init_db(): # Run init init_db() +# Transcription Task Status +transcription_tasks = {} + def get_db_connection(): + conn = sqlite3.connect(DB_NAME) conn.row_factory = sqlite3.Row return conn @@ -151,11 +163,44 @@ def save_video(): return jsonify({'status': 'already_saved'}) conn.execute('INSERT INTO user_videos (user_id, video_id, title, thumbnail, type) VALUES (?, ?, ?, ?, ?)', - (session['user_id'], video_id, title, thumbnail, action_type)) + (1, video_id, title, thumbnail, action_type)) # Default user_id 1 conn.commit() conn.close() return jsonify({'status': 'success'}) +@app.route('/api/history') +def get_history(): + conn = get_db_connection() + rows = conn.execute('SELECT video_id as id, title, thumbnail FROM user_videos WHERE type = "history" ORDER BY timestamp DESC LIMIT 50').fetchall() + conn.close() + return jsonify([dict(row) for row in rows]) + +@app.route('/api/suggested') +def get_suggested(): + # Simple recommendation based on history: search for "trending" related to the last 3 viewed channels/titles + conn = get_db_connection() + history = conn.execute('SELECT title FROM user_videos WHERE type = "history" ORDER BY timestamp DESC LIMIT 3').fetchall() + conn.close() + + if not history: + return jsonify(fetch_videos("trending", limit=20)) + + all_suggestions = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + queries = [f"{row['title']} related" for row in history] + results = list(executor.map(lambda q: fetch_videos(q, limit=10), queries)) + for res in results: + all_suggestions.extend(res) + + # Remove duplicates and shuffle + unique_vids = {v['id']: v for v in all_suggestions}.values() + import random + final_list = list(unique_vids) + random.shuffle(final_list) + + return jsonify(final_list[:30]) + + @app.route('/stream/') def stream_local(filename): return send_from_directory(VIDEO_DIR, filename) @@ -615,7 +660,7 @@ def get_stream_info(): response = jsonify(response_data) response.headers['X-Cache'] = 'MISS' return response - + except Exception as e: return jsonify({'error': str(e)}), 500 @@ -975,6 +1020,9 @@ def trending(): base = queries.get(cat, 'trending') + if s_sort == 'newest': + return base + ', today' # Or use explicit date filter + from datetime import datetime, timedelta three_months_ago = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d') @@ -987,10 +1035,13 @@ def trending(): } return base + sort_filters.get(s_sort, f" after:{three_months_ago}") + sort = request.args.get('sort', 'newest') # Ensure newest is default + # === Parallel Fetching for Home Feed === if category == 'all': sections_to_fetch = [ {'id': 'trending', 'title': 'Trending Now', 'icon': 'fire'}, + {'id': 'all', 'title': 'New Releases', 'icon': 'clock'}, # 'all' results in trending, but we'll sort by newest {'id': 'tech', 'title': 'AI & Tech', 'icon': 'microchip'}, {'id': 'music', 'title': 'Music', 'icon': 'music'}, {'id': 'movies', 'title': 'Movies', 'icon': 'film'}, @@ -1000,17 +1051,19 @@ def trending(): ] def fetch_section(section): - q = get_query(section['id'], region, sort) - # Fetch 80 videos per section to guarantee density (target: 50+ after filters) - vids = fetch_videos(q, limit=80, filter_type='video', playlist_start=1) + target_sort = 'newest' if section['id'] != 'trending' else 'relevance' + q = get_query(section['id'], region, target_sort) + # Add a unique component to query for freshness + q_fresh = f"{q} {int(time.time())}" if section['id'] == 'all' else q + vids = fetch_videos(q_fresh, limit=20, filter_type='video', playlist_start=1) return { 'id': section['id'], 'title': section['title'], 'icon': section['icon'], - 'videos': vids[:60] + 'videos': vids[:16] } - with concurrent.futures.ThreadPoolExecutor(max_workers=7) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: results = list(executor.map(fetch_section, sections_to_fetch)) return jsonify({'mode': 'sections', 'data': results}) @@ -1029,8 +1082,14 @@ def trending(): filter_mode = 'short' if category == 'shorts' else 'video' results = fetch_videos(query, limit=limit, filter_type=filter_mode, playlist_start=start) + # Randomize a bit for "freshness" if it's the first page + if page == 1: + import random + random.shuffle(results) + return jsonify(results) + except Exception as e: return jsonify({'error': str(e)}), 500 @@ -1099,5 +1158,9 @@ def get_comments(): except Exception as e: return jsonify({'comments': [], 'count': 0, 'error': str(e)}) + +# --- AI Transcription REMOVED --- + if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5001) + print("Starting KV-Tube Server on port 5002 (Reloader Disabled)") + app.run(debug=True, host='0.0.0.0', port=5002, use_reloader=False) diff --git a/deploy-docker.bat b/deploy-docker.bat new file mode 100644 index 0000000..d618619 --- /dev/null +++ b/deploy-docker.bat @@ -0,0 +1,66 @@ +@echo off +REM deploy-docker.bat - Build and push KV-Tube to Docker Hub + +set DOCKER_USER=vndangkhoa +set IMAGE_NAME=kvtube +set TAG=latest +set FULL_IMAGE=%DOCKER_USER%/%IMAGE_NAME%:%TAG% + +echo ======================================== +echo KV-Tube Docker Deployment Script +echo ======================================== +echo. + +REM Step 1: Check Docker +echo [1/4] Checking Docker... +docker info >nul 2>&1 +if %errorlevel% neq 0 ( + echo X Docker is not running. Please start Docker Desktop. + pause + exit /b 1 +) +echo OK Docker is running + +REM Step 2: Build Image +echo. +echo [2/4] Building Docker image: %FULL_IMAGE% +docker build --no-cache -t %FULL_IMAGE% . +if %errorlevel% neq 0 ( + echo X Build failed! + pause + exit /b 1 +) +echo OK Build successful + +REM Step 3: Login to Docker Hub +echo. +echo [3/4] Logging into Docker Hub... +docker login +if %errorlevel% neq 0 ( + echo X Login failed! + pause + exit /b 1 +) +echo OK Login successful + +REM Step 4: Push Image +echo. +echo [4/4] Pushing to Docker Hub... +docker push %FULL_IMAGE% +if %errorlevel% neq 0 ( + echo X Push failed! + pause + exit /b 1 +) +echo OK Push successful + +echo. +echo ======================================== +echo Deployment Complete! +echo Image: %FULL_IMAGE% +echo URL: https://hub.docker.com/r/%DOCKER_USER%/%IMAGE_NAME% +echo ======================================== +echo. +echo To run: docker run -p 5001:5001 %FULL_IMAGE% +echo. +pause diff --git a/deploy-docker.ps1 b/deploy-docker.ps1 new file mode 100644 index 0000000..944593d --- /dev/null +++ b/deploy-docker.ps1 @@ -0,0 +1,63 @@ +#!/usr/bin/env pwsh +# deploy-docker.ps1 - Build and push KV-Tube to Docker Hub + +$ErrorActionPreference = "Stop" + +$DOCKER_USER = "vndangkhoa" +$IMAGE_NAME = "kvtube" +$TAG = "latest" +$FULL_IMAGE = "${DOCKER_USER}/${IMAGE_NAME}:${TAG}" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " KV-Tube Docker Deployment Script" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +# Step 1: Check Docker +Write-Host "[1/4] Checking Docker..." -ForegroundColor Yellow +try { + docker info | Out-Null + Write-Host " ✓ Docker is running" -ForegroundColor Green +} catch { + Write-Host " ✗ Docker is not running. Please start Docker Desktop." -ForegroundColor Red + exit 1 +} + +# Step 2: Build Image +Write-Host "" +Write-Host "[2/4] Building Docker image: $FULL_IMAGE" -ForegroundColor Yellow +docker build --no-cache -t $FULL_IMAGE . +if ($LASTEXITCODE -ne 0) { + Write-Host " ✗ Build failed!" -ForegroundColor Red + exit 1 +} +Write-Host " ✓ Build successful" -ForegroundColor Green + +# Step 3: Login to Docker Hub +Write-Host "" +Write-Host "[3/4] Logging into Docker Hub..." -ForegroundColor Yellow +docker login +if ($LASTEXITCODE -ne 0) { + Write-Host " ✗ Login failed!" -ForegroundColor Red + exit 1 +} +Write-Host " ✓ Login successful" -ForegroundColor Green + +# Step 4: Push Image +Write-Host "" +Write-Host "[4/4] Pushing to Docker Hub..." -ForegroundColor Yellow +docker push $FULL_IMAGE +if ($LASTEXITCODE -ne 0) { + Write-Host " ✗ Push failed!" -ForegroundColor Red + exit 1 +} +Write-Host " ✓ Push successful" -ForegroundColor Green + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Deployment Complete!" -ForegroundColor Cyan +Write-Host " Image: $FULL_IMAGE" -ForegroundColor Cyan +Write-Host " URL: https://hub.docker.com/r/${DOCKER_USER}/${IMAGE_NAME}" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "To run: docker run -p 5001:5001 $FULL_IMAGE" -ForegroundColor White diff --git a/doc/Product Requirements Document (PRD) - Khoavo-Tube b/doc/Product Requirements Document (PRD) - KV-Tube similarity index 85% rename from doc/Product Requirements Document (PRD) - Khoavo-Tube rename to doc/Product Requirements Document (PRD) - KV-Tube index de29e9e..5d5cd69 100644 --- a/doc/Product Requirements Document (PRD) - Khoavo-Tube +++ b/doc/Product Requirements Document (PRD) - KV-Tube @@ -1,46 +1,46 @@ -Product Requirements Document (PRD) - KCTube -1. Product Overview -Product Name: KCTube Version: 1.0 (In Development) Description: KCTube is a comprehensive media center web application designed to provide an ad-free YouTube experience, a curated movie streaming service, and a local video management system. It emphasizes privacy, absence of advertisements, and utility features like AI summarization and language learning tools. - -2. User Personas -The Binge Watcher: Wants uninterrupted access to YouTube content and movies without ads. -The Archivist: Maintains a local collection of videos and wants a clean interface to organize and watch them securely. -The Learner: Uses video content for educational purposes, specifically English learning. -The Tech Enthusiast: Appreciates PWA support, torrent integration, and customizable settings. -3. Core Features -3.1. YouTube Viewer (Home) -Ad-Free Experience: Plays YouTube videos without third-party advertisements. -Search: Integrated search bar powered by yt-dlp to find videos, channels, and playlists. -Playback: Custom video player with support for quality selection and playback speed. -AI Summarization: Feature to summarize video content using Google Gemini API (Optional). -3.2. local Video Manager ("My Videos") -Secure Access: Password-protected section for personal video collections. -File Management: Scans local directories for video files. -Metadata: Extracts metadata (duration, format) and generates thumbnails using FFmpeg/MoviePy. -Playback: Native HTML5 player for local files. -3.3. Utilities -Torrent Player: Interface for streaming/playing video content via torrents. -Playlist Manager: Create and manage custom playlists of YouTube videos. -Camera/Photo: ("Chụp ảnh") Feature to capture or manage photos (Webcam integration). -Configuration: Web-based settings to manage application behavior (e.g., password, storage paths). -4. Technical Architecture -Backend: Python / Flask -Frontend: HTML5, CSS3, JavaScript (Vanilla) -Database/Storage: JSON-based local storage and file system. -Video Processing: yt-dlp (YouTube), FFmpeg (Conversion/Thumbnail), MoviePy (Optional). -AI Service: Google Gemini API (for summarization). -Deployment: Docker container support (xehopnet/kctube). -5. Non-Functional Requirements -Performance: Fast load times and responsive UI. -Compatibility: PWA-ready for installation on desktop and mobile. -Reliability: graceful degradation if optional dependencies (MoviePy, Gemini) are missing. -Privacy: No user tracking or external analytics. -6. Known Limitations -Search Reliability: Dependent on yt-dlp stability and YouTube's anti-bot measures. -External APIs: Movie features rely on third-party APIs which may have downtime. -Dependency Management: Some Python libraries (MoviePy, Numpy) require compilation tools. -7. Future Roadmap -Database: Migrate from JSON to SQLite for better performance with large libraries. -User Accounts: Individual user profiles and history. -Offline Mode: Enhanced offline capabilities for PWA. +Product Requirements Document (PRD) - KV-Tube +1. Product Overview +Product Name: KV-Tube Version: 1.0 (In Development) Description: KV-Tube is a comprehensive media center web application designed to provide an ad-free YouTube experience, a curated movie streaming service, and a local video management system. It emphasizes privacy, absence of advertisements, and utility features like AI summarization and language learning tools. + +2. User Personas +The Binge Watcher: Wants uninterrupted access to YouTube content and movies without ads. +The Archivist: Maintains a local collection of videos and wants a clean interface to organize and watch them securely. +The Learner: Uses video content for educational purposes, specifically English learning. +The Tech Enthusiast: Appreciates PWA support, torrent integration, and customizable settings. +3. Core Features +3.1. YouTube Viewer (Home) +Ad-Free Experience: Plays YouTube videos without third-party advertisements. +Search: Integrated search bar powered by yt-dlp to find videos, channels, and playlists. +Playback: Custom video player with support for quality selection and playback speed. +AI Summarization: Feature to summarize video content using Google Gemini API (Optional). +3.2. local Video Manager ("My Videos") +Secure Access: Password-protected section for personal video collections. +File Management: Scans local directories for video files. +Metadata: Extracts metadata (duration, format) and generates thumbnails using FFmpeg/MoviePy. +Playback: Native HTML5 player for local files. +3.3. Utilities +Torrent Player: Interface for streaming/playing video content via torrents. +Playlist Manager: Create and manage custom playlists of YouTube videos. +Camera/Photo: ("Chụp ảnh") Feature to capture or manage photos (Webcam integration). +Configuration: Web-based settings to manage application behavior (e.g., password, storage paths). +4. Technical Architecture +Backend: Python / Flask +Frontend: HTML5, CSS3, JavaScript (Vanilla) +Database/Storage: JSON-based local storage and file system. +Video Processing: yt-dlp (YouTube), FFmpeg (Conversion/Thumbnail), MoviePy (Optional). +AI Service: Google Gemini API (for summarization). +Deployment: Docker container support (xehopnet/kctube). +5. Non-Functional Requirements +Performance: Fast load times and responsive UI. +Compatibility: PWA-ready for installation on desktop and mobile. +Reliability: graceful degradation if optional dependencies (MoviePy, Gemini) are missing. +Privacy: No user tracking or external analytics. +6. Known Limitations +Search Reliability: Dependent on yt-dlp stability and YouTube's anti-bot measures. +External APIs: Movie features rely on third-party APIs which may have downtime. +Dependency Management: Some Python libraries (MoviePy, Numpy) require compilation tools. +7. Future Roadmap +Database: Migrate from JSON to SQLite for better performance with large libraries. +User Accounts: Individual user profiles and history. +Offline Mode: Enhanced offline capabilities for PWA. Casting: Support for Chromecast/AirPlay. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 43af283..adb4a58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,7 @@ flask==3.0.2 requests==2.31.0 -pytube==15.0.0 -python-dotenv==1.0.1 yt-dlp -moviepy -numpy -google-generativeai==0.8.3 -flask-cors==4.0.0 youtube-transcript-api==0.6.2 werkzeug==3.0.1 -Pillow -pysrt==1.1.2 -cairosvg -gunicorn==21.2.0 gunicorn==21.2.0 +python-dotenv==1.0.1 diff --git a/static/css/style.css b/static/css/style.css index c9e0d0f..b13f965 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -407,24 +407,45 @@ button { /* ===== Video Grid ===== */ .yt-video-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 16px 16px; + grid-template-columns: repeat(4, 1fr); + gap: 16px; } -/* 4-Row Horizontal Grid for Sections */ +@media (max-width: 1400px) { + + .yt-video-grid, + .yt-section-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 1100px) { + + .yt-video-grid, + .yt-section-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 600px) { + + .yt-video-grid, + .yt-section-grid { + grid-template-columns: 1fr; + } +} + +/* Grid Layout for Sections (4 rows x 4 columns = 16 videos) */ .yt-section-grid { display: grid; - grid-template-rows: repeat(4, min-content); - /* Force 4 rows */ - grid-auto-flow: column; - /* Fill columns first (horizontal scroll) */ - grid-auto-columns: 280px; - /* Fixed column width */ + grid-template-columns: repeat(4, 1fr); gap: 16px; - overflow-x: auto; - padding-bottom: 16px; - /* Space for scrollbar if any (hidden typically) */ - scrollbar-width: none; + padding-bottom: 24px; +} + +.yt-section-grid .yt-video-card { + width: 100%; + min-width: 0; } @media (max-width: 768px) { @@ -527,6 +548,7 @@ button { color: var(--yt-text-primary); display: -webkit-box; -webkit-line-clamp: 2; + line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 4px; @@ -1246,6 +1268,7 @@ button { color: var(--yt-text-primary); display: -webkit-box; -webkit-line-clamp: 2; + line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; diff --git a/static/js/main.js b/static/js/main.js index 59c3fae..0851eda 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -197,9 +197,26 @@ async function switchCategory(category, btn) { } } + // Handle Special Categories + if (category === 'history') { + const response = await fetch('/api/history'); + const data = await response.json(); + displayResults(data, false); + isLoading = false; + return; + } + if (category === 'suggested') { + const response = await fetch('/api/suggested'); + const data = await response.json(); + displayResults(data, false); + isLoading = false; + return; + } + // Load both videos and shorts with current category, sort, and region await loadTrending(true); + // Also reload shorts to match category if (typeof loadShorts === 'function') { loadShorts(); @@ -235,12 +252,15 @@ async function loadTrending(reset = true) { } try { - // Get sort and region values from page (if available) - const sortValue = window.currentSort || 'month'; + // Default to 'newest' for fresh content on main page + const sortValue = window.currentSort || (currentCategory === 'all' ? 'newest' : 'month'); const regionValue = window.currentRegion || 'vietnam'; - const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}®ion=${regionValue}`); + // Add cache-buster for home page to ensure fresh content + const cb = reset && currentCategory === 'all' ? `&_=${Date.now()}` : ''; + const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}®ion=${regionValue}${cb}`); const data = await response.json(); + if (data.error) { console.error('Trending error:', data.error); if (reset) { diff --git a/templates/index.html b/templates/index.html index 2b62ead..4913022 100644 --- a/templates/index.html +++ b/templates/index.html @@ -11,16 +11,19 @@
- - - - - - - - - - + + + + + + + + + + + + +
diff --git a/templates/watch.html b/templates/watch.html index 732988f..a5a46ca 100644 --- a/templates/watch.html +++ b/templates/watch.html @@ -63,19 +63,14 @@ Queue - + + +
+ - +
@@ -710,7 +705,14 @@ uploader: "" }; + let currentRotation = 0; + // Rotation function removed + + // Transcription functions removed + function initArtplayer(url, poster, subtitleUrl = '', type = 'auto') { + // Store art instance globally for other functions + window.art = null; // Update currentVideoData with poster (thumbnail) if (poster) { // Use stable YouTube thumbnail URL to prevent expiration of signed URLs @@ -732,7 +734,7 @@ }, 2000); } - const art = new Artplayer({ + window.art = new Artplayer({ container: '#artplayer-app', url: url, poster: poster,