feat: Grid layout, feed freshness, and feature cleanup

This commit is contained in:
KV-Tube Deployer 2026-01-01 20:08:35 +07:00
parent a988bdaac3
commit 0ebea200b0
9 changed files with 332 additions and 101 deletions

81
app.py
View file

@ -13,7 +13,15 @@ from functools import wraps
from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled
import re import re
import heapq 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 = Flask(__name__)
app.secret_key = 'super_secret_key_change_this' # Required for sessions app.secret_key = 'super_secret_key_change_this' # Required for sessions
@ -59,7 +67,11 @@ def init_db():
# Run init # Run init
init_db() init_db()
# Transcription Task Status
transcription_tasks = {}
def get_db_connection(): def get_db_connection():
conn = sqlite3.connect(DB_NAME) conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
@ -151,11 +163,44 @@ def save_video():
return jsonify({'status': 'already_saved'}) return jsonify({'status': 'already_saved'})
conn.execute('INSERT INTO user_videos (user_id, video_id, title, thumbnail, type) VALUES (?, ?, ?, ?, ?)', 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.commit()
conn.close() conn.close()
return jsonify({'status': 'success'}) 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/<path:filename>') @app.route('/stream/<path:filename>')
def stream_local(filename): def stream_local(filename):
return send_from_directory(VIDEO_DIR, filename) return send_from_directory(VIDEO_DIR, filename)
@ -615,7 +660,7 @@ def get_stream_info():
response = jsonify(response_data) response = jsonify(response_data)
response.headers['X-Cache'] = 'MISS' response.headers['X-Cache'] = 'MISS'
return response return response
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@ -975,6 +1020,9 @@ def trending():
base = queries.get(cat, 'trending') base = queries.get(cat, 'trending')
if s_sort == 'newest':
return base + ', today' # Or use explicit date filter
from datetime import datetime, timedelta from datetime import datetime, timedelta
three_months_ago = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d') 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}") 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 === # === Parallel Fetching for Home Feed ===
if category == 'all': if category == 'all':
sections_to_fetch = [ sections_to_fetch = [
{'id': 'trending', 'title': 'Trending Now', 'icon': 'fire'}, {'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': 'tech', 'title': 'AI & Tech', 'icon': 'microchip'},
{'id': 'music', 'title': 'Music', 'icon': 'music'}, {'id': 'music', 'title': 'Music', 'icon': 'music'},
{'id': 'movies', 'title': 'Movies', 'icon': 'film'}, {'id': 'movies', 'title': 'Movies', 'icon': 'film'},
@ -1000,17 +1051,19 @@ def trending():
] ]
def fetch_section(section): def fetch_section(section):
q = get_query(section['id'], region, sort) target_sort = 'newest' if section['id'] != 'trending' else 'relevance'
# Fetch 80 videos per section to guarantee density (target: 50+ after filters) q = get_query(section['id'], region, target_sort)
vids = fetch_videos(q, limit=80, filter_type='video', playlist_start=1) # 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 { return {
'id': section['id'], 'id': section['id'],
'title': section['title'], 'title': section['title'],
'icon': section['icon'], '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)) results = list(executor.map(fetch_section, sections_to_fetch))
return jsonify({'mode': 'sections', 'data': results}) return jsonify({'mode': 'sections', 'data': results})
@ -1029,8 +1082,14 @@ def trending():
filter_mode = 'short' if category == 'shorts' else 'video' filter_mode = 'short' if category == 'shorts' else 'video'
results = fetch_videos(query, limit=limit, filter_type=filter_mode, playlist_start=start) 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) return jsonify(results)
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@ -1099,5 +1158,9 @@ def get_comments():
except Exception as e: except Exception as e:
return jsonify({'comments': [], 'count': 0, 'error': str(e)}) return jsonify({'comments': [], 'count': 0, 'error': str(e)})
# --- AI Transcription REMOVED ---
if __name__ == '__main__': 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)

66
deploy-docker.bat Normal file
View file

@ -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

63
deploy-docker.ps1 Normal file
View file

@ -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

View file

