import datetime
import json
import os
import re
import shutil
import subprocess
import threading
import time
import traceback
import urllib
from pathlib import Path
from typing import List, Tuple, Dict, Any, Optional

import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver
from sqlalchemy.orm import Session

from app import schemas
from app.chain.tmdb import TmdbChain
from app.chain.transfer import TransferChain
from app.core.config import settings
from app.core.event import eventmanager, Event
from app.db import db_query, db_update
from app.db.models import TransferHistory, DownloadHistory, DownloadFiles
from app.log import logger
from app.modules.emby import Emby
from app.plugins import _PluginBase
from app.schemas.types import EventType, SystemConfigKey, MediaType, NotificationType, MediaImageType
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
from app.utils.system import SystemUtils

lock = threading.Lock()


class CloudFileMonitorHandler(FileSystemEventHandler):
    """
    目录监控响应类
    """

    def __init__(self, monpath: str, sync: Any, **kwargs):
        super(CloudFileMonitorHandler, self).__init__(**kwargs)
        self._watch_path = monpath
        self.sync = sync

    def on_created(self, event):
        self.sync.event_handler(event=event, text="创建",
                                mon_path=self._watch_path, event_path=event.src_path)

    def on_moved(self, event):
        self.sync.event_handler(event=event, text="移动",
                                mon_path=self._watch_path, event_path=event.dest_path)


