import json import pathlib from typing import Optional, Dict, List from sqlalchemy import Column, Integer, String, Boolean, Date, ForeignKey, BigInteger, Index, text, JSON from sqlalchemy.orm import relationship, Session, deferred, undefer, joinedload, selectinload from sqlalchemy.orm.collections import InstrumentedList from modules.videos.errors import UnknownVideo, UnknownChannel from wrolpi.captions import read_captions, read_captions_with_timestamps from wrolpi.common import 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.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: FileGroup = relationship('FileGroup') def __repr__(self): v = None if self.video_path: v = repr(str(self.video_path.relative_to(get_media_directory()))) return f'