@ -1,46 +1,46 @@
Product Requirements Document (PRD) - KCTube Product Requirements Document (PRD) - KV-Tube
1. Product Overview 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. 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 2. User Personas
The Binge Watcher: Wants uninterrupted access to YouTube content and movies without ads. 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 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 Learner: Uses video content for educational purposes, specifically English learning.
The Tech Enthusiast: Appreciates PWA support, torrent integration, and customizable settings. The Tech Enthusiast: Appreciates PWA support, torrent integration, and customizable settings.
3. Core Features 3. Core Features
3.1. YouTube Viewer (Home) 3.1. YouTube Viewer (Home)
Ad-Free Experience: Plays YouTube videos without third-party advertisements. Ad-Free Experience: Plays YouTube videos without third-party advertisements.
Search: Integrated search bar powered by yt-dlp to find videos, channels, and playlists. 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. Playback: Custom video player with support for quality selection and playback speed.
AI Summarization: Feature to summarize video content using Google Gemini API (Optional). AI Summarization: Feature to summarize video content using Google Gemini API (Optional).
3.2. local Video Manager ("My Videos") 3.2. local Video Manager ("My Videos")
Secure Access: Password-protected section for personal video collections. Secure Access: Password-protected section for personal video collections.
File Management: Scans local directories for video files. File Management: Scans local directories for video files.
Metadata: Extracts metadata (duration, format) and generates thumbnails using FFmpeg/MoviePy. Metadata: Extracts metadata (duration, format) and generates thumbnails using FFmpeg/MoviePy.
Playback: Native HTML5 player for local files. Playback: Native HTML5 player for local files.
3.3. Utilities 3.3. Utilities
Torrent Player: Interface for streaming/playing video content via torrents. Torrent Player: Interface for streaming/playing video content via torrents.
Playlist Manager: Create and manage custom playlists of YouTube videos. Playlist Manager: Create and manage custom playlists of YouTube videos.
Camera/Photo: ("Chụp ảnh") Feature to capture or manage photos (Webcam integration). 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). Configuration: Web-based settings to manage application behavior (e.g., password, storage paths).
4. Technical Architecture 4. Technical Architecture
Backend: Python / Flask Backend: Python / Flask
Frontend: HTML5, CSS3, JavaScript (Vanilla) Frontend: HTML5, CSS3, JavaScript (Vanilla)
Database/Storage: JSON-based local storage and file system. Database/Storage: JSON-based local storage and file system.
Video Processing: yt-dlp (YouTube), FFmpeg (Conversion/Thumbnail), MoviePy (Optional). Video Processing: yt-dlp (YouTube), FFmpeg (Conversion/Thumbnail), MoviePy (Optional).
AI Service: Google Gemini API (for summarization). AI Service: Google Gemini API (for summarization).
Deployment: Docker container support (xehopnet/kctube). Deployment: Docker container support (xehopnet/kctube).
5. Non-Functional Requirements 5. Non-Functional Requirements
Performance: Fast load times and responsive UI. Performance: Fast load times and responsive UI.
Compatibility: PWA-ready for installation on desktop and mobile. Compatibility: PWA-ready for installation on desktop and mobile.
Reliability: graceful degradation if optional dependencies (MoviePy, Gemini) are missing. Reliability: graceful degradation if optional dependencies (MoviePy, Gemini) are missing.
Privacy: No user tracking or external analytics. Privacy: No user tracking or external analytics.
6. Known Limitations 6. Known Limitations
Search Reliability: Dependent on yt-dlp stability and YouTube's anti-bot measures. 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. External APIs: Movie features rely on third-party APIs which may have downtime.
Dependency Management: Some Python libraries (MoviePy, Numpy) require compilation tools. Dependency Management: Some Python libraries (MoviePy, Numpy) require compilation tools.
7. Future Roadmap 7. Future Roadmap
Database: Migrate from JSON to SQLite for better performance with large libraries. Database: Migrate from JSON to SQLite for better performance with large libraries.
User Accounts: Individual user profiles and history. User Accounts: Individual user profiles and history.
Offline Mode: Enhanced offline capabilities for PWA. Offline Mode: Enhanced offline capabilities for PWA.
Casting: Support for Chromecast/AirPlay. Casting: Support for Chromecast/AirPlay.

View file

@ -1,16 +1,7 @@
flask==3.0.2 flask==3.0.2
requests==2.31.0 requests==2.31.0
pytube==15.0.0
python-dotenv==1.0.1
yt-dlp yt-dlp
moviepy
numpy
google-generativeai==0.8.3
flask-cors==4.0.0
youtube-transcript-api==0.6.2 youtube-transcript-api==0.6.2
werkzeug==3.0.1 werkzeug==3.0.1
Pillow
pysrt==1.1.2
cairosvg
gunicorn==21.2.0
gunicorn==21.2.0 gunicorn==21.2.0
python-dotenv==1.0.1

View file

