130 lines
4.9 KiB
Python
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) |