Files
youclonedl/backend/models.py
Mattias Tall 74e2b4cd73 Fix comments (Python API), add dislike bar
Comments: switch from CLI --write-comments to yt-dlp Python API with
getcomments=True — more reliable, proper extractor_args dict format

Dislikes: add dislike_count column, fetch from returnyoutubedislike.com
after each video metadata upsert (5s timeout, non-fatal)

UI: replace emoji like count with a like/dislike ratio bar — blue fill
showing like proportion, labels on each end; views stay in meta row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:33:00 +02:00

241 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
like_count = Column(Integer)
dislike_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) # 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
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) # 010
feed_weight_affinity = Column(Float, default=5.0) # 010
feed_weight_channel = Column(Float, default=5.0) # 010
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)