2024-11-19 17:39:11 +08:00
2024-11-19 17:39:11 +08:00
2024-03-27 00:41:24 +08:00
2023-11-01 10:24:05 +08:00
2024-11-17 15:34:28 +08:00
2024-11-19 17:39:11 +08:00

MoviePilot-Plugins

MoviePilot官方插件市场https://github.com/jxxghp/MoviePilot-Plugins

第三方插件库开发说明

请不要开发用于破解MoviePilot用户认证、色情、赌博等违法违规内容的插件共同维护健康的开发环境

1. 目录结构

  • 插件仓库需要保持与本项目一致的目录结构建议fork后修改仅支持Github仓库plugins存放插件代码,一个插件一个子目录,子目录名必须为插件类名的小写,插件主类在__init__.py中编写。
  • package.json为插件仓库中所有插件概要信息用于在MoviePilot的插件市场显示其中版本号等需与插件代码保持一致通过修改版本号可触发MoviePilot显示插件更新。

2. 插件图标

  • 插件图标可复用官方插件库中icons下已有图标否则需使用完整的http格式的url图片链接包括package.json中的icon和插件代码中的plugin_icon
  • 插件的背景颜色会自动提取使用图标的主色调。

3. 插件命名

  • 插件命名请勿与官方库插件中的插件冲突否则会在MoviePilot版本升级时被官方插件覆盖。

4. 依赖

  • 可在插件目录中放置requirements.txt文件用于指定插件依赖的第三方库MoviePilot会在插件安装时自动安装依赖库。

5. 界面开发

  • 插件支持插件配置详情展示仪表板Widget三个展示页面,通过配置化的方式组装,使用 Vuetify 组件库所有该组件库有的组件都可以通过Json配置使用。

常见问题

1. 如何扩展消息推送渠道?

  • 注册 NoticeMessage 事件响应,event_data 包含消息中的所有数据,参考 IYUU消息通知 插件:

    注册事件:

    @eventmanager.register(EventType.NoticeMessage)
    
  • 事件对象:

    {
         "channel": MessageChannel|None,
         "type": NotificationType|None,
         "title": str,
         "text": str,
         "image": str,
         "userid": str|int,
    }
    
  • MoviePilot中所有事件清单可以通过实现这些事情来扩展功能同时插件之前也可以通过发送和监听事件实现联动。

class EventType(Enum):
    # 插件需要重载
    PluginReload = "plugin.reload"
    # 插件动作
    PluginAction = "plugin.action"
    # 执行命令
    CommandExcute = "command.excute"
    # 站点已删除
    SiteDeleted = "site.deleted"
    # 站点已更新
    SiteUpdated = "site.updated"
    # 转移完成
    TransferComplete = "transfer.complete"
    # 下载已添加
    DownloadAdded = "download.added"
    # 删除历史记录
    HistoryDeleted = "history.deleted"
    # 删除下载源文件
    DownloadFileDeleted = "downloadfile.deleted"
    # 删除下载任务
    DownloadDeleted = "download.deleted"
    # 收到用户外来消息
    UserMessage = "user.message"
    # 收到Webhook消息
    WebhookMessage = "webhook.message"
    # 发送消息通知
    NoticeMessage = "notice.message"
    # 名称识别请求
    NameRecognize = "name.recognize"
    # 名称识别结果
    NameRecognizeResult = "name.recognize.result"
    # 订阅已添加
    SubscribeAdded = "subscribe.added"
    # 订阅已完成
    SubscribeComplete = "subscribe.complete"
    # 系统错误
    SystemError = "system.error"

2. 如何在插件中实现远程命令响应?

  • 实现 get_command() 方法,按以下格式返回命令列表:

    [{
        "cmd": "/douban_sync", // 动作ID必须以/开始
        "event": EventType.PluginAction,// 事件类型,固定值
        "desc": "命令名称",
        "category": "命令菜单(微信)",
        "data": {
            "action": "douban_sync" // 动作标识
        }
    }]
    
  • 注册 PluginAction 事件响应,根据 event_data.action 是否为插件设定的动作标识来判断是否为本插件事件:

    注册事件:

    @eventmanager.register(EventType.PluginAction)
    

    事件判定:

    event_data = event.event_data
    if not event_data or event_data.get("action") != "douban_sync":
        return
    

