From 0283c16a4f7c32c48e40a7b0299503adc28810e2 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 10 Dec 2023 13:35:38 +0800 Subject: [PATCH] =?UTF-8?q?add=20FFmpeg=E7=BC=A9=E7=95=A5=E5=9B=BE?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/ffmpeg.png | Bin 0 -> 10885 bytes package.json | 8 + plugins/ffmpegthumb/__init__.py | 354 +++++++++++++++++++++++++++ plugins/ffmpegthumb/ffmpeg_helper.py | 82 +++++++ plugins/libraryscraper/__init__.py | 2 +- 5 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 icons/ffmpeg.png create mode 100644 plugins/ffmpegthumb/__init__.py create mode 100644 plugins/ffmpegthumb/ffmpeg_helper.py diff --git a/icons/ffmpeg.png b/icons/ffmpeg.png new file mode 100644 index 0000000000000000000000000000000000000000..f59e98c82695ea045e33b36eaa8d7822a45057cb GIT binary patch literal 10885 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index f661210..675c020 100644 --- a/package.json +++ b/package.json @@ -318,5 +318,13 @@ "icon": "spider.png", "author": "jxxghp", "level": 1 + }, + "FFmpegThumb": { + "name": "FFmpeg缩略图", + "description": "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图", + "version": "1.0", + "icon": "ffmpeg.png", + "author": "jxxghp", + "level": 1 } } diff --git a/plugins/ffmpegthumb/__init__.py b/plugins/ffmpegthumb/__init__.py new file mode 100644 index 0000000..055a8f9 --- /dev/null +++ b/plugins/ffmpegthumb/__init__.py @@ -0,0 +1,354 @@ +from datetime import datetime, timedelta +from pathlib import Path +from threading import Event as ThreadEvent +from typing import List, Tuple, Dict, Any + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +from app.core.config import settings +from app.core.event import eventmanager, Event +from app.log import logger +from app.plugins import _PluginBase +from app.plugins.ffmpegthumb.ffmpeg_helper import FfmpegHelper +from app.schemas import TransferInfo +from app.schemas.types import EventType +from app.utils.system import SystemUtils + + +class FFmpegThumb(_PluginBase): + # 插件名称 + plugin_name = "FFmpeg缩略图" + # 插件描述 + plugin_desc = "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图。" + # 插件图标 + plugin_icon = "scan.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "ffmpegthumb_" + # 加载顺序 + plugin_order = 31 + # 可使用的用户级别 + user_level = 1 + + # 私有属性 + _scheduler = None + _enabled = False + _onlyonce = False + _cron = None + _timeline = "00:03:01" + _scan_paths = "" + _exclude_paths = "" + # 退出事件 + _event = ThreadEvent() + + def init_plugin(self, config: dict = None): + # 读取配置 + if config: + self._enabled = config.get("enabled") + self._onlyonce = config.get("onlyonce") + self._cron = config.get("cron") + self._timeline = config.get("timeline") + self._scan_paths = config.get("scan_paths") or "" + self._exclude_paths = config.get("exclude_paths") or "" + + # 停止现有任务 + self.stop_service() + + # 启动定时任务 & 立即运行一次 + if self._enabled or self._onlyonce: + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + if self._cron: + logger.info(f"FFmpeg缩略图服务启动,周期:{self._cron}") + try: + self._scheduler.add_job(func=self.__libraryscan, + trigger=CronTrigger.from_crontab(self._cron), + name="FFmpeg缩略图") + except Exception as e: + logger.error(f"FFmpeg缩略图服务启动失败,原因:{str(e)}") + self.systemmessage.put(f"FFmpeg缩略图服务启动失败,原因:{str(e)}") + if self._onlyonce: + logger.info(f"FFmpeg缩略图服务,立即运行一次") + self._scheduler.add_job(func=self.__libraryscan, trigger='date', + run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="FFmpeg缩略图") + # 关闭一次性开关 + self._onlyonce = False + self.update_config({ + "onlyonce": False, + "enabled": self._enabled, + "cron": self._cron, + "timeline": self._timeline, + "scan_paths": self._scan_paths, + "exclude_paths": self._exclude_paths + }) + if self._scheduler.get_jobs(): + # 启动服务 + self._scheduler.print_jobs() + self._scheduler.start() + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'timeline', + 'label': '截取时间', + 'placeholder': '00:03:01' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '定时扫描周期', + 'placeholder': '5位cron表达式,留空关闭' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'scan_paths', + 'label': '定时扫描路径', + 'rows': 5, + 'placeholder': '每一行一个目录' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'exclude_paths', + 'label': '定时扫描排除路径', + 'rows': 2, + 'placeholder': '每一行一个目录' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '开启插件后默认会实时处理增量整理的媒体文件,需要处理存量媒体文件时才需开启定时;需要提前安装FFmpeg:https://www.ffmpeg.org' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "cron": "", + "timeline": "00:03:01", + "scan_paths": "", + "err_hosts": "" + } + + def get_page(self) -> List[dict]: + pass + + @eventmanager.register(EventType.TransferComplete) + def scan_rt(self, event: Event): + """ + 根据事件实时扫描缩略图 + """ + if not self._enabled: + return + # 事件数据 + transferinfo: TransferInfo = event.event_data.get("transferinfo") + if not transferinfo: + return + file_list = transferinfo.file_list_new + for file in file_list: + logger.info(f"FFmpeg缩略图处理文件:{file}") + file_path = Path(file) + if not file_path.exists(): + logger.warn(f"{file_path} 不存在") + continue + if file_path.suffix not in settings.RMT_MEDIAEXT: + logger.warn(f"{file_path} 不是支持的视频文件") + continue + self.gen_file_thumb(file_path) + + def __libraryscan(self): + """ + 开始扫描媒体库 + """ + if not self._scan_paths: + return + # 排除目录 + exclude_paths = self._exclude_paths.split("\n") + # 已选择的目录 + paths = self._scan_paths.split("\n") + for path in paths: + if not path: + continue + scan_path = Path(path) + if not scan_path.exists(): + logger.warning(f"FFmpeg缩略图扫描路径不存在:{path}") + continue + logger.info(f"开始FFmpeg缩略图扫描:{path} ...") + # 遍历目录下的所有文件 + for file_path in SystemUtils.list_files(scan_path, extensions=settings.RMT_MEDIAEXT): + if self._event.is_set(): + logger.info(f"FFmpeg缩略图扫描服务停止") + return + # 排除目录 + exclude_flag = False + for exclude_path in exclude_paths: + try: + if file_path.is_relative_to(Path(exclude_path)): + exclude_flag = True + break + except Exception as err: + print(str(err)) + if exclude_flag: + logger.debug(f"{file_path} 在排除目录中,跳过 ...") + continue + # 开始处理文件 + self.gen_file_thumb(file_path) + logger.info(f"目录 {path} 扫描完成") + + def gen_file_thumb(self, file_path: Path): + """ + 处理一个文件 + """ + try: + thumb_path = file_path.with_name(file_path.stem + "-thumb.jpg") + if FfmpegHelper.get_thumb(video_path=str(file_path), + image_path=str(thumb_path), frames=self._timeline): + logger.info(f"{file_path} 缩略图已生成:{thumb_path}") + else: + logger.warn(f"{file_path} 缩略图生成失败!") + except Exception as err: + logger.error(f"FFmpeg处理文件 {file_path} 时发生错误:{str(err)}") + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._event.set() + self._scheduler.shutdown() + self._event.clear() + self._scheduler = None + except Exception as e: + print(str(e)) diff --git a/plugins/ffmpegthumb/ffmpeg_helper.py b/plugins/ffmpegthumb/ffmpeg_helper.py new file mode 100644 index 0000000..d4ee67c --- /dev/null +++ b/plugins/ffmpegthumb/ffmpeg_helper.py @@ -0,0 +1,82 @@ +import json +import subprocess + +from app.utils.system import SystemUtils + + +class FfmpegHelper: + + @staticmethod + def get_thumb(video_path: str, image_path: str, frames: str = None): + """ + 使用ffmpeg从视频文件中截取缩略图 + """ + if not frames: + frames = "00:03:01" + if not video_path or not image_path: + return False + cmd = 'ffmpeg -i "{video_path}" -ss {frames} -vframes 1 -f image2 "{image_path}"'.format(video_path=video_path, + frames=frames, + image_path=image_path) + result = SystemUtils.execute(cmd) + if result: + return True + return False + + @staticmethod + def extract_wav(video_path: str, audio_path: str, audio_index: str = None): + """ + 使用ffmpeg从视频文件中提取16000hz, 16-bit的wav格式音频 + """ + if not video_path or not audio_path: + return False + + # 提取指定音频流 + if audio_index: + command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, + '-map', f'0:a:{audio_index}', + '-acodec', 'pcm_s16le', '-ac', '1', '-ar', '16000', audio_path] + else: + command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, + '-acodec', 'pcm_s16le', '-ac', '1', '-ar', '16000', audio_path] + + ret = subprocess.run(command).returncode + if ret == 0: + return True + return False + + @staticmethod + def get_metadata(video_path: str): + """ + 获取视频元数据 + """ + if not video_path: + return False + + try: + command = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', video_path] + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if result.returncode == 0: + return json.loads(result.stdout.decode("utf-8")) + except Exception as e: + print(e) + return None + + @staticmethod + def extract_subtitle(video_path: str, subtitle_path: str, subtitle_index: str = None): + """ + 从视频中提取字幕 + """ + if not video_path or not subtitle_path: + return False + + if subtitle_index: + command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, + '-map', f'0:s:{subtitle_index}', + subtitle_path] + else: + command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, subtitle_path] + ret = subprocess.run(command).returncode + if ret == 0: + return True + return False diff --git a/plugins/libraryscraper/__init__.py b/plugins/libraryscraper/__init__.py index 3534a04..85938d8 100644 --- a/plugins/libraryscraper/__init__.py +++ b/plugins/libraryscraper/__init__.py @@ -257,7 +257,7 @@ class LibraryScraper(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '刮削路径要配置到二级分类路径。(如果配置了LIBRARY_CATEGORY=true)' + 'text': '刮削路径要配置到二级分类路径(如果配置了LIBRARY_CATEGORY=true);开启插件后默认会实时处理增量整理的媒体文件,需要处理存量媒体文件时才需开启定时。' } } ]