Files
youclonedl/backend/main.py
2026-05-25 20:16:15 +02:00

157 lines
7.5 KiB
Python

import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from .config import settings
from .database import init_db, SessionLocal
from .services import ytdlp as ytdlp_service
from .routers import auth, channels, videos, search, downloads, discovery, settings as settings_router, stats as stats_router, export as export_router, collections as collections_router, admin as admin_router
app = FastAPI(title="YouTube Hub", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(channels.router, prefix="/api/channels", tags=["channels"])
app.include_router(videos.router, prefix="/api/videos", tags=["videos"])
app.include_router(search.router, prefix="/api/search", tags=["search"])
app.include_router(downloads.router, prefix="/api/downloads", tags=["downloads"])
app.include_router(discovery.router, prefix="/api/discovery", tags=["discovery"])
app.include_router(settings_router.router, prefix="/api/settings", tags=["settings"])
app.include_router(stats_router.router, prefix="/api/stats", tags=["stats"])
app.include_router(export_router.router, prefix="/api/export", tags=["export"])
app.include_router(collections_router.router, prefix="/api/collections", tags=["collections"])
app.include_router(admin_router.router, prefix="/api/admin", tags=["admin"])
os.makedirs(settings.download_path, exist_ok=True)
app.mount("/files", StaticFiles(directory=settings.download_path), name="files")
@app.on_event("startup")
def on_startup():
from sqlalchemy import text
init_db()
db = SessionLocal()
for col_sql in [
"ALTER TABLE user_videos ADD COLUMN liked BOOLEAN DEFAULT FALSE",
"ALTER TABLE user_videos ADD COLUMN liked_at DATETIME",
"ALTER TABLE downloads ADD COLUMN resolution TEXT",
"ALTER TABLE user_channels ADD COLUMN auto_download BOOLEAN DEFAULT NULL",
"ALTER TABLE user_channels ADD COLUMN last_seen_at DATETIME",
"ALTER TABLE discovery_queue ADD COLUMN preview_json TEXT",
"ALTER TABLE channels ADD COLUMN subscriber_count INTEGER",
"ALTER TABLE user_settings ADD COLUMN cookies_browser TEXT DEFAULT ''",
"ALTER TABLE user_settings ADD COLUMN theater_mode INTEGER DEFAULT 0",
"ALTER TABLE user_channels ADD COLUMN muted_until DATETIME DEFAULT NULL",
"ALTER TABLE user_settings ADD COLUMN calm_mode INTEGER DEFAULT 0",
"ALTER TABLE user_settings ADD COLUMN hide_subscriber_counts INTEGER DEFAULT 0",
"ALTER TABLE user_settings ADD COLUMN autoplay_enabled INTEGER DEFAULT 0",
"ALTER TABLE user_videos ADD COLUMN rating INTEGER DEFAULT NULL",
"ALTER TABLE downloads ADD COLUMN pending_delete_at DATETIME DEFAULT NULL",
"ALTER TABLE user_channels ADD COLUMN notes TEXT DEFAULT ''",
"ALTER TABLE videos ADD COLUMN chapters TEXT DEFAULT NULL",
"ALTER TABLE video_bookmarks ADD COLUMN source TEXT DEFAULT 'manual'",
"ALTER TABLE user_videos ADD COLUMN completion_percent REAL DEFAULT NULL",
"ALTER TABLE user_videos ADD COLUMN rewatch_count INTEGER DEFAULT 0",
"ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0",
"""CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)""",
"ALTER TABLE user_settings ADD COLUMN feed_weight_recency REAL DEFAULT 5.0",
"ALTER TABLE user_settings ADD COLUMN feed_weight_affinity REAL DEFAULT 5.0",
"ALTER TABLE user_settings ADD COLUMN feed_weight_channel REAL DEFAULT 5.0",
"""CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
query TEXT NOT NULL,
searched_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""",
"""CREATE TABLE IF NOT EXISTS user_tag_affinity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
score REAL DEFAULT 0.0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, tag)
)""",
"""CREATE TABLE IF NOT EXISTS collections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""",
"""CREATE TABLE IF NOT EXISTS collection_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, video_id)
)""",
"""CREATE TABLE IF NOT EXISTS video_bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
timestamp_seconds INTEGER NOT NULL,
note TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""",
]:
try:
db.execute(text(col_sql))
db.commit()
except Exception:
db.rollback()
try:
# Rebuild FTS indexes so all existing rows are searchable
db.execute(text("INSERT INTO videos_fts(videos_fts) VALUES('rebuild')"))
db.execute(text("INSERT INTO channels_fts(channels_fts) VALUES('rebuild')"))
# Migrate signed/expiring YouTube thumbnail URLs to stable format
db.execute(text("""
UPDATE videos
SET thumbnail_url = 'https://i.ytimg.com/vi/' || youtube_video_id || '/hqdefault.jpg'
WHERE thumbnail_url IS NULL
OR thumbnail_url NOT LIKE 'https://i.ytimg.com/vi/%/hqdefault.jpg'
"""))
db.commit()
# On a fresh install with no admin yet, promote the first registered user
from .models import User as UserModel, SystemConfig
has_admin = db.query(UserModel).filter_by(is_admin=True).first()
if not has_admin:
first_user = db.query(UserModel).order_by(UserModel.id).first()
if first_user:
first_user.is_admin = True
db.commit()
# Seed system_config from env if not already set
if not db.query(SystemConfig).filter_by(key="allow_registration").first():
db.add(SystemConfig(key="allow_registration", value="true"))
db.commit()
# Apply user's saved concurrent download limit on startup
from .models import UserSettings
first_user_settings = db.query(UserSettings).first()
if first_user_settings:
ytdlp_service.set_max_concurrent(first_user_settings.max_concurrent_downloads)
ytdlp_service.set_cookies_browser(first_user_settings.cookies_browser or "")
finally:
db.close()
# Backfill descriptions for videos that don't have them yet (runs in background)
import threading
from .routers.channels import _enrich_missing_task
threading.Thread(target=_enrich_missing_task, args=(10,), daemon=True).start()
@app.get("/api/health")
def health():
return {"status": "ok"}