@ -407,24 +407,45 @@ button {
/* ===== Video Grid ===== */ /* ===== Video Grid ===== */
.yt-video-grid { .yt-video-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(4, 1fr);
gap: 16px 16px; 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 { .yt-section-grid {
display: grid; display: grid;
grid-template-rows: repeat(4, min-content); grid-template-columns: repeat(4, 1fr);
/* Force 4 rows */
grid-auto-flow: column;
/* Fill columns first (horizontal scroll) */
grid-auto-columns: 280px;
/* Fixed column width */
gap: 16px; gap: 16px;
overflow-x: auto; padding-bottom: 24px;
padding-bottom: 16px; }
/* Space for scrollbar if any (hidden typically) */
scrollbar-width: none; .yt-section-grid .yt-video-card {
width: 100%;
min-width: 0;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -527,6 +548,7 @@ button {
color: var(--yt-text-primary); color: var(--yt-text-primary);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
margin-bottom: 4px; margin-bottom: 4px;
@ -1246,6 +1268,7 @@ button {
color: var(--yt-text-primary); color: var(--yt-text-primary);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View file

@ -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 // Load both videos and shorts with current category, sort, and region
await loadTrending(true); await loadTrending(true);
// Also reload shorts to match category // Also reload shorts to match category
if (typeof loadShorts === 'function') { if (typeof loadShorts === 'function') {
loadShorts(); loadShorts();
@ -235,12 +252,15 @@ async function loadTrending(reset = true) {
} }
try { try {
// Get sort and region values from page (if available) // Default to 'newest' for fresh content on main page
const sortValue = window.currentSort || 'month'; const sortValue = window.currentSort || (currentCategory === 'all' ? 'newest' : 'month');
const regionValue = window.currentRegion || 'vietnam'; const regionValue = window.currentRegion || 'vietnam';
const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}&region=${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}&region=${regionValue}${cb}`);
const data = await response.json(); const data = await response.json();
if (data.error) { if (data.error) {
console.error('Trending error:', data.error); console.error('Trending error:', data.error);
if (reset) { if (reset) {

View file

@ -11,16 +11,19 @@
<!-- Filters & Categories --> <!-- Filters & Categories -->
<div class="yt-filter-bar"> <div class="yt-filter-bar">
<div class="yt-categories" id="categoryList"> <div class="yt-categories" id="categoryList">
<!-- "All" removed, starting with Tech --> <!-- Pinned Categories -->
<button class="yt-chip" onclick="switchCategory('tech')">Tech</button> <button class="yt-chip" onclick="switchCategory('history', this)"><i class="fas fa-history"></i> Watched</button>
<button class="yt-chip" onclick="switchCategory('music')">Music</button> <button class="yt-chip" onclick="switchCategory('suggested', this)"><i class="fas fa-magic"></i> Suggested</button>
<button class="yt-chip" onclick="switchCategory('movies')">Movies</button> <!-- Standard Categories -->
<button class="yt-chip" onclick="switchCategory('news')">News</button> <button class="yt-chip" onclick="switchCategory('tech', this)">Tech</button>
<button class="yt-chip" onclick="switchCategory('trending')">Trending</button> <button class="yt-chip" onclick="switchCategory('music', this)">Music</button>
<button class="yt-chip" onclick="switchCategory('podcasts')">Podcasts</button> <button class="yt-chip" onclick="switchCategory('movies', this)">Movies</button>
<button class="yt-chip" onclick="switchCategory('live')">Live</button> <button class="yt-chip" onclick="switchCategory('news', this)">News</button>
<button class="yt-chip" onclick="switchCategory('gaming')">Gaming</button> <button class="yt-chip" onclick="switchCategory('trending', this)">Trending</button>
<button class="yt-chip" onclick="switchCategory('sports')">Sports</button> <button class="yt-chip" onclick="switchCategory('podcasts', this)">Podcasts</button>
<button class="yt-chip" onclick="switchCategory('live', this)">Live</button>
<button class="yt-chip" onclick="switchCategory('gaming', this)">Gaming</button>
<button class="yt-chip" onclick="switchCategory('sports', this)">Sports</button>
</div> </div>
<div class="yt-filter-actions"> <div class="yt-filter-actions">

View file

@ -63,19 +63,14 @@
Queue Queue
<span id="queueBadge" class="queue-badge" style="display:none;">0</span> <span id="queueBadge" class="queue-badge" style="display:none;">0</span>
</button> </button>
<button class="yt-action-btn" id="summarizeBtn" onclick="summarizeVideo()"> <!-- Summarize button removed -->
<i class="fas fa-magic"></i> <!-- Transcribe button removed -->
Summarize <!-- Rotation controls removed -->
</button>
</div> </div>
<!-- Summary Box (Hidden by default) --> <!-- Summary Box (Hidden by default) -->
<div class="yt-description-box" id="summaryBox" <!-- Summary Box removed -->
style="display:none; margin-bottom:16px; border: 1px solid var(--yt-accent-blue); background: rgba(62, 166, 255, 0.1);">
<p class="yt-description-stats" style="color: var(--yt-accent-blue);"><i class="fas fa-sparkles"></i> AI
Summary</p>
<p class="yt-description-text" id="summaryText">Generating summary...</p>
</div>
<!-- Channel Info --> <!-- Channel Info -->
<div class="yt-channel-info"> <div class="yt-channel-info">
@ -710,7 +705,14 @@
uploader: "" uploader: ""
}; };
let currentRotation = 0;
// Rotation function removed
// Transcription functions removed
function initArtplayer(url, poster, subtitleUrl = '', type = 'auto') { function initArtplayer(url, poster, subtitleUrl = '', type = 'auto') {
// Store art instance globally for other functions
window.art = null;
// Update currentVideoData with poster (thumbnail) // Update currentVideoData with poster (thumbnail)
if (poster) { if (poster) {
// Use stable YouTube thumbnail URL to prevent expiration of signed URLs // Use stable YouTube thumbnail URL to prevent expiration of signed URLs
@ -732,7 +734,7 @@
}, 2000); }, 2000);
} }
const art = new Artplayer({ window.art = new Artplayer({
container: '#artplayer-app', container: '#artplayer-app',
url: url, url: url,
poster: poster, poster: poster,