3. 如何在插件中对外暴露API

  • 实现 get_api() 方法按以下格式返回API列表

    [{
        "path": "/refresh_by_domain", // API路径必须以/开始
        "endpoint": self.refresh_by_domain, // API响应方法
        "methods": ["GET"], // 请求方式GET/POST/PUT/DELETE
        "summary": "刷新站点数据", // API名称
        "description": "刷新对应域名的站点数据", // API描述
    }]
    

    注意在插件中暴露API接口时注意安全控制推荐使用settings.API_TOKEN进行身份验证。

  • 在对应的方法中实现API响应方法逻辑通过 http://localhost:3001/docs 查看API文档和调试

4. 如何在插件中注册公共定时服务?

  • 注册公共定时服务后,可以在设定-服务中查看运行状态和手动启动,更加便捷。
  • 实现 get_service() 方法,按以下格式返回服务注册信息:
    [{
        "id": "服务ID", // 不能与其它服务ID重复
        "name": "服务名称", // 显示在服务列表中的名称
        "trigger": "触发器cron/interval/date/CronTrigger.from_crontab()",
        "func": self.xxx, // 服务方法
        "kwargs": {} // 定时器参数参考APScheduler
    }]
    

5. 如何通过插件增强MoviePilot的识别功能

  • 注册 NameRecognize 事件实现识别逻辑参考ChatGPT插件注意只有主程序无法识别时才会触发该事件

    @eventmanager.register(EventType.NameRecognize)
    
  • 完成识别后发送 NameRecognizeResult 事件,将识别结果注入主程序

    eventmanager.send_event(
        EventType.NameRecognizeResult,
        {
            'title': title, # 原传入标题
            'name': str, # 识别的名称
            'year': str, # 识别的年份
            'season': int, # 识别的季号
            'episode': int, # 识别的集号
        }
    )
    
  • 注意识别请求需要在15秒内响应否则结果会被丢弃插件未启用或参数不完整时应立即回复空结果事件,避免主程序等待; 多个插件开启识别功能时,以先收到的识别结果事件为准。

    eventmanager.send_event(
        EventType.NameRecognizeResult,
        {
            'title': title # 结果只含原标题,代表空识别结果事件
        }
    )
    

