import json import pathlib from datetime import datetime, timezone from typing import Optional, Dict, List, Tuple from sqlalchemy import Column, Integer, String, Boolean, Date, ForeignKey, BigInteger, Index, text, JSON from sqlalchemy.orm import Mapped, relationship, Session, deferred, undefer, joinedload, selectinload from modules.videos.errors import UnknownVideo, UnknownChannel from wrolpi.captions import read_captions, read_captions_with_timestamps from wrolpi.common import loaded, Base, ModelHelper, logger, get_media_directory, get_relative_to_media_directory, \ background_task from wrolpi.db import get_db_curs, get_db_session, session_curs, serializer from wrolpi.collections.models import Collection from wrolpi.downloader import Download from wrolpi.files.lib import split_path_stem_and_suffix from wrolpi.files.worker import file_worker from wrolpi.files.models import FileGroup from wrolpi.files.progress import save_recently_viewed_config from wrolpi.tags import Tag, TagFile from wrolpi.vars import PYTEST, VIDEO_INFO_JSON_KEYS_TO_CLEAN logger = logger.getChild(__name__) __all__ = ['Video', 'Channel', 'AUDIO_PLAYLIST_MIMETYPES'] # Playlists that detect as audio/* but are text lists of URLs, not media. Detection varies by # libmagic version (.pls is text/plain on some, audio/x-scpls on others); the denylist covers # whatever comes back audio/*. AUDIO_PLAYLIST_MIMETYPES = ('audio/x-mpegurl', 'audio/mpegurl', 'audio/x-scpls', 'audio/scpls') class Video(ModelHelper, Base): __tablename__ = 'video' __table_args__ = ( Index('video_channel_id_idx', 'channel_id'), Index('video_source_id_idx', 'source_id'), Index('video_view_count_idx', 'view_count'), # Do not reuse ids of deleted Videos (AUTOINCREMENT keeps a sequence table). {'sqlite_autoincrement': True}, ) id = Column(Integer, primary_key=True) source_id = Column(String) # The id from yt-dlp view_count = Column(BigInteger) # The view count from the ChannelDownloader (or from initial download) ffprobe_json = deferred(Column(JSON)) # Data that is fetched once from ffprobe (ffmpeg) have_comments = Column(Boolean, default=False) # see `get_missing_videos_comments` comments_failed = Column(Boolean, default=False) # see `get_missing_videos_comments` channel_id = Column(Integer, ForeignKey('channel.id')) channel = relationship('Channel', primaryjoin='Video.channel_id==Channel.id', back_populates='videos') file_group_id = Column(BigInteger, ForeignKey('file_group.id', ondelete='CASCADE'), unique=True, nullable=False) file_group: Mapped[FileGroup] = relationship('FileGroup') def __repr__(self): title = loaded(self, 'file_group', 'title') path = repr(str(loaded(self, 'file_group', 'primary_path'))) return f'