feat: Grid layout, feed freshness, and feature cleanup
This commit is contained in:
parent
a988bdaac3
commit
0ebea200b0
9 changed files with 332 additions and 101 deletions
81
app.py
81
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/<path:filename>')
|
||||
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)
|
||||
|
|
|
|||
66
deploy-docker.bat
Normal file
66
deploy-docker.bat
Normal 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
63
deploy-docker.ps1
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -11,16 +11,19 @@
|
|||
<!-- Filters & Categories -->
|
||||
<div class="yt-filter-bar">
|
||||
<div class="yt-categories" id="categoryList">
|
||||
<!-- "All" removed, starting with Tech -->
|
||||
<button class="yt-chip" onclick="switchCategory('tech')">Tech</button>
|
||||
<button class="yt-chip" onclick="switchCategory('music')">Music</button>
|
||||
<button class="yt-chip" onclick="switchCategory('movies')">Movies</button>
|
||||
<button class="yt-chip" onclick="switchCategory('news')">News</button>
|
||||
<button class="yt-chip" onclick="switchCategory('trending')">Trending</button>
|
||||
<button class="yt-chip" onclick="switchCategory('podcasts')">Podcasts</button>
|
||||
<button class="yt-chip" onclick="switchCategory('live')">Live</button>
|
||||
<button class="yt-chip" onclick="switchCategory('gaming')">Gaming</button>
|
||||
<button class="yt-chip" onclick="switchCategory('sports')">Sports</button>
|
||||
<!-- Pinned Categories -->
|
||||
<button class="yt-chip" onclick="switchCategory('history', this)"><i class="fas fa-history"></i> Watched</button>
|
||||
<button class="yt-chip" onclick="switchCategory('suggested', this)"><i class="fas fa-magic"></i> Suggested</button>
|
||||
<!-- Standard Categories -->
|
||||
<button class="yt-chip" onclick="switchCategory('tech', this)">Tech</button>
|
||||
<button class="yt-chip" onclick="switchCategory('music', this)">Music</button>
|
||||
<button class="yt-chip" onclick="switchCategory('movies', this)">Movies</button>
|
||||
<button class="yt-chip" onclick="switchCategory('news', this)">News</button>
|
||||
<button class="yt-chip" onclick="switchCategory('trending', this)">Trending</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 class="yt-filter-actions">
|
||||
|
|
|
|||
|
|
@ -63,19 +63,14 @@
|
|||
Queue
|
||||
<span id="queueBadge" class="queue-badge" style="display:none;">0</span>
|
||||
</button>
|
||||
<button class="yt-action-btn" id="summarizeBtn" onclick="summarizeVideo()">
|
||||
<i class="fas fa-magic"></i>
|
||||
Summarize
|
||||
</button>
|
||||
<!-- Summarize button removed -->
|
||||
<!-- Transcribe button removed -->
|
||||
<!-- Rotation controls removed -->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Summary Box (Hidden by default) -->
|
||||
<div class="yt-description-box" id="summaryBox"
|
||||
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>
|
||||
<!-- Summary Box removed -->
|
||||
|
||||
<!-- Channel Info -->
|
||||
<div class="yt-channel-info">
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue