- Video model: view_count column (Integer, nullable) - ytdlp._normalize_video: extract view_count from yt-dlp info - _VIDEO_SELECT: include v.view_count in all queries - VideoDetail schema: view_count field - Watch page: formatViews() helper, show "X.XM views" in meta row alongside date and category Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
239 lines
9.9 KiB
Python
239 lines
9.9 KiB
Python
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}
|
||
view_count = Column(Integer)
|
||
|
||
|
||
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
|
||
cookies_file = Column(String, default="") # path to Netscape cookies.txt, "" = 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
|
||
use_oauth2 = Column(Boolean, default=False)
|
||
|
||
|
||
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 VideoComment(Base):
|
||
__tablename__ = "video_comments"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
video_id = Column(Integer, ForeignKey("videos.id", ondelete="CASCADE"), nullable=False, index=True)
|
||
youtube_comment_id = Column(String)
|
||
author = Column(String)
|
||
text = Column(Text)
|
||
likes = Column(Integer, default=0)
|
||
is_pinned = Column(Boolean, default=False)
|
||
published_at = Column(DateTime)
|
||
fetched_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)
|