class CloudAssistant(_PluginBase):
    # 插件名称
    plugin_name = "云盘助手"
    # 插件描述
    plugin_desc = "本地文件定时转移到云盘，软连接/strm回本地，定时清理无效软连接。"
    # 插件图标
    plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/cloudassistant.png"
    # 插件版本
    plugin_version = "2.1.5"
    # 插件作者
    plugin_author = "thsrite"
    # 作者主页
    author_url = "https://github.com/thsrite"
    # 插件配置项ID前缀
    plugin_config_prefix = "cloudassistant_"
    # 加载顺序
    plugin_order = 4
    # 可使用的用户级别
    auth_level = 99
    plugin_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlgN7RtXlPNXoFE9B67yey2mog/hDDrhBAIJogdvfAgBMZ1qVzIPcBfdjENPJ9kV/F+zOoh0CzEaDufM54ERTykK1pQw7yj7quRZDbv5byxVNqI8bJg8zQo8Q66SQ8SP+aftmpFrADKClQ8VcVYzZJ+YDu9H9q+TcvBqVtLyKfAH5T9WAxn0bXEh4OgkJn7oO5eI5+Fsi6Aq9suVN/HyKz2bDr237GmXJT4YPn9s7kj4Rypzg2ldiuBwtVnaTw+xjZRlCRr4Gs0eFUIMUqnoQip4Px8Mrq5cqHl0HrJ/av/pJLCN1icCgegYW63b2gjjJwmps9NGGOydRzgoFkqj0DwIDAQAB"

    # 私有属性
    _scheduler = None
    transferhis = None
    downloadhis = None
    transferchian = None
    tmdbchain = None
    _observer = []
    _enabled = False
    _notify = False
    _onlyonce = False
    _monitor = False
    _invalid = False
    _only_media_history = False
    _refresh = False
    _cron = None
    _invalid_cron = None
    _clean = False
    _exclude_keywords = ""
    _interval: int = 60
    _dir_confs = {}
    _dirconf = {}
    _medias = {}
    _transfer_type = None
    _rmt_mediaext = ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v"

    # 退出事件
    _event = threading.Event()

    example = {
        "transfer_type": "copy/move",
        "return_mode": "softlink",
        "monitor_dirs": [
            {
                "retention_time": 0,
                "monitor_mode": "模式 compatibility/fast",
                "dest_path": "/mnt/media/movies",
                "mount_path": "/mnt/cloud/115/media/movies",
                "return_path": "/mnt/softlink/movies",
                "delete_dest": "false",
                "dest_preserve_hierarchy": 0,
                "delete_history": "false",
                "delete_src": "false",
                "src_paths": "/mnt/media/movies, /mnt/media/series",
                "src_preserve_hierarchy": 0,
                "only_media": "true",
                "overwrite": "false",
                "upload_cloud": "true",
                "notify_url": ""
            }
        ]
    }
    # _client = None
    # _fs = None
    _return_mode = None
    _EMBY_HOST = settings.EMBY_HOST
    _EMBY_APIKEY = settings.EMBY_API_KEY

    def init_plugin(self, config: dict = None):
        self.transferchian = TransferChain()
        self.tmdbchain = TmdbChain()
        # 清空配置
        self._dirconf = {}

        # 读取配置
        if config:
            self._enabled = config.get("enabled")
            self._notify = config.get("notify")
            self._onlyonce = config.get("onlyonce")
            self._monitor = config.get("monitor") or False
            self._invalid = config.get("invalid")
            self._clean = config.get("clean")
            self._exclude_keywords = config.get("exclude_keywords") or ""
            self._interval = config.get("interval") or 60
            self._refresh = config.get("refresh")
            self._only_media_history = config.get("only_media_history")
            self._cron = config.get("cron")
            self._invalid_cron = config.get("invalid_cron")
            self._dir_confs = config.get("dir_confs") or None
            self._rmt_mediaext = config.get(
                "rmt_mediaext") or ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v"

            if self._EMBY_HOST:
                if not self._EMBY_HOST.endswith("/"):
                    self._EMBY_HOST += "/"
                if not self._EMBY_HOST.startswith("http"):
                    self._EMBY_HOST = "http://" + self._EMBY_HOST

            # 清理插件历史
            if self._clean:
                self.del_data(key="history")
                self._clean = False
                self.__update_config()

            if not self._dir_confs:
                return

            # 停止现有任务
            self.stop_service()

            # 定时服务管理器
            self._scheduler = BackgroundScheduler(timezone=settings.TZ)

            if self._invalid:
                logger.info("清理无效软连接服务启动，立即运行一次")
                # 关闭无效软连接开关
                self._invalid = False
                # 保存配置
                self.__update_config()

                self._scheduler.add_job(func=self.handle_invalid_links, trigger='date',
                                        run_date=datetime.datetime.now(
                                            tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3)
                                        )

            if self._enabled or self._onlyonce:
                if self._notify and (self._cron or self._monitor):
                    # 追加入库消息统一发送服务
                    self._scheduler.add_job(self.send_msg, trigger='interval', seconds=15)

                dir_confs = json.loads(self._dir_confs)
                # 检查cd2配置
                # if not dir_confs.get("cd2_url") or not dir_confs.get("username") or not dir_confs.get("password"):
                #     if not dir_confs.get("transfer_type"):
                #         logger.error("未正确配置CloudDrive2或者transfer_type，请检查配置")
                #         return
                #     else:
                #         self._transfer_type = dir_confs.get("transfer_type")
                #         logger.warn("未配置CloudDrive2，使用transfer_type转移模式")
                # else:
                #     try:
                #         self._client = CloudDriveClient(dir_confs.get("cd2_url"),
                #                                         dir_confs.get("username"),
                #                                         dir_confs.get("password"))
                #         if self._client:
                #             self._fs = self._client.fs
                #     except Exception as e:
                #         logger.warn(f"未正确配置CloudDrive2，请检查配置：{e}")
                #         return

                self._transfer_type = dir_confs.get("transfer_type")
                self._return_mode = dir_confs.get("return_mode") or "softlink"

                # 读取目录配置
                monitor_dirs = dir_confs.get("monitor_dirs") or []
                if not monitor_dirs:
                    return
                for monitor_dir in monitor_dirs:
                    if not monitor_dir:
                        continue

                    # 读取监控目录配置
                    mon_path = monitor_dir.get("dest_path")
                    # 云盘挂载路径
                    target_path = monitor_dir.get("mount_path")
                    # 监控模式
                    monitor_mode = monitor_dir.get("monitor_mode") or "compatibility"
                    self._dirconf[mon_path] = monitor_dir

                    # 启用目录监控
                    if self._enabled and self._monitor:
                        # 检查媒体库目录是不是下载目录的子目录
                        try:
                            if target_path and Path(target_path).is_relative_to(Path(mon_path)):
                                logger.warn(f"{target_path} 是监控目录 {mon_path} 的子目录，无法监控")
                                self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录，无法监控",
                                                       title="云盘助手媒体库监控")
                                continue
                        except Exception as e:
                            logger.debug(str(e))
                            pass

                        try:
                            if str(monitor_mode) == "compatibility":
                                # 兼容模式，目录同步性能降低且NAS不能休眠，但可以兼容挂载的远程共享目录如SMB
                                observer = PollingObserver(timeout=10)
                            else:
                                # 内部处理系统操作类型选择最优解
                                observer = Observer(timeout=10)
                            self._observer.append(observer)
                            observer.schedule(CloudFileMonitorHandler(mon_path, self), path=mon_path, recursive=True)
                            observer.daemon = True
                            observer.start()
                            logger.info(f"{mon_path} 的云盘助手媒体库监控服务启动")
                        except Exception as e:
                            err_msg = str(e)
                            if "inotify" in err_msg and "reached" in err_msg:
                                logger.warn(
                                    f"云盘助手媒体库监控服务启动出现异常：{err_msg}，请在宿主机上（不是docker容器内）执行以下命令并重启："
                                    + """
                                         echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
                                         echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf
                                         sudo sysctl -p
                                         """)
                            else:
                                logger.error(f"{mon_path} 启动云盘助手媒体库监控失败：{err_msg}")
                            self.systemmessage.put(f"{mon_path} 启动云盘助手媒体库监控失败：{err_msg}",
                                                   title="云盘助手媒体库监控")

                # 运行一次定时服务
                if self._onlyonce:
                    logger.info("云盘助手媒体库监控服务启动，立即运行一次")
                    self._scheduler.add_job(func=self.sync_all, trigger='date',
                                            run_date=datetime.datetime.now(
                                                tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3)
                                            )
                    # 关闭一次性开关
                    self._onlyonce = False
                    # 保存配置
                    self.__update_config()

                if self._invalid_cron:
                    self._scheduler.add_job(func=self.handle_invalid_links,
                                            trigger=CronTrigger.from_crontab(self._invalid_cron),
                                            id="handle_invalid_links")
                    logger.info(f"清理无效软连接服务启动，定时任务：{self._invalid_cron}")

                # 启动定时服务
                if self._scheduler.get_jobs():
                    self._scheduler.print_jobs()
                    self._scheduler.start()

    def __update_config(self):
        """
        更新配置
        """
        self.update_config({
            "enabled": self._enabled,
            "notify": self._notify,
            "onlyonce": self._onlyonce,
            "monitor": self._monitor,
            "invalid": self._invalid,
            "clean": self._clean,
            "dir_confs": self._dir_confs,
            "exclude_keywords": self._exclude_keywords,
            "interval": self._interval,
            "cron": self._cron,
            "only_media_history": self._only_media_history,
            "refresh": self._refresh,
            "invalid_cron": self._invalid_cron,
            "rmt_mediaext": self._rmt_mediaext
        })

    @eventmanager.register(EventType.PluginAction)
    def remote_sync(self, event: Event):
        """
        远程全量同步
        """
        if event:
            event_data = event.event_data
            if not event_data or event_data.get("action") != "cloud_assistant":
                return
            self.post_message(channel=event.event_data.get("channel"),
                              title="云盘助手开始同步监控目录 ...",
                              userid=event.event_data.get("user"))
        self.sync_all()
        if event:
            self.post_message(channel=event.event_data.get("channel"),
                              title="云盘助手监控目录同步完成！", userid=event.event_data.get("user"))

    def sync_all(self):
        """
        立即运行一次，全量同步目录中所有文件
        """
        logger.info("云盘助手全量同步监控目录 ...")

        # 遍历所有监控目录
        for mon_path in self._dirconf.keys():
            video_files = []
            other_files = []
            # 遍历目录下所有文件
            for root, dirs, files in os.walk(mon_path):
                for name in dirs + files:
                    file_path = os.path.join(root, name)
                    if Path(str(file_path)).is_file():
                        if Path(str(file_path)).suffix.lower() in [ext.strip() for ext in
                                                                   self._rmt_mediaext.split(",")]:
                            video_files.append(file_path)
                        else:
                            other_files.append(file_path)

            monitor_dir = self._dirconf.get(mon_path)
            only_media = monitor_dir.get("only_media") or "true"
            if str(only_media) == "false":
                # Then, handle other files
                for other_file in other_files:
                    self.__handle_file(event_path=str(other_file), mon_path=mon_path)
            # First, handle video files
            for video_file in video_files:
                self.__handle_file(event_path=str(video_file), mon_path=mon_path)

        logger.info("云盘助手全量同步监控目录完成！")

    def event_handler(self, event, mon_path: str, text: str, event_path: str):
        """
        处理文件变化
        :param event: 事件
        :param mon_path: 监控目录
        :param text: 事件描述
        :param event_path: 事件文件路径
        """
        if not event.is_directory:
            # 文件发生变化
            logger.debug("文件%s：%s" % (text, event_path))
            self.__handle_file(event_path=event_path, mon_path=mon_path)

    def __handle_file(self, event_path: str, mon_path: str):
        """
        同步一个文件
        :param event_path: 事件文件路径
        :param mon_path: 监控目录
        """
        file_path = Path(event_path)
        if Path(file_path).is_dir():
            return
        try:
            if not file_path.exists():
                return
            # 全程加锁
            with lock:
                # 回收站及隐藏的文件不处理
                if event_path.find('/@Recycle/') != -1 \
                        or event_path.find('/#recycle/') != -1 \
                        or event_path.find('/.') != -1 \
                        or event_path.find('/@eaDir') != -1:
                    logger.debug(f"{event_path} 是回收站或隐藏的文件")
                    return

                # 命中过滤关键字不处理
                if self._exclude_keywords:
                    for keyword in self._exclude_keywords.split("\n"):
                        if keyword and re.findall(keyword, event_path):
                            logger.info(f"{event_path} 命中过滤关键字 {keyword}，不处理")
                            return

                # 整理屏蔽词不处理
                transfer_exclude_words = self.systemconfig.get(SystemConfigKey.TransferExcludeWords)
                if transfer_exclude_words:
                    for keyword in transfer_exclude_words:
                        if not keyword:
                            continue
                        if keyword and re.search(r"%s" % keyword, event_path, re.IGNORECASE):
                            logger.info(f"{event_path} 命中整理屏蔽词 {keyword}，不处理")
                            return

                # 查询转移配置
                monitor_dir = self._dirconf.get(mon_path)
                mount_path = monitor_dir.get("mount_path")
                # cd2_path = monitor_dir.get("cd2_path")
                return_path = monitor_dir.get("return_path")
                delete_dest = monitor_dir.get("delete_dest") or "false"
                delete_src = monitor_dir.get("delete_src") or "false"
                delete_history = monitor_dir.get("delete_history") or "false"
                overwrite = monitor_dir.get("overwrite") or "false"
                upload_cloud = monitor_dir.get("upload_cloud") or "true"
                only_media = monitor_dir.get("only_media") or "true"
                dest_preserve_hierarchy = monitor_dir.get("dest_preserve_hierarchy") or 0
                src_paths = monitor_dir.get("src_paths") or ""
                src_preserve_hierarchy = monitor_dir.get("src_preserve_hierarchy") or 0
                # 本地文件保留时间 （小时）
                retention_time = monitor_dir.get("retention_time") or 0
                if not self._monitor and retention_time > 0:
                    creation_time = self.__get_file_creation_time(file_path)
                    creation_datetime = datetime.datetime.fromtimestamp(creation_time)
                    current_datetime = datetime.datetime.now()
                    time_difference = (current_datetime - creation_datetime).total_seconds()
                    if time_difference < (int(retention_time) * 3600):
                        logger.warning(
                            f"{file_path} 创建 {round(time_difference / 3600, 2)} 小时，小于保留时间 {retention_time} 小时，暂不处理")
                        return

                # 判断是不是蓝光目录
                if re.search(r"BDMV[/\\]STREAM", event_path, re.IGNORECASE):
                    # 截取BDMV前面的路径
                    blurray_dir = event_path[:event_path.find("BDMV")]
                    file_path = Path(blurray_dir)
                    logger.info(f"{event_path} 是蓝光目录，更正文件路径为：{str(file_path)}")

                # 1、转移到云盘挂载路径 上传到cd2
                # 挂载的路径
                logger.info(f"挂载目录文件 {file_path}")
                mount_file = str(file_path).replace(str(mon_path), str(mount_path))

                # 上传cloud时，如果不是仅媒体文件，则全上传，如果是仅媒体文件，则只上传媒体文件
                if str(upload_cloud) == "true" and (str(only_media) == "false" or (
                        str(only_media) == "true" and Path(file_path).suffix.lower() in [ext.strip() for ext in
                                                                                         self._rmt_mediaext.split(
                                                                                             ",")])):
                    upload = True
                    if str(overwrite) == "false":
                        if Path(mount_file).exists():
                            logger.info(f"云盘文件 {mount_file} 已存在且未开启覆盖，跳过上传")
                            upload = False
                            if str(self._transfer_type) == "move":
                                Path(file_path).unlink()
                    else:
                        if Path(mount_file).exists():
                            logger.info(f"云盘文件 {mount_file} 已存在且开启覆盖，删除原云盘文件")
                            Path(mount_file).unlink()
                    if upload:
                        # 媒体文件转移
                        if Path(file_path).suffix.lower() in [ext.strip() for ext in
                                                              self._rmt_mediaext.split(",")]:
                            self.__transfer_file(file_path=file_path,
                                                 target_file=mount_file,
                                                 transfer_type=self._transfer_type)
                        else:
                            # 其他文件复制
                            self.__transfer_file(file_path=file_path,
                                                 target_file=mount_file,
                                                 transfer_type="copy")

                # 2、软连接或者strm回本地路径
                target_return_file = str(file_path).replace(str(mon_path), str(return_path))
                if Path(target_return_file).suffix.lower() in [ext.strip() for ext in
                                                               self._rmt_mediaext.split(",")]:
                    # 检查云盘文件是否存在
                    if not Path(mount_file).exists():
                        logger.info(f"挂载目录文件 {mount_file} 不存在，不创建 {self._return_mode}")
                        return

                    # 媒体文件软连接
                    if str(self._return_mode) == "softlink":
                        # 检查软连接指向是否正确，如果不正确，修正
                        if os.path.islink(target_return_file):
                            current_target = os.readlink(target_return_file)
                            if str(current_target) != str(target_return_file):
                                subprocess.run(['ln', '-sf', mount_file, target_return_file])
                                logger.info(f"修正软连接 {target_return_file} -> {mount_file}")
                            retcode = 0
                        else:
                            retcode = self.__transfer_file(file_path=mount_file,
                                                           target_file=target_return_file,
                                                           transfer_type="softlink")
                    else:
                        # 生成strm文件
                        retcode = self.__create_strm_file(mount_file=mount_file,
                                                          mount_path=mount_path,
                                                          target_file=target_return_file,
                                                          library_dir=monitor_dir.get("library_dir"),
                                                          cloud_type=monitor_dir.get("cloud_type"),
                                                          cloud_path=monitor_dir.get("cloud_path"),
                                                          cloud_url=monitor_dir.get("cloud_url"),
                                                          cloud_scheme=monitor_dir.get("cloud_scheme"))

                    if monitor_dir.get("notify_url"):
                        RequestUtils(content_type="application/json").post(url=monitor_dir.get("notify_url"), json={
                            "path": str(mount_file),
                            "type": "add"
                        })
                else:
                    if Path(target_return_file).is_file() and Path(target_return_file).exists():
                        logger.info(f"目标文件 {target_return_file} 已存在，不处理")
                        retcode = 0
                    else:
                        # 其他nfo、jpg等复制文件
                        SystemUtils.copy(file_path, Path(target_return_file))
                        # shutil.copy2(str(file_path), target_return_file)
                        logger.info(f"复制其他文件 {str(file_path)} 到 {target_return_file}")
                        retcode = 0

                if retcode == 0:
                    transferhis = self.__get_transferhis_by_dest(db=None, dest_path=str(file_path))
                    if transferhis and self._refresh:
                        self.__refresh_emby(transferhis)

                    # 是否删除本地历史
                    if str(delete_history) == "true":
                        if transferhis:
                            self.__delete_history(transferhis)

                    # 3、存操作记录
                    if (self._only_media_history and Path(file_path).suffix.lower() in [ext.strip() for ext in
                                                                                        self._rmt_mediaext.split(",")]) \
                            or not self._only_media_history:
                        history = self.get_data('history') or []
                        history.append({
                            "file_path": str(file_path),
                            "target_cloud_file": mount_file,
                            "target_soft_file": target_return_file,
                            "delete_dest": delete_dest,
                            "delete_history": delete_history,
                            "time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
                        })
                        # 保存历史
                        self.save_data(key="history", value=history)

                    # 移动模式删除空目录
                    if str(delete_dest) == "true":
                        self.__delete_dest_file(file_path, mon_path, dest_preserve_hierarchy, only_media)
                    # 是否删除源文件
                    if str(delete_src) == "true" and transferhis:
                        self.__delete_src_file(transferhis, src_paths, src_preserve_hierarchy, only_media)
                    # 发送消息汇总
                    if self._notify and transferhis:
                        self.__msg_handler(transferhis)

        except Exception as e:
            logger.error("目录监控发生错误：%s - %s" % (str(e), traceback.format_exc()))

    def __delete_history(self, transferhis):
        """
        删除历史记录
        """
        self.__delete_transferhis_by_id(db=None, transferhisid=transferhis.id)
        logger.info(f"删除转移历史记录：{transferhis.id} {transferhis.download_hash}")

        downloadhis = self.__get_downloadhis_by_hash(db=None, download_hash=transferhis.download_hash)
        if downloadhis:
            self.__delete_downloadhis_by_id(db=None, downloadhisid=downloadhis.id)
            logger.info(f"删除下载历史记录：{downloadhis.id} {transferhis.download_hash}")
            downloadfiles = self.__get_downloadfiles_by_hash(db=None, download_hash=transferhis.download_hash)
            if downloadfiles:
                for downloadfile in downloadfiles:
                    self.__delete_downloadfile_by_id(db=None, downloadfileid=downloadfile.id)
                    logger.info(f"删除下载文件记录：{downloadfile.id} {transferhis.download_hash}")

    def __delete_dest_file(self, file_path: Path, mon_path: str, dest_preserve_hierarchy: int, only_media: str):
        """
        删除监控文件
        """
        if file_path.exists():
            file_path.unlink()
            logger.info(f"删除监控文件：{file_path}")

        # 保留层级
        mon_path_depth = len(Path(mon_path).parts)
        retain_depth = mon_path_depth + int(dest_preserve_hierarchy)

        for file_dir in file_path.parents:
            if len(file_dir.parts) <= retain_depth:
                # 重要，删除到保留层级目录为止
                break
            if str(only_media) == "true":
                files = SystemUtils.list_files(file_dir, [ext.strip() for ext in
                                                          self._rmt_mediaext.split(",")] + settings.DOWNLOAD_TMPEXT)
            else:
                files = SystemUtils.list_files(file_dir, [".*"])
            if not files:
                logger.warn(f"删除监控空目录：{file_dir}")
                shutil.rmtree(file_dir, ignore_errors=True)

    def __delete_src_file(self, transferhis, src_paths, src_preserve_hierarchy, only_media: str):
        """
        删除源文件
        """
        if Path(transferhis.src).exists():
            Path(transferhis.src).unlink()
            logger.info(f"删除源文件：{transferhis.src}")

        # 删除下载文件记录
        self.__delete_downloadfile_by_fullpath(db=None, fullpath=transferhis.src)

        # 发送事件 删种
        eventmanager.send_event(
            EventType.DownloadFileDeleted,
            {
                "src": transferhis.src,
                "hash": transferhis.download_hash
            }
        )

        # 源文件保留层级
        source_path = None
        for source_dir in src_paths.split(","):
            source_dir = source_dir.strip()
            if not source_dir:
                continue
            if transferhis.src.startswith(source_dir):
                source_path = source_dir
                logger.info(f"获取到源文件 {transferhis.src} 根目录 {source_path}")
                break

        # 删除源文件空目录
        if source_path:
            # 保留层级
            source_path_depth = len(Path(source_path).parts)
            retain_depth = source_path_depth + int(src_preserve_hierarchy)

            for file_dir in Path(transferhis.src).parents:
                if len(file_dir.parts) <= retain_depth:
                    # 重要，删除到保留层级目录为止
                    break
                if str(only_media) == "true":
                    files = SystemUtils.list_files(file_dir, [ext.strip() for ext in
                                                              self._rmt_mediaext.split(",")] + settings.DOWNLOAD_TMPEXT)
                else:
                    files = SystemUtils.list_files(file_dir, [".*"])
                if not files:
                    logger.warn(f"删除源文件空目录：{file_dir}")
                    shutil.rmtree(file_dir, ignore_errors=True)

    @staticmethod
    def __get_file_creation_time(file_path):
        """获取文件的创建时间"""
        if os.name == 'nt':  # Windows系统
            return os.path.getctime(file_path)
        else:  # Unix系统
            stat = os.stat(file_path)
            try:
                return stat.st_birthtime
            except AttributeError:
                return stat.st_mtime

    def __msg_handler(self, transferhis):
        """
        组织消息发送数据
        """
        """
          {
              "title_year season": {
                  "key": "title_year",
                  "mtype": "mtype",
                  "season": "season",
                  "category": "category",
                  "image": "image",
                  "episodes": [],
                  "time": "2023-08-24 23:23:23.332"
              }
          }
        """
        key = f"{transferhis.title} ({transferhis.year})"

        # 发送消息汇总
        media_list = self._medias.get(
            key + " " + transferhis.seasons) or {}
        if media_list:
            episodes = media_list.get("episodes") or []
            if transferhis.type == MediaType.TV.value:
                if episodes:
                    if int(transferhis.episodes.replace("E", "")) not in episodes:
                        episodes.append(int(transferhis.episodes.replace("E", "")))
                else:
                    episodes.append(int(transferhis.episodes.replace("E", "")))
            media_list = {
                "key": key,
                "mtype": transferhis.type,
                "category": transferhis.category,
                "image": transferhis.image,
                "season": transferhis.seasons,
                "episodes": episodes,
                "tmdbid": transferhis.tmdbid,
                "time": datetime.datetime.now()
            }
        else:
            media_list = {
                "key": key,
                "mtype": transferhis.type,
                "category": transferhis.category,
                "image": transferhis.image,
                "season": transferhis.seasons,
                "episodes": [
                    int(transferhis.episodes.replace("E", ""))] if transferhis.type == MediaType.TV.value else [],
                "tmdbid": transferhis.tmdbid,
                "time": datetime.datetime.now()
            }
        self._medias[key + " " + transferhis.seasons] = media_list

    def send_msg(self):
        """
        定时检查是否有媒体处理完，发送统一消息
        """
        if not self._medias or not self._medias.keys():
            return

        # 遍历检查是否已刮削完，发送消息
        for medis_title_year_season in list(self._medias.keys()):
            media_list = self._medias.get(medis_title_year_season)
            logger.info(f"开始处理媒体 {medis_title_year_season} 消息")

            if not media_list:
                del self._medias[medis_title_year_season]
                continue

            # 获取最后更新时间
            last_update_time = media_list.get("time")
            key = media_list.get("key")
            mtype = media_list.get("mtype")
            category = media_list.get("category")
            image = media_list.get("image")
            season = media_list.get("season")
            episodes = media_list.get("episodes")
            tmdbid = media_list.get("tmdbid")
            if not last_update_time:
                del self._medias[medis_title_year_season]
                continue

            # 判断剧集最后更新时间距现在是已超过10秒或者电影，发送消息
            if (datetime.datetime.now() - last_update_time).total_seconds() > int(self._interval) \
                    or mtype == MediaType.MOVIE.name:
                # 发送通知
                if self._notify:
                    # 剧集季集信息 S01 E01-E04 || S01 E01、E02、E04
                    season_episode = None
                    # 处理文件多，说明是剧集，显示季入库消息
                    if season:
                        # 季集文本
                        season_episode = f"{season} {StringUtils.format_ep(episodes)}"

                    if tmdbid:
                        # 获取图片
                        try:
                            image = self.chain.obtain_specific_image(
                                mediaid=tmdbid,
                                mtype=MediaType(mtype),
                                image_type=MediaImageType.Backdrop,
                                season=int(season.replace("S", "")) if season else None,
                                episode=int(episodes[0]) if episodes else None,
                            ) or image
                        except Exception:
                            image = image

                    # 发送消息
                    self.__send_transfer_message(title_year=key,
                                                 season_episodes=season_episode,
                                                 mtype=mtype,
                                                 category=category,
                                                 image=image,
                                                 count=len(episodes) if episodes else 1)
                    logger.info(f"发送媒体 {medis_title_year_season} 转移消息成功")
                # 发送完消息，移出key
                del self._medias[medis_title_year_season]
                continue

    def __send_transfer_message(self, title_year, season_episodes, mtype, category, image, count):
        """
        发送入库成功的消息
        """
        msg_title = f"{title_year} {season_episodes if season_episodes else ''} 已转移完成"
        msg_str = f"类型：{mtype}，类别：{category}，共{count}个文件"
        # 发送
        self.post_message(
            mtype=NotificationType.Plugin,
            title=msg_title,
            text=msg_str,
            image=image,
            link=settings.MP_DOMAIN('#/history')
        )

    def __transfer_file(self, file_path, target_file, transfer_type):
        """
        转移文件
        """
        logger.info(f"开始 {transfer_type} 文件 {str(file_path)} 到 {target_file}")
        # 如果是文件夹
        if Path(target_file).is_dir():
            if not Path(target_file).exists():
                logger.info(f"创建目标文件夹 {target_file}")
                os.makedirs(target_file)
                return 1
        else:
            if not Path(target_file).parent.exists():
                logger.info(f"创建目标文件夹 {Path(target_file).parent}")
                os.makedirs(Path(target_file).parent)

            # 媒体文件转移
            retcode, retmsg = self.__transfer_command(file_path, Path(target_file), transfer_type)
            logger.info(
                f"媒体文件{str(file_path)} {transfer_type} 到 {target_file} {'成功' if retcode == 0 else '失败'} {retmsg}")
            return retcode

    def __transfer_command(self, file_item: Path, target_file: Path, transfer_type: str):
        """
        使用系统命令处理单个文件
        :param file_item: 文件路径
        :param target_file: 目标文件路径
        :param transfer_type: RmtMode转移方式
        """
        # 转移
        if transfer_type == 'link':
            # 硬链接
            retcode, retmsg = SystemUtils.link(file_item, target_file)
        elif transfer_type == 'softlink':
            # 软链接
            retcode, retmsg = SystemUtils.softlink(file_item, target_file)
        elif transfer_type == 'move':
            # 复制
            retcode, retmsg = SystemUtils.copy(file_item, target_file)
            if retcode == 0:
                file_item.unlink()
            else:
                logger.error(f"移动文件失败 {file_item} {target_file} {retcode} {retmsg}")
            # 移动
            # retcode, retmsg = SystemUtils.move(file_item, target_file)
        else:
            # 复制
            retcode, retmsg = SystemUtils.copy(file_item, target_file)

        if retcode != 0:
            logger.error(retmsg)

        return retcode, retmsg

    @staticmethod
    def __create_strm_file(mount_file: str, mount_path: str, target_file: str, library_dir: str = None,
                           cloud_type: str = None, cloud_path: str = None, cloud_url: str = None,
                           cloud_scheme: str = None):
        """
        生成strm文件
        :param library_dir:
        :param mount_path:
        :param mount_file:
        """
        try:
            # 获取视频文件名和目录
            video_name = Path(target_file).name
            # 获取视频目录
            dest_path = Path(target_file).parent

            if not dest_path.exists():
                logger.info(f"创建目标文件夹 {dest_path}")
                os.makedirs(str(dest_path))

            # 构造.strm文件路径
            strm_path = os.path.join(dest_path, f"{os.path.splitext(video_name)[0]}.strm")
            # strm已存在跳过处理
            if Path(strm_path).exists():
                logger.info(f"strm文件已存在 {strm_path}")
                return

            logger.info(f"替换前挂载云盘路径:::{mount_file}")

            # 云盘模式
            if cloud_type:
                # 替换路径中的\为/
                dest_file = mount_file.replace("\\", "/")
                dest_file = dest_file.replace(cloud_path, "")
                # 对盘符之后的所有内容进行url转码
                dest_file = urllib.parse.quote(dest_file, safe='')
                if str(cloud_type) == "cd2":
                    # 将路径的开头盘符"/mnt/user/downloads"替换为"http://localhost:19798/static/http/localhost:19798/False/"
                    dest_file = f"{cloud_scheme}://{cloud_url}/static/{cloud_scheme}/{cloud_url}/False/{dest_file}"
                    logger.info(f"替换后cd2路径:::{dest_file}")
                elif str(cloud_type) == "alist":
                    dest_file = f"{cloud_scheme}://{cloud_url}/d/{dest_file}"
                    logger.info(f"替换后alist路径:::{dest_file}")
                else:
                    logger.error(f"云盘类型 {cloud_type} 错误")
                    return
            else:
                # 本地挂载路径转为emby路径
                dest_file = mount_file.replace(mount_path, library_dir)
                logger.info(f"替换后emby容器内路径:::{dest_file}")

            # 写入.strm文件
            with open(strm_path, 'w') as f:
                f.write(dest_file)

            logger.info(f"创建strm文件 {strm_path}")
            return 0
        except Exception as e:
            logger.error(f"创建strm文件失败")
            print(str(e))
            return 1

    @staticmethod
    def is_broken_symlink(path):
        current_target = os.readlink(path)
        if not os.path.exists(current_target):
            return True
        return False

    @staticmethod
    def __get_softlink_list(path, parrent):
        """
        获取软链接列表
        """
        if not os.path.exists(path):
            return []
        softlink_list = []
        for file_path in Path(path).rglob('**/*'):
            if file_path.is_symlink() and file_path.suffix in parrent:
                softlink_list.append(file_path)
        return softlink_list

    def handle_invalid_links(self):
        """
        立即运行一次，清理无效软连接
        """
        # 遍历所有监控目录
        for mon_path in self._dirconf.keys():
            monitor_dir = self._dirconf.get(mon_path)
            return_path = monitor_dir.get("return_path")
            logger.info(f"{return_path} 开始检查无效软连接")

            # 遍历目录及子目录
            softlink_list = self.__get_softlink_list(path=return_path,
                                                     parrent=[ext.strip() for ext in self._rmt_mediaext.split(",")])
            if not softlink_list:
                logger.info(f"{return_path} 没有软连接")
                continue
            logger.info(f"{return_path} 软连接数量：{len(softlink_list)}")

            for file_path in softlink_list:
                if self.is_broken_symlink(file_path):
                    logger.warn(f"删除无效软连接: {str(file_path)}")
                    file_path.unlink()

                    # 递归删除空目录，最多到三级深度
                    for depth, file_dir in enumerate(file_path.parents):
                        if depth >= 3 and str(file_dir) == str(return_path):
                            break
                        if not any(file_dir.iterdir()):  # 检查目录是否为空
                            logger.warning(f"删除空目录：{file_dir}")
                            try:
                                shutil.rmtree(file_dir, ignore_errors=True)
                            except OSError as e:
                                logger.error(f"删除目录失败: {e}")
                                break
                else:
                    logger.info(f"{str(file_path)} 链接正常，跳过处理")

            logger.info(f"{return_path} 处理无效软连接完成！")

        logger.info("云盘助手清理无效软连接完成！")

    def __refresh_emby(self, transferinfo):
        """
        刷新emby
        """
        if transferinfo.type == "电影":
            movies = Emby().get_movies(title=transferinfo.title, year=transferinfo.year)
            if not movies:
                logger.error(f"Emby中没有找到{transferinfo.title} ({transferinfo.year})")
                return
            for movie in movies:
                self.__refresh_emby_library_by_id(item_id=movie.item_id)
                logger.info(f"已通知刷新Emby电影：{movie.title} ({movie.year}) item_id:{movie.item_id}")
        else:
            item_id = self.__get_emby_series_id_by_name(name=transferinfo.title, year=transferinfo.year)
            if not item_id or item_id is None:
                logger.error(f"Emby中没有找到{transferinfo.title} ({transferinfo.year})")
                return

            # 验证tmdbid是否相同
            item_info = Emby().get_iteminfo(item_id)
            if item_info:
                if transferinfo.tmdbid and item_info.tmdbid:
                    if str(transferinfo.tmdbid) != str(item_info.tmdbid):
                        logger.error(f"Emby中{transferinfo.title} ({transferinfo.year})的tmdbId与入库记录不一致")
                        return

            # 查询集的item_id
            season = int(transferinfo.seasons.replace("S", ""))
            episode = int(transferinfo.episodes.replace("E", ""))
            episode_item_id = self.__get_emby_episode_item_id(item_id=item_id, season=season, episode=episode)
            if not episode_item_id or episode_item_id is None:
                logger.error(
                    f"Emby中没有找到{transferinfo.title} ({transferinfo.year}) {transferinfo.seasons}{transferinfo.episodes}")
                return

            self.__refresh_emby_library_by_id(item_id=episode_item_id)
            logger.info(
                f"已通知刷新Emby电视剧：{transferinfo.title} ({transferinfo.year}) {transferinfo.seasons}{transferinfo.episodes} item_id:{episode_item_id}")

    def __get_emby_episode_item_id(self, item_id: str, season: int, episode: int) -> Optional[str]:
        """
        根据剧集信息查询Emby中集的item_id
        """
        if not self._EMBY_HOST or not self._EMBY_APIKEY:
            return None
        req_url = "%semby/Shows/%s/Episodes?Season=%s&IsMissing=false&api_key=%s" % (
            self._EMBY_HOST, item_id, season, self._EMBY_APIKEY)
        try:
            with RequestUtils().get_res(req_url) as res_json:
                if res_json:
                    tv_item = res_json.json()
                    res_items = tv_item.get("Items")
                    for res_item in res_items:
                        season_index = res_item.get("ParentIndexNumber")
                        if not season_index:
                            continue
                        if season and season != season_index:
                            continue
                        episode_index = res_item.get("IndexNumber")
                        if not episode_index:
                            continue
                        if episode and episode != episode_index:
                            continue
                        episode_item_id = res_item.get("Id")
                        return episode_item_id
        except Exception as e:
            logger.error(f"连接Shows/Id/Episodes出错：" + str(e))
            return None
        return None

    def __refresh_emby_library_by_id(self, item_id: str) -> bool:
        """
        通知Emby刷新一个项目的媒体库
        """
        if not self._EMBY_HOST or not self._EMBY_APIKEY:
            return False
        req_url = "%semby/Items/%s/Refresh?MetadataRefreshMode=FullRefresh" \
                  "&ImageRefreshMode=FullRefresh&ReplaceAllMetadata=true&ReplaceAllImages=true&api_key=%s" % (
                      self._EMBY_HOST, item_id, self._EMBY_APIKEY)
        try:
            with RequestUtils().post_res(req_url) as res:
                if res:
                    return True
                else:
                    logger.info(f"刷新媒体库对象 {item_id} 失败，无法连接Emby！")
        except Exception as e:
            logger.error(f"连接Items/Id/Refresh出错：" + str(e))
            return False
        return False

    def __get_emby_series_id_by_name(self, name: str, year: str) -> Optional[str]:
        """
        根据名称查询Emby中剧集的SeriesId
        :param name: 标题
        :param year: 年份
        :return: None 表示连不通，""表示未找到，找到返回ID
        """
        if not self._EMBY_HOST or not self._EMBY_APIKEY:
            return None
        req_url = ("%semby/Items?"
                   "IncludeItemTypes=Series"
                   "&Fields=ProductionYear"
                   "&StartIndex=0"
                   "&Recursive=true"
                   "&SearchTerm=%s"
                   "&Limit=10"
                   "&IncludeSearchTypes=false"
                   "&api_key=%s") % (
                      self._EMBY_HOST, name, self._EMBY_APIKEY)
        try:
            with RequestUtils().get_res(req_url) as res:
                if res:
                    res_items = res.json().get("Items")
                    if res_items:
                        for res_item in res_items:
                            if res_item.get('Name') == name and (
                                    not year or str(res_item.get('ProductionYear')) == str(year)):
                                return res_item.get('Id')
        except Exception as e:
            logger.error(f"连接Items出错：" + str(e))
            return None
        return ""

    def get_state(self) -> bool:
        return self._enabled

    @staticmethod
    def get_command() -> List[Dict[str, Any]]:
        """
        定义远程控制命令
        :return: 命令关键字、事件、描述、附带数据
        """
        return [{
            "cmd": "/cloud_assistant",
            "event": EventType.PluginAction,
            "desc": "云盘助手同步",
            "category": "",
            "data": {
                "action": "cloud_assistant"
            }
        }]

    def get_api(self) -> List[Dict[str, Any]]:
        return [{
            "path": "/cloud_assistant",
            "endpoint": self.sync,
            "methods": ["GET"],
            "summary": "云盘助手同步",
            "description": "云盘助手同步",
        }]

    def get_service(self) -> List[Dict[str, Any]]:
        """
        注册插件公共服务
        [{
            "id": "服务ID",
            "name": "服务名称",
            "trigger": "触发器：cron/interval/date/CronTrigger.from_crontab()",
            "func": self.xxx,
            "kwargs": {} # 定时器参数
        }]
        """
        if self._enabled and self._cron:
            return [{
                "id": "CloudAssistantSyncAll",
                "name": "云盘助手全量同步服务",
                "trigger": CronTrigger.from_crontab(self._cron),
                "func": self.sync_all,
                "kwargs": {}
            }]
        return []

    def sync(self, apikey: str) -> schemas.Response:
        """
        API调用目录同步
        """
        if apikey != settings.API_TOKEN:
            return schemas.Response(success=False, message="API密钥错误")
        self.sync_all()
        return schemas.Response(success=True)

    def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
        return [
            {
                'component': 'VForm',
                'content': [
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'enabled',
                                            'label': '启用插件',
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'notify',
                                            'label': '发送通知',
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'onlyonce',
                                            'label': '立即全量同步一次',
                                        }
                                    }
                                ]
                            },
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'monitor',
                                            'label': '实时监控',
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'only_media_history',
                                            'label': '插件历史仅媒体文件',
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'invalid',
                                            'label': '立即清理无效软连接',
                                        }
                                    }
                                ]
                            },
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                "component": "VCol",
                                "props": {
                                    "cols": 12,
                                    "md": 4
                                },
                                "content": [
                                    {
                                        "component": "VSwitch",
                                        "props": {
                                            "model": "dialog_closed",
                                            "label": "插件配置"
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'refresh',
                                            'label': '刷新媒体库（emby）',
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VSwitch',
                                        'props': {
                                            'model': 'clean',
                                            'label': '立即清空插件历史',
                                        }
                                    }
                                ]
                            },
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VTextField',
                                        'props': {
                                            'model': 'cron',
                                            'label': '定时全量同步周期',
                                            'placeholder': '5位cron表达式，留空关闭'
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VTextField',
                                        'props': {
                                            'model': 'invalid_cron',
                                            'label': '定时清理无效软连接周期',
                                            'placeholder': '5位cron表达式，留空关闭'
                                        }
                                    }
                                ]
                            },
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                    'md': 4
                                },
                                'content': [
                                    {
                                        'component': 'VTextField',
                                        'props': {
                                            'model': 'interval',
                                            'label': '入库消息延迟',
                                            'placeholder': '10'
                                        }
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                },
                                'content': [
                                    {
                                        'component': 'VTextarea',
                                        'props': {
                                            'model': 'exclude_keywords',
                                            'label': '排除关键词',
                                            'rows': 2,
                                            'placeholder': '每一行一个关键词'
                                        }
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12
                                },
                                'content': [
                                    {
                                        'component': 'VTextarea',
                                        'props': {
                                            'model': 'rmt_mediaext',
                                            'label': '视频格式',
                                            'rows': 2,
                                            'placeholder': ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v"
                                        }
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                },
                                'content': [
                                    {
                                        'component': 'VAlert',
                                        'props': {
                                            'type': 'info',
                                            'variant': 'tonal',
                                            'text': '插件开启后，开启监控才会实时处理。与定时任务执行不冲突。入库消息延迟建议调大，读写需要时间。'
                                        }
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        'component': 'VRow',
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                },
                                'content': [
                                    {
                                        'component': 'VAlert',
                                        'props': {
                                            'type': 'error',
                                            'variant': 'tonal',
                                            'text': '插件没想公开，传的快，死的快。'
                                        }
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        'component': 'VRow',
                        'props': {
                            'style': {
                                'margin-top': '12px'
                            },
                        },
                        'content': [
                            {
                                'component': 'VCol',
                                'props': {
                                    'cols': 12,
                                },
                                'content': [
                                    {
                                        'component': 'VAlert',
                                        'props': {
                                            'type': 'success',
                                            'variant': 'tonal'
                                        },
                                        'content': [
                                            {
                                                'component': 'span',
                                                'text': '配置教程请参考：'
                                            },
                                            {
                                                'component': 'a',
                                                'props': {
                                                    'href': 'https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/docs/CloudAssistant.md',
                                                    'target': '_blank'
                                                },
                                                'text': 'https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/docs/CloudAssistant.md'
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        "component": "VDialog",
                        "props": {
                            "model": "dialog_closed",
                            "max-width": "65rem",
                            "overlay-class": "v-dialog--scrollable v-overlay--scroll-blocked",
                            "content-class": "v-card v-card--density-default v-card--variant-elevated rounded-t"
                        },
                        "content": [
                            {
                                "component": "VCard",
                                "props": {
                                    "title": "插件配置"
                                },
                                "content": [
                                    {
                                        "component": "VDialogCloseBtn",
                                        "props": {
                                            "model": "dialog_closed"
                                        }
                                    },
                                    {
                                        "component": "VCardText",
                                        "props": {},
                                        "content": [
                                            {
                                                'component': 'VRow',
                                                'content': [
                                                    {
                                                        'component': 'VCol',
                                                        'props': {
                                                            'cols': 12,
                                                        },
                                                        'content': [
                                                            {
                                                                'component': 'VAceEditor',
                                                                'props': {
                                                                    'modelvalue': 'dir_confs',
                                                                    'lang': 'json',
                                                                    'theme': 'monokai',
                                                                    'style': 'height: 30rem',
                                                                }
                                                            }
                                                        ]
                                                    }
                                                ]
                                            },
                                            {
                                                'component': 'VRow',
                                                'content': [
                                                    {
                                                        'component': 'VCol',
                                                        'props': {
                                                            'cols': 12,
                                                        },
                                                        'content': [
                                                            {
                                                                'component': 'VAlert',
                                                                'props': {
                                                                    'type': 'info',
                                                                    'variant': 'tonal'
                                                                },
                                                                'content': [
                                                                    {
                                                                        'component': 'span',
                                                                        'text': '注意：只有正确配置时，该助手才能正常工作。'
                                                                    }
                                                                ]
                                                            }
                                                        ]
                                                    }
                                                ]
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ], {
            "enabled": False,
            "notify": False,
            "monitor": False,
            "onlyonce": False,
            "invalid": False,
            "refresh": False,
            "only_media_history": False,
            "clean": False,
            "exclude_keywords": "",
            "interval": 60,
            "cron": "",
            "invalid_cron": "",
            "dir_confs": json.dumps(CloudAssistant.example, indent=4, ensure_ascii=False),
            "rmt_mediaext": ".mp4, .mkv, .ts, .iso,.rmvb, .avi, .mov, .mpeg,.mpg, .wmv, .3gp, .asf, .m4v, .flv, .m2ts, .strm,.tp, .f4v"
        }

    def get_page(self) -> List[dict]:
        # 查询同步详情
        historys = self.get_data('history')
        if not historys:
            return [
                {
                    'component': 'div',
                    'text': '暂无数据',
                    'props': {
                        'class': 'text-center',
                    }
                }
            ]

        if not isinstance(historys, list):
            historys = [historys]

        # 按照时间倒序
        historys = sorted(historys, key=lambda x: x.get("time") or 0, reverse=True)

        msgs = [
            {
                'component': 'tr',
                'props': {
                    'class': 'text-sm'
                },
                'content': [
                    {
                        'component': 'td',
                        'props': {
                            'class': 'whitespace-nowrap break-keep text-high-emphasis'
                        },
                        'text': history.get("time")
                    },
                    {
                        'component': 'td',
                        'text': history.get("file_path")
                    },
                    {
                        'component': 'td',
                        'text': history.get("target_cloud_file")
                    },
                    {
                        'component': 'td',
                        'text': history.get("target_soft_file")
                    },
                    {
                        'component': 'td',
                        'text': history.get("delete_dest")
                    },
                    {
                        'component': 'td',
                        'text': history.get("delete_history")
                    }
                ]
            } for history in historys
        ]

        # 拼装页面
        return [
            {
                'component': 'VRow',
                'content': [
                    {
                        'component': 'VCol',
                        'props': {
                            'cols': 12,
                        },
                        'content': [
                            {
                                'component': 'VTable',
                                'props': {
                                    'hover': True
                                },
                                'content': [
                                    {
                                        'component': 'thead',
                                        'content': [
                                            {
                                                'component': 'th',
                                                'props': {
                                                    'class': 'text-start ps-4'
                                                },
                                                'text': 'time'
                                            },
                                            {
                                                'component': 'th',
                                                'props': {
                                                    'class': 'text-start ps-4'
                                                },
                                                'text': '本地文件'
                                            },
                                            {
                                                'component': 'th',
                                                'props': {
                                                    'class': 'text-start ps-4'
                                                },
                                                'text': '云盘挂载文件'
                                            },
                                            {
                                                'component': 'th',
                                                'props': {
                                                    'class': 'text-start ps-4'
                                                },
                                                'text': '软连接文件'
                                            },
                                            {
                                                'component': 'th',
                                                'props': {
                                                    'class': 'text-start ps-4'
                                                },
                                                'text': '是否删除本地文件'
                                            },
                                            {
                                                'component': 'th',
                                                'props': {
                                                    'class': 'text-start ps-4'
                                                },
                                                'text': '是否删除历史记录'
                                            },
                                        ]
                                    },
                                    {
                                        'component': 'tbody',
                                        'content': msgs
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]

    def stop_service(self):
        """
        退出插件
        """
        if self._observer:
            for observer in self._observer:
                try:
                    observer.stop()
                    observer.join()
                except Exception as e:
                    print(str(e))
        self._observer = []
        if self._scheduler:
            self._scheduler.remove_all_jobs()
            if self._scheduler.running:
                self._event.set()
                self._scheduler.shutdown()
                self._event.clear()
            self._scheduler = None

    @staticmethod
    @db_query
    def __get_transferhis_by_dest(db: Optional[Session], dest_path: str) -> TransferHistory:
        """
        根据目标路径查询转移记录
        """
        return db.query(TransferHistory).filter(TransferHistory.dest == dest_path).first()

    @staticmethod
    @db_update
    def __delete_transferhis_by_id(db: Optional[Session], transferhisid: int):
        """
        根据转移记录ID删除转移记录
        """
        TransferHistory.delete(db, transferhisid)
        db.commit()

    @staticmethod
    @db_query
    def __get_downloadhis_by_hash(db: Optional[Session], download_hash: str) -> DownloadHistory:
        """
        根据下载记录hash查询下载记录
        """
        return DownloadHistory.get_by_hash(db, download_hash)

    @staticmethod
    @db_update
    def __delete_downloadhis_by_id(db: Optional[Session], downloadhisid: int):
        """
        根据下载记录ID删除下载记录
        """
        DownloadHistory.delete(db, downloadhisid)
        db.commit()

    @staticmethod
    @db_query
    def __get_downloadfiles_by_hash(db: Optional[Session], download_hash: str) -> List[DownloadFiles]:
        """
        根据下载记录hash查询下载文件记录
        """
        return DownloadFiles.get_by_hash(db, download_hash, None)

    @staticmethod
    @db_update
    def __delete_downloadfile_by_id(db: Optional[Session], downloadfileid: int):
        """
        根据下载文件记录ID删除下载文件记录
        """
        DownloadFiles.delete(db, downloadfileid)
        db.commit()

    @staticmethod
    @db_update
    def __delete_downloadfile_by_fullpath(db: Optional[Session], fullpath: str):
        """
        根据下载文件路径删除下载文件记录
        """
        DownloadFiles.delete_by_fullpath(db, fullpath)
        db.commit()
