├── .gitignore ├── LICENSE ├── README.md ├── data └── EmbyReporter │ └── res │ ├── PingFang Bold.ttf │ ├── bg │ ├── 0.jpg │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ └── 6.jpg │ └── cover-ranks-mask-2.png ├── docs ├── Cd2Assistant.md ├── CloudStrm.md ├── CloudStrmIncrement.md ├── CustomCommand.md ├── EmbyReporter.md ├── HomePage.md ├── Lucky.md ├── ShortPlayMonitor.md ├── StrmConvert.md └── WeChatForward.md ├── icons ├── 98tang.png ├── IMG_1929.png ├── SpeedLimiter.jpg ├── actor.png ├── actorsubscribeplus.png ├── audiobook.png ├── autosubtitles.jpeg ├── backup.png ├── bark.webp ├── broom.png ├── brush.jpg ├── chatgpt.png ├── chinesesubfinder.png ├── clean.png ├── cloud.png ├── cloudassistant.png ├── cloudcompanion.png ├── clouddisk.png ├── clouddrive.png ├── cloudflare.jpg ├── cloudstrm.png ├── code.png ├── command.png ├── convert.png ├── cookiecloud.png ├── copy_files.png ├── create.png ├── danmu.png ├── database.png ├── delete.png ├── directory.png ├── diskusage.jpg ├── douban.png ├── download.png ├── downloadmsg.png ├── emby-icon.png ├── emby.png ├── embyactorsync.png ├── extendtype.png ├── fileupload.png ├── forward.png ├── homepage.png ├── hosts.png ├── invites.png ├── iyuu.png ├── libraryduplicate.png ├── like.jpg ├── login.png ├── media.png ├── mediaplay.png ├── mediarelease.png ├── mediasyncdel.png ├── movie.jpg ├── nfo.png ├── opensubtitles.png ├── pluginupdate.png ├── popular.png ├── pushdeer.png ├── random.png ├── refresh.png ├── refresh2.png ├── regex.png ├── reinstall.png ├── reminder.png ├── removetorrent.png ├── rss.png ├── scraper.png ├── seed.png ├── signin.png ├── sitesafe.png ├── softlink.png ├── softlinkredirect.png ├── sqlite.png ├── statistic.png ├── subscribe_reminder.png ├── subscribeclear.png ├── subscribestatistic.png ├── sync.png ├── sync_file.png ├── synology.png ├── tag.png ├── teamwork.png ├── torrent.png ├── torrenttransfer.jpg ├── uninstall.png ├── unread.png ├── update.png ├── upload.png ├── webhook.png └── world.png ├── img ├── Cd2Assistant │ └── img.png ├── EmbyReporter │ ├── img.png │ └── img_1.png └── HomePage │ └── img.png ├── package.json ├── package.v2.json ├── plugins.v2 ├── autobackup │ └── __init__.py ├── cd2assistant │ ├── __init__.py │ └── requirements.txt ├── cloudlinkmonitor │ └── __init__.py ├── cloudstrmcompanion │ └── __init__.py ├── commandexecute │ └── __init__.py ├── downloadtorrent │ └── __init__.py ├── embyactorsync │ └── __init__.py ├── embyaudiobook │ └── __init__.py ├── embycollectionsort │ └── __init__.py ├── embydanmu │ └── __init__.py ├── embyextendtype │ └── __init__.py ├── embymetarefresh │ └── __init__.py ├── embymetatag │ └── __init__.py ├── embyreporter │ └── __init__.py ├── filesoftlink │ └── __init__.py ├── homepage │ └── __init__.py ├── libraryduplicatecheck │ └── __init__.py ├── mediarelease │ └── __init__.py ├── mediasyncdel │ └── __init__.py ├── moviepilotupdatenotify │ └── __init__.py ├── pluginreinstall │ └── __init__.py ├── pluginuninstall │ └── __init__.py ├── shortplaymonitor │ └── __init__.py ├── sqlexecute │ └── __init__.py ├── strmredirect │ └── __init__.py ├── subscribegroup │ └── __init__.py ├── synccookiecloud │ └── __init__.py ├── syncdownloadfiles │ └── __init__.py └── wechatforward │ └── __init__.py └── plugins ├── actorsubscribe └── __init__.py ├── actorsubscribeplus └── __init__.py ├── cd2assistant ├── __init__.py └── requirements.txt ├── cloudlinkmonitor └── __init__.py ├── cloudstrm └── __init__.py ├── cloudstrmapi └── __init__.py ├── cloudstrmincrement └── __init__.py ├── cloudstrmlocal └── __init__.py ├── cloudsyncdel └── __init__.py ├── commandexecute └── __init__.py ├── customcommand └── __init__.py ├── dirmonitorenhanced └── __init__.py ├── dockermanager └── __init__.py ├── downloadtorrent └── __init__.py ├── embyactorsync └── __init__.py ├── embyaudiobook └── __init__.py ├── embycollectionsort └── __init__.py ├── embydanmu └── __init__.py ├── embyextendtype └── __init__.py ├── embymetarefresh └── __init__.py ├── embymetatag └── __init__.py ├── embyreporter └── __init__.py ├── filecopy └── __init__.py ├── filesoftlink └── __init__.py ├── homepage └── __init__.py ├── libraryduplicatecheck └── __init__.py ├── linktosrc └── __init__.py ├── lucky └── __init__.py ├── mediarelease └── __init__.py ├── pluginautoupdate └── __init__.py ├── pluginreinstall └── __init__.py ├── pluginuninstall └── __init__.py ├── popularsubscribe └── __init__.py ├── removetorrent └── __init__.py ├── schedulereminder └── __init__.py ├── shortplaymonitor └── __init__.py ├── siteunreadmsg └── __init__.py ├── softlinkredirect └── __init__.py ├── sqlexecute └── __init__.py ├── strmconvert └── __init__.py ├── subscribeclear └── __init__.py ├── subscribegroup └── __init__.py ├── subscribereminder └── __init__.py ├── subscribestatistic └── __init__.py ├── synccookiecloud └── __init__.py ├── synologynotify └── __init__.py ├── urlredirect └── __init__.py └── wechatforward └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | .idea 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | test.py 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoviePilot-Plugin-Market 2 | 3 | MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ 4 | 5 | ### 插件列表 6 | 7 | 1. [自动备份 v1.3](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/autobackup) `自动备份数据和配置文件。` 8 | 2. [定时清理媒体库 v1.1](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/autoclean) `定时清理用户下载的种子、源文件、媒体库文件。` 9 | 3. [站点自动签到 v2.4](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/autosignin) `自动模拟登录、签到站点。` 10 | 4. [云盘文件删除 v1.3](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/clouddiskdel) `媒体库删除strm文件后同步删除云盘资源。` 11 | 5. [Cloudflare IP优选 v1.4](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/cloudflarespeedtest) `🌩 测试 Cloudflare CDN 延迟和速度,自动优选IP。` 12 | 6. [自定义Hosts v1.1](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/customhosts) `修改系统hosts文件,加速网络访问。` 13 | 7. [下载进度推送 v1.1](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/downloadingmsg) `定时推送正在下载进度。` 14 | 8. [药丸签到 v1.4](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/invitessignin) `药丸论坛签到。` 15 | 9. [媒体文件同步删除 v1.7](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/mediasyncdel) `同步删除历史记录、源文件和下载任务。` 16 | 10. [消息转发 v1.1](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/messageforward) `根据正则转发通知到其他WeChat应用。` 17 | 11. [MoviePilot更新推送 v1.4](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/moviepilotupdatenotify) `MoviePilot推送release更新通知、自动重启。` 18 | 12. [历史记录同步 v1.0](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/nastoolsync) `同步NAStool历史记录、下载记录、插件记录到MoviePilot。` 19 | 13. [站点自动更新 v1.2](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/siterefresh) `使用浏览器模拟登录站点获取Cookie和UA。` 20 | 14. [下载器文件同步 v1.1](https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/syncdownloadfiles) `同步下载器的文件信息到数据库,删除文件时联动删除下载任务。` 21 | 15. 站点数据统计 v1.4 (无未读消息版本)(废弃) 22 | 16. 站点未读消息 v1.9 (依赖于[站点数据统计]插件) `发送站点未读消息。` 23 | 17. [云盘Strm生成 v4.4](docs%2FCloudStrm.md) `监控文件创建,生成Strm文件。` 24 | 18. [云盘Strm生成(增量版) v1.1](docs%2FCloudStrmIncrement.md) `监控文件创建,生成Strm文件(增量版)。` 25 | 19. [Strm文件模式转换 v1.0](docs%2FStrmConvert.md) `Strm文件内容转为本地路径或者cd2/alist API路径。` 26 | 20. 清理订阅缓存 v1.0 `清理订阅已下载集数。` 27 | 21. 添加种子下载 v1.1 `选择下载器,添加种子任务。` 28 | 22. 删除站点种子 v1.2 `删除下载器中某站点种子。` 29 | 23. 插件更新管理 v1.9.2 `监测已安装插件,推送更新提醒,可配置自动更新。` 30 | 24. 插件强制重装 v1.7 `卸载当前插件,强制重装。` 31 | 25. 群辉Webhook通知 v1.1 `接收群辉webhook通知并推送。` 32 | 26. 同步CookieCloud v1.3 `同步MoviePilot站点Cookie到本地CookieCloud。` 33 | 27. 日程提醒 v1.0 `自定义提醒事项、提醒时间。` 34 | 28. 订阅提醒 v1.1 `推送当天订阅更新内容。` 35 | 29. [Emby观影报告 v2.0](docs%2FEmbyReporter.md) `推送Emby观影报告,需Emby安装Playback Report 插件。` 36 | 30. 演员订阅 v2.1.1 `自动订阅指定演员热映电影、电视剧。` 37 | 31. [短剧刮削 v3.2](docs%2FShortPlayMonitor.md) `监控视频短剧创建,刮削。` 38 | 32. 目录实时监控 v2.5 `监控云盘目录文件变化,自动转移媒体文件。` 39 | 33. 源文件恢复 v1.2 `根据MoviePilot的转移记录中的硬链文件恢复源文件。` 40 | 34. [微信消息转发 v2.8](docs%2FWeChatForward.md) `根据正则转发通知到其他WeChat应用。` 41 | 35. 订阅下载统计 v1.6 `统计指定时间内各站点订阅及下载情况。` 42 | 36. [自定义命令 v1.7](docs%2FCustomCommand.md) `自定义执行周期执行命令并推送结果。` 43 | 37. docker自定义任务 v1.3 `管理宿主机docker,自定义容器定时任务。` 44 | 38. 插件彻底卸载 v1.2 `删除数据库中已安装插件记录、清理插件文件。` 45 | 39. 实时软连接 v2.0.1 `监控目录文件变化,媒体文件软连接,其他文件可选复制。` 46 | 40. 订阅规则自动填充 v2.8 `电视剧下载后自动添加官组等信息到订阅;添加订阅后根据二级分类名称自定义订阅规则。` 47 | 41. Emby元数据刷新 v2.0.1 `定时刷新Emby媒体库元数据,演职人员中文。` 48 | 42. Emby媒体标签 v1.2 `自动给媒体库媒体添加标签。` 49 | 43. 热门媒体订阅 v1.7 `自定添加热门媒体到订阅。` 50 | 44. [HomePage v1.2](docs%2FHomePage.md) `HomePage自定义API。` 51 | 45. 目录监控(统一入库消息增强版)v1.0 `监控目录文件发生变化时实时整理到媒体库。(统一入库消息增强版)(测试中-.-)` 52 | 46. Sql执行器 v1.3 `自定义MoviePilot数据库Sql执行。` 53 | 47. 命令执行器 v1.2 `自定义容器命令执行。` 54 | 48. `-----------------------------` 55 | 49. [CloudDrive2助手v1.8.4](docs%2FCd2Assistant.md) `监控上传任务,检测是否有异常,发送通知。` 56 | 50. 软连接重定向 v1.2 `重定向软连接指向。` 57 | 51. 云盘同步删除 v1.5.4 `媒体库删除软连接文件后,同步删除云盘文件。` 58 | 52. 媒体库重复媒体检测 v1.9 `媒体库重复媒体检查,可选保留规则保留其一。` 59 | 53. 演员作品订阅 v1.1 `获取TMDB演员作品,并自动添加到订阅。` 60 | 54. 文件复制 v1.1 `自定义文件类型从源目录复制到目的目录。` 61 | 55. Emby合集媒体排序 v1.1 `Emby保留按照加入时间倒序的前提下,把合集中的媒体按照发布日期排序,修改加入时间已到达顺序排列的目的。` 62 | 56. 影视将映订阅 v1.1 `监控未上线影视作品,自动添加订阅。` 63 | 57. Emby视频类型检查 v1.0 `定期检查Emby媒体库中是否包含指定的视频类型,发送通知。` 64 | 58. Emby有声书整理 v1.3 `还在为Emby有声书整理烦恼吗?入库存在很多单集?` 65 | 59. Emby弹幕下载 v1.5 `通知Emby Danmu插件下载弹幕。` 66 | 60. Emby剧集演员同步 v1.5 `同步剧演员信息到集演员信息。` 67 | 61. 云盘Strm助手 v1.0.4 `实时监控、定时全量增量生成strm文件。` 68 | 62. Strm重定向 v1.0 `重写Strm文件内容。` -------------------------------------------------------------------------------- /data/EmbyReporter/res/PingFang Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/PingFang Bold.ttf -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/0.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/1.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/2.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/3.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/4.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/5.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/bg/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/bg/6.jpg -------------------------------------------------------------------------------- /data/EmbyReporter/res/cover-ranks-mask-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/data/EmbyReporter/res/cover-ranks-mask-2.png -------------------------------------------------------------------------------- /docs/Cd2Assistant.md: -------------------------------------------------------------------------------- 1 | # CloudDriver HomePage自定义API 2 | 3 | # 插件安装不上 4 | 容器内执行 `pip install clouddrive` 5 | 6 | ![img.png](../img/Cd2Assistant/img.png) 7 | 8 | HomePage services.yaml配置 9 | ```angular2html 10 | - CloudDrive2: 11 | icon: /icons/icon/clouddrive.png 12 | href: http://cd2ip:cd2端口/ 13 | ping: http://cd2ip:cd2端口 14 | #server: unraid 15 | #container: CloudDrive 16 | showStats: true 17 | display: list 18 | widget: 19 | type: customapi 20 | url: http://mp_ip:mp_port/api/v1/plugin/Cd2Assistant/homepage?apikey=mp_apikey 21 | method: GET 22 | mappings: 23 | # - field: uptime 24 | # label: 运行时间 25 | - field: upload_count 26 | label: 上传数量 27 | - field: upload_speed 28 | label: 上传速度 29 | - field: download_count 30 | label: 下载数量 31 | - field: download_speed 32 | label: 下载速度 33 | 34 | # - field: cloud_space 35 | # label: 剩余空间 36 | ``` 37 | 38 | ### 自定义API Response字段 39 | - cpuUsage: CPU使用率 40 | - memUsageKB: 内存使用量 41 | - uptime: 运行时间 42 | - upload_count: 上传数量 43 | - upload_speed: 上传速度 44 | - download_count: 下载数量 45 | - download_speed: 下载速度 46 | - cloud_space: 剩余空间 47 | 48 | ### HomePage自定义API文档 49 | https://gethomepage.dev/latest/widgets/services/customapi/#custom-request-body -------------------------------------------------------------------------------- /docs/CloudStrm.md: -------------------------------------------------------------------------------- 1 | # 云盘strm生成 2 | 3 | ### 使用说明 4 | 5 | 目录监控格式: 6 | 7 | - 1.监控目录#目的目录#媒体服务器内源文件路径 8 | - 2.监控目录#目的目录#cd2#cd2挂载本地跟路径#cd2服务地址 9 | - 3.监控目录#目的目录#alist#alist挂载本地跟路径#alist服务地址 10 | 11 | 路径: 12 | 13 | - 监控目录:源文件目录即云盘挂载到MoviePilot中的路径 14 | - 目的路径:MoviePilot中strm生成路径 15 | - 媒体服务器内源文件路径:源文件目录即云盘挂载到媒体服务器的路径 16 | 17 | 示例: 18 | 19 | - MoviePilot上云盘源文件路径 /mount/cloud/aliyun/emby`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 20 | 21 | - MoviePilot上strm生成路径 /mnt/link/aliyun`/tvshow/爸爸去哪儿/Season 5/14.特别版.strm` 22 | 23 | - 媒体服务器内源文件路径 /mount/cloud/aliyun/emby`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 24 | 25 | - 监控配置为:/mount/cloud/aliyun/emby#/mnt/link/aliyun#/mount/cloud/aliyun/emby 26 | -------------------------------------------------------------------------------- /docs/CloudStrmIncrement.md: -------------------------------------------------------------------------------- 1 | # 云盘strm生成(增量版) 2 | 3 | ### 使用说明 4 | 5 | 目录监控格式: 6 | 7 | - 1.增量目录#监控目录#目的目录#媒体服务器内源文件路径 8 | - 2.增量目录#监控目录#目的目录#cd2#cd2挂载本地跟路径#cd2服务地址 9 | - 3.增量目录#监控目录#目的目录#alist#alist挂载本地跟路径#alist服务地址 10 | 11 | 路径: 12 | 13 | - 增量目录:转存到云盘的路径,插件只会扫描该路径下的文件,移动到监控路径,生成目的路径的strm文件 14 | - 监控目录:源文件目录即云盘挂载到MoviePilot中的路径 15 | - 目的路径:MoviePilot中strm生成路径 16 | - 媒体服务器内源文件路径:源文件目录即云盘挂载到媒体服务器的路径 17 | 18 | 示例: 19 | 20 | - 增量目录:/increment`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 21 | 22 | - MoviePilot上云盘源文件路径 /mount/cloud/aliyun/emby`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 23 | 24 | - MoviePilot上strm生成路径 /mnt/link/aliyun`/tvshow/爸爸去哪儿/Season 5/14.特别版.strm` 25 | 26 | - 媒体服务器内源文件路径 /mount/cloud/aliyun/emby`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 27 | 28 | - 监控配置为:/increment#/mount/cloud/aliyun/emby#/mnt/link/aliyun#/mount/cloud/aliyun/emby 29 | 30 | 31 | 保留路径: 32 | 33 | 扫描到增量目录的文件,会移动到监控目录,并生成目的路径的strm文件,删除空的增量目录,如果想保留某些父目录,可以将它们添加到保留路径中。 34 | 35 | 例如: 36 | 37 | /increment/series/庆余年/Season 1/1.第一集.mp4 38 | 39 | 保留路径为series 40 | 41 | 则文件移动到目的路径名后,会删除庆余年/Season 1,父路径/increment/series保留 42 | 43 | -------------------------------------------------------------------------------- /docs/CustomCommand.md: -------------------------------------------------------------------------------- 1 | # 自定义命令 2 | 3 | ### 使用说明 4 | 5 | 默认把python脚本最后一个print作为返回值 6 | 7 | 命令名#0 9 * * *#python main.py 8 | 命令名#0 9 * * *#python main.py#1-600 9 | 10 | 11 | 1-600为随机延时,单位秒 -------------------------------------------------------------------------------- /docs/EmbyReporter.md: -------------------------------------------------------------------------------- 1 | # Emby观影报告 2 | 3 | ### 使用说明 4 | 5 | **注意**:需 `Emby` 安装 `Playback Report` 插件 6 | 7 | 将本项目**下载到本地**,并将 `/data/EmbyReporter/res` 下文件路径映射到 `MoviePilot` 容器可访问的目录下,如 `/config/plugins/EmbyReporter` 8 | 9 |
10 | 具体步骤 11 | 12 | 1. 下载源码:`git clone https://github.com/thsrite/MoviePilot-Plugins.git` 或者从网页直接下载并解压 13 | 2. 复制 `/data/EmbyReporter/res` 到容器可访问目录,如 `/config/plugins/EmbyReporter` 14 | 3. 配置该插件的素材路径 `/config/plugins/EmbyReporter/`,如下面图中所示 15 | 4. 立即运行一次,如果网络正常,`tg` 通道已配置的话,`tg` 即可收到推送 16 | 17 |
18 | 19 | ![img.png](../img/EmbyReporter/img.png) 20 | ![img_1.png](../img/EmbyReporter/img_1.png) 21 | 22 | -------------------------------------------------------------------------------- /docs/HomePage.md: -------------------------------------------------------------------------------- 1 | # HomePage自定义API 2 | 3 | ![img.png](../img/HomePage/img.png) 4 | 5 | HomePage services.yaml配置 6 | ```angular2html 7 | - Media: 8 | - MoviePilot: 9 | icon: /icons/icon/MoviePilot.png 10 | href: http://MoviePilot_IP:NGINX_PORT 11 | ping: http://MoviePilot_IP:NGINX_PORT 12 | # server: unraid 13 | # container: MoviePilot 14 | showStats: true 15 | widget: 16 | type: customapi 17 | url: http://MoviePilot_IP:NGINX_PORT/api/v1/plugin/HomePage/statistic?apikey=api_token 18 | method: GET 19 | mappings: 20 | - field: movie_subscribes 21 | label: 电影订阅 22 | - field: tv_subscribes 23 | label: 电视剧订阅 24 | - field: movie_count 25 | label: 电影数量 26 | - field: tv_count 27 | label: 电视剧数量 28 | # - field: episode_count 29 | # label: 电影剧集数量 30 | # - field: user_count 31 | # label: 用户数量 32 | # - field: total_storage 33 | # label: 总空间 34 | # - field: free_storage 35 | # label: 剩余空间 36 | # - field: now_tasks 37 | ``` 38 | 39 | ### 自定义API Response字段 40 | - movie_subscribes: 电影订阅 41 | - tv_subscribes: 电视剧订阅 42 | - movie_count: 电影数量 43 | - tv_count: 电视剧数量 44 | - episode_count: 电影剧集数量 45 | - user_count: 用户数量 46 | - total_storage: 总空间 47 | - used_storage: 已用空间 48 | - free_storage: 剩余空间 49 | 50 | ### HomePage自定义API文档 51 | https://gethomepage.dev/latest/widgets/services/customapi/#custom-request-body -------------------------------------------------------------------------------- /docs/Lucky.md: -------------------------------------------------------------------------------- 1 | # Lucky HomePage自定义API 2 | 3 | ![img.png](../img/HomePage/img.png) 4 | 5 | HomePage services.yaml配置 6 | ```angular2html 7 | - Media: 8 | - Lucky: 9 | icon: /icons/icon/lucky.png 10 | href: http://lucky_ip:lucky_port 11 | ping: http://lucky_ip:lucky_port 12 | # server: unraid 13 | # container: lucky 14 | showStats: true 15 | widget: 16 | type: customapi 17 | url: http://MoviePilot_IP:NGINX_PORT/api/v1/plugin/Lucky/lucky?apikey=api_token 18 | method: GET 19 | mappings: 20 | - field: enabled_cnt 21 | label: 启用配置数量 22 | - field: closed_cnt 23 | label: 关闭配置数量 24 | - field: ipaddr 25 | label: 公网ip地址 26 | - field: expire_time 27 | label: 证书过期日期 28 | - field: total_cnt 29 | label: 总配置数量 30 | # - field: connections 31 | # label: 链接数 32 | # - field: trafficIn 33 | # label: 流量In 34 | # - field: trafficOut 35 | # label: 流量Out 36 | ``` 37 | 38 | ### HomePage自定义API文档 39 | https://gethomepage.dev/latest/widgets/services/customapi/#custom-request-body -------------------------------------------------------------------------------- /docs/ShortPlayMonitor.md: -------------------------------------------------------------------------------- 1 | # 短剧刮削 2 | 3 | ### 使用说明 4 | 5 | 监控方式: 6 | 7 | - fast:性能模式,内部处理系统操作类型选择最优解 8 | - compatibility:兼容模式,目录同步性能降低且NAS不能休眠,但可以兼容挂载的远程共享目录如SMB (建议使用) 9 | 10 | 是否重命名 11 | 12 | - true 自定义识别词 13 | - false 14 | - smart 我看着取 (尝试从agsv、萝莉站获取封面) 15 | 16 | 封面比例: 17 | 2:3 -------------------------------------------------------------------------------- /docs/StrmConvert.md: -------------------------------------------------------------------------------- 1 | # Strm文件模式转换 2 | 3 | ### 使用说明 4 | 5 | #### 本地模式 6 | - MoviePilot上strm视频路径 /mnt/link/aliyun`/tvshow/爸爸去哪儿/Season 5/14.特别版.strm` 7 | - 云盘源文件挂载本地后 挂载`进媒体服务器的路径`,与上方对应 /mount/cloud/aliyun/emby`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 8 | 9 | - 转换配置为:`/mnt/link/aliyun#/mount/cloud/aliyun/emby` 10 | 11 | #### API模式 12 | - MoviePilot上strm视频根路径 /mnt/link/aliyun`/tvshow/爸爸去哪儿/Season 5/14.特别版.strm` 13 | - cd2挂载后路径 /aliyun/emby`/tvshow/爸爸去哪儿/Season 5/14.特别版.mp4` 14 | 15 | - 转换配置为:`/mnt/link/aliyun#/aliyun/emby#cd2#192.168.31.103:19798` 16 | 17 | 18 | ## 具体自己多尝试吧。 -------------------------------------------------------------------------------- /docs/WeChatForward.md: -------------------------------------------------------------------------------- 1 | # 微信消息转发 2 | 3 | ### 使用说明 4 | 5 | #### 消息转发插件加强版 6 | 7 | 根据正则表达式将对应title的消息转发到不同的企业微信应用上 8 | 9 | 企业微信应用配置与正则表达式一一对应(一行对应一行) 10 | 11 | 如果某条消息不想指定userid发送,则填写忽略userid正则表达式. 12 | 13 | #### 额外消息配置 14 | 15 | `开始下载 > userid > 后台下载任务已提交,请耐心等候入库通知。 > appid` 16 | 17 | `已添加订阅 > userid > 电视剧正在更新,已添加订阅,待更新后自动下载。 > appid` 18 | 19 | 中间用` > `分割 20 | 21 | 消息title匹配到`开始下载`的正则 22 | 23 | 且消息text中的`用户:`匹配到userid, 24 | 25 | 则发送`后台下载任务已提交,请耐心等候入库通知。`额外通知。 26 | 27 | 发送给appid为`appid`的企业微信应用。(环境变量配置或者本插件配置均可。) 28 | 29 | #### 特定消息指定用户 30 | 31 | `title正则 > text|title正则 > userid` 32 | 33 | 当要发送的消息的`title`和`text|title`均匹配正则,则强制指定该消息的userid 34 | 35 | #### 2.0版本兼容旧版配置 36 | 37 | 如更新到2.0版本,设置微信配置界面配置没有格式化,无须担心,重启下即可(不重启功能也正常使用)。 -------------------------------------------------------------------------------- /icons/98tang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/98tang.png -------------------------------------------------------------------------------- /icons/IMG_1929.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/IMG_1929.png -------------------------------------------------------------------------------- /icons/SpeedLimiter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/SpeedLimiter.jpg -------------------------------------------------------------------------------- /icons/actor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/actor.png -------------------------------------------------------------------------------- /icons/actorsubscribeplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/actorsubscribeplus.png -------------------------------------------------------------------------------- /icons/audiobook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/audiobook.png -------------------------------------------------------------------------------- /icons/autosubtitles.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/autosubtitles.jpeg -------------------------------------------------------------------------------- /icons/backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/backup.png -------------------------------------------------------------------------------- /icons/bark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/bark.webp -------------------------------------------------------------------------------- /icons/broom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/broom.png -------------------------------------------------------------------------------- /icons/brush.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/brush.jpg -------------------------------------------------------------------------------- /icons/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/chatgpt.png -------------------------------------------------------------------------------- /icons/chinesesubfinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/chinesesubfinder.png -------------------------------------------------------------------------------- /icons/clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/clean.png -------------------------------------------------------------------------------- /icons/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/cloud.png -------------------------------------------------------------------------------- /icons/cloudassistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/cloudassistant.png -------------------------------------------------------------------------------- /icons/cloudcompanion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/cloudcompanion.png -------------------------------------------------------------------------------- /icons/clouddisk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/clouddisk.png -------------------------------------------------------------------------------- /icons/clouddrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/clouddrive.png -------------------------------------------------------------------------------- /icons/cloudflare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/cloudflare.jpg -------------------------------------------------------------------------------- /icons/cloudstrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/cloudstrm.png -------------------------------------------------------------------------------- /icons/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/code.png -------------------------------------------------------------------------------- /icons/command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/command.png -------------------------------------------------------------------------------- /icons/convert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/convert.png -------------------------------------------------------------------------------- /icons/cookiecloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/cookiecloud.png -------------------------------------------------------------------------------- /icons/copy_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/copy_files.png -------------------------------------------------------------------------------- /icons/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/create.png -------------------------------------------------------------------------------- /icons/danmu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/danmu.png -------------------------------------------------------------------------------- /icons/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/database.png -------------------------------------------------------------------------------- /icons/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/delete.png -------------------------------------------------------------------------------- /icons/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/directory.png -------------------------------------------------------------------------------- /icons/diskusage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/diskusage.jpg -------------------------------------------------------------------------------- /icons/douban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/douban.png -------------------------------------------------------------------------------- /icons/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/download.png -------------------------------------------------------------------------------- /icons/downloadmsg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/downloadmsg.png -------------------------------------------------------------------------------- /icons/emby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/emby-icon.png -------------------------------------------------------------------------------- /icons/emby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/emby.png -------------------------------------------------------------------------------- /icons/embyactorsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/embyactorsync.png -------------------------------------------------------------------------------- /icons/extendtype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/extendtype.png -------------------------------------------------------------------------------- /icons/fileupload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/fileupload.png -------------------------------------------------------------------------------- /icons/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/forward.png -------------------------------------------------------------------------------- /icons/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/homepage.png -------------------------------------------------------------------------------- /icons/hosts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/hosts.png -------------------------------------------------------------------------------- /icons/invites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/invites.png -------------------------------------------------------------------------------- /icons/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/iyuu.png -------------------------------------------------------------------------------- /icons/libraryduplicate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/libraryduplicate.png -------------------------------------------------------------------------------- /icons/like.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/like.jpg -------------------------------------------------------------------------------- /icons/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/login.png -------------------------------------------------------------------------------- /icons/media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/media.png -------------------------------------------------------------------------------- /icons/mediaplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/mediaplay.png -------------------------------------------------------------------------------- /icons/mediarelease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/mediarelease.png -------------------------------------------------------------------------------- /icons/mediasyncdel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/mediasyncdel.png -------------------------------------------------------------------------------- /icons/movie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/movie.jpg -------------------------------------------------------------------------------- /icons/nfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/nfo.png -------------------------------------------------------------------------------- /icons/opensubtitles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/opensubtitles.png -------------------------------------------------------------------------------- /icons/pluginupdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/pluginupdate.png -------------------------------------------------------------------------------- /icons/popular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/popular.png -------------------------------------------------------------------------------- /icons/pushdeer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/pushdeer.png -------------------------------------------------------------------------------- /icons/random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/random.png -------------------------------------------------------------------------------- /icons/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/refresh.png -------------------------------------------------------------------------------- /icons/refresh2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/refresh2.png -------------------------------------------------------------------------------- /icons/regex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/regex.png -------------------------------------------------------------------------------- /icons/reinstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/reinstall.png -------------------------------------------------------------------------------- /icons/reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/reminder.png -------------------------------------------------------------------------------- /icons/removetorrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/removetorrent.png -------------------------------------------------------------------------------- /icons/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/rss.png -------------------------------------------------------------------------------- /icons/scraper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/scraper.png -------------------------------------------------------------------------------- /icons/seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/seed.png -------------------------------------------------------------------------------- /icons/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/signin.png -------------------------------------------------------------------------------- /icons/sitesafe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/sitesafe.png -------------------------------------------------------------------------------- /icons/softlink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/softlink.png -------------------------------------------------------------------------------- /icons/softlinkredirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/softlinkredirect.png -------------------------------------------------------------------------------- /icons/sqlite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/sqlite.png -------------------------------------------------------------------------------- /icons/statistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/statistic.png -------------------------------------------------------------------------------- /icons/subscribe_reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/subscribe_reminder.png -------------------------------------------------------------------------------- /icons/subscribeclear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/subscribeclear.png -------------------------------------------------------------------------------- /icons/subscribestatistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/subscribestatistic.png -------------------------------------------------------------------------------- /icons/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/sync.png -------------------------------------------------------------------------------- /icons/sync_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/sync_file.png -------------------------------------------------------------------------------- /icons/synology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/synology.png -------------------------------------------------------------------------------- /icons/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/tag.png -------------------------------------------------------------------------------- /icons/teamwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/teamwork.png -------------------------------------------------------------------------------- /icons/torrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/torrent.png -------------------------------------------------------------------------------- /icons/torrenttransfer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/torrenttransfer.jpg -------------------------------------------------------------------------------- /icons/uninstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/uninstall.png -------------------------------------------------------------------------------- /icons/unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/unread.png -------------------------------------------------------------------------------- /icons/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/update.png -------------------------------------------------------------------------------- /icons/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/upload.png -------------------------------------------------------------------------------- /icons/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/webhook.png -------------------------------------------------------------------------------- /icons/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/icons/world.png -------------------------------------------------------------------------------- /img/Cd2Assistant/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/img/Cd2Assistant/img.png -------------------------------------------------------------------------------- /img/EmbyReporter/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/img/EmbyReporter/img.png -------------------------------------------------------------------------------- /img/EmbyReporter/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/img/EmbyReporter/img_1.png -------------------------------------------------------------------------------- /img/HomePage/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/6b182a441ac1a324470de0d901c7b8725609068a/img/HomePage/img.png -------------------------------------------------------------------------------- /plugins.v2/cd2assistant/requirements.txt: -------------------------------------------------------------------------------- 1 | clouddrive 2 | lz4 -------------------------------------------------------------------------------- /plugins.v2/commandexecute/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from app.core.event import eventmanager, Event 4 | from app.plugins import _PluginBase 5 | from typing import Any, List, Dict, Tuple 6 | from app.log import logger 7 | from app.schemas.types import EventType, MessageChannel 8 | 9 | 10 | class CommandExecute(_PluginBase): 11 | # 插件名称 12 | plugin_name = "命令执行器" 13 | # 插件描述 14 | plugin_desc = "自定义容器命令执行。" 15 | # 插件图标 16 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/command.png" 17 | # 插件版本 18 | plugin_version = "1.3" 19 | # 插件作者 20 | plugin_author = "thsrite" 21 | # 作者主页 22 | author_url = "https://github.com/thsrite" 23 | # 插件配置项ID前缀 24 | plugin_config_prefix = "commandexecute_" 25 | # 加载顺序 26 | plugin_order = 99 27 | # 可使用的用户级别 28 | auth_level = 1 29 | 30 | # 私有属性 31 | _onlyonce = None 32 | _command = None 33 | 34 | def init_plugin(self, config: dict = None): 35 | if config: 36 | self._onlyonce = config.get("onlyonce") 37 | self._command = config.get("command") 38 | 39 | if self._onlyonce and self._command: 40 | # 执行SQL语句 41 | try: 42 | for command in self._command.split("\n"): 43 | logger.info(f"开始执行命令 {command}") 44 | ouptut = self.execute_command(command) 45 | # logger.info('\n'.join(ouptut)) 46 | except Exception as e: 47 | logger.error(f"命令执行失败 {str(e)}") 48 | return 49 | finally: 50 | self._onlyonce = False 51 | self.update_config({ 52 | "onlyonce": self._onlyonce, 53 | "command": self._command 54 | }) 55 | 56 | @staticmethod 57 | def execute_command(command: str): 58 | """ 59 | 执行命令 60 | :param command: 命令 61 | """ 62 | result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 63 | ouptut = [] 64 | while True: 65 | error = result.stderr.readline().decode("utf-8") 66 | if error == '' and result.poll() is not None: 67 | break 68 | if error: 69 | logger.info(error.strip()) 70 | ouptut.append(error.strip()) 71 | while True: 72 | output = result.stdout.readline().decode("utf-8") 73 | if output == '' and result.poll() is not None: 74 | break 75 | if output: 76 | logger.info(output.strip()) 77 | ouptut.append(output.strip()) 78 | 79 | return ouptut 80 | 81 | @eventmanager.register(EventType.PluginAction) 82 | def execute(self, event: Event = None): 83 | if event: 84 | event_data = event.event_data 85 | if not event_data or event_data.get("action") != "command_execute": 86 | return 87 | logger.info(f"收到命令执行事件 ...{event_data}") 88 | args = event_data.get("arg_str") 89 | if not args: 90 | return 91 | 92 | logger.info(f"收到命令,开始执行命令 ...{args}") 93 | ouptut = self.execute_command(args) 94 | result = '\n'.join(ouptut) 95 | 96 | if event.event_data.get("channel") == MessageChannel.Telegram: 97 | result = f"```plaintext\n{result}\n```" 98 | self.post_message(channel=event.event_data.get("channel"), 99 | title="命令执行结果", 100 | text=result, 101 | userid=event.event_data.get("user")) 102 | 103 | def get_state(self) -> bool: 104 | return True 105 | 106 | @staticmethod 107 | def get_command() -> List[Dict[str, Any]]: 108 | """ 109 | 定义远程控制命令 110 | :return: 命令关键字、事件、描述、附带数据 111 | """ 112 | return [{ 113 | "cmd": "/cmd", 114 | "event": EventType.PluginAction, 115 | "desc": "自定义命令执行", 116 | "category": "", 117 | "data": { 118 | "action": "command_execute" 119 | } 120 | }] 121 | 122 | def get_api(self) -> List[Dict[str, Any]]: 123 | pass 124 | 125 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 126 | """ 127 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 128 | """ 129 | return [ 130 | { 131 | 'component': 'VForm', 132 | 'content': [ 133 | { 134 | 'component': 'VRow', 135 | 'content': [ 136 | { 137 | 'component': 'VCol', 138 | 'props': { 139 | 'cols': 12, 140 | 'md': 6 141 | }, 142 | 'content': [ 143 | { 144 | 'component': 'VSwitch', 145 | 'props': { 146 | 'model': 'onlyonce', 147 | 'label': '执行命令' 148 | } 149 | } 150 | ] 151 | } 152 | ] 153 | }, 154 | { 155 | 'component': 'VRow', 156 | 'content': [ 157 | { 158 | 'component': 'VCol', 159 | 'props': { 160 | 'cols': 12, 161 | }, 162 | 'content': [ 163 | { 164 | 'component': 'VTextarea', 165 | 'props': { 166 | 'model': 'command', 167 | 'rows': '2', 168 | 'label': 'command命令', 169 | 'placeholder': '一行一条' 170 | } 171 | } 172 | ] 173 | } 174 | ] 175 | }, 176 | { 177 | 'component': 'VRow', 178 | 'content': [ 179 | { 180 | 'component': 'VCol', 181 | 'props': { 182 | 'cols': 12, 183 | }, 184 | 'content': [ 185 | { 186 | 'component': 'VAlert', 187 | 'props': { 188 | 'type': 'info', 189 | 'variant': 'tonal' 190 | }, 191 | 'content': [ 192 | { 193 | 'component': 'span', 194 | 'text': '执行日志将会输出到控制台,请谨慎操作。' 195 | } 196 | ] 197 | } 198 | ] 199 | } 200 | ] 201 | }, 202 | { 203 | 'component': 'VRow', 204 | 'content': [ 205 | { 206 | 'component': 'VCol', 207 | 'props': { 208 | 'cols': 12, 209 | }, 210 | 'content': [ 211 | { 212 | 'component': 'VAlert', 213 | 'props': { 214 | 'type': 'info', 215 | 'variant': 'tonal' 216 | }, 217 | 'content': [ 218 | { 219 | 'component': 'span', 220 | 'text': '可使用交互命令/cmd ls' 221 | } 222 | ] 223 | } 224 | ] 225 | } 226 | ] 227 | } 228 | ] 229 | } 230 | ], { 231 | "onlyonce": False, 232 | "command": "", 233 | } 234 | 235 | def get_page(self) -> List[dict]: 236 | pass 237 | 238 | def stop_service(self): 239 | """ 240 | 退出插件 241 | """ 242 | pass 243 | -------------------------------------------------------------------------------- /plugins.v2/pluginuninstall/__init__.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | from app.core.config import settings 5 | from app.core.plugin import PluginManager 6 | from app.db.systemconfig_oper import SystemConfigOper 7 | from app.plugins import _PluginBase 8 | from typing import Any, List, Dict, Tuple 9 | from app.log import logger 10 | from app.scheduler import Scheduler 11 | from app.schemas.types import SystemConfigKey 12 | 13 | 14 | class PluginUnInstall(_PluginBase): 15 | # 插件名称 16 | plugin_name = "插件彻底卸载" 17 | # 插件描述 18 | plugin_desc = "删除数据库中已安装插件记录、清理插件文件。" 19 | # 插件图标 20 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/uninstall.png" 21 | # 插件版本 22 | plugin_version = "2.2" 23 | # 插件作者 24 | plugin_author = "thsrite" 25 | # 作者主页 26 | author_url = "https://github.com/thsrite" 27 | # 插件配置项ID前缀 28 | plugin_config_prefix = "pluginuninstall_" 29 | # 加载顺序 30 | plugin_order = 98 31 | # 可使用的用户级别 32 | auth_level = 1 33 | 34 | # 私有属性 35 | _plugin_ids = [] 36 | _clear_config = False 37 | _clear_data = False 38 | 39 | def init_plugin(self, config: dict = None): 40 | if config: 41 | self._plugin_ids = config.get("plugin_ids") or [] 42 | self._clear_config = config.get("clear_config") 43 | self._clear_data = config.get("clear_data") 44 | if not self._plugin_ids: 45 | return 46 | 47 | # 已安装插件 48 | install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or [] 49 | 50 | new_install_plugins = [] 51 | for install_plugin in install_plugins: 52 | if install_plugin in self._plugin_ids: 53 | # 移除插件服务 54 | Scheduler().remove_plugin_job(install_plugin) 55 | # 移除插件 56 | PluginManager().remove_plugin(install_plugin) 57 | # 删除插件文件 58 | plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / install_plugin.lower() 59 | if plugin_dir.exists(): 60 | shutil.rmtree(plugin_dir, ignore_errors=True) 61 | if self._clear_config: 62 | # 删除配置 63 | PluginManager().delete_plugin_config(install_plugin) 64 | if self._clear_data: 65 | # 删除插件所有数据 66 | PluginManager().delete_plugin_data(install_plugin) 67 | logger.info(f"插件 {install_plugin} 已卸载") 68 | else: 69 | new_install_plugins.append(install_plugin) 70 | 71 | # 保存已安装插件 72 | SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, new_install_plugins) 73 | 74 | self.update_config({ 75 | "plugin_ids": [], 76 | "clear_config": self._clear_config, 77 | "clear_data": self._clear_data 78 | }) 79 | 80 | def get_state(self) -> bool: 81 | return False 82 | 83 | @staticmethod 84 | def get_command() -> List[Dict[str, Any]]: 85 | pass 86 | 87 | def get_api(self) -> List[Dict[str, Any]]: 88 | pass 89 | 90 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 91 | """ 92 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 93 | """ 94 | # 直接调用修复后的 get_local_plugins 获取选项 95 | pluginOptions = self.get_local_plugins() 96 | return [ 97 | { 98 | 'component': 'VForm', 99 | 'content': [ 100 | { 101 | 'component': 'VRow', 102 | 'content': [ 103 | { 104 | 'component': 'VCol', 105 | 'props': { 106 | 'cols': 12, 107 | 'md': 3 108 | }, 109 | 'content': [ 110 | { 111 | 'component': 'VSwitch', 112 | 'props': { 113 | 'model': 'clear_config', 114 | 'label': '清除配置(配置信息)', 115 | } 116 | } 117 | ] 118 | }, 119 | { 120 | 'component': 'VCol', 121 | 'props': { 122 | 'cols': 12, 123 | 'md': 3 124 | }, 125 | 'content': [ 126 | { 127 | 'component': 'VSwitch', 128 | 'props': { 129 | 'model': 'clear_data', 130 | 'label': '清除数据(运行数据)', 131 | } 132 | } 133 | ] 134 | }, 135 | { 136 | 'component': 'VCol', 137 | 'props': { 138 | 'cols': 12, 139 | 'md': 6 140 | }, 141 | 'content': [ 142 | { 143 | 'component': 'VSelect', 144 | 'props': { 145 | 'multiple': True, 146 | 'chips': True, 147 | 'model': 'plugin_ids', 148 | 'label': '卸载插件', 149 | 'items': pluginOptions 150 | } 151 | } 152 | ] 153 | }, 154 | ] 155 | }, 156 | { 157 | 'component': 'VRow', 158 | 'content': [ 159 | { 160 | 'component': 'VCol', 161 | 'props': { 162 | 'cols': 12, 163 | }, 164 | 'content': [ 165 | { 166 | 'component': 'VAlert', 167 | 'props': { 168 | 'type': 'info', 169 | 'variant': 'tonal', 170 | 'text': '删除数据库中已安装插件记录、清理插件文件。' 171 | } 172 | } 173 | ] 174 | } 175 | ] 176 | }, 177 | ] 178 | } 179 | ], { 180 | "plugin_ids": [], 181 | "clear_config": False, 182 | "clear_data": False 183 | } 184 | 185 | @staticmethod 186 | def get_local_plugins(): 187 | """ 188 | 获取本地插件 189 | (修改为只获取已安装插件,避免 compare_version 和市场查询) 190 | """ 191 | plugin_manager = PluginManager() 192 | # 获取本地所有插件实例 193 | local_plugin_instances = plugin_manager.get_local_plugins() or [] 194 | 195 | # 过滤出已安装的插件 196 | installed_plugins = [p for p in local_plugin_instances if getattr(p, 'installed', False)] 197 | 198 | # 根据插件顺序排序 (可选) 199 | sorted_plugins = sorted(installed_plugins, key=lambda p: getattr(p, 'plugin_order', 1000)) 200 | 201 | # 构建 VSelect 需要的选项列表 202 | plugin_options = [] 203 | for plugin in sorted_plugins: 204 | # 确保 getattr 有默认值 205 | plugin_name = getattr(plugin, 'plugin_name', getattr(plugin, 'id', '未知插件')) 206 | plugin_version = getattr(plugin, 'plugin_version', 'N/A') 207 | plugin_id = getattr(plugin, 'id', None) 208 | if plugin_id: 209 | plugin_options.append({ 210 | "title": f"{plugin_name} v{plugin_version}", 211 | "value": plugin_id 212 | }) 213 | 214 | return plugin_options 215 | 216 | def get_page(self) -> List[dict]: 217 | pass 218 | 219 | def stop_service(self): 220 | """ 221 | 退出插件 222 | """ 223 | pass -------------------------------------------------------------------------------- /plugins.v2/sqlexecute/__init__.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from app.core.event import eventmanager, Event 4 | from app.plugins import _PluginBase 5 | from typing import Any, List, Dict, Tuple 6 | from app.log import logger 7 | from app.schemas.types import EventType, MessageChannel 8 | 9 | 10 | class SqlExecute(_PluginBase): 11 | # 插件名称 12 | plugin_name = "Sql执行器" 13 | # 插件描述 14 | plugin_desc = "自定义MoviePilot数据库Sql执行。" 15 | # 插件图标 16 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/sqlite.png" 17 | # 插件版本 18 | plugin_version = "1.4" 19 | # 插件作者 20 | plugin_author = "thsrite" 21 | # 作者主页 22 | author_url = "https://github.com/thsrite" 23 | # 插件配置项ID前缀 24 | plugin_config_prefix = "sqlexecute_" 25 | # 加载顺序 26 | plugin_order = 99 27 | # 可使用的用户级别 28 | auth_level = 1 29 | 30 | # 私有属性 31 | _onlyonce = None 32 | _sql = None 33 | 34 | def init_plugin(self, config: dict = None): 35 | if config: 36 | self._onlyonce = config.get("onlyonce") 37 | self._sql = config.get("sql") 38 | 39 | if self._onlyonce and self._sql: 40 | # 读取sqlite数据 41 | try: 42 | gradedb = sqlite3.connect("/config/user.db") 43 | except Exception as e: 44 | logger.error(f"数据库链接失败 {str(e)}") 45 | return 46 | 47 | # 创建游标cursor来执行executeSQL语句 48 | cursor = gradedb.cursor() 49 | 50 | # 执行SQL语句 51 | try: 52 | for sql in self._sql.split("\n"): 53 | logger.info(f"开始执行SQL语句 {sql}") 54 | # 执行SQL语句 55 | cursor.execute(sql) 56 | 57 | if 'select' in sql.lower(): 58 | rows = cursor.fetchall() 59 | # 获取列名 60 | columns = [desc[0] for desc in cursor.description] 61 | # 将查询结果转换为key-value对的列表 62 | results = [] 63 | for row in rows: 64 | result = dict(zip(columns, row)) 65 | results.append(result) 66 | result = "\n".join([str(i) for i in results]) 67 | result = str(result).replace("'", "\"") 68 | logger.info(result) 69 | else: 70 | gradedb.commit() 71 | result = f"执行成功,影响行数:{cursor.rowcount}" 72 | logger.info(result) 73 | except Exception as e: 74 | logger.error(f"SQL语句执行失败 {str(e)}") 75 | return 76 | finally: 77 | # 关闭游标 78 | cursor.close() 79 | 80 | self._onlyonce = False 81 | self.update_config({ 82 | "onlyonce": self._onlyonce, 83 | "sql": self._sql 84 | }) 85 | 86 | @eventmanager.register(EventType.PluginAction) 87 | def execute(self, event: Event = None): 88 | if event: 89 | event_data = event.event_data 90 | if not event_data or event_data.get("action") != "sql_execute": 91 | return 92 | args = event_data.get("arg_str") 93 | if not args: 94 | return 95 | 96 | logger.info(f"收到命令,开始执行SQL ...{args}") 97 | 98 | # 读取sqlite数据 99 | try: 100 | gradedb = sqlite3.connect("/config/user.db") 101 | except Exception as e: 102 | logger.error(f"数据库链接失败 {str(e)}") 103 | return 104 | 105 | # 创建游标cursor来执行executeSQL语句 106 | cursor = gradedb.cursor() 107 | 108 | # 执行SQL语句 109 | try: 110 | # 执行SQL语句 111 | cursor.execute(args) 112 | if 'select' in args.lower(): 113 | rows = cursor.fetchall() 114 | # 获取列名 115 | columns = [desc[0] for desc in cursor.description] 116 | # 将查询结果转换为key-value对的列表 117 | results = [] 118 | for row in rows: 119 | result = dict(zip(columns, row)) 120 | results.append(result) 121 | result = "\n".join([str(i) for i in results]) 122 | result = str(result).replace("'", "\"") 123 | logger.info(result) 124 | 125 | if event.event_data.get("channel") == MessageChannel.Telegram: 126 | result = f"```plaintext\n{result}\n```" 127 | self.post_message(channel=event.event_data.get("channel"), 128 | title="SQL执行结果", 129 | text=result, 130 | userid=event.event_data.get("user")) 131 | else: 132 | gradedb.commit() 133 | result = f"执行成功,影响行数:{cursor.rowcount}" 134 | logger.info(result) 135 | 136 | if event.event_data.get("channel") == MessageChannel.Telegram: 137 | result = f"```plaintext\n{result}\n```" 138 | self.post_message(channel=event.event_data.get("channel"), 139 | title="SQL执行结果", 140 | text=result, 141 | userid=event.event_data.get("user")) 142 | 143 | except Exception as e: 144 | logger.error(f"SQL语句执行失败 {str(e)}") 145 | return 146 | finally: 147 | # 关闭游标 148 | cursor.close() 149 | 150 | def get_state(self) -> bool: 151 | return True 152 | 153 | @staticmethod 154 | def get_command() -> List[Dict[str, Any]]: 155 | """ 156 | 定义远程控制命令 157 | :return: 命令关键字、事件、描述、附带数据 158 | """ 159 | return [{ 160 | "cmd": "/sql", 161 | "event": EventType.PluginAction, 162 | "desc": "自定义sql执行", 163 | "category": "", 164 | "data": { 165 | "action": "sql_execute" 166 | } 167 | }] 168 | 169 | def get_api(self) -> List[Dict[str, Any]]: 170 | pass 171 | 172 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 173 | """ 174 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 175 | """ 176 | return [ 177 | { 178 | 'component': 'VForm', 179 | 'content': [ 180 | { 181 | 'component': 'VRow', 182 | 'content': [ 183 | { 184 | 'component': 'VCol', 185 | 'props': { 186 | 'cols': 12, 187 | 'md': 6 188 | }, 189 | 'content': [ 190 | { 191 | 'component': 'VSwitch', 192 | 'props': { 193 | 'model': 'onlyonce', 194 | 'label': '执行sql' 195 | } 196 | } 197 | ] 198 | } 199 | ] 200 | }, 201 | { 202 | 'component': 'VRow', 203 | 'content': [ 204 | { 205 | 'component': 'VCol', 206 | 'props': { 207 | 'cols': 12, 208 | }, 209 | 'content': [ 210 | { 211 | 'component': 'VTextarea', 212 | 'props': { 213 | 'model': 'sql', 214 | 'rows': '2', 215 | 'label': 'sql语句', 216 | 'placeholder': '一行一条' 217 | } 218 | } 219 | ] 220 | } 221 | ] 222 | }, 223 | { 224 | 'component': 'VRow', 225 | 'content': [ 226 | { 227 | 'component': 'VCol', 228 | 'props': { 229 | 'cols': 12, 230 | }, 231 | 'content': [ 232 | { 233 | 'component': 'VAlert', 234 | 'props': { 235 | 'type': 'info', 236 | 'variant': 'tonal' 237 | }, 238 | 'content': [ 239 | { 240 | 'component': 'span', 241 | 'text': '执行日志将会输出到控制台,请谨慎操作。' 242 | } 243 | ] 244 | } 245 | ] 246 | } 247 | ] 248 | }, 249 | { 250 | 'component': 'VRow', 251 | 'content': [ 252 | { 253 | 'component': 'VCol', 254 | 'props': { 255 | 'cols': 12, 256 | }, 257 | 'content': [ 258 | { 259 | 'component': 'VAlert', 260 | 'props': { 261 | 'type': 'info', 262 | 'variant': 'tonal' 263 | }, 264 | 'content': [ 265 | { 266 | 'component': 'span', 267 | 'text': '可使用交互命令/sql select *****' 268 | } 269 | ] 270 | } 271 | ] 272 | } 273 | ] 274 | } 275 | ] 276 | } 277 | ], { 278 | "onlyonce": False, 279 | "sql": "", 280 | } 281 | 282 | def get_page(self) -> List[dict]: 283 | pass 284 | 285 | def stop_service(self): 286 | """ 287 | 退出插件 288 | """ 289 | pass 290 | -------------------------------------------------------------------------------- /plugins.v2/strmredirect/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import urllib.parse 4 | from pathlib import Path 5 | from typing import List, Tuple, Dict, Any 6 | 7 | from app.log import logger 8 | from app.plugins import _PluginBase 9 | 10 | 11 | class StrmRedirect(_PluginBase): 12 | # 插件名称 13 | plugin_name = "Strm重定向" 14 | # 插件描述 15 | plugin_desc = "重写Strm文件内容。" 16 | # 插件图标 17 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/softlinkredirect.png" 18 | # 插件版本 19 | plugin_version = "1.2.1" 20 | # 插件作者 21 | plugin_author = "thsrite" 22 | # 作者主页 23 | author_url = "https://github.com/thsrite" 24 | # 插件配置项ID前缀 25 | plugin_config_prefix = "strmredirect_" 26 | # 加载顺序 27 | plugin_order = 27 28 | # 可使用的用户级别 29 | auth_level = 1 30 | 31 | # 私有属性 32 | _onlyonce = False 33 | _unquote = False 34 | _strm_path = None 35 | _origin_path = None 36 | _redirect_path = None 37 | 38 | def init_plugin(self, config: dict = None): 39 | # 读取配置 40 | if config: 41 | self._onlyonce = config.get("onlyonce") 42 | self._unquote = config.get("unquote") 43 | self._strm_path = config.get("strm_path") 44 | self._origin_path = config.get("origin_path") 45 | self._redirect_path = config.get("redirect_path") 46 | 47 | if self._onlyonce and self._strm_path and ((self._origin_path and self._redirect_path) or self._unquote): 48 | logger.info(f"{self._strm_path} Strm重定向开始 {self._origin_path} - {self._redirect_path}") 49 | self.update_strm(self._origin_path, self._redirect_path, self._strm_path) 50 | logger.info(f"{self._strm_path} Strm重定向完成") 51 | self._onlyonce = False 52 | self.update_config({ 53 | "onlyonce": self._onlyonce, 54 | "unquote": self._unquote, 55 | "strm_path": self._strm_path, 56 | "origin_path": self._origin_path, 57 | "redirect_path": self._redirect_path 58 | }) 59 | 60 | def update_strm(self, target_from, target_to, directory): 61 | for root, dirs, files in os.walk(directory): 62 | for name in dirs + files: 63 | file_path = os.path.join(root, name) 64 | if Path(str(file_path)).is_dir(): 65 | continue 66 | if Path(str(file_path)).is_file(): 67 | if Path(str(file_path)).suffix.lower() != ".strm": 68 | continue 69 | with open(str(file_path), 'r', encoding='utf-8') as file: 70 | strm_content = file.read() 71 | if not strm_content: 72 | continue 73 | # unencoded = self.find_unencoded_parts(strm_content) 74 | # 解码url 75 | unercoded_strm_content = urllib.parse.unquote(strm_content) 76 | if self._unquote: 77 | with open(str(file_path), 'w', encoding='utf-8') as file: 78 | file.write(unercoded_strm_content) 79 | logger.info(f"Unquote Strm: {strm_content} -> {unercoded_strm_content} success") 80 | if target_from and target_to: 81 | if str(unercoded_strm_content).startswith(target_from): 82 | strm_content = unercoded_strm_content.replace(target_from, target_to) 83 | # no_encoded = unencoded[0] 84 | # encoded = strm_content.replace(no_encoded, "") 85 | # encoded = urllib.parse.quote(encoded) 86 | # strm_content = no_encoded + encoded 87 | 88 | # 如果不是url,不进行编码 89 | if not str(strm_content).startswith("http"): 90 | strm_content = urllib.parse.unquote(strm_content) 91 | with open(str(file_path), 'w', encoding='utf-8') as file: 92 | file.write(strm_content) 93 | logger.info( 94 | f"Updated Strm: {unercoded_strm_content} -> {strm_content} success") 95 | 96 | @staticmethod 97 | def find_unencoded_parts(input_string: str): 98 | # 匹配URL编码的部分 99 | url_encoded_pattern = re.compile(r'%[0-9A-Fa-f]{2}') 100 | 101 | # 用于存储未编码的部分 102 | unencoded_parts = [] 103 | 104 | # 找到所有的URL编码部分 105 | last_index = 0 106 | for match in url_encoded_pattern.finditer(input_string): 107 | # 提取未编码的部分 108 | start_index = match.start() 109 | if start_index > last_index: 110 | unencoded_parts.append(input_string[last_index:start_index]) 111 | last_index = match.end() 112 | 113 | # 提取最后一部分,可能未被编码 114 | if last_index < len(input_string): 115 | unencoded_parts.append(input_string[last_index:]) 116 | 117 | return unencoded_parts 118 | 119 | @staticmethod 120 | def get_command() -> List[Dict[str, Any]]: 121 | """ 122 | 定义远程控制命令 123 | :return: 命令关键字、事件、描述、附带数据 124 | """ 125 | pass 126 | 127 | def get_api(self) -> List[Dict[str, Any]]: 128 | pass 129 | 130 | def get_service(self) -> List[Dict[str, Any]]: 131 | """ 132 | 注册插件公共服务 133 | [{ 134 | "id": "服务ID", 135 | "name": "服务名称", 136 | "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", 137 | "func": self.xxx, 138 | "kwargs": {} # 定时器参数 139 | }] 140 | """ 141 | return [] 142 | 143 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 144 | """ 145 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 146 | """ 147 | return [ 148 | { 149 | 'component': 'VForm', 150 | 'content': [ 151 | { 152 | 'component': 'VRow', 153 | 'content': [ 154 | { 155 | 'component': 'VCol', 156 | 'props': { 157 | 'cols': 12, 158 | 'md': 3 159 | }, 160 | 'content': [ 161 | { 162 | 'component': 'VSwitch', 163 | 'props': { 164 | 'model': 'onlyonce', 165 | 'label': '立即运行', 166 | } 167 | } 168 | ] 169 | }, 170 | { 171 | 'component': 'VCol', 172 | 'props': { 173 | 'cols': 12, 174 | 'md': 3 175 | }, 176 | 'content': [ 177 | { 178 | 'component': 'VSwitch', 179 | 'props': { 180 | 'model': 'unquote', 181 | 'label': '解码URL', 182 | } 183 | } 184 | ] 185 | } 186 | ] 187 | }, 188 | { 189 | 'component': 'VRow', 190 | 'content': [ 191 | { 192 | 'component': 'VCol', 193 | 'props': { 194 | 'cols': 12, 195 | }, 196 | 'content': [ 197 | { 198 | 'component': 'VTextField', 199 | 'props': { 200 | 'model': 'strm_path', 201 | 'label': 'strm路径', 202 | } 203 | } 204 | ] 205 | }, 206 | ] 207 | }, 208 | { 209 | 'component': 'VRow', 210 | 'content': [ 211 | { 212 | 'component': 'VCol', 213 | 'props': { 214 | 'cols': 12, 215 | }, 216 | 'content': [ 217 | { 218 | 'component': 'VTextField', 219 | 'props': { 220 | 'model': 'origin_path', 221 | 'label': '源路径', 222 | } 223 | } 224 | ] 225 | }, 226 | ] 227 | }, 228 | { 229 | 'component': 'VRow', 230 | 'content': [ 231 | { 232 | 'component': 'VCol', 233 | 'props': { 234 | 'cols': 12, 235 | }, 236 | 'content': [ 237 | { 238 | 'component': 'VTextField', 239 | 'props': { 240 | 'model': 'redirect_path', 241 | 'label': '新路径', 242 | } 243 | } 244 | ] 245 | } 246 | ] 247 | }, 248 | { 249 | 'component': 'VRow', 250 | 'content': [ 251 | { 252 | 'component': 'VCol', 253 | 'props': { 254 | 'cols': 12, 255 | }, 256 | 'content': [ 257 | { 258 | 'component': 'VAlert', 259 | 'props': { 260 | 'type': 'info', 261 | 'variant': 'tonal', 262 | 'text': '源路径->新路径,将会替换所有.strm文件中的源路径为新路径。' 263 | } 264 | } 265 | ] 266 | } 267 | ] 268 | }, 269 | { 270 | 'component': 'VRow', 271 | 'content': [ 272 | { 273 | 'component': 'VCol', 274 | 'props': { 275 | 'cols': 12, 276 | }, 277 | 'content': [ 278 | { 279 | 'component': 'VAlert', 280 | 'props': { 281 | 'type': 'info', 282 | 'variant': 'tonal', 283 | 'text': '如想解码Strm中的url路径,仅需勾选解码URL和填写strm路径即可。' 284 | } 285 | } 286 | ] 287 | } 288 | ] 289 | } 290 | ] 291 | } 292 | ], { 293 | "onlyonce": False, 294 | "unquote": False, 295 | "strm_path": "", 296 | "origin_path": "", 297 | "redirect_path": "", 298 | } 299 | 300 | def get_page(self) -> List[dict]: 301 | pass 302 | 303 | def get_state(self): 304 | return False 305 | 306 | def stop_service(self): 307 | """ 308 | 退出插件 309 | """ 310 | pass 311 | -------------------------------------------------------------------------------- /plugins.v2/synccookiecloud/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | from hashlib import md5 4 | from urllib.parse import urlparse 5 | 6 | import pytz 7 | 8 | from app.core.config import settings 9 | from app.db.site_oper import SiteOper 10 | from app.plugins import _PluginBase 11 | from typing import Any, List, Dict, Tuple, Optional 12 | from app.log import logger 13 | from apscheduler.schedulers.background import BackgroundScheduler 14 | from apscheduler.triggers.cron import CronTrigger 15 | 16 | from app.utils.crypto import CryptoJsUtils 17 | 18 | 19 | class SyncCookieCloud(_PluginBase): 20 | # 插件名称 21 | plugin_name = "同步CookieCloud" 22 | # 插件描述 23 | plugin_desc = "同步MoviePilot站点Cookie到本地CookieCloud。" 24 | # 插件图标 25 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/cookiecloud.png" 26 | # 插件版本 27 | plugin_version = "1.3" 28 | # 插件作者 29 | plugin_author = "thsrite" 30 | # 作者主页 31 | author_url = "https://github.com/thsrite" 32 | # 插件配置项ID前缀 33 | plugin_config_prefix = "synccookiecloud_" 34 | # 加载顺序 35 | plugin_order = 28 36 | # 可使用的用户级别 37 | auth_level = 1 38 | 39 | # 私有属性 40 | _enabled: bool = False 41 | _onlyonce: bool = False 42 | _cron: str = "" 43 | siteoper = None 44 | _scheduler: Optional[BackgroundScheduler] = None 45 | 46 | def init_plugin(self, config: dict = None): 47 | self.siteoper = SiteOper() 48 | 49 | # 停止现有任务 50 | self.stop_service() 51 | 52 | if config: 53 | self._enabled = config.get("enabled") 54 | self._onlyonce = config.get("onlyonce") 55 | self._cron = config.get("cron") 56 | 57 | if self._enabled or self._onlyonce: 58 | # 定时服务 59 | self._scheduler = BackgroundScheduler(timezone=settings.TZ) 60 | 61 | # 立即运行一次 62 | if self._onlyonce: 63 | logger.info(f"同步CookieCloud服务启动,立即运行一次") 64 | self._scheduler.add_job(self.__sync_to_cookiecloud, 'date', 65 | run_date=datetime.now( 66 | tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), 67 | name="同步CookieCloud") 68 | # 关闭一次性开关 69 | self._onlyonce = False 70 | 71 | # 保存配置 72 | self.__update_config() 73 | 74 | # 周期运行 75 | if self._cron: 76 | try: 77 | self._scheduler.add_job(func=self.__sync_to_cookiecloud, 78 | trigger=CronTrigger.from_crontab(self._cron), 79 | name="同步CookieCloud") 80 | except Exception as err: 81 | logger.error(f"定时任务配置错误:{err}") 82 | # 推送实时消息 83 | self.systemmessage.put(f"执行周期配置错误:{err}") 84 | 85 | # 启动任务 86 | if self._scheduler.get_jobs(): 87 | self._scheduler.print_jobs() 88 | self._scheduler.start() 89 | 90 | def __sync_to_cookiecloud(self): 91 | """ 92 | 同步站点cookie到cookiecloud 93 | """ 94 | # 获取所有站点 95 | sites = self.siteoper.list_order_by_pri() 96 | if not sites: 97 | return 98 | 99 | if not settings.COOKIECLOUD_ENABLE_LOCAL: 100 | logger.error('本地CookieCloud服务器未启用') 101 | return 102 | 103 | cookies = {} 104 | for site in sites: 105 | domain = urlparse(site.url).netloc 106 | cookie = site.cookie 107 | 108 | if not cookie: 109 | logger.error(f"站点{domain}无cookie,跳过处理") 110 | continue 111 | 112 | # 解析cookie 113 | site_cookies = [] 114 | for ck in cookie.split(";"): 115 | site_cookies.append({ 116 | "domain": domain, 117 | "name": ck.split("=")[0], 118 | "value": ck.split("=")[1] 119 | }) 120 | # 存储cookies 121 | cookies[domain] = site_cookies 122 | if cookies: 123 | decrypted_cookies_data = self.__download() 124 | if decrypted_cookies_data: 125 | update_data = self.__update_to_cloud(cookies, decrypted_cookies_data) 126 | crypt_key = self._get_crypt_key() 127 | try: 128 | cookies = {'cookie_data': update_data} 129 | encrypted_data = CryptoJsUtils.encrypt(json.dumps(cookies).encode('utf-8'), crypt_key).decode('utf-8') 130 | except Exception as e: 131 | logger.error(f"CookieCloud加密失败,{e}") 132 | return 133 | 134 | ck = {'encrypted': encrypted_data} 135 | file = open(settings.COOKIE_PATH / f'{settings.COOKIECLOUD_KEY}.json', 'w') 136 | file.write(json.dumps(ck)) 137 | file.close() 138 | 139 | logger.info(f"------当前储存的cookie数据------") 140 | logger.info(cookies) 141 | logger.info(f"------当前储存的cookie数据------") 142 | logger.info(f"同步站点cookie到CookieCloud成功") 143 | 144 | def __download(self): 145 | """ 146 | 获取并解密本地CookieCloud数据 147 | """ 148 | encrypt_data = self.__load_local_encrypt_data(uuid=self._get_crypt_key()) 149 | if not encrypt_data: 150 | return {}, "未获取到本地CookieCloud数据" 151 | encrypted = encrypt_data.get("encrypted") 152 | if not encrypted: 153 | return {}, "未获取到cookie密文" 154 | else: 155 | crypt_key = self._get_crypt_key() 156 | try: 157 | decrypted_data = CryptoJsUtils.decrypt(encrypted, crypt_key).decode('utf-8') 158 | result = json.loads(decrypted_data) 159 | except Exception as e: 160 | return {}, "cookie解密失败:" + str(e) 161 | 162 | if not result: 163 | return {}, "cookie解密为空" 164 | 165 | if result.get("cookie_data"): 166 | contents = result.get("cookie_data") 167 | else: 168 | contents = result 169 | return contents 170 | 171 | def __load_local_encrypt_data(self, uuid: bytes) -> Dict[str, Any]: 172 | """ 173 | 加载本地CookieCloud加密数据 174 | """ 175 | file_path = settings.COOKIE_PATH / f"{settings.COOKIECLOUD_KEY}.json" 176 | # 检查文件是否存在 177 | if not file_path.exists(): 178 | return {} 179 | 180 | # 读取文件 181 | with open(file_path, encoding="utf-8", mode="r") as file: 182 | read_content = file.read() 183 | data = json.loads(read_content.encode("utf-8")) 184 | return data 185 | 186 | def __update_to_cloud(self, in_list, out_list): 187 | """ 188 | 构建站点数据 189 | """ 190 | # 清除空值 191 | out_list = {key: value for key, value in out_list.items() if value} 192 | 193 | temp_list = {} 194 | for domain in in_list.keys(): 195 | # 构建站点数据模板 196 | template = {} 197 | for domain_out in out_list: 198 | if domain.endswith(domain_out): 199 | for d in out_list[domain_out]: 200 | for key, value in d.items(): 201 | if key not in template: 202 | template[key] = value 203 | 204 | # 构建站点新数据 205 | temp_list[domain] = [] 206 | for d1 in in_list[domain]: 207 | temp_dict = {k: template.get(k, "") for k in template.keys()} 208 | temp_dict.update(d1) 209 | temp_list[domain].append(temp_dict) 210 | 211 | # 覆盖修改源站点数据 212 | for temp_domain in temp_list.keys(): 213 | found_match = False 214 | for idx, domain2 in enumerate(out_list): 215 | if temp_domain.endswith(domain2): 216 | out_list[temp_domain] = out_list.pop(domain2) 217 | out_list[temp_domain] = temp_list[temp_domain] 218 | found_match = True 219 | break 220 | if not found_match: 221 | out_list[temp_domain] = temp_list[temp_domain] 222 | return out_list 223 | 224 | def _get_crypt_key(self) -> bytes: 225 | """ 226 | 使用UUID和密码生成CookieCloud的加解密密钥 227 | """ 228 | md5_generator = md5() 229 | md5_generator.update( 230 | (str(settings.COOKIECLOUD_KEY).strip() + '-' + str(settings.COOKIECLOUD_PASSWORD).strip()).encode('utf-8')) 231 | return (md5_generator.hexdigest()[:16]).encode('utf-8') 232 | 233 | def __update_config(self): 234 | self.update_config({ 235 | "enabled": self._enabled, 236 | "onlyonce": self._onlyonce, 237 | "cron": self._cron 238 | }) 239 | 240 | def get_state(self) -> bool: 241 | return self._enabled 242 | 243 | @staticmethod 244 | def get_command() -> List[Dict[str, Any]]: 245 | pass 246 | 247 | def get_api(self) -> List[Dict[str, Any]]: 248 | pass 249 | 250 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 251 | """ 252 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 253 | """ 254 | return [ 255 | { 256 | 'component': 'VForm', 257 | 'content': [ 258 | { 259 | 'component': 'VRow', 260 | 'content': [ 261 | { 262 | 'component': 'VCol', 263 | 'props': { 264 | 'cols': 12, 265 | 'md': 6 266 | }, 267 | 'content': [ 268 | { 269 | 'component': 'VSwitch', 270 | 'props': { 271 | 'model': 'enabled', 272 | 'label': '启用插件', 273 | } 274 | } 275 | ] 276 | }, 277 | { 278 | 'component': 'VCol', 279 | 'props': { 280 | 'cols': 12, 281 | 'md': 6 282 | }, 283 | 'content': [ 284 | { 285 | 'component': 'VSwitch', 286 | 'props': { 287 | 'model': 'onlyonce', 288 | 'label': '立即运行一次', 289 | } 290 | } 291 | ] 292 | } 293 | ] 294 | }, 295 | { 296 | 'component': 'VRow', 297 | 'content': [ 298 | { 299 | 'component': 'VCol', 300 | 'props': { 301 | 'cols': 12, 302 | }, 303 | 'content': [ 304 | { 305 | 'component': 'VTextField', 306 | 'props': { 307 | 'model': 'cron', 308 | 'label': '执行周期', 309 | 'placeholder': '5位cron表达式,留空自动' 310 | } 311 | } 312 | ] 313 | }, 314 | ] 315 | }, 316 | { 317 | 'component': 'VRow', 318 | 'content': [ 319 | { 320 | 'component': 'VCol', 321 | 'props': { 322 | 'cols': 12, 323 | }, 324 | 'content': [ 325 | { 326 | 'component': 'VAlert', 327 | 'props': { 328 | 'type': 'info', 329 | 'variant': 'tonal', 330 | 'text': '需要MoviePilot设定-站点启用本地CookieCloud服务器。' 331 | } 332 | } 333 | ] 334 | } 335 | ] 336 | }, 337 | ] 338 | } 339 | ], { 340 | "enabled": False, 341 | "onlyonce": False, 342 | "cron": "5 1 * * *", 343 | } 344 | 345 | def get_page(self) -> List[dict]: 346 | pass 347 | 348 | def stop_service(self): 349 | """ 350 | 退出插件 351 | """ 352 | try: 353 | if self._scheduler: 354 | self._scheduler.remove_all_jobs() 355 | if self._scheduler.running: 356 | self._scheduler.shutdown() 357 | self._scheduler = None 358 | except Exception as e: 359 | logger.error("退出插件失败:%s" % str(e)) 360 | -------------------------------------------------------------------------------- /plugins/cd2assistant/requirements.txt: -------------------------------------------------------------------------------- 1 | clouddrive -------------------------------------------------------------------------------- /plugins/commandexecute/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from app.core.event import eventmanager, Event 4 | from app.plugins import _PluginBase 5 | from typing import Any, List, Dict, Tuple 6 | from app.log import logger 7 | from app.schemas.types import EventType, MessageChannel 8 | 9 | 10 | class CommandExecute(_PluginBase): 11 | # 插件名称 12 | plugin_name = "命令执行器" 13 | # 插件描述 14 | plugin_desc = "自定义容器命令执行。" 15 | # 插件图标 16 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/command.png" 17 | # 插件版本 18 | plugin_version = "1.2" 19 | # 插件作者 20 | plugin_author = "thsrite" 21 | # 作者主页 22 | author_url = "https://github.com/thsrite" 23 | # 插件配置项ID前缀 24 | plugin_config_prefix = "commandexecute_" 25 | # 加载顺序 26 | plugin_order = 99 27 | # 可使用的用户级别 28 | auth_level = 1 29 | 30 | # 私有属性 31 | _onlyonce = None 32 | _command = None 33 | 34 | def init_plugin(self, config: dict = None): 35 | if config: 36 | self._onlyonce = config.get("onlyonce") 37 | self._command = config.get("command") 38 | 39 | if self._onlyonce and self._command: 40 | # 执行SQL语句 41 | try: 42 | for command in self._command.split("\n"): 43 | logger.info(f"开始执行命令 {command}") 44 | ouptut = self.execute_command(command) 45 | # logger.info('\n'.join(ouptut)) 46 | except Exception as e: 47 | logger.error(f"命令执行失败 {str(e)}") 48 | return 49 | finally: 50 | self._onlyonce = False 51 | self.update_config({ 52 | "onlyonce": self._onlyonce, 53 | "command": self._command 54 | }) 55 | 56 | @staticmethod 57 | def execute_command(command: str): 58 | """ 59 | 执行命令 60 | :param command: 命令 61 | """ 62 | result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 63 | ouptut = [] 64 | while True: 65 | error = result.stderr.readline().decode("utf-8") 66 | if error == '' and result.poll() is not None: 67 | break 68 | if error: 69 | logger.info(error.strip()) 70 | ouptut.append(error.strip()) 71 | while True: 72 | output = result.stdout.readline().decode("utf-8") 73 | if output == '' and result.poll() is not None: 74 | break 75 | if output: 76 | logger.info(output.strip()) 77 | ouptut.append(output.strip()) 78 | 79 | return ouptut 80 | 81 | @eventmanager.register(EventType.PluginAction) 82 | def execute(self, event: Event = None): 83 | if event: 84 | event_data = event.event_data 85 | if not event_data or event_data.get("action") != "command_execute": 86 | return 87 | logger.info(f"收到命令执行事件 ...{event_data}") 88 | args = event_data.get("args") 89 | if not args: 90 | return 91 | 92 | logger.info(f"收到命令,开始执行命令 ...{args}") 93 | ouptut = self.execute_command(args) 94 | result = '\n'.join(ouptut) 95 | 96 | if event.event_data.get("channel") == MessageChannel.Telegram: 97 | result = f"```plaintext\n{result}\n```" 98 | self.post_message(channel=event.event_data.get("channel"), 99 | title="命令执行结果", 100 | text=result, 101 | userid=event.event_data.get("user")) 102 | 103 | def get_state(self) -> bool: 104 | return True 105 | 106 | @staticmethod 107 | def get_command() -> List[Dict[str, Any]]: 108 | """ 109 | 定义远程控制命令 110 | :return: 命令关键字、事件、描述、附带数据 111 | """ 112 | return [{ 113 | "cmd": "/cmd", 114 | "event": EventType.PluginAction, 115 | "desc": "自定义命令执行", 116 | "category": "", 117 | "data": { 118 | "action": "command_execute" 119 | } 120 | }] 121 | 122 | def get_api(self) -> List[Dict[str, Any]]: 123 | pass 124 | 125 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 126 | """ 127 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 128 | """ 129 | return [ 130 | { 131 | 'component': 'VForm', 132 | 'content': [ 133 | { 134 | 'component': 'VRow', 135 | 'content': [ 136 | { 137 | 'component': 'VCol', 138 | 'props': { 139 | 'cols': 12, 140 | 'md': 6 141 | }, 142 | 'content': [ 143 | { 144 | 'component': 'VSwitch', 145 | 'props': { 146 | 'model': 'onlyonce', 147 | 'label': '执行命令' 148 | } 149 | } 150 | ] 151 | } 152 | ] 153 | }, 154 | { 155 | 'component': 'VRow', 156 | 'content': [ 157 | { 158 | 'component': 'VCol', 159 | 'props': { 160 | 'cols': 12, 161 | }, 162 | 'content': [ 163 | { 164 | 'component': 'VTextarea', 165 | 'props': { 166 | 'model': 'command', 167 | 'rows': '2', 168 | 'label': 'command命令', 169 | 'placeholder': '一行一条' 170 | } 171 | } 172 | ] 173 | } 174 | ] 175 | }, 176 | { 177 | 'component': 'VRow', 178 | 'content': [ 179 | { 180 | 'component': 'VCol', 181 | 'props': { 182 | 'cols': 12, 183 | }, 184 | 'content': [ 185 | { 186 | 'component': 'VAlert', 187 | 'props': { 188 | 'type': 'info', 189 | 'variant': 'tonal' 190 | }, 191 | 'content': [ 192 | { 193 | 'component': 'span', 194 | 'text': '执行日志将会输出到控制台,请谨慎操作。' 195 | } 196 | ] 197 | } 198 | ] 199 | } 200 | ] 201 | }, 202 | { 203 | 'component': 'VRow', 204 | 'content': [ 205 | { 206 | 'component': 'VCol', 207 | 'props': { 208 | 'cols': 12, 209 | }, 210 | 'content': [ 211 | { 212 | 'component': 'VAlert', 213 | 'props': { 214 | 'type': 'info', 215 | 'variant': 'tonal' 216 | }, 217 | 'content': [ 218 | { 219 | 'component': 'span', 220 | 'text': '可使用交互命令/cmd ls' 221 | } 222 | ] 223 | } 224 | ] 225 | } 226 | ] 227 | } 228 | ] 229 | } 230 | ], { 231 | "onlyonce": False, 232 | "command": "", 233 | } 234 | 235 | def get_page(self) -> List[dict]: 236 | pass 237 | 238 | def stop_service(self): 239 | """ 240 | 退出插件 241 | """ 242 | pass 243 | -------------------------------------------------------------------------------- /plugins/linktosrc/__init__.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from pathlib import Path 3 | from typing import List, Tuple, Dict, Any 4 | 5 | from app.core.config import Settings 6 | from app.log import logger 7 | from app.plugins import _PluginBase 8 | 9 | 10 | class LinkToSrc(_PluginBase): 11 | # 插件名称 12 | plugin_name = "源文件恢复" 13 | # 插件描述 14 | plugin_desc = "根据MoviePilot的转移记录中的硬链文件恢复源文件" 15 | # 插件图标 16 | plugin_icon = "Time_machine_A.png" 17 | # 插件版本 18 | plugin_version = "1.2" 19 | # 插件作者 20 | plugin_author = "thsrite" 21 | # 作者主页 22 | author_url = "https://github.com/thsrite" 23 | # 插件配置项ID前缀 24 | plugin_config_prefix = "linktosrc_" 25 | # 加载顺序 26 | plugin_order = 32 27 | # 可使用的用户级别 28 | auth_level = 1 29 | 30 | _onlyonce: bool = False 31 | _link_dirs: str = None 32 | 33 | def init_plugin(self, config: dict = None): 34 | if config: 35 | self._onlyonce = config.get("onlyonce") 36 | self._link_dirs = config.get("link_dirs") 37 | 38 | if self._onlyonce: 39 | # 执行替换 40 | self._task() 41 | self._onlyonce = False 42 | self.__update_config() 43 | 44 | def _task(self): 45 | db_path = Settings().CONFIG_PATH / 'user.db' 46 | try: 47 | gradedb = sqlite3.connect(db_path) 48 | except Exception as e: 49 | logger.error(f"无法打开数据库文件 {db_path},请检查路径是否正确:{str(e)}") 50 | return 51 | 52 | transfer_history = [] 53 | # 创建游标cursor来执行executeSQL语句 54 | cursor = gradedb.cursor() 55 | if self._link_dirs: 56 | link_dirs = self._link_dirs.split("\n") 57 | for link_dir in link_dirs: 58 | sql = f''' 59 | SELECT 60 | src, 61 | dest 62 | FROM 63 | transferhistory 64 | WHERE 65 | src IS NOT NULL and dest IS NOT NULL and dest like '{link_dir}%'; 66 | ''' 67 | cursor.execute(sql) 68 | transfer_history += cursor.fetchall() 69 | else: 70 | sql = ''' 71 | SELECT 72 | src, 73 | dest 74 | FROM 75 | transferhistory 76 | WHERE 77 | src IS NOT NULL and dest IS NOT NULL; 78 | ''' 79 | cursor.execute(sql) 80 | transfer_history = cursor.fetchall() 81 | logger.info(f"查询到历史记录{len(transfer_history)}条") 82 | cursor.close() 83 | 84 | if not transfer_history: 85 | logger.error("未获取到历史记录,停止处理") 86 | return 87 | 88 | for history in transfer_history: 89 | src = history[0] 90 | dest = history[1] 91 | # 判断源文件是否存在 92 | if Path(src).exists(): 93 | logger.warn(f"源文件{src}已存在,跳过处理") 94 | continue 95 | # 源文件不存在,目标文件也不存在,跳过 96 | if not Path(dest).exists(): 97 | logger.warn(f"源文件{src}不存在且硬链文件{dest}不存在,跳过处理") 98 | continue 99 | # 创建源文件目录,防止目录不存在无法执行 100 | Path(src).parent.mkdir(parents=True, exist_ok=True) 101 | # 目标文件硬链回源文件 102 | Path(src).hardlink_to(dest) 103 | logger.info(f"硬链文件{dest}重新链接回源文件{src}") 104 | 105 | logger.info("全部处理完成") 106 | 107 | def __update_config(self): 108 | self.update_config({ 109 | "onlyonce": self._onlyonce, 110 | "link_dirs": self._link_dirs 111 | }) 112 | 113 | @staticmethod 114 | def get_command() -> List[Dict[str, Any]]: 115 | pass 116 | 117 | def get_api(self) -> List[Dict[str, Any]]: 118 | pass 119 | 120 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 121 | return [ 122 | { 123 | 'component': 'VForm', 124 | 'content': [ 125 | { 126 | 'component': 'VRow', 127 | 'content': [ 128 | { 129 | 'component': 'VCol', 130 | 'props': { 131 | 'cols': 12, 132 | 'md': 6 133 | }, 134 | 'content': [ 135 | { 136 | 'component': 'VSwitch', 137 | 'props': { 138 | 'model': 'onlyonce', 139 | 'label': '立即运行一次', 140 | } 141 | } 142 | ] 143 | } 144 | ] 145 | }, 146 | { 147 | 'component': 'VRow', 148 | 'content': [ 149 | { 150 | 'component': 'VCol', 151 | 'props': { 152 | 'cols': 12, 153 | }, 154 | 'content': [ 155 | { 156 | 'component': 'VTextarea', 157 | 'props': { 158 | 'model': 'link_dirs', 159 | 'label': '需要恢复的硬链接目录', 160 | 'rows': 5, 161 | 'placeholder': '硬链接目录 (一行一个)' 162 | } 163 | } 164 | ] 165 | } 166 | ] 167 | }, 168 | { 169 | 'component': 'VRow', 170 | 'content': [ 171 | { 172 | 'component': 'VCol', 173 | 'props': { 174 | 'cols': 12, 175 | }, 176 | 'content': [ 177 | { 178 | 'component': 'VAlert', 179 | 'props': { 180 | 'type': 'info', 181 | 'variant': 'tonal', 182 | 'text': '根据转移记录中的硬链接恢复源文件', 183 | 'style': 'white-space: pre-line;' 184 | } 185 | } 186 | ] 187 | } 188 | ] 189 | } 190 | ] 191 | } 192 | ], { 193 | "onlyonce": False, 194 | "link_dirs": "" 195 | } 196 | 197 | def get_page(self) -> List[dict]: 198 | pass 199 | 200 | def get_state(self) -> bool: 201 | return self._onlyonce 202 | 203 | def stop_service(self): 204 | pass 205 | -------------------------------------------------------------------------------- /plugins/pluginreinstall/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from fastapi import APIRouter 4 | 5 | from app.core.config import settings 6 | from app.core.plugin import PluginManager 7 | from app.db.systemconfig_oper import SystemConfigOper 8 | from app.helper.plugin import PluginHelper 9 | from app.plugins import _PluginBase 10 | from typing import Any, List, Dict, Tuple 11 | from app.log import logger 12 | from app.schemas.types import SystemConfigKey 13 | from app.utils.string import StringUtils 14 | from app.scheduler import Scheduler 15 | 16 | router = APIRouter() 17 | 18 | 19 | class PluginReInstall(_PluginBase): 20 | # 插件名称 21 | plugin_name = "插件强制重装" 22 | # 插件描述 23 | plugin_desc = "卸载当前插件,强制重装。" 24 | # 插件图标 25 | plugin_icon = "refresh.png" 26 | # 插件版本 27 | plugin_version = "1.8" 28 | # 插件作者 29 | plugin_author = "thsrite" 30 | # 作者主页 31 | author_url = "https://github.com/thsrite" 32 | # 插件配置项ID前缀 33 | plugin_config_prefix = "pluginreinstall_" 34 | # 加载顺序 35 | plugin_order = 98 36 | # 可使用的用户级别 37 | auth_level = 1 38 | 39 | # 私有属性 40 | _reload = False 41 | _plugin_ids = [] 42 | _plugin_url = [] 43 | _base_url = "https://raw.githubusercontent.com/%s/%s/main/" 44 | 45 | def init_plugin(self, config: dict = None): 46 | if config: 47 | self._reload = config.get("reload") 48 | self._plugin_ids = config.get("plugin_ids") or [] 49 | if not self._plugin_ids: 50 | return 51 | self._plugin_url = config.get("plugin_url") 52 | 53 | # 仅重载插件 54 | if self._reload: 55 | for plugin_id in self._plugin_ids: 56 | self.__reload_plugin(plugin_id) 57 | logger.info(f"插件 {plugin_id} 热重载成功") 58 | self.__update_conifg() 59 | else: 60 | # 校验插件仓库格式 61 | plugin_url = None 62 | if self._plugin_url: 63 | pattern = "https://github.com/(.*?)/(.*?)/" 64 | matches = re.findall(pattern, str(self._plugin_url)) 65 | if not matches: 66 | logger.warn(f"指定插件仓库地址 {self._plugin_url} 错误,将使用插件默认地址重装") 67 | self._plugin_url = "" 68 | 69 | user, repo = PluginHelper().get_repo_info(self._plugin_url) 70 | plugin_url = self._base_url % (user, repo) 71 | 72 | self.__update_conifg() 73 | 74 | # 本地插件 75 | local_plugins = PluginManager().get_local_plugins() 76 | 77 | # 开始重载插件 78 | for plugin in local_plugins: 79 | if plugin.id in self._plugin_ids: 80 | logger.info( 81 | f"开始重载插件 {plugin.plugin_name} v{plugin.plugin_version}") 82 | 83 | # 开始安装线上插件 84 | state, msg = PluginHelper().install(pid=plugin.id, 85 | repo_url=plugin_url or plugin.repo_url) 86 | # 安装失败 87 | if not state: 88 | logger.error( 89 | f"插件 {plugin.plugin_name} 重装失败,当前版本 v{plugin.plugin_version}") 90 | continue 91 | 92 | logger.info( 93 | f"插件 {plugin.plugin_name} 重装成功,当前版本 v{plugin.plugin_version}") 94 | 95 | self.__reload_plugin(plugin.id) 96 | 97 | def __update_conifg(self): 98 | self.update_config({ 99 | "reload": self._reload, 100 | "plugin_url": self._plugin_url, 101 | }) 102 | 103 | def __reload_plugin(self, plugin_id): 104 | """ 105 | 重载插件 106 | """ 107 | # 加载插件到内存 108 | PluginManager().reload_plugin(plugin_id) 109 | # 注册插件服务 110 | Scheduler().update_plugin_job(plugin_id) 111 | # 注册插件API 112 | self.register_plugin_api(plugin_id) 113 | 114 | @staticmethod 115 | def register_plugin_api(plugin_id: str = None): 116 | """ 117 | 注册插件API(先删除后新增) 118 | """ 119 | for api in PluginManager().get_plugin_apis(plugin_id): 120 | for r in router.routes: 121 | if r.path == api.get("path"): 122 | router.routes.remove(r) 123 | break 124 | router.add_api_route(**api) 125 | 126 | def get_state(self) -> bool: 127 | return False 128 | 129 | @staticmethod 130 | def get_command() -> List[Dict[str, Any]]: 131 | pass 132 | 133 | def get_api(self) -> List[Dict[str, Any]]: 134 | pass 135 | 136 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 137 | """ 138 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 139 | """ 140 | # 已安装插件 141 | local_plugins = PluginManager().get_local_plugins() 142 | # 编历 local_plugins,生成插件类型选项 143 | pluginOptions = [] 144 | 145 | for plugin in local_plugins: 146 | pluginOptions.append({ 147 | "title": f"{plugin.plugin_name} v{plugin.plugin_version}", 148 | "value": plugin.id 149 | }) 150 | return [ 151 | { 152 | 'component': 'VForm', 153 | 'content': [ 154 | { 155 | 'component': 'VRow', 156 | 'content': [ 157 | { 158 | 'component': 'VCol', 159 | 'props': { 160 | 'cols': 12, 161 | 'md': 3 162 | }, 163 | 'content': [ 164 | { 165 | 'component': 'VSwitch', 166 | 'props': { 167 | 'model': 'reload', 168 | 'label': '仅重载', 169 | } 170 | } 171 | ] 172 | }, 173 | ] 174 | }, 175 | { 176 | 'component': 'VRow', 177 | 'content': [ 178 | { 179 | 'component': 'VCol', 180 | 'props': { 181 | 'cols': 12, 182 | 'md': 4 183 | }, 184 | 'content': [ 185 | { 186 | 'component': 'VSelect', 187 | 'props': { 188 | 'multiple': True, 189 | 'chips': True, 190 | 'model': 'plugin_ids', 191 | 'label': '重装插件', 192 | 'items': pluginOptions 193 | } 194 | } 195 | ] 196 | }, 197 | { 198 | 'component': 'VCol', 199 | 'props': { 200 | 'cols': 12, 201 | 'md': 8 202 | }, 203 | 'content': [ 204 | { 205 | 'component': 'VTextField', 206 | 'props': { 207 | 'model': 'plugin_url', 208 | 'label': '仓库地址', 209 | 'placeholder': 'https://github.com/%s/%s/' 210 | } 211 | } 212 | ] 213 | } 214 | ] 215 | }, 216 | { 217 | 'component': 'VRow', 218 | 'content': [ 219 | { 220 | 'component': 'VCol', 221 | 'props': { 222 | 'cols': 12, 223 | }, 224 | 'content': [ 225 | { 226 | 'component': 'VAlert', 227 | 'props': { 228 | 'type': 'info', 229 | 'variant': 'tonal', 230 | 'text': '选择已安装的本地插件,强制安装插件市场最新版本。' 231 | } 232 | } 233 | ] 234 | } 235 | ] 236 | }, 237 | { 238 | 'component': 'VRow', 239 | 'content': [ 240 | { 241 | 'component': 'VCol', 242 | 'props': { 243 | 'cols': 12, 244 | }, 245 | 'content': [ 246 | { 247 | 'component': 'VAlert', 248 | 'props': { 249 | 'type': 'info', 250 | 'variant': 'tonal', 251 | 'text': '支持指定插件仓库地址(https://github.com/%s/%s/)' 252 | } 253 | } 254 | ] 255 | } 256 | ] 257 | }, 258 | { 259 | 'component': 'VRow', 260 | 'content': [ 261 | { 262 | 'component': 'VCol', 263 | 'props': { 264 | 'cols': 12, 265 | }, 266 | 'content': [ 267 | { 268 | 'component': 'VAlert', 269 | 'props': { 270 | 'type': 'info', 271 | 'variant': 'tonal', 272 | 'text': '仅重载:不会获取最新代码,而是基于本地代码重新加载插件。' 273 | } 274 | } 275 | ] 276 | } 277 | ] 278 | }, 279 | ] 280 | } 281 | ], { 282 | "reload": False, 283 | "plugin_ids": [], 284 | "plugin_url": "", 285 | } 286 | 287 | @staticmethod 288 | def get_local_plugins(): 289 | """ 290 | 获取本地插件 291 | """ 292 | # 已安装插件 293 | install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or [] 294 | 295 | local_plugins = {} 296 | # 线上插件列表 297 | markets = settings.PLUGIN_MARKET.split(",") 298 | for market in markets: 299 | online_plugins = PluginHelper().get_plugins(market) or {} 300 | for pid, plugin in online_plugins.items(): 301 | if pid in install_plugins: 302 | local_plugin = local_plugins.get(pid) 303 | if local_plugin: 304 | if StringUtils.compare_version(local_plugin.get("plugin_version"), plugin.get("version")) < 0: 305 | local_plugins[pid] = { 306 | "id": pid, 307 | "plugin_name": plugin.get("name"), 308 | "repo_url": market, 309 | "plugin_version": plugin.get("version") 310 | } 311 | else: 312 | local_plugins[pid] = { 313 | "id": pid, 314 | "plugin_name": plugin.get("name"), 315 | "repo_url": market, 316 | "plugin_version": plugin.get("version") 317 | } 318 | 319 | return local_plugins 320 | 321 | def get_page(self) -> List[dict]: 322 | pass 323 | 324 | def stop_service(self): 325 | """ 326 | 退出插件 327 | """ 328 | pass 329 | -------------------------------------------------------------------------------- /plugins/pluginuninstall/__init__.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | from app.core.config import settings 5 | from app.core.plugin import PluginManager 6 | from app.db.systemconfig_oper import SystemConfigOper 7 | from app.helper.plugin import PluginHelper 8 | from app.plugins import _PluginBase 9 | from typing import Any, List, Dict, Tuple 10 | from app.log import logger 11 | from app.scheduler import Scheduler 12 | from app.schemas.types import SystemConfigKey 13 | from app.utils.string import StringUtils 14 | 15 | 16 | class PluginUnInstall(_PluginBase): 17 | # 插件名称 18 | plugin_name = "插件彻底卸载" 19 | # 插件描述 20 | plugin_desc = "删除数据库中已安装插件记录、清理插件文件。" 21 | # 插件图标 22 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/uninstall.png" 23 | # 插件版本 24 | plugin_version = "1.2.1" 25 | # 插件作者 26 | plugin_author = "thsrite" 27 | # 作者主页 28 | author_url = "https://github.com/thsrite" 29 | # 插件配置项ID前缀 30 | plugin_config_prefix = "pluginuninstall_" 31 | # 加载顺序 32 | plugin_order = 98 33 | # 可使用的用户级别 34 | auth_level = 1 35 | 36 | # 私有属性 37 | _plugin_ids = [] 38 | _clear_config = False 39 | _clear_data = False 40 | 41 | def init_plugin(self, config: dict = None): 42 | if config: 43 | self._plugin_ids = config.get("plugin_ids") or [] 44 | self._clear_config = config.get("clear_config") 45 | self._clear_data = config.get("clear_data") 46 | if not self._plugin_ids: 47 | return 48 | 49 | # 已安装插件 50 | install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or [] 51 | 52 | new_install_plugins = [] 53 | for install_plugin in install_plugins: 54 | if install_plugin in self._plugin_ids: 55 | # 移除插件服务 56 | Scheduler().remove_plugin_job(install_plugin) 57 | # 移除插件 58 | PluginManager().remove_plugin(install_plugin) 59 | # 删除插件文件 60 | plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / install_plugin.lower() 61 | if plugin_dir.exists(): 62 | shutil.rmtree(plugin_dir, ignore_errors=True) 63 | if self._clear_config: 64 | # 删除配置 65 | PluginManager().delete_plugin_config(install_plugin) 66 | if self._clear_data: 67 | # 删除插件所有数据 68 | PluginManager().delete_plugin_data(install_plugin) 69 | logger.info(f"插件 {install_plugin} 已卸载") 70 | else: 71 | new_install_plugins.append(install_plugin) 72 | 73 | # 保存已安装插件 74 | SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, new_install_plugins) 75 | 76 | self.update_config({ 77 | "plugin_ids": [], 78 | "clear_config": self._clear_config, 79 | "clear_data": self._clear_data 80 | }) 81 | 82 | def get_state(self) -> bool: 83 | return False 84 | 85 | @staticmethod 86 | def get_command() -> List[Dict[str, Any]]: 87 | pass 88 | 89 | def get_api(self) -> List[Dict[str, Any]]: 90 | pass 91 | 92 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 93 | """ 94 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 95 | """ 96 | # 已安装插件 97 | local_plugins = self.get_local_plugins() 98 | # 编历 local_plugins,生成插件类型选项 99 | pluginOptions = [] 100 | 101 | for plugin_id in list(local_plugins.keys()): 102 | local_plugin = local_plugins.get(plugin_id) 103 | pluginOptions.append({ 104 | "title": f"{local_plugin.get('plugin_name')} v{local_plugin.get('plugin_version')}", 105 | "value": local_plugin.get("id") 106 | }) 107 | return [ 108 | { 109 | 'component': 'VForm', 110 | 'content': [ 111 | { 112 | 'component': 'VRow', 113 | 'content': [ 114 | { 115 | 'component': 'VCol', 116 | 'props': { 117 | 'cols': 12, 118 | 'md': 3 119 | }, 120 | 'content': [ 121 | { 122 | 'component': 'VSwitch', 123 | 'props': { 124 | 'model': 'clear_config', 125 | 'label': '清除配置(配置信息)', 126 | } 127 | } 128 | ] 129 | }, 130 | { 131 | 'component': 'VCol', 132 | 'props': { 133 | 'cols': 12, 134 | 'md': 3 135 | }, 136 | 'content': [ 137 | { 138 | 'component': 'VSwitch', 139 | 'props': { 140 | 'model': 'clear_data', 141 | 'label': '清除数据(运行数据)', 142 | } 143 | } 144 | ] 145 | }, 146 | { 147 | 'component': 'VCol', 148 | 'props': { 149 | 'cols': 12, 150 | 'md': 6 151 | }, 152 | 'content': [ 153 | { 154 | 'component': 'VSelect', 155 | 'props': { 156 | 'multiple': True, 157 | 'chips': True, 158 | 'model': 'plugin_ids', 159 | 'label': '卸载插件', 160 | 'items': pluginOptions 161 | } 162 | } 163 | ] 164 | }, 165 | ] 166 | }, 167 | { 168 | 'component': 'VRow', 169 | 'content': [ 170 | { 171 | 'component': 'VCol', 172 | 'props': { 173 | 'cols': 12, 174 | }, 175 | 'content': [ 176 | { 177 | 'component': 'VAlert', 178 | 'props': { 179 | 'type': 'info', 180 | 'variant': 'tonal', 181 | 'text': '删除数据库中已安装插件记录、清理插件文件。' 182 | } 183 | } 184 | ] 185 | } 186 | ] 187 | }, 188 | ] 189 | } 190 | ], { 191 | "plugin_ids": [], 192 | "clear_config": False, 193 | "clear_data": False 194 | } 195 | 196 | @staticmethod 197 | def get_local_plugins(): 198 | """ 199 | 获取本地插件 200 | """ 201 | # 已安装插件 202 | install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or [] 203 | 204 | local_plugins = {} 205 | # 线上插件列表 206 | markets = settings.PLUGIN_MARKET.split(",") 207 | for market in markets: 208 | online_plugins = PluginHelper().get_plugins(market) or {} 209 | for pid, plugin in online_plugins.items(): 210 | if pid in install_plugins: 211 | local_plugin = local_plugins.get(pid) 212 | if local_plugin: 213 | if StringUtils.compare_version(local_plugin.get("plugin_version"), plugin.get("version")) < 0: 214 | local_plugins[pid] = { 215 | "id": pid, 216 | "plugin_name": plugin.get("name"), 217 | "repo_url": market, 218 | "plugin_version": plugin.get("version") 219 | } 220 | else: 221 | local_plugins[pid] = { 222 | "id": pid, 223 | "plugin_name": plugin.get("name"), 224 | "repo_url": market, 225 | "plugin_version": plugin.get("version") 226 | } 227 | 228 | return local_plugins 229 | 230 | def get_page(self) -> List[dict]: 231 | pass 232 | 233 | def stop_service(self): 234 | """ 235 | 退出插件 236 | """ 237 | pass 238 | -------------------------------------------------------------------------------- /plugins/schedulereminder/__init__.py: -------------------------------------------------------------------------------- 1 | from app.core.config import settings 2 | from app.db.site_oper import SiteOper 3 | from app.plugins import _PluginBase 4 | from typing import Any, List, Dict, Tuple, Optional 5 | from app.log import logger 6 | from apscheduler.schedulers.background import BackgroundScheduler 7 | from apscheduler.triggers.cron import CronTrigger 8 | 9 | from app.schemas import NotificationType 10 | 11 | 12 | class ScheduleReminder(_PluginBase): 13 | # 插件名称 14 | plugin_name = "日程提醒" 15 | # 插件描述 16 | plugin_desc = "自定义提醒事项、提醒时间。" 17 | # 插件图标 18 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/reminder.png" 19 | # 插件版本 20 | plugin_version = "1.0" 21 | # 插件作者 22 | plugin_author = "thsrite" 23 | # 作者主页 24 | author_url = "https://github.com/thsrite" 25 | # 插件配置项ID前缀 26 | plugin_config_prefix = "schedulereminder_" 27 | # 加载顺序 28 | plugin_order = 32 29 | # 可使用的用户级别 30 | auth_level = 1 31 | 32 | # 私有属性 33 | _enabled: bool = False 34 | _confs = None 35 | siteoper = None 36 | _scheduler: Optional[BackgroundScheduler] = None 37 | 38 | def init_plugin(self, config: dict = None): 39 | self.siteoper = SiteOper() 40 | 41 | # 停止现有任务 42 | self.stop_service() 43 | 44 | if config: 45 | self._enabled = config.get("enabled") 46 | self._confs = config.get("confs") 47 | 48 | if self._enabled and self._confs: 49 | # 周期运行 50 | self._scheduler = BackgroundScheduler(timezone=settings.TZ) 51 | 52 | # 读取目录配置 53 | confs = self._confs.split("\n") 54 | if not confs: 55 | return 56 | for conf in confs: 57 | if str(conf).count(":") != 1: 58 | logger.warn(f"{conf} 格式错误,跳过处理") 59 | continue 60 | try: 61 | self._scheduler.add_job(func=self.__send_notify, 62 | trigger=CronTrigger.from_crontab(str(conf).split(":")[1]), 63 | name=f"{str(conf).split(':')[0]}提醒", 64 | kwargs={"theme": str(conf).split(":")[0]}) 65 | except Exception as err: 66 | logger.error(f"定时任务配置错误:{err}") 67 | # 推送实时消息 68 | self.systemmessage.put(f"执行周期配置错误:{err}") 69 | 70 | # 启动任务 71 | if self._scheduler.get_jobs(): 72 | self._scheduler.print_jobs() 73 | self._scheduler.start() 74 | 75 | def __send_notify(self, theme: str): 76 | """ 77 | 同步站点cookie到cookiecloud 78 | """ 79 | self.post_message(mtype=NotificationType.Manual, 80 | title="日程提醒", 81 | text=theme) 82 | 83 | def get_state(self) -> bool: 84 | return self._enabled 85 | 86 | @staticmethod 87 | def get_command() -> List[Dict[str, Any]]: 88 | pass 89 | 90 | def get_api(self) -> List[Dict[str, Any]]: 91 | pass 92 | 93 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 94 | """ 95 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 96 | """ 97 | return [ 98 | { 99 | 'component': 'VForm', 100 | 'content': [ 101 | { 102 | 'component': 'VRow', 103 | 'content': [ 104 | { 105 | 'component': 'VCol', 106 | 'props': { 107 | 'cols': 12, 108 | }, 109 | 'content': [ 110 | { 111 | 'component': 'VSwitch', 112 | 'props': { 113 | 'model': 'enabled', 114 | 'label': '启用插件', 115 | } 116 | } 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | 'component': 'VRow', 123 | 'content': [ 124 | { 125 | 'component': 'VCol', 126 | 'props': { 127 | 'cols': 12, 128 | }, 129 | 'content': [ 130 | { 131 | 'component': 'VTextarea', 132 | 'props': { 133 | 'model': 'confs', 134 | 'label': '提醒事项', 135 | 'rows': 5, 136 | 'placeholder': '提醒内容:cron' 137 | } 138 | } 139 | ] 140 | }, 141 | ] 142 | }, 143 | { 144 | 'component': 'VRow', 145 | 'content': [ 146 | { 147 | 'component': 'VCol', 148 | 'props': { 149 | 'cols': 12, 150 | }, 151 | 'content': [ 152 | { 153 | 'component': 'VAlert', 154 | 'props': { 155 | 'type': 'info', 156 | 'variant': 'tonal', 157 | 'text': '提醒事项格式为:提醒内容:提醒时间cron表达式(一行一条)。' 158 | '需开启(手动处理通知)通知类型' 159 | } 160 | } 161 | ] 162 | } 163 | ] 164 | } 165 | ] 166 | } 167 | ], { 168 | "enabled": False, 169 | "confs": "", 170 | } 171 | 172 | def get_page(self) -> List[dict]: 173 | pass 174 | 175 | def stop_service(self): 176 | """ 177 | 退出插件 178 | """ 179 | try: 180 | if self._scheduler: 181 | self._scheduler.remove_all_jobs() 182 | if self._scheduler.running: 183 | self._scheduler.shutdown() 184 | self._scheduler = None 185 | except Exception as e: 186 | logger.error("退出插件失败:%s" % str(e)) 187 | -------------------------------------------------------------------------------- /plugins/softlinkredirect/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from typing import List, Tuple, Dict, Any 4 | from app.log import logger 5 | from app.plugins import _PluginBase 6 | 7 | 8 | class SoftLinkRedirect(_PluginBase): 9 | # 插件名称 10 | plugin_name = "软连接重定向" 11 | # 插件描述 12 | plugin_desc = "重定向软连接指向。" 13 | # 插件图标 14 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/softlinkredirect.png" 15 | # 插件版本 16 | plugin_version = "1.2" 17 | # 插件作者 18 | plugin_author = "thsrite" 19 | # 作者主页 20 | author_url = "https://github.com/thsrite" 21 | # 插件配置项ID前缀 22 | plugin_config_prefix = "softlinkredirect_" 23 | # 加载顺序 24 | plugin_order = 9 25 | # 可使用的用户级别 26 | auth_level = 2 27 | 28 | # 私有属性 29 | _onlyonce = False 30 | _soft_path = None 31 | _origin_path = None 32 | _redirect_path = None 33 | 34 | def init_plugin(self, config: dict = None): 35 | # 读取配置 36 | if config: 37 | self._onlyonce = config.get("onlyonce") 38 | self._soft_path = config.get("soft_path") 39 | self._origin_path = config.get("origin_path") 40 | self._redirect_path = config.get("redirect_path") 41 | 42 | if self._onlyonce and self._soft_path and self._origin_path and self._redirect_path: 43 | logger.info(f"{self._soft_path} 软连接重定向开始 {self._origin_path} - {self._redirect_path}") 44 | self.update_symlink(self._origin_path, self._redirect_path, self._soft_path) 45 | logger.info(f"{self._soft_path} 软连接重定向完成") 46 | self._onlyonce = False 47 | self.update_config({ 48 | "onlyonce": self._onlyonce, 49 | "soft_path": self._soft_path, 50 | "origin_path": self._origin_path, 51 | "redirect_path": self._redirect_path 52 | }) 53 | 54 | @staticmethod 55 | def update_symlink(target_from, target_to, directory): 56 | for root, dirs, files in os.walk(directory): 57 | for name in dirs + files: 58 | file_path = os.path.join(root, name) 59 | if os.path.islink(file_path): 60 | current_target = os.readlink(file_path) 61 | if str(current_target).startswith(target_from): 62 | new_target = current_target.replace(target_from, target_to) 63 | result = subprocess.run(['ln', '-sf', new_target, file_path]) 64 | logger.info( 65 | f"Updated symlink: {file_path} -> {new_target} ({'success' if result.returncode == 0 else 'failed'})") 66 | 67 | @staticmethod 68 | def get_command() -> List[Dict[str, Any]]: 69 | """ 70 | 定义远程控制命令 71 | :return: 命令关键字、事件、描述、附带数据 72 | """ 73 | pass 74 | 75 | def get_api(self) -> List[Dict[str, Any]]: 76 | pass 77 | 78 | def get_service(self) -> List[Dict[str, Any]]: 79 | """ 80 | 注册插件公共服务 81 | [{ 82 | "id": "服务ID", 83 | "name": "服务名称", 84 | "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", 85 | "func": self.xxx, 86 | "kwargs": {} # 定时器参数 87 | }] 88 | """ 89 | return [] 90 | 91 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 92 | """ 93 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 94 | """ 95 | return [ 96 | { 97 | 'component': 'VForm', 98 | 'content': [ 99 | { 100 | 'component': 'VRow', 101 | 'content': [ 102 | { 103 | 'component': 'VCol', 104 | 'props': { 105 | 'cols': 12, 106 | 'md': 3 107 | }, 108 | 'content': [ 109 | { 110 | 'component': 'VSwitch', 111 | 'props': { 112 | 'model': 'onlyonce', 113 | 'label': '立即运行', 114 | } 115 | } 116 | ] 117 | } 118 | ] 119 | }, 120 | { 121 | 'component': 'VRow', 122 | 'content': [ 123 | { 124 | 'component': 'VCol', 125 | 'props': { 126 | 'cols': 12, 127 | }, 128 | 'content': [ 129 | { 130 | 'component': 'VTextField', 131 | 'props': { 132 | 'model': 'soft_path', 133 | 'label': '软连接路径', 134 | } 135 | } 136 | ] 137 | }, 138 | ] 139 | }, 140 | { 141 | 'component': 'VRow', 142 | 'content': [ 143 | { 144 | 'component': 'VCol', 145 | 'props': { 146 | 'cols': 12, 147 | }, 148 | 'content': [ 149 | { 150 | 'component': 'VTextField', 151 | 'props': { 152 | 'model': 'origin_path', 153 | 'label': '原来源文件路径', 154 | } 155 | } 156 | ] 157 | }, 158 | ] 159 | }, 160 | { 161 | 'component': 'VRow', 162 | 'content': [ 163 | { 164 | 'component': 'VCol', 165 | 'props': { 166 | 'cols': 12, 167 | }, 168 | 'content': [ 169 | { 170 | 'component': 'VTextField', 171 | 'props': { 172 | 'model': 'redirect_path', 173 | 'label': '重定向源文件路径', 174 | } 175 | } 176 | ] 177 | } 178 | ] 179 | }, 180 | { 181 | 'component': 'VRow', 182 | 'content': [ 183 | { 184 | 'component': 'VCol', 185 | 'props': { 186 | 'cols': 12, 187 | }, 188 | 'content': [ 189 | { 190 | 'component': 'VAlert', 191 | 'props': { 192 | 'type': 'info', 193 | 'variant': 'tonal', 194 | 'text': '软连接指向由A路径改为B路径' 195 | } 196 | } 197 | ] 198 | } 199 | ] 200 | } 201 | ] 202 | } 203 | ], { 204 | "onlyonce": False, 205 | "soft_path": "", 206 | "origin_path": "", 207 | "redirect_path": "", 208 | } 209 | 210 | def get_page(self) -> List[dict]: 211 | pass 212 | 213 | def get_state(self): 214 | return False 215 | 216 | def stop_service(self): 217 | """ 218 | 退出插件 219 | """ 220 | pass 221 | -------------------------------------------------------------------------------- /plugins/sqlexecute/__init__.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from app.core.event import eventmanager, Event 4 | from app.plugins import _PluginBase 5 | from typing import Any, List, Dict, Tuple 6 | from app.log import logger 7 | from app.schemas.types import EventType, MessageChannel 8 | 9 | 10 | class SqlExecute(_PluginBase): 11 | # 插件名称 12 | plugin_name = "Sql执行器" 13 | # 插件描述 14 | plugin_desc = "自定义MoviePilot数据库Sql执行。" 15 | # 插件图标 16 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/sqlite.png" 17 | # 插件版本 18 | plugin_version = "1.3" 19 | # 插件作者 20 | plugin_author = "thsrite" 21 | # 作者主页 22 | author_url = "https://github.com/thsrite" 23 | # 插件配置项ID前缀 24 | plugin_config_prefix = "sqlexecute_" 25 | # 加载顺序 26 | plugin_order = 99 27 | # 可使用的用户级别 28 | auth_level = 1 29 | 30 | # 私有属性 31 | _onlyonce = None 32 | _sql = None 33 | 34 | def init_plugin(self, config: dict = None): 35 | if config: 36 | self._onlyonce = config.get("onlyonce") 37 | self._sql = config.get("sql") 38 | 39 | if self._onlyonce and self._sql: 40 | # 读取sqlite数据 41 | try: 42 | gradedb = sqlite3.connect("/config/user.db") 43 | except Exception as e: 44 | logger.error(f"数据库链接失败 {str(e)}") 45 | return 46 | 47 | # 创建游标cursor来执行executeSQL语句 48 | cursor = gradedb.cursor() 49 | 50 | # 执行SQL语句 51 | try: 52 | for sql in self._sql.split("\n"): 53 | logger.info(f"开始执行SQL语句 {sql}") 54 | # 执行SQL语句 55 | cursor.execute(sql) 56 | 57 | if 'select' in sql.lower(): 58 | rows = cursor.fetchall() 59 | # 获取列名 60 | columns = [desc[0] for desc in cursor.description] 61 | # 将查询结果转换为key-value对的列表 62 | results = [] 63 | for row in rows: 64 | result = dict(zip(columns, row)) 65 | results.append(result) 66 | result = "\n".join([str(i) for i in results]) 67 | result = str(result).replace("'", "\"") 68 | logger.info(result) 69 | else: 70 | gradedb.commit() 71 | result = f"执行成功,影响行数:{cursor.rowcount}" 72 | logger.info(result) 73 | except Exception as e: 74 | logger.error(f"SQL语句执行失败 {str(e)}") 75 | return 76 | finally: 77 | # 关闭游标 78 | cursor.close() 79 | 80 | self._onlyonce = False 81 | self.update_config({ 82 | "onlyonce": self._onlyonce, 83 | "sql": self._sql 84 | }) 85 | 86 | @eventmanager.register(EventType.PluginAction) 87 | def execute(self, event: Event = None): 88 | if event: 89 | event_data = event.event_data 90 | if not event_data or event_data.get("action") != "sql_execute": 91 | return 92 | args = event_data.get("args") 93 | if not args: 94 | return 95 | 96 | logger.info(f"收到命令,开始执行SQL ...{args}") 97 | 98 | # 读取sqlite数据 99 | try: 100 | gradedb = sqlite3.connect("/config/user.db") 101 | except Exception as e: 102 | logger.error(f"数据库链接失败 {str(e)}") 103 | return 104 | 105 | # 创建游标cursor来执行executeSQL语句 106 | cursor = gradedb.cursor() 107 | 108 | # 执行SQL语句 109 | try: 110 | # 执行SQL语句 111 | cursor.execute(args) 112 | if 'select' in args.lower(): 113 | rows = cursor.fetchall() 114 | # 获取列名 115 | columns = [desc[0] for desc in cursor.description] 116 | # 将查询结果转换为key-value对的列表 117 | results = [] 118 | for row in rows: 119 | result = dict(zip(columns, row)) 120 | results.append(result) 121 | result = "\n".join([str(i) for i in results]) 122 | result = str(result).replace("'", "\"") 123 | logger.info(result) 124 | 125 | if event.event_data.get("channel") == MessageChannel.Telegram: 126 | result = f"```plaintext\n{result}\n```" 127 | self.post_message(channel=event.event_data.get("channel"), 128 | title="SQL执行结果", 129 | text=result, 130 | userid=event.event_data.get("user")) 131 | else: 132 | gradedb.commit() 133 | result = f"执行成功,影响行数:{cursor.rowcount}" 134 | logger.info(result) 135 | 136 | if event.event_data.get("channel") == MessageChannel.Telegram: 137 | result = f"```plaintext\n{result}\n```" 138 | self.post_message(channel=event.event_data.get("channel"), 139 | title="SQL执行结果", 140 | text=result, 141 | userid=event.event_data.get("user")) 142 | 143 | except Exception as e: 144 | logger.error(f"SQL语句执行失败 {str(e)}") 145 | return 146 | finally: 147 | # 关闭游标 148 | cursor.close() 149 | 150 | def get_state(self) -> bool: 151 | return True 152 | 153 | @staticmethod 154 | def get_command() -> List[Dict[str, Any]]: 155 | """ 156 | 定义远程控制命令 157 | :return: 命令关键字、事件、描述、附带数据 158 | """ 159 | return [{ 160 | "cmd": "/sql", 161 | "event": EventType.PluginAction, 162 | "desc": "自定义sql执行", 163 | "category": "", 164 | "data": { 165 | "action": "sql_execute" 166 | } 167 | }] 168 | 169 | def get_api(self) -> List[Dict[str, Any]]: 170 | pass 171 | 172 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 173 | """ 174 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 175 | """ 176 | return [ 177 | { 178 | 'component': 'VForm', 179 | 'content': [ 180 | { 181 | 'component': 'VRow', 182 | 'content': [ 183 | { 184 | 'component': 'VCol', 185 | 'props': { 186 | 'cols': 12, 187 | 'md': 6 188 | }, 189 | 'content': [ 190 | { 191 | 'component': 'VSwitch', 192 | 'props': { 193 | 'model': 'onlyonce', 194 | 'label': '执行sql' 195 | } 196 | } 197 | ] 198 | } 199 | ] 200 | }, 201 | { 202 | 'component': 'VRow', 203 | 'content': [ 204 | { 205 | 'component': 'VCol', 206 | 'props': { 207 | 'cols': 12, 208 | }, 209 | 'content': [ 210 | { 211 | 'component': 'VTextarea', 212 | 'props': { 213 | 'model': 'sql', 214 | 'rows': '2', 215 | 'label': 'sql语句', 216 | 'placeholder': '一行一条' 217 | } 218 | } 219 | ] 220 | } 221 | ] 222 | }, 223 | { 224 | 'component': 'VRow', 225 | 'content': [ 226 | { 227 | 'component': 'VCol', 228 | 'props': { 229 | 'cols': 12, 230 | }, 231 | 'content': [ 232 | { 233 | 'component': 'VAlert', 234 | 'props': { 235 | 'type': 'info', 236 | 'variant': 'tonal' 237 | }, 238 | 'content': [ 239 | { 240 | 'component': 'span', 241 | 'text': '执行日志将会输出到控制台,请谨慎操作。' 242 | } 243 | ] 244 | } 245 | ] 246 | } 247 | ] 248 | }, 249 | { 250 | 'component': 'VRow', 251 | 'content': [ 252 | { 253 | 'component': 'VCol', 254 | 'props': { 255 | 'cols': 12, 256 | }, 257 | 'content': [ 258 | { 259 | 'component': 'VAlert', 260 | 'props': { 261 | 'type': 'info', 262 | 'variant': 'tonal' 263 | }, 264 | 'content': [ 265 | { 266 | 'component': 'span', 267 | 'text': '可使用交互命令/sql select *****' 268 | } 269 | ] 270 | } 271 | ] 272 | } 273 | ] 274 | } 275 | ] 276 | } 277 | ], { 278 | "onlyonce": False, 279 | "sql": "", 280 | } 281 | 282 | def get_page(self) -> List[dict]: 283 | pass 284 | 285 | def stop_service(self): 286 | """ 287 | 退出插件 288 | """ 289 | pass 290 | -------------------------------------------------------------------------------- /plugins/strmconvert/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib.parse 3 | from pathlib import Path 4 | 5 | from app.plugins import _PluginBase 6 | from typing import Any, List, Dict, Tuple 7 | from app.log import logger 8 | 9 | 10 | class StrmConvert(_PluginBase): 11 | # 插件名称 12 | plugin_name = "Strm文件模式转换" 13 | # 插件描述 14 | plugin_desc = "Strm文件内容转为本地路径或者cd2/alist API路径。" 15 | # 插件图标 16 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/convert.png" 17 | # 插件版本 18 | plugin_version = "1.0" 19 | # 插件作者 20 | plugin_author = "thsrite" 21 | # 作者主页 22 | author_url = "https://github.com/thsrite" 23 | # 插件配置项ID前缀 24 | plugin_config_prefix = "strmconvert_" 25 | # 加载顺序 26 | plugin_order = 27 27 | # 可使用的用户级别 28 | auth_level = 1 29 | 30 | # 私有属性 31 | _to_local = False 32 | _to_api = False 33 | _convert_confs = None 34 | _library_path = None 35 | _api_url = None 36 | 37 | def init_plugin(self, config: dict = None): 38 | if config: 39 | self._to_local = config.get("to_local") 40 | self._to_api = config.get("to_api") 41 | self._convert_confs = config.get("convert_confs") 42 | 43 | if self._to_local and self._to_api: 44 | logger.error(f"本地模式和API模式同时只能开启一个") 45 | return 46 | 47 | convert_confs = self._convert_confs.split("\n") 48 | if not convert_confs: 49 | return 50 | 51 | self.update_config({ 52 | "to_local": False, 53 | "to_api": False, 54 | "convert_confs": self._convert_confs 55 | }) 56 | 57 | if self._to_local: 58 | self.__convert_to_local(convert_confs) 59 | 60 | if self._to_api: 61 | self.__convert_to_api(convert_confs) 62 | 63 | def __convert_to_local(self, convert_confs: list): 64 | """ 65 | 转为本地模式 66 | """ 67 | for convert_conf in convert_confs: 68 | if str(convert_conf).count("#") != 1: 69 | logger.error(f"转换配置 {convert_conf} 格式错误,已跳过处理") 70 | continue 71 | source_path = str(convert_conf).split("#")[0] 72 | library_path = str(convert_conf).split("#")[1] 73 | logger.info(f"{source_path} 开始转为本地模式") 74 | self.__to_local(source_path, library_path) 75 | logger.info(f"{source_path} 转换本地模式已结束") 76 | 77 | def __to_local(self, source_path: str, library_path: str): 78 | files = self.__list_files(Path(source_path), ['.strm']) 79 | for f in files: 80 | logger.debug(f"开始处理文件 {f}") 81 | try: 82 | with open(f, 'r') as file: 83 | content = file.read() 84 | # 获取扩展名 85 | ext = str(content).split(".")[-1] 86 | library_file = str(f).replace(source_path, library_path) 87 | library_file = Path(library_file).parent.joinpath(Path(library_file).stem + "." + ext) 88 | with open(f, 'w') as file2: 89 | logger.debug(f"开始写入 媒体库路径 {library_file}") 90 | file2.write(str(library_file)) 91 | except Exception as e: 92 | print(e) 93 | 94 | def __convert_to_api(self, convert_confs: list): 95 | """ 96 | 转为api模式 97 | """ 98 | for convert_conf in convert_confs: 99 | if str(convert_conf).count("#") != 3: 100 | logger.error(f"转换配置 {convert_conf} 格式错误,已跳过处理") 101 | continue 102 | source_path = str(convert_conf).split("#")[0] 103 | library_path = str(convert_conf).split("#")[1] 104 | cloud_type = str(convert_conf).split("#")[2] 105 | cloud_url = str(convert_conf).split("#")[3] 106 | logger.info(f"{source_path} 开始转为API模式") 107 | self.__to_api(source_path, library_path, cloud_type, cloud_url) 108 | logger.info(f"{source_path} 转换本地模式已结束") 109 | 110 | def __to_api(self, source_path: str, library_path: str, cloud_type: str, cloud_url: str): 111 | files = self.__list_files(Path(source_path), ['.strm']) 112 | for f in files: 113 | logger.debug(f"开始处理文件 {f}") 114 | try: 115 | library_file = str(f).replace(source_path, library_path) 116 | # 对盘符之后的所有内容进行url转码 117 | library_file = urllib.parse.quote(library_file, safe='') 118 | 119 | if str(cloud_type) == "cd2": 120 | # 将路径的开头盘符"/mnt/user/downloads"替换为"http://localhost:19798/static/http/localhost:19798/False/" 121 | # http://192.168.31.103:19798/static/http/192.168.31.103:19798/False/%2F115%2Femby%2Fanime%2F%20%E4%B8%83%E9%BE%99%E7%8F%A0%20%281986%29%2FSeason%201.%E5%9B%BD%E8%AF%AD%2F%E4%B8%83%E9%BE%99%E7%8F%A0%20-%20S01E002%20-%201080p%20AAC%20h264.mp4 122 | api_file = f"http://{cloud_url}/static/http/{cloud_url}/False/{library_file}" 123 | else: 124 | api_file = f"http://{cloud_url}/d/{library_file}" 125 | with open(f, 'w') as file2: 126 | logger.debug(f"开始写入 api路径 {api_file}") 127 | file2.write(str(api_file)) 128 | except Exception as e: 129 | print(e) 130 | 131 | @staticmethod 132 | def __list_files(directory: Path, extensions: list, min_filesize: int = 0) -> List[Path]: 133 | """ 134 | 获取目录下所有指定扩展名的文件(包括子目录) 135 | """ 136 | if not min_filesize: 137 | min_filesize = 0 138 | 139 | if not directory.exists(): 140 | return [] 141 | 142 | if directory.is_file(): 143 | return [directory] 144 | 145 | if not min_filesize: 146 | min_filesize = 0 147 | 148 | files = [] 149 | pattern = r".*(" + "|".join(extensions) + ")$" 150 | 151 | # 遍历目录及子目录 152 | for path in directory.rglob('**/*'): 153 | if path.is_file() \ 154 | and re.match(pattern, path.name, re.IGNORECASE) \ 155 | and path.stat().st_size >= min_filesize * 1024 * 1024: 156 | files.append(path) 157 | 158 | return files 159 | 160 | def get_state(self) -> bool: 161 | return False 162 | 163 | @staticmethod 164 | def get_command() -> List[Dict[str, Any]]: 165 | pass 166 | 167 | def get_api(self) -> List[Dict[str, Any]]: 168 | pass 169 | 170 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 171 | """ 172 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 173 | """ 174 | return [ 175 | { 176 | 'component': 'VForm', 177 | 'content': [ 178 | { 179 | 'component': 'VRow', 180 | 'content': [ 181 | { 182 | 'component': 'VCol', 183 | 'props': { 184 | 'cols': 12, 185 | 'md': 6 186 | }, 187 | 'content': [ 188 | { 189 | 'component': 'VSwitch', 190 | 'props': { 191 | 'model': 'to_local', 192 | 'label': '转为本地模式', 193 | } 194 | } 195 | ] 196 | }, 197 | { 198 | 'component': 'VCol', 199 | 'props': { 200 | 'cols': 12, 201 | 'md': 6 202 | }, 203 | 'content': [ 204 | { 205 | 'component': 'VSwitch', 206 | 'props': { 207 | 'model': 'to_api', 208 | 'label': '转为API模式', 209 | } 210 | } 211 | ] 212 | } 213 | ] 214 | }, 215 | { 216 | 'component': 'VRow', 217 | 'content': [ 218 | { 219 | 'component': 'VCol', 220 | 'props': { 221 | 'cols': 12 222 | }, 223 | 'content': [ 224 | { 225 | 'component': 'VTextarea', 226 | 'props': { 227 | 'model': 'convert_confs', 228 | 'label': '转换配置', 229 | 'rows': 3, 230 | 'placeholder': 'strm文件根路径#转换路径' 231 | } 232 | } 233 | ] 234 | }, 235 | ] 236 | }, 237 | { 238 | 'component': 'VRow', 239 | 'content': [ 240 | { 241 | 'component': 'VCol', 242 | 'props': { 243 | 'cols': 12, 244 | }, 245 | 'content': [ 246 | { 247 | 'component': 'VAlert', 248 | 'props': { 249 | 'type': 'info', 250 | 'variant': 'tonal', 251 | 'text': '转换配置(转为本地模式):' 252 | 'strm文件根路径#转换路径。' 253 | '转换路径为源文件挂载进媒体服务器的路径。' 254 | } 255 | } 256 | ] 257 | } 258 | ] 259 | }, 260 | { 261 | 'component': 'VRow', 262 | 'content': [ 263 | { 264 | 'component': 'VCol', 265 | 'props': { 266 | 'cols': 12, 267 | }, 268 | 'content': [ 269 | { 270 | 'component': 'VAlert', 271 | 'props': { 272 | 'type': 'info', 273 | 'variant': 'tonal', 274 | 'text': '转换配置(转为API模式):' 275 | 'strm文件根路径#转换路径#cd2/alist#cd2/alist服务地址(ip:port)。' 276 | '转换路径为云盘根路径。' 277 | } 278 | } 279 | ] 280 | } 281 | ] 282 | }, 283 | { 284 | 'component': 'VRow', 285 | 'content': [ 286 | { 287 | 'component': 'VCol', 288 | 'props': { 289 | 'cols': 12, 290 | }, 291 | 'content': [ 292 | { 293 | 'component': 'VAlert', 294 | 'props': { 295 | 'type': 'info', 296 | 'variant': 'tonal', 297 | 'text': '配置说明:' 298 | 'https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/docs/StrmConvert.md' 299 | } 300 | } 301 | ] 302 | } 303 | ] 304 | } 305 | ] 306 | } 307 | ], { 308 | "to_local": False, 309 | "to_api": False, 310 | "convert_confs": "" 311 | } 312 | 313 | def get_page(self) -> List[dict]: 314 | pass 315 | 316 | def stop_service(self): 317 | """ 318 | 退出插件 319 | """ 320 | pass 321 | -------------------------------------------------------------------------------- /plugins/subscribeclear/__init__.py: -------------------------------------------------------------------------------- 1 | from app.plugins import _PluginBase 2 | from app.db.subscribe_oper import SubscribeOper 3 | from typing import Any, List, Dict, Tuple 4 | from app.log import logger 5 | 6 | 7 | class SubscribeClear(_PluginBase): 8 | # 插件名称 9 | plugin_name = "清理订阅缓存" 10 | # 插件描述 11 | plugin_desc = "清理订阅已下载集数。" 12 | # 插件图标 13 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/broom.png" 14 | # 插件版本 15 | plugin_version = "1.0" 16 | # 插件作者 17 | plugin_author = "thsrite" 18 | # 作者主页 19 | author_url = "https://github.com/thsrite" 20 | # 插件配置项ID前缀 21 | plugin_config_prefix = "subscribeclear_" 22 | # 加载顺序 23 | plugin_order = 28 24 | # 可使用的用户级别 25 | auth_level = 1 26 | 27 | # 任务执行间隔 28 | _subscribe_ids = None 29 | subscribe = None 30 | 31 | def init_plugin(self, config: dict = None): 32 | self.subscribe = SubscribeOper() 33 | if config: 34 | self._subscribe_ids = config.get("subscribe_ids") 35 | if self._subscribe_ids: 36 | # 遍历 清理订阅下载缓存 37 | for subscribe_id in self._subscribe_ids: 38 | self.subscribe.update(subscribe_id, {'note': ""}) 39 | logger.info(f"订阅 {subscribe_id} 下载缓存已清理") 40 | 41 | self.update_config( 42 | { 43 | "subscribe_ids": [] 44 | } 45 | ) 46 | 47 | def get_state(self) -> bool: 48 | return False 49 | 50 | @staticmethod 51 | def get_command() -> List[Dict[str, Any]]: 52 | pass 53 | 54 | def get_api(self) -> List[Dict[str, Any]]: 55 | pass 56 | 57 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 58 | """ 59 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 60 | """ 61 | subscribe_options = [{"title": subscribe.name, "value": subscribe.id} for subscribe in 62 | self.subscribe.list('R') if subscribe.type == '电视剧'] 63 | return [ 64 | { 65 | 'component': 'VForm', 66 | 'content': [ 67 | { 68 | 'component': 'VRow', 69 | 'content': [ 70 | { 71 | 'component': 'VCol', 72 | 'content': [ 73 | { 74 | 'component': 'VSelect', 75 | 'props': { 76 | 'chips': True, 77 | 'multiple': True, 78 | 'model': 'subscribe_ids', 79 | 'label': '电视剧订阅', 80 | 'items': subscribe_options 81 | } 82 | } 83 | ] 84 | } 85 | ] 86 | }, 87 | { 88 | 'component': 'VRow', 89 | 'content': [ 90 | { 91 | 'component': 'VCol', 92 | 'props': { 93 | 'cols': 12, 94 | }, 95 | 'content': [ 96 | { 97 | 'component': 'VAlert', 98 | 'props': { 99 | 'type': 'info', 100 | 'variant': 'tonal', 101 | 'text': '请选择需要清理缓存的订阅,用于清理该订阅已下载集数。' 102 | '注意!!!未入库的会被重新下载。' 103 | } 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | ] 110 | } 111 | ], { 112 | "subscribe_ids": [] 113 | } 114 | 115 | def get_page(self) -> List[dict]: 116 | pass 117 | 118 | def stop_service(self): 119 | """ 120 | 退出插件 121 | """ 122 | pass 123 | -------------------------------------------------------------------------------- /plugins/synccookiecloud/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | from hashlib import md5 4 | from urllib.parse import urlparse 5 | 6 | import pytz 7 | 8 | from app.core.config import settings 9 | from app.db.site_oper import SiteOper 10 | from app.plugins import _PluginBase 11 | from typing import Any, List, Dict, Tuple, Optional 12 | from app.log import logger 13 | from apscheduler.schedulers.background import BackgroundScheduler 14 | from apscheduler.triggers.cron import CronTrigger 15 | from app.utils.common import encrypt, decrypt 16 | 17 | 18 | class SyncCookieCloud(_PluginBase): 19 | # 插件名称 20 | plugin_name = "同步CookieCloud" 21 | # 插件描述 22 | plugin_desc = "同步MoviePilot站点Cookie到本地CookieCloud。" 23 | # 插件图标 24 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/cookiecloud.png" 25 | # 插件版本 26 | plugin_version = "1.3" 27 | # 插件作者 28 | plugin_author = "thsrite" 29 | # 作者主页 30 | author_url = "https://github.com/thsrite" 31 | # 插件配置项ID前缀 32 | plugin_config_prefix = "synccookiecloud_" 33 | # 加载顺序 34 | plugin_order = 28 35 | # 可使用的用户级别 36 | auth_level = 1 37 | 38 | # 私有属性 39 | _enabled: bool = False 40 | _onlyonce: bool = False 41 | _cron: str = "" 42 | siteoper = None 43 | _scheduler: Optional[BackgroundScheduler] = None 44 | 45 | def init_plugin(self, config: dict = None): 46 | self.siteoper = SiteOper() 47 | 48 | # 停止现有任务 49 | self.stop_service() 50 | 51 | if config: 52 | self._enabled = config.get("enabled") 53 | self._onlyonce = config.get("onlyonce") 54 | self._cron = config.get("cron") 55 | 56 | if self._enabled or self._onlyonce: 57 | # 定时服务 58 | self._scheduler = BackgroundScheduler(timezone=settings.TZ) 59 | 60 | # 立即运行一次 61 | if self._onlyonce: 62 | logger.info(f"同步CookieCloud服务启动,立即运行一次") 63 | self._scheduler.add_job(self.__sync_to_cookiecloud, 'date', 64 | run_date=datetime.now( 65 | tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), 66 | name="同步CookieCloud") 67 | # 关闭一次性开关 68 | self._onlyonce = False 69 | 70 | # 保存配置 71 | self.__update_config() 72 | 73 | # 周期运行 74 | if self._cron: 75 | try: 76 | self._scheduler.add_job(func=self.__sync_to_cookiecloud, 77 | trigger=CronTrigger.from_crontab(self._cron), 78 | name="同步CookieCloud") 79 | except Exception as err: 80 | logger.error(f"定时任务配置错误:{err}") 81 | # 推送实时消息 82 | self.systemmessage.put(f"执行周期配置错误:{err}") 83 | 84 | # 启动任务 85 | if self._scheduler.get_jobs(): 86 | self._scheduler.print_jobs() 87 | self._scheduler.start() 88 | 89 | def __sync_to_cookiecloud(self): 90 | """ 91 | 同步站点cookie到cookiecloud 92 | """ 93 | # 获取所有站点 94 | sites = self.siteoper.list_order_by_pri() 95 | if not sites: 96 | return 97 | 98 | if not settings.COOKIECLOUD_ENABLE_LOCAL: 99 | logger.error('本地CookieCloud服务器未启用') 100 | return 101 | 102 | cookies = {} 103 | for site in sites: 104 | domain = urlparse(site.url).netloc 105 | cookie = site.cookie 106 | 107 | if not cookie: 108 | logger.error(f"站点{domain}无cookie,跳过处理") 109 | continue 110 | 111 | # 解析cookie 112 | site_cookies = [] 113 | for ck in cookie.split(";"): 114 | site_cookies.append({ 115 | "domain": domain, 116 | "name": ck.split("=")[0], 117 | "value": ck.split("=")[1] 118 | }) 119 | # 存储cookies 120 | cookies[domain] = site_cookies 121 | if cookies: 122 | decrypted_cookies_data = self.__download() 123 | if decrypted_cookies_data: 124 | update_data = self.__update_to_cloud(cookies, decrypted_cookies_data) 125 | crypt_key = self._get_crypt_key() 126 | try: 127 | cookies = {'cookie_data': update_data} 128 | encrypted_data = encrypt(json.dumps(cookies).encode('utf-8'), crypt_key).decode('utf-8') 129 | except Exception as e: 130 | logger.error(f"CookieCloud加密失败,{e}") 131 | return 132 | 133 | ck = {'encrypted': encrypted_data} 134 | file = open(settings.COOKIE_PATH / f'{settings.COOKIECLOUD_KEY}.json', 'w') 135 | file.write(json.dumps(ck)) 136 | file.close() 137 | 138 | logger.info(f"------当前储存的cookie数据------") 139 | logger.info(cookies) 140 | logger.info(f"------当前储存的cookie数据------") 141 | logger.info(f"同步站点cookie到CookieCloud成功") 142 | 143 | def __download(self): 144 | """ 145 | 获取并解密本地CookieCloud数据 146 | """ 147 | encrypt_data = self.__load_local_encrypt_data(uuid=self._get_crypt_key()) 148 | if not encrypt_data: 149 | return {}, "未获取到本地CookieCloud数据" 150 | encrypted = encrypt_data.get("encrypted") 151 | if not encrypted: 152 | return {}, "未获取到cookie密文" 153 | else: 154 | crypt_key = self._get_crypt_key() 155 | try: 156 | decrypted_data = decrypt(encrypted, crypt_key).decode('utf-8') 157 | result = json.loads(decrypted_data) 158 | except Exception as e: 159 | return {}, "cookie解密失败:" + str(e) 160 | 161 | if not result: 162 | return {}, "cookie解密为空" 163 | 164 | if result.get("cookie_data"): 165 | contents = result.get("cookie_data") 166 | else: 167 | contents = result 168 | return contents 169 | 170 | def __load_local_encrypt_data(self, uuid: bytes) -> Dict[str, Any]: 171 | """ 172 | 加载本地CookieCloud加密数据 173 | """ 174 | file_path = settings.COOKIE_PATH / f"{settings.COOKIECLOUD_KEY}.json" 175 | # 检查文件是否存在 176 | if not file_path.exists(): 177 | return {} 178 | 179 | # 读取文件 180 | with open(file_path, encoding="utf-8", mode="r") as file: 181 | read_content = file.read() 182 | data = json.loads(read_content.encode("utf-8")) 183 | return data 184 | 185 | def __update_to_cloud(self, in_list, out_list): 186 | """ 187 | 构建站点数据 188 | """ 189 | # 清除空值 190 | out_list = {key: value for key, value in out_list.items() if value} 191 | 192 | temp_list = {} 193 | for domain in in_list.keys(): 194 | # 构建站点数据模板 195 | template = {} 196 | for domain_out in out_list: 197 | if domain.endswith(domain_out): 198 | for d in out_list[domain_out]: 199 | for key, value in d.items(): 200 | if key not in template: 201 | template[key] = value 202 | 203 | # 构建站点新数据 204 | temp_list[domain] = [] 205 | for d1 in in_list[domain]: 206 | temp_dict = {k: template.get(k, "") for k in template.keys()} 207 | temp_dict.update(d1) 208 | temp_list[domain].append(temp_dict) 209 | 210 | # 覆盖修改源站点数据 211 | for temp_domain in temp_list.keys(): 212 | found_match = False 213 | for idx, domain2 in enumerate(out_list): 214 | if temp_domain.endswith(domain2): 215 | out_list[temp_domain] = out_list.pop(domain2) 216 | out_list[temp_domain] = temp_list[temp_domain] 217 | found_match = True 218 | break 219 | if not found_match: 220 | out_list[temp_domain] = temp_list[temp_domain] 221 | return out_list 222 | 223 | def _get_crypt_key(self) -> bytes: 224 | """ 225 | 使用UUID和密码生成CookieCloud的加解密密钥 226 | """ 227 | md5_generator = md5() 228 | md5_generator.update( 229 | (str(settings.COOKIECLOUD_KEY).strip() + '-' + str(settings.COOKIECLOUD_PASSWORD).strip()).encode('utf-8')) 230 | return (md5_generator.hexdigest()[:16]).encode('utf-8') 231 | 232 | def __update_config(self): 233 | self.update_config({ 234 | "enabled": self._enabled, 235 | "onlyonce": self._onlyonce, 236 | "cron": self._cron 237 | }) 238 | 239 | def get_state(self) -> bool: 240 | return self._enabled 241 | 242 | @staticmethod 243 | def get_command() -> List[Dict[str, Any]]: 244 | pass 245 | 246 | def get_api(self) -> List[Dict[str, Any]]: 247 | pass 248 | 249 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 250 | """ 251 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 252 | """ 253 | return [ 254 | { 255 | 'component': 'VForm', 256 | 'content': [ 257 | { 258 | 'component': 'VRow', 259 | 'content': [ 260 | { 261 | 'component': 'VCol', 262 | 'props': { 263 | 'cols': 12, 264 | 'md': 6 265 | }, 266 | 'content': [ 267 | { 268 | 'component': 'VSwitch', 269 | 'props': { 270 | 'model': 'enabled', 271 | 'label': '启用插件', 272 | } 273 | } 274 | ] 275 | }, 276 | { 277 | 'component': 'VCol', 278 | 'props': { 279 | 'cols': 12, 280 | 'md': 6 281 | }, 282 | 'content': [ 283 | { 284 | 'component': 'VSwitch', 285 | 'props': { 286 | 'model': 'onlyonce', 287 | 'label': '立即运行一次', 288 | } 289 | } 290 | ] 291 | } 292 | ] 293 | }, 294 | { 295 | 'component': 'VRow', 296 | 'content': [ 297 | { 298 | 'component': 'VCol', 299 | 'props': { 300 | 'cols': 12, 301 | }, 302 | 'content': [ 303 | { 304 | 'component': 'VTextField', 305 | 'props': { 306 | 'model': 'cron', 307 | 'label': '执行周期', 308 | 'placeholder': '5位cron表达式,留空自动' 309 | } 310 | } 311 | ] 312 | }, 313 | ] 314 | }, 315 | { 316 | 'component': 'VRow', 317 | 'content': [ 318 | { 319 | 'component': 'VCol', 320 | 'props': { 321 | 'cols': 12, 322 | }, 323 | 'content': [ 324 | { 325 | 'component': 'VAlert', 326 | 'props': { 327 | 'type': 'info', 328 | 'variant': 'tonal', 329 | 'text': '需要MoviePilot设定-站点启用本地CookieCloud服务器。' 330 | } 331 | } 332 | ] 333 | } 334 | ] 335 | }, 336 | ] 337 | } 338 | ], { 339 | "enabled": False, 340 | "onlyonce": False, 341 | "cron": "5 1 * * *", 342 | } 343 | 344 | def get_page(self) -> List[dict]: 345 | pass 346 | 347 | def stop_service(self): 348 | """ 349 | 退出插件 350 | """ 351 | try: 352 | if self._scheduler: 353 | self._scheduler.remove_all_jobs() 354 | if self._scheduler.running: 355 | self._scheduler.shutdown() 356 | self._scheduler = None 357 | except Exception as e: 358 | logger.error("退出插件失败:%s" % str(e)) 359 | -------------------------------------------------------------------------------- /plugins/synologynotify/__init__.py: -------------------------------------------------------------------------------- 1 | from app.plugins import _PluginBase 2 | from typing import Any, List, Dict, Tuple 3 | from app.log import logger 4 | from app.schemas import NotificationType 5 | from app import schemas 6 | 7 | 8 | class SynologyNotify(_PluginBase): 9 | # 插件名称 10 | plugin_name = "群辉Webhook通知" 11 | # 插件描述 12 | plugin_desc = "接收群辉webhook通知并推送。" 13 | # 插件图标 14 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/synology.png" 15 | # 插件版本 16 | plugin_version = "1.2" 17 | # 插件作者 18 | plugin_author = "thsrite" 19 | # 作者主页 20 | author_url = "https://github.com/thsrite" 21 | # 插件配置项ID前缀 22 | plugin_config_prefix = "synologynotify_" 23 | # 加载顺序 24 | plugin_order = 30 25 | # 可使用的用户级别 26 | auth_level = 1 27 | 28 | # 任务执行间隔 29 | _enabled = False 30 | _notify = False 31 | _msgtype = None 32 | 33 | def init_plugin(self, config: dict = None): 34 | if config: 35 | self._enabled = config.get("enabled") 36 | self._notify = config.get("notify") 37 | self._msgtype = config.get("msgtype") 38 | 39 | def send_notify(self, text: str) -> schemas.Response: 40 | """ 41 | 发送通知 42 | """ 43 | logger.info(f"收到webhook消息啦。。。 {text}") 44 | if self._enabled and self._notify: 45 | mtype = NotificationType.Manual 46 | if self._msgtype: 47 | mtype = NotificationType.__getitem__(str(self._msgtype)) or NotificationType.Manual 48 | self.post_message(title="群辉通知", 49 | mtype=mtype, 50 | text=text) 51 | 52 | return schemas.Response( 53 | success=True, 54 | message="发送成功" 55 | ) 56 | 57 | def get_state(self) -> bool: 58 | return self._enabled 59 | 60 | @staticmethod 61 | def get_command() -> List[Dict[str, Any]]: 62 | pass 63 | 64 | def get_api(self) -> List[Dict[str, Any]]: 65 | """ 66 | 获取插件API 67 | [{ 68 | "path": "/xx", 69 | "endpoint": self.xxx, 70 | "methods": ["GET", "POST"], 71 | "summary": "API说明" 72 | }] 73 | """ 74 | return [{ 75 | "path": "/webhook", 76 | "endpoint": self.send_notify, 77 | "methods": ["GET"], 78 | "summary": "群辉webhook", 79 | "description": "接受群辉webhook通知并推送", 80 | }] 81 | 82 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 83 | """ 84 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 85 | """ 86 | # 编历 NotificationType 枚举,生成消息类型选项 87 | MsgTypeOptions = [] 88 | for item in NotificationType: 89 | MsgTypeOptions.append({ 90 | "title": item.value, 91 | "value": item.name 92 | }) 93 | return [ 94 | { 95 | 'component': 'VForm', 96 | 'content': [ 97 | { 98 | 'component': 'VRow', 99 | 'content': [ 100 | { 101 | 'component': 'VCol', 102 | 'props': { 103 | 'cols': 12, 104 | 'md': 6 105 | }, 106 | 'content': [ 107 | { 108 | 'component': 'VSwitch', 109 | 'props': { 110 | 'model': 'enabled', 111 | 'label': '启用插件', 112 | } 113 | } 114 | ] 115 | }, 116 | { 117 | 'component': 'VCol', 118 | 'props': { 119 | 'cols': 12, 120 | 'md': 6 121 | }, 122 | 'content': [ 123 | { 124 | 'component': 'VSwitch', 125 | 'props': { 126 | 'model': 'notify', 127 | 'label': '开启通知', 128 | } 129 | } 130 | ] 131 | }, 132 | ] 133 | }, 134 | { 135 | 'component': 'VRow', 136 | 'content': [ 137 | { 138 | 'component': 'VCol', 139 | 'props': { 140 | 'cols': 12 141 | }, 142 | 'content': [ 143 | { 144 | 'component': 'VSelect', 145 | 'props': { 146 | 'multiple': False, 147 | 'chips': True, 148 | 'model': 'msgtype', 149 | 'label': '消息类型', 150 | 'items': MsgTypeOptions 151 | } 152 | } 153 | ] 154 | } 155 | ] 156 | }, 157 | { 158 | 'component': 'VRow', 159 | 'content': [ 160 | { 161 | 'component': 'VCol', 162 | 'props': { 163 | 'cols': 12, 164 | }, 165 | 'content': [ 166 | { 167 | 'component': 'VAlert', 168 | 'props': { 169 | 'type': 'info', 170 | 'variant': 'tonal', 171 | 'text': '群辉webhook配置http://ip:3001/api/v1/plugin/SynologyNotify/webhook?apikey=*****&text=hello world。' 172 | 'text参数类型是消息内容。此插件安装完需要重启生效api。消息类型默认为手动处理通知。' 173 | } 174 | } 175 | ] 176 | } 177 | ] 178 | }, 179 | { 180 | 'component': 'VRow', 181 | 'content': [ 182 | { 183 | 'component': 'VCol', 184 | 'props': { 185 | 'cols': 12, 186 | }, 187 | 'content': [ 188 | { 189 | 'component': 'VAlert', 190 | 'props': { 191 | 'type': 'info', 192 | 'variant': 'tonal', 193 | 'text': '如安装完插件后,群晖发送webhook提示404,重启MoviePilot即可。' 194 | } 195 | } 196 | ] 197 | } 198 | ] 199 | } 200 | ] 201 | } 202 | ], { 203 | "enabled": False, 204 | "notify": False, 205 | "msgtype": "" 206 | } 207 | 208 | def get_page(self) -> List[dict]: 209 | pass 210 | 211 | def stop_service(self): 212 | """ 213 | 退出插件 214 | """ 215 | pass 216 | -------------------------------------------------------------------------------- /plugins/urlredirect/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import requests 4 | 5 | from app.plugins import _PluginBase 6 | from typing import Any, List, Dict, Tuple 7 | from app.log import logger 8 | from app.schemas import NotificationType 9 | from app import schemas 10 | 11 | 12 | class UrlRedirect(_PluginBase): 13 | # 插件名称 14 | plugin_name = "UrlRedirect" 15 | # 插件描述 16 | plugin_desc = "访问指定url返回Header Location指定的地址。" 17 | # 插件图标 18 | plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/synology.png" 19 | # 插件版本 20 | plugin_version = "1.0" 21 | # 插件作者 22 | plugin_author = "thsrite" 23 | # 作者主页 24 | author_url = "https://github.com/thsrite" 25 | # 插件配置项ID前缀 26 | plugin_config_prefix = "urlredirect_" 27 | # 加载顺序 28 | plugin_order = 30 29 | # 可使用的用户级别 30 | auth_level = 2 31 | 32 | # 任务执行间隔 33 | _enabled = False 34 | _notify = False 35 | _msgtype = None 36 | 37 | def init_plugin(self, config: dict = None): 38 | if config: 39 | self._enabled = config.get("enabled") 40 | self._notify = config.get("notify") 41 | self._msgtype = config.get("msgtype") 42 | 43 | def redirect(self, url: str, ua: str) -> schemas.Response: 44 | """ 45 | 发送通知 46 | """ 47 | logger.info(f"收到请求 {url}") 48 | if self._enabled: 49 | headers = { 50 | "User-Agent": ua, 51 | } 52 | 53 | try: 54 | start_time = time.time() # Record the start time 55 | response = requests.head(url, headers=headers, allow_redirects=True) 56 | end_time = time.time() # Record the end time 57 | 58 | content_type = response.headers.get("Content-Type", "Unknown") 59 | duration = end_time - start_time # Calculate the duration 60 | 61 | # Log the response status, Content-Type, and duration 62 | logger.info( 63 | f"fetchStrmLastLink response.status: {response.status_code}, contentType: {content_type}, duration: {duration:.4f} seconds") 64 | 65 | logger.info(f"Last link: {response.url}") 66 | return schemas.Response( 67 | success=True, 68 | data={'url': response.url} 69 | ) 70 | except requests.RequestException as e: 71 | logger.error(f"Request failed: {e}") 72 | 73 | return schemas.Response( 74 | success=False, 75 | message=str(e) 76 | ) 77 | 78 | def get_state(self) -> bool: 79 | return self._enabled 80 | 81 | @staticmethod 82 | def get_command() -> List[Dict[str, Any]]: 83 | pass 84 | 85 | def get_api(self) -> List[Dict[str, Any]]: 86 | """ 87 | 获取插件API 88 | [{ 89 | "path": "/xx", 90 | "endpoint": self.xxx, 91 | "methods": ["GET", "POST"], 92 | "summary": "API说明" 93 | }] 94 | """ 95 | return [{ 96 | "path": "/redirect", 97 | "endpoint": self.redirect, 98 | "methods": ["GET"], 99 | "summary": "UrlRedirect" 100 | }] 101 | 102 | def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: 103 | """ 104 | 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 105 | """ 106 | # 编历 NotificationType 枚举,生成消息类型选项 107 | MsgTypeOptions = [] 108 | for item in NotificationType: 109 | MsgTypeOptions.append({ 110 | "title": item.value, 111 | "value": item.name 112 | }) 113 | return [ 114 | { 115 | 'component': 'VForm', 116 | 'content': [ 117 | { 118 | 'component': 'VRow', 119 | 'content': [ 120 | { 121 | 'component': 'VCol', 122 | 'props': { 123 | 'cols': 12, 124 | 'md': 6 125 | }, 126 | 'content': [ 127 | { 128 | 'component': 'VSwitch', 129 | 'props': { 130 | 'model': 'enabled', 131 | 'label': '启用插件', 132 | } 133 | } 134 | ] 135 | }, 136 | { 137 | 'component': 'VCol', 138 | 'props': { 139 | 'cols': 12, 140 | 'md': 6 141 | }, 142 | 'content': [ 143 | { 144 | 'component': 'VSwitch', 145 | 'props': { 146 | 'model': 'notify', 147 | 'label': '开启通知', 148 | } 149 | } 150 | ] 151 | }, 152 | ] 153 | }, 154 | { 155 | 'component': 'VRow', 156 | 'content': [ 157 | { 158 | 'component': 'VCol', 159 | 'props': { 160 | 'cols': 12 161 | }, 162 | 'content': [ 163 | { 164 | 'component': 'VSelect', 165 | 'props': { 166 | 'multiple': False, 167 | 'chips': True, 168 | 'model': 'msgtype', 169 | 'label': '消息类型', 170 | 'items': MsgTypeOptions 171 | } 172 | } 173 | ] 174 | } 175 | ] 176 | }, 177 | { 178 | 'component': 'VRow', 179 | 'content': [ 180 | { 181 | 'component': 'VCol', 182 | 'props': { 183 | 'cols': 12, 184 | }, 185 | 'content': [ 186 | { 187 | 'component': 'VAlert', 188 | 'props': { 189 | 'type': 'info', 190 | 'variant': 'tonal', 191 | 'text': '群辉webhook配置http://ip:3001/api/v1/plugin/SynologyNotify/webhook?text=hello world。' 192 | 'text参数类型是消息内容。此插件安装完需要重启生效api。消息类型默认为手动处理通知。' 193 | } 194 | } 195 | ] 196 | } 197 | ] 198 | }, 199 | { 200 | 'component': 'VRow', 201 | 'content': [ 202 | { 203 | 'component': 'VCol', 204 | 'props': { 205 | 'cols': 12, 206 | }, 207 | 'content': [ 208 | { 209 | 'component': 'VAlert', 210 | 'props': { 211 | 'type': 'info', 212 | 'variant': 'tonal', 213 | 'text': '如安装完插件后,群晖发送webhook提示404,重启MoviePilot即可。' 214 | } 215 | } 216 | ] 217 | } 218 | ] 219 | } 220 | ] 221 | } 222 | ], { 223 | "enabled": False, 224 | "notify": False, 225 | "msgtype": "" 226 | } 227 | 228 | def get_page(self) -> List[dict]: 229 | pass 230 | 231 | def stop_service(self): 232 | """ 233 | 退出插件 234 | """ 235 | pass 236 | --------------------------------------------------------------------------------