6. 如何扩展内建索引器的索引站点?

  • 通过调用 SitesHelper().add_indexer(domain: str, indexer: dict) 方法,新增或修改内建索引器的支持范围,其中indexer为站点配置Json格式示例如下

    示例一:

    {
      "id": "nyaa",
      "name": "Nyaa",
      "domain": "https://nyaa.si/",
      "encoding": "UTF-8",
      "public": true,
      "proxy": true,
      "result_num": 100,
      "timeout": 30,
      "search": {
        "paths": [
          {
            "path": "?f=0&c=0_0&q={keyword}",
            "method": "get"
          }
        ]
      },
      "browse": {
        "path": "?p={page}",
        "start": 1
      },
      "torrents": {
        "list": {
          "selector": "table.torrent-list > tbody > tr"
        },
        "fields": {
          "id": {
            "selector": "a[href*=\"/view/\"]",
            "attribute": "href",
            "filters": [
              {
                "name": "re_search",
                "args": [
                  "\\d+",
                  0
                ]
              }
            ]
          },
          "title": {
            "selector": "td:nth-child(2) > a"
          },
          "details": {
            "selector": "td:nth-child(2) > a",
            "attribute": "href"
          },
          "download": {
            "selector": "td:nth-child(3) > a[href*=\"/download/\"]",
            "attribute": "href"
          },
          "date_added": {
            "selector": "td:nth-child(5)"
          },
          "size": {
            "selector": "td:nth-child(4)"
          },
          "seeders": {
            "selector": "td:nth-child(6)"
          },
          "leechers": {
            "selector": "td:nth-child(7)"
          },
          "grabs": {
            "selector": "td:nth-child(8)"
          },
          "downloadvolumefactor": {
            "case": {
              "*": 0
            }
          },
          "uploadvolumefactor": {
            "case": {
              "*": 1
            }
          }
        }
      }
    }
    

    示例二:

    {
        "id": "xxx",
        "name": "站点名称",
        "domain": "https://www.xxx.com/",
        "ext_domains": [
          "https://www.xxx1.com/",
          "https://www.xxx2.com/"
        ],
        "encoding": "UTF-8",
        "public": false,
        "search": {
          "paths": [
            {
              "path": "torrents.php",
              "method": "get"
            }
          ],
          "params": {
            "search": "{keyword}",
            "search_area": 4
          },
          "batch": {
            "delimiter": " ",
            "space_replace": "_"
          }
        },
        "category": {
          "movie": [
            {
              "id": 401,
              "cat": "Movies",
              "desc": "Movies电影"
            },
            {
              "id": 405,
              "cat": "Anime",
              "desc": "Animations动漫"
            },
            {
              "id": 404,
              "cat": "Documentary",
              "desc": "Documentaries纪录片"
            }
          ],
          "tv": [
            {
              "id": 402,
              "cat": "TV",
              "desc": "TV Series电视剧"
            },
            {
              "id": 403,
              "cat": "TV",
              "desc": "TV Shows综艺"
            },
            {
              "id": 404,
              "cat": "Documentary",
              "desc": "Documentaries纪录片"
            },
            {
              "id": 405,
              "cat": "Anime",
              "desc": "Animations动漫"
            }
          ]
        },
        "torrents": {
          "list": {
            "selector": "table.torrents > tr:has(\"table.torrentname\")"
          },
          "fields": {
            "id": {
              "selector": "a[href*=\"details.php?id=\"]",
              "attribute": "href",
              "filters": [
                {
                  "name": "re_search",
                  "args": [
                    "\\d+",
                    0
                  ]
                }
              ]
            },
            "title_default": {
              "selector": "a[href*=\"details.php?id=\"]"
            },
            "title_optional": {
              "optional": true,
              "selector": "a[title][href*=\"details.php?id=\"]",
              "attribute": "title"
            },
            "title": {
              "text": "{% if fields['title_optional'] %}{{ fields['title_optional'] }}{% else %}{{ fields['title_default'] }}{% endif %}"
            },
            "details": {
              "selector": "a[href*=\"details.php?id=\"]",
              "attribute": "href"
            },
            "download": {
              "selector": "a[href*=\"download.php?id=\"]",
              "attribute": "href"
            },
            "imdbid": {
              "selector": "div.imdb_100 > a",
              "attribute": "href",
              "filters": [
                {
                  "name": "re_search",
                  "args": [
                    "tt\\d+",
                    0
                  ]
                }
              ]
            },
            "date_elapsed": {
              "selector": "td:nth-child(4) > span",
              "optional": true
            },
            "date_added": {
              "selector": "td:nth-child(4) > span",
              "attribute": "title",
              "optional": true
            },
            "size": {
              "selector": "td:nth-child(5)"
            },
            "seeders": {
              "selector": "td:nth-child(6)"
            },
            "leechers": {
              "selector": "td:nth-child(7)"
            },
            "grabs": {
              "selector": "td:nth-child(8)"
            },
            "downloadvolumefactor": {
              "case": {
                "img.pro_free": 0,
                "img.pro_free2up": 0,
                "img.pro_50pctdown": 0.5,
                "img.pro_50pctdown2up": 0.5,
                "img.pro_30pctdown": 0.3,
                "*": 1
              }
            },
            "uploadvolumefactor": {
              "case": {
                "img.pro_50pctdown2up": 2,
                "img.pro_free2up": 2,
                "img.pro_2up": 2,
                "*": 1
              }
            },
            "description": {
              "selector": "td:nth-child(2) > table > tr > td.embedded > span[style]",
              "contents": -1
            },
            "labels": {
              "selector": "td:nth-child(2) > table > tr > td.embedded > span.tags"
            }
          }
        }
      }
    
  • 需要注意的是,如果你没有完成用户认证,通过插件配置进去的索引站点也是无法正常使用的。

  • 请不要添加对黄赌毒站点的支持,否则随时封闭接口。

