import base64 import re import shutil import time from datetime import datetime from pathlib import Path import enum import pandas as pd from shortGPT.audio.audio_utils import downloadYoutubeAudio, get_asset_duration from shortGPT.database.db_document import TinyMongoDocument AUDIO_EXTENSIONS = {".mp3", ".m4a", ".wav", ".flac", ".aac", ".ogg", ".wma", ".opus"} IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp"} VIDEO_EXTENSIONS = {".mp4", ".mkv", ".flv", ".avi", ".mov", ".wmv", ".webm", ".m4v"} TEMPLATE_ASSETS_DB_PATH = '.database/template_asset_db.json' ASSETS_DB_PATH = '.database/asset_db.json' class AssetType(enum.Enum): VIDEO = "video" AUDIO = "audio" IMAGE = "image" BACKGROUND_MUSIC = "background music" BACKGROUND_VIDEO = "background video" OTHER = "other" class AssetDatabase: """ Class for managing assets, both local and remote. The class provides methods to add, remove, get and sync assets. It uses a MongoDB-like database to store information about the assets. """ if not Path(ASSETS_DB_PATH).exists(): shutil.copy(TEMPLATE_ASSETS_DB_PATH, ASSETS_DB_PATH) local_assets = TinyMongoDocument("asset_db", "asset_collection", "local_assets", create=True) remote_assets = TinyMongoDocument("asset_db", "asset_collection", "remote_assets", create=True) if not remote_assets._get('subscribe animation'): remote_assets._save({ 'subscribe animation':{ "type": AssetType.VIDEO.value, "url": "https://www.youtube.com/watch?v=72WhUT0OM98", "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } }) @classmethod def asset_exists(cls, name: str) -> bool: return name in cls.local_assets._get() or name in cls.remote_assets._get() @classmethod def add_local_asset(cls, name: str, asset_type: AssetType, path: str): cls.local_assets._save({ name: { "type": asset_type.value, "path": path, "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } }) @classmethod def add_remote_asset(cls, name: str, asset_type: AssetType, url: str): cls.remote_assets._save({ name: { "type": asset_type.value, "url": url, "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } }) @classmethod def remove_asset(cls, name: str): if name in cls.local_assets._get(): cls._remove_local_asset(name) elif name in cls.remote_assets._get(): cls.remote_assets._delete(name) else: raise ValueError(f"Asset '{name}' does not exist in the database.") @classmethod def get_df(cls, source=None) -> pd.DataFrame: data = [] if source is None or source == 'local': for key, asset in cls.local_assets._get().items(): data.append({'name': key, 'type': asset['type'], 'link': asset['path'], 'source': 'local', 'ts': asset.get('ts') }) if source is None or source == 'youtube': for key, asset in cls.remote_assets._get().items(): data.append({'name': key, 'type': asset['type'], 'link': asset['url'], 'source': 'youtube' if 'youtube' in asset['url'] else 'internet', 'ts': asset.get('ts') }) df = pd.DataFrame(data) if (not df.empty): df.sort_values(by='ts', ascending=False, inplace=True) return df.drop(columns='ts') return df @classmethod def sync_local_assets(cls): """ Loads all local assets from the static-assets folder into the database. """ local_assets = cls.local_assets._get() local_paths = {asset['path'] for asset in local_assets.values()} for path in Path('public').rglob('*'): if path.is_file() and str(path) not in local_paths: cls._add_local_asset_from_path(path) @classmethod def get_asset_link(cls, key: str) -> str: """ Get the link to an asset. Args: key (str): Name of the asset. Returns: str: Link to the asset. """ if key in cls.local_assets._get(): return cls._update_local_asset_timestamp_and_get_link(key) elif key in cls.remote_assets._get(): return cls._get_remote_asset_link(key) else: raise ValueError(f"Asset '{key}' does not exist in the database.") @classmethod def get_asset_duration(cls, key: str) -> str: """ Get the duration of an asset. Args: key (str): Name of the asset. Returns: str: Duration of the asset. """ if key in cls.local_assets._get(): return cls._get_local_asset_duration(key) elif key in cls.remote_assets._get(): return cls._get_remote_asset_duration(key) else: raise ValueError(f"Asset '{key}' does not exist in the database.") @classmethod def _remove_local_asset(cls, name: str): """ Remove a local asset from the database. Args: name (str): Name of the asset. """ asset = cls.local_assets._get(name) if 'required' not in asset: try: Path(asset['path']).unlink() except FileNotFoundError as e: print(f"File not found: {e}") cls.local_assets._delete(name) @classmethod def _add_local_asset_from_path(cls, path: Path): """ Add a local asset to the database from a file path. Args: path (Path): Path to the asset. """ file_ext = path.suffix if file_ext in AUDIO_EXTENSIONS: asset_type = AssetType.AUDIO elif file_ext in IMAGE_EXTENSIONS: asset_type = AssetType.IMAGE elif file_ext in VIDEO_EXTENSIONS: asset_type = AssetType.VIDEO else: asset_type = AssetType.OTHER cls.local_assets._save({ path.stem: { "path": str(path), "type": asset_type.value, "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } }) @classmethod def _update_local_asset_timestamp_and_get_link(cls, key: str) -> str: """ Update the timestamp of a local asset and get its link. Args: key (str): Name of the asset. Returns: str: Link to the asset. """ asset = cls.local_assets._get(key) asset['ts'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cls.local_assets._save({key: asset}) return asset['path'] @classmethod def _get_remote_asset_link(cls, key: str) -> str: """ Get the link to a remote asset. Args: key (str): Name of the asset. Returns: str: Link to the asset. """ asset = cls.remote_assets._get(key) asset['ts'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cls.remote_assets._save({key: asset}) if 'youtube' in asset['url']: return cls._get_youtube_asset_link(key, asset) return asset['url'] @classmethod def _get_local_asset_duration(cls, key: str) -> str: """ Get the duration of a local asset. Args: key (str): Name of the asset. Returns: str: Duration of the asset. """ asset = cls.local_assets._get(key) asset['ts'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cls.local_assets._save({key: asset}) if 'duration' not in asset: _, duration = cls._update_local_asset_duration(key) return duration return asset['duration'] @classmethod def _get_remote_asset_duration(cls, key: str) -> str: """ Get the duration of a remote asset. Args: key (str): Name of the asset. Returns: str: Duration of the asset. """ asset = cls.remote_assets._get(key) asset['ts'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cls.remote_assets._save({key: asset}) if 'duration' in asset: return asset['duration'] _, duration = cls._update_youtube_asset_duration(key) return duration @classmethod def _update_local_asset_duration(cls, key: str) -> str: """ Update the duration of a local asset. Args: key (str): Name of the asset. Returns: str: Duration of the asset. """ asset = cls.local_assets._get(key) path = Path(asset['path']) if any(t in asset['type'] for t in ['audio', 'video', 'music']): _, duration = get_asset_duration(str(path)) asset['duration'] = duration else: duration = None cls.local_assets._save({key: asset}) return str(path), duration @classmethod def _update_youtube_asset_duration(cls, key: str) -> str: """ Update the duration of a Youtube asset. Args: key (str): Name of the asset. Returns: str: Duration of the asset. """ asset = cls.remote_assets._get(key) youtube_url = asset['url'] remote_url, duration = get_asset_duration(youtube_url, isVideo="video" in asset['type']) asset.update({ "remote_url": base64.b64encode(remote_url.encode()).decode('utf-8'), "duration": duration, }) cls.remote_assets._save({key: asset}) return remote_url, duration @classmethod def _get_youtube_asset_link(cls, key: str, asset: dict) -> str: """ Get the link to a Youtube asset. Args: key (str): Name of the asset. asset (dict): Asset data. Returns: str: Link to the asset. """ if any(t in asset['type'] for t in ['audio', 'music']): local_audio_file, duration = downloadYoutubeAudio(asset['url'], f"public/{key}.wav") cls.local_assets._save({ key: { 'path': local_audio_file, 'duration': duration, 'type': 'audio', 'ts': datetime.now().strftime("%Y-%m-%d %H:%M:%S") } }) return local_audio_file if 'remote_url' in asset: asset['remote_url'] = base64.b64decode(asset['remote_url']).decode('utf-8') expire_timestamp_match = re.search(r"expire=(\d+)", asset['remote_url']) not_expired = expire_timestamp_match and int(expire_timestamp_match.group(1)) > time.time() + 1800 if not_expired and 'duration' in asset: return asset['remote_url'] remote_url, _ = cls._update_youtube_asset_duration(key) return remote_url