tubewatch/playlist-monitor/app/models/video.py

130 lines
4.9 KiB
Python

"""
Video record model for tracking individual videos in playlists
"""
import uuid
from datetime import datetime
from enum import Enum
from typing import Optional
from sqlalchemy import Column, String, Integer, DateTime, Boolean, Text, ForeignKey, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..core.database import Base
class VideoStatus(str, Enum):
"""Video download status enumeration"""
PENDING = "PENDING" # Not yet downloaded
DOWNLOADING = "DOWNLOADING" # Currently being downloaded
COMPLETED = "COMPLETED" # Successfully downloaded
FAILED = "FAILED" # Download failed
SKIPPED = "SKIPPED" # Before start_point or manually skipped
class VideoRecord(Base):
"""Video record model for tracking individual videos"""
__tablename__ = "videos"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
playlist_id = Column(String, ForeignKey("playlists.id", ondelete="CASCADE"), nullable=False)
# Video metadata
video_url = Column(String, nullable=False)
video_id = Column(String, nullable=False) # YouTube video ID
title = Column(String, nullable=True)
playlist_index = Column(Integer, nullable=True) # Position in playlist
upload_date = Column(DateTime, nullable=True)
# Download tracking
status = Column(String, default=VideoStatus.PENDING, nullable=False)
download_requested_at = Column(DateTime, nullable=True)
download_completed_at = Column(DateTime, nullable=True)
metube_download_id = Column(String, nullable=True) # Reference to MeTube download
# File tracking (decoupled from actual file)
original_filename = Column(String, nullable=True) # Filename when downloaded
file_moved = Column(Boolean, default=False, nullable=False) # Whether user moved the file
file_location_note = Column(Text, nullable=True) # Optional note about file location
# Error handling
error_message = Column(Text, nullable=True)
retry_count = Column(Integer, default=0, nullable=False)
last_error_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
# Relationships
playlist = relationship("PlaylistSubscription", back_populates="videos")
def __repr__(self):
return f"<VideoRecord(id='{self.id}', title='{self.title}', status='{self.status}')>"
@property
def is_downloadable(self) -> bool:
"""Check if video can be downloaded"""
return self.status in [VideoStatus.PENDING, VideoStatus.FAILED]
@property
def is_completed(self) -> bool:
"""Check if video download is completed"""
return self.status == VideoStatus.COMPLETED
def can_retry(self) -> bool:
"""Check if video can be retried"""
if self.status not in [VideoStatus.FAILED]:
return False
# Limit retry attempts
return self.retry_count < 3
def mark_as_downloading(self, metube_download_id: str) -> None:
"""Mark video as downloading"""
self.status = VideoStatus.DOWNLOADING
self.download_requested_at = datetime.utcnow()
self.metube_download_id = metube_download_id
self.error_message = None
self.last_error_at = None
def mark_as_completed(self, filename: Optional[str] = None) -> None:
"""Mark video as completed"""
self.status = VideoStatus.COMPLETED
self.download_completed_at = datetime.utcnow()
if filename:
self.original_filename = filename
self.error_message = None
self.retry_count = 0
def mark_as_failed(self, error_message: str) -> None:
"""Mark video as failed"""
self.status = VideoStatus.FAILED
self.error_message = error_message
self.last_error_at = datetime.utcnow()
self.retry_count += 1
def mark_as_skipped(self) -> None:
"""Mark video as skipped"""
self.status = VideoStatus.SKIPPED
self.error_message = None
def reset_to_pending(self) -> None:
"""Reset video to pending status"""
self.status = VideoStatus.PENDING
self.download_requested_at = None
self.download_completed_at = None
self.metube_download_id = None
self.error_message = None
self.last_error_at = None
self.retry_count = 0
# Create indexes for better query performance
Index("idx_videos_playlist_id", VideoRecord.playlist_id)
Index("idx_videos_status", VideoRecord.status)
Index("idx_videos_video_id", VideoRecord.video_id)
Index("idx_videos_playlist_index", VideoRecord.playlist_id, VideoRecord.playlist_index)
Index("idx_videos_metube_download_id", VideoRecord.metube_download_id)