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:
221
backend/models.py
Normal file
221
backend/models.py
Normal 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) # 0–100, 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) # 1–5
|
||||
hide_watched_from_feed = Column(Boolean, default=False)
|
||||
mark_watched_at_percent = Column(Integer, default=90) # 50–100
|
||||
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) # 0–10
|
||||
feed_weight_affinity = Column(Float, default=5.0) # 0–10
|
||||
feed_weight_channel = Column(Float, default=5.0) # 0–10
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user