Initial commit — YT Hub

Self-hosted personal YouTube management app.
FastAPI + SQLite backend, React + Vite + Tailwind frontend.
Dockerfiles and compose included for Portainer deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
inputnoise
2026-05-25 20:09:04 +02:00
commit 1827dd6c4e
63 changed files with 14480 additions and 0 deletions

221
backend/models.py Normal file
View File

@@ -0,0 +1,221 @@
from datetime import datetime
from sqlalchemy import (
Boolean, Column, DateTime, Float, ForeignKey,
Integer, String, Text, UniqueConstraint,
)
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, nullable=False, index=True)
email = Column(String, unique=True, nullable=False, index=True)
hashed_password = Column(String, nullable=False)
is_admin = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
class Channel(Base):
__tablename__ = "channels"
id = Column(Integer, primary_key=True, index=True)
youtube_channel_id = Column(String, unique=True, nullable=False, index=True)
name = Column(String, nullable=False)
description = Column(Text)
thumbnail_url = Column(String)
banner_url = Column(String)
crawled_at = Column(DateTime)
subscriber_count = Column(Integer)
class UserChannel(Base):
__tablename__ = "user_channels"
__table_args__ = (UniqueConstraint("user_id", "channel_id"),)
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
channel_id = Column(Integer, ForeignKey("channels.id", ondelete="CASCADE"), nullable=False)
status = Column(String, default="followed") # followed / dismissed / pending
added_at = Column(DateTime, default=datetime.utcnow)
auto_download = Column(Boolean, default=None) # None = use global, True/False = override
last_seen_at = Column(DateTime, default=None)
muted_until = Column(DateTime, default=None)
notes = Column(Text, default="")
class Video(Base):
__tablename__ = "videos"
id = Column(Integer, primary_key=True, index=True)
youtube_video_id = Column(String, unique=True, nullable=False, index=True)
channel_id = Column(Integer, ForeignKey("channels.id", ondelete="SET NULL"), nullable=True)
title = Column(String, nullable=False)
description = Column(Text)
thumbnail_url = Column(String)
duration_seconds = Column(Integer)
published_at = Column(DateTime)
indexed_at = Column(DateTime, default=datetime.utcnow)
tags = Column(Text) # JSON array string
category = Column(String)
chapters = Column(Text) # JSON array of {start_time, end_time, title}
class UserVideo(Base):
__tablename__ = "user_videos"
__table_args__ = (UniqueConstraint("user_id", "video_id"),)
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
video_id = Column(Integer, ForeignKey("videos.id", ondelete="CASCADE"), nullable=False)
watched = Column(Boolean, default=False)
watch_progress_seconds = Column(Integer, default=0)
completion_percent = Column(Float, default=None) # 0100, set when video ends/navigates away
rewatch_count = Column(Integer, default=0) # incremented each time a completed video is replayed
queued = Column(Boolean, default=False)
downloaded = Column(Boolean, default=False)
liked = Column(Boolean, default=False)
rating = Column(Integer, default=None) # NULL=unrated, 1=thumbs up, -1=thumbs down
downloaded_at = Column(DateTime)
liked_at = Column(DateTime)
last_watched_at = Column(DateTime)
class Download(Base):
__tablename__ = "downloads"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
video_id = Column(Integer, ForeignKey("videos.id", ondelete="CASCADE"), nullable=False)
status = Column(String, default="pending") # pending / downloading / complete / failed
progress_percent = Column(Float, default=0.0)
file_path = Column(String)
resolution = Column(String) # e.g. "1080p", "720p"
created_at = Column(DateTime, default=datetime.utcnow)
completed_at = Column(DateTime)
error_message = Column(Text)
pending_delete_at = Column(DateTime, default=None)
class UserSettings(Base):
__tablename__ = "user_settings"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True)
preferred_quality = Column(String, default="best") # best / 1080p / 720p / 480p / 360p
max_concurrent_downloads = Column(Integer, default=3) # 15
hide_watched_from_feed = Column(Boolean, default=False)
mark_watched_at_percent = Column(Integer, default=90) # 50100
auto_download_on_sync = Column(Boolean, default=False)
cookies_browser = Column(String, default="") # chrome / firefox / etc., "" = disabled
theater_mode = Column(Boolean, default=False)
discovery_regions = Column(String, default="US,SE") # comma-separated ISO country codes
calm_mode = Column(Boolean, default=False)
hide_subscriber_counts = Column(Boolean, default=False)
autoplay_enabled = Column(Boolean, default=False)
feed_weight_recency = Column(Float, default=5.0) # 010
feed_weight_affinity = Column(Float, default=5.0) # 010
feed_weight_channel = Column(Float, default=5.0) # 010
class DiscoveryQueue(Base):
__tablename__ = "discovery_queue"
__table_args__ = (UniqueConstraint("user_id", "channel_id"),)
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
channel_id = Column(Integer, ForeignKey("channels.id", ondelete="CASCADE"), nullable=False)
score = Column(Float, default=0.0)
source = Column(String) # search / community / category / liked
seen = Column(Boolean, default=False)
preview_json = Column(Text) # JSON: [{thumbnail_url, title}, ...]
created_at = Column(DateTime, default=datetime.utcnow)
class ChannelGroup(Base):
__tablename__ = "channel_groups"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
name = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
class ChannelGroupMember(Base):
__tablename__ = "channel_group_members"
__table_args__ = (UniqueConstraint("group_id", "channel_id"),)
id = Column(Integer, primary_key=True, index=True)
group_id = Column(Integer, ForeignKey("channel_groups.id", ondelete="CASCADE"), nullable=False)
channel_id = Column(Integer, ForeignKey("channels.id", ondelete="CASCADE"), nullable=False)
class UserTagAffinity(Base):
"""Per-user taste signal: how much the user engages with a given tag or category."""
__tablename__ = "user_tag_affinity"
__table_args__ = (UniqueConstraint("user_id", "tag"),)
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
tag = Column(String, nullable=False, index=True)
score = Column(Float, default=0.0)
updated_at = Column(DateTime, default=datetime.utcnow)
class Collection(Base):
__tablename__ = "collections"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
name = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
class CollectionItem(Base):
__tablename__ = "collection_items"
__table_args__ = (UniqueConstraint("collection_id", "video_id"),)
id = Column(Integer, primary_key=True, index=True)
collection_id = Column(Integer, ForeignKey("collections.id", ondelete="CASCADE"), nullable=False)
video_id = Column(Integer, ForeignKey("videos.id", ondelete="CASCADE"), nullable=False)
added_at = Column(DateTime, default=datetime.utcnow)
class GraphEdge(Base):
__tablename__ = "graph_edges"
__table_args__ = (UniqueConstraint("from_channel_id", "to_channel_id"),)
id = Column(Integer, primary_key=True, index=True)
from_channel_id = Column(Integer, ForeignKey("channels.id", ondelete="CASCADE"), nullable=False)
to_channel_id = Column(Integer, ForeignKey("channels.id", ondelete="CASCADE"), nullable=False)
mention_count = Column(Integer, default=1)
last_seen = Column(DateTime, default=datetime.utcnow)
class SystemConfig(Base):
__tablename__ = "system_config"
key = Column(String, primary_key=True)
value = Column(String, nullable=False)
class SearchHistory(Base):
__tablename__ = "search_history"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
query = Column(String, nullable=False)
searched_at = Column(DateTime, default=datetime.utcnow)
class VideoBookmark(Base):
__tablename__ = "video_bookmarks"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
video_id = Column(Integer, ForeignKey("videos.id", ondelete="CASCADE"), nullable=False)
timestamp_seconds = Column(Integer, nullable=False)
note = Column(Text, default="")
source = Column(String, default="manual") # manual | auto
created_at = Column(DateTime, default=datetime.utcnow)