7. 如何在插件中调用API接口

  • v1.8.4+ 在插件的数据页面支持GET/POSTAPI接口调用可调用插件自身、主程序或其它插件的API。
  • get_page中定义好元素的事件以及相应的API参数具体可参考插件豆瓣想看
{
  "component": "VDialogCloseBtn", // 触发事件的元素
  "events": {
    "click": { // 点击事件
      "api": "plugin/DoubanSync/delete_history", // API的相对路径
      "method": "get", // GET/POST
      "params": {
        // API上送参数
        "doubanid": ""
      }
    }
  }
}
  • 每次API调用完成后均会自动刷新一次插件数据页。

8. 如何将插件内容显示到仪表板?

  • v1.8.7+ 支持将插件的内容显示到仪表盘,并支持定义占据的单元格大小,插件产生的仪表板仅管理员可见。
    1. 根据插件需要展示的Widget内容规划展示内容的样式和规格也可设计多个规格样式并提供配置项供用户选择。
    1. 实现 get_dashboard_meta 方法定义仪表板key及名称支持一个插件有多个仪表板
def get_dashboard_meta(self) -> Optional[List[Dict[str, str]]]:
    """
    获取插件仪表盘元信息
    返回示例:
        [{
            "key": "dashboard1", // 仪表盘的key在当前插件范围唯一
            "name": "仪表盘1" // 仪表盘的名称
        }, {
            "key": "dashboard2",
            "name": "仪表盘2"
        }]
    """
    pass
    1. 实现 get_dashboard 方法根据key返回仪表盘的详细配置信息包括仪表盘的cols列配置适配不同屏幕以及仪表盘的页面配置json具体可参考插件站点数据统计
def get_dashboard(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]:
    """
    获取插件仪表盘页面需要返回1、仪表板col配置字典2、全局配置自动刷新等3、仪表板页面元素配置json含数据
    1、col配置参考
    {
        "cols": 12, "md": 6
    }
    2、全局配置参考
    {
        "refresh": 10, // 自动刷新时间,单位秒
        "border": True, // 是否显示边框默认True为False时取消组件边框和边距由插件自行控制
        "title": "组件标题", // 组件标题,如有将显示该标题,否则显示插件名称
        "subtitle": "组件子标题", // 组件子标题,缺省时不展示子标题
    }
    3、页面配置使用Vuetify组件拼装参考https://vuetifyjs.com/

    kwargs参数可获取的值1、user_agent浏览器UA

    :param key: 仪表盘key根据指定的key返回相应的仪表盘数据缺省时返回一个固定的仪表盘数据兼容旧版
    """
    pass

9. 如何发布插件版本?

  • 修改插件代码后,需要修改package.json中的version版本号MoviePilot才会提示用户有更新注意版本号需要与__init__.py文件中的plugin_version保持一致。
  • package.json中的level用于定义插件用户可见权限,1为所有用户可见,2为仅认证用户可见,3为需要密钥才可见一般用于测试。如果插件功能需要使用到站点则应该为2否则即使插件对用户可见但因为用户未认证相关功能也无法正常使用。
  • package.json中的history用于记录插件更新日志,格式如下:
{
  "history": {
    "v1.8": "修复空目录删除逻辑",
    "v1.7": "增加定时清理空目录功能"
  }
}
  • 新增加的插件请配置在package.json中的末尾,这样可被识别为最新增加,可用于用户排序。

10. 如何开发V2版本的插件以及实现插件多版本兼容

Description
MoviePilot官方插件市场
Readme GPL-3.0 45 MiB
Languages
Python 100%