Showing preview only (4,103K chars total). Download the full file or copy to clipboard to get everything.
Repository: jxxghp/MoviePilot
Branch: v2
Commit: afcdefbbf332
Files: 471
Total size: 3.8 MB
Directory structure:
gitextract_jejzfg73/
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── rfc.yml
│ └── workflows/
│ ├── beta.yml
│ ├── build.yml
│ ├── issues.yml
│ └── pylint.yml
├── .gitignore
├── .pylintrc
├── LICENSE
├── README.md
├── app/
│ ├── __init__.py
│ ├── agent/
│ │ ├── __init__.py
│ │ ├── callback/
│ │ │ └── __init__.py
│ │ ├── memory/
│ │ │ └── __init__.py
│ │ ├── prompt/
│ │ │ ├── Agent Prompt.txt
│ │ │ └── __init__.py
│ │ └── tools/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── factory.py
│ │ ├── impl/
│ │ │ ├── __init__.py
│ │ │ ├── _torrent_search_utils.py
│ │ │ ├── add_download.py
│ │ │ ├── add_subscribe.py
│ │ │ ├── delete_download.py
│ │ │ ├── delete_subscribe.py
│ │ │ ├── execute_command.py
│ │ │ ├── get_recommendations.py
│ │ │ ├── get_search_results.py
│ │ │ ├── list_directory.py
│ │ │ ├── query_directory_settings.py
│ │ │ ├── query_download_tasks.py
│ │ │ ├── query_downloaders.py
│ │ │ ├── query_episode_schedule.py
│ │ │ ├── query_library_exists.py
│ │ │ ├── query_library_latest.py
│ │ │ ├── query_media_detail.py
│ │ │ ├── query_popular_subscribes.py
│ │ │ ├── query_rule_groups.py
│ │ │ ├── query_schedulers.py
│ │ │ ├── query_site_userdata.py
│ │ │ ├── query_sites.py
│ │ │ ├── query_subscribe_history.py
│ │ │ ├── query_subscribe_shares.py
│ │ │ ├── query_subscribes.py
│ │ │ ├── query_transfer_history.py
│ │ │ ├── query_workflows.py
│ │ │ ├── recognize_media.py
│ │ │ ├── run_scheduler.py
│ │ │ ├── run_workflow.py
│ │ │ ├── scrape_metadata.py
│ │ │ ├── search_media.py
│ │ │ ├── search_person.py
│ │ │ ├── search_person_credits.py
│ │ │ ├── search_subscribe.py
│ │ │ ├── search_torrents.py
│ │ │ ├── search_web.py
│ │ │ ├── send_message.py
│ │ │ ├── test_site.py
│ │ │ ├── transfer_file.py
│ │ │ ├── update_site.py
│ │ │ ├── update_site_cookie.py
│ │ │ └── update_subscribe.py
│ │ └── manager.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── apiv1.py
│ │ ├── endpoints/
│ │ │ ├── __init__.py
│ │ │ ├── bangumi.py
│ │ │ ├── dashboard.py
│ │ │ ├── discover.py
│ │ │ ├── douban.py
│ │ │ ├── download.py
│ │ │ ├── history.py
│ │ │ ├── login.py
│ │ │ ├── mcp.py
│ │ │ ├── media.py
│ │ │ ├── mediaserver.py
│ │ │ ├── message.py
│ │ │ ├── mfa.py
│ │ │ ├── plugin.py
│ │ │ ├── recommend.py
│ │ │ ├── search.py
│ │ │ ├── site.py
│ │ │ ├── storage.py
│ │ │ ├── subscribe.py
│ │ │ ├── system.py
│ │ │ ├── tmdb.py
│ │ │ ├── torrent.py
│ │ │ ├── transfer.py
│ │ │ ├── user.py
│ │ │ ├── webhook.py
│ │ │ └── workflow.py
│ │ ├── servarr.py
│ │ └── servcookie.py
│ ├── chain/
│ │ ├── __init__.py
│ │ ├── ai_recommend.py
│ │ ├── bangumi.py
│ │ ├── dashboard.py
│ │ ├── douban.py
│ │ ├── download.py
│ │ ├── media.py
│ │ ├── mediaserver.py
│ │ ├── message.py
│ │ ├── recommend.py
│ │ ├── search.py
│ │ ├── site.py
│ │ ├── storage.py
│ │ ├── subscribe.py
│ │ ├── system.py
│ │ ├── tmdb.py
│ │ ├── torrents.py
│ │ ├── transfer.py
│ │ ├── tvdb.py
│ │ ├── user.py
│ │ ├── webhook.py
│ │ └── workflow.py
│ ├── command.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── cache.py
│ │ ├── config.py
│ │ ├── context.py
│ │ ├── event.py
│ │ ├── meta/
│ │ │ ├── __init__.py
│ │ │ ├── customization.py
│ │ │ ├── metaanime.py
│ │ │ ├── metabase.py
│ │ │ ├── metavideo.py
│ │ │ ├── releasegroup.py
│ │ │ ├── streamingplatform.py
│ │ │ └── words.py
│ │ ├── metainfo.py
│ │ ├── module.py
│ │ ├── plugin.py
│ │ └── security.py
│ ├── db/
│ │ ├── __init__.py
│ │ ├── downloadhistory_oper.py
│ │ ├── init.py
│ │ ├── mediaserver_oper.py
│ │ ├── message_oper.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── downloadhistory.py
│ │ │ ├── mediaserver.py
│ │ │ ├── message.py
│ │ │ ├── passkey.py
│ │ │ ├── plugindata.py
│ │ │ ├── site.py
│ │ │ ├── siteicon.py
│ │ │ ├── sitestatistic.py
│ │ │ ├── siteuserdata.py
│ │ │ ├── subscribe.py
│ │ │ ├── subscribehistory.py
│ │ │ ├── systemconfig.py
│ │ │ ├── transferhistory.py
│ │ │ ├── user.py
│ │ │ ├── userconfig.py
│ │ │ └── workflow.py
│ │ ├── plugindata_oper.py
│ │ ├── site_oper.py
│ │ ├── subscribe_oper.py
│ │ ├── systemconfig_oper.py
│ │ ├── transferhistory_oper.py
│ │ ├── user_oper.py
│ │ ├── userconfig_oper.py
│ │ └── workflow_oper.py
│ ├── factory.py
│ ├── helper/
│ │ ├── __init__.py
│ │ ├── browser.py
│ │ ├── cloudflare.py
│ │ ├── cookie.py
│ │ ├── cookiecloud.py
│ │ ├── directory.py
│ │ ├── display.py
│ │ ├── doh.py
│ │ ├── downloader.py
│ │ ├── format.py
│ │ ├── image.py
│ │ ├── llm.py
│ │ ├── mediaserver.py
│ │ ├── message.py
│ │ ├── module.py
│ │ ├── nfo.py
│ │ ├── notification.py
│ │ ├── ocr.py
│ │ ├── passkey.py
│ │ ├── plugin.py
│ │ ├── progress.py
│ │ ├── redis.py
│ │ ├── resource.py
│ │ ├── rss.py
│ │ ├── rule.py
│ │ ├── service.py
│ │ ├── storage.py
│ │ ├── subscribe.py
│ │ ├── system.py
│ │ ├── thread.py
│ │ ├── torrent.py
│ │ ├── twofa.py
│ │ └── workflow.py
│ ├── log.py
│ ├── main.py
│ ├── modules/
│ │ ├── __init__.py
│ │ ├── bangumi/
│ │ │ ├── __init__.py
│ │ │ └── bangumi.py
│ │ ├── discord/
│ │ │ ├── __init__.py
│ │ │ └── discord.py
│ │ ├── douban/
│ │ │ ├── __init__.py
│ │ │ ├── apiv2.py
│ │ │ ├── douban_cache.py
│ │ │ └── scraper.py
│ │ ├── emby/
│ │ │ ├── __init__.py
│ │ │ └── emby.py
│ │ ├── fanart/
│ │ │ └── __init__.py
│ │ ├── filemanager/
│ │ │ ├── __init__.py
│ │ │ ├── storages/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── alipan.py
│ │ │ │ ├── alist.py
│ │ │ │ ├── local.py
│ │ │ │ ├── rclone.py
│ │ │ │ ├── smb.py
│ │ │ │ └── u115.py
│ │ │ └── transhandler.py
│ │ ├── filter/
│ │ │ ├── RuleParser.py
│ │ │ └── __init__.py
│ │ ├── indexer/
│ │ │ ├── __init__.py
│ │ │ ├── parser/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── bitpt.py
│ │ │ │ ├── discuz.py
│ │ │ │ ├── file_list.py
│ │ │ │ ├── gazelle.py
│ │ │ │ ├── hddolby.py
│ │ │ │ ├── ipt_project.py
│ │ │ │ ├── mtorrent.py
│ │ │ │ ├── nexus_audiences.py
│ │ │ │ ├── nexus_hhanclub.py
│ │ │ │ ├── nexus_php.py
│ │ │ │ ├── nexus_project.py
│ │ │ │ ├── nexus_rabbit.py
│ │ │ │ ├── rousi.py
│ │ │ │ ├── small_horse.py
│ │ │ │ ├── tnode.py
│ │ │ │ ├── torrent_leech.py
│ │ │ │ ├── unit3d.py
│ │ │ │ ├── yema.py
│ │ │ │ └── zhixing.py
│ │ │ └── spider/
│ │ │ ├── __init__.py
│ │ │ ├── haidan.py
│ │ │ ├── hddolby.py
│ │ │ ├── mtorrent.py
│ │ │ ├── rousi.py
│ │ │ ├── tnode.py
│ │ │ ├── torrentleech.py
│ │ │ └── yema.py
│ │ ├── jellyfin/
│ │ │ ├── __init__.py
│ │ │ └── jellyfin.py
│ │ ├── plex/
│ │ │ ├── __init__.py
│ │ │ └── plex.py
│ │ ├── postgresql/
│ │ │ └── __init__.py
│ │ ├── qbittorrent/
│ │ │ ├── __init__.py
│ │ │ └── qbittorrent.py
│ │ ├── qqbot/
│ │ │ ├── __init__.py
│ │ │ ├── api.py
│ │ │ ├── gateway.py
│ │ │ └── qqbot.py
│ │ ├── redis/
│ │ │ └── __init__.py
│ │ ├── rtorrent/
│ │ │ ├── __init__.py
│ │ │ └── rtorrent.py
│ │ ├── slack/
│ │ │ ├── __init__.py
│ │ │ └── slack.py
│ │ ├── subtitle/
│ │ │ └── __init__.py
│ │ ├── synologychat/
│ │ │ ├── __init__.py
│ │ │ └── synologychat.py
│ │ ├── telegram/
│ │ │ ├── __init__.py
│ │ │ └── telegram.py
│ │ ├── themoviedb/
│ │ │ ├── __init__.py
│ │ │ ├── category.py
│ │ │ ├── scraper.py
│ │ │ ├── tmdb_cache.py
│ │ │ ├── tmdbapi.py
│ │ │ └── tmdbv3api/
│ │ │ ├── __init__.py
│ │ │ ├── as_obj.py
│ │ │ ├── exceptions.py
│ │ │ ├── objs/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── account.py
│ │ │ │ ├── auth.py
│ │ │ │ ├── certification.py
│ │ │ │ ├── change.py
│ │ │ │ ├── collection.py
│ │ │ │ ├── company.py
│ │ │ │ ├── configuration.py
│ │ │ │ ├── credit.py
│ │ │ │ ├── discover.py
│ │ │ │ ├── episode.py
│ │ │ │ ├── find.py
│ │ │ │ ├── genre.py
│ │ │ │ ├── group.py
│ │ │ │ ├── keyword.py
│ │ │ │ ├── list.py
│ │ │ │ ├── movie.py
│ │ │ │ ├── network.py
│ │ │ │ ├── person.py
│ │ │ │ ├── provider.py
│ │ │ │ ├── review.py
│ │ │ │ ├── search.py
│ │ │ │ ├── season.py
│ │ │ │ ├── trending.py
│ │ │ │ └── tv.py
│ │ │ └── tmdb.py
│ │ ├── thetvdb/
│ │ │ ├── __init__.py
│ │ │ └── tvdb_v4_official.py
│ │ ├── transmission/
│ │ │ ├── __init__.py
│ │ │ └── transmission.py
│ │ ├── trimemedia/
│ │ │ ├── __init__.py
│ │ │ ├── api.py
│ │ │ └── trimemedia.py
│ │ ├── ugreen/
│ │ │ ├── __init__.py
│ │ │ ├── api.py
│ │ │ └── ugreen.py
│ │ ├── vocechat/
│ │ │ ├── __init__.py
│ │ │ └── vocechat.py
│ │ ├── webpush/
│ │ │ └── __init__.py
│ │ └── wechat/
│ │ ├── WXBizMsgCrypt3.py
│ │ ├── __init__.py
│ │ ├── wechat.py
│ │ └── wechatbot.py
│ ├── monitor.py
│ ├── plugins/
│ │ └── __init__.py
│ ├── scheduler.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── agent.py
│ │ ├── category.py
│ │ ├── context.py
│ │ ├── dashboard.py
│ │ ├── download.py
│ │ ├── event.py
│ │ ├── exception.py
│ │ ├── file.py
│ │ ├── history.py
│ │ ├── mcp.py
│ │ ├── mediaserver.py
│ │ ├── message.py
│ │ ├── monitoring.py
│ │ ├── plugin.py
│ │ ├── response.py
│ │ ├── rule.py
│ │ ├── servarr.py
│ │ ├── servcookie.py
│ │ ├── site.py
│ │ ├── subscribe.py
│ │ ├── system.py
│ │ ├── tmdb.py
│ │ ├── token.py
│ │ ├── transfer.py
│ │ ├── types.py
│ │ ├── user.py
│ │ └── workflow.py
│ ├── startup/
│ │ ├── __init__.py
│ │ ├── agent_initializer.py
│ │ ├── command_initializer.py
│ │ ├── lifecycle.py
│ │ ├── modules_initializer.py
│ │ ├── monitor_initializer.py
│ │ ├── plugins_initializer.py
│ │ ├── routers_initializer.py
│ │ ├── scheduler_initializer.py
│ │ └── workflow_initializer.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── common.py
│ │ ├── crypto.py
│ │ ├── debounce.py
│ │ ├── dom.py
│ │ ├── gc.py
│ │ ├── http.py
│ │ ├── ip.py
│ │ ├── limit.py
│ │ ├── mixins.py
│ │ ├── object.py
│ │ ├── otp.py
│ │ ├── security.py
│ │ ├── singleton.py
│ │ ├── site.py
│ │ ├── string.py
│ │ ├── structures.py
│ │ ├── system.py
│ │ ├── timer.py
│ │ ├── tokens.py
│ │ ├── ugreen_crypto.py
│ │ ├── url.py
│ │ └── web.py
│ └── workflow/
│ ├── __init__.py
│ └── actions/
│ ├── __init__.py
│ ├── add_download.py
│ ├── add_subscribe.py
│ ├── fetch_downloads.py
│ ├── fetch_medias.py
│ ├── fetch_rss.py
│ ├── fetch_torrents.py
│ ├── filter_medias.py
│ ├── filter_torrents.py
│ ├── invoke_plugin.py
│ ├── note.py
│ ├── scan_file.py
│ ├── scrape_file.py
│ ├── send_event.py
│ ├── send_message.py
│ └── transfer_file.py
├── config/
│ └── category.yaml
├── database/
│ ├── env.py
│ ├── gen.py
│ ├── script.py.mako
│ └── versions/
│ ├── 0fb94bf69b38_2_0_2.py
│ ├── 262735d025da_2_0_1.py
│ ├── 279a949d81b6_2_1_1.py
│ ├── 294b007932ef_2_0_0.py
│ ├── 3891a5e722a1_2_1_7.py
│ ├── 3df653756eec_2_1_6.py
│ ├── 41ef1dd7467c_2_2_2.py
│ ├── 4666ce24a443_2_1_8.py
│ ├── 486e56a62dcb_2_1_5.py
│ ├── 4b544f5d3b07_2_1_3.py
│ ├── 55390f1f77c1_2_0_9.py
│ ├── 58edfac72c32_2_2_3.py
│ ├── 5b3355c964bb_2_2_0.py
│ ├── 610bb05ddeef_2_1_2.py
│ ├── 89d24811e894_2_1_4.py
│ ├── a295e41830a6_2_0_6.py
│ ├── a73f2dbf5c09_2_0_4.py
│ ├── a946dae52526_2_2_1.py
│ ├── bf28a012734c_2_0_8.py
│ ├── ca5461f314f2_2_1_0.py
│ ├── d58298a0879f_2_1_9.py
│ ├── e2dbe1421fa4_2_0_3.py
│ ├── eaf9cbc49027_2_0_7.py
│ └── ecf3c693fdf3_2_0_5.py
├── docker/
│ ├── Dockerfile
│ ├── cert.sh
│ ├── docker_http_proxy.conf
│ ├── entrypoint.sh
│ ├── nginx.common.conf
│ ├── nginx.template.conf
│ └── update.sh
├── docs/
│ ├── development-setup.md
│ ├── mcp-api.md
│ └── postgresql-setup.md
├── frozen.spec
├── requirements.in
├── requirements.txt
├── safety.policy.yml
├── setup.py
├── skills/
│ └── moviepilot-cli/
│ ├── SKILL.md
│ └── scripts/
│ └── mp-cli.js
├── tests/
│ ├── __init__.py
│ ├── cases/
│ │ ├── __init__.py
│ │ ├── files.py
│ │ ├── groups.py
│ │ └── meta.py
│ ├── manual/
│ │ └── ugreen_media_cli.py
│ ├── run.py
│ ├── test_bluray.py
│ ├── test_mediascrape.py
│ ├── test_metainfo.py
│ ├── test_object.py
│ ├── test_release_group.py
│ ├── test_string.py
│ ├── test_telegram.py
│ ├── test_transfer_history_retransfer.py
│ ├── test_ugreen_api.py
│ ├── test_ugreen_crypto.py
│ └── test_ugreen_mediaserver.py
└── version.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Git
.github
.git
.gitignore
# Documentation
docs/
README.md
LICENSE
# Development files
.pylintrc
*.pyc
__pycache__/
*.pyo
*.pyd
.Python
*.so
.pytest_cache/
.coverage
htmlcov/
.tox/
.nox/
.hypothesis/
.mypy_cache/
.dmypy.json
dmypy.json
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
logs/
# Temporary files
*.tmp
*.temp
tmp/
temp/
# Database
*.db
*.sqlite
*.sqlite3
# Test files
tests/
test_*
*_test.py
# Build artifacts
build/
dist/
*.egg-info/
# Docker
Dockerfile*
docker-compose*
.dockerignore
# Other
app.ico
frozen.spec
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 问题反馈
description: File a bug report
title: "[错误报告]: 请在此处简单描述你的问题"
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
请确认以下信息:
1. 请按此模板提交issues,不按模板提交的问题将直接关闭。
2. 如果你的问题可以直接在以往 issue 或者 Telegram频道 中找到,那么你的 issue 将会被直接关闭。
3. **$\color{red}{提交问题务必描述清楚、附上日志}$**,描述不清导致无法理解和分析的问题会被直接关闭。
4. 此仓库为后端仓库,如果是前端 WebUI 问题请在[前端仓库](https://github.com/jxxghp/MoviePilot-Frontend)提 issue。
5. **$\color{red}{不要通过issues来寻求解决你的环境问题、配置安装类问题、咨询类问题}$**,否则直接关闭并加入用户 $\color{red}{黑名单}$ !实在没有精力陪一波又一波的伸手党玩。
- type: checkboxes
id: ensure
attributes:
label: 确认
description: 在提交 issue 之前,请确认你已经阅读并确认以下内容
options:
- label: 我的版本是最新版本,我的版本号与 [version](https://github.com/jxxghp/MoviePilot/releases/latest) 相同。
required: true
- label: 我已经 [issue](https://github.com/jxxghp/MoviePilot/issues) 中搜索过,确认我的问题没有被提出过。
required: true
- label: 我已经 [Telegram频道](https://t.me/moviepilot_channel) 中搜索过,确认我的问题没有被提出过。
required: true
- label: 我已经修改标题,将标题中的 描述 替换为我遇到的问题。
required: true
- type: input
id: version
attributes:
label: 当前程序版本
description: 遇到问题时程序所在的版本号
validations:
required: true
- type: dropdown
id: environment
attributes:
label: 运行环境
description: 当前程序运行环境
options:
- Docker
- Windows
validations:
required: true
- type: dropdown
id: type
attributes:
label: 问题类型
description: 你在以下哪个部分碰到了问题
options:
- 主程序运行问题
- 插件问题
- 其他问题
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 问题描述
description: 请详细描述你碰到的问题
placeholder: "问题描述"
validations:
required: true
- type: textarea
id: logs
attributes:
label: 发生问题时系统日志和配置文件
description: 问题出现时,程序运行日志请复制到这里。
render: bash
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 项目讨论
url: https://github.com/jxxghp/MoviePilot/discussions/new/choose
about: discussion
- name: Telegram 频道
url: https://t.me/moviepilot_channel
about: 更新日志
- name: Telegram 交流群
url: https://t.me/moviepilot_official
about: 交流互助
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 功能改进
description: Feature Request
title: "[Feature Request]: "
labels: ["feature request"]
body:
- type: markdown
attributes:
value: |
请说明你希望添加的功能。
- type: input
id: version
attributes:
label: 当前程序版本
description: 目前使用的程序版本
validations:
required: true
- type: dropdown
id: environment
attributes:
label: 运行环境
description: 当前程序运行环境
options:
- Docker
- Windows
validations:
required: true
- type: dropdown
id: type
attributes:
label: 功能改进类型
description: 你需要在下面哪个方面改进功能
options:
- 主程序
- 插件
- 其他
validations:
required: true
- type: textarea
id: feature-request
attributes:
label: 功能改进
description: 请详细描述需要改进或者添加的功能。
placeholder: "功能改进"
validations:
required: true
- type: textarea
id: references
attributes:
label: 参考资料
description: 可以列举一些参考资料,但是不要引用同类但商业化软件的任何内容。
placeholder: "参考资料"
================================================
FILE: .github/ISSUE_TEMPLATE/rfc.yml
================================================
name: 功能提案
description: Request for Comments
title: "[RFC]"
labels: ["RFC"]
body:
- type: markdown
attributes:
value: |
一份提案(RFC)定位为 **「在某功能/重构的具体开发前,用于开发者间 review 技术设计/方案的文档」**,
目的是让协作的开发者间清晰的知道「要做什么」和「具体会怎么做」,以及所有的开发者都能公开透明的参与讨论;
以便评估和讨论产生的影响 (遗漏的考虑、向后兼容性、与现有功能的冲突),
因此提案侧重在对解决问题的 **方案、设计、步骤** 的描述上。
如果仅希望讨论是否添加或改进某功能本身,请使用 -> [Issue: 功能改进](https://github.com/jxxghp/MoviePilot/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+)
- type: textarea
id: background
attributes:
label: 背景 or 问题
description: 简单描述遇到的什么问题或需要改动什么。可以引用其他 issue、讨论、文档等。
validations:
required: true
- type: textarea
id: goal
attributes:
label: "目标 & 方案简述"
description: 简单描述提案此提案实现后,**预期的目标效果**,以及简单大致描述会采取的方案/步骤,可能会/不会产生什么影响。
validations:
required: true
- type: textarea
id: design
attributes:
label: "方案设计 & 实现步骤"
description: |
详细描述你设计的具体方案,可以考虑拆分列表或要点,一步步描述具体打算如何实现的步骤和相关细节。
这部份不需要一次性写完整,即使在创建完此提案 issue 后,依旧可以再次编辑修改。
validations:
required: false
- type: textarea
id: alternative
attributes:
label: "替代方案 & 对比"
description: |
[可选] 为来实现目标效果,还考虑过什么其他方案,有什么对比?
validations:
required: false
================================================
FILE: .github/workflows/beta.yml
================================================
name: MoviePilot Builder Beta
on:
workflow_dispatch:
jobs:
Docker-build:
runs-on: ubuntu-latest
name: Build Docker Image
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Release version
id: release_version
run: |
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v2
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=beta
- name: Set Up QEMU
uses: docker/setup-qemu-action@v3
- name: Set Up Buildx
uses: docker/setup-buildx-action@v3
- name: Login DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha, scope=${{ github.workflow }}-docker
cache-to: type=gha, scope=${{ github.workflow }}-docker
================================================
FILE: .github/workflows/build.yml
================================================
name: MoviePilot Builder v2
on:
workflow_dispatch:
push:
branches:
- v2
paths:
- 'version.py'
jobs:
Docker-build:
runs-on: ubuntu-latest
name: Build Docker Image
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Release version
id: release_version
run: |
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v2
${{ secrets.DOCKER_USERNAME }}/moviepilot
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ env.app_version }}
type=raw,value=latest
- name: Set Up QEMU
uses: docker/setup-qemu-action@v3
- name: Set Up Buildx
uses: docker/setup-buildx-action@v3
- name: Login DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha, scope=${{ github.workflow }}-docker
cache-to: type=gha, scope=${{ github.workflow }}-docker
- name: Get existing release body
id: get_release_body
continue-on-error: true
run: |
release_body=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ env.app_version }}" | \
jq -r '.body // ""')
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
echo "$release_body" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Delete Release
uses: dev-drprasad/delete-tag-and-release@v1.1
continue-on-error: true
with:
tag_name: v${{ env.app_version }}
delete_release: true
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.app_version }}
name: v${{ env.app_version }}
body: ${{ env.RELEASE_BODY }}
draft: false
prerelease: false
make_latest: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/issues.yml
================================================
name: Close inactive issues
on:
workflow_dispatch:
schedule:
# Github Action 只支持 UTC 时间。
# '0 18 * * *' 对应 UTC 时间的 18:00,也就是中国时区 (UTC+8) 的第二天凌晨 02:00。
- cron: "0 18 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
# 标记 stale 标签时间
days-before-issue-stale: 30
# 关闭 issues 标签时间
days-before-issue-close: 14
# 自定义标签名
stale-issue-label: "stale"
stale-issue-message: "此问题已过时,因为它已打开 30 天且没有任何活动。"
close-issue-message: "此问题已关闭,因为它在标记为 stale 后,已处于无更新状态 14 天。"
# 忽略所有的 Pull Request,只处理 Issue
days-before-pr-stale: -1
days-before-pr-close: -1
# 排除带有RFC标签的issue
exempt-issue-labels: "RFC"
operations-per-run: 500
repo-token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/pylint.yml
================================================
name: Pylint Code Quality Check
on:
# 允许手动触发
workflow_dispatch:
jobs:
pylint:
runs-on: ubuntu-latest
name: Pylint Code Quality Check
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements.in') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install pylint
# 安装项目依赖
if [ -f requirements.txt ]; then
echo "📦 安装 requirements.txt 中的依赖..."
pip install -r requirements.txt
elif [ -f requirements.in ]; then
echo "📦 安装 requirements.in 中的依赖..."
pip install -r requirements.in
else
echo "⚠️ 未找到依赖文件,仅安装 pylint"
fi
- name: Verify pylint config
run: |
# 检查项目中的pylint配置文件是否存在
if [ -f .pylintrc ]; then
echo "✅ 找到项目配置文件: .pylintrc"
echo "配置文件内容预览:"
head -10 .pylintrc
else
echo "❌ 未找到 .pylintrc 配置文件"
exit 1
fi
- name: Run pylint
run: |
# 运行pylint,检查主要的Python文件
echo "🚀 运行 Pylint 错误检查..."
# 检查主要目录 - 只关注错误,如果有错误则退出
echo "📂 检查 app/ 目录..."
pylint app/ --output-format=colorized --reports=yes --score=yes
# 检查根目录的Python文件
echo "📂 检查根目录 Python 文件..."
for file in $(find . -name "*.py" -not -path "./.*" -not -path "./.venv/*" -not -path "./build/*" -not -path "./dist/*" -not -path "./tests/*" -not -path "./docs/*" -not -path "./__pycache__/*" -maxdepth 1); do
echo "检查文件: $file"
pylint "$file" --output-format=colorized || exit 1
done
# 生成详细报告
echo "📊 生成 Pylint 详细报告..."
pylint app/ --output-format=json > pylint-report.json || true
# 显示评分(仅供参考)
echo "📈 Pylint 评分(仅供参考):"
pylint app/ --score=yes --reports=no | tail -2 || true
- name: Upload pylint report
uses: actions/upload-artifact@v4
if: always()
with:
name: pylint-report
path: pylint-report.json
- name: Summary
run: |
echo "🎉 Pylint 检查完成!"
echo "✅ 没有发现语法错误或严重问题"
echo "📊 详细报告已保存为构建工件"
================================================
FILE: .gitignore
================================================
.idea/
*.c
*.so
*.pyd
build/
cython_cache/
dist/
nginx/
test.py
safety_report.txt
app/helper/sites.py
app/helper/*.so
app/helper/*.pyd
app/helper/*.bin
app/plugins/**
!app/plugins/__init__.py
config/cookies/**
config/user.db*
config/sites/**
config/logs/
config/temp/
config/cache/
*.pyc
*.log
.vscode
venv
# Pylint
pylint-report.json
.pylint.d/
# AI
.claude/
================================================
FILE: .pylintrc
================================================
[MASTER]
# 指定Python路径
init-hook='import sys; sys.path.append(".")'
# 忽略的文件和目录
ignore=.git,__pycache__,.venv,build,dist,tests,docs
# 并行作业数量
jobs=0
[MESSAGES CONTROL]
# 只关注错误级别的问题,禁用警告、约定和重构建议
# E = Error (错误) - 会导致构建失败
# W = Warning (警告) - 仅显示,不会失败
# R = Refactor (重构建议) - 仅显示,不会失败
# C = Convention (约定) - 仅显示,不会失败
# I = Information (信息) - 仅显示,不会失败
# 禁用大部分警告、约定和重构建议,只保留错误和重要警告
disable=all
enable=error,
syntax-error,
undefined-variable,
used-before-assignment,
unreachable,
return-outside-function,
yield-outside-function,
continue-in-finally,
nonlocal-without-binding,
undefined-loop-variable,
redefined-builtin,
not-callable,
assignment-from-no-return,
no-value-for-parameter,
too-many-function-args,
unexpected-keyword-arg,
redundant-keyword-arg,
import-error,
relative-beyond-top-level
[REPORTS]
# 设置报告格式
output-format=colorized
reports=yes
score=yes
[FORMAT]
# 最大行长度
max-line-length=120
# 缩进大小
indent-string=' '
[DESIGN]
# 最大参数数量
max-args=10
# 最大本地变量数量
max-locals=20
# 最大分支数量
max-branches=15
# 最大语句数量
max-statements=50
# 最大父类数量
max-parents=7
# 最大属性数量
max-attributes=10
# 最小公共方法数量
min-public-methods=1
# 最大公共方法数量
max-public-methods=25
[SIMILARITIES]
# 最小相似行数
min-similarity-lines=6
# 忽略注释
ignore-comments=yes
# 忽略文档字符串
ignore-docstrings=yes
# 忽略导入
ignore-imports=yes
[TYPECHECK]
# 生成缺失成员提示的类列表
generated-members=requests.packages.urllib3
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
# MoviePilot








基于 [NAStool](https://github.com/NAStool/nas-tools) 部分代码重新设计,聚焦自动化核心需求,减少问题同时更易于扩展和维护。
# 仅用于学习交流使用,请勿在任何国内平台宣传该项目!
发布频道:https://t.me/moviepilot_channel
## 主要特性
- 前后端分离,基于FastApi + Vue3。
- 聚焦核心需求,简化功能和设置,部分设置项可直接使用默认值。
- 重新设计了用户界面,更加美观易用。
## 安装使用
官方Wiki:https://wiki.movie-pilot.org
### 为 AI Agent 添加 Skills
```shell
npx skills add https://github.com/jxxghp/MoviePilot
```
## 参与开发
API文档:https://api.movie-pilot.org
MCP工具API文档:详见 [docs/mcp-api.md](docs/mcp-api.md)
本地运行需要 `Python 3.12`、`Node JS v20.12.1`
- 克隆主项目 [MoviePilot](https://github.com/jxxghp/MoviePilot)
```shell
git clone https://github.com/jxxghp/MoviePilot
```
- 克隆资源项目 [MoviePilot-Resources](https://github.com/jxxghp/MoviePilot-Resources) ,将 `resources` 目录下对应平台及版本的库 `.so`/`.pyd`/`.bin` 文件复制到 `app/helper` 目录
```shell
git clone https://github.com/jxxghp/MoviePilot-Resources
```
- 安装后端依赖,运行 `main.py` 启动后端服务,默认监听端口:`3001`,API文档地址:`http://localhost:3001/docs`
```shell
cd MoviePilot
pip install -r requirements.txt
python3 -m app.main
```
- 克隆前端项目 [MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)
```shell
git clone https://github.com/jxxghp/MoviePilot-Frontend
```
- 安装前端依赖,运行前端项目,访问:`http://localhost:5173`
```shell
yarn
yarn dev
```
- 参考 [插件开发指引](https://wiki.movie-pilot.org/zh/plugindev) 在 `app/plugins` 目录下开发插件代码
## 相关项目
- [MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)
- [MoviePilot-Resources](https://github.com/jxxghp/MoviePilot-Resources)
- [MoviePilot-Plugins](https://github.com/jxxghp/MoviePilot-Plugins)
- [MoviePilot-Server](https://github.com/jxxghp/MoviePilot-Server)
- [MoviePilot-Wiki](https://github.com/jxxghp/MoviePilot-Wiki)
## 免责申明
- 本软件仅供学习交流使用,任何人不得将本软件用于商业用途,任何人不得将本软件用于违法犯罪活动,软件对用户行为不知情,一切责任由使用者承担。
- 本软件代码开源,基于开源代码进行修改,人为去除相关限制导致软件被分发、传播并造成责任事件的,需由代码修改发布者承担全部责任,不建议对用户认证机制进行规避或修改并公开发布。
- 本项目不接受捐赠,没有在任何地方发布捐赠信息页面,软件本身不收费也不提供任何收费相关服务,请仔细辨别避免误导。
## 贡献者
<a href="https://github.com/jxxghp/MoviePilot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jxxghp/MoviePilot" />
</a>
================================================
FILE: app/__init__.py
================================================
================================================
FILE: app/agent/__init__.py
================================================
import asyncio
from typing import Dict, List, Any, Union
import json
import tiktoken
from langchain.agents import AgentExecutor
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.callbacks import get_openai_callback
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage, ToolCall, ToolMessage, SystemMessage, trim_messages
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain.agents.format_scratchpad.openai_tools import format_to_openai_tool_messages
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from app.agent.callback import StreamingCallbackHandler
from app.agent.memory import conversation_manager
from app.agent.prompt import prompt_manager
from app.agent.tools.factory import MoviePilotToolFactory
from app.chain import ChainBase
from app.core.config import settings
from app.helper.llm import LLMHelper
from app.helper.message import MessageHelper
from app.log import logger
from app.schemas import Notification
class AgentChain(ChainBase):
pass
class MoviePilotAgent:
"""
MoviePilot AI智能体
"""
def __init__(self, session_id: str, user_id: str = None,
channel: str = None, source: str = None, username: str = None):
self.session_id = session_id
self.user_id = user_id
self.channel = channel # 消息渠道
self.source = source # 消息来源
self.username = username # 用户名
# 消息助手
self.message_helper = MessageHelper()
# 回调处理器
self.callback_handler = StreamingCallbackHandler(
session_id=session_id
)
# LLM模型
self.llm = self._initialize_llm()
# 工具
self.tools = self._initialize_tools()
# 提示词模板
self.prompt = self._initialize_prompt()
# Agent执行器
self.agent_executor = self._create_agent_executor()
def _initialize_llm(self):
"""
初始化LLM模型
"""
return LLMHelper.get_llm(streaming=True, callbacks=[self.callback_handler])
def _initialize_tools(self) -> List:
"""
初始化工具列表
"""
return MoviePilotToolFactory.create_tools(
session_id=self.session_id,
user_id=self.user_id,
channel=self.channel,
source=self.source,
username=self.username,
callback_handler=self.callback_handler
)
@staticmethod
def _initialize_session_store() -> Dict[str, InMemoryChatMessageHistory]:
"""
初始化内存存储
"""
return {}
def get_session_history(self, session_id: str) -> InMemoryChatMessageHistory:
"""
获取会话历史
"""
chat_history = InMemoryChatMessageHistory()
messages: List[dict] = conversation_manager.get_recent_messages_for_agent(
session_id=session_id,
user_id=self.user_id
)
if messages:
for msg in messages:
if msg.get("role") == "user":
chat_history.add_message(HumanMessage(content=msg.get("content", "")))
elif msg.get("role") == "agent":
chat_history.add_message(AIMessage(content=msg.get("content", "")))
elif msg.get("role") == "tool_call":
metadata = msg.get("metadata", {})
chat_history.add_message(
AIMessage(
content=msg.get("content", ""),
tool_calls=[
ToolCall(
id=metadata.get("call_id"),
name=metadata.get("tool_name"),
args=metadata.get("parameters"),
)
]
)
)
elif msg.get("role") == "tool_result":
metadata = msg.get("metadata", {})
chat_history.add_message(ToolMessage(
content=msg.get("content", ""),
tool_call_id=metadata.get("call_id", "unknown")
))
elif msg.get("role") == "system":
chat_history.add_message(SystemMessage(content=msg.get("content", "")))
return chat_history
@staticmethod
def _initialize_prompt() -> ChatPromptTemplate:
"""
初始化提示词模板
"""
try:
prompt_template = ChatPromptTemplate.from_messages([
("system", "{system_prompt}"),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
logger.info("LangChain提示词模板初始化成功")
return prompt_template
except Exception as e:
logger.error(f"初始化提示词失败: {e}")
raise e
@staticmethod
def _token_counter(messages: List[Union[HumanMessage, AIMessage, ToolMessage, SystemMessage]]) -> int:
"""
通用的Token计数器
"""
try:
# 尝试从模型获取编码集,如果失败则回退到 cl100k_base (大多数现代模型使用的编码)
try:
encoding = tiktoken.encoding_for_model(settings.LLM_MODEL)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
num_tokens = 0
for message in messages:
# 基础开销 (每个消息大约 3 个 token)
num_tokens += 3
# 1. 处理文本内容 (content)
if isinstance(message.content, str):
num_tokens += len(encoding.encode(message.content))
elif isinstance(message.content, list):
for part in message.content:
if isinstance(part, dict) and part.get("type") == "text":
num_tokens += len(encoding.encode(part.get("text", "")))
# 2. 处理工具调用 (仅 AIMessage 包含 tool_calls)
if getattr(message, "tool_calls", None):
for tool_call in message.tool_calls:
# 函数名
num_tokens += len(encoding.encode(tool_call.get("name", "")))
# 参数 (转为 JSON 估算)
args_str = json.dumps(tool_call.get("args", {}), ensure_ascii=False)
num_tokens += len(encoding.encode(args_str))
# 额外的结构开销 (ID 等)
num_tokens += 3
# 3. 处理角色权重
num_tokens += 1
# 加上回复的起始 Token (大约 3 个 token)
num_tokens += 3
return num_tokens
except Exception as e:
logger.error(f"Token计数失败: {e}")
# 发生错误时返回一个保守的估算值
return len(str(messages)) // 4
def _create_agent_executor(self) -> RunnableWithMessageHistory:
"""
创建Agent执行器
"""
try:
# 消息裁剪器,防止上下文超出限制
base_trimmer = trim_messages(
max_tokens=settings.LLM_MAX_CONTEXT_TOKENS * 1000 * 0.8,
strategy="last",
token_counter=self._token_counter,
include_system=True,
allow_partial=False,
start_on="human",
)
# 包装trimmer,在裁剪后验证工具调用的完整性
def validated_trimmer(messages):
# 如果输入是 PromptValue,转换为消息列表
if hasattr(messages, "to_messages"):
messages = messages.to_messages()
trimmed = base_trimmer.invoke(messages)
# 二次校验:确保不出现 broken tool chains
# 1. AIMessage with tool_calls 必须紧跟着对应的 ToolMessage
# 2. ToolMessage 必须有对应的 AIMessage 前置
safe_messages = []
i = 0
while i < len(trimmed):
msg = trimmed[i]
if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None):
# 检查工具调用序列是否完整
tool_calls = msg.tool_calls
is_valid_sequence = True
tool_results = []
# 向后查找对应的 ToolMessage
temp_i = i + 1
for tool_call in tool_calls:
if temp_i >= len(trimmed):
is_valid_sequence = False
break
next_msg = trimmed[temp_i]
if isinstance(next_msg, ToolMessage) and next_msg.tool_call_id == tool_call.get("id"):
tool_results.append(next_msg)
temp_i += 1
else:
is_valid_sequence = False
break
if is_valid_sequence:
# 序列完整,保留消息
safe_messages.append(msg)
safe_messages.extend(tool_results)
i = temp_i # 跳过已处理的工具结果
else:
# 序列不完整,丢弃该 AIMessage(后续的孤立 ToolMessage 会在下一次循环被当做 orphaned 处理掉)
logger.warning(f"移除无效的工具调用链: {len(tool_calls)} calls, incomplete results")
i += 1
continue
if isinstance(msg, ToolMessage):
# 如果在这里遇到 ToolMessage,说明它没有被上面的逻辑消费,则是孤立的(或者顺序错乱)
logger.warning("移除孤立的 ToolMessage")
i += 1
continue
# 其他类型的消息直接保留
safe_messages.append(msg)
i += 1
if len(safe_messages) < len(messages):
logger.info(f"LangChain消息上下文已裁剪: {len(messages)} -> {len(safe_messages)}")
return safe_messages
# 创建Agent执行链
agent = (
RunnablePassthrough.assign(
agent_scratchpad=lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
)
)
| self.prompt
| RunnableLambda(validated_trimmer)
| self.llm.bind_tools(self.tools)
| OpenAIToolsAgentOutputParser()
)
executor = AgentExecutor(
agent=agent,
tools=self.tools,
verbose=settings.LLM_VERBOSE,
max_iterations=settings.LLM_MAX_ITERATIONS,
return_intermediate_steps=True,
handle_parsing_errors=True,
early_stopping_method="force"
)
return RunnableWithMessageHistory(
executor,
self.get_session_history,
input_messages_key="input",
history_messages_key="chat_history"
)
except Exception as e:
logger.error(f"创建Agent执行器失败: {e}")
raise e
async def _summarize_history(self):
"""
总结提炼之前的对话和工具执行情况,并把会话总结变成新的系统提示词取代之前的对话
"""
try:
# 获取当前历史记录
chat_history = self.get_session_history(self.session_id)
messages = chat_history.messages
if not messages:
return
logger.info(f"会话 {self.session_id} 历史消息长度已超过 90%,开始总结并重置上下文...")
# 将消息转换为摘要所需的文本格式
history_text = ""
for msg in messages:
if isinstance(msg, HumanMessage):
history_text += f"用户: {msg.content}\n"
elif isinstance(msg, AIMessage):
history_text += f"智能体: {msg.content}\n"
if getattr(msg, "tool_calls", None):
for tool_call in msg.tool_calls:
history_text += f"智能体调用工具: {tool_call.get('name')},参数: {tool_call.get('args')}\n"
elif isinstance(msg, ToolMessage):
history_text += f"工具响应: {msg.content}\n"
elif isinstance(msg, SystemMessage):
history_text += f"系统: {msg.content}\n"
# 摘要提示词
summary_prompt = (
"Please provide a comprehensive and highly informational summary of the preceding conversation and tool executions. "
"Your goal is to condense the history while retaining all critical details for future reference. "
"Ensure you include:\n"
"1. User's core intents, specific requests, and any mentioned preferences.\n"
"2. Names of movies, TV shows, or other key entities discussed.\n"
"3. A concise log of tool calls made and their specific results/outcomes.\n"
"4. The current status of any tasks and any pending actions.\n"
"5. Any important context that would be necessary for the agent to continue the conversation seamlessly.\n"
"The summary should be dense with information and serve as the primary context for the next stage of the interaction."
)
# 调用 LLM 进行总结 (非流式)
summary_llm = LLMHelper.get_llm(streaming=False)
response = await summary_llm.ainvoke([
SystemMessage(content=summary_prompt),
HumanMessage(content=f"Here is the conversation history to summarize:\n{history_text}")
])
summary_content = str(response.content)
if not summary_content:
logger.warning("总结生成失败,跳过重置逻辑。")
return
# 清空原有的会话记录并插入新的系统总结
await conversation_manager.clear_memory(self.session_id, self.user_id)
await conversation_manager.add_conversation(
session_id=self.session_id,
user_id=self.user_id,
role="system",
content=f"<history_summary>\n{summary_content}\n</history_summary>"
)
logger.info(f"会话 {self.session_id} 历史摘要替换完成。")
except Exception as e:
logger.error(f"执行会话总结出错: {str(e)}")
async def process_message(self, message: str) -> str:
"""
处理用户消息
"""
try:
# 检查上下文长度是否超过 90%
history = self.get_session_history(self.session_id)
if self._token_counter(history.messages) > settings.LLM_MAX_CONTEXT_TOKENS * 1000 * 0.9:
await self._summarize_history()
# 添加用户消息到记忆
await conversation_manager.add_conversation(
self.session_id,
user_id=self.user_id,
role="user",
content=message
)
# 构建输入上下文
input_context = {
"system_prompt": prompt_manager.get_agent_prompt(channel=self.channel),
"input": message
}
# 执行Agent
logger.info(f"Agent执行推理: session_id={self.session_id}, input={message}")
result = await self._execute_agent(input_context)
# 获取Agent回复
agent_message = await self.callback_handler.get_message()
# 发送Agent回复给用户(通过原渠道)
if agent_message:
# 发送回复
await self.send_agent_message(agent_message)
# 添加Agent回复到记忆
await conversation_manager.add_conversation(
session_id=self.session_id,
user_id=self.user_id,
role="agent",
content=agent_message
)
else:
agent_message = result.get("output") or "很抱歉,智能体出错了,未能生成回复内容。"
await self.send_agent_message(agent_message)
return agent_message
except Exception as e:
error_message = f"处理消息时发生错误: {str(e)}"
logger.error(error_message)
# 发送错误消息给用户(通过原渠道)
await self.send_agent_message(error_message)
return error_message
async def _execute_agent(self, input_context: Dict[str, Any]) -> Dict[str, Any]:
"""
执行LangChain Agent
"""
try:
with get_openai_callback() as cb:
result = await self.agent_executor.ainvoke(
input_context,
config={"configurable": {"session_id": self.session_id}},
callbacks=[self.callback_handler]
)
logger.info(f"LLM调用消耗: \n{cb}")
if cb.total_tokens > 0:
result["token_usage"] = {
"prompt_tokens": cb.prompt_tokens,
"completion_tokens": cb.completion_tokens,
"total_tokens": cb.total_tokens
}
return result
except asyncio.CancelledError:
logger.info(f"Agent执行被取消: session_id={self.session_id}")
return {
"output": "任务已取消",
"intermediate_steps": [],
"token_usage": {}
}
except Exception as e:
logger.error(f"Agent执行失败: {e}")
return {
"output": str(e),
"intermediate_steps": [],
"token_usage": {}
}
async def send_agent_message(self, message: str, title: str = "MoviePilot助手"):
"""
通过原渠道发送消息给用户
"""
await AgentChain().async_post_message(
Notification(
channel=self.channel,
source=self.source,
userid=self.user_id,
username=self.username,
title=title,
text=message
)
)
async def cleanup(self):
"""
清理智能体资源
"""
logger.info(f"MoviePilot智能体已清理: session_id={self.session_id}")
class AgentManager:
"""
AI智能体管理器
"""
def __init__(self):
self.active_agents: Dict[str, MoviePilotAgent] = {}
@staticmethod
async def initialize():
"""
初始化管理器
"""
await conversation_manager.initialize()
async def close(self):
"""
关闭管理器
"""
await conversation_manager.close()
# 清理所有活跃的智能体
for agent in self.active_agents.values():
await agent.cleanup()
self.active_agents.clear()
async def process_message(self, session_id: str, user_id: str, message: str,
channel: str = None, source: str = None, username: str = None) -> str:
"""
处理用户消息
"""
# 获取或创建Agent实例
if session_id not in self.active_agents:
logger.info(f"创建新的AI智能体实例,session_id: {session_id}, user_id: {user_id}")
agent = MoviePilotAgent(
session_id=session_id,
user_id=user_id,
channel=channel,
source=source,
username=username
)
self.active_agents[session_id] = agent
else:
agent = self.active_agents[session_id]
agent.user_id = user_id # 确保user_id是最新的
# 更新渠道信息
if channel:
agent.channel = channel
if source:
agent.source = source
if username:
agent.username = username
# 处理消息
return await agent.process_message(message)
async def clear_session(self, session_id: str, user_id: str):
"""
清空会话
"""
if session_id in self.active_agents:
agent = self.active_agents[session_id]
await agent.cleanup()
del self.active_agents[session_id]
await conversation_manager.clear_memory(session_id, user_id)
logger.info(f"会话 {session_id} 的记忆已清空")
# 全局智能体管理器实例
agent_manager = AgentManager()
================================================
FILE: app/agent/callback/__init__.py
================================================
import threading
from langchain_core.callbacks import AsyncCallbackHandler
from app.log import logger
class StreamingCallbackHandler(AsyncCallbackHandler):
"""
流式输出回调处理器
"""
def __init__(self, session_id: str):
self._lock = threading.Lock()
self.session_id = session_id
self.current_message = ""
async def get_message(self):
"""
获取当前消息内容,获取后清空
"""
with self._lock:
if not self.current_message:
return ""
msg = self.current_message
logger.info(f"Agent消息: {msg}")
self.current_message = ""
return msg
async def on_llm_new_token(self, token: str, **kwargs):
"""
处理新的token
"""
if not token:
return
with self._lock:
# 缓存当前消息
self.current_message += token
================================================
FILE: app/agent/memory/__init__.py
================================================
"""对话记忆管理器"""
import asyncio
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from app.core.config import settings
from app.helper.redis import AsyncRedisHelper
from app.log import logger
from app.schemas.agent import ConversationMemory
class ConversationMemoryManager:
"""
对话记忆管理器
"""
def __init__(self):
# 内存中的会话记忆缓存
self.memory_cache: Dict[str, ConversationMemory] = {}
# 使用现有的Redis助手
self.redis_helper = AsyncRedisHelper()
# 内存缓存清理任务(Redis通过TTL自动过期)
self.cleanup_task: Optional[asyncio.Task] = None
async def initialize(self):
"""
初始化记忆管理器
"""
try:
# 启动内存缓存清理任务(Redis通过TTL自动过期)
self.cleanup_task = asyncio.create_task(self._cleanup_expired_memories())
logger.info("对话记忆管理器初始化完成")
except Exception as e:
logger.warning(f"Redis连接失败,将使用内存存储: {e}")
async def close(self):
"""
关闭记忆管理器
"""
if self.cleanup_task:
self.cleanup_task.cancel()
try:
await self.cleanup_task
except asyncio.CancelledError:
pass
await self.redis_helper.close()
logger.info("对话记忆管理器已关闭")
@staticmethod
def _get_memory_key(session_id: str, user_id: str):
"""
计算内存Key
"""
return f"{user_id}:{session_id}" if user_id else session_id
@staticmethod
def _get_redis_key(session_id: str, user_id: str):
"""
计算Redis Key
"""
return f"agent_memory:{user_id}:{session_id}" if user_id else f"agent_memory:{session_id}"
def _get_memory(self, session_id: str, user_id: str):
"""
获取内存中的记忆
"""
cache_key = self._get_memory_key(session_id, user_id)
return self.memory_cache.get(cache_key)
async def _get_redis(self, session_id: str, user_id: str) -> Optional[ConversationMemory]:
"""
从Redis获取记忆
"""
if settings.CACHE_BACKEND_TYPE == "redis":
try:
redis_key = self._get_redis_key(session_id, user_id)
memory_data = await self.redis_helper.get(redis_key, region="AI_AGENT")
if memory_data:
memory_dict = json.loads(memory_data) if isinstance(memory_data, str) else memory_data
memory = ConversationMemory(**memory_dict)
return memory
except Exception as e:
logger.warning(f"从Redis加载记忆失败: {e}")
return None
async def get_conversation(self, session_id: str, user_id: str) -> ConversationMemory:
"""
获取会话记忆
"""
# 首先检查缓存
conversion = self._get_memory(session_id, user_id)
if conversion:
return conversion
# 尝试从Redis加载
memory = await self._get_redis(session_id, user_id)
if memory:
# 加载到内存缓存
self._save_memory(memory)
return memory
# 创建新的记忆
memory = ConversationMemory(session_id=session_id, user_id=user_id)
await self._save_conversation(memory)
return memory
async def set_title(self, session_id: str, user_id: str, title: str):
"""
设置会话标题
"""
memory = await self.get_conversation(session_id=session_id, user_id=user_id)
memory.title = title
memory.updated_at = datetime.now()
await self._save_conversation(memory)
async def get_title(self, session_id: str, user_id: str) -> Optional[str]:
"""
获取会话标题
"""
memory = await self.get_conversation(session_id=session_id, user_id=user_id)
return memory.title
async def list_sessions(self, user_id: str, limit: int = 100) -> List[Dict[str, Any]]:
"""
列出历史会话摘要(按更新时间倒序)
- 当启用Redis时:遍历 `agent_memory:*` 键并读取摘要
- 当未启用Redis时:基于内存缓存返回
"""
sessions: List[ConversationMemory] = []
# 从Redis遍历
if settings.CACHE_BACKEND_TYPE == "redis":
try:
# 使用Redis助手的items方法遍历所有键
async for key, value in self.redis_helper.items(region="AI_AGENT"):
if key.startswith("agent_memory:"):
try:
# 解析键名获取user_id和session_id
key_parts = key.split(":")
if len(key_parts) >= 3:
key_user_id = key_parts[2] if len(key_parts) > 3 else None
if not user_id or key_user_id == user_id:
data = value if isinstance(value, dict) else json.loads(value)
memory = ConversationMemory(**data)
sessions.append(memory)
except Exception as err:
logger.warning(f"解析Redis记忆数据失败: {err}")
continue
except Exception as e:
logger.warning(f"遍历Redis会话失败: {e}")
# 合并内存缓存(确保包含近期的会话)
for cache_key, memory in self.memory_cache.items():
# 如果指定了user_id,只返回该用户的会话
if not user_id or memory.user_id == user_id:
sessions.append(memory)
# 去重(以 session_id 为键,取最近updated)
uniq: Dict[str, ConversationMemory] = {}
for mem in sessions:
existed = uniq.get(mem.session_id)
if (not existed) or (mem.updated_at > existed.updated_at):
uniq[mem.session_id] = mem
# 排序并裁剪
sorted_list = sorted(uniq.values(), key=lambda m: m.updated_at, reverse=True)[:limit]
return [
{
"session_id": m.session_id,
"title": m.title or "新会话",
"message_count": len(m.messages),
"created_at": m.created_at.isoformat(),
"updated_at": m.updated_at.isoformat(),
}
for m in sorted_list
]
async def add_conversation(
self,
session_id: str,
user_id: str,
role: str,
content: str,
metadata: Optional[Dict[str, Any]] = None
):
"""
添加消息到记忆
"""
memory = await self.get_conversation(session_id=session_id, user_id=user_id)
message = {
"role": role,
"content": content,
"timestamp": datetime.now().isoformat(),
"metadata": metadata or {}
}
memory.messages.append(message)
memory.updated_at = datetime.now()
# 限制消息数量,避免记忆过大
max_messages = settings.LLM_MAX_MEMORY_MESSAGES
if len(memory.messages) > max_messages:
# 保留最近的消息,但保留第一条系统消息
system_messages = [msg for msg in memory.messages if msg["role"] == "system"]
recent_messages = memory.messages[-(max_messages - len(system_messages)):]
memory.messages = system_messages + recent_messages
await self._save_conversation(memory)
logger.debug(f"消息已添加到记忆: session_id={session_id}, user_id={user_id}, role={role}")
def get_recent_messages_for_agent(
self,
session_id: str,
user_id: str
) -> List[Dict[str, Any]]:
"""
为Agent获取最近的消息(仅内存缓存)
如果消息Token数量超过模型最大上下文长度的阀值,会自动进行摘要裁剪
"""
cache_key = self._get_memory_key(session_id, user_id)
memory = self.memory_cache.get(cache_key)
if not memory:
return []
# 获取所有消息
return memory.messages[:-1]
async def get_recent_messages(
self,
session_id: str,
user_id: str,
limit: int = 10,
role_filter: Optional[list] = None
) -> List[Dict[str, Any]]:
"""
获取最近的消息
"""
memory = await self.get_conversation(session_id=session_id, user_id=user_id)
messages = memory.messages
if role_filter:
messages = [msg for msg in messages if msg["role"] in role_filter]
return messages[-limit:] if messages else []
async def get_context(self, session_id: str, user_id: str) -> Dict[str, Any]:
"""
获取会话上下文
"""
memory = await self.get_conversation(session_id=session_id, user_id=user_id)
return memory.context
async def clear_memory(self, session_id: str, user_id: str):
"""
清空会话记忆
"""
cache_key = f"{user_id}:{session_id}" if user_id else session_id
if cache_key in self.memory_cache:
del self.memory_cache[cache_key]
if settings.CACHE_BACKEND_TYPE == "redis":
redis_key = self._get_redis_key(session_id, user_id)
await self.redis_helper.delete(redis_key, region="AI_AGENT")
logger.info(f"会话记忆已清空: session_id={session_id}, user_id={user_id}")
def _save_memory(self, memory: ConversationMemory):
"""
保存记忆到内存
"""
cache_key = self._get_memory_key(memory.session_id, memory.user_id)
self.memory_cache[cache_key] = memory
async def _save_redis(self, memory: ConversationMemory):
"""
保存记忆到Redis
"""
if settings.CACHE_BACKEND_TYPE == "redis":
try:
memory_dict = memory.model_dump()
redis_key = self._get_redis_key(memory.session_id, memory.user_id)
ttl = int(timedelta(days=settings.LLM_REDIS_MEMORY_RETENTION_DAYS).total_seconds())
await self.redis_helper.set(
redis_key,
memory_dict,
ttl=ttl,
region="AI_AGENT"
)
except Exception as e:
logger.warning(f"保存记忆到Redis失败: {e}")
async def _save_conversation(self, memory: ConversationMemory):
"""
保存记忆到存储
Redis中的记忆会自动通过TTL机制过期,无需手动清理
"""
# 更新内存缓存
self._save_memory(memory)
# 保存到Redis,设置TTL自动过期
await self._save_redis(memory)
async def _cleanup_expired_memories(self):
"""
清理内存中过期记忆的后台任务
注意:Redis中的记忆通过TTL机制自动过期,这里只清理内存缓存
"""
while True:
try:
# 每小时清理一次
await asyncio.sleep(3600)
current_time = datetime.now()
expired_sessions = []
# 只检查内存缓存中的过期记忆
# Redis中的记忆会通过TTL自动过期,无需手动处理
for cache_key, memory in self.memory_cache.items():
if (current_time - memory.updated_at).days > settings.LLM_MEMORY_RETENTION_DAYS:
expired_sessions.append(cache_key)
# 只清理内存缓存,不删除Redis中的键(Redis会自动过期)
for cache_key in expired_sessions:
if cache_key in self.memory_cache:
del self.memory_cache[cache_key]
if expired_sessions:
logger.info(f"清理了{len(expired_sessions)}个过期内存会话记忆")
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"清理记忆时发生错误: {e}")
conversation_manager = ConversationMemoryManager()
================================================
FILE: app/agent/prompt/Agent Prompt.txt
================================================
You are an AI media assistant powered by MoviePilot, specialized in managing home media ecosystems. Your expertise covers searching for movies/TV shows, managing subscriptions, overseeing downloads, and organizing media libraries.
All your responses must be in **Chinese (中文)**.
You act as a proactive agent. Your goal is to fully resolve the user's media-related requests autonomously. Do not end your turn until the task is complete or you are blocked and require user feedback.
Core Capabilities:
1. Media Search & Recognition
- Identify movies, TV shows, and anime across various metadata providers.
- Recognize media info from fuzzy filenames or incomplete titles.
2. Subscription Management
- Create complex rules for automated downloading of new episodes.
- Monitor trending movies/shows for automated suggestions.
3. Download Control
- Intelligent torrent searching across private/public trackers.
- Filter resources by quality (4K/1080p), codec (H265/H264), and release groups.
4. System Status & Organization
- Monitor download progress and server health.
- Manage file transfers, renaming, and library cleanup.
<communication>
- Use Markdown for structured data like movie lists, download statuses, or technical details.
- Avoid wrapping the entire response in a single code block. Use `inline code` for titles or parameters and ```code blocks``` for structured logs or data only when necessary.
- ALWAYS use backticks for media titles (e.g., `Interstellar`), file paths, or specific parameters.
- Optimize your writing for clarity and readability, using bold text for key information.
- Provide comprehensive details for media (year, rating, resolution) to help users make informed decisions.
- Do not stop for approval for read-only operations. Only stop for critical actions like starting a download or deleting a subscription.
Important Notes:
- User-Centric: Your tone should be helpful, professional, and media-savvy.
- No Coding Hallucinations: You are NOT a coding assistant. Do not offer code snippets, IDE tips, or programming help. Focus entirely on the MoviePilot media ecosystem.
- Contextual Memory: Remember if the user preferred a specific version previously and prioritize similar results in future searches.
</communication>
<status_update_spec>
Definition: Provide a brief progress narrative (1-3 sentences) explaining what you have searched, what you found, and what you are about to execute.
- **Immediate Execution**: If you state an intention to perform an action (e.g., "I'll search for the movie"), execute the corresponding tool call in the same turn.
- Use natural tenses: "I've found...", "I'm checking...", "I will now add...".
- Skip redundant updates if no significant progress has been made since the last message.
</status_update_spec>
<summary_spec>
At the end of your session/turn, provide a concise summary of your actions.
- Highlight key results: "Subscribed to `Stranger Things`", "Added `Avatar` 4K to download queue".
- Use bullet points for multiple actions.
- Do not repeat the internal execution steps; focus on the outcome for the user.
</summary_spec>
<flow>
1. Media Discovery: Start by identifying the exact media metadata (TMDB ID, Season/Episode) using search tools.
2. Context Checking: Verify current status (Is it already in the library? Is it already subscribed?).
3. Action Execution: Perform the requested task (Subscribe, Search Torrents, etc.) with a brief status update.
4. Final Confirmation: Summarize the final state and wait for the next user command.
</flow>
<tool_calling_strategy>
- Parallel Execution: You MUST call independent tools in parallel. For example, search for torrents on multiple sites or check both subscription and download status at once.
- Information Depth: If a search returns ambiguous results, use `query_media_detail` or `recognize_media` to resolve the ambiguity before proceeding.
- Proactive Fallback: If `search_media` fails, try `search_web` or fuzzy search with `recognize_media`. Do not ask the user for help unless all automated search methods are exhausted.
</tool_calling_strategy>
<media_management_rules>
1. Download Safety: You MUST present a list of found torrents (including size, seeds, and quality) and obtain the user's explicit consent before initiating any download.
2. Subscription Logic: When adding a subscription, always check for the best matching quality profile based on user history or the default settings.
3. Library Awareness: Always check if the user already has the content in their library to avoid duplicate downloads.
4. Error Handling: If a site is down or a tool returns an error, explain the situation in plain Chinese (e.g., "站点响应超时") and suggest an alternative (e.g., "尝试从其他站点进行搜索").
</media_management_rules>
<markdown_spec>
Specific markdown rules:
{markdown_spec}
</markdown_spec>
================================================
FILE: app/agent/prompt/__init__.py
================================================
"""提示词管理器"""
from pathlib import Path
from typing import Dict
from app.log import logger
from app.schemas import ChannelCapability, ChannelCapabilities, MessageChannel, ChannelCapabilityManager
class PromptManager:
"""
提示词管理器
"""
def __init__(self, prompts_dir: str = None):
if prompts_dir is None:
self.prompts_dir = Path(__file__).parent
else:
self.prompts_dir = Path(prompts_dir)
self.prompts_cache: Dict[str, str] = {}
def load_prompt(self, prompt_name: str) -> str:
"""
加载指定的提示词
"""
if prompt_name in self.prompts_cache:
return self.prompts_cache[prompt_name]
prompt_file = self.prompts_dir / prompt_name
try:
with open(prompt_file, 'r', encoding='utf-8') as f:
content = f.read().strip()
# 缓存提示词
self.prompts_cache[prompt_name] = content
logger.info(f"提示词加载成功: {prompt_name},长度:{len(content)} 字符")
return content
except FileNotFoundError:
logger.error(f"提示词文件不存在: {prompt_file}")
raise
except Exception as e:
logger.error(f"加载提示词失败: {prompt_name}, 错误: {e}")
raise
def get_agent_prompt(self, channel: str = None) -> str:
"""
获取智能体提示词
:param channel: 消息渠道(Telegram、微信、Slack等)
:return: 提示词内容
"""
# 基础提示词
base_prompt = self.load_prompt("Agent Prompt.txt")
# 识别渠道
msg_channel = next((c for c in MessageChannel if c.value.lower() == channel.lower()), None) if channel else None
if msg_channel:
# 获取渠道能力说明
caps = ChannelCapabilityManager.get_capabilities(msg_channel)
if caps:
base_prompt = base_prompt.replace(
"{markdown_spec}",
self._generate_formatting_instructions(caps)
)
return base_prompt
@staticmethod
def _generate_formatting_instructions(caps: ChannelCapabilities) -> str:
"""
根据渠道能力动态生成格式指令
"""
instructions = []
if ChannelCapability.RICH_TEXT not in caps.capabilities:
instructions.append("- Formatting: Use **Plain Text ONLY**. The channel does NOT support Markdown.")
instructions.append(
"- No Markdown Symbols: NEVER use `**`, `*`, `__`, or `[` blocks. Use natural text to emphasize (e.g., using ALL CAPS or separators).")
instructions.append(
"- Lists: Use plain text symbols like `>` or `*` at the start of lines, followed by manual line breaks.")
instructions.append("- Links: Paste URLs directly as text.")
return "\n".join(instructions)
def clear_cache(self):
"""
清空缓存
"""
self.prompts_cache.clear()
logger.info("提示词缓存已清空")
prompt_manager = PromptManager()
================================================
FILE: app/agent/tools/__init__.py
================================================
================================================
FILE: app/agent/tools/base.py
================================================
import json
import uuid
from abc import ABCMeta, abstractmethod
from typing import Any, Optional
from langchain.tools import BaseTool
from pydantic import PrivateAttr
from app.agent import StreamingCallbackHandler, conversation_manager
from app.chain import ChainBase
from app.log import logger
from app.schemas import Notification
class ToolChain(ChainBase):
pass
class MoviePilotTool(BaseTool, metaclass=ABCMeta):
"""
MoviePilot专用工具基类
"""
_session_id: str = PrivateAttr()
_user_id: str = PrivateAttr()
_channel: str = PrivateAttr(default=None)
_source: str = PrivateAttr(default=None)
_username: str = PrivateAttr(default=None)
_callback_handler: StreamingCallbackHandler = PrivateAttr(default=None)
def __init__(self, session_id: str, user_id: str, **kwargs):
super().__init__(**kwargs)
self._session_id = session_id
self._user_id = user_id
def _run(self, *args: Any, **kwargs: Any) -> Any:
pass
async def _arun(self, **kwargs) -> str:
"""
异步运行工具
"""
# 获取工具调用前的agent消息
agent_message = await self._callback_handler.get_message()
# 生成唯一的工具调用ID
call_id = f"call_{str(uuid.uuid4())[:16]}"
# 记忆工具调用
await conversation_manager.add_conversation(
session_id=self._session_id,
user_id=self._user_id,
role="tool_call",
content=agent_message,
metadata={
"call_id": call_id,
"tool_name": self.name,
"parameters": kwargs
}
)
# 获取执行工具说明,优先使用工具自定义的提示消息,如果没有则使用 explanation
tool_message = self.get_tool_message(**kwargs)
if not tool_message:
explanation = kwargs.get("explanation")
if explanation:
tool_message = explanation
# 合并agent消息和工具执行消息,一起发送
messages = []
if agent_message:
messages.append(agent_message)
if tool_message:
messages.append(f"⚙️ => {tool_message}")
# 发送合并后的消息
if messages:
merged_message = "\n\n".join(messages)
await self.send_tool_message(merged_message, title="MoviePilot助手")
logger.debug(f'Executing tool {self.name} with args: {kwargs}')
# 执行工具,捕获异常确保结果总是被存储到记忆中
try:
result = await self.run(**kwargs)
logger.debug(f'Tool {self.name} executed with result: {result}')
except Exception as e:
# 记录异常详情
error_message = f"工具执行异常 ({type(e).__name__}): {str(e)}"
logger.error(f'Tool {self.name} execution failed: {e}', exc_info=True)
result = error_message
# 记忆工具调用结果
if isinstance(result, str):
formated_result = result
elif isinstance(result, (int, float)):
formated_result = str(result)
else:
formated_result = json.dumps(result, ensure_ascii=False, indent=2)
await conversation_manager.add_conversation(
session_id=self._session_id,
user_id=self._user_id,
role="tool_result",
content=formated_result,
metadata={
"call_id": call_id,
"tool_name": self.name,
}
)
return result
def get_tool_message(self, **kwargs) -> Optional[str]:
"""
获取工具执行时的友好提示消息
子类可以重写此方法,根据实际参数生成个性化的提示消息。
如果返回 None 或空字符串,将回退使用 explanation 参数。
Args:
**kwargs: 工具的所有参数(包括 explanation)
Returns:
str: 友好的提示消息,如果返回 None 或空字符串则使用 explanation
"""
return None
@abstractmethod
async def run(self, **kwargs) -> str:
raise NotImplementedError
def set_message_attr(self, channel: str, source: str, username: str):
"""
设置消息属性
"""
self._channel = channel
self._source = source
self._username = username
def set_callback_handler(self, callback_handler: StreamingCallbackHandler):
"""
设置回调处理器
"""
self._callback_handler = callback_handler
async def send_tool_message(self, message: str, title: str = ""):
"""
发送工具消息
"""
await ToolChain().async_post_message(
Notification(
channel=self._channel,
source=self._source,
userid=self._user_id,
username=self._username,
title=title,
text=message
)
)
================================================
FILE: app/agent/tools/factory.py
================================================
from typing import List, Callable
from app.agent.tools.impl.add_download import AddDownloadTool
from app.agent.tools.impl.add_subscribe import AddSubscribeTool
from app.agent.tools.impl.update_subscribe import UpdateSubscribeTool
from app.agent.tools.impl.search_subscribe import SearchSubscribeTool
from app.agent.tools.impl.get_recommendations import GetRecommendationsTool
from app.agent.tools.impl.query_downloaders import QueryDownloadersTool
from app.agent.tools.impl.query_download_tasks import QueryDownloadTasksTool
from app.agent.tools.impl.query_library_exists import QueryLibraryExistsTool
from app.agent.tools.impl.query_library_latest import QueryLibraryLatestTool
from app.agent.tools.impl.query_sites import QuerySitesTool
from app.agent.tools.impl.update_site import UpdateSiteTool
from app.agent.tools.impl.query_site_userdata import QuerySiteUserdataTool
from app.agent.tools.impl.test_site import TestSiteTool
from app.agent.tools.impl.query_subscribes import QuerySubscribesTool
from app.agent.tools.impl.query_subscribe_shares import QuerySubscribeSharesTool
from app.agent.tools.impl.query_rule_groups import QueryRuleGroupsTool
from app.agent.tools.impl.query_popular_subscribes import QueryPopularSubscribesTool
from app.agent.tools.impl.query_subscribe_history import QuerySubscribeHistoryTool
from app.agent.tools.impl.delete_subscribe import DeleteSubscribeTool
from app.agent.tools.impl.search_media import SearchMediaTool
from app.agent.tools.impl.search_person import SearchPersonTool
from app.agent.tools.impl.search_person_credits import SearchPersonCreditsTool
from app.agent.tools.impl.recognize_media import RecognizeMediaTool
from app.agent.tools.impl.scrape_metadata import ScrapeMetadataTool
from app.agent.tools.impl.query_episode_schedule import QueryEpisodeScheduleTool
from app.agent.tools.impl.query_media_detail import QueryMediaDetailTool
from app.agent.tools.impl.search_torrents import SearchTorrentsTool
from app.agent.tools.impl.get_search_results import GetSearchResultsTool
from app.agent.tools.impl.search_web import SearchWebTool
from app.agent.tools.impl.send_message import SendMessageTool
from app.agent.tools.impl.query_schedulers import QuerySchedulersTool
from app.agent.tools.impl.run_scheduler import RunSchedulerTool
from app.agent.tools.impl.query_workflows import QueryWorkflowsTool
from app.agent.tools.impl.run_workflow import RunWorkflowTool
from app.agent.tools.impl.update_site_cookie import UpdateSiteCookieTool
from app.agent.tools.impl.delete_download import DeleteDownloadTool
from app.agent.tools.impl.query_directory_settings import QueryDirectorySettingsTool
from app.agent.tools.impl.list_directory import ListDirectoryTool
from app.agent.tools.impl.query_transfer_history import QueryTransferHistoryTool
from app.agent.tools.impl.transfer_file import TransferFileTool
from app.agent.tools.impl.execute_command import ExecuteCommandTool
from app.core.plugin import PluginManager
from app.log import logger
from .base import MoviePilotTool
class MoviePilotToolFactory:
"""
MoviePilot工具工厂
"""
@staticmethod
def create_tools(session_id: str, user_id: str,
channel: str = None, source: str = None, username: str = None,
callback_handler: Callable = None) -> List[MoviePilotTool]:
"""
创建MoviePilot工具列表
"""
tools = []
tool_definitions = [
SearchMediaTool,
SearchPersonTool,
SearchPersonCreditsTool,
RecognizeMediaTool,
ScrapeMetadataTool,
QueryEpisodeScheduleTool,
QueryMediaDetailTool,
AddSubscribeTool,
UpdateSubscribeTool,
SearchSubscribeTool,
SearchTorrentsTool,
GetSearchResultsTool,
SearchWebTool,
AddDownloadTool,
QuerySubscribesTool,
QuerySubscribeSharesTool,
QueryPopularSubscribesTool,
QueryRuleGroupsTool,
QuerySubscribeHistoryTool,
DeleteSubscribeTool,
QueryDownloadTasksTool,
DeleteDownloadTool,
QueryDownloadersTool,
QuerySitesTool,
UpdateSiteTool,
QuerySiteUserdataTool,
TestSiteTool,
UpdateSiteCookieTool,
GetRecommendationsTool,
QueryLibraryExistsTool,
QueryLibraryLatestTool,
QueryDirectorySettingsTool,
ListDirectoryTool,
QueryTransferHistoryTool,
TransferFileTool,
SendMessageTool,
QuerySchedulersTool,
RunSchedulerTool,
QueryWorkflowsTool,
RunWorkflowTool,
ExecuteCommandTool
]
# 创建内置工具
for ToolClass in tool_definitions:
tool = ToolClass(
session_id=session_id,
user_id=user_id
)
tool.set_message_attr(channel=channel, source=source, username=username)
tool.set_callback_handler(callback_handler=callback_handler)
tools.append(tool)
# 加载插件提供的工具
plugin_tools_count = 0
plugin_tools_info = PluginManager().get_plugin_agent_tools()
for plugin_info in plugin_tools_info:
plugin_id = plugin_info.get("plugin_id")
plugin_name = plugin_info.get("plugin_name")
tool_classes = plugin_info.get("tools", [])
for ToolClass in tool_classes:
try:
# 验证工具类是否继承自 MoviePilotTool
if not issubclass(ToolClass, MoviePilotTool):
logger.warning(f"插件 {plugin_name}({plugin_id}) 提供的工具类 {ToolClass.__name__} 未继承自 MoviePilotTool,已跳过")
continue
# 创建工具实例
tool = ToolClass(
session_id=session_id,
user_id=user_id
)
tool.set_message_attr(channel=channel, source=source, username=username)
tool.set_callback_handler(callback_handler=callback_handler)
tools.append(tool)
plugin_tools_count += 1
logger.debug(f"成功加载插件 {plugin_name}({plugin_id}) 的工具: {ToolClass.__name__}")
except Exception as e:
logger.error(f"加载插件 {plugin_name}({plugin_id}) 的工具 {ToolClass.__name__} 失败: {str(e)}")
builtin_tools_count = len(tool_definitions)
if plugin_tools_count > 0:
logger.info(f"成功创建 {len(tools)} 个MoviePilot工具(内置工具: {builtin_tools_count} 个,插件工具: {plugin_tools_count} 个)")
else:
logger.info(f"成功创建 {len(tools)} 个MoviePilot工具")
return tools
================================================
FILE: app/agent/tools/impl/__init__.py
================================================
================================================
FILE: app/agent/tools/impl/_torrent_search_utils.py
================================================
"""种子搜索工具辅助函数"""
import re
from typing import List, Optional
from app.core.context import Context
from app.utils.crypto import HashUtils
from app.utils.string import StringUtils
SEARCH_RESULT_CACHE_FILE = "__search_result__"
TORRENT_RESULT_LIMIT = 50
def build_torrent_ref(context: Optional[Context]) -> str:
"""生成用于下载校验的短引用"""
if not context or not context.torrent_info:
return ""
return HashUtils.sha1(context.torrent_info.enclosure or "")[:7]
def sort_season_options(options: List[str]) -> List[str]:
"""按前端逻辑排序季集选项"""
if len(options) <= 1:
return options
parsed_options = []
for index, option in enumerate(options):
match = re.match(r"^S(\d+)(?:-S(\d+))?\s*(?:E(\d+)(?:-E(\d+))?)?$", option or "")
if not match:
parsed_options.append({
"original": option,
"season_num": 0,
"episode_num": 0,
"max_episode_num": 0,
"is_whole_season": False,
"index": index,
})
continue
episode_num = int(match.group(3)) if match.group(3) else 0
max_episode_num = int(match.group(4)) if match.group(4) else episode_num
parsed_options.append({
"original": option,
"season_num": int(match.group(1)),
"episode_num": episode_num,
"max_episode_num": max_episode_num,
"is_whole_season": not match.group(3),
"index": index,
})
whole_seasons = [item for item in parsed_options if item["is_whole_season"]]
episodes = [item for item in parsed_options if not item["is_whole_season"]]
whole_seasons.sort(key=lambda item: (-item["season_num"], item["index"]))
episodes.sort(
key=lambda item: (
-item["season_num"],
-(item["max_episode_num"] or item["episode_num"]),
-item["episode_num"],
item["index"],
)
)
return [item["original"] for item in whole_seasons + episodes]
def append_option(options: List[str], value: Optional[str]) -> None:
"""按前端逻辑收集去重后的筛选项"""
if value and value not in options:
options.append(value)
def build_filter_options(items: List[Context]) -> dict:
"""从搜索结果中构建筛选项汇总"""
filter_options = {
"site": [],
"season": [],
"freeState": [],
"edition": [],
"resolution": [],
"videoCode": [],
"releaseGroup": [],
}
for item in items:
torrent_info = item.torrent_info
meta_info = item.meta_info
append_option(filter_options["site"], getattr(torrent_info, "site_name", None))
append_option(filter_options["season"], getattr(meta_info, "season_episode", None))
append_option(filter_options["freeState"], getattr(torrent_info, "volume_factor", None))
append_option(filter_options["edition"], getattr(meta_info, "edition", None))
append_option(filter_options["resolution"], getattr(meta_info, "resource_pix", None))
append_option(filter_options["videoCode"], getattr(meta_info, "video_encode", None))
append_option(filter_options["releaseGroup"], getattr(meta_info, "resource_team", None))
filter_options["season"] = sort_season_options(filter_options["season"])
return filter_options
def match_filter(filter_values: Optional[List[str]], value: Optional[str]) -> bool:
"""匹配前端同款多选筛选规则"""
return not filter_values or bool(value and value in filter_values)
def filter_contexts(items: List[Context],
site: Optional[List[str]] = None,
season: Optional[List[str]] = None,
free_state: Optional[List[str]] = None,
video_code: Optional[List[str]] = None,
edition: Optional[List[str]] = None,
resolution: Optional[List[str]] = None,
release_group: Optional[List[str]] = None) -> List[Context]:
"""按前端同款维度筛选结果"""
filtered_items = []
for item in items:
torrent_info = item.torrent_info
meta_info = item.meta_info
if (
match_filter(site, getattr(torrent_info, "site_name", None))
and match_filter(free_state, getattr(torrent_info, "volume_factor", None))
and match_filter(season, getattr(meta_info, "season_episode", None))
and match_filter(release_group, getattr(meta_info, "resource_team", None))
and match_filter(video_code, getattr(meta_info, "video_encode", None))
and match_filter(resolution, getattr(meta_info, "resource_pix", None))
and match_filter(edition, getattr(meta_info, "edition", None))
):
filtered_items.append(item)
return filtered_items
def simplify_search_result(context: Context, index: int) -> dict:
"""精简单条搜索结果"""
simplified = {}
torrent_info = context.torrent_info
meta_info = context.meta_info
media_info = context.media_info
if torrent_info:
simplified["torrent_info"] = {
"title": torrent_info.title,
"size": StringUtils.format_size(torrent_info.size),
"seeders": torrent_info.seeders,
"peers": torrent_info.peers,
"site_name": torrent_info.site_name,
"torrent_url": f"{build_torrent_ref(context)}:{index}",
"page_url": torrent_info.page_url,
"volume_factor": torrent_info.volume_factor,
"freedate_diff": torrent_info.freedate_diff,
"pubdate": torrent_info.pubdate,
}
if media_info:
simplified["media_info"] = {
"title": media_info.title,
"en_title": media_info.en_title,
"year": media_info.year,
"type": media_info.type.value if media_info.type else None,
"season": media_info.season,
"tmdb_id": media_info.tmdb_id,
}
if meta_info:
simplified["meta_info"] = {
"name": meta_info.name,
"cn_name": meta_info.cn_name,
"en_name": meta_info.en_name,
"year": meta_info.year,
"type": meta_info.type.value if meta_info.type else None,
"begin_season": meta_info.begin_season,
"season_episode": meta_info.season_episode,
"resource_team": meta_info.resource_team,
"video_encode": meta_info.video_encode,
"edition": meta_info.edition,
"resource_pix": meta_info.resource_pix,
}
return simplified
================================================
FILE: app/agent/tools/impl/add_download.py
================================================
"""添加下载工具"""
import re
from pathlib import Path
from typing import List, Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool, ToolChain
from app.chain.search import SearchChain
from app.chain.download import DownloadChain
from app.core.config import settings
from app.core.context import Context
from app.core.metainfo import MetaInfo
from app.db.site_oper import SiteOper
from app.helper.directory import DirectoryHelper
from app.log import logger
from app.schemas import TorrentInfo, FileURI
from app.utils.crypto import HashUtils
class AddDownloadInput(BaseModel):
"""添加下载工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
torrent_url: List[str] = Field(
...,
description="One or more torrent_url values. Values matching the hash:id pattern from get_search_results are treated as internal references; other values must be direct torrent URLs or magnet links."
)
downloader: Optional[str] = Field(None,
description="Name of the downloader to use (optional, uses default if not specified)")
save_path: Optional[str] = Field(None,
description="Directory path where the downloaded files should be saved. Using `<storage>:<path>` for remote storage. e.g. rclone:/MP, smb:/server/share/Movies. (optional, uses default path if not specified)")
labels: Optional[str] = Field(None,
description="Comma-separated list of labels/tags to assign to the download (optional, e.g., 'movie,hd,bluray')")
class AddDownloadTool(MoviePilotTool):
name: str = "add_download"
description: str = "Add torrent download task to the configured downloader (qBittorrent, Transmission, etc.) using hash:id references from get_search_results or direct torrent URLs / magnet links."
args_schema: Type[BaseModel] = AddDownloadInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据下载参数生成友好的提示消息"""
torrent_urls = self._normalize_torrent_urls(kwargs.get("torrent_url"))
downloader = kwargs.get("downloader")
if torrent_urls:
if len(torrent_urls) == 1:
if self._is_torrent_ref(torrent_urls[0]):
message = f"正在添加下载任务: 资源 {torrent_urls[0]}"
else:
message = "正在添加下载任务: 直链或磁力链接"
else:
message = f"正在批量添加下载任务: 共 {len(torrent_urls)} 个资源"
else:
message = "正在添加下载任务"
if downloader:
message += f" [下载器: {downloader}]"
return message
@staticmethod
def _build_torrent_ref(context: Context) -> str:
"""生成用于校验缓存项的短引用"""
if not context or not context.torrent_info:
return ""
return HashUtils.sha1(context.torrent_info.enclosure or "")[:7]
@staticmethod
def _is_torrent_ref(torrent_ref: Optional[str]) -> bool:
"""判断是否为内部搜索结果引用"""
if not torrent_ref:
return False
return bool(re.fullmatch(r"[0-9a-f]{7}:\d+", str(torrent_ref).strip()))
@staticmethod
def _is_direct_download_url(torrent_url: Optional[str]) -> bool:
"""判断是否为允许直传下载器的下载内容"""
if not torrent_url:
return False
value = str(torrent_url).strip()
return value.startswith("http://") or value.startswith("https://") or value.startswith("magnet:")
@classmethod
def _resolve_cached_context(cls, torrent_ref: str) -> Optional[Context]:
"""从最近一次搜索缓存中解析种子上下文,仅支持 hash:id 格式"""
ref = str(torrent_ref).strip()
if ":" not in ref:
return None
try:
ref_hash, ref_index = ref.split(":", 1)
index = int(ref_index)
except (TypeError, ValueError):
return None
if index < 1:
return None
results = SearchChain().last_search_results() or []
if index > len(results):
return None
context = results[index - 1]
if not ref_hash or cls._build_torrent_ref(context) != ref_hash:
return None
return context
@staticmethod
def _merge_labels_with_system_tag(labels: Optional[str]) -> Optional[str]:
"""合并用户标签与系统默认标签,确保任务可被系统管理"""
system_tag = (settings.TORRENT_TAG or "").strip()
user_labels = [item.strip() for item in (labels or "").split(",") if item.strip()]
if system_tag and system_tag not in user_labels:
user_labels.append(system_tag)
return ",".join(user_labels) if user_labels else None
@staticmethod
def _format_failed_result(failed_messages: List[str]) -> str:
"""统一格式化失败结果"""
return ", ".join([message for message in failed_messages if message])
@staticmethod
def _build_failure_message(torrent_ref: str, error_msg: Optional[str] = None) -> str:
"""构造失败提示"""
normalized_error = (error_msg or "").strip()
prefix = "添加种子任务失败:"
if normalized_error.startswith(prefix):
normalized_error = normalized_error[len(prefix):].lstrip()
if AddDownloadTool._is_direct_download_url(normalized_error):
normalized_error = ""
if normalized_error:
return f"{torrent_ref} {normalized_error}"
if AddDownloadTool._is_torrent_ref(torrent_ref):
return torrent_ref
return ""
@classmethod
def _normalize_torrent_urls(cls, torrent_url: Optional[List[str] | str]) -> List[str]:
"""统一规范 torrent_url 输入,保留所有非空值"""
if torrent_url is None:
return []
if isinstance(torrent_url, str):
candidates = torrent_url.split(",")
else:
candidates = torrent_url
return [str(item).strip() for item in candidates if item and str(item).strip()]
@staticmethod
def _resolve_direct_download_dir(save_path: Optional[str]) -> Optional[Path]:
"""解析直接下载使用的目录,优先使用 save_path,其次使用默认下载目录"""
if save_path:
return Path(save_path)
download_dirs = DirectoryHelper().get_download_dirs()
if not download_dirs:
return None
dir_conf = download_dirs[0]
if not dir_conf.download_path:
return None
return Path(FileURI(storage=dir_conf.storage or "local", path=dir_conf.download_path).uri)
async def run(self, torrent_url: Optional[List[str]] = None,
downloader: Optional[str] = None, save_path: Optional[str] = None,
labels: Optional[str] = None, **kwargs) -> str:
logger.info(
f"执行工具: {self.name}, 参数: torrent_url={torrent_url}, downloader={downloader}, save_path={save_path}, labels={labels}")
try:
torrent_inputs = self._normalize_torrent_urls(torrent_url)
if not torrent_inputs:
return "错误:torrent_url 不能为空。"
download_chain = DownloadChain()
merged_labels = self._merge_labels_with_system_tag(labels)
success_count = 0
failed_messages = []
for torrent_input in torrent_inputs:
if self._is_torrent_ref(torrent_input):
cached_context = self._resolve_cached_context(torrent_input)
if not cached_context or not cached_context.torrent_info:
failed_messages.append(f"{torrent_input} 引用无效,请重新使用 get_search_results 查看搜索结果")
continue
cached_torrent = cached_context.torrent_info
site_name = cached_torrent.site_name
torrent_title = cached_torrent.title or torrent_input
torrent_description = cached_torrent.description
enclosure = cached_torrent.enclosure
if not site_name:
failed_messages.append(f"{torrent_input} 缺少站点名称")
continue
siteinfo = await SiteOper().async_get_by_name(site_name)
if not siteinfo:
failed_messages.append(f"{torrent_input} 未找到站点信息 {site_name}")
continue
torrent_info = TorrentInfo(
title=torrent_title,
description=torrent_description,
enclosure=enclosure,
site_name=site_name,
site_ua=siteinfo.ua,
site_cookie=siteinfo.cookie,
site_proxy=siteinfo.proxy,
site_order=siteinfo.pri,
site_downloader=siteinfo.downloader
)
meta_info = MetaInfo(title=torrent_title, subtitle=torrent_description)
media_info = cached_context.media_info if cached_context.media_info else None
if not media_info:
media_info = await ToolChain().async_recognize_media(meta=meta_info)
if not media_info:
failed_messages.append(f"{torrent_input} 无法识别媒体信息")
continue
context = Context(
torrent_info=torrent_info,
meta_info=meta_info,
media_info=media_info
)
else:
if not self._is_direct_download_url(torrent_input):
failed_messages.append(
f"{torrent_input} 不是有效的下载内容,非 hash:id 时仅支持 http://、https:// 或 magnet: 开头"
)
continue
download_dir = self._resolve_direct_download_dir(save_path)
if not download_dir:
failed_messages.append(f"{torrent_input} 缺少保存路径,且系统未配置可用下载目录")
continue
result = download_chain.download(
content=torrent_input,
download_dir=download_dir,
cookie=None,
label=merged_labels,
downloader=downloader
)
if result:
_, did, _, error_msg = result
else:
did, error_msg = None, "未找到下载器"
if did:
success_count += 1
else:
failed_messages.append(self._build_failure_message(torrent_input, error_msg))
continue
did, error_msg = download_chain.download_single(
context=context,
downloader=downloader,
save_path=save_path,
label=merged_labels,
return_detail=True
)
if did:
success_count += 1
else:
failed_messages.append(self._build_failure_message(torrent_input, error_msg))
if success_count and not failed_messages:
return "任务添加成功"
if success_count:
return f"部分任务添加失败:{self._format_failed_result(failed_messages)}"
return f"任务添加失败:{self._format_failed_result(failed_messages)}"
except Exception as e:
logger.error(f"添加下载任务失败: {e}", exc_info=True)
return f"添加下载任务时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/add_subscribe.py
================================================
"""添加订阅工具"""
from typing import Optional, Type, List
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.subscribe import SubscribeChain
from app.log import logger
from app.schemas.types import MediaType
class AddSubscribeInput(BaseModel):
"""添加订阅工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
title: str = Field(..., description="The title of the media to subscribe to (e.g., 'The Matrix', 'Breaking Bad')")
year: str = Field(..., description="Release year of the media (required for accurate identification)")
media_type: str = Field(...,
description="Allowed values: movie, tv")
season: Optional[int] = Field(None,
description="Season number for TV shows (optional, if not specified will subscribe to all seasons)")
tmdb_id: Optional[int] = Field(None,
description="TMDB database ID for precise media identification (optional, can be obtained from search_media tool)")
douban_id: Optional[str] = Field(None,
description="Douban ID for precise media identification (optional, alternative to tmdb_id)")
start_episode: Optional[int] = Field(None,
description="Starting episode number for TV shows (optional, defaults to 1 if not specified)")
total_episode: Optional[int] = Field(None,
description="Total number of episodes for TV shows (optional, will be auto-detected from TMDB if not specified)")
quality: Optional[str] = Field(None,
description="Quality filter as regular expression (optional, e.g., 'BluRay|WEB-DL|HDTV')")
resolution: Optional[str] = Field(None,
description="Resolution filter as regular expression (optional, e.g., '1080p|720p|2160p')")
effect: Optional[str] = Field(None,
description="Effect filter as regular expression (optional, e.g., 'HDR|DV|SDR')")
filter_groups: Optional[List[str]] = Field(None,
description="List of filter rule group names to apply (optional, can be obtained from query_rule_groups tool)")
sites: Optional[List[int]] = Field(None,
description="List of site IDs to search from (optional, can be obtained from query_sites tool)")
class AddSubscribeTool(MoviePilotTool):
name: str = "add_subscribe"
description: str = "Add media subscription to create automated download rules for movies and TV shows. The system will automatically search and download new episodes or releases based on the subscription criteria. Supports advanced filtering options like quality, resolution, and effect filters using regular expressions."
args_schema: Type[BaseModel] = AddSubscribeInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据订阅参数生成友好的提示消息"""
title = kwargs.get("title", "")
year = kwargs.get("year", "")
media_type = kwargs.get("media_type", "")
season = kwargs.get("season")
message = f"正在添加订阅: {title}"
if year:
message += f" ({year})"
if media_type:
message += f" [{media_type}]"
if season:
message += f" 第{season}季"
return message
async def run(self, title: str, year: str, media_type: str,
season: Optional[int] = None, tmdb_id: Optional[int] = None,
douban_id: Optional[str] = None,
start_episode: Optional[int] = None, total_episode: Optional[int] = None,
quality: Optional[str] = None, resolution: Optional[str] = None,
effect: Optional[str] = None, filter_groups: Optional[List[str]] = None,
sites: Optional[List[int]] = None, **kwargs) -> str:
logger.info(
f"执行工具: {self.name}, 参数: title={title}, year={year}, media_type={media_type}, "
f"season={season}, tmdb_id={tmdb_id}, douban_id={douban_id}, start_episode={start_episode}, "
f"total_episode={total_episode}, quality={quality}, resolution={resolution}, "
f"effect={effect}, filter_groups={filter_groups}, sites={sites}")
try:
subscribe_chain = SubscribeChain()
media_type_enum = MediaType.from_agent(media_type)
if not media_type_enum:
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv'"
# 构建额外的订阅参数
subscribe_kwargs = {}
if start_episode is not None:
subscribe_kwargs['start_episode'] = start_episode
if total_episode is not None:
subscribe_kwargs['total_episode'] = total_episode
if quality:
subscribe_kwargs['quality'] = quality
if resolution:
subscribe_kwargs['resolution'] = resolution
if effect:
subscribe_kwargs['effect'] = effect
if filter_groups:
subscribe_kwargs['filter_groups'] = filter_groups
if sites:
subscribe_kwargs['sites'] = sites
sid, message = await subscribe_chain.async_add(
mtype=media_type_enum,
title=title,
year=year,
tmdbid=tmdb_id,
doubanid=douban_id,
season=season,
username=self._user_id,
**subscribe_kwargs
)
if sid:
if message and "已存在" in message:
return f"订阅已存在:{title} ({year})。如需修改参数请先删除旧订阅。"
result_msg = f"成功添加订阅:{title} ({year})"
if subscribe_kwargs:
params = []
if start_episode is not None:
params.append(f"开始集数: {start_episode}")
if total_episode is not None:
params.append(f"总集数: {total_episode}")
if quality:
params.append(f"质量过滤: {quality}")
if resolution:
params.append(f"分辨率过滤: {resolution}")
if effect:
params.append(f"特效过滤: {effect}")
if filter_groups:
params.append(f"规则组: {', '.join(filter_groups)}")
if sites:
params.append(f"站点: {', '.join(map(str, sites))}")
if params:
result_msg += f"\n配置参数: {', '.join(params)}"
return result_msg
else:
return f"添加订阅失败:{message}"
except Exception as e:
logger.error(f"添加订阅失败: {e}", exc_info=True)
return f"添加订阅时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/delete_download.py
================================================
"""删除下载任务工具"""
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.download import DownloadChain
from app.log import logger
class DeleteDownloadInput(BaseModel):
"""删除下载任务工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
hash: str = Field(..., description="Task hash (can be obtained from query_download_tasks tool)")
downloader: Optional[str] = Field(None, description="Name of specific downloader (optional, if not provided will search all downloaders)")
delete_files: Optional[bool] = Field(False, description="Whether to delete downloaded files along with the task (default: False, only removes the task from downloader)")
class DeleteDownloadTool(MoviePilotTool):
name: str = "delete_download"
description: str = "Delete a download task from the downloader by task hash only. Optionally specify the downloader name and whether to delete downloaded files."
args_schema: Type[BaseModel] = DeleteDownloadInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据删除参数生成友好的提示消息"""
hash_value = kwargs.get("hash", "")
downloader = kwargs.get("downloader")
delete_files = kwargs.get("delete_files", False)
message = f"正在删除下载任务: {hash_value}"
if downloader:
message += f" [下载器: {downloader}]"
if delete_files:
message += " (包含文件)"
return message
async def run(self, hash: str, downloader: Optional[str] = None,
delete_files: Optional[bool] = False, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: hash={hash}, downloader={downloader}, delete_files={delete_files}")
try:
download_chain = DownloadChain()
# 仅支持通过hash删除任务
if len(hash) != 40 or not all(c in '0123456789abcdefABCDEF' for c in hash):
return "参数错误:hash 格式无效,请先使用 query_download_tasks 工具获取正确的 hash。"
# 删除下载任务
# remove_torrents 支持 delete_file 参数,可以控制是否删除文件
result = download_chain.remove_torrents(hashs=[hash], downloader=downloader, delete_file=delete_files)
if result:
files_info = "(包含文件)" if delete_files else "(不包含文件)"
return f"成功删除下载任务:{hash} {files_info}"
else:
return f"删除下载任务失败:{hash},请检查任务是否存在或下载器是否可用"
except Exception as e:
logger.error(f"删除下载任务失败: {e}", exc_info=True)
return f"删除下载任务时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/delete_subscribe.py
================================================
"""删除订阅工具"""
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.core.event import eventmanager
from app.db.subscribe_oper import SubscribeOper
from app.helper.subscribe import SubscribeHelper
from app.log import logger
from app.schemas.types import EventType
class DeleteSubscribeInput(BaseModel):
"""删除订阅工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
subscribe_id: int = Field(..., description="The ID of the subscription to delete (can be obtained from query_subscribes tool)")
class DeleteSubscribeTool(MoviePilotTool):
name: str = "delete_subscribe"
description: str = "Delete a media subscription by its ID. This will remove the subscription and stop automatic downloads for that media."
args_schema: Type[BaseModel] = DeleteSubscribeInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据删除参数生成友好的提示消息"""
subscribe_id = kwargs.get("subscribe_id")
return f"正在删除订阅 (ID: {subscribe_id})"
async def run(self, subscribe_id: int, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: subscribe_id={subscribe_id}")
try:
subscribe_oper = SubscribeOper()
# 获取订阅信息
subscribe = await subscribe_oper.async_get(subscribe_id)
if not subscribe:
return f"订阅 ID {subscribe_id} 不存在"
# 在删除之前获取订阅信息(用于事件)
subscribe_info = subscribe.to_dict()
# 删除订阅
subscribe_oper.delete(subscribe_id)
# 发送事件
await eventmanager.async_send_event(EventType.SubscribeDeleted, {
"subscribe_id": subscribe_id,
"subscribe_info": subscribe_info
})
# 统计订阅
SubscribeHelper().sub_done_async({
"tmdbid": subscribe.tmdbid,
"doubanid": subscribe.doubanid
})
return f"成功删除订阅:{subscribe.name} ({subscribe.year})"
except Exception as e:
logger.error(f"删除订阅失败: {e}", exc_info=True)
return f"删除订阅时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/execute_command.py
================================================
"""执行Shell命令工具"""
import asyncio
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.log import logger
class ExecuteCommandInput(BaseModel):
"""执行Shell命令工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this command is being executed")
command: str = Field(..., description="The shell command to execute")
timeout: Optional[int] = Field(60, description="Max execution time in seconds (default: 60)")
class ExecuteCommandTool(MoviePilotTool):
name: str = "execute_command"
description: str = "Safely execute shell commands on the server. Useful for system maintenance, checking status, or running custom scripts. Includes timeout and output limits."
args_schema: Type[BaseModel] = ExecuteCommandInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据命令生成友好的提示消息"""
command = kwargs.get("command", "")
return f"正在执行系统命令: {command}"
async def run(self, command: str, timeout: Optional[int] = 60, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: command={command}, timeout={timeout}")
# 简单安全过滤
forbidden_keywords = ["rm -rf /", ":(){ :|:& };:", "dd if=/dev/zero", "mkfs", "reboot", "shutdown"]
for keyword in forbidden_keywords:
if keyword in command:
return f"错误:命令包含禁止使用的关键字 '{keyword}'"
try:
# 执行命令
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
try:
# 等待完成,带超时
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
# 处理输出
stdout_str = stdout.decode('utf-8', errors='replace').strip()
stderr_str = stderr.decode('utf-8', errors='replace').strip()
exit_code = process.returncode
result = f"命令执行完成 (退出码: {exit_code})"
if stdout_str:
result += f"\n\n标准输出:\n{stdout_str}"
if stderr_str:
result += f"\n\n错误输出:\n{stderr_str}"
# 如果没有输出
if not stdout_str and not stderr_str:
result += "\n\n(无输出内容)"
# 限制输出长度,防止上下文过长
if len(result) > 3000:
result = result[:3000] + "\n\n...(输出内容过长,已截断)"
return result
except asyncio.TimeoutError:
# 超时处理
try:
process.kill()
except ProcessLookupError:
pass
return f"命令执行超时 (限制: {timeout}秒)"
except Exception as e:
logger.error(f"执行命令失败: {e}", exc_info=True)
return f"执行命令时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/get_recommendations.py
================================================
"""获取推荐工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.recommend import RecommendChain
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
class GetRecommendationsInput(BaseModel):
"""获取推荐工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
source: Optional[str] = Field("tmdb_trending",
description="Recommendation source: "
"'tmdb_trending' for TMDB trending content, "
"'tmdb_movies' for TMDB popular movies, "
"'tmdb_tvs' for TMDB popular TV shows, "
"'douban_hot' for Douban popular content, "
"'douban_movie_hot' for Douban hot movies, "
"'douban_tv_hot' for Douban hot TV shows, "
"'douban_movie_showing' for Douban movies currently showing, "
"'douban_movies' for Douban latest movies, "
"'douban_tvs' for Douban latest TV shows, "
"'douban_movie_top250' for Douban movie TOP250, "
"'douban_tv_weekly_chinese' for Douban Chinese TV weekly chart, "
"'douban_tv_weekly_global' for Douban global TV weekly chart, "
"'douban_tv_animation' for Douban popular animation, "
"'bangumi_calendar' for Bangumi anime calendar")
media_type: Optional[str] = Field("all",
description="Allowed values: movie, tv, all")
limit: Optional[int] = Field(20,
description="Maximum number of recommendations to return (default: 20, maximum: 100)")
class GetRecommendationsTool(MoviePilotTool):
name: str = "get_recommendations"
description: str = "Get trending and popular media recommendations from various sources. Returns curated lists of popular movies, TV shows, and anime based on different criteria like trending, ratings, or calendar schedules."
args_schema: Type[BaseModel] = GetRecommendationsInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据推荐参数生成友好的提示消息"""
source = kwargs.get("source", "tmdb_trending")
media_type = kwargs.get("media_type", "all")
limit = kwargs.get("limit", 20)
source_map = {
"tmdb_trending": "TMDB流行趋势",
"tmdb_movies": "TMDB热门电影",
"tmdb_tvs": "TMDB热门电视剧",
"douban_hot": "豆瓣热门",
"douban_movie_hot": "豆瓣热门电影",
"douban_tv_hot": "豆瓣热门电视剧",
"douban_movie_showing": "豆瓣正在热映",
"douban_movies": "豆瓣最新电影",
"douban_tvs": "豆瓣最新电视剧",
"douban_movie_top250": "豆瓣电影TOP250",
"douban_tv_weekly_chinese": "豆瓣国产剧集榜",
"douban_tv_weekly_global": "豆瓣全球剧集榜",
"douban_tv_animation": "豆瓣热门动漫",
"bangumi_calendar": "番组计划"
}
source_desc = source_map.get(source, source)
message = f"正在获取推荐: {source_desc}"
if media_type != "all":
message += f" [{media_type}]"
message += f" (限制: {limit}条)"
return message
async def run(self, source: Optional[str] = "tmdb_trending",
media_type: Optional[str] = "all", limit: Optional[int] = 20, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: source={source}, media_type={media_type}, limit={limit}")
try:
if media_type != "all":
media_type_enum = MediaType.from_agent(media_type)
if not media_type_enum:
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv', 'all'"
media_type = media_type_enum.to_agent() # 归一化为 "movie"/"tv"
recommend_chain = RecommendChain()
results = []
if source == "tmdb_trending":
# async_tmdb_trending 只接受 page 参数,返回固定数量的结果
# 如果需要限制数量,需要在返回后截取
results = await recommend_chain.async_tmdb_trending(page=1)
if limit and limit > 0:
results = results[:limit]
elif source == "tmdb_movies":
# async_tmdb_movies 接受 page 参数,返回固定数量的结果
results = await recommend_chain.async_tmdb_movies(page=1)
if limit and limit > 0:
results = results[:limit]
elif source == "tmdb_tvs":
# async_tmdb_tvs 接受 page 参数,返回固定数量的结果
results = await recommend_chain.async_tmdb_tvs(page=1)
if limit and limit > 0:
results = results[:limit]
elif source == "douban_hot":
if media_type == "movie":
results = await recommend_chain.async_douban_movie_hot(page=1, count=limit)
elif media_type == "tv":
results = await recommend_chain.async_douban_tv_hot(page=1, count=limit)
else: # all
results.extend(await recommend_chain.async_douban_movie_hot(page=1, count=limit))
results.extend(await recommend_chain.async_douban_tv_hot(page=1, count=limit))
elif source == "douban_movie_hot":
results = await recommend_chain.async_douban_movie_hot(page=1, count=limit)
elif source == "douban_tv_hot":
results = await recommend_chain.async_douban_tv_hot(page=1, count=limit)
elif source == "douban_movie_showing":
results = await recommend_chain.async_douban_movie_showing(page=1, count=limit)
elif source == "douban_movies":
results = await recommend_chain.async_douban_movies(page=1, count=limit)
elif source == "douban_tvs":
results = await recommend_chain.async_douban_tvs(page=1, count=limit)
elif source == "douban_movie_top250":
results = await recommend_chain.async_douban_movie_top250(page=1, count=limit)
elif source == "douban_tv_weekly_chinese":
results = await recommend_chain.async_douban_tv_weekly_chinese(page=1, count=limit)
elif source == "douban_tv_weekly_global":
results = await recommend_chain.async_douban_tv_weekly_global(page=1, count=limit)
elif source == "douban_tv_animation":
results = await recommend_chain.async_douban_tv_animation(page=1, count=limit)
elif source == "bangumi_calendar":
results = await recommend_chain.async_bangumi_calendar(page=1, count=limit)
else:
# 不支持的推荐来源
supported_sources = [
"tmdb_trending", "tmdb_movies", "tmdb_tvs",
"douban_hot", "douban_movie_hot", "douban_tv_hot",
"douban_movie_showing", "douban_movies", "douban_tvs",
"douban_movie_top250", "douban_tv_weekly_chinese",
"douban_tv_weekly_global", "douban_tv_animation",
"bangumi_calendar"
]
return f"不支持的推荐来源: {source}。支持的来源包括: {', '.join(supported_sources)}"
if results:
# 限制最多20条结果
total_count = len(results)
limited_results = results[:20]
# 精简字段,只保留关键信息
simplified_results = []
for r in limited_results:
# r 应该是字典格式(to_dict的结果),但为了安全起见进行检查
if not isinstance(r, dict):
logger.warning(f"推荐结果格式异常,跳过: {type(r)}")
continue
simplified = {
"title": r.get("title"),
"en_title": r.get("en_title"),
"year": r.get("year"),
"type": media_type_to_agent(r.get("type")),
"season": r.get("season"),
"tmdb_id": r.get("tmdb_id"),
"imdb_id": r.get("imdb_id"),
"douban_id": r.get("douban_id"),
"vote_average": r.get("vote_average"),
"poster_path": r.get("poster_path"),
"detail_link": r.get("detail_link")
}
simplified_results.append(simplified)
result_json = json.dumps(simplified_results, ensure_ascii=False, indent=2)
# 如果结果被裁剪,添加提示信息
if total_count > 20:
return f"注意:推荐结果共找到 {total_count} 条,为节省上下文空间,仅显示前 20 条结果。\n\n{result_json}"
return result_json
return "未找到推荐内容。"
except Exception as e:
logger.error(f"获取推荐失败: {e}", exc_info=True)
return f"获取推荐时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/get_search_results.py
================================================
"""获取搜索结果工具"""
import json
import re
from typing import List, Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.search import SearchChain
from app.log import logger
from ._torrent_search_utils import (
TORRENT_RESULT_LIMIT,
build_filter_options,
filter_contexts,
simplify_search_result,
)
class GetSearchResultsInput(BaseModel):
"""获取搜索结果工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
site: Optional[List[str]] = Field(None, description="Site name filters")
season: Optional[List[str]] = Field(None, description="Season or episode filters")
free_state: Optional[List[str]] = Field(None, description="Promotion state filters")
video_code: Optional[List[str]] = Field(None, description="Video codec filters")
edition: Optional[List[str]] = Field(None, description="Edition filters")
resolution: Optional[List[str]] = Field(None, description="Resolution filters")
release_group: Optional[List[str]] = Field(None, description="Release group filters")
title_pattern: Optional[str] = Field(None, description="Regular expression pattern to filter torrent titles (e.g., '4K|2160p|UHD', '1080p.*BluRay')")
show_filter_options: Optional[bool] = Field(False, description="Whether to return only optional filter options for re-checking available conditions")
class GetSearchResultsTool(MoviePilotTool):
name: str = "get_search_results"
description: str = "Get cached torrent search results from search_torrents with optional filters. Returns at most the first 50 matches."
args_schema: Type[BaseModel] = GetSearchResultsInput
def get_tool_message(self, **kwargs) -> Optional[str]:
return "正在获取搜索结果"
async def run(self, site: Optional[List[str]] = None, season: Optional[List[str]] = None,
free_state: Optional[List[str]] = None, video_code: Optional[List[str]] = None,
edition: Optional[List[str]] = None, resolution: Optional[List[str]] = None,
release_group: Optional[List[str]] = None, title_pattern: Optional[str] = None,
show_filter_options: bool = False,
**kwargs) -> str:
logger.info(
f"执行工具: {self.name}, 参数: site={site}, season={season}, free_state={free_state}, video_code={video_code}, edition={edition}, resolution={resolution}, release_group={release_group}, title_pattern={title_pattern}, show_filter_options={show_filter_options}")
try:
items = await SearchChain().async_last_search_results() or []
if not items:
return "没有可用的搜索结果,请先使用 search_torrents 搜索"
if show_filter_options:
payload = {
"total_count": len(items),
"filter_options": build_filter_options(items),
}
return json.dumps(payload, ensure_ascii=False, indent=2)
regex_pattern = None
if title_pattern:
try:
regex_pattern = re.compile(title_pattern, re.IGNORECASE)
except re.error as e:
logger.warning(f"正则表达式编译失败: {title_pattern}, 错误: {e}")
return f"正则表达式格式错误: {str(e)}"
filtered_items = filter_contexts(
items=items,
site=site,
season=season,
free_state=free_state,
video_code=video_code,
edition=edition,
resolution=resolution,
release_group=release_group,
)
if regex_pattern:
filtered_items = [
item for item in filtered_items
if item.torrent_info and item.torrent_info.title
and regex_pattern.search(item.torrent_info.title)
]
if not filtered_items:
return "没有符合筛选条件的搜索结果,请调整筛选条件"
total_count = len(filtered_items)
filtered_ids = {id(item) for item in filtered_items}
matched_indices = [index for index, item in enumerate(items, start=1) if id(item) in filtered_ids]
limited_items = filtered_items[:TORRENT_RESULT_LIMIT]
limited_indices = matched_indices[:TORRENT_RESULT_LIMIT]
results = [
simplify_search_result(item, index)
for item, index in zip(limited_items, limited_indices)
]
payload = {
"total_count": total_count,
"results": results,
}
if total_count > TORRENT_RESULT_LIMIT:
payload["message"] = f"搜索结果共找到 {total_count} 条,仅显示前 {TORRENT_RESULT_LIMIT} 条结果。"
return json.dumps(payload, ensure_ascii=False, indent=2)
except Exception as e:
error_message = f"获取搜索结果失败: {str(e)}"
logger.error(f"获取搜索结果失败: {e}", exc_info=True)
return error_message
================================================
FILE: app/agent/tools/impl/list_directory.py
================================================
"""查询文件系统目录内容工具"""
import json
from datetime import datetime
from pathlib import Path
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.storage import StorageChain
from app.log import logger
from app.schemas.file import FileItem
from app.utils.string import StringUtils
class ListDirectoryInput(BaseModel):
"""查询文件系统目录内容工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
path: str = Field(..., description="Directory path to list contents (e.g., '/home/user/downloads' or 'C:/Downloads')")
storage: Optional[str] = Field("local", description="Storage type (default: 'local' for local file system, can be 'smb', 'alist', etc.)")
sort_by: Optional[str] = Field("name", description="Sort order: 'name' for alphabetical sorting, 'time' for modification time sorting (default: 'name')")
class ListDirectoryTool(MoviePilotTool):
name: str = "list_directory"
description: str = "List actual files and folders in a file system directory (NOT configuration). Shows files and subdirectories with their names, types, sizes, and modification times. Returns up to 20 items and the total count if there are more items. Use 'query_directory_settings' to query directory configuration settings."
args_schema: Type[BaseModel] = ListDirectoryInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据目录参数生成友好的提示消息"""
path = kwargs.get("path", "")
storage = kwargs.get("storage", "local")
message = f"正在查询目录: {path}"
if storage != "local":
message += f" [存储: {storage}]"
return message
async def run(self, path: str, storage: Optional[str] = "local",
sort_by: Optional[str] = "name", **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: path={path}, storage={storage}, sort_by={sort_by}")
try:
# 规范化路径
if not path:
return "错误:路径不能为空"
# 确保路径格式正确
if storage == "local":
# 本地路径处理
if not path.startswith("/") and not (len(path) > 1 and path[1] == ":"):
# 相对路径,尝试转换为绝对路径
path = str(Path(path).resolve())
else:
# 远程存储路径,确保以/开头
if not path.startswith("/"):
path = "/" + path
# 创建FileItem
fileitem = FileItem(
storage=storage or "local",
path=path,
type="dir"
)
# 查询目录内容
storage_chain = StorageChain()
file_list = storage_chain.list_files(fileitem, recursion=False)
if file_list is None:
return f"无法访问目录:{path},请检查路径是否正确或存储是否可用"
if not file_list:
return f"目录 {path} 为空"
# 排序
if sort_by == "time":
file_list.sort(key=lambda x: x.modify_time or 0, reverse=True)
else:
# 默认按名称排序(目录优先,然后按名称)
file_list.sort(key=lambda x: (
0 if x.type == "dir" else 1,
StringUtils.natural_sort_key(x.name or "")
))
# 限制返回数量
total_count = len(file_list)
limited_list = file_list[:20]
# 转换为字典格式
simplified_items = []
for item in limited_list:
# 格式化文件大小
size_str = None
if item.size:
size_str = StringUtils.str_filesize(item.size)
# 格式化修改时间
modify_time_str = None
if item.modify_time:
try:
modify_time_str = datetime.fromtimestamp(item.modify_time).strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, OSError):
modify_time_str = str(item.modify_time)
simplified = {
"name": item.name,
"type": item.type,
"path": item.path,
"size": size_str,
"modify_time": modify_time_str
}
# 如果是文件,添加扩展名
if item.type == "file" and item.extension:
simplified["extension"] = item.extension
simplified_items.append(simplified)
result_json = json.dumps(simplified_items, ensure_ascii=False, indent=2)
# 如果结果被裁剪,添加提示信息
if total_count > 20:
return f"注意:目录中共有 {total_count} 个项目,为节省上下文空间,仅显示前 20 个项目。\n\n{result_json}"
else:
return result_json
except Exception as e:
logger.error(f"查询目录内容失败: {e}", exc_info=True)
return f"查询目录内容时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/query_directory_settings.py
================================================
"""查询系统目录设置工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.helper.directory import DirectoryHelper
from app.log import logger
class QueryDirectorySettingsInput(BaseModel):
"""查询系统目录设置工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
directory_type: Optional[str] = Field("all",
description="Filter directories by type: 'download' for download directories, 'library' for media library directories, 'all' for all directories")
storage_type: Optional[str] = Field("all",
description="Filter directories by storage type: 'local' for local storage, 'remote' for remote storage, 'all' for all storage types")
name: Optional[str] = Field(None,
description="Filter directories by name (partial match, optional)")
class QueryDirectorySettingsTool(MoviePilotTool):
name: str = "query_directory_settings"
description: str = "Query system directory configuration settings (NOT file listings). Returns configured directory paths, storage types, transfer modes, and other directory-related settings. Use 'list_directory' to list actual files and folders in a directory."
args_schema: Type[BaseModel] = QueryDirectorySettingsInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息"""
directory_type = kwargs.get("directory_type", "all")
storage_type = kwargs.get("storage_type", "all")
name = kwargs.get("name")
parts = ["正在查询目录配置"]
if directory_type != "all":
type_map = {"download": "下载目录", "library": "媒体库目录"}
parts.append(f"类型: {type_map.get(directory_type, directory_type)}")
if storage_type != "all":
storage_map = {"local": "本地存储", "remote": "远程存储"}
parts.append(f"存储: {storage_map.get(storage_type, storage_type)}")
if name:
parts.append(f"名称: {name}")
return " | ".join(parts) if len(parts) > 1 else parts[0]
async def run(self, directory_type: Optional[str] = "all",
storage_type: Optional[str] = "all",
name: Optional[str] = None, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: directory_type={directory_type}, storage_type={storage_type}, name={name}")
try:
directory_helper = DirectoryHelper()
# 根据目录类型获取目录列表
if directory_type == "download":
dirs = directory_helper.get_download_dirs()
elif directory_type == "library":
dirs = directory_helper.get_library_dirs()
else:
dirs = directory_helper.get_dirs()
# 按存储类型过滤
filtered_dirs = []
for d in dirs:
# 按存储类型过滤
if storage_type == "local":
# 对于下载目录,检查 storage;对于媒体库目录,检查 library_storage
if directory_type == "download" and d.storage != "local":
continue
elif directory_type == "library" and d.library_storage != "local":
continue
elif directory_type == "all":
# 检查是否有本地存储配置
if d.download_path and d.storage != "local":
continue
if d.library_path and d.library_storage != "local":
continue
elif storage_type == "remote":
# 对于下载目录,检查 storage;对于媒体库目录,检查 library_storage
if directory_type == "download" and d.storage == "local":
continue
elif directory_type == "library" and d.library_storage == "local":
continue
elif directory_type == "all":
# 检查是否有远程存储配置
if d.download_path and d.storage == "local":
continue
if d.library_path and d.library_storage == "local":
continue
# 按名称过滤(部分匹配)
if name and d.name and name.lower() not in d.name.lower():
continue
filtered_dirs.append(d)
if filtered_dirs:
# 转换为字典格式,只保留关键信息
simplified_dirs = []
for d in filtered_dirs:
simplified = {
"name": d.name,
"priority": d.priority,
"storage": d.storage,
"download_path": d.download_path,
"library_path": d.library_path,
"library_storage": d.library_storage,
"media_type": d.media_type,
"media_category": d.media_category,
"monitor_type": d.monitor_type,
"monitor_mode": d.monitor_mode,
"transfer_type": d.transfer_type,
"overwrite_mode": d.overwrite_mode,
"renaming": d.renaming,
"scraping": d.scraping,
"notify": d.notify,
"download_type_folder": d.download_type_folder,
"download_category_folder": d.download_category_folder,
"library_type_folder": d.library_type_folder,
"library_category_folder": d.library_category_folder
}
simplified_dirs.append(simplified)
result_json = json.dumps(simplified_dirs, ensure_ascii=False, indent=2)
return result_json
return "未找到相关目录配置"
except Exception as e:
logger.error(f"查询系统目录设置失败: {e}", exc_info=True)
return f"查询系统目录设置时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/query_download_tasks.py
================================================
"""查询下载工具"""
import json
from typing import Optional, Type, List, Union
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.download import DownloadChain
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.log import logger
from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus, media_type_to_agent
class QueryDownloadTasksInput(BaseModel):
"""查询下载工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
downloader: Optional[str] = Field(None,
description="Name of specific downloader to query (optional, if not provided queries all configured downloaders)")
status: Optional[str] = Field("all",
description="Filter downloads by status: 'downloading' for active downloads, 'completed' for finished downloads, 'paused' for paused downloads, 'all' for all downloads")
hash: Optional[str] = Field(None, description="Query specific download task by hash (optional, if provided will search for this specific task regardless of status)")
title: Optional[str] = Field(None, description="Query download tasks by title/name (optional, supports partial match, searches all tasks if provided)")
class QueryDownloadTasksTool(MoviePilotTool):
name: str = "query_download_tasks"
description: str = "Query download status and list download tasks. Can query all active downloads, or search for specific tasks by hash or title. Shows download progress, completion status, and task details from configured downloaders."
args_schema: Type[BaseModel] = QueryDownloadTasksInput
@staticmethod
def _get_all_torrents(download_chain: DownloadChain, downloader: Optional[str] = None) -> List[Union[TransferTorrent, DownloadingTorrent]]:
"""
查询所有状态的任务(包括下载中和已完成的任务)
"""
all_torrents = []
# 查询正在下载的任务
downloading_torrents = download_chain.list_torrents(
downloader=downloader,
status=TorrentStatus.DOWNLOADING
) or []
all_torrents.extend(downloading_torrents)
# 查询已完成的任务(可转移状态)
transfer_torrents = download_chain.list_torrents(
downloader=downloader,
status=TorrentStatus.TRANSFER
) or []
all_torrents.extend(transfer_torrents)
return all_torrents
@staticmethod
def _format_progress(progress: Optional[float]) -> Optional[str]:
"""
将下载进度格式化为保留一位小数的百分比字符串
"""
try:
if progress is None:
return None
return f"{float(progress):.1f}%"
except (TypeError, ValueError):
return None
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息"""
downloader = kwargs.get("downloader")
status = kwargs.get("status", "all")
hash_value = kwargs.get("hash")
title = kwargs.get("title")
parts = ["正在查询下载任务"]
if downloader:
parts.append(f"下载器: {downloader}")
if status != "all":
status_map = {"downloading": "下载中", "completed": "已完成", "paused": "已暂停"}
parts.append(f"状态: {status_map.get(status, status)}")
if hash_value:
parts.append(f"Hash: {hash_value[:8]}...")
elif title:
parts.append(f"标题: {title}")
return " | ".join(parts) if len(parts) > 1 else parts[0]
async def run(self, downloader: Optional[str] = None,
status: Optional[str] = "all",
hash: Optional[str] = None,
title: Optional[str] = None, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: downloader={downloader}, status={status}, hash={hash}, title={title}")
try:
download_chain = DownloadChain()
# 如果提供了hash,直接查询该hash的任务(不限制状态)
if hash:
torrents = download_chain.list_torrents(downloader=downloader, hashs=[hash]) or []
if not torrents:
return f"未找到hash为 {hash} 的下载任务(该任务可能已完成、已删除或不存在)"
# 转换为DownloadingTorrent格式
downloads = []
for torrent in torrents:
# 获取下载历史信息
history = DownloadHistoryOper().get_by_hash(torrent.hash)
if history:
torrent.media = {
"tmdbid": history.tmdbid,
"type": history.type,
"title": history.title,
"season": history.seasons,
"episode": history.episodes,
"image": history.image,
}
torrent.userid = history.userid
torrent.username = history.username
downloads.append(torrent)
filtered_downloads = downloads
elif title:
# 如果提供了title,查询所有任务并搜索匹配的标题
# 查询所有状态的任务
all_torrents = self._get_all_torrents(download_chain, downloader)
filtered_downloads = []
title_lower = title.lower()
for torrent in all_torrents:
# 获取下载历史信息
history = DownloadHistoryOper().get_by_hash(torrent.hash)
# 检查标题或名称是否匹配(包括下载历史中的标题)
matched = False
# 检查torrent的title和name字段
if (title_lower in (torrent.title or "").lower()) or \
(title_lower in (torrent.name or "").lower()):
matched = True
# 检查下载历史中的标题
if history and history.title:
if title_lower in history.title.lower():
matched = True
if matched:
if history:
torrent.media = {
"tmdbid": history.tmdbid,
"type": history.type,
"title": history.title,
"season": history.seasons,
"episode": history.episodes,
"image": history.image,
}
torrent.userid = history.userid
torrent.username = history.username
filtered_downloads.append(torrent)
if not filtered_downloads:
return f"未找到标题包含 '{title}' 的下载任务"
else:
# 根据status决定查询方式
if status == "downloading":
# 如果status为下载中,使用downloading方法
downloads = download_chain.downloading(name=downloader) or []
filtered_downloads = []
for dl in downloads:
if downloader and dl.downloader != downloader:
continue
filtered_downloads.append(dl)
else:
# 其他状态(completed、paused、all),使用list_torrents查询所有任务
# 查询所有状态的任务
all_torrents = self._get_all_torrents(download_chain, downloader)
filtered_downloads = []
for torrent in all_torrents:
if downloader and torrent.downloader != downloader:
continue
# 根据status过滤
if status == "completed":
# 已完成的任务(state为seeding或completed)
if torrent.state not in ["seeding", "completed"]:
continue
elif status == "paused":
# 已暂停的任务
if torrent.state != "paused":
continue
# status == "all" 时不过滤
# 获取下载历史信息
history = DownloadHistoryOper().get_by_hash(torrent.hash)
if history:
torrent.media = {
"tmdbid": history.tmdbid,
"type": history.type,
"title": history.title,
"season": history.seasons,
"episode": history.episodes,
"image": history.image,
}
torrent.userid = history.userid
torrent.username = history.username
filtered_downloads.append(torrent)
if filtered_downloads:
# 限制最多20条结果
total_count = len(filtered_downloads)
limited_downloads = filtered_downloads[:20]
# 精简字段,只保留关键信息
simplified_downloads = []
for d in limited_downloads:
simplified = {
"downloader": d.downloader,
"hash": d.hash,
"title": d.title,
"name": d.name,
"year": d.year,
"season_episode": d.season_episode,
"size": d.size,
"progress": self._format_progress(d.progress),
"state": d.state,
"upspeed": d.upspeed,
"dlspeed": d.dlspeed,
"left_time": d.left_time
}
# 精简 media 字段
if d.media:
simplified["media"] = {
"tmdbid": d.media.get("tmdbid"),
"type": media_type_to_agent(d.media.get("type")),
"title": d.media.get("title"),
"season": d.media.get("season"),
"episode": d.media.get("episode")
}
simplified_downloads.append(simplified)
result_json = json.dumps(simplified_downloads, ensure_ascii=False, indent=2)
# 如果结果被裁剪,添加提示信息
if total_count > 20:
return f"注意:查询结果共找到 {total_count} 条,为节省上下文空间,仅显示前 20 条结果。\n\n{result_json}"
# 如果查询的是特定hash或title,添加明确的状态信息
if hash:
return f"找到hash为 {hash} 的下载任务:\n\n{result_json}"
elif title:
return f"找到 {total_count} 个标题包含 '{title}' 的下载任务:\n\n{result_json}"
return result_json
return "未找到相关下载任务"
except Exception as e:
logger.error(f"查询下载失败: {e}", exc_info=True)
return f"查询下载时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/query_downloaders.py
================================================
"""查询下载器工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.schemas.types import SystemConfigKey
class QueryDownloadersInput(BaseModel):
"""查询下载器工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
class QueryDownloadersTool(MoviePilotTool):
name: str = "query_downloaders"
description: str = "Query downloader configuration and list all available downloaders. Shows downloader status, connection details, and configuration settings."
args_schema: Type[BaseModel] = QueryDownloadersInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""生成友好的提示消息"""
return "正在查询下载器配置"
async def run(self, **kwargs) -> str:
logger.info(f"执行工具: {self.name}")
try:
system_config_oper = SystemConfigOper()
downloaders_config = system_config_oper.get(SystemConfigKey.Downloaders)
if downloaders_config:
return json.dumps(downloaders_config, ensure_ascii=False, indent=2)
return "未配置下载器。"
except Exception as e:
logger.error(f"查询下载器失败: {e}")
return f"查询下载器时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/query_episode_schedule.py
================================================
"""查询剧集上映时间工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.tmdb import TmdbChain
from app.log import logger
class QueryEpisodeScheduleInput(BaseModel):
"""查询剧集上映时间工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
tmdb_id: int = Field(..., description="TMDB ID of the TV series (can be obtained from search_media tool)")
season: int = Field(..., description="Season number to query")
episode_group: Optional[str] = Field(None, description="Episode group ID (optional)")
class QueryEpisodeScheduleTool(MoviePilotTool):
name: str = "query_episode_schedule"
description: str = "Query TV series episode air dates and schedule. Returns non-duplicated schedule fields, including episode list, air-date statistics, and per-episode metadata. Filters out episodes without air dates."
args_schema: Type[BaseModel] = QueryEpisodeScheduleInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息"""
tmdb_id = kwargs.get("tmdb_id")
season = kwargs.get("season")
episode_group = kwargs.get("episode_group")
message = f"正在查询剧集上映时间: TMDB ID {tmdb_id} 第{season}季"
if episode_group:
message += f" (剧集组: {episode_group})"
return message
async def run(self, tmdb_id: int, season: int, episode_group: Optional[str] = None, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: tmdb_id={tmdb_id}, season={season}, episode_group={episode_group}")
try:
# 获取集列表
tmdb_chain = TmdbChain()
episodes = await tmdb_chain.async_tmdb_episodes(
tmdbid=tmdb_id,
season=season,
episode_group=episode_group
)
if not episodes:
return json.dumps({
"success": False,
"message": f"未找到 TMDB ID {tmdb_id} 第{season}季的集信息"
}, ensure_ascii=False)
# 过滤掉没有上映日期的集,并构建每集的详细信息
episode_list = []
for episode in episodes:
air_date = episode.air_date
# 过滤掉没有上映日期的数据
if not air_date:
continue
episode_info = {
"episode_number": episode.episode_number,
"name": episode.name,
"air_date": air_date,
"runtime": episode.runtime,
"vote_average": episode.vote_average,
"still_path": episode.still_path,
"episode_type": episode.episode_type,
"season_number": episode.season_number
}
episode_list.append(episode_info)
if not episode_list:
return json.dumps({
"success": False,
"message": f"未找到 TMDB ID {tmdb_id} 第{season}季的播出时间信息(所有集都没有播出日期)"
}, ensure_ascii=False)
# 按播出日期排序
episode_list.sort(key=lambda x: (x["air_date"] or "", x["episode_number"] or 0))
result = {
"season": season,
"total_episodes": len(episodes),
"episodes_with_air_date": len(episode_list),
"episodes": episode_list
}
return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
error_message = f"查询剧集上映时间失败: {str(e)}"
logger.error(f"查询剧集上映时间失败: {e}", exc_info=True)
return json.dumps({
"success": False,
"message": error_message,
"tmdb_id": tmdb_id,
"season": season
}, ensure_ascii=False)
================================================
FILE: app/agent/tools/impl/query_library_exists.py
================================================
"""查询媒体库工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.mediaserver import MediaServerChain
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
class QueryLibraryExistsInput(BaseModel):
"""查询媒体库工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
tmdb_id: Optional[int] = Field(None, description="TMDB ID (can be obtained from search_media tool). Either tmdb_id or douban_id must be provided.")
douban_id: Optional[str] = Field(None, description="Douban ID (can be obtained from search_media tool). Either tmdb_id or douban_id must be provided.")
media_type: Optional[str] = Field(None, description="Allowed values: movie, tv")
class QueryLibraryExistsTool(MoviePilotTool):
name: str = "query_library_exists"
description: str = "Check whether a specific media resource already exists in the media library (Plex, Emby, Jellyfin) by media ID. Requires tmdb_id or douban_id (can be obtained from search_media tool) for accurate matching."
args_schema: Type[BaseModel] = QueryLibraryExistsInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息"""
tmdb_id = kwargs.get("tmdb_id")
douban_id = kwargs.get("douban_id")
media_type = kwargs.get("media_type")
if tmdb_id:
message = f"正在查询媒体库: TMDB={tmdb_id}"
elif douban_id:
message = f"正在查询媒体库: 豆瓣={douban_id}"
else:
message = "正在查询媒体库"
if media_type:
message += f" [{media_type}]"
return message
async def run(self, tmdb_id: Optional[int] = None, douban_id: Optional[str] = None,
media_type: Optional[str] = None, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: tmdb_id={tmdb_id}, douban_id={douban_id}, media_type={media_type}")
try:
if not tmdb_id and not douban_id:
return "参数错误:tmdb_id 和 douban_id 至少需要提供一个,请先使用 search_media 工具获取媒体 ID。"
media_type_enum = None
if media_type:
media_type_enum = MediaType.from_agent(media_type)
if not media_type_enum:
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv'"
media_chain = MediaServerChain()
mediainfo = media_chain.recognize_media(
tmdbid=tmdb_id,
doubanid=douban_id,
mtype=media_type_enum,
)
if not mediainfo:
media_id = f"TMDB={tmdb_id}" if tmdb_id else f"豆瓣={douban_id}"
return f"未识别到媒体信息: {media_id}"
# 2. 调用媒体服务器接口实时查询存在信息
existsinfo = media_chain.media_exists(mediainfo=mediainfo)
if not existsinfo:
return "媒体库中未找到相关媒体"
# 3. 如果找到了,获取详细信息并组装结果
result_items = []
if existsinfo.itemid and existsinfo.server:
iteminfo = media_chain.iteminfo(server=existsinfo.server, item_id=existsinfo.itemid)
if iteminfo:
# 使用 model_dump() 转换为字典格式
item_dict = iteminfo.model_dump(exclude_none=True)
# 对于电视剧,补充已存在的季集详情及进度统计
if existsinfo.type == MediaType.TV:
# 注入已存在集信息 (Dict[int, list])
item_dict["seasoninfo"] = existsinfo.seasons
# 统计库中已存在的季集总数
if existsinfo.seasons:
item_dict["existing_episodes_count"] = sum(len(e) for e in existsinfo.seasons.values())
item_dict["seasons_existing_count"] = {str(s): len(e) for s, e in existsinfo.seasons.items()}
# 如果识别到了元数据,补充总计对比和进度概览
if mediainfo.seasons:
item_dict["seasons_total_count"] = {str(s): len(e) for s, e in mediainfo.seasons.items()}
# 进度概览,例如 "Season 1": "3/12"
item_dict["seasons_progress"] = {
f"第{s}季": f"{len(existsinfo.seasons.get(s, []))}/{len(mediainfo.seasons.get(s, []))} 集"
for s in mediainfo.seasons.keys() if (s in existsinfo.seasons or s > 0)
}
result_items.append(item_dict)
if result_items:
return json.dumps(result_items, ensure_ascii=False)
# 如果找到了但没有获取到 iteminfo,返回基本信息
result_dict = {
"title": mediainfo.title,
"year": mediainfo.year,
"type": media_type_to_agent(existsinfo.type),
"server": existsinfo.server,
"server_type": existsinfo.server_type,
"itemid": existsinfo.itemid,
"seasons": existsinfo.seasons if existsinfo.seasons else {}
}
if existsinfo.type == MediaType.TV and existsinfo.seasons:
result_dict["existing_episodes_count"] = sum(len(e) for e in existsinfo.seasons.values())
result_dict["seasons_existing_count"] = {str(s): len(e) for s, e in existsinfo.seasons.items()}
if mediainfo.seasons:
result_dict["seasons_total_count"] = {str(s): len(e) for s, e in mediainfo.seasons.items()}
return json.dumps([result_dict], ensure_ascii=False)
except Exception as e:
logger.error(f"查询媒体库失败: {e}", exc_info=True)
return f"查询媒体库时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/query_library_latest.py
================================================
"""查询媒体服务器最近入库影片工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.mediaserver import MediaServerChain
from app.helper.service import ServiceConfigHelper
from app.log import logger
class QueryLibraryLatestInput(BaseModel):
"""查询媒体服务器最近入库影片工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
server: Optional[str] = Field(None, description="Media server name (optional, if not specified queries all enabled media servers)")
count: Optional[int] = Field(20, description="Number of items to return (default: 20)")
class QueryLibraryLatestTool(MoviePilotTool):
name: str = "query_library_latest"
description: str = "Query the latest media items added to the media server (Plex, Emby, Jellyfin). Returns recently added movies and TV series with their titles, images, links, and other metadata."
args_schema: Type[BaseModel] = QueryLibraryLatestInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息"""
server = kwargs.get("server")
count = kwargs.get("count", 20)
parts = ["正在查询媒体服务器最近入库影片"]
if server:
parts.append(f"服务器: {server}")
else:
parts.append("所有服务器")
parts.append(f"数量: {count}条")
return " | ".join(parts)
async def run(self, server: Optional[str] = None, count: Optional[int] = 20, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: server={server}, count={count}")
try:
media_chain = MediaServerChain()
results = []
# 如果没有指定服务器,获取所有启用的媒体服务器
if not server:
mediaservers = ServiceConfigHelper.get_mediaserver_configs()
enabled_servers = [ms.name for ms in mediaservers if ms.enabled]
if not enabled_servers:
return "未找到启用的媒体服务器"
# 遍历所有启用的服务器
for server_name in enabled_servers:
latest_items = media_chain.latest(server=server_name, count=count, username=self._username)
if latest_items:
for item in latest_items:
item_dict = item.model_dump(exclude_none=True)
item_dict["server"] = server_name
results.append(item_dict)
else:
# 查询指定服务器
latest_items = media_chain.latest(server=server, count=count, username=self._username)
if latest_items:
for item in latest_items:
item_dict = item.model_dump(exclude_none=True)
item_dict["server"] = server
results.append(item_dict)
if not results:
server_info = f"服务器 {server}" if server else "所有服务器"
return f"未找到 {server_info} 的最近入库影片"
# 限制返回数量,避免结果过多
if len(results) > count:
results = results[:count]
return json.dumps(results, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"查询媒体服务器最近入库影片失败: {e}", exc_info=True)
return f"查询媒体服务器最近入库影片时发生错误: {str(e)}"
================================================
FILE: app/agent/tools/impl/query_media_detail.py
================================================
"""查询媒体详情工具"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.media import MediaChain
from app.log import logger
from app.schemas.types import MediaType
class QueryMediaDetailInput(BaseModel):
"""查询媒体详情工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
tmdb_id: Optional[int] = Field(None, description="TMDB ID of the media (movie or TV series, can be obtained from search_media tool)")
douban_id: Optional[str] = Field(None, description="Douban ID of the media (alternative to tmdb_id)")
media_type: str = Field(..., description="Allowed values: movie, tv")
class QueryMediaDetailTool(MoviePilotTool):
name: str = "query_media_detail"
description: str = "Query supplementary media details from TMDB by ID and media_type. Accepts tmdb_id or douban_id (at least one required). media_type accepts 'movie' or 'tv'. Returns non-duplicated detail fields such as status, genres, directors, actors, and season info for TV series."
args_schema: Type[BaseModel] = QueryMediaDetailInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据查询参数生成友好的提示消息"""
tmdb_id = kwargs.get("tmdb_id")
douban_id = kwargs.get("douban_id")
if tmdb_id:
return f"正在查询媒体详情: TMDB ID {tmdb_id}"
return f"正在查询媒体详情: 豆瓣 ID {douban_id}"
async def run(self, media_type: str, tmdb_id: Optional[int] = None, douban_id: Optional[str] = None, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: tmdb_id={tmdb_id}, douban_id={douban_id}, media_type={media_type}")
if tmdb_id is None and douban_id is None:
return json.dumps({
"success": False,
"message": "必须提供 tmdb_id 或 douban_id 之一"
}, ensure_ascii=False)
try:
media_chain = MediaChain()
media_type_enum = MediaType.from_agent(media_type)
if not media_type_enum:
return json.dumps({
"success": False,
"message": f"无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv'"
}, ensure_ascii=False)
mediainfo = await media_chain.async_recognize_media(tmdbid=tmdb_id, doubanid=douban_id, mtype=media_type_enum)
if not mediainfo:
id_info = f"TMDB ID {tmdb_id}" if tmdb_id else f"豆瓣 ID {douban_id}"
return json.dumps({
"success": False,
"message": f"未找到 {id_info} 的媒体信息"
}, ensure_ascii=False)
# 精简 genres - 只保留名称
genres = [g.get("name") for g in (mediainfo.genres or []) if g.get("name")]
# 精简 directors - 只保留姓名和职位
directors = [
{
"name": d.get("name"),
"job": d.get("job")
}
for d in (mediainfo.directors or [])
if d.get("name")
]
# 精简 actors - 只保留姓名和角色
actors = [
{
"name": a.get("name"),
"character": a.get("character")
}
for a in (mediainfo.actors or [])
if a.get("name")
]
# 构建基础媒体详情信息
result = {
"status": mediainfo.status,
"genres": genres,
"directors": directors,
"actors": actors
}
# 如果是电视剧,添加电视剧特有信息
if mediainfo.type == MediaType.TV:
# 精简 season_info - 只保留基础摘要
season_info = [
{
"season_number": s.get("season_number"),
"name": s.get("name"),
"episode_count": s.get("episode_count"),
"air_date": s.get("air_date")
}
for s in (mediainfo.season_info or [])
if s.get("season_number") is not None
]
result.update({
"number_of_seasons": mediainfo.number_of_seasons,
"number_of_episodes": mediainfo.number_of_episodes,
"first_air_date": mediainfo.first_air_date,
"last_air_date": mediainfo.last_air_date,
"season_info": season_info
})
return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
error_message = f"查询媒体详情失败: {str(e)}"
logger.error(f"查询媒体详情失败: {e}", exc_info=True)
return json.dumps({
"success": False,
"message": error_message,
"tmdb_id": tmdb_id,
"douban_id": douban_id
}, ensure_ascii=False)
================================================
FILE: app/agent/tools/impl/query_popular_subscribes.py
================================================
"""查询热门订阅工具"""
import json
from typing import Optional, Type
import cn2an
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.core.con
gitextract_jejzfg73/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── rfc.yml │ └── workflows/ │ ├── beta.yml │ ├── build.yml │ ├── issues.yml │ └── pylint.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── app/ │ ├── __init__.py │ ├── agent/ │ │ ├── __init__.py │ │ ├── callback/ │ │ │ └── __init__.py │ │ ├── memory/ │ │ │ └── __init__.py │ │ ├── prompt/ │ │ │ ├── Agent Prompt.txt │ │ │ └── __init__.py │ │ └── tools/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── factory.py │ │ ├── impl/ │ │ │ ├── __init__.py │ │ │ ├── _torrent_search_utils.py │ │ │ ├── add_download.py │ │ │ ├── add_subscribe.py │ │ │ ├── delete_download.py │ │ │ ├── delete_subscribe.py │ │ │ ├── execute_command.py │ │ │ ├── get_recommendations.py │ │ │ ├── get_search_results.py │ │ │ ├── list_directory.py │ │ │ ├── query_directory_settings.py │ │ │ ├── query_download_tasks.py │ │ │ ├── query_downloaders.py │ │ │ ├── query_episode_schedule.py │ │ │ ├── query_library_exists.py │ │ │ ├── query_library_latest.py │ │ │ ├── query_media_detail.py │ │ │ ├── query_popular_subscribes.py │ │ │ ├── query_rule_groups.py │ │ │ ├── query_schedulers.py │ │ │ ├── query_site_userdata.py │ │ │ ├── query_sites.py │ │ │ ├── query_subscribe_history.py │ │ │ ├── query_subscribe_shares.py │ │ │ ├── query_subscribes.py │ │ │ ├── query_transfer_history.py │ │ │ ├── query_workflows.py │ │ │ ├── recognize_media.py │ │ │ ├── run_scheduler.py │ │ │ ├── run_workflow.py │ │ │ ├── scrape_metadata.py │ │ │ ├── search_media.py │ │ │ ├── search_person.py │ │ │ ├── search_person_credits.py │ │ │ ├── search_subscribe.py │ │ │ ├── search_torrents.py │ │ │ ├── search_web.py │ │ │ ├── send_message.py │ │ │ ├── test_site.py │ │ │ ├── transfer_file.py │ │ │ ├── update_site.py │ │ │ ├── update_site_cookie.py │ │ │ └── update_subscribe.py │ │ └── manager.py │ ├── api/ │ │ ├── __init__.py │ │ ├── apiv1.py │ │ ├── endpoints/ │ │ │ ├── __init__.py │ │ │ ├── bangumi.py │ │ │ ├── dashboard.py │ │ │ ├── discover.py │ │ │ ├── douban.py │ │ │ ├── download.py │ │ │ ├── history.py │ │ │ ├── login.py │ │ │ ├── mcp.py │ │ │ ├── media.py │ │ │ ├── mediaserver.py │ │ │ ├── message.py │ │ │ ├── mfa.py │ │ │ ├── plugin.py │ │ │ ├── recommend.py │ │ │ ├── search.py │ │ │ ├── site.py │ │ │ ├── storage.py │ │ │ ├── subscribe.py │ │ │ ├── system.py │ │ │ ├── tmdb.py │ │ │ ├── torrent.py │ │ │ ├── transfer.py │ │ │ ├── user.py │ │ │ ├── webhook.py │ │ │ └── workflow.py │ │ ├── servarr.py │ │ └── servcookie.py │ ├── chain/ │ │ ├── __init__.py │ │ ├── ai_recommend.py │ │ ├── bangumi.py │ │ ├── dashboard.py │ │ ├── douban.py │ │ ├── download.py │ │ ├── media.py │ │ ├── mediaserver.py │ │ ├── message.py │ │ ├── recommend.py │ │ ├── search.py │ │ ├── site.py │ │ ├── storage.py │ │ ├── subscribe.py │ │ ├── system.py │ │ ├── tmdb.py │ │ ├── torrents.py │ │ ├── transfer.py │ │ ├── tvdb.py │ │ ├── user.py │ │ ├── webhook.py │ │ └── workflow.py │ ├── command.py │ ├── core/ │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── config.py │ │ ├── context.py │ │ ├── event.py │ │ ├── meta/ │ │ │ ├── __init__.py │ │ │ ├── customization.py │ │ │ ├── metaanime.py │ │ │ ├── metabase.py │ │ │ ├── metavideo.py │ │ │ ├── releasegroup.py │ │ │ ├── streamingplatform.py │ │ │ └── words.py │ │ ├── metainfo.py │ │ ├── module.py │ │ ├── plugin.py │ │ └── security.py │ ├── db/ │ │ ├── __init__.py │ │ ├── downloadhistory_oper.py │ │ ├── init.py │ │ ├── mediaserver_oper.py │ │ ├── message_oper.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ ├── downloadhistory.py │ │ │ ├── mediaserver.py │ │ │ ├── message.py │ │ │ ├── passkey.py │ │ │ ├── plugindata.py │ │ │ ├── site.py │ │ │ ├── siteicon.py │ │ │ ├── sitestatistic.py │ │ │ ├── siteuserdata.py │ │ │ ├── subscribe.py │ │ │ ├── subscribehistory.py │ │ │ ├── systemconfig.py │ │ │ ├── transferhistory.py │ │ │ ├── user.py │ │ │ ├── userconfig.py │ │ │ └── workflow.py │ │ ├── plugindata_oper.py │ │ ├── site_oper.py │ │ ├── subscribe_oper.py │ │ ├── systemconfig_oper.py │ │ ├── transferhistory_oper.py │ │ ├── user_oper.py │ │ ├── userconfig_oper.py │ │ └── workflow_oper.py │ ├── factory.py │ ├── helper/ │ │ ├── __init__.py │ │ ├── browser.py │ │ ├── cloudflare.py │ │ ├── cookie.py │ │ ├── cookiecloud.py │ │ ├── directory.py │ │ ├── display.py │ │ ├── doh.py │ │ ├── downloader.py │ │ ├── format.py │ │ ├── image.py │ │ ├── llm.py │ │ ├── mediaserver.py │ │ ├── message.py │ │ ├── module.py │ │ ├── nfo.py │ │ ├── notification.py │ │ ├── ocr.py │ │ ├── passkey.py │ │ ├── plugin.py │ │ ├── progress.py │ │ ├── redis.py │ │ ├── resource.py │ │ ├── rss.py │ │ ├── rule.py │ │ ├── service.py │ │ ├── storage.py │ │ ├── subscribe.py │ │ ├── system.py │ │ ├── thread.py │ │ ├── torrent.py │ │ ├── twofa.py │ │ └── workflow.py │ ├── log.py │ ├── main.py │ ├── modules/ │ │ ├── __init__.py │ │ ├── bangumi/ │ │ │ ├── __init__.py │ │ │ └── bangumi.py │ │ ├── discord/ │ │ │ ├── __init__.py │ │ │ └── discord.py │ │ ├── douban/ │ │ │ ├── __init__.py │ │ │ ├── apiv2.py │ │ │ ├── douban_cache.py │ │ │ └── scraper.py │ │ ├── emby/ │ │ │ ├── __init__.py │ │ │ └── emby.py │ │ ├── fanart/ │ │ │ └── __init__.py │ │ ├── filemanager/ │ │ │ ├── __init__.py │ │ │ ├── storages/ │ │ │ │ ├── __init__.py │ │ │ │ ├── alipan.py │ │ │ │ ├── alist.py │ │ │ │ ├── local.py │ │ │ │ ├── rclone.py │ │ │ │ ├── smb.py │ │ │ │ └── u115.py │ │ │ └── transhandler.py │ │ ├── filter/ │ │ │ ├── RuleParser.py │ │ │ └── __init__.py │ │ ├── indexer/ │ │ │ ├── __init__.py │ │ │ ├── parser/ │ │ │ │ ├── __init__.py │ │ │ │ ├── bitpt.py │ │ │ │ ├── discuz.py │ │ │ │ ├── file_list.py │ │ │ │ ├── gazelle.py │ │ │ │ ├── hddolby.py │ │ │ │ ├── ipt_project.py │ │ │ │ ├── mtorrent.py │ │ │ │ ├── nexus_audiences.py │ │ │ │ ├── nexus_hhanclub.py │ │ │ │ ├── nexus_php.py │ │ │ │ ├── nexus_project.py │ │ │ │ ├── nexus_rabbit.py │ │ │ │ ├── rousi.py │ │ │ │ ├── small_horse.py │ │ │ │ ├── tnode.py │ │ │ │ ├── torrent_leech.py │ │ │ │ ├── unit3d.py │ │ │ │ ├── yema.py │ │ │ │ └── zhixing.py │ │ │ └── spider/ │ │ │ ├── __init__.py │ │ │ ├── haidan.py │ │ │ ├── hddolby.py │ │ │ ├── mtorrent.py │ │ │ ├── rousi.py │ │ │ ├── tnode.py │ │ │ ├── torrentleech.py │ │ │ └── yema.py │ │ ├── jellyfin/ │ │ │ ├── __init__.py │ │ │ └── jellyfin.py │ │ ├── plex/ │ │ │ ├── __init__.py │ │ │ └── plex.py │ │ ├── postgresql/ │ │ │ └── __init__.py │ │ ├── qbittorrent/ │ │ │ ├── __init__.py │ │ │ └── qbittorrent.py │ │ ├── qqbot/ │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── gateway.py │ │ │ └── qqbot.py │ │ ├── redis/ │ │ │ └── __init__.py │ │ ├── rtorrent/ │ │ │ ├── __init__.py │ │ │ └── rtorrent.py │ │ ├── slack/ │ │ │ ├── __init__.py │ │ │ └── slack.py │ │ ├── subtitle/ │ │ │ └── __init__.py │ │ ├── synologychat/ │ │ │ ├── __init__.py │ │ │ └── synologychat.py │ │ ├── telegram/ │ │ │ ├── __init__.py │ │ │ └── telegram.py │ │ ├── themoviedb/ │ │ │ ├── __init__.py │ │ │ ├── category.py │ │ │ ├── scraper.py │ │ │ ├── tmdb_cache.py │ │ │ ├── tmdbapi.py │ │ │ └── tmdbv3api/ │ │ │ ├── __init__.py │ │ │ ├── as_obj.py │ │ │ ├── exceptions.py │ │ │ ├── objs/ │ │ │ │ ├── __init__.py │ │ │ │ ├── account.py │ │ │ │ ├── auth.py │ │ │ │ ├── certification.py │ │ │ │ ├── change.py │ │ │ │ ├── collection.py │ │ │ │ ├── company.py │ │ │ │ ├── configuration.py │ │ │ │ ├── credit.py │ │ │ │ ├── discover.py │ │ │ │ ├── episode.py │ │ │ │ ├── find.py │ │ │ │ ├── genre.py │ │ │ │ ├── group.py │ │ │ │ ├── keyword.py │ │ │ │ ├── list.py │ │ │ │ ├── movie.py │ │ │ │ ├── network.py │ │ │ │ ├── person.py │ │ │ │ ├── provider.py │ │ │ │ ├── review.py │ │ │ │ ├── search.py │ │ │ │ ├── season.py │ │ │ │ ├── trending.py │ │ │ │ └── tv.py │ │ │ └── tmdb.py │ │ ├── thetvdb/ │ │ │ ├── __init__.py │ │ │ └── tvdb_v4_official.py │ │ ├── transmission/ │ │ │ ├── __init__.py │ │ │ └── transmission.py │ │ ├── trimemedia/ │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── trimemedia.py │ │ ├── ugreen/ │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── ugreen.py │ │ ├── vocechat/ │ │ │ ├── __init__.py │ │ │ └── vocechat.py │ │ ├── webpush/ │ │ │ └── __init__.py │ │ └── wechat/ │ │ ├── WXBizMsgCrypt3.py │ │ ├── __init__.py │ │ ├── wechat.py │ │ └── wechatbot.py │ ├── monitor.py │ ├── plugins/ │ │ └── __init__.py │ ├── scheduler.py │ ├── schemas/ │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── category.py │ │ ├── context.py │ │ ├── dashboard.py │ │ ├── download.py │ │ ├── event.py │ │ ├── exception.py │ │ ├── file.py │ │ ├── history.py │ │ ├── mcp.py │ │ ├── mediaserver.py │ │ ├── message.py │ │ ├── monitoring.py │ │ ├── plugin.py │ │ ├── response.py │ │ ├── rule.py │ │ ├── servarr.py │ │ ├── servcookie.py │ │ ├── site.py │ │ ├── subscribe.py │ │ ├── system.py │ │ ├── tmdb.py │ │ ├── token.py │ │ ├── transfer.py │ │ ├── types.py │ │ ├── user.py │ │ └── workflow.py │ ├── startup/ │ │ ├── __init__.py │ │ ├── agent_initializer.py │ │ ├── command_initializer.py │ │ ├── lifecycle.py │ │ ├── modules_initializer.py │ │ ├── monitor_initializer.py │ │ ├── plugins_initializer.py │ │ ├── routers_initializer.py │ │ ├── scheduler_initializer.py │ │ └── workflow_initializer.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── common.py │ │ ├── crypto.py │ │ ├── debounce.py │ │ ├── dom.py │ │ ├── gc.py │ │ ├── http.py │ │ ├── ip.py │ │ ├── limit.py │ │ ├── mixins.py │ │ ├── object.py │ │ ├── otp.py │ │ ├── security.py │ │ ├── singleton.py │ │ ├── site.py │ │ ├── string.py │ │ ├── structures.py │ │ ├── system.py │ │ ├── timer.py │ │ ├── tokens.py │ │ ├── ugreen_crypto.py │ │ ├── url.py │ │ └── web.py │ └── workflow/ │ ├── __init__.py │ └── actions/ │ ├── __init__.py │ ├── add_download.py │ ├── add_subscribe.py │ ├── fetch_downloads.py │ ├── fetch_medias.py │ ├── fetch_rss.py │ ├── fetch_torrents.py │ ├── filter_medias.py │ ├── filter_torrents.py │ ├── invoke_plugin.py │ ├── note.py │ ├── scan_file.py │ ├── scrape_file.py │ ├── send_event.py │ ├── send_message.py │ └── transfer_file.py ├── config/ │ └── category.yaml ├── database/ │ ├── env.py │ ├── gen.py │ ├── script.py.mako │ └── versions/ │ ├── 0fb94bf69b38_2_0_2.py │ ├── 262735d025da_2_0_1.py │ ├── 279a949d81b6_2_1_1.py │ ├── 294b007932ef_2_0_0.py │ ├── 3891a5e722a1_2_1_7.py │ ├── 3df653756eec_2_1_6.py │ ├── 41ef1dd7467c_2_2_2.py │ ├── 4666ce24a443_2_1_8.py │ ├── 486e56a62dcb_2_1_5.py │ ├── 4b544f5d3b07_2_1_3.py │ ├── 55390f1f77c1_2_0_9.py │ ├── 58edfac72c32_2_2_3.py │ ├── 5b3355c964bb_2_2_0.py │ ├── 610bb05ddeef_2_1_2.py │ ├── 89d24811e894_2_1_4.py │ ├── a295e41830a6_2_0_6.py │ ├── a73f2dbf5c09_2_0_4.py │ ├── a946dae52526_2_2_1.py │ ├── bf28a012734c_2_0_8.py │ ├── ca5461f314f2_2_1_0.py │ ├── d58298a0879f_2_1_9.py │ ├── e2dbe1421fa4_2_0_3.py │ ├── eaf9cbc49027_2_0_7.py │ └── ecf3c693fdf3_2_0_5.py ├── docker/ │ ├── Dockerfile │ ├── cert.sh │ ├── docker_http_proxy.conf │ ├── entrypoint.sh │ ├── nginx.common.conf │ ├── nginx.template.conf │ └── update.sh ├── docs/ │ ├── development-setup.md │ ├── mcp-api.md │ └── postgresql-setup.md ├── frozen.spec ├── requirements.in ├── requirements.txt ├── safety.policy.yml ├── setup.py ├── skills/ │ └── moviepilot-cli/ │ ├── SKILL.md │ └── scripts/ │ └── mp-cli.js ├── tests/ │ ├── __init__.py │ ├── cases/ │ │ ├── __init__.py │ │ ├── files.py │ │ ├── groups.py │ │ └── meta.py │ ├── manual/ │ │ └── ugreen_media_cli.py │ ├── run.py │ ├── test_bluray.py │ ├── test_mediascrape.py │ ├── test_metainfo.py │ ├── test_object.py │ ├── test_release_group.py │ ├── test_string.py │ ├── test_telegram.py │ ├── test_transfer_history_retransfer.py │ ├── test_ugreen_api.py │ ├── test_ugreen_crypto.py │ └── test_ugreen_mediaserver.py └── version.py
Showing preview only (488K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (5429 symbols across 416 files)
FILE: app/agent/__init__.py
class AgentChain (line 28) | class AgentChain(ChainBase):
class MoviePilotAgent (line 32) | class MoviePilotAgent:
method __init__ (line 37) | def __init__(self, session_id: str, user_id: str = None,
method _initialize_llm (line 65) | def _initialize_llm(self):
method _initialize_tools (line 71) | def _initialize_tools(self) -> List:
method _initialize_session_store (line 85) | def _initialize_session_store() -> Dict[str, InMemoryChatMessageHistory]:
method get_session_history (line 91) | def get_session_history(self, session_id: str) -> InMemoryChatMessageH...
method _initialize_prompt (line 132) | def _initialize_prompt() -> ChatPromptTemplate:
method _token_counter (line 150) | def _token_counter(messages: List[Union[HumanMessage, AIMessage, ToolM...
method _create_agent_executor (line 196) | def _create_agent_executor(self) -> RunnableWithMessageHistory:
method _summarize_history (line 303) | async def _summarize_history(self):
method process_message (line 368) | async def process_message(self, message: str) -> str:
method _execute_agent (line 425) | async def _execute_agent(self, input_context: Dict[str, Any]) -> Dict[...
method send_agent_message (line 460) | async def send_agent_message(self, message: str, title: str = "MoviePi...
method cleanup (line 475) | async def cleanup(self):
class AgentManager (line 482) | class AgentManager:
method __init__ (line 487) | def __init__(self):
method initialize (line 491) | async def initialize():
method close (line 497) | async def close(self):
method process_message (line 507) | async def process_message(self, session_id: str, user_id: str, message...
method clear_session (line 537) | async def clear_session(self, session_id: str, user_id: str):
FILE: app/agent/callback/__init__.py
class StreamingCallbackHandler (line 8) | class StreamingCallbackHandler(AsyncCallbackHandler):
method __init__ (line 13) | def __init__(self, session_id: str):
method get_message (line 18) | async def get_message(self):
method on_llm_new_token (line 30) | async def on_llm_new_token(self, token: str, **kwargs):
FILE: app/agent/memory/__init__.py
class ConversationMemoryManager (line 14) | class ConversationMemoryManager:
method __init__ (line 19) | def __init__(self):
method initialize (line 27) | async def initialize(self):
method close (line 39) | async def close(self):
method _get_memory_key (line 55) | def _get_memory_key(session_id: str, user_id: str):
method _get_redis_key (line 62) | def _get_redis_key(session_id: str, user_id: str):
method _get_memory (line 68) | def _get_memory(self, session_id: str, user_id: str):
method _get_redis (line 75) | async def _get_redis(self, session_id: str, user_id: str) -> Optional[...
method get_conversation (line 91) | async def get_conversation(self, session_id: str, user_id: str) -> Con...
method set_title (line 113) | async def set_title(self, session_id: str, user_id: str, title: str):
method get_title (line 122) | async def get_title(self, session_id: str, user_id: str) -> Optional[s...
method list_sessions (line 129) | async def list_sessions(self, user_id: str, limit: int = 100) -> List[...
method add_conversation (line 184) | async def add_conversation(
method get_recent_messages_for_agent (line 219) | def get_recent_messages_for_agent(
method get_recent_messages (line 237) | async def get_recent_messages(
method get_context (line 255) | async def get_context(self, session_id: str, user_id: str) -> Dict[str...
method clear_memory (line 262) | async def clear_memory(self, session_id: str, user_id: str):
method _save_memory (line 276) | def _save_memory(self, memory: ConversationMemory):
method _save_redis (line 283) | async def _save_redis(self, memory: ConversationMemory):
method _save_conversation (line 301) | async def _save_conversation(self, memory: ConversationMemory):
method _cleanup_expired_memories (line 314) | async def _cleanup_expired_memories(self):
FILE: app/agent/prompt/__init__.py
class PromptManager (line 9) | class PromptManager:
method __init__ (line 14) | def __init__(self, prompts_dir: str = None):
method load_prompt (line 21) | def load_prompt(self, prompt_name: str) -> str:
method get_agent_prompt (line 43) | def get_agent_prompt(self, channel: str = None) -> str:
method _generate_formatting_instructions (line 66) | def _generate_formatting_instructions(caps: ChannelCapabilities) -> str:
method clear_cache (line 80) | def clear_cache(self):
FILE: app/agent/tools/base.py
class ToolChain (line 15) | class ToolChain(ChainBase):
class MoviePilotTool (line 19) | class MoviePilotTool(BaseTool, metaclass=ABCMeta):
method __init__ (line 31) | def __init__(self, session_id: str, user_id: str, **kwargs):
method _run (line 36) | def _run(self, *args: Any, **kwargs: Any) -> Any:
method _arun (line 39) | async def _arun(self, **kwargs) -> str:
method get_tool_message (line 114) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 130) | async def run(self, **kwargs) -> str:
method set_message_attr (line 133) | def set_message_attr(self, channel: str, source: str, username: str):
method set_callback_handler (line 141) | def set_callback_handler(self, callback_handler: StreamingCallbackHand...
method send_tool_message (line 147) | async def send_tool_message(self, message: str, title: str = ""):
FILE: app/agent/tools/factory.py
class MoviePilotToolFactory (line 49) | class MoviePilotToolFactory:
method create_tools (line 55) | def create_tools(session_id: str, user_id: str,
FILE: app/agent/tools/impl/_torrent_search_utils.py
function build_torrent_ref (line 14) | def build_torrent_ref(context: Optional[Context]) -> str:
function sort_season_options (line 21) | def sort_season_options(options: List[str]) -> List[str]:
function append_option (line 66) | def append_option(options: List[str], value: Optional[str]) -> None:
function build_filter_options (line 72) | def build_filter_options(items: List[Context]) -> dict:
function match_filter (line 99) | def match_filter(filter_values: Optional[List[str]], value: Optional[str...
function filter_contexts (line 104) | def filter_contexts(items: List[Context],
function simplify_search_result (line 130) | def simplify_search_result(context: Context, index: int) -> dict:
FILE: app/agent/tools/impl/add_download.py
class AddDownloadInput (line 22) | class AddDownloadInput(BaseModel):
class AddDownloadTool (line 37) | class AddDownloadTool(MoviePilotTool):
method get_tool_message (line 42) | def get_tool_message(self, **kwargs) -> Optional[str]:
method _build_torrent_ref (line 63) | def _build_torrent_ref(context: Context) -> str:
method _is_torrent_ref (line 70) | def _is_torrent_ref(torrent_ref: Optional[str]) -> bool:
method _is_direct_download_url (line 77) | def _is_direct_download_url(torrent_url: Optional[str]) -> bool:
method _resolve_cached_context (line 85) | def _resolve_cached_context(cls, torrent_ref: str) -> Optional[Context]:
method _merge_labels_with_system_tag (line 108) | def _merge_labels_with_system_tag(labels: Optional[str]) -> Optional[s...
method _format_failed_result (line 119) | def _format_failed_result(failed_messages: List[str]) -> str:
method _build_failure_message (line 124) | def _build_failure_message(torrent_ref: str, error_msg: Optional[str] ...
method _normalize_torrent_urls (line 139) | def _normalize_torrent_urls(cls, torrent_url: Optional[List[str] | str...
method _resolve_direct_download_dir (line 152) | def _resolve_direct_download_dir(save_path: Optional[str]) -> Optional...
method run (line 167) | async def run(self, torrent_url: Optional[List[str]] = None,
FILE: app/agent/tools/impl/add_subscribe.py
class AddSubscribeInput (line 13) | class AddSubscribeInput(BaseModel):
class AddSubscribeTool (line 42) | class AddSubscribeTool(MoviePilotTool):
method get_tool_message (line 47) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 64) | async def run(self, title: str, year: str, media_type: str,
FILE: app/agent/tools/impl/delete_download.py
class DeleteDownloadInput (line 12) | class DeleteDownloadInput(BaseModel):
class DeleteDownloadTool (line 20) | class DeleteDownloadTool(MoviePilotTool):
method get_tool_message (line 25) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 39) | async def run(self, hash: str, downloader: Optional[str] = None,
FILE: app/agent/tools/impl/delete_subscribe.py
class DeleteSubscribeInput (line 15) | class DeleteSubscribeInput(BaseModel):
class DeleteSubscribeTool (line 21) | class DeleteSubscribeTool(MoviePilotTool):
method get_tool_message (line 26) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 31) | async def run(self, subscribe_id: int, **kwargs) -> str:
FILE: app/agent/tools/impl/execute_command.py
class ExecuteCommandInput (line 12) | class ExecuteCommandInput(BaseModel):
class ExecuteCommandTool (line 19) | class ExecuteCommandTool(MoviePilotTool):
method get_tool_message (line 24) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 29) | async def run(self, command: str, timeout: Optional[int] = 60, **kwarg...
FILE: app/agent/tools/impl/get_recommendations.py
class GetRecommendationsInput (line 14) | class GetRecommendationsInput(BaseModel):
class GetRecommendationsTool (line 39) | class GetRecommendationsTool(MoviePilotTool):
method get_tool_message (line 44) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 75) | async def run(self, source: Optional[str] = "tmdb_trending",
FILE: app/agent/tools/impl/get_search_results.py
class GetSearchResultsInput (line 20) | class GetSearchResultsInput(BaseModel):
class GetSearchResultsTool (line 33) | class GetSearchResultsTool(MoviePilotTool):
method get_tool_message (line 38) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 41) | async def run(self, site: Optional[List[str]] = None, season: Optional...
FILE: app/agent/tools/impl/list_directory.py
class ListDirectoryInput (line 17) | class ListDirectoryInput(BaseModel):
class ListDirectoryTool (line 25) | class ListDirectoryTool(MoviePilotTool):
method get_tool_message (line 30) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 41) | async def run(self, path: str, storage: Optional[str] = "local",
FILE: app/agent/tools/impl/query_directory_settings.py
class QueryDirectorySettingsInput (line 13) | class QueryDirectorySettingsInput(BaseModel):
class QueryDirectorySettingsTool (line 24) | class QueryDirectorySettingsTool(MoviePilotTool):
method get_tool_message (line 29) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 50) | async def run(self, directory_type: Optional[str] = "all",
FILE: app/agent/tools/impl/query_download_tasks.py
class QueryDownloadTasksInput (line 16) | class QueryDownloadTasksInput(BaseModel):
class QueryDownloadTasksTool (line 27) | class QueryDownloadTasksTool(MoviePilotTool):
method _get_all_torrents (line 33) | def _get_all_torrents(download_chain: DownloadChain, downloader: Optio...
method _format_progress (line 55) | def _format_progress(progress: Optional[float]) -> Optional[str]:
method get_tool_message (line 66) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 89) | async def run(self, downloader: Optional[str] = None,
FILE: app/agent/tools/impl/query_downloaders.py
class QueryDownloadersInput (line 14) | class QueryDownloadersInput(BaseModel):
class QueryDownloadersTool (line 19) | class QueryDownloadersTool(MoviePilotTool):
method get_tool_message (line 24) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 28) | async def run(self, **kwargs) -> str:
FILE: app/agent/tools/impl/query_episode_schedule.py
class QueryEpisodeScheduleInput (line 13) | class QueryEpisodeScheduleInput(BaseModel):
class QueryEpisodeScheduleTool (line 21) | class QueryEpisodeScheduleTool(MoviePilotTool):
method get_tool_message (line 26) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 38) | async def run(self, tmdb_id: int, season: int, episode_group: Optional...
FILE: app/agent/tools/impl/query_library_exists.py
class QueryLibraryExistsInput (line 14) | class QueryLibraryExistsInput(BaseModel):
class QueryLibraryExistsTool (line 22) | class QueryLibraryExistsTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 43) | async def run(self, tmdb_id: Optional[int] = None, douban_id: Optional...
FILE: app/agent/tools/impl/query_library_latest.py
class QueryLibraryLatestInput (line 14) | class QueryLibraryLatestInput(BaseModel):
class QueryLibraryLatestTool (line 21) | class QueryLibraryLatestTool(MoviePilotTool):
method get_tool_message (line 26) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 42) | async def run(self, server: Optional[str] = None, count: Optional[int]...
FILE: app/agent/tools/impl/query_media_detail.py
class QueryMediaDetailInput (line 14) | class QueryMediaDetailInput(BaseModel):
class QueryMediaDetailTool (line 22) | class QueryMediaDetailTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 35) | async def run(self, media_type: str, tmdb_id: Optional[int] = None, do...
FILE: app/agent/tools/impl/query_popular_subscribes.py
class QueryPopularSubscribesInput (line 16) | class QueryPopularSubscribesInput(BaseModel):
class QueryPopularSubscribesTool (line 29) | class QueryPopularSubscribesTool(MoviePilotTool):
method get_tool_message (line 34) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 55) | async def run(self, media_type: str,
FILE: app/agent/tools/impl/query_rule_groups.py
class QueryRuleGroupsInput (line 13) | class QueryRuleGroupsInput(BaseModel):
class QueryRuleGroupsTool (line 18) | class QueryRuleGroupsTool(MoviePilotTool):
method get_tool_message (line 23) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 27) | async def run(self, **kwargs) -> str:
FILE: app/agent/tools/impl/query_schedulers.py
class QuerySchedulersInput (line 13) | class QuerySchedulersInput(BaseModel):
class QuerySchedulersTool (line 18) | class QuerySchedulersTool(MoviePilotTool):
method get_tool_message (line 23) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 27) | async def run(self, **kwargs) -> str:
FILE: app/agent/tools/impl/query_site_userdata.py
class QuerySiteUserdataInput (line 15) | class QuerySiteUserdataInput(BaseModel):
class QuerySiteUserdataTool (line 22) | class QuerySiteUserdataTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 40) | async def run(self, site_id: int, workdate: Optional[str] = None, **kw...
FILE: app/agent/tools/impl/query_sites.py
class QuerySitesInput (line 13) | class QuerySitesInput(BaseModel):
class QuerySitesTool (line 22) | class QuerySitesTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 43) | async def run(self, status: Optional[str] = "all", name: Optional[str]...
FILE: app/agent/tools/impl/query_subscribe_history.py
class QuerySubscribeHistoryInput (line 15) | class QuerySubscribeHistoryInput(BaseModel):
class QuerySubscribeHistoryTool (line 22) | class QuerySubscribeHistoryTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 41) | async def run(self, media_type: Optional[str] = "all",
FILE: app/agent/tools/impl/query_subscribe_shares.py
class QuerySubscribeSharesInput (line 13) | class QuerySubscribeSharesInput(BaseModel):
class QuerySubscribeSharesTool (line 25) | class QuerySubscribeSharesTool(MoviePilotTool):
method get_tool_message (line 30) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 50) | async def run(self, name: Optional[str] = None,
FILE: app/agent/tools/impl/query_subscribes.py
class QuerySubscribesInput (line 14) | class QuerySubscribesInput(BaseModel):
class QuerySubscribesTool (line 25) | class QuerySubscribesTool(MoviePilotTool):
method get_tool_message (line 30) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 48) | async def run(self, status: Optional[str] = "all", media_type: Optiona...
FILE: app/agent/tools/impl/query_transfer_history.py
class QueryTransferHistoryInput (line 16) | class QueryTransferHistoryInput(BaseModel):
class QueryTransferHistoryTool (line 25) | class QueryTransferHistoryTool(MoviePilotTool):
method get_tool_message (line 30) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 48) | async def run(self, title: Optional[str] = None,
FILE: app/agent/tools/impl/query_workflows.py
class QueryWorkflowsInput (line 14) | class QueryWorkflowsInput(BaseModel):
class QueryWorkflowsTool (line 22) | class QueryWorkflowsTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 48) | async def run(self, state: Optional[str] = "all",
FILE: app/agent/tools/impl/recognize_media.py
class RecognizeMediaInput (line 16) | class RecognizeMediaInput(BaseModel):
class RecognizeMediaTool (line 24) | class RecognizeMediaTool(MoviePilotTool):
method get_tool_message (line 29) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 46) | async def run(self, title: Optional[str] = None, subtitle: Optional[st...
method _format_context_result (line 102) | def _format_context_result(self, context: Context, source_type: str) -...
FILE: app/agent/tools/impl/run_scheduler.py
class RunSchedulerInput (line 12) | class RunSchedulerInput(BaseModel):
class RunSchedulerTool (line 18) | class RunSchedulerTool(MoviePilotTool):
method get_tool_message (line 23) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 28) | async def run(self, job_id: str, **kwargs) -> str:
FILE: app/agent/tools/impl/run_workflow.py
class RunWorkflowInput (line 14) | class RunWorkflowInput(BaseModel):
class RunWorkflowTool (line 21) | class RunWorkflowTool(MoviePilotTool):
method get_tool_message (line 26) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 39) | async def run(self, workflow_id: int,
FILE: app/agent/tools/impl/scrape_metadata.py
class ScrapeMetadataInput (line 17) | class ScrapeMetadataInput(BaseModel):
class ScrapeMetadataTool (line 28) | class ScrapeMetadataTool(MoviePilotTool):
method get_tool_message (line 33) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 47) | async def run(self, path: str, storage: Optional[str] = "local",
FILE: app/agent/tools/impl/search_media.py
class SearchMediaInput (line 14) | class SearchMediaInput(BaseModel):
class SearchMediaTool (line 25) | class SearchMediaTool(MoviePilotTool):
method get_tool_message (line 30) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 47) | async def run(self, title: str, year: Optional[str] = None,
FILE: app/agent/tools/impl/search_person.py
class SearchPersonInput (line 13) | class SearchPersonInput(BaseModel):
class SearchPersonTool (line 19) | class SearchPersonTool(MoviePilotTool):
method get_tool_message (line 24) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 29) | async def run(self, name: str, **kwargs) -> str:
FILE: app/agent/tools/impl/search_person_credits.py
class SearchPersonCreditsInput (line 15) | class SearchPersonCreditsInput(BaseModel):
class SearchPersonCreditsTool (line 23) | class SearchPersonCreditsTool(MoviePilotTool):
method get_tool_message (line 28) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 34) | async def run(self, person_id: int, source: str, page: Optional[int] =...
FILE: app/agent/tools/impl/search_subscribe.py
class SearchSubscribeInput (line 16) | class SearchSubscribeInput(BaseModel):
class SearchSubscribeTool (line 25) | class SearchSubscribeTool(MoviePilotTool):
method get_tool_message (line 30) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 41) | async def run(self, subscribe_id: int, manual: Optional[bool] = False,
FILE: app/agent/tools/impl/search_torrents.py
class SearchTorrentsInput (line 20) | class SearchTorrentsInput(BaseModel):
class SearchTorrentsTool (line 30) | class SearchTorrentsTool(MoviePilotTool):
method get_tool_message (line 37) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 53) | async def run(self, tmdb_id: Optional[int] = None, douban_id: Optional...
FILE: app/agent/tools/impl/search_web.py
class SearchWebInput (line 18) | class SearchWebInput(BaseModel):
class SearchWebTool (line 26) | class SearchWebTool(MoviePilotTool):
method get_tool_message (line 31) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 37) | async def run(self, query: str, max_results: Optional[int] = 5, **kwar...
method _search_tavily (line 71) | async def _search_tavily(query: str, max_results: int) -> List[Dict]:
method _get_proxy_url (line 104) | def _get_proxy_url(proxy_setting) -> Optional[str]:
method _search_duckduckgo (line 112) | async def _search_duckduckgo(self, query: str, max_results: int) -> Li...
method _format_and_truncate_results (line 150) | def _format_and_truncate_results(results: List[Dict], max_results: int...
FILE: app/agent/tools/impl/send_message.py
class SendMessageInput (line 11) | class SendMessageInput(BaseModel):
class SendMessageTool (line 19) | class SendMessageTool(MoviePilotTool):
method get_tool_message (line 24) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 38) | async def run(self, message: str, message_type: Optional[str] = None, ...
FILE: app/agent/tools/impl/test_site.py
class TestSiteInput (line 13) | class TestSiteInput(BaseModel):
class TestSiteTool (line 19) | class TestSiteTool(MoviePilotTool):
method get_tool_message (line 24) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 29) | async def run(self, site_identifier: int, **kwargs) -> str:
FILE: app/agent/tools/impl/transfer_file.py
class TransferFileInput (line 14) | class TransferFileInput(BaseModel):
class TransferFileTool (line 29) | class TransferFileTool(MoviePilotTool):
method get_tool_message (line 34) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 52) | async def run(self, file_path: str, storage: Optional[str] = "local",
FILE: app/agent/tools/impl/update_site.py
class UpdateSiteInput (line 17) | class UpdateSiteInput(BaseModel):
class UpdateSiteTool (line 40) | class UpdateSiteTool(MoviePilotTool):
method get_tool_message (line 45) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 71) | async def run(self, site_id: int,
FILE: app/agent/tools/impl/update_site_cookie.py
class UpdateSiteCookieInput (line 13) | class UpdateSiteCookieInput(BaseModel):
class UpdateSiteCookieTool (line 22) | class UpdateSiteCookieTool(MoviePilotTool):
method get_tool_message (line 27) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 39) | async def run(self, site_identifier: int, username: str, password: str,
FILE: app/agent/tools/impl/update_subscribe.py
class UpdateSubscribeInput (line 16) | class UpdateSubscribeInput(BaseModel):
class UpdateSubscribeTool (line 42) | class UpdateSubscribeTool(MoviePilotTool):
method get_tool_message (line 47) | def get_tool_message(self, **kwargs) -> Optional[str]:
method run (line 74) | async def run(self, subscribe_id: int,
FILE: app/agent/tools/manager.py
class ToolDefinition (line 9) | class ToolDefinition:
method __init__ (line 14) | def __init__(self, name: str, description: str, input_schema: Dict[str...
class MoviePilotToolsManager (line 20) | class MoviePilotToolsManager:
method __init__ (line 25) | def __init__(self, user_id: str = "api_user", session_id: str = uuid.u...
method _load_tools (line 38) | def _load_tools(self):
method list_tools (line 57) | def list_tools(self) -> List[ToolDefinition]:
method get_tool (line 87) | def get_tool(self, tool_name: str) -> Optional[Any]:
method _resolve_field_schema (line 103) | def _resolve_field_schema(field_info: Dict[str, Any]) -> Dict[str, Any]:
method _normalize_scalar_value (line 126) | def _normalize_scalar_value(field_type: Optional[str], value: Any, key...
method _parse_array_string (line 153) | def _parse_array_string(value: str, key: str, item_type: str = "string...
method _normalize_arguments (line 166) | def _normalize_arguments(tool_instance: Any, arguments: Dict[str, Any]...
method call_tool (line 212) | async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -...
method _convert_to_json_schema (line 259) | def _convert_to_json_schema(args_schema: Any) -> Dict[str, Any]:
FILE: app/api/endpoints/bangumi.py
function bangumi_credits (line 14) | async def bangumi_credits(bangumiid: int,
function bangumi_recommend (line 28) | async def bangumi_recommend(bangumiid: int,
function bangumi_person (line 42) | async def bangumi_person(person_id: int,
function bangumi_person_credits (line 51) | async def bangumi_person_credits(person_id: int,
function bangumi_info (line 65) | async def bangumi_info(bangumiid: int,
FILE: app/api/endpoints/dashboard.py
function statistic (line 21) | def statistic(name: Optional[str] = None, _: schemas.TokenPayload = Depe...
function statistic2 (line 46) | def statistic2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
function storage (line 54) | def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function storage2 (line 75) | def storage2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
function processes (line 83) | def processes(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function downloader (line 91) | def downloader(name: Optional[str] = None, _: schemas.TokenPayload = Dep...
function downloader2 (line 112) | def downloader2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
function schedule (line 120) | async def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function schedule2 (line 128) | async def schedule2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
function transfer (line 136) | async def transfer(days: Optional[int] = 7,
function cpu (line 147) | def cpu(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function cpu2 (line 155) | def cpu2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
function memory (line 163) | def memory(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function memory2 (line 171) | def memory2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
function network (line 179) | def network(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function network2 (line 187) | def network2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
FILE: app/api/endpoints/discover.py
function source (line 18) | def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function bangumi (line 34) | async def bangumi(type: Optional[int] = 2,
function douban_movies (line 52) | async def douban_movies(sort: Optional[str] = "R",
function douban_tvs (line 66) | async def douban_tvs(sort: Optional[str] = "R",
function tmdb_movies (line 80) | async def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
function tmdb_tvs (line 107) | async def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
FILE: app/api/endpoints/douban.py
function douban_person (line 15) | async def douban_person(person_id: int,
function douban_person_credits (line 24) | async def douban_person_credits(person_id: int,
function douban_credits (line 37) | async def douban_credits(doubanid: str,
function douban_recommend (line 52) | async def douban_recommend(doubanid: str,
function douban_info (line 71) | async def douban_info(doubanid: str,
FILE: app/api/endpoints/download.py
function current (line 22) | def current(
function download (line 32) | def download(
function add (line 67) | def add(
function start (line 109) | def start(
function stop (line 120) | def stop(hashString: str, name: Optional[str] = None,
function clients (line 130) | async def clients(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function delete (line 141) | def delete(hashString: str, name: Optional[str] = None,
FILE: app/api/endpoints/history.py
function download_history (line 24) | async def download_history(page: Optional[int] = 1,
function delete_download_history (line 35) | async def delete_download_history(history_in: schemas.DownloadHistory,
function transfer_history (line 46) | async def transfer_history(title: Optional[str] = None,
function delete_transfer_history (line 80) | def delete_transfer_history(history_in: schemas.TransferHistory,
function empty_transfer_history (line 118) | async def empty_transfer_history(db: AsyncSession = Depends(get_async_db),
FILE: app/api/endpoints/login.py
function login_access_token (line 20) | def login_access_token(
function wallpaper (line 65) | def wallpaper() -> Any:
function wallpapers (line 79) | def wallpapers() -> Any:
FILE: app/api/endpoints/mcp.py
function list_exposed_tools (line 25) | def list_exposed_tools():
function create_jsonrpc_response (line 35) | def create_jsonrpc_response(request_id: Union[str, int, None], result: A...
function create_jsonrpc_error (line 47) | def create_jsonrpc_error(request_id: Union[str, int, None], code: int, m...
function mcp_jsonrpc (line 66) | async def mcp_jsonrpc(
function handle_initialize (line 148) | async def handle_initialize(params: Dict[str, Any]) -> Dict[str, Any]:
function handle_tools_list (line 184) | async def handle_tools_list() -> Dict[str, Any]:
function handle_tools_call (line 205) | async def handle_tools_call(params: Dict[str, Any]) -> Dict[str, Any]:
function delete_mcp_session (line 243) | async def delete_mcp_session(
function list_tools (line 255) | async def list_tools(
function call_tool (line 284) | async def call_tool(
function get_tool_info (line 313) | async def get_tool_info(
function get_tool_schema (line 345) | async def get_tool_schema(
FILE: app/api/endpoints/media.py
function recognize (line 24) | async def recognize(title: str,
function recognize2 (line 39) | async def recognize2(_: Annotated[str, Depends(verify_apitoken)],
function recognize_file (line 51) | async def recognize_file(path: str,
function recognize_file2 (line 64) | async def recognize_file2(path: str,
function search (line 74) | async def search(title: str,
function scrape (line 114) | def scrape(fileitem: schemas.FileItem,
function get_category_config (line 138) | def get_category_config(_: User = Depends(get_current_active_user)):
function save_category_config (line 147) | def save_category_config(config: CategoryConfig, _: User = Depends(get_c...
function category (line 158) | async def category(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function group_seasons (line 166) | async def group_seasons(episode_group: str, _: schemas.TokenPayload = De...
function groups (line 174) | async def groups(tmdbid: int, _: schemas.TokenPayload = Depends(verify_t...
function seasons (line 185) | async def seasons(mediaid: Optional[str] = None,
function detail (line 228) | async def detail(mediaid: str, type_name: str, title: Optional[str] = No...
FILE: app/api/endpoints/mediaserver.py
function play_item (line 24) | def play_item(itemid: str, _: schemas.TokenPayload = Depends(verify_toke...
function exists_local (line 46) | async def exists_local(title: Optional[str] = None,
function exists (line 75) | def exists(media_in: schemas.MediaInfo,
function not_exists (line 94) | def not_exists(media_in: schemas.MediaInfo,
function latest (line 124) | def latest(server: str, count: Optional[int] = 20,
function playing (line 133) | def playing(server: str, count: Optional[int] = 12,
function library (line 142) | def library(server: str, hidden: Optional[bool] = False,
function clients (line 151) | async def clients(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
FILE: app/api/endpoints/message.py
function start_message_chain (line 25) | def start_message_chain(body: Any, form: Any, args: Any):
function user_message (line 33) | async def user_message(background_tasks: BackgroundTasks, request: Request,
function web_message (line 46) | def web_message(text: str, current_user: User = Depends(get_current_acti...
function get_web_message (line 61) | async def get_web_message(_: schemas.TokenPayload = Depends(verify_token),
function wechat_verify (line 79) | def wechat_verify(echostr: str, msg_signature: str, timestamp: Union[str...
function vocechat_verify (line 112) | def vocechat_verify() -> Any:
function incoming_verify (line 120) | def incoming_verify(token: Optional[str] = None, echostr: Optional[str] ...
function subscribe (line 134) | async def subscribe(subscription: schemas.Subscription, _: schemas.Token...
function send_notification (line 146) | def send_notification(payload: schemas.SubscriptionMessage, _: schemas.T...
FILE: app/api/endpoints/mfa.py
function _build_credential_list (line 29) | def _build_credential_list(passkeys: list[PassKey]) -> list[dict[str, An...
function _extract_and_standardize_credential_id (line 45) | def _extract_and_standardize_credential_id(credential: dict) -> str:
function _verify_passkey_and_update (line 59) | def _verify_passkey_and_update(
function _check_user_has_passkey (line 85) | async def _check_user_has_passkey(db: AsyncSession, user_id: int) -> bool:
class OtpVerifyRequest (line 98) | class OtpVerifyRequest(schemas.BaseModel):
class OtpDisableRequest (line 103) | class OtpDisableRequest(schemas.BaseModel):
class PassKeyDeleteRequest (line 107) | class PassKeyDeleteRequest(schemas.BaseModel):
function mfa_status (line 115) | async def mfa_status(username: str, db: AsyncSession = Depends(get_async...
function otp_generate (line 136) | def otp_generate(
function otp_verify (line 145) | async def otp_verify(
function otp_disable (line 158) | async def otp_disable(
class PassKeyRegistrationStart (line 181) | class PassKeyRegistrationStart(schemas.BaseModel):
class PassKeyRegistrationFinish (line 186) | class PassKeyRegistrationFinish(schemas.BaseModel):
class PassKeyAuthenticationStart (line 193) | class PassKeyAuthenticationStart(schemas.BaseModel):
class PassKeyAuthenticationFinish (line 198) | class PassKeyAuthenticationFinish(schemas.BaseModel):
function passkey_register_start (line 205) | def passkey_register_start(
function passkey_register_finish (line 245) | def passkey_register_finish(
function passkey_authenticate_start (line 289) | def passkey_authenticate_start(
function passkey_authenticate_finish (line 330) | def passkey_authenticate_finish(
function passkey_list (line 389) | def passkey_list(
function passkey_delete (line 421) | async def passkey_delete(
function passkey_verify_mfa (line 453) | def passkey_verify_mfa(
FILE: app/api/endpoints/plugin.py
function register_plugin_api (line 32) | def register_plugin_api(plugin_id: Optional[str] = None):
function remove_plugin_api (line 40) | def remove_plugin_api(plugin_id: str):
function _update_plugin_api_routes (line 48) | def _update_plugin_api_routes(plugin_id: Optional[str], action: str):
function _remove_routes (line 95) | def _remove_routes(plugin_id: str) -> bool:
function _clean_protected_routes (line 116) | def _clean_protected_routes(existing_paths: dict):
function register_plugin (line 130) | def register_plugin(plugin_id: str):
function all_plugins (line 143) | async def all_plugins(_: User = Depends(get_current_active_superuser_asy...
function installed (line 192) | async def installed(_: User = Depends(get_current_active_superuser_async...
function statistic (line 200) | async def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> ...
function reload_plugin (line 208) | def reload_plugin(plugin_id: str, _: User = Depends(get_current_active_s...
function install (line 220) | async def install(plugin_id: str,
function remotes (line 254) | async def remotes(token: str) -> Any:
function plugin_form (line 264) | def plugin_form(plugin_id: str,
function plugin_page (line 289) | def plugin_page(plugin_id: str, _: User = Depends(get_current_active_sup...
function plugin_dashboard_meta (line 311) | def plugin_dashboard_meta(_: schemas.TokenPayload = Depends(verify_token...
function plugin_dashboard_by_key (line 319) | def plugin_dashboard_by_key(plugin_id: str, key: str, user_agent: Annota...
function plugin_dashboard (line 328) | def plugin_dashboard(plugin_id: str, user_agent: Annotated[str | None, H...
function reset_plugin (line 337) | def reset_plugin(plugin_id: str,
function plugin_static_file (line 353) | async def plugin_static_file(plugin_id: str, filepath: str):
function get_plugin_folders (line 410) | async def get_plugin_folders(_: User = Depends(get_current_active_superu...
function save_plugin_folders (line 423) | async def save_plugin_folders(folders: dict, _: User = Depends(get_curre...
function create_plugin_folder (line 436) | async def create_plugin_folder(folder_name: str,
function delete_plugin_folder (line 451) | async def delete_plugin_folder(folder_name: str,
function update_folder_plugins (line 466) | async def update_folder_plugins(folder_name: str, plugin_ids: List[str],
function clone_plugin (line 478) | def clone_plugin(plugin_id: str,
function plugin_config (line 508) | async def plugin_config(plugin_id: str,
function set_plugin_config (line 517) | def set_plugin_config(plugin_id: str, conf: dict,
function uninstall_plugin (line 533) | def uninstall_plugin(plugin_id: str,
function _add_clone_to_plugin_folder (line 572) | def _add_clone_to_plugin_folder(original_plugin_id: str, clone_plugin_id...
function _remove_plugin_from_folders (line 621) | def _remove_plugin_from_folders(plugin_id: str):
FILE: app/api/endpoints/recommend.py
function source (line 16) | def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
function bangumi_calendar (line 32) | async def bangumi_calendar(page: Optional[int] = 1,
function douban_showing (line 42) | async def douban_showing(page: Optional[int] = 1,
function douban_movies (line 52) | async def douban_movies(sort: Optional[str] = "R",
function douban_tvs (line 64) | async def douban_tvs(sort: Optional[str] = "R",
function douban_movie_top250 (line 76) | async def douban_movie_top250(page: Optional[int] = 1,
function douban_tv_weekly_chinese (line 86) | async def douban_tv_weekly_chinese(page: Optional[int] = 1,
function douban_tv_weekly_global (line 96) | async def douban_tv_weekly_global(page: Optional[int] = 1,
function douban_tv_animation (line 106) | async def douban_tv_animation(page: Optional[int] = 1,
function douban_movie_hot (line 116) | async def douban_movie_hot(page: Optional[int] = 1,
function douban_tv_hot (line 126) | async def douban_tv_hot(page: Optional[int] = 1,
function tmdb_movies (line 136) | async def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
function tmdb_tvs (line 161) | async def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
function tmdb_trending (line 186) | async def tmdb_trending(page: Optional[int] = 1,
FILE: app/api/endpoints/search.py
function search_latest (line 21) | async def search_latest(_: schemas.TokenPayload = Depends(verify_token))...
function search_by_id (line 30) | async def search_by_id(mediaid: str,
function search_by_title (line 160) | async def search_by_title(keyword: Optional[str] = None,
function recommend_search_results (line 181) | async def recommend_search_results(
FILE: app/api/endpoints/site.py
function read_sites (line 34) | async def read_sites(db: AsyncSession = Depends(get_async_db),
function add_site (line 43) | async def add_site(
function update_site (line 80) | async def update_site(
function cookie_cloud_sync (line 108) | async def cookie_cloud_sync(background_tasks: BackgroundTasks,
function reset (line 118) | def reset(db: AsyncSession = Depends(get_db),
function update_sites_priority (line 137) | async def update_sites_priority(
function update_cookie (line 152) | def update_cookie(
function refresh_userdata (line 178) | def refresh_userdata(
function read_userdata_latest (line 199) | async def read_userdata_latest(
function read_userdata (line 212) | async def read_userdata(
function test_site (line 233) | def test_site(site_id: int,
function site_icon (line 250) | async def site_icon(site_id: int,
function site_category (line 271) | async def site_category(site_id: int,
function site_resource (line 301) | async def site_resource(site_id: int,
function read_site_by_domain (line 323) | async def read_site_by_domain(
function read_statistic_by_domain (line 342) | async def read_statistic_by_domain(
function read_statistics (line 358) | async def read_statistics(
function read_rss_sites (line 369) | async def read_rss_sites(db: AsyncSession = Depends(get_async_db),
function read_auth_sites (line 388) | async def read_auth_sites(_: schemas.TokenPayload = Depends(verify_token...
function auth_site (line 396) | def auth_site(
function site_mapping (line 416) | async def site_mapping(_: User = Depends(get_current_active_superuser_as...
function support_sites (line 431) | async def support_sites(_: User = Depends(get_current_active_superuser_a...
function read_site (line 439) | async def read_site(
function delete_site (line 457) | async def delete_site(
FILE: app/api/endpoints/storage.py
function qrcode (line 24) | def qrcode(name: str, _: schemas.TokenPayload = Depends(verify_token)) -...
function auth_url (line 35) | def auth_url(name: str, _: schemas.TokenPayload = Depends(verify_token))...
function check (line 46) | def check(name: str, ck: Optional[str] = None, t: Optional[str] = None,
function save (line 61) | def save(name: str,
function reset (line 72) | def reset(name: str,
function list_files (line 82) | def list_files(fileitem: schemas.FileItem,
function mkdir (line 102) | def mkdir(fileitem: schemas.FileItem,
function delete (line 120) | def delete(fileitem: schemas.FileItem,
function download (line 134) | def download(fileitem: schemas.FileItem,
function image (line 149) | def image(fileitem: schemas.FileItem,
function rename (line 164) | def rename(fileitem: schemas.FileItem,
function usage (line 225) | def usage(name: str, _: User = Depends(get_current_active_superuser)) ->...
function transtype (line 236) | async def transtype(name: str, _: User = Depends(get_current_active_supe...
FILE: app/api/endpoints/subscribe.py
function start_subscribe_add (line 28) | def start_subscribe_add(title: str, year: str,
function read_subscribes (line 38) | async def read_subscribes(
function list_subscribes (line 48) | async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -...
function create_subscribe (line 56) | async def create_subscribe(
function update_subscribe (line 95) | async def update_subscribe(
function update_subscribe_status (line 136) | async def update_subscribe_status(
function subscribe_mediaid (line 166) | async def subscribe_mediaid(
function refresh_subscribes (line 210) | def refresh_subscribes(
function reset_subscribes (line 220) | async def reset_subscribes(
function check_subscribes (line 250) | def check_subscribes(
function search_subscribes (line 260) | async def search_subscribes(
function search_subscribe (line 279) | async def search_subscribe(
function delete_subscribe_by_mediaid (line 299) | async def delete_subscribe_by_mediaid(
function seerr_subscribe (line 340) | async def seerr_subscribe(request: Request, background_tasks: Background...
function subscribe_history (line 393) | async def subscribe_history(
function delete_subscribe (line 406) | async def delete_subscribe(
function popular_subscribes (line 419) | async def popular_subscribes(
function user_subscribes (line 476) | async def user_subscribes(
function subscribe_files (line 487) | def subscribe_files(
function subscribe_share (line 501) | async def subscribe_share(
function subscribe_share_delete (line 515) | async def subscribe_share_delete(
function subscribe_fork (line 526) | async def subscribe_fork(
function followed_subscribers (line 545) | async def followed_subscribers(_: schemas.TokenPayload = Depends(verify_...
function follow_subscriber (line 553) | async def follow_subscriber(
function unfollow_subscriber (line 567) | async def unfollow_subscriber(
function popular_subscribes (line 581) | async def popular_subscribes(
function subscribe_share_statistics (line 605) | async def subscribe_share_statistics(_: schemas.TokenPayload = Depends(v...
function read_subscribe (line 614) | async def read_subscribe(
function delete_subscribe (line 627) | async def delete_subscribe(
FILE: app/api/endpoints/system.py
function fetch_image (line 49) | async def fetch_image(
function proxy_img (line 91) | async def proxy_img(
function cache_img (line 116) | async def cache_img(
function get_global_setting (line 130) | def get_global_setting(token: str):
function get_user_global_setting (line 156) | async def get_user_global_setting(_: User = Depends(get_current_active_u...
function get_env_setting (line 186) | async def get_env_setting(_: User = Depends(get_current_active_user_asyn...
function set_env_setting (line 204) | async def set_env_setting(env: dict,
function get_progress (line 241) | async def get_progress(request: Request, process_type: str, _: schemas.T...
function get_setting (line 262) | async def get_setting(key: str,
function set_setting (line 277) | async def set_setting(
function get_llm_models (line 315) | async def get_llm_models(provider: str, api_key: str, base_url: Optional...
function get_message (line 327) | async def get_message(request: Request, role: Optional[str] = "system",
function get_logging (line 349) | async def get_logging(request: Request, length: Optional[int] = 50, logf...
function latest_version (line 451) | async def latest_version(_: schemas.TokenPayload = Depends(verify_token)):
function ruletest (line 465) | def ruletest(title: str,
function nettest (line 497) | async def nettest(
function modulelist (line 574) | def modulelist(_: schemas.TokenPayload = Depends(verify_token)):
function moduletest (line 588) | def moduletest(moduleid: str, _: schemas.TokenPayload = Depends(verify_t...
function restart_system (line 597) | def restart_system(_: User = Depends(get_current_active_superuser)):
function run_scheduler (line 611) | def run_scheduler(jobid: str,
function run_scheduler2 (line 626) | def run_scheduler2(jobid: str,
FILE: app/api/endpoints/tmdb.py
function tmdb_seasons (line 14) | async def tmdb_seasons(tmdbid: int, _: schemas.TokenPayload = Depends(ve...
function tmdb_similar (line 25) | async def tmdb_similar(tmdbid: int,
function tmdb_recommend (line 44) | async def tmdb_recommend(tmdbid: int,
function tmdb_collection (line 63) | async def tmdb_collection(collection_id: int,
function tmdb_credits (line 77) | async def tmdb_credits(tmdbid: int,
function tmdb_person (line 95) | async def tmdb_person(person_id: int,
function tmdb_person_credits (line 104) | async def tmdb_person_credits(person_id: int,
function tmdb_season_episodes (line 117) | async def tmdb_season_episodes(tmdbid: int, season: int, episode_group: ...
FILE: app/api/endpoints/torrent.py
function torrents_cache (line 19) | async def torrents_cache(_: User = Depends(get_current_active_superuser_...
function delete_cache (line 66) | async def delete_cache(domain: str, torrent_hash: str, _: User = Depends...
function clear_cache (line 102) | async def clear_cache(_: User = Depends(get_current_active_superuser_asy...
function refresh_cache (line 116) | def refresh_cache(_: User = Depends(get_current_active_superuser)):
function reidentify_cache (line 137) | async def reidentify_cache(domain: str, torrent_hash: str,
FILE: app/api/endpoints/transfer.py
function query_name (line 25) | def query_name(path: str, filetype: str,
function query_queue (line 62) | async def query_queue(_: schemas.TokenPayload = Depends(verify_token)) -...
function remove_queue (line 71) | async def remove_queue(fileitem: schemas.FileItem, _: schemas.TokenPaylo...
function manual_transfer (line 84) | def manual_transfer(transer_item: ManualTransferItem,
function now (line 195) | def now(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
FILE: app/api/endpoints/user.py
function list_users (line 21) | async def list_users(
function create_user (line 32) | async def create_user(
function update_user (line 53) | async def update_user(
function read_current_user (line 87) | async def read_current_user(
function upload_avatar (line 97) | async def upload_avatar(user_id: int, db: AsyncSession = Depends(get_asy...
function get_config (line 115) | def get_config(key: str,
function set_config (line 127) | def set_config(
function delete_user_by_id (line 140) | async def delete_user_by_id(
function delete_user_by_name (line 157) | async def delete_user_by_name(
function read_user_by_name (line 174) | async def read_user_by_name(
FILE: app/api/endpoints/webhook.py
function start_webhook_chain (line 12) | def start_webhook_chain(body: Any, form: Any, args: Any):
function webhook_message (line 20) | async def webhook_message(background_tasks: BackgroundTasks,
function webhook_message (line 35) | async def webhook_message(background_tasks: BackgroundTasks,
FILE: app/api/endpoints/workflow.py
function list_workflows (line 27) | async def list_workflows(db: AsyncSession = Depends(get_async_db),
function create_workflow (line 36) | async def create_workflow(workflow: schemas.Workflow,
function list_plugin_actions (line 56) | def list_plugin_actions(plugin_id: str = None, _: schemas.TokenPayload =...
function list_actions (line 64) | async def list_actions(_: schemas.TokenPayload = Depends(verify_token)) ...
function get_event_types (line 72) | async def get_event_types(_: schemas.TokenPayload = Depends(verify_token...
function workflow_share (line 83) | async def workflow_share(
function workflow_share_delete (line 100) | async def workflow_share_delete(
function workflow_fork (line 111) | async def workflow_fork(
function workflow_shares (line 167) | async def workflow_shares(
function run_workflow (line 179) | def run_workflow(workflow_id: int,
function start_workflow (line 192) | def start_workflow(workflow_id: int,
function pause_workflow (line 213) | def pause_workflow(workflow_id: int,
function reset_workflow (line 237) | async def reset_workflow(workflow_id: int,
function get_workflow (line 256) | async def get_workflow(workflow_id: int,
function update_workflow (line 266) | def update_workflow(workflow: schemas.Workflow,
function delete_workflow (line 291) | def delete_workflow(workflow_id: int,
FILE: app/api/servarr.py
function arr_system_status (line 23) | async def arr_system_status(_: Annotated[str, Depends(verify_apikey)]) -...
function arr_qualityProfile (line 77) | async def arr_qualityProfile(_: Annotated[str, Depends(verify_apikey)]) ...
function arr_rootfolder (line 118) | async def arr_rootfolder(_: Annotated[str, Depends(verify_apikey)]) -> Any:
function arr_tag (line 134) | async def arr_tag(_: Annotated[str, Depends(verify_apikey)]) -> Any:
function arr_languageprofile (line 147) | async def arr_languageprofile(_: Annotated[str, Depends(verify_apikey)])...
function arr_movies (line 173) | async def arr_movies(_: Annotated[str, Depends(verify_apikey)], db: Asyn...
function arr_movie_lookup (line 264) | def arr_movie_lookup(term: str, _: Annotated[str, Depends(verify_apikey)...
function arr_movie (line 310) | async def arr_movie(mid: int, _: Annotated[str, Depends(verify_apikey)],
function arr_add_movie (line 337) | async def arr_add_movie(_: Annotated[str, Depends(verify_apikey)],
function arr_remove_movie (line 368) | async def arr_remove_movie(mid: int, _: Annotated[str, Depends(verify_ap...
function arr_series (line 385) | async def arr_series(_: Annotated[str, Depends(verify_apikey)], db: Asyn...
function arr_series_lookup (line 521) | def arr_series_lookup(term: str, _: Annotated[str, Depends(verify_apikey...
function arr_serie (line 611) | async def arr_serie(tid: int, _: Annotated[str, Depends(verify_apikey)],
function arr_add_series (line 646) | async def arr_add_series(tv: schemas.SonarrSeries,
function arr_update_series (line 690) | async def arr_update_series(tv: schemas.SonarrSeries, _: Annotated[str, ...
function arr_remove_series (line 698) | async def arr_remove_series(tid: int, _: Annotated[str, Depends(verify_a...
FILE: app/api/servcookie.py
class GzipRequest (line 17) | class GzipRequest(Request):
method body (line 19) | async def body(self) -> bytes:
class GzipRoute (line 28) | class GzipRoute(APIRoute):
method get_route_handler (line 30) | def get_route_handler(self) -> Callable:
function verify_server_enabled (line 40) | async def verify_server_enabled():
function get_root (line 55) | async def get_root():
function post_root (line 60) | async def post_root():
function update_cookie (line 65) | async def update_cookie(req: schemas.CookieData):
function load_encrypt_data (line 81) | async def load_encrypt_data(uuid: str) -> Dict[str, Any]:
function get_decrypted_cookie_data (line 98) | def get_decrypted_cookie_data(uuid: str, password: str,
function get_cookie (line 120) | async def get_cookie(
function post_cookie (line 129) | async def post_cookie(
FILE: app/chain/__init__.py
class ChainBase (line 34) | class ChainBase(metaclass=ABCMeta):
method __init__ (line 39) | def __init__(self):
method load_cache (line 54) | def load_cache(self, filename: str) -> Any:
method async_load_cache (line 67) | async def async_load_cache(self, filename: str) -> Any:
method async_save_cache (line 80) | async def async_save_cache(self, cache: Any, filename: str) -> None:
method save_cache (line 90) | def save_cache(self, cache: Any, filename: str) -> None:
method remove_cache (line 100) | def remove_cache(self, filename: str) -> None:
method async_remove_cache (line 106) | async def async_remove_cache(self, filename: str) -> None:
method __is_valid_empty (line 113) | def __is_valid_empty(ret):
method __handle_plugin_error (line 122) | def __handle_plugin_error(self, err: Exception, plugin_id: str, plugin...
method __handle_system_error (line 145) | def __handle_system_error(self, err: Exception, module_id: str, module...
method __execute_plugin_modules (line 168) | def __execute_plugin_modules(self, method: str, result: Any, *args, **...
method __async_execute_plugin_modules (line 193) | async def __async_execute_plugin_modules(self, method: str, result: An...
method __execute_system_modules (line 226) | def __execute_system_modules(self, method: str, result: Any, *args, **...
method __async_execute_system_modules (line 259) | async def __async_execute_system_modules(self, method: str, result: An...
method run_module (line 301) | def run_module(self, method: str, *args, **kwargs) -> Any:
method async_run_module (line 318) | async def async_run_module(self, method: str, *args, **kwargs) -> Any:
method recognize_media (line 336) | def recognize_media(self, meta: MetaBase = None,
method async_recognize_media (line 370) | async def async_recognize_media(self, meta: MetaBase = None,
method match_doubaninfo (line 404) | def match_doubaninfo(self, name: str, imdbid: Optional[str] = None,
method async_match_doubaninfo (line 419) | async def async_match_doubaninfo(self, name: str, imdbid: Optional[str...
method match_tmdbinfo (line 435) | def match_tmdbinfo(self, name: str, mtype: Optional[MediaType] = None,
method async_match_tmdbinfo (line 447) | async def async_match_tmdbinfo(self, name: str, mtype: Optional[MediaT...
method obtain_images (line 459) | def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
method async_obtain_images (line 467) | async def async_obtain_images(self, mediainfo: MediaInfo) -> Optional[...
method obtain_specific_image (line 475) | def obtain_specific_image(self, mediaid: Union[str, int], mtype: Media...
method douban_info (line 491) | def douban_info(self, doubanid: str, mtype: Optional[MediaType] = None,
method async_douban_info (line 502) | async def async_douban_info(self, doubanid: str, mtype: Optional[Media...
method tvdb_info (line 514) | def tvdb_info(self, tvdbid: int) -> Optional[dict]:
method tmdb_info (line 522) | def tmdb_info(self, tmdbid: int, mtype: MediaType, season: Optional[in...
method async_tmdb_info (line 532) | async def async_tmdb_info(self, tmdbid: int, mtype: MediaType, season:...
method bangumi_info (line 542) | def bangumi_info(self, bangumiid: int) -> Optional[dict]:
method async_bangumi_info (line 550) | async def async_bangumi_info(self, bangumiid: int) -> Optional[dict]:
method message_parser (line 558) | def message_parser(self, source: str, body: Any, form: Any,
method webhook_parser (line 573) | def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[...
method search_medias (line 583) | def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]:
method async_search_medias (line 591) | async def async_search_medias(self, meta: MetaBase) -> Optional[List[M...
method search_persons (line 599) | def search_persons(self, name: str) -> Optional[List[MediaPerson]]:
method async_search_persons (line 606) | async def async_search_persons(self, name: str) -> Optional[List[Media...
method search_collections (line 613) | def search_collections(self, name: str) -> Optional[List[MediaInfo]]:
method async_search_collections (line 620) | async def async_search_collections(self, name: str) -> Optional[List[M...
method search_torrents (line 627) | def search_torrents(self, site: dict,
method async_search_torrents (line 642) | async def async_search_torrents(self, site: dict,
method refresh_torrents (line 657) | def refresh_torrents(self, site: dict, keyword: Optional[str] = None,
method async_refresh_torrents (line 669) | async def async_refresh_torrents(self, site: dict, keyword: Optional[s...
method filter_torrents (line 682) | def filter_torrents(self, rule_groups: List[str],
method download (line 695) | def download(self, content: Union[Path, str, bytes], download_dir: Pat...
method download_added (line 714) | def download_added(self, context: Context, download_dir: Path, torrent...
method list_torrents (line 726) | def list_torrents(self, status: TorrentStatus = None,
method transfer (line 739) | def transfer(self, fileitem: FileItem, meta: MetaBase, mediainfo: Medi...
method transfer_completed (line 773) | def transfer_completed(self, hashs: str, downloader: Optional[str] = N...
method remove_torrents (line 781) | def remove_torrents(self, hashs: Union[str, list], delete_file: bool =...
method start_torrents (line 792) | def start_torrents(self, hashs: Union[list, str], downloader: Optional...
method stop_torrents (line 801) | def stop_torrents(self, hashs: Union[list, str], downloader: Optional[...
method torrent_files (line 810) | def torrent_files(self, tid: str,
method media_exists (line 820) | def media_exists(self, mediainfo: MediaInfo, itemid: Optional[str] = N...
method media_files (line 831) | def media_files(self, mediainfo: MediaInfo) -> Optional[List[FileItem]]:
method post_message (line 839) | def post_message(self,
method async_post_message (line 924) | async def async_post_message(self,
method post_medias_message (line 1010) | def post_medias_message(self, message: Notification, medias: List[Medi...
method post_torrents_message (line 1023) | def post_torrents_message(self, message: Notification, torrents: List[...
method delete_message (line 1036) | def delete_message(self, channel: MessageChannel, source: str,
method metadata_img (line 1049) | def metadata_img(self, mediainfo: MediaInfo,
method media_category (line 1059) | def media_category(self) -> Optional[Dict[str, list]]:
method category_config (line 1066) | def category_config(self) -> CategoryConfig:
method save_category_config (line 1072) | def save_category_config(self, config: CategoryConfig) -> bool:
method register_commands (line 1078) | def register_commands(self, commands: Dict[str, dict]) -> None:
method scheduler_job (line 1084) | def scheduler_job(self) -> None:
method clear_cache (line 1090) | def clear_cache(self) -> None:
FILE: app/chain/ai_recommend.py
class AIRecommendChain (line 15) | class AIRecommendChain(ChainBase, metaclass=Singleton):
method _calculate_request_hash (line 32) | def _calculate_request_hash(
method is_enabled (line 47) | def is_enabled(self) -> bool:
method _build_status (line 53) | def _build_status(self) -> Dict[str, Any]:
method get_current_status_only (line 79) | def get_current_status_only(self) -> Dict[str, Any]:
method get_status (line 85) | def get_status(
method async_ai_recommend (line 108) | async def async_ai_recommend(self, items: List[str], preference: str =...
method is_ai_recommend_running (line 162) | def is_ai_recommend_running(self) -> bool:
method cancel_ai_recommend (line 168) | def cancel_ai_recommend(self):
method start_recommend_task (line 181) | def start_recommend_task(
FILE: app/chain/bangumi.py
class BangumiChain (line 8) | class BangumiChain(ChainBase):
method calendar (line 13) | def calendar(self) -> Optional[List[MediaInfo]]:
method discover (line 19) | def discover(self, **kwargs) -> Optional[List[MediaInfo]]:
method bangumi_info (line 25) | def bangumi_info(self, bangumiid: int) -> Optional[dict]:
method bangumi_credits (line 33) | def bangumi_credits(self, bangumiid: int) -> List[schemas.MediaPerson]:
method bangumi_recommend (line 40) | def bangumi_recommend(self, bangumiid: int) -> Optional[List[MediaInfo]]:
method person_detail (line 47) | def person_detail(self, person_id: int) -> Optional[schemas.MediaPerson]:
method person_credits (line 54) | def person_credits(self, person_id: int) -> Optional[List[MediaInfo]]:
method async_calendar (line 61) | async def async_calendar(self) -> Optional[List[MediaInfo]]:
method async_discover (line 67) | async def async_discover(self, **kwargs) -> Optional[List[MediaInfo]]:
method async_bangumi_info (line 73) | async def async_bangumi_info(self, bangumiid: int) -> Optional[dict]:
method async_bangumi_credits (line 81) | async def async_bangumi_credits(self, bangumiid: int) -> List[schemas....
method async_bangumi_recommend (line 88) | async def async_bangumi_recommend(self, bangumiid: int) -> Optional[Li...
method async_person_detail (line 95) | async def async_person_detail(self, person_id: int) -> Optional[schema...
method async_person_credits (line 102) | async def async_person_credits(self, person_id: int) -> Optional[List[...
FILE: app/chain/dashboard.py
class DashboardChain (line 7) | class DashboardChain(ChainBase):
method media_statistic (line 11) | def media_statistic(self, server: Optional[str] = None) -> Optional[Li...
method downloader_info (line 17) | def downloader_info(self, downloader: Optional[str] = None) -> Optiona...
FILE: app/chain/douban.py
class DoubanChain (line 9) | class DoubanChain(ChainBase):
method person_detail (line 14) | def person_detail(self, person_id: int) -> Optional[schemas.MediaPerson]:
method person_credits (line 21) | def person_credits(self, person_id: int, page: Optional[int] = 1) -> L...
method movie_top250 (line 29) | def movie_top250(self, page: Optional[int] = 1, count: Optional[int] =...
method movie_showing (line 37) | def movie_showing(self, page: Optional[int] = 1, count: Optional[int] ...
method tv_weekly_chinese (line 43) | def tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[i...
method tv_weekly_global (line 49) | def tv_weekly_global(self, page: Optional[int] = 1, count: Optional[in...
method douban_discover (line 55) | def douban_discover(self, mtype: MediaType, sort: str, tags: str,
method tv_animation (line 69) | def tv_animation(self, page: Optional[int] = 1, count: Optional[int] =...
method movie_hot (line 75) | def movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30...
method tv_hot (line 81) | def tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -...
method movie_credits (line 87) | def movie_credits(self, doubanid: str) -> Optional[List[schemas.MediaP...
method tv_credits (line 94) | def tv_credits(self, doubanid: str) -> Optional[List[schemas.MediaPers...
method movie_recommend (line 101) | def movie_recommend(self, doubanid: str) -> List[MediaInfo]:
method tv_recommend (line 108) | def tv_recommend(self, doubanid: str) -> List[MediaInfo]:
method async_person_detail (line 115) | async def async_person_detail(self, person_id: int) -> Optional[schema...
method async_person_credits (line 122) | async def async_person_credits(self, person_id: int, page: Optional[in...
method async_movie_top250 (line 130) | async def async_movie_top250(self, page: Optional[int] = 1,
method async_movie_showing (line 139) | async def async_movie_showing(self, page: Optional[int] = 1,
method async_tv_weekly_chinese (line 146) | async def async_tv_weekly_chinese(self, page: Optional[int] = 1,
method async_tv_weekly_global (line 153) | async def async_tv_weekly_global(self, page: Optional[int] = 1,
method async_douban_discover (line 160) | async def async_douban_discover(self, mtype: MediaType, sort: str, tag...
method async_tv_animation (line 174) | async def async_tv_animation(self, page: Optional[int] = 1,
method async_movie_hot (line 181) | async def async_movie_hot(self, page: Optional[int] = 1,
method async_tv_hot (line 188) | async def async_tv_hot(self, page: Optional[int] = 1,
method async_movie_credits (line 195) | async def async_movie_credits(self, doubanid: str) -> Optional[List[sc...
method async_tv_credits (line 202) | async def async_tv_credits(self, doubanid: str) -> Optional[List[schem...
method async_movie_recommend (line 209) | async def async_movie_recommend(self, doubanid: str) -> List[MediaInfo]:
method async_tv_recommend (line 216) | async def async_tv_recommend(self, doubanid: str) -> List[MediaInfo]:
FILE: app/chain/download.py
class DownloadChain (line 30) | class DownloadChain(ChainBase):
method download_torrent (line 35) | def download_torrent(self, torrent: TorrentInfo,
method download_single (line 145) | def download_single(self, context: Context,
method batch_download (line 398) | def batch_download(self,
method get_no_exists_info (line 780) | def get_no_exists_info(self, meta: MetaBase,
method remote_downloading (line 912) | def remote_downloading(self, channel: MessageChannel, userid: Union[st...
method downloading (line 946) | def downloading(self, name: Optional[str] = None) -> List[DownloadingT...
method set_downloading (line 972) | def set_downloading(self, hash_str, oper: str, name: Optional[str] = N...
method remove_downloading (line 982) | def remove_downloading(self, hash_str: str, name: Optional[str] = None...
method download_file_deleted (line 989) | def download_file_deleted(self, event: Event):
FILE: app/chain/media.py
class ScrapingOption (line 32) | class ScrapingOption:
method __init__ (line 38) | def __init__(
method is_skip (line 63) | def is_skip(self) -> bool:
method is_overwrite (line 68) | def is_overwrite(self) -> bool:
class ScrapingConfig (line 73) | class ScrapingConfig:
method __init__ (line 78) | def __init__(self, config_dict: dict[str, str] = None):
method option (line 97) | def option(self, item: Union[str, ScrapingTarget], metadata: Union[str...
method from_system_config (line 107) | def from_system_config(cls) -> 'ScrapingConfig':
method get_default_config (line 117) | def get_default_config() -> dict[str, str]:
class MediaChain (line 132) | class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
method on_config_changed (line 153) | def on_config_changed(self):
method _should_scrape (line 156) | def _should_scrape(self, scraping_option: ScrapingOption, file_exists:...
method _save_file (line 184) | def _save_file(self, fileitem: schemas.FileItem, path: Path, content: ...
method _download_and_save_image (line 215) | def _download_and_save_image(self, fileitem: schemas.FileItem, path: P...
method _get_target_fileitem_and_path (line 256) | def _get_target_fileitem_and_path(self, current_fileitem: schemas.File...
method metadata_nfo (line 319) | def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
method select_recognize_source (line 331) | def select_recognize_source(self, log_name: str, log_context: str,
method recognize_by_meta (line 359) | def recognize_by_meta(self, metainfo: MetaBase, episode_group: Optiona...
method recognize_help (line 381) | def recognize_help(self, title: str, org_meta: MetaBase) -> Optional[M...
method recognize_by_path (line 430) | def recognize_by_path(self, path: str, episode_group: Optional[str] = ...
method search (line 454) | def search(self, title: str) -> Tuple[Optional[MetaBase], List[MediaIn...
method get_tmdbinfo_by_doubanid (line 486) | def get_tmdbinfo_by_doubanid(self, doubanid: str, mtype: MediaType = N...
method get_tmdbinfo_by_bangumiid (line 522) | def get_tmdbinfo_by_bangumiid(self, bangumiid: int) -> Optional[dict]:
method get_doubaninfo_by_tmdbid (line 548) | def get_doubaninfo_by_tmdbid(self, tmdbid: int,
method get_doubaninfo_by_bangumiid (line 569) | def get_doubaninfo_by_bangumiid(self, bangumiid: int) -> Optional[dict]:
method scrape_metadata_event (line 592) | def scrape_metadata_event(self, event: Event):
method _scrape_nfo_generic (line 689) | def _scrape_nfo_generic(self, current_fileitem: schemas.FileItem,
method _scrape_images_generic (line 731) | def _scrape_images_generic(self, current_fileitem: schemas.FileItem,
method scrape_metadata (line 800) | def scrape_metadata(self, fileitem: schemas.FileItem,
method _handle_movie_scraping (line 859) | def _handle_movie_scraping(self, fileitem: schemas.FileItem,
method _handle_movie_directory (line 888) | def _handle_movie_directory(self, fileitem: schemas.FileItem,
method _handle_tv_scraping (line 928) | def _handle_tv_scraping(self, fileitem: schemas.FileItem,
method _handle_tv_episode_file (line 959) | def _handle_tv_episode_file(self, fileitem: schemas.FileItem,
method _handle_tv_directory (line 1001) | def _handle_tv_directory(self, fileitem: schemas.FileItem,
method _initialize_tv_directory_metadata (line 1037) | def _initialize_tv_directory_metadata(self, fileitem: schemas.FileItem,
method async_select_recognize_source (line 1095) | async def async_select_recognize_source(self, log_name: str, log_conte...
method async_recognize_by_meta (line 1123) | async def async_recognize_by_meta(self, metainfo: MetaBase,
method async_recognize_help (line 1151) | async def async_recognize_help(self, title: str, org_meta: MetaBase) -...
method async_recognize_by_path (line 1200) | async def async_recognize_by_path(self, path: str, episode_group: Opti...
method async_search (line 1229) | async def async_search(self, title: str) -> Tuple[Optional[MetaBase], ...
method _extract_year_from_bangumi (line 1262) | def _extract_year_from_bangumi(bangumiinfo: dict) -> Optional[str]:
method _extract_year_from_tmdb (line 1272) | def _extract_year_from_tmdb(tmdbinfo: dict, season: Optional[int] = No...
method _match_tmdb_with_names (line 1290) | def _match_tmdb_with_names(self, meta_names: list, year: Optional[str],
method _async_match_tmdb_with_names (line 1306) | async def _async_match_tmdb_with_names(self, meta_names: list, year: O...
method async_get_tmdbinfo_by_doubanid (line 1322) | async def async_get_tmdbinfo_by_doubanid(self, doubanid: str, mtype: M...
method async_get_tmdbinfo_by_bangumiid (line 1358) | async def async_get_tmdbinfo_by_bangumiid(self, bangumiid: int) -> Opt...
method async_get_doubaninfo_by_tmdbid (line 1384) | async def async_get_doubaninfo_by_tmdbid(self, tmdbid: int, mtype: Med...
method async_get_doubaninfo_by_bangumiid (line 1405) | async def async_get_doubaninfo_by_bangumiid(self, bangumiid: int) -> O...
FILE: app/chain/mediaserver.py
class MediaServerChain (line 14) | class MediaServerChain(ChainBase):
method librarys (line 19) | def librarys(self, server: str, username: Optional[str] = None,
method items (line 26) | def items(self, server: str, library_id: Union[str, int],
method iteminfo (line 68) | def iteminfo(self, server: str, item_id: Union[str, int]) -> MediaServ...
method episodes (line 74) | def episodes(self, server: str, item_id: Union[str, int]) -> List[Medi...
method playing (line 80) | def playing(self, server: str, count: Optional[int] = 20,
method latest (line 87) | def latest(self, server: str, count: Optional[int] = 20,
method get_latest_wallpapers (line 94) | def get_latest_wallpapers(self, server: Optional[str] = None, count: O...
method get_latest_wallpaper (line 102) | def get_latest_wallpaper(self, server: Optional[str] = None,
method get_play_url (line 110) | def get_play_url(self, server: str, item_id: Union[str, int]) -> Optio...
method get_image_cookies (line 116) | def get_image_cookies(
method sync (line 126) | def sync(self):
FILE: app/chain/message.py
class MessageChain (line 32) | class MessageChain(ChainBase):
method __get_noexits_info (line 46) | def __get_noexits_info(
method process (line 99) | def process(self, body: Any, form: Any, args: Any) -> None:
method handle_message (line 134) | def handle_message(self, channel: MessageChannel, source: str,
method _handle_callback (line 556) | def _handle_callback(self, text: str, channel: MessageChannel, source:...
method __auto_download (line 605) | def __auto_download(self, channel: MessageChannel, source: str, cache_...
method __post_medias_message (line 657) | def __post_medias_message(self, channel: MessageChannel, source: str,
method _create_media_buttons (line 695) | def _create_media_buttons(self, channel: MessageChannel, items: list, ...
method __post_torrents_message (line 746) | def __post_torrents_message(self, channel: MessageChannel, source: str,
method _create_torrent_buttons (line 785) | def _create_torrent_buttons(self, channel: MessageChannel, items: list...
method _get_or_create_session_id (line 841) | def _get_or_create_session_id(self, userid: Union[str, int]) -> str:
method clear_user_session (line 869) | def clear_user_session(self, userid: Union[str, int]) -> bool:
method remote_clear_session (line 880) | def remote_clear_session(self, channel: MessageChannel, userid: Union[...
method _handle_ai_message (line 917) | def _handle_ai_message(self, text: str, channel: MessageChannel, sourc...
FILE: app/chain/recommend.py
class RecommendChain (line 18) | class RecommendChain(ChainBase, metaclass=Singleton):
method refresh_recommend (line 30) | def refresh_recommend(self, manual: bool = False):
method __cache_posters (line 83) | def __cache_posters(self, datas: List[dict]):
method __fetch_and_save_image (line 100) | def __fetch_and_save_image(url: str):
method tmdb_movies (line 109) | def tmdb_movies(self, sort_by: Optional[str] = "popularity.desc",
method tmdb_tvs (line 135) | def tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc",
method tmdb_trending (line 161) | def tmdb_trending(self, page: Optional[int] = 1) -> List[dict]:
method bangumi_calendar (line 170) | def bangumi_calendar(self, page: Optional[int] = 1, count: Optional[in...
method douban_movie_showing (line 179) | def douban_movie_showing(self, page: Optional[int] = 1, count: Optiona...
method douban_movies (line 188) | def douban_movies(self, sort: Optional[str] = "R", tags: Optional[str]...
method douban_tvs (line 199) | def douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "",
method douban_movie_top250 (line 210) | def douban_movie_top250(self, page: Optional[int] = 1, count: Optional...
method douban_tv_weekly_chinese (line 219) | def douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Opt...
method douban_tv_weekly_global (line 228) | def douban_tv_weekly_global(self, page: Optional[int] = 1, count: Opti...
method douban_tv_animation (line 237) | def douban_tv_animation(self, page: Optional[int] = 1, count: Optional...
method douban_movie_hot (line 246) | def douban_movie_hot(self, page: Optional[int] = 1, count: Optional[in...
method douban_tv_hot (line 255) | def douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] ...
method async_tmdb_movies (line 264) | async def async_tmdb_movies(self, sort_by: Optional[str] = "popularity...
method async_tmdb_tvs (line 290) | async def async_tmdb_tvs(self, sort_by: Optional[str] = "popularity.de...
method async_tmdb_trending (line 316) | async def async_tmdb_trending(self, page: Optional[int] = 1) -> List[d...
method async_bangumi_calendar (line 325) | async def async_bangumi_calendar(self, page: Optional[int] = 1, count:...
method async_douban_movie_showing (line 334) | async def async_douban_movie_showing(self, page: Optional[int] = 1, co...
method async_douban_movies (line 343) | async def async_douban_movies(self, sort: Optional[str] = "R", tags: O...
method async_douban_tvs (line 354) | async def async_douban_tvs(self, sort: Optional[str] = "R", tags: Opti...
method async_douban_movie_top250 (line 365) | async def async_douban_movie_top250(self, page: Optional[int] = 1, cou...
method async_douban_tv_weekly_chinese (line 374) | async def async_douban_tv_weekly_chinese(self, page: Optional[int] = 1...
method async_douban_tv_weekly_global (line 383) | async def async_douban_tv_weekly_global(self, page: Optional[int] = 1,...
method async_douban_tv_animation (line 392) | async def async_douban_tv_animation(self, page: Optional[int] = 1, cou...
method async_douban_movie_hot (line 401) | async def async_douban_movie_hot(self, page: Optional[int] = 1, count:...
method async_douban_tv_hot (line 410) | async def async_douban_tv_hot(self, page: Optional[int] = 1, count: Op...
FILE: app/chain/search.py
class SearchChain (line 26) | class SearchChain(ChainBase):
method search_by_id (line 34) | def search_by_id(self, tmdbid: Optional[int] = None, doubanid: Optiona...
method search_by_title (line 64) | def search_by_title(self, title: str, page: Optional[int] = 0,
method last_search_results (line 90) | def last_search_results(self) -> Optional[List[Context]]:
method async_last_search_results (line 96) | async def async_last_search_results(self) -> Optional[List[Context]]:
method async_last_ai_results (line 102) | async def async_last_ai_results(self) -> Optional[List[Context]]:
method async_save_ai_results (line 108) | async def async_save_ai_results(self, results: List[Context]):
method async_search_by_id (line 114) | async def async_search_by_id(self, tmdbid: Optional[int] = None, douba...
method async_search_by_title (line 144) | async def async_search_by_title(self, title: str, page: Optional[int] ...
method __prepare_params (line 171) | def __prepare_params(mediainfo: MediaInfo,
method __parse_result (line 207) | def __parse_result(self, torrents: List[TorrentInfo],
method __remove_duplicate (line 334) | def __remove_duplicate(_torrents: List[Context]) -> List[Context]:
method process (line 343) | def process(self, mediainfo: MediaInfo,
method async_process (line 426) | async def async_process(self, mediainfo: MediaInfo,
method __search_all_sites (line 506) | def __search_all_sites(self, keyword: str,
method __async_search_all_sites (line 588) | async def __async_search_all_sites(self, keyword: str,
method remove_site (line 674) | def remove_site(self, event: Event):
FILE: app/chain/site.py
class SiteChain (line 29) | class SiteChain(ChainBase):
method __init__ (line 34) | def __init__(self):
method refresh_userdata (line 50) | def refresh_userdata(self, site: dict = None) -> Optional[SiteUserData]:
method refresh_userdatas (line 92) | def refresh_userdatas(self) -> Optional[Dict[str, SiteUserData]]:
method is_special_site (line 113) | def is_special_site(self, domain: str) -> bool:
method __zhuque_test (line 120) | def __zhuque_test(site: Site) -> Tuple[bool, str]:
method __mteam_test (line 165) | def __mteam_test(site: Site) -> Tuple[bool, str]:
method __yema_test (line 193) | def __yema_test(site: Site) -> Tuple[bool, str]:
method __indexphp_test (line 220) | def __indexphp_test(self, site: Site) -> Tuple[bool, str]:
method __hddolby_test (line 228) | def __hddolby_test(site: Site) -> Tuple[bool, str]:
method __rousi_test (line 254) | def __rousi_test(site: Site) -> Tuple[bool, str]:
method __parse_favicon (line 280) | def __parse_favicon(url: str, cookie: str, ua: str) -> Tuple[str, Opti...
method sync_cookies (line 312) | def sync_cookies(self, manual=False) -> Tuple[bool, str]:
method cache_site_icon (line 464) | def cache_site_icon(self, event: Event):
method clear_site_data (line 506) | def clear_site_data(self, event: Event):
method cache_site_userdata (line 528) | def cache_site_userdata(self, event: Event):
method test (line 547) | def test(self, url: str) -> Tuple[bool, str]:
method __test (line 581) | def __test(site_info: Site) -> Tuple[bool, str]:
method remote_list (line 629) | def remote_list(self, channel: MessageChannel,
method remote_disable (line 663) | def remote_disable(self, arg_str: str, channel: MessageChannel,
method remote_enable (line 689) | def remote_enable(self, arg_str: str, channel: MessageChannel,
method update_cookie (line 717) | def update_cookie(site_info: Site,
method remote_cookie (line 747) | def remote_cookie(self, arg_str: str, channel: MessageChannel,
method remote_refresh_userdatas (line 817) | def remote_refresh_userdatas(self, channel: MessageChannel,
FILE: app/chain/storage.py
class StorageChain (line 11) | class StorageChain(ChainBase):
method save_config (line 16) | def save_config(self, storage: str, conf: dict) -> None:
method reset_config (line 22) | def reset_config(self, storage: str) -> None:
method generate_qrcode (line 28) | def generate_qrcode(self, storage: str) -> Optional[Tuple[dict, str]]:
method generate_auth_url (line 34) | def generate_auth_url(self, storage: str) -> Optional[Tuple[dict, str]]:
method check_login (line 40) | def check_login(self, storage: str, **kwargs) -> Optional[Tuple[dict, ...
method list_files (line 46) | def list_files(self, fileitem: schemas.FileItem, recursion: bool = Fal...
method any_files (line 52) | def any_files(self, fileitem: schemas.FileItem, extensions: list = Non...
method create_folder (line 58) | def create_folder(self, fileitem: schemas.FileItem, name: str) -> Opti...
method download_file (line 64) | def download_file(self, fileitem: schemas.FileItem, path: Path = None)...
method upload_file (line 72) | def upload_file(self, fileitem: schemas.FileItem, path: Path,
method delete_file (line 82) | def delete_file(self, fileitem: schemas.FileItem) -> Optional[bool]:
method rename_file (line 88) | def rename_file(self, fileitem: schemas.FileItem, name: str) -> Option...
method exists (line 94) | def exists(self, fileitem: schemas.FileItem) -> Optional[bool]:
method get_item (line 100) | def get_item(self, fileitem: schemas.FileItem) -> Optional[schemas.Fil...
method get_file_item (line 106) | def get_file_item(self, storage: str, path: Path) -> Optional[schemas....
method get_parent_item (line 112) | def get_parent_item(self, fileitem: schemas.FileItem) -> Optional[sche...
method snapshot_storage (line 118) | def snapshot_storage(self, storage: str, path: Path,
method storage_usage (line 130) | def storage_usage(self, storage: str) -> Optional[schemas.StorageUsage]:
method support_transtype (line 136) | def support_transtype(self, storage: str) -> Optional[dict]:
method is_bluray_folder (line 142) | def is_bluray_folder(self, fileitem: Optional[schemas.FileItem]) -> bool:
method contains_bluray_subdirectories (line 155) | def contains_bluray_subdirectories(fileitems: Optional[List[schemas.Fi...
method delete_media_file (line 165) | def delete_media_file(self, fileitem: schemas.FileItem, delete_self: b...
FILE: app/chain/subscribe.py
class SubscribeChain (line 35) | class SubscribeChain(ChainBase):
method __get_event_media (line 45) | def __get_event_media(_mediaid: str, _meta: MetaBase) -> Optional[Medi...
method __async_get_event_meida (line 67) | async def __async_get_event_meida(_mediaid: str, _meta: MetaBase) -> O...
method __get_default_kwargs (line 88) | def __get_default_kwargs(self, mtype: MediaType, **kwargs) -> dict:
method add (line 120) | def add(self, title: str, year: str,
method async_add (line 297) | async def async_add(self, title: str, year: str,
method exists (line 475) | def exists(mediainfo: MediaInfo, meta: MetaBase = None):
method search (line 485) | def search(self, sid: Optional[int] = None, state: Optional[str] = 'N'...
method update_subscribe_priority (line 663) | def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaBase,
method finish_subscribe_or_not (line 686) | def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaBase...
method refresh (line 723) | def refresh(self):
method get_sub_sites (line 736) | def get_sub_sites(subscribe: Subscribe) -> List[int]:
method get_subscribed_sites (line 757) | def get_subscribed_sites(self) -> Optional[List[int]]:
method match (line 778) | def match(self, torrents: Dict[str, List[Context]]):
method check (line 1067) | def check(self):
method get_subscribe_by_source (line 1122) | def get_subscribe_by_source(self, source: str) -> Optional[Subscribe]:
method follow (line 1136) | def follow():
method cache_calendar (line 1200) | async def cache_calendar(self):
method __update_subscribe_note (line 1237) | def __update_subscribe_note(subscribe: Subscribe, downloads: Optional[...
method __get_downloaded (line 1274) | def __get_downloaded(subscribe: Subscribe) -> List[int]:
method __update_lack_episodes (line 1294) | def __update_lack_episodes(lefts: Dict[Union[int, str], Dict[int, sche...
method __finish_subscribe (line 1329) | def __finish_subscribe(self, subscribe: Subscribe, mediainfo: MediaInf...
method remote_list (line 1375) | def remote_list(self, channel: MessageChannel,
method remote_delete (line 1403) | def remote_delete(self, arg_str: str, channel: MessageChannel,
method __get_subscribe_no_exits (line 1437) | def __get_subscribe_no_exits(subscribe_name: str,
method remove_site (line 1548) | def remove_site(self, event: Event):
method __get_default_subscribe_config (line 1587) | def __get_default_subscribe_config(mtype: MediaType, default_config_ke...
method get_params (line 1608) | def get_params(subscribe: Subscribe):
method subscribe_files_info (line 1627) | def subscribe_files_info(self, subscribe: Subscribe) -> Optional[schem...
method check_and_handle_existing_media (line 1740) | def check_and_handle_existing_media(self, subscribe: Subscribe, meta: ...
method get_states_for_search (line 1825) | def get_states_for_search(state: str) -> str:
method get_subscribe_source_keyword (line 1841) | def get_subscribe_source_keyword(subscribe: Subscribe) -> str:
method parse_subscribe_source_keyword (line 1863) | def parse_subscribe_source_keyword(source_keyword_str: str) -> Optiona...
FILE: app/chain/system.py
class SystemChain (line 18) | class SystemChain(ChainBase):
method remote_clear_cache (line 25) | def remote_clear_cache(self, channel: MessageChannel, userid: Union[in...
method restart (line 33) | def restart(self, channel: MessageChannel, userid: Union[int, str], so...
method backup_plugins (line 55) | def backup_plugins():
method restore_plugins (line 105) | def restore_plugins():
method __get_version_message (line 161) | def __get_version_message(self) -> str:
method version (line 179) | def version(self, channel: MessageChannel, userid: Union[int, str], so...
method restart_finish (line 187) | def restart_finish(self):
method __get_server_release_version (line 211) | def __get_server_release_version():
method __get_front_release_version (line 238) | def __get_front_release_version():
method get_server_local_version (line 265) | def get_server_local_version():
method get_frontend_version (line 272) | def get_frontend_version():
FILE: app/chain/tmdb.py
class TmdbChain (line 10) | class TmdbChain(ChainBase):
method tmdb_discover (line 15) | def tmdb_discover(self, mtype: MediaType,
method tmdb_trending (line 49) | def tmdb_trending(self, page: Optional[int] = 1) -> Optional[List[Medi...
method tmdb_collection (line 57) | def tmdb_collection(self, collection_id: int) -> Optional[List[MediaIn...
method tmdb_seasons (line 64) | def tmdb_seasons(self, tmdbid: int) -> List[schemas.TmdbSeason]:
method tmdb_group_seasons (line 71) | def tmdb_group_seasons(self, group_id: str) -> List[schemas.TmdbSeason]:
method tmdb_episodes (line 78) | def tmdb_episodes(self, tmdbid: int, season: int, episode_group: Optio...
method movie_similar (line 87) | def movie_similar(self, tmdbid: int) -> Optional[List[MediaInfo]]:
method tv_similar (line 94) | def tv_similar(self, tmdbid: int) -> Optional[List[MediaInfo]]:
method movie_recommend (line 101) | def movie_recommend(self, tmdbid: int) -> Optional[List[MediaInfo]]:
method tv_recommend (line 108) | def tv_recommend(self, tmdbid: int) -> Optional[List[MediaInfo]]:
method movie_credits (line 115) | def movie_credits(self, tmdbid: int, page: Optional[int] = 1) -> Optio...
method tv_credits (line 123) | def tv_credits(self, tmdbid: int, page: Optional[int] = 1) -> Optional...
method person_detail (line 131) | def person_detail(self, person_id: int) -> Optional[schemas.MediaPerson]:
method person_credits (line 138) | def person_credits(self, person_id: int, page: Optional[int] = 1) -> O...
method get_random_wallpager (line 146) | def get_random_wallpager(self) -> Optional[str]:
method get_trending_wallpapers (line 159) | def get_trending_wallpapers(self, num: Optional[int] = 10) -> List[str]:
method async_tmdb_discover (line 168) | async def async_tmdb_discover(self, mtype: MediaType,
method async_tmdb_trending (line 203) | async def async_tmdb_trending(self, page: Optional[int] = 1) -> Option...
method async_tmdb_collection (line 211) | async def async_tmdb_collection(self, collection_id: int) -> Optional[...
method async_tmdb_seasons (line 218) | async def async_tmdb_seasons(self, tmdbid: int) -> List[schemas.TmdbSe...
method async_tmdb_group_seasons (line 225) | async def async_tmdb_group_seasons(self, group_id: str) -> List[schema...
method async_tmdb_episodes (line 232) | async def async_tmdb_episodes(self, tmdbid: int, season: int,
method async_movie_similar (line 243) | async def async_movie_similar(self, tmdbid: int) -> Optional[List[Medi...
method async_tv_similar (line 250) | async def async_tv_similar(self, tmdbid: int) -> Optional[List[MediaIn...
method async_movie_recommend (line 257) | async def async_movie_recommend(self, tmdbid: int) -> Optional[List[Me...
method async_tv_recommend (line 264) | async def async_tv_recommend(self, tmdbid: int) -> Optional[List[Media...
method async_movie_credits (line 271) | async def async_movie_credits(self, tmdbid: int, page: Optional[int] =...
method async_tv_credits (line 279) | async def async_tv_credits(self, tmdbid: int, page: Optional[int] = 1)...
method async_person_detail (line 287) | async def async_person_detail(self, person_id: int) -> Optional[schema...
method async_person_credits (line 294) | async def async_person_credits(self, person_id: int, page: Optional[in...
method async_get_random_wallpager (line 302) | async def async_get_random_wallpager(self) -> Optional[str]:
method async_get_trending_wallpapers (line 315) | async def async_get_trending_wallpapers(self, num: Optional[int] = 10)...
FILE: app/chain/torrents.py
class TorrentsChain (line 21) | class TorrentsChain(ChainBase):
method cache_file (line 30) | def cache_file(self) -> str:
method remote_refresh (line 38) | def remote_refresh(self, channel: MessageChannel, userid: Union[str, i...
method get_torrents (line 48) | def get_torrents(self, stype: Optional[str] = None) -> Dict[str, List[...
method async_get_torrents (line 68) | async def async_get_torrents(self, stype: Optional[str] = None) -> Dic...
method clear_torrents (line 88) | def clear_torrents(self):
method async_clear_torrents (line 97) | async def async_clear_torrents(self):
method browse (line 106) | def browse(self, domain: str, keyword: Optional[str] = None, cat: Opti...
method async_browse (line 122) | async def async_browse(self, domain: str, keyword: Optional[str] = Non...
method rss (line 138) | def rss(self, domain: str) -> List[TorrentInfo]:
method refresh (line 187) | def refresh(self, stype: Optional[str] = None, sites: List[int] = None...
method _ensure_context_compatibility (line 320) | def _ensure_context_compatibility(torrents_cache: Dict[str, List[Conte...
method __renew_rss_url (line 334) | def __renew_rss_url(self, domain: str, site: dict):
FILE: app/chain/transfer.py
class JobManager (line 48) | class JobManager:
method __init__ (line 59) | def __init__(self):
method __get_meta_id (line 64) | def __get_meta_id(meta: MetaBase = None, season: Optional[int] = None)...
method __get_media_id (line 71) | def __get_media_id(media: MediaInfo = None, season: Optional[int] = No...
method __get_id (line 79) | def __get_id(self, task: TransferTask = None) -> Tuple:
method __get_media (line 89) | def __get_media(task: TransferTask) -> schemas.MediaInfo:
method __get_meta (line 109) | def __get_meta(task: TransferTask) -> schemas.MetaInfo:
method add_task (line 115) | def add_task(self, task: TransferTask, state: Optional[str] = "waiting...
method running_task (line 158) | def running_task(self, task: TransferTask):
method finish_task (line 172) | def finish_task(self, task: TransferTask):
method fail_task (line 186) | def fail_task(self, task: TransferTask):
method remove_task (line 205) | def remove_task(self, fileitem: FileItem) -> Optional[TransferJobTask]:
method remove_job (line 226) | def remove_job(self, task: TransferTask) -> Optional[TransferJob]:
method try_remove_job (line 239) | def try_remove_job(self, task: TransferTask):
method is_done (line 267) | def is_done(self, task: TransferTask) -> bool:
method is_finished (line 288) | def is_finished(self, task: TransferTask) -> bool:
method is_success (line 312) | def is_success(self, task: TransferTask) -> bool:
method get_all_torrent_hashes (line 333) | def get_all_torrent_hashes(self) -> set[str]:
method is_torrent_done (line 344) | def is_torrent_done(self, download_hash: str) -> bool:
method is_torrent_success (line 358) | def is_torrent_success(self, download_hash: str) -> bool:
method has_tasks (line 372) | def has_tasks(self, meta: MetaBase, mediainfo: Optional[MediaInfo] = N...
method success_tasks (line 385) | def success_tasks(self, media: MediaInfo, season: Optional[int] = None...
method all_tasks (line 395) | def all_tasks(self, media: MediaInfo, season: Optional[int] = None) ->...
method count (line 405) | def count(self, media: MediaInfo, season: Optional[int] = None) -> int:
method size (line 415) | def size(self, media: MediaInfo, season: Optional[int] = None) -> int:
method total (line 431) | def total(self) -> int:
method list_jobs (line 438) | def list_jobs(self) -> List[TransferJob]:
method season_episodes (line 445) | def season_episodes(self, media: MediaInfo, season: Optional[int] = No...
class TransferChain (line 454) | class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
method __init__ (line 463) | def __init__(self):
method __init (line 495) | def __init(self):
method __stop (line 508) | def __stop(self):
method on_config_changed (line 518) | def on_config_changed(self):
method __is_subtitle_file (line 522) | def __is_subtitle_file(self, fileitem: FileItem) -> bool:
method __is_audio_file (line 530) | def __is_audio_file(self, fileitem: FileItem) -> bool:
method __is_media_file (line 538) | def __is_media_file(self, fileitem: FileItem) -> bool:
method __is_allowed_file (line 549) | def __is_allowed_file(self, fileitem: FileItem) -> bool:
method __is_allow_filesize (line 558) | def __is_allow_filesize(fileitem: FileItem, min_filesize: int) -> bool:
method __default_callback (line 564) | def __default_callback(self, task: TransferTask,
method put_to_queue (line 777) | def put_to_queue(self, task: TransferTask) -> bool:
method __put_to_jobview (line 795) | def __put_to_jobview(self, task: TransferTask) -> bool:
method remove_from_queue (line 802) | def remove_from_queue(self, fileitem: FileItem):
method __start_transfer (line 810) | def __start_transfer(self):
method __handle_transfer (line 900) | def __handle_transfer(self, task: TransferTask,
method get_queue_tasks (line 1058) | def get_queue_tasks(self) -> List[TransferJob]:
method recommend_name (line 1064) | def recommend_name(self, meta: MetaBase, mediainfo: MediaInfo) -> Opti...
method process (line 1073) | def process(self) -> bool:
method __get_trans_fileitems (line 1179) | def __get_trans_fileitems(
method do_transfer (line 1262) | def do_transfer(self, fileitem: FileItem,
method remote_transfer (line 1529) | def remote_transfer(self, arg_str: str, channel: MessageChannel,
method __re_transfer (line 1571) | def __re_transfer(self, logid: int, mtype: MediaType = None,
method manual_transfer (line 1621) | def manual_transfer(self,
method send_transfer_message (line 1717) | def send_transfer_message(self, meta: MetaBase, mediainfo: MediaInfo,
method _is_blocked_by_exclude_words (line 1739) | def _is_blocked_by_exclude_words(file_path: str, exclude_words: list) ...
method _can_delete_torrent (line 1755) | def _can_delete_torrent(self, download_hash: str, downloader: str, tra...
FILE: app/chain/tvdb.py
class TvdbChain (line 6) | class TvdbChain(ChainBase):
method get_tvdbid_by_name (line 11) | def get_tvdbid_by_name(self, title: str) -> List[int]:
FILE: app/chain/user.py
class UserChain (line 17) | class UserChain(ChainBase):
method user_authenticate (line 22) | def user_authenticate(
method password_authenticate (line 95) | def password_authenticate(credentials: AuthCredentials) -> Tuple[bool,...
method auxiliary_authenticate (line 123) | def auxiliary_authenticate(self, credentials: AuthCredentials) -> Tupl...
method _verify_mfa (line 168) | def _verify_mfa(user: User, mfa_code: Optional[str]) -> Union[bool, str]:
method _process_auth_success (line 210) | def _process_auth_success(self, username: str, credentials: AuthCreden...
FILE: app/chain/webhook.py
class WebhookChain (line 7) | class WebhookChain(ChainBase):
method message (line 12) | def message(self, body: Any, form: Any, args: Any) -> None:
FILE: app/chain/workflow.py
class WorkflowExecutor (line 22) | class WorkflowExecutor:
method __init__ (line 27) | def __init__(self, workflow: Workflow, step_callback: Callable = None):
method execute (line 86) | def execute(self):
method execute_node (line 125) | def execute_node(self, workflow_id: int, node_id: int,
method on_node_complete (line 134) | def on_node_complete(self, future):
method merge_context (line 179) | def merge_context(self, context: ActionContext):
class WorkflowChain (line 188) | class WorkflowChain(ChainBase):
method event_process (line 194) | def event_process(self, event: Event):
method process (line 204) | def process(workflow_id: int, from_begin: Optional[bool] = True) -> Tu...
method get_workflows (line 257) | def get_workflows() -> List[Workflow]:
method get_timer_workflows (line 264) | def get_timer_workflows() -> List[Workflow]:
method get_event_workflows (line 271) | def get_event_workflows() -> List[Workflow]:
FILE: app/command.py
class CommandChain (line 26) | class CommandChain(ChainBase):
class Command (line 30) | class Command(metaclass=Singleton):
method __init__ (line 35) | def __init__(self):
method init_commands (line 167) | def init_commands(self, pid: Optional[str] = None) -> None:
method __init_commands_background (line 174) | def __init_commands_background(self, pid: Optional[str] = None) -> None:
method __trigger_register_commands_event (line 225) | def __trigger_register_commands_event(self) -> tuple[Optional[Event], ...
method __build_plugin_commands (line 260) | def __build_plugin_commands(self, _: Optional[str] = None) -> Dict[str...
method __run_command (line 282) | def __run_command(self, command: Dict[str, any], data_str: Optional[st...
method get_commands (line 336) | def get_commands(self):
method get (line 342) | def get(self, cmd: str) -> Any:
method register (line 348) | def register(self, cmd: str, func: Any, data: Optional[dict] = None,
method execute (line 363) | def execute(self, cmd: str, data_str: Optional[str] = "",
method send_plugin_event (line 392) | def send_plugin_event(etype: EventType, data: dict) -> None:
method command_event (line 399) | def command_event(self, event: ManagerEvent) -> None:
method module_reload_event (line 422) | def module_reload_event(self, _: ManagerEvent) -> None:
FILE: app/core/cache.py
class CacheBackend (line 34) | class CacheBackend(ABC):
method __getitem__ (line 39) | def __getitem__(self, key: str) -> Any:
method __setitem__ (line 48) | def __setitem__(self, key: str, value: Any) -> None:
method __delitem__ (line 54) | def __delitem__(self, key: str) -> None:
method __contains__ (line 62) | def __contains__(self, key: str) -> bool:
method __iter__ (line 68) | def __iter__(self):
method __len__ (line 75) | def __len__(self) -> int:
method set (line 82) | def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 96) | def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method get (line 107) | def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) ...
method delete (line 118) | def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method clear (line 128) | def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
method items (line 137) | def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Gener...
method keys (line 146) | def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Genera...
method values (line 153) | def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Gene...
method update (line 160) | def update(self, other: Dict[str, Any], region: Optional[str] = DEFAUL...
method pop (line 168) | def pop(self, key: str, default: Any = None, region: Optional[str] = D...
method popitem (line 180) | def popitem(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Tup...
method setdefault (line 191) | def setdefault(self, key: str, default: Any = None, region: Optional[s...
method close (line 203) | def close(self) -> None:
method get_region (line 210) | def get_region(region: Optional[str] = None) -> str:
method is_redis (line 217) | def is_redis() -> bool:
class AsyncCacheBackend (line 224) | class AsyncCacheBackend(CacheBackend):
method set (line 230) | async def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 244) | async def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method get (line 255) | async def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_RE...
method delete (line 266) | async def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method clear (line 276) | async def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method items (line 285) | async def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method keys (line 294) | async def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> ...
method values (line 301) | async def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -...
method update (line 308) | async def update(self, other: Dict[str, Any], region: Optional[str] = ...
method pop (line 316) | async def pop(self, key: str, default: Any = None, region: Optional[st...
method popitem (line 328) | async def popitem(self, region: Optional[str] = DEFAULT_CACHE_REGION) ...
method setdefault (line 341) | async def setdefault(self, key: str, default: Any = None, region: Opti...
method close (line 353) | async def close(self) -> None:
class MemoryBackend (line 360) | class MemoryBackend(CacheBackend):
method __init__ (line 370) | def __init__(self, cache_type: Literal['ttl', 'lru'] = 'ttl',
method __get_region_cache (line 383) | def __get_region_cache(self, region: str) -> Optional[Union[MemoryTTLC...
method set (line 390) | def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 413) | def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method get (line 426) | def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) ...
method delete (line 439) | def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION):
method clear (line 452) | def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
method items (line 472) | def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Gener...
method close (line 490) | def close(self) -> None:
class AsyncMemoryBackend (line 497) | class AsyncMemoryBackend(AsyncCacheBackend):
method __init__ (line 502) | def __init__(self, cache_type: Literal['ttl', 'lru'] = 'ttl',
method set (line 513) | async def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 525) | async def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method get (line 535) | async def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_RE...
method delete (line 545) | async def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method clear (line 554) | async def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method items (line 562) | async def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method close (line 572) | async def close(self) -> None:
class RedisBackend (line 579) | class RedisBackend(CacheBackend):
method __init__ (line 584) | def __init__(self, ttl: Optional[int] = None):
method set (line 593) | def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 607) | def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method get (line 617) | def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) ...
method delete (line 627) | def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method clear (line 636) | def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
method items (line 644) | def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Gener...
method close (line 653) | def close(self) -> None:
class AsyncRedisBackend (line 660) | class AsyncRedisBackend(AsyncCacheBackend):
method __init__ (line 665) | def __init__(self, ttl: Optional[int] = None):
method set (line 674) | async def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 688) | async def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method get (line 698) | async def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_RE...
method delete (line 708) | async def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method clear (line 717) | async def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method items (line 725) | async def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method close (line 735) | async def close(self) -> None:
class FileBackend (line 742) | class FileBackend(CacheBackend):
method __init__ (line 747) | def __init__(self, base: Path):
method set (line 755) | def set(self, key: str, value: Any, region: Optional[str] = DEFAULT_CA...
method exists (line 773) | def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method get (line 784) | def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) ...
method delete (line 798) | def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGIO...
method clear (line 809) | def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
method items (line 832) | def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Gener...
method close (line 848) | def close(self) -> None:
class AsyncFileBackend (line 855) | class AsyncFileBackend(AsyncCacheBackend):
method __init__ (line 860) | def __init__(self, base: Path):
method set (line 868) | async def set(self, key: str, value: Any, region: Optional[str] = DEFA...
method exists (line 886) | async def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method get (line 897) | async def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_RE...
method delete (line 911) | async def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE...
method clear (line 922) | async def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method items (line 945) | async def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) ->...
method close (line 961) | async def close(self) -> None:
function fresh (line 969) | def fresh(fresh: bool = True):
function async_fresh (line 984) | async def async_fresh(fresh: bool = True):
function is_fresh (line 998) | def is_fresh() -> bool:
function FileCache (line 1007) | def FileCache(base: Path = settings.TEMP_PATH, ttl: Optional[int] = None...
function AsyncFileCache (line 1019) | def AsyncFileCache(base: Path = settings.TEMP_PATH, ttl: Optional[int] =...
function Cache (line 1031) | def Cache(cache_type: Literal['ttl', 'lru'] = 'ttl',
function AsyncCache (line 1049) | def AsyncCache(cache_type: Literal['ttl', 'lru'] = 'ttl',
function cached (line 1067) | def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ...
class CacheProxy (line 1245) | class CacheProxy:
method __init__ (line 1250) | def __init__(self, cache_backend: CacheBackend, region: str):
method __getitem__ (line 1260) | def __getitem__(self, key):
method __setitem__ (line 1269) | def __setitem__(self, key, value):
method __delitem__ (line 1276) | def __delitem__(self, key):
method __contains__ (line 1284) | def __contains__(self, key):
method __iter__ (line 1290) | def __iter__(self):
method __len__ (line 1297) | def __len__(self):
method is_redis (line 1303) | def is_redis(self) -> bool:
method get (line 1309) | def get(self, key: str, **kwargs) -> Any:
method set (line 1316) | def set(self, key: str, value: Any, **kwargs) -> None:
method delete (line 1323) | def delete(self, key: str, **kwargs) -> None:
method exists (line 1330) | def exists(self, key: str, **kwargs) -> bool:
method clear (line 1337) | def clear(self, **kwargs) -> None:
method items (line 1344) | def items(self, **kwargs):
method keys (line 1351) | def keys(self, **kwargs):
method values (line 1358) | def values(self, **kwargs):
method update (line 1365) | def update(self, other: Dict[str, Any], **kwargs) -> None:
method pop (line 1372) | def pop(self, key: str, default: Any = None, **kwargs) -> Any:
method popitem (line 1379) | def popitem(self, **kwargs) -> Tuple[str, Any]:
method setdefault (line 1386) | def setdefault(self, key: str, default: Any = None, **kwargs) -> Any:
method close (line 1393) | def close(self) -> None:
class TTLCache (line 1400) | class TTLCache(CacheProxy):
method __init__ (line 1406) | def __init__(self,
class LRUCache (line 1420) | class LRUCache(CacheProxy):
method __init__ (line 1426) | def __init__(self,
FILE: app/core/config.py
class SystemConfModel (line 26) | class SystemConfModel(BaseModel):
class ConfigModel (line 50) | class ConfigModel(BaseModel):
class Settings (line 469) | class Settings(BaseSettings, ConfigModel, LogConfigModel):
method __init__ (line 480) | def __init__(self, **kwargs):
method validate_api_token (line 493) | def validate_api_token(value: Any, original_value: Any) -> Tuple[Any, ...
method generic_type_converter (line 510) | def generic_type_converter(value: Any, original_value: Any, expected_t...
method generic_type_validator (line 577) | def generic_type_validator(cls, data: Any): # noqa
method update_env_config (line 611) | def update_env_config(field_name: str, original_value: Any, converted_...
method update_setting (line 638) | def update_setting(self, key: str, value: Any) -> Tuple[Optional[bool]...
method update_settings (line 670) | def update_settings(self, env: Dict[str, Any]) -> Dict[str, Tuple[Opti...
method VERSION_FLAG (line 680) | def VERSION_FLAG(self) -> str:
method USER_AGENT (line 687) | def USER_AGENT(self) -> str:
method NORMAL_USER_AGENT (line 694) | def NORMAL_USER_AGENT(self) -> str:
method INNER_CONFIG_PATH (line 701) | def INNER_CONFIG_PATH(self):
method CONFIG_PATH (line 705) | def CONFIG_PATH(self):
method TEMP_PATH (line 715) | def TEMP_PATH(self):
method CACHE_PATH (line 719) | def CACHE_PATH(self):
method ROOT_PATH (line 723) | def ROOT_PATH(self):
method PLUGIN_DATA_PATH (line 727) | def PLUGIN_DATA_PATH(self):
method LOG_PATH (line 731) | def LOG_PATH(self):
method COOKIE_PATH (line 735) | def COOKIE_PATH(self):
method CONF (line 739) | def CONF(self) -> SystemConfModel:
method PROXY (line 768) | def PROXY(self):
method PROXY_SERVER (line 777) | def PROXY_SERVER(self):
method GITHUB_HEADERS (line 798) | def GITHUB_HEADERS(self):
method REPO_GITHUB_HEADERS (line 809) | def REPO_GITHUB_HEADERS(self, repo: str = None):
method VAPID (line 842) | def VAPID(self):
method MP_DOMAIN (line 849) | def MP_DOMAIN(self, url: str = None):
method RENAME_FORMAT (line 854) | def RENAME_FORMAT(self, media_type: MediaType):
method TMDB_IMAGE_URL (line 871) | def TMDB_IMAGE_URL(
class GlobalVar (line 892) | class GlobalVar(object):
method stop_system (line 907) | def stop_system(self):
method is_system_stopped (line 914) | def is_system_stopped(self):
method get_subscriptions (line 920) | def get_subscriptions(self):
method push_subscription (line 926) | def push_subscription(self, subscription: dict):
method stop_workflow (line 932) | def stop_workflow(self, workflow_id: int):
method workflow_resume (line 939) | def workflow_resume(self, workflow_id: int):
method is_workflow_stopped (line 946) | def is_workflow_stopped(self, workflow_id: int) -> bool:
method stop_transfer (line 952) | def stop_transfer(self, path: str):
method is_transfer_stopped (line 959) | def is_transfer_stopped(self, path: str) -> bool:
method loop (line 971) | def loop(self) -> AbstractEventLoop:
method set_loop (line 977) | def set_loop(self, loop: AbstractEventLoop):
FILE: app/core/context.py
class TorrentInfo (line 14) | class TorrentInfo:
method __setattr__ (line 66) | def __setattr__(self, name: str, value: Any):
method __get_properties (line 69) | def __get_properties(self):
method from_dict (line 80) | def from_dict(self, data: dict):
method get_free_string (line 91) | def get_free_string(upload_volume_factor: float, download_volume_facto...
method volume_factor (line 114) | def volume_factor(self):
method freedate_diff (line 121) | def freedate_diff(self):
method pub_minutes (line 129) | def pub_minutes(self) -> float:
method to_dict (line 143) | def to_dict(self):
class MediaInfo (line 154) | class MediaInfo:
method __post_init__ (line 276) | def __post_init__(self):
method __setattr__ (line 285) | def __setattr__(self, name: str, value: Any):
method __get_properties (line 288) | def __get_properties(self):
method from_dict (line 299) | def from_dict(self, data: dict):
method set_image (line 311) | def set_image(self, name: str, image: str):
method get_image (line 317) | def get_image(self, name: str):
method set_category (line 326) | def set_category(self, cat: str):
method set_tmdb_info (line 332) | def set_tmdb_info(self, info: dict):
method set_douban_info (line 505) | def set_douban_info(self, info: dict):
method set_bangumi_info (line 646) | def set_bangumi_info(self, info: dict):
method title_year (line 715) | def title_year(self):
method detail_link (line 721) | def detail_link(self):
method stars (line 737) | def stars(self):
method vote_star (line 746) | def vote_star(self):
method get_backdrop_image (line 751) | def get_backdrop_image(self, default: bool = False):
method get_message_image (line 759) | def get_message_image(self, default: Optional[bool] = None):
method get_poster_image (line 767) | def get_poster_image(self, default: Optional[bool] = None):
method get_overview_string (line 775) | def get_overview_string(self, max_len: Optional[int] = 140):
method to_dict (line 787) | def to_dict(self):
method clear (line 800) | def clear(self):
class Context (line 822) | class Context:
method to_dict (line 836) | def to_dict(self):
FILE: app/core/event.py
class Event (line 28) | class Event:
method __init__ (line 33) | def __init__(self, event_type: Union[EventType, ChainEventType],
method __repr__ (line 46) | def __repr__(self) -> str:
method __lt__ (line 53) | def __lt__(self, other):
method get_event_kind (line 61) | def get_event_kind(event_type: Union[EventType, ChainEventType]) -> str:
class EventManager (line 70) | class EventManager(metaclass=Singleton):
method __init__ (line 75) | def __init__(self):
method start (line 95) | def start(self):
method stop (line 106) | def stop(self):
method check (line 120) | def check(self, etype: Union[EventType, ChainEventType]) -> bool:
method send_event (line 139) | def send_event(self, etype: Union[EventType, ChainEventType], data: Op...
method async_send_event (line 157) | async def async_send_event(self, etype: Union[EventType, ChainEventType],
method add_event_listener (line 176) | def add_event_listener(self, event_type: Union[EventType, ChainEventTy...
method remove_event_listener (line 214) | def remove_event_listener(self, event_type: Union[EventType, ChainEven...
method disable_event_handler (line 230) | def disable_event_handler(self, target: Union[Callable, type]):
method enable_event_handler (line 245) | def enable_event_handler(self, target: Union[Callable, type]):
method visualize_handlers (line 258) | def visualize_handlers(self) -> List[Dict]:
method __get_handler_identifier (line 294) | def __get_handler_identifier(cls, target: Union[Callable, type]) -> Op...
method __get_class_from_callable (line 309) | def __get_class_from_callable(cls, handler: Callable) -> Optional[str]:
method __is_handler_enabled (line 333) | def __is_handler_enabled(self, handler: Callable) -> bool:
method __trigger_chain_event (line 351) | def __trigger_chain_event(self, event: Event) -> Optional[Event]:
method __trigger_chain_event_async (line 359) | async def __trigger_chain_event_async(self, event: Event) -> Optional[...
method __trigger_broadcast_event (line 367) | def __trigger_broadcast_event(self, event: Event):
method __dispatch_chain_event (line 375) | def __dispatch_chain_event(self, event: Event) -> bool:
method __dispatch_chain_event_async (line 404) | async def __dispatch_chain_event_async(self, event: Event) -> bool:
method __dispatch_broadcast_event (line 433) | def __dispatch_broadcast_event(self, event: Event):
method __safe_invoke_handler (line 462) | def __safe_invoke_handler(self, handler: Callable, event: Event):
method __safe_invoke_handler_async (line 474) | async def __safe_invoke_handler_async(self, handler: Callable, event: ...
method __invoke_handler_by_type_sync (line 486) | def __invoke_handler_by_type_sync(self, handler: Callable, event: Event):
method __invoke_handler_by_type_async (line 540) | async def __invoke_handler_by_type_async(self, handler: Callable, even...
method __parse_handler_names (line 562) | def __parse_handler_names(handler: Callable) -> Tuple[str, str]:
method __invoke_plugin_method_async (line 571) | async def __invoke_plugin_method_async(self, handler: Any, class_name:...
method __invoke_module_method_async (line 591) | async def __invoke_module_method_async(self, handler: Any, class_name:...
method __invoke_global_method_async (line 610) | async def __invoke_global_method_async(self, class_name: str, method_n...
method __get_class_instance (line 630) | def __get_class_instance(class_name: str):
method __broadcast_consumer_loop (line 672) | def __broadcast_consumer_loop(self):
method __log_event_lifecycle (line 692) | def __log_event_lifecycle(event: Event, stage: str):
method __handle_event_error (line 698) | def __handle_event_error(self, event: Event, module_name: str,
method register (line 721) | def register(self, etype: Union[EventType, ChainEventType, List[Union[...
FILE: app/core/meta/customization.py
class CustomizationMatcher (line 8) | class CustomizationMatcher(metaclass=Singleton):
method __init__ (line 13) | def __init__(self):
method match (line 18) | def match(self, title=None):
FILE: app/core/meta/metaanime.py
class MetaAnime (line 14) | class MetaAnime(MetaBase):
method __init__ (line 22) | def __init__(self, title: str, subtitle: str = None, isfile: bool = Fa...
method __init_anime_fps (line 188) | def __init_anime_fps(self, anitopy_info: dict, original_title: str):
method __prepare_title (line 203) | def __prepare_title(title: str):
FILE: app/core/meta/metabase.py
class MetaBase (line 14) | class MetaBase(object):
method __init__ (line 82) | def __init__(self, title: str, subtitle: str = None, isfile: bool = Fa...
method name (line 90) | def name(self) -> str:
method name (line 103) | def name(self, name: str):
method init_subtitle (line 113) | def init_subtitle(self, title_text: str):
method season (line 272) | def season(self) -> str:
method sea (line 289) | def sea(self) -> str:
method season_seq (line 299) | def season_seq(self) -> str:
method season_list (line 312) | def season_list(self) -> List[int]:
method episode (line 327) | def episode(self) -> str:
method episode_list (line 342) | def episode_list(self) -> List[int]:
method episodes (line 354) | def episodes(self) -> str:
method episode_seqs (line 361) | def episode_seqs(self) -> str:
method episode_seq (line 376) | def episode_seq(self) -> str:
method season_episode (line 387) | def season_episode(self) -> str:
method resource_term (line 405) | def resource_term(self) -> str:
method edition (line 419) | def edition(self) -> str:
method release_group (line 431) | def release_group(self) -> str:
method video_term (line 441) | def video_term(self) -> str:
method audio_term (line 448) | def audio_term(self) -> str:
method frame_rate (line 455) | def frame_rate(self) -> int:
method is_in_season (line 461) | def is_in_season(self, season: Union[list, int, str]) -> bool:
method is_in_episode (line 484) | def is_in_episode(self, episode: Union[list, int, str]) -> bool:
method set_season (line 500) | def set_season(self, sea: Union[list, int, str]):
method set_episode (line 517) | def set_episode(self, ep: Union[list, int, str]):
method set_episodes (line 535) | def set_episodes(self, begin: int, end: int):
method merge (line 546) | def merge(self, meta: Self):
method to_dict (line 607) | def to_dict(self):
FILE: app/core/meta/metavideo.py
class MetaVideo (line 16) | class MetaVideo(MetaBase):
method __init__ (line 57) | def __init__(self, title: str, subtitle: str = None, isfile: bool = Fa...
method __get_title_from_description (line 173) | def __get_title_from_description(description: str) -> Optional[str]:
method __is_pinyin (line 185) | def __is_pinyin(name_str: Optional[str]) -> bool:
method __fix_name (line 196) | def __fix_name(self, name: Optional[str]):
method __init_name (line 220) | def __init_name(self, token: Optional[str]):
method __init_part (line 317) | def __init_part(self, token: str, tokens: Tokens):
method __init_year (line 343) | def __init_year(self, token: str):
method __init_resource_pix (line 368) | def __init_resource_pix(self, token: str):
method __init_season (line 407) | def __init_season(self, token: str):
method __init_episode (line 459) | def __init_episode(self, token: str):
method __init_resource_type (line 532) | def __init_resource_type(self, token):
method __init_web_source (line 588) | def __init_web_source(self, token: str, tokens: Tokens, streaming_plat...
method __init_video_encode (line 639) | def __init_video_encode(self, token: str):
method __init_audio_encode (line 687) | def __init_audio_encode(self, token: str):
method __init_fps (line 723) | def __init_fps(self, token: str):
FILE: app/core/meta/releasegroup.py
class ReleaseGroupsMatcher (line 8) | class ReleaseGroupsMatcher(metaclass=Singleton):
method __init__ (line 83) | def __init__(self):
method match (line 90) | def match(self, title: str = None, groups: str = None):
FILE: app/core/meta/streamingplatform.py
class StreamingPlatforms (line 6) | class StreamingPlatforms(metaclass=Singleton):
method __init__ (line 280) | def __init__(self):
method _build_cache (line 285) | def _build_cache(self) -> None:
method get_streaming_platform_name (line 300) | def get_streaming_platform_name(self, platform_code: str) -> Optional[...
method is_streaming_platform (line 308) | def is_streaming_platform(self, name: str) -> bool:
FILE: app/core/meta/words.py
class WordsMatcher (line 12) | class WordsMatcher(metaclass=Singleton):
method __init__ (line 14) | def __init__(self):
method prepare (line 17) | def prepare(self, title: str, custom_words: List[str] = None) -> Tuple...
method __replace_regex (line 72) | def __replace_regex(title: str, replaced: str, replace: str) -> Tuple[...
method __episode_offset (line 86) | def __episode_offset(title: str, front: str, back: str, offset: str) -...
FILE: app/core/metainfo.py
function MetaInfo (line 13) | def MetaInfo(title: str, subtitle: Optional[str] = None, custom_words: L...
function MetaInfoPath (line 66) | def MetaInfoPath(path: Path, custom_words: List[str] = None) -> MetaBase:
function is_anime (line 87) | def is_anime(name: str) -> bool:
function find_metainfo (line 108) | def find_metainfo(title: str) -> Tuple[str, dict]:
FILE: app/core/module.py
class ModuleManager (line 14) | class ModuleManager(metaclass=Singleton):
method __init__ (line 22) | def __init__(self):
method load_modules (line 29) | def load_modules(self):
method stop (line 55) | def stop(self):
method reload (line 69) | def reload(self):
method test (line 77) | def test(self, modleid: str) -> Tuple[bool, str]:
method check_setting (line 93) | def check_setting(setting: Optional[tuple]) -> bool:
method get_running_module (line 109) | def get_running_module(self, module_id: str) -> Any:
method get_running_modules (line 119) | def get_running_modules(self, method: str) -> Generator:
method get_running_type_modules (line 130) | def get_running_type_modules(self, module_type: ModuleType) -> Generator:
method get_running_subtype_module (line 141) | def get_running_subtype_module(self, module_subtype: SubType) -> Gener...
method get_module (line 152) | def get_module(self, module_id: str) -> Any:
method get_modules (line 162) | def get_modules(self) -> dict:
method get_module_ids (line 168) | def get_module_ids(self) -> List[str]:
FILE: app/core/plugin.py
class PluginManager (line 39) | class PluginManager(ConfigReloadMixin, metaclass=Singleton):
method __init__ (line 43) | def __init__(self):
method init_config (line 58) | def init_config(self):
method start (line 64) | def start(self, pid: Optional[str] = None):
method init_plugin (line 112) | def init_plugin(self, plugin_id: str, conf: dict):
method stop (line 131) | def stop(self, pid: Optional[str] = None):
method _load_selective_plugins (line 166) | def _load_selective_plugins(pid: Optional[str], installed_plugins: Lis...
method running_plugins (line 239) | def running_plugins(self) -> Dict[str, Any]:
method plugins (line 247) | def plugins(self) -> Dict[str, Any]:
method on_config_changed (line 254) | def on_config_changed(self):
method get_reload_name (line 257) | def get_reload_name(self) -> str:
method reload_monitor (line 260) | def reload_monitor(self):
method __start_monitor (line 271) | def __start_monitor(self):
method stop_monitor (line 291) | def stop_monitor(self):
method _run_file_watcher (line 306) | def _run_file_watcher(self):
method _get_plugin_id_from_path (line 347) | def _get_plugin_id_from_path(event_path: Path) -> Optional[str]:
method __stop_plugin (line 393) | def __stop_plugin(plugin: Any):
method remove_plugin (line 408) | def remove_plugin(self, plugin_id: str):
method reload_plugin (line 415) | def reload_plugin(self, plugin_id: str):
method _clear_plugin_modules (line 428) | def _clear_plugin_modules(plugin_id: Optional[str] = None):
method sync (line 464) | def sync(self) -> List[str]:
method install_plugin_missing_dependencies (line 523) | def install_plugin_missing_dependencies() -> List[str]:
method get_plugin_config (line 544) | def get_plugin_config(self, pid: str) -> dict:
method save_plugin_config (line 557) | def save_plugin_config(self, pid: str, conf: dict, force: bool = False...
method delete_plugin_config (line 569) | def delete_plugin_config(self, pid: str) -> bool:
method delete_plugin_data (line 578) | def delete_plugin_data(self, pid: str) -> bool:
method get_plugin_state (line 588) | def get_plugin_state(self, pid: str) -> bool:
method get_plugin_commands (line 596) | def get_plugin_commands(self, pid: Optional[str] = None) -> List[Dict[...
method get_plugin_apis (line 625) | def get_plugin_apis(self, pid: Optional[str] = None) -> List[Dict[str,...
method get_plugin_services (line 657) | def get_plugin_services(self, pid: Optional[str] = None) -> List[Dict[...
method get_plugin_modules (line 685) | def get_plugin_modules(self, pid: Optional[str] = None) -> Dict[tuple,...
method get_plugin_actions (line 710) | def get_plugin_actions(self, pid: Optional[str] = None) -> List[Dict[s...
method get_plugin_agent_tools (line 741) | def get_plugin_agent_tools(self, pid: Optional[str] = None) -> List[Di...
method get_plugin_remote_entry (line 772) | def get_plugin_remote_entry(plugin_id: str, dist_path: str) -> str:
method get_plugin_remotes (line 791) | def get_plugin_remotes(self, pid: Optional[str] = None) -> List[Dict[s...
method get_plugin_dashboard_meta (line 812) | def get_plugin_dashboard_meta(self) -> List[Dict[str, str]]:
method get_plugin_dashboard (line 844) | def get_plugin_dashboard(self, pid: str, key: str, user_agent: str = N...
method get_plugin_attr (line 888) | def get_plugin_attr(self, pid: str, attr: str) -> Any:
method run_plugin_method (line 901) | def run_plugin_method(self, pid: str, method: str, *args, **kwargs) ->...
method async_run_plugin_method (line 916) | async def async_run_plugin_method(self, pid: str, method: str, *args, ...
method get_plugin_ids (line 935) | def get_plugin_ids(self) -> List[str]:
method get_running_plugin_ids (line 941) | def get_running_plugin_ids(self) -> List[str]:
method get_online_plugins (line 947) | def get_online_plugins(self, force: bool = False) -> List[schemas.Plug...
method get_local_plugins (line 988) | def get_local_plugins(self) -> List[schemas.Plugin]:
method is_plugin_exists (line 1062) | def is_plugin_exists(pid: str, version: str = None) -> bool:
method get_plugins_from_market (line 1093) | def get_plugins_from_market(self, market: str,
method _process_plugins_list (line 1125) | def _process_plugins_list(higher_version_plugins: List[schemas.Plugin],
method _process_plugin_info (line 1154) | def _process_plugin_info(self, pid: str, plugin_info: dict, market: str,
method async_get_online_plugins (line 1247) | async def async_get_online_plugins(self, force: bool = False) -> List[...
method async_get_plugins_from_market (line 1301) | async def async_get_plugins_from_market(self, market: str,
method __set_and_check_auth_level (line 1333) | def __set_and_check_auth_level(plugin: Union[schemas.Plugin, Type[Any]],
method __get_plugin_private_key (line 1371) | def __get_plugin_private_key(plugin_id: str) -> Optional[str]:
method clone_plugin (line 1386) | def clone_plugin(self, plugin_id: str, suffix: str, name: str, descrip...
method _modify_plugin_files (line 1485) | def _modify_plugin_files(self, plugin_dir: Path, original_id: str, suf...
method _modify_python_file (line 1542) | def _modify_python_file(file_path: Path, original_class_name: str,
method _modify_federation_files (line 1620) | def _modify_federation_files(self, dist_dir: Path, original_class_name...
method _rename_federation_assets (line 1690) | def _rename_federation_assets(dist_dir: Path, original_class_name: str...
FILE: app/core/security.py
function __get_api_token (line 46) | def __get_api_token(
function __get_api_key (line 57) | def __get_api_key(
function __create_superuser_token_payload (line 71) | def __create_superuser_token_payload() -> schemas.TokenPayload:
function create_access_token (line 98) | def create_access_token(
function __set_or_refresh_resource_token_cookie (line 145) | def __set_or_refresh_resource_token_cookie(request: Request, response: R...
function __verify_token (line 193) | def __verify_token(token: str, purpose: Optional[str] = "authentication"...
function verify_token (line 230) | def verify_token(
function verify_resource_token (line 272) | def verify_resource_token(
function __verify_key (line 285) | def __verify_key(key: str | None, expected_key: str, key_type: str) -> str:
function verify_apitoken (line 302) | def verify_apitoken(token: Annotated[str | None, Security(__get_api_toke...
function verify_apikey (line 311) | def verify_apikey(apikey: Annotated[str | None, Security(__get_api_key)]...
function verify_password (line 320) | def verify_password(plain_password: str, hashed_password: str) -> bool:
function get_password_hash (line 324) | def get_password_hash(password: str) -> str:
function decrypt (line 328) | def decrypt(data: bytes, key: bytes) -> Optional[bytes]:
function encrypt_message (line 340) | def encrypt_message(message: str, key: bytes) -> str:
function hash_sha256 (line 349) | def hash_sha256(message: str) -> str:
function aes_decrypt (line 356) | def aes_decrypt(data: str, key: str) -> str:
function aes_encrypt (line 376) | def aes_encrypt(data: str, key: str) -> str:
function nexusphp_encrypt (line 392) | def nexusphp_encrypt(data_str: str, key: bytes) -> str:
FILE: app/db/__init__.py
function get_id_column (line 12) | def get_id_column():
function _get_database_engine (line 24) | def _get_database_engine(is_async: bool = False):
function _get_sqlite_engine (line 37) | def _get_sqlite_engine(is_async: bool = False):
function _get_postgresql_engine (line 115) | def _get_postgresql_engine(is_async: bool = False):
function get_db (line 192) | def get_db() -> Generator:
function get_async_db (line 206) | async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
function close_database (line 218) | async def close_database():
function _get_args_db (line 231) | def _get_args_db(args: tuple, kwargs: dict) -> Optional[Session]:
function _get_args_async_db (line 249) | def _get_args_async_db(args: tuple, kwargs: dict) -> Optional[AsyncSessi...
function _update_args_db (line 267) | def _update_args_db(args: tuple, kwargs: dict, db: Session) -> Tuple[tup...
function _update_args_async_db (line 281) | def _update_args_async_db(args: tuple, kwargs: dict, db: AsyncSession) -...
function db_update (line 295) | def db_update(func):
function async_db_update (line 330) | def async_db_update(func):
function db_query (line 365) | def db_query(func):
function async_db_query (line 397) | def async_db_query(func):
class Base (line 430) | class Base:
method create (line 435) | def create(self, db: Session):
method async_create (line 439) | async def async_create(self, db: AsyncSession):
method get (line 446) | def get(cls, db: Session, rid: int) -> Self:
method async_get (line 451) | async def async_get(cls, db: AsyncSession, rid: int) -> Self:
method update (line 456) | def update(self, db: Session, payload: dict):
method async_update (line 463) | async def async_update(self, db: AsyncSession, payload: dict):
method delete (line 471) | def delete(cls, db: Session, rid):
method async_delete (line 476) | async def async_delete(cls, db: AsyncSession, rid):
method truncate (line 484) | def truncate(cls, db: Session):
method async_truncate (line 489) | async def async_truncate(cls, db: AsyncSession):
method list (line 494) | def list(cls, db: Session) -> List[Self]:
method async_list (line 499) | async def async_list(cls, db: AsyncSession) -> Sequence[Self]:
method to_dict (line 503) | def to_dict(self):
method __tablename__ (line 507) | def __tablename__(self) -> str:
class DbOper (line 511) | class DbOper:
method __init__ (line 516) | def __init__(self, db: Union[Session, AsyncSession] = None):
FILE: app/db/downloadhistory_oper.py
class DownloadHistoryOper (line 7) | class DownloadHistoryOper(DbOper):
method get_by_path (line 12) | def get_by_path(self, path: str) -> DownloadHistory:
method get_by_hash (line 19) | def get_by_hash(self, download_hash: str) -> DownloadHistory:
method get_by_mediaid (line 26) | def get_by_mediaid(self, tmdbid: int, doubanid: str) -> List[DownloadH...
method add (line 34) | def add(self, **kwargs):
method add_files (line 40) | def add_files(self, file_items: List[dict]):
method truncate_files (line 48) | def truncate_files(self):
method get_files_by_hash (line 54) | def get_files_by_hash(self, download_hash: str, state: Optional[int] =...
method get_file_by_fullpath (line 62) | def get_file_by_fullpath(self, fullpath: str) -> DownloadFiles:
method get_files_by_fullpath (line 69) | def get_files_by_fullpath(self, fullpath: str) -> List[DownloadFiles]:
method get_files_by_savepath (line 76) | def get_files_by_savepath(self, fullpath: str) -> List[DownloadFiles]:
method delete_file_by_fullpath (line 83) | def delete_file_by_fullpath(self, fullpath: str):
method get_hash_by_fullpath (line 90) | def get_hash_by_fullpath(self, fullpath: str) -> str:
method list_by_page (line 100) | def list_by_page(self, page: Optional[int] = 1, count: Optional[int] =...
method truncate (line 106) | def truncate(self):
method get_last_by (line 112) | def get_last_by(self, mtype=None, title: Optional[str] = None, year: O...
method list_by_user_date (line 126) | def list_by_user_date(self, date: str, username: Optional[str] = None)...
method list_by_date (line 134) | def list_by_date(self, date: str, type: str, tmdbid: str, seasons: Opt...
method list_by_type (line 144) | def list_by_type(self, mtype: str, days: Optional[int] = 7) -> List[Do...
method delete_history (line 152) | def delete_history(self, historyid):
method delete_downloadfile (line 158) | def delete_downloadfile(self, downloadfileid):
FILE: app/db/init.py
function init_db (line 9) | def init_db():
function update_db (line 17) | def update_db():
FILE: app/db/mediaserver_oper.py
class MediaServerOper (line 9) | class MediaServerOper(DbOper):
method __init__ (line 14) | def __init__(self, db: Session = None):
method add (line 17) | def add(self, **kwargs) -> bool:
method empty (line 29) | def empty(self, server: Optional[str] = None):
method exists (line 35) | def exists(self, **kwargs) -> Optional[MediaServerItem]:
method async_exists (line 61) | async def async_exists(self, **kwargs) -> Optional[MediaServerItem]:
method get_item_id (line 87) | def get_item_id(self, **kwargs) -> Optional[str]:
method async_get_item_id (line 96) | async def async_get_item_id(self, **kwargs) -> Optional[str]:
FILE: app/db/message_oper.py
class MessageOper (line 11) | class MessageOper(DbOper):
method __init__ (line 16) | def __init__(self, db: Session = None):
method add (line 19) | def add(self,
method async_add (line 65) | async def async_add(self,
method list_by_page (line 101) | def list_by_page(self, page: Optional[int] = 1, count: Optional[int] =...
FILE: app/db/models/downloadhistory.py
class DownloadHistory (line 11) | class DownloadHistory(Base):
method get_by_hash (line 63) | def get_by_hash(cls, db: Session, download_hash: str):
method get_by_mediaid (line 70) | def get_by_mediaid(cls, db: Session, tmdbid: int, doubanid: str):
method list_by_page (line 79) | def list_by_page(cls, db: Session, page: Optional[int] = 1, count: Opt...
method async_list_by_page (line 84) | async def async_list_by_page(cls, db: AsyncSession, page: Optional[int...
method get_by_path (line 92) | def get_by_path(cls, db: Session, path: str):
method get_last_by (line 97) | def get_last_by(cls, db: Session, mtype: Optional[str] = None, title: ...
method list_by_user_date (line 149) | def list_by_user_date(cls, db: Session, date: str, username: Optional[...
method list_by_date (line 163) | def list_by_date(cls, db: Session, date: str, type: str, tmdbid: str, ...
method list_by_type (line 181) | def list_by_type(cls, db: Session, mtype: str, days: int):
class DownloadFiles (line 189) | class DownloadFiles(Base):
method get_by_hash (line 211) | def get_by_hash(cls, db: Session, download_hash: str, state: Optional[...
method get_by_fullpath (line 220) | def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool =...
method get_by_savepath (line 230) | def get_by_savepath(cls, db: Session, savepath: str):
method delete_by_fullpath (line 235) | def delete_by_fullpath(cls, db: Session, fullpath: str):
FILE: app/db/models/mediaserver.py
class MediaServerItem (line 12) | class MediaServerItem(Base):
method get_by_itemid (line 48) | def get_by_itemid(cls, db: Session, item_id: str):
method empty (line 53) | def empty(cls, db: Session, server: Optional[str] = None):
method exist_by_tmdbid (line 61) | def exist_by_tmdbid(cls, db: Session, tmdbid: int, mtype: str):
method exists_by_title (line 67) | def exists_by_title(cls, db: Session, title: str, mtype: str, year: str):
method async_get_by_itemid (line 82) | async def async_get_by_itemid(cls, db: AsyncSession, item_id: str):
method async_exist_by_tmdbid (line 88) | async def async_exist_by_tmdbid(cls, db: AsyncSession, tmdbid: int, mt...
method async_exists_by_title (line 95) | async def async_exists_by_title(cls, db: AsyncSession, title: str, mty...
FILE: app/db/models/message.py
class Message (line 10) | class Message(Base):
method list_by_page (line 40) | def list_by_page(cls, db: Session, page: Optional[int] = 1, count: Opt...
method async_list_by_page (line 45) | async def async_list_by_page(cls, db: AsyncSession, page: Optional[int...
FILE: app/db/models/passkey.py
class PassKey (line 9) | class PassKey(Base):
method get_by_user_id (line 38) | def get_by_user_id(cls, db: Session, user_id: int):
method async_get_by_user_id (line 44) | async def async_get_by_user_id(cls, db: AsyncSession, user_id: int):
method get_by_credential_id (line 53) | def get_by_credential_id(cls, db: Session, credential_id: str):
method async_get_by_credential_id (line 59) | async def async_get_by_credential_id(cls, db: AsyncSession, credential...
method get_by_id (line 68) | def get_by_id(cls, db: Session, passkey_id: int):
method async_get_by_id (line 74) | async def async_get_by_id(cls, db: AsyncSession, passkey_id: int):
method delete_by_id (line 83) | def delete_by_id(cls, db: Session, passkey_id: int, user_id: int):
method async_delete_by_id (line 96) | async def async_delete_by_id(cls, db: AsyncSession, passkey_id: int, u...
method update_last_used (line 111) | def update_last_used(self, db: Session, sign_count: int):
method async_update_last_used (line 120) | async def async_update_last_used(self, db: AsyncSession, sign_count: i...
FILE: app/db/models/plugindata.py
class PluginData (line 7) | class PluginData(Base):
method get_plugin_data (line 18) | def get_plugin_data(cls, db: Session, plugin_id: str):
method get_plugin_data_by_key (line 23) | def get_plugin_data_by_key(cls, db: Session, plugin_id: str, key: str):
method del_plugin_data_by_key (line 28) | def del_plugin_data_by_key(cls, db: Session, plugin_id: str, key: str):
method del_plugin_data (line 33) | def del_plugin_data(cls, db: Session, plugin_id: str):
method get_plugin_data_by_plugin_id (line 38) | def get_plugin_data_by_plugin_id(cls, db: Session, plugin_id: str):
FILE: app/db/models/site.py
class Site (line 10) | class Site(Base):
method get_by_domain (line 60) | def get_by_domain(cls, db: Session, domain: str):
method async_get_by_domain (line 65) | async def async_get_by_domain(cls, db: AsyncSession, domain: str):
method async_get_by_name (line 71) | async def async_get_by_name(cls, db: AsyncSession, name: str):
method get_actives (line 77) | def get_actives(cls, db: Session):
method async_get_actives (line 82) | async def async_get_actives(cls, db: AsyncSession):
method list_order_by_pri (line 88) | def list_order_by_pri(cls, db: Session):
method async_list_order_by_pri (line 93) | async def async_list_order_by_pri(cls, db: AsyncSession):
method get_domains_by_ids (line 99) | def get_domains_by_ids(cls, db: Session, ids: list):
method reset (line 104) | def reset(cls, db: Session):
method async_reset (line 109) | async def async_reset(cls, db: AsyncSession):
FILE: app/db/models/siteicon.py
class SiteIcon (line 8) | class SiteIcon(Base):
method get_by_domain (line 24) | def get_by_domain(cls, db: Session, domain: str):
method async_get_by_domain (line 29) | async def async_get_by_domain(cls, db: AsyncSession, domain: str):
FILE: app/db/models/sitestatistic.py
class SiteStatistic (line 10) | class SiteStatistic(Base):
method get_by_domain (line 32) | def get_by_domain(cls, db: Session, domain: str):
method async_get_by_domain (line 37) | async def async_get_by_domain(cls, db: AsyncSession, domain: str):
method reset (line 43) | def reset(cls, db: Session):
FILE: app/db/models/siteuserdata.py
class SiteUserData (line 11) | class SiteUserData(Base):
method get_by_domain (line 59) | def get_by_domain(cls, db: Session, domain: str, workdate: Optional[st...
method async_get_by_domain (line 71) | async def async_get_by_domain(cls, db: AsyncSession, domain: str, work...
method get_by_date (line 82) | def get_by_date(cls, db: Session, date: str):
method get_latest (line 87) | def get_latest(cls, db: Session):
method async_get_latest (line 110) | async def async_get_latest(cls, db: AsyncSession):
FILE: app/db/models/subscribe.py
class Subscribe (line 11) | class Subscribe(Base):
method exists (line 93) | def exists(cls, db: Session, tmdbid: Optional[int] = None, doubanid: O...
method async_exists (line 106) | async def async_exists(cls, db: AsyncSession, tmdbid: Optional[int] = ...
method get_by_state (line 127) | def get_by_state(cls, db: Session, state: str):
method async_get_by_state (line 137) | async def async_get_by_state(cls, db: AsyncSession, state: str):
method get_by_title (line 150) | def get_by_title(cls, db: Session, title: str, season: Optional[int] =...
method async_get_by_title (line 158) | async def async_get_by_title(cls, db: AsyncSession, title: str, season...
method get_by_tmdbid (line 171) | def get_by_tmdbid(cls, db: Session, tmdbid: int, season: Optional[int]...
method async_get_by_tmdbid (line 180) | async def async_get_by_tmdbid(cls, db: AsyncSession, tmdbid: int, seas...
method get_by_doubanid (line 193) | def get_by_doubanid(cls, db: Session, doubanid: str):
method async_get_by_doubanid (line 198) | async def async_get_by_doubanid(cls, db: AsyncSession, doubanid: str):
method get_by_bangumiid (line 206) | def get_by_bangumiid(cls, db: Session, bangumiid: int):
method async_get_by_bangumiid (line 211) | async def async_get_by_bangumiid(cls, db: AsyncSession, bangumiid: int):
method get_by_mediaid (line 219) | def get_by_mediaid(cls, db: Session, mediaid: str):
method async_get_by_mediaid (line 224) | async def async_get_by_mediaid(cls, db: AsyncSession, mediaid: str):
method get_by (line 232) | def get_by(cls, db: Session, type: str, season: Optional[str] = None,
method async_get_by (line 258) | async def async_get_by(cls, db: AsyncSession, type: str, season: Optio...
method delete_by_tmdbid (line 291) | def delete_by_tmdbid(self, db: Session, tmdbid: int, season: int):
method async_delete_by_tmdbid (line 298) | async def async_delete_by_tmdbid(self, db: AsyncSession, tmdbid: int, ...
method delete_by_doubanid (line 305) | def delete_by_doubanid(self, db: Session, doubanid: str):
method async_delete_by_doubanid (line 312) | async def async_delete_by_doubanid(self, db: AsyncSession, doubanid: s...
method delete_by_mediaid (line 319) | def delete_by_mediaid(self, db: Session, mediaid: str):
method async_delete_by_mediaid (line 326) | async def async_delete_by_mediaid(self, db: AsyncSession, mediaid: str):
method list_by_username (line 334) | def list_by_username(cls, db: Session, username: str, state: Optional[...
method async_list_by_username (line 352) | async def async_list_by_username(cls, db: AsyncSession, username: str,...
method list_by_type (line 376) | def list_by_type(cls, db: Session, mtype: str, days: int):
method async_list_by_type (line 385) | async def async_list_by_type(cls, db: AsyncSession, mtype: str, days: ...
FILE: app/db/models/subscribehistory.py
class SubscribeHistory (line 10) | class SubscribeHistory(Base):
method list_by_type (line 78) | def list_by_type(cls, db: Session, mtype: str, page: Optional[int] = 1...
method async_list_by_type (line 87) | async def async_list_by_type(cls, db: AsyncSession, mtype: str, page: ...
method exists (line 99) | def exists(cls, db: Session, tmdbid: Optional[int] = None, doubanid: O...
method async_exists (line 112) | async def async_exists(cls, db: AsyncSession, tmdbid: Optional[int] = ...
FILE: app/db/models/systemconfig.py
class SystemConfig (line 8) | class SystemConfig(Base):
method get_by_key (line 20) | def get_by_key(cls, db: Session, key: str):
method async_get_by_key (line 25) | async def async_get_by_key(cls, db: AsyncSession, key: str):
method delete_by_key (line 30) | def delete_by_key(self, db: Session, key: str):
FILE: app/db/models/transferhistory.py
class TransferHistory (line 11) | class TransferHistory(Base):
method list_by_title (line 65) | def list_by_title(cls, db: Session, title: str, page: Optional[int] = ...
method async_list_by_title (line 90) | async def async_list_by_title(cls, db: AsyncSession, title: str, page:...
method list_by_page (line 116) | def list_by_page(cls, db: Session, page: Optional[int] = 1, count: Opt...
method async_list_by_page (line 136) | async def async_list_by_page(cls, db: AsyncSession, page: Optional[int...
method get_by_hash (line 158) | def get_by_hash(cls, db: Session, download_hash: str):
method get_by_src (line 163) | def get_by_src(cls, db: Session, src: str, storage: Optional[str] = No...
method get_by_dest (line 172) | def get_by_dest(cls, db: Session, dest: str):
method list_by_hash (line 177) | def list_by_hash(cls, db: Session, download_hash: str):
method statistic (line 182) | def statistic(cls, db: Session, days: Optional[int] = 7):
method async_statistic (line 194) | async def async_statistic(cls, db: AsyncSession, days: Optional[int] =...
method count (line 209) | def count(cls, db: Session, status: bool = None):
method async_count (line 217) | async def async_count(cls, db: AsyncSession, status: bool = None):
method count_by_title (line 230) | def count_by_title(cls, db: Session, title: str, status: bool = None):
method async_count_by_title (line 242) | async def async_count_by_title(cls, db: AsyncSession, title: str, stat...
method list_by (line 259) | def list_by(cls, db: Session, mtype: Optional[str] = None, title: Opti...
method get_by_type_tmdbid (line 324) | def get_by_type_tmdbid(cls, db: Session, mtype: Optional[str] = None, ...
method update_download_hash (line 333) | def update_download_hash(cls, db: Session, historyid: Optional[int] = ...
method list_by_date (line 342) | def list_by_date(cls, db: Session, date: str):
FILE: app/db/models/user.py
class User (line 8) | class User(Base):
method get_by_name (line 37) | def get_by_name(cls, db: Session, name: str):
method async_get_by_name (line 42) | async def async_get_by_name(cls, db: AsyncSession, name: str):
method get_by_id (line 50) | def get_by_id(cls, db: Session, user_id: int):
method async_get_by_id (line 55) | async def async_get_by_id(cls, db: AsyncSession, user_id: int):
method delete_by_name (line 62) | def delete_by_name(self, db: Session, name: str):
method async_delete_by_name (line 69) | async def async_delete_by_name(self, db: AsyncSession, name: str):
method delete_by_id (line 76) | def delete_by_id(self, db: Session, user_id: int):
method async_delete_by_id (line 83) | async def async_delete_by_id(self, db: AsyncSession, user_id: int):
method update_otp_by_name (line 90) | def update_otp_by_name(self, db: Session, name: str, otp: bool, secret...
method async_update_otp_by_name (line 101) | async def async_update_otp_by_name(self, db: AsyncSession, name: str, ...
FILE: app/db/models/userconfig.py
class UserConfig (line 7) | class UserConfig(Base):
method get_by_key (line 27) | def get_by_key(cls, db: Session, username: str, key: str):
method delete_by_key (line 34) | def delete_by_key(self, db: Session, username: str, key: str):
FILE: app/db/models/workflow.py
class Workflow (line 10) | class Workflow(Base):
method list (line 49) | def list(cls, db):
method async_list (line 54) | async def async_list(cls, db: AsyncSession):
method get_enabled_workflows (line 60) | def get_enabled_workflows(cls, db):
method async_get_enabled_workflows (line 65) | async def async_get_enabled_workflows(cls, db: AsyncSession):
method get_timer_triggered_workflows (line 71) | def get_timer_triggered_workflows(cls, db):
method async_get_timer_triggered_workflows (line 85) | async def async_get_timer_triggered_workflows(cls, db: AsyncSession):
method get_event_triggered_workflows (line 100) | def get_event_triggered_workflows(cls, db):
method async_get_event_triggered_workflows (line 111) | async def async_get_event_triggered_workflows(cls, db: AsyncSession):
method get_by_name (line 123) | def get_by_name(cls, db, name: str):
method async_get_by_name (line 128) | async def async_get_by_name(cls, db: AsyncSession, name: str):
method update_state (line 134) | def update_state(cls, db, wid: int, state: str):
method async_update_state (line 140) | async def async_update_state(cls, db: AsyncSession, wid: int, state: s...
method start (line 147) | def start(cls, db, wid: int):
method async_start (line 155) | async def async_start(cls, db: AsyncSession, wid: int):
method fail (line 162) | def fail(cls, db, wid: int, result: str):
method async_fail (line 172) | async def async_fail(cls, db: AsyncSession, wid: int, result: str):
method success (line 185) | def success(cls, db, wid: int, result: Optional[str] = None):
method async_success (line 196) | async def async_success(cls, db: AsyncSession, wid: int, result: Optio...
method reset (line 210) | def reset(cls, db, wid: int, reset_count: Optional[bool] = False):
method async_reset (line 221) | async def async_reset(cls, db: AsyncSession, wid: int, reset_count: Op...
method update_current_action (line 233) | def update_current_action(cls, db, wid: int, action_id: str, context: ...
method async_update_current_action (line 242) | async def async_update_current_action(cls, db: AsyncSession, wid: int,...
FILE: app/db/plugindata_oper.py
class PluginDataOper (line 7) | class PluginDataOper(DbOper):
method save (line 12) | def save(self, plugin_id: str, key: str, value: Any):
method get_data (line 27) | def get_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
method del_data (line 41) | def del_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
method truncate (line 52) | def truncate(self):
method get_data_all (line 58) | def get_data_all(self, plugin_id: str) -> Any:
FILE: app/db/site_oper.py
class SiteOper (line 11) | class SiteOper(DbOper):
method add (line 16) | def add(self, **kwargs) -> Tuple[bool, str]:
method get (line 26) | def get(self, sid: int) -> Site:
method async_get (line 32) | async def async_get(self, sid: int) -> Site:
method list (line 38) | def list(self) -> List[Site]:
method async_list (line 44) | async def async_list(self) -> List[Site]:
method list_order_by_pri (line 50) | def list_order_by_pri(self) -> List[Site]:
method list_active (line 56) | def list_active(self) -> List[Site]:
method async_list_active (line 62) | async def async_list_active(self) -> List[Site]:
method delete (line 68) | def delete(self, sid: int):
method update (line 74) | def update(self, sid: int, payload: dict) -> Site:
method get_by_domain (line 82) | def get_by_domain(self, domain: str) -> Site:
method async_get_by_domain (line 88) | async def async_get_by_domain(self, domain: str) -> Site:
method async_get_by_name (line 94) | async def async_get_by_name(self, name: str) -> Site:
method get_domains_by_ids (line 100) | def get_domains_by_ids(self, ids: List[int]) -> List[str]:
method exists (line 106) | def exists(self, domain: str) -> bool:
method update_cookie (line 112) | def update_cookie(self, domain: str, cookies: str) -> Tuple[bool, str]:
method update_rss (line 124) | def update_rss(self, domain: str, rss: str) -> Tuple[bool, str]:
method update_userdata (line 136) | def update_userdata(self, domain: str, name: str, payload: dict) -> Tu...
method get_userdata (line 161) | def get_userdata(self) -> List[SiteUserData]:
method get_userdata_by_domain (line 167) | def get_userdata_by_domain(self, domain: str, workdate: Optional[str] ...
method get_userdata_by_date (line 173) | def get_userdata_by_date(self, date: str) -> List[SiteUserData]:
method get_userdata_latest (line 179) | def get_userdata_latest(self) -> List[SiteUserData]:
method get_icon_by_domain (line 185) | def get_icon_by_domain(self, domain: str) -> SiteIcon:
method update_icon (line 191) | def update_icon(self, name: str, domain: str, icon_url: str, icon_base...
method success (line 206) | def success(self, domain: str, seconds: Optional[int] = None):
method fail (line 247) | def fail(self, domain: str):
method async_success (line 268) | async def async_success(self, domain: str, seconds: Optional[int] = No...
method async_fail (line 309) | async def async_fail(self, domain: str):
FILE: app/db/subscribe_oper.py
class SubscribeOper (line 10) | class SubscribeOper(DbOper):
method add (line 15) | def add(self, mediainfo: MediaInfo, **kwargs) -> Tuple[int, str]:
method async_add (line 52) | async def async_add(self, mediainfo: MediaInfo, **kwargs) -> Tuple[int...
method exists (line 89) | def exists(self, tmdbid: Optional[int] = None, doubanid: Optional[str]...
method get (line 103) | def get(self, sid: int) -> Subscribe:
method async_get (line 109) | async def async_get(self, sid: int) -> Subscribe:
method get_by (line 115) | def get_by(self, type: str, season: Optional[str] = None, tmdbid: Opti...
method async_get_by (line 122) | async def async_get_by(self, type: str, season: Optional[str] = None, ...
method list (line 129) | def list(self, state: Optional[str] = None) -> List[Subscribe]:
method async_list (line 137) | async def async_list(self, state: Optional[str] = None) -> List[Subscr...
method delete (line 145) | def delete(self, sid: int):
method update (line 151) | def update(self, sid: int, payload: dict) -> Subscribe:
method list_by_tmdbid (line 160) | def list_by_tmdbid(self, tmdbid: int, season: Optional[int] = None) ->...
method list_by_username (line 166) | def list_by_username(self, username: str, state: Optional[str] = None,
method list_by_type (line 173) | def list_by_type(self, mtype: str, days: Optional[int] = 7) -> Subscribe:
method add_history (line 179) | def add_history(self, **kwargs):
method exist_history (line 193) | def exist_history(self, tmdbid: Optional[int] = None, doubanid: Option...
FILE: app/db/systemconfig_oper.py
class SystemConfigOper (line 12) | class SystemConfigOper(DbOper, metaclass=Singleton):
method __init__ (line 16) | def __init__(self):
method set (line 27) | def set(self, key: Union[str, SystemConfigKey], value: Any) -> Optiona...
method async_set (line 55) | async def async_set(self, key: Union[str, SystemConfigKey], value: Any...
method get (line 92) | def get(self, key: Union[str, SystemConfigKey] = None) -> Any:
method all (line 104) | def all(self):
method delete (line 112) | def delete(self, key: Union[str, SystemConfigKey]) -> bool:
FILE: app/db/transferhistory_oper.py
class TransferHistoryOper (line 11) | class TransferHistoryOper(DbOper):
method get (line 16) | def get(self, historyid: int) -> TransferHistory:
method get_by_title (line 23) | def get_by_title(self, title: str) -> List[TransferHistory]:
method get_by_src (line 30) | def get_by_src(self, src: str, storage: Optional[str] = None) -> Trans...
method get_by_dest (line 38) | def get_by_dest(self, dest: str) -> TransferHistory:
method list_by_hash (line 45) | def list_by_hash(self, download_hash: str) -> List[TransferHistory]:
method add (line 52) | def add(self, **kwargs):
method statistic (line 61) | def statistic(self, days: Optional[int] = 7) -> List[Any]:
method get_by (line 67) | def get_by(self, title: Optional[str] = None, year: Optional[str] = No...
method get_by_type_tmdbid (line 82) | def get_by_type_tmdbid(self, mtype: Optional[str] = None, tmdbid: Opti...
method delete (line 90) | def delete(self, historyid):
method truncate (line 96) | def truncate(self):
method add_force (line 102) | def add_force(self, **kwargs) -> TransferHistory:
method update_download_hash (line 116) | def update_download_hash(self, historyid, download_hash):
method add_success (line 122) | def add_success(self, fileitem: FileItem, mode: str, meta: MetaBase,
method add_fail (line 153) | def add_fail(self, fileitem: FileItem, mode: str, meta: MetaBase, medi...
method list_by_date (line 202) | def list_by_date(self, date: str) -> List[TransferHistory]:
FILE: app/db/user_oper.py
function get_current_user (line 13) | def get_current_user(
function get_current_user_async (line 26) | async def get_current_user_async(
function get_current_active_user (line 39) | def get_current_active_user(
function get_current_active_user_async (line 50) | async def get_current_active_user_async(
function get_current_active_superuser (line 61) | def get_current_active_superuser(
function get_current_active_superuser_async (line 74) | async def get_current_active_superuser_async(
class UserOper (line 87) | class UserOper(DbOper):
method list (line 92) | def list(self) -> List[User]:
method add (line 98) | def add(self, **kwargs):
method get_by_name (line 105) | def get_by_name(self, name: str) -> User:
method get_permissions (line 111) | def get_permissions(self, name: str) -> dict:
method get_settings (line 120) | def get_settings(self, name: str) -> Optional[dict]:
method get_setting (line 129) | def get_setting(self, name: str, key: str) -> Optional[str]:
method get_name (line 138) | def get_name(self, **kwargs) -> Optional[str]:
FILE: app/db/userconfig_oper.py
class UserConfigOper (line 9) | class UserConfigOper(DbOper, metaclass=Singleton):
method __init__ (line 13) | def __init__(self):
method set (line 22) | def set(self, username: str, key: Union[str, UserConfigKey], value: Any):
method get (line 41) | def get(self, username: str, key: Union[str, UserConfigKey] = None) ->...
method __set_config_cache (line 53) | def __set_config_cache(self, username: str, key: str, value: Any):
method __get_config_caches (line 69) | def __get_config_caches(self, username: str) -> Optional[Dict[str, Any]]:
method __get_config_cache (line 77) | def __get_config_cache(self, username: str, key: str) -> Any:
FILE: app/db/workflow_oper.py
class WorkflowOper (line 7) | class WorkflowOper(DbOper):
method add (line 12) | def add(self, **kwargs) -> Tuple[bool, str]:
method get (line 22) | def get(self, wid: int) -> Workflow:
method async_get (line 28) | async def async_get(self, wid: int) -> Workflow:
method list (line 34) | def list(self) -> List[Workflow]:
method async_list (line 40) | async def async_list(self) -> Coroutine[Any, Any, Sequence[Any]]:
method list_enabled (line 46) | def list_enabled(self) -> List[Workflow]:
method get_timer_triggered_workflows (line 52) | def get_timer_triggered_workflows(self) -> List[Workflow]:
method get_event_triggered_workflows (line 58) | def get_event_triggered_workflows(self) -> List[Workflow]:
method get_by_name (line 64) | def get_by_name(self, name: str) -> Workflow:
method async_get_by_name (line 70) | async def async_get_by_name(self, name: str) -> Workflow:
method start (line 76) | def start(self, wid: int) -> bool:
method success (line 82) | def success(self, wid: int, result: Optional[str] = None) -> bool:
method fail (line 88) | def fail(self, wid: int, result: str) -> bool:
method step (line 94) | def step(self, wid: int, action_id: str, context: dict) -> bool:
method reset (line 100) | def reset(self, wid: int, reset_count: bool = False) -> bool:
FILE: app/factory.py
function create_app (line 8) | def create_app() -> FastAPI:
FILE: app/helper/browser.py
class PlaywrightHelper (line 12) | class PlaywrightHelper:
method __init__ (line 13) | def __init__(self, browser_type=settings.PLAYWRIGHT_BROWSER_TYPE):
method __pass_cloudflare (line 17) | def __pass_cloudflare(url: str, page: Page) -> bool:
method __fs_cookie_str (line 26) | def __fs_cookie_str(cookies: list) -> str:
method __flaresolverr_request (line 32) | def __flaresolverr_request(url: str,
method action (line 132) | def action(self, url: str,
method get_page_source (line 199) | def get_page_source(self, url: str,
FILE: app/helper/cloudflare.py
function under_challenge (line 26) | def under_challenge(html_text: str):
FILE: app/helper/cookie.py
class CookieHelper (line 16) | class CookieHelper:
method parse_cookies (line 62) | def parse_cookies(cookies: list) -> str:
method get_site_cookie_ua (line 73) | def get_site_cookie_ua(self,
method __get_captcha_text (line 240) | def __get_captcha_text(cookie: str, ua: str, code_url: str) -> str:
method __get_captcha_url (line 257) | def __get_captcha_url(siteurl: str, imageurl: str) -> str:
FILE: app/helper/cookiecloud.py
class CookieCloudHelper (line 12) | class CookieCloudHelper:
method __init__ (line 15) | def __init__(self):
method __sync_setting (line 18) | def __sync_setting(self):
method download (line 28) | def download(self) -> Tuple[Optional[dict], str]:
method __get_crypt_key (line 111) | def __get_crypt_key(self) -> bytes:
method __load_local_encrypt_data (line 118) | def __load_local_encrypt_data(self, uuid: str) -> Dict[str, Any]:
FILE: app/helper/directory.py
class DirectoryHelper (line 15) | class DirectoryHelper:
method get_dirs (line 21) | def get_dirs() -> List[schemas.TransferDirectoryConf]:
method get_download_dirs (line 30) | def get_download_dirs(self) -> List[schemas.TransferDirectoryConf]:
method get_local_download_dirs (line 36) | def get_local_download_dirs(self) -> List[schemas.TransferDirectoryConf]:
method get_library_dirs (line 42) | def get_library_dirs(self) -> List[schemas.TransferDirectoryConf]:
method get_local_library_dirs (line 48) | def get_local_library_dirs(self) -> List[schemas.TransferDirectoryConf]:
method get_dir (line 54) | def get_dir(self, media: Optional[MediaInfo], include_unsorted: Option...
method _is_same_source (line 115) | def _is_same_source(src: Tuple[Path, str], tar: Tuple[Path, str]) -> ...
method get_media_root_path (line 131) | def get_media_root_path(rename_format: str, rename_path: Path) -> Opti...
FILE: app/helper/display.py
class DisplayHelper (line 10) | class DisplayHelper(metaclass=Singleton):
method __init__ (line 12) | def __init__(self):
method stop (line 22) | def stop(self):
FILE: app/helper/doh.py
function enable_doh (line 32) | def enable_doh(enable: bool):
class DohHelper (line 70) | class DohHelper(ConfigReloadMixin, metaclass=Singleton):
method __init__ (line 76) | def __init__(self):
method on_config_changed (line 79) | def on_config_changed(self):
method get_reload_name (line 85) | def get_reload_name(self):
function _doh_query (line 88) | def _doh_query(resolver: str, host: str) -> Optional[str]:
function doh_query_json (line 145) | def doh_query_json(resolver: str, host: str) -> Optional[str]:
FILE: app/helper/downloader.py
class DownloaderHelper (line 8) | class DownloaderHelper(ServiceBaseHelper[DownloaderConf]):
method __init__ (line 13) | def __init__(self):
method is_downloader (line 20) | def is_downloader(
FILE: app/helper/format.py
class FormatParser (line 9) | class FormatParser(object):
method __init__ (line 13) | def __init__(self, eformat: str, details: Optional[str] = None, part: ...
method format (line 51) | def format(self):
method start_ep (line 55) | def start_ep(self):
method end_ep (line 59) | def end_ep(self):
method part (line 63) | def part(self):
method offset (line 67) | def offset(self):
method match (line 70) | def match(self, file: str) -> bool:
method split_episode (line 82) | def split_episode(self, file_name: str, file_meta: MetaBase) -> Tuple[...
method __handle_single (line 119) | def __handle_single(self, file: str) -> Tuple[Optional[int], Optional[...
FILE: app/helper/image.py
class WallpaperHelper (line 18) | class WallpaperHelper(metaclass=Singleton):
method get_wallpaper (line 23) | def get_wallpaper(self) -> Optional[str]:
method get_wallpapers (line 37) | def get_wallpapers(self, num: int = 10) -> List[str]:
method get_tmdb_wallpaper (line 52) | def get_tmdb_wallpaper(self) -> Optional[str]:
method get_tmdb_wallpapers (line 59) | def get_tmdb_wallpapers(self, num: int = 10) -> List[str]:
method get_bing_wallpaper (line 66) | def get_bing_wallpaper(self) -> Optional[str]:
method get_bing_wallpapers (line 83) | def get_bing_wallpapers(self, num: int = 7) -> List[str]:
method get_mediaserver_wallpaper (line 99) | def get_mediaserver_wallpaper(self) -> Optional[str]:
method get_mediaserver_wallpapers (line 106) | def get_mediaserver_wallpapers(self, num: int = 10) -> List[str]:
method get_customize_wallpaper (line 113) | def get_customize_wallpaper(self) -> Optional[str]:
method get_customize_wallpapers (line 123) | def get_customize_wallpapers(self) -> List[str]:
class ImageHelper (line 173) | class ImageHelper(metaclass=Singleton):
method __init__ (line 175) | def __init__(self):
method _prepare_cache_path (line 182) | def _prepare_cache_path(url: str) -> str:
method _validate_image (line 191) | def _validate_image(content: bytes) -> bool:
method _get_request_params (line 203) | def _get_request_params(url: str, proxy: Optional[bool], cookies: Opti...
method fetch_image (line 218) | def fetch_image(
method async_fetch_image (line 254) | async def async_fetch_image(
FILE: app/helper/llm.py
class LLMHelper (line 8) | class LLMHelper:
method get_llm (line 12) | def get_llm(streaming: bool = False, callbacks: Optional[list] = None):
method get_models (line 74) | def get_models(self, provider: str, api_key: str, base_url: str = None...
method _get_google_models (line 83) | def _get_google_models(api_key: str) -> List[str]:
method _get_openai_compatible_models (line 95) | def _get_openai_compatible_models(provider: str, api_key: str, base_ur...
FILE: app/helper/mediaserver.py
class MediaServerHelper (line 8) | class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
method __init__ (line 13) | def __init__(self):
method is_media_server (line 20) | def is_media_server(
FILE: app/helper/message.py
class TemplateContextBuilder (line 29) | class TemplateContextBuilder:
method __init__ (line 34) | def __init__(self):
method build (line 37) | def build(
method _add_media_info (line 73) | def _add_media_info(self, mediainfo: MediaInfo):
method _add_episode_details (line 126) | def _add_episode_details(self, meta: Optional[MetaBase], episodes: Opt...
method _add_torrent_info (line 193) | def _add_torrent_info(self, torrentinfo: Optional[TorrentInfo]):
method _add_transfer_info (line 236) | def _add_transfer_info(self, transferinfo: Optional[TransferInfo]) -> ...
method _add_file_info (line 250) | def _add_file_info(self, file_extension: Optional[str]):
method _add_raw_objects (line 262) | def _add_raw_objects(
method __convert_invalid_characters (line 288) | def __convert_invalid_characters(filename: str):
class TemplateHelper (line 305) | class TemplateHelper(metaclass=SingletonClass):
method __init__ (line 310) | def __init__(self):
method _generate_cache_key (line 315) | def _generate_cache_key(cuntent: Union[str, dict]) -> str:
method get_cache_context (line 325) | def get_cache_context(self, cuntent: Union[str, dict]) -> Optional[dict]:
method set_cache_context (line 332) | def set_cache_context(self, cuntent: Union[str, dict], context: dict) ...
method render (line 339) | def render(self,
method render_with_context (line 375) | def render_with_context(template_content: str, context: dict) -> str:
method parse_template_content (line 386) | def parse_template_content(template_content: Union[str, dict],
method __process_formatted_string (line 436) | def __process_formatted_string(rendered: str) -> Optional[Union[dict, ...
method close (line 475) | def close(self):
class MessageTemplateHelper (line 483) | class MessageTemplateHelper:
method render (line 489) | def render(message: Notification, *args, **kwargs) -> Optional[Notific...
method is_instance_valid (line 500) | def is_instance_valid(message: Notification) -> bool:
method meets_update_conditions (line 509) | def meets_update_conditions(message: Notification, *args, **kwargs) ->...
method _apply_template_data (line 523) | def _apply_template_data(message: Notification, *args, **kwargs) -> Op...
method _get_template (line 539) | def _get_template(message: Notification) -> Optional[str]:
class MessageQueueManager (line 547) | class MessageQueueManager(metaclass=SingletonClass):
method __init__ (line 552) | def __init__(
method init_config (line 575) | def init_config(self):
method _parse_schedule (line 584) | def _parse_schedule(periods: Union[list, dict]) -> List[tuple[int, int...
method _time_to_minutes (line 627) | def _time_to_minutes(time_str: str) -> int:
method _is_in_scheduled_time (line 634) | def _is_in_scheduled_time(self, current_time: datetime) -> bool:
method send_message (line 654) | def send_message(self, *args, **kwargs) -> None:
method async_send_message (line 668) | async def async_send_message(self, *args, **kwargs) -> None:
method _send (line 679) | def _send(self, *args, **kwargs) -> None:
method _monitor_loop (line 690) | def _monitor_loop(self) -> None:
method stop (line 710) | def stop(self) -> None:
class MessageHelper (line 720) | class MessageHelper(metaclass=Singleton):
method __init__ (line 725) | def __init__(self):
method put (line 729) | def put(self, message: Any, role: str = "plugin", title: str = None, n...
method get (line 766) | def get(self, role: str = "system") -> Optional[str]:
function stop_message (line 780) | def stop_message():
FILE: app/helper/module.py
function _default_filter (line 13) | def _default_filter(name: str, obj: Any) -> bool:
class ModuleHelper (line 20) | class ModuleHelper:
method load (line 26) | def load(cls, package_path: str, filter_func: FilterFuncType = _defaul...
method load_with_pre_filter (line 58) | def load_with_pre_filter(cls, package_path: str, filter_func: FilterFu...
method dynamic_import_all_modules (line 110) | def dynamic_import_all_modules(base_path: Path, package_name: str):
FILE: app/helper/nfo.py
class NfoReader (line 6) | class NfoReader:
method __init__ (line 7) | def __init__(self, xml_file_path: Path):
method get_element_value (line 12) | def get_element_value(self, element_path) -> Optional[str]:
method get_elements (line 16) | def get_elements(self, element_path) -> List[ET.Element]:
FILE: app/helper/notification.py
class NotificationHelper (line 8) | class NotificationHelper(ServiceBaseHelper[NotificationConf]):
method __init__ (line 13) | def __init__(self):
method is_notification (line 20) | def is_notification(
FILE: app/helper/ocr.py
class OcrHelper (line 8) | class OcrHelper:
method get_captcha_text (line 12) | def get_captcha_text(self, image_url: Optional[str] = None, image_b64:...
FILE: app/helper/passkey.py
class PassKeyHelper (line 35) | class PassKeyHelper:
method get_rp_id (line 41) | def get_rp_id() -> str:
method get_rp_name (line 64) | def get_rp_name() -> str:
method get_origin (line 71) | def get_origin() -> str:
method standardize_credential_id (line 81) | def standardize_credential_id(credential_id: str) -> str:
method _base64_encode_urlsafe (line 94) | def _base64_encode_urlsafe(data: bytes) -> str:
method _base64_decode_urlsafe (line 104) | def _base64_decode_urlsafe(data: str) -> bytes:
method _parse_credential_list (line 114) | def _parse_credential_list(credentials: List[Dict[str, Any]]) -> List[...
method _get_user_verification_requirement (line 138) | def _get_user_verification_requirement(user_verification: Optional[str...
method _get_verification_params (line 151) | def _get_verification_params(
method generate_registration_options (line 167) | def generate_registration_options(
method verify_registration_response (line 225) | def verify_registration_response(
method generate_authentication_options (line 278) | def generate_authentication_options(
method verify_authentication_response (line 317) | def verify_authentication_response(
FILE: app/helper/plugin.py
class PluginHelper (line 35) | class PluginHelper(metaclass=WeakSingleton):
method __init__ (line 45) | def __init__(self):
method get_plugins (line 53) | def get_plugins(self, repo_url: str,
method get_plugin_package_version (line 83) | def get_plugin_package_version(self, pid: str, repo_url: str,
method get_repo_info (line 114) | def get_repo_info(repo_url: str) -> Tuple[Optional[str], Optional[str]]:
method get_statistic (line 132) | def get_statistic(self) -> Dict:
method install_reg (line 143) | def install_reg(self, pid: str, repo_url: Optional[str] = None) -> bool:
method install_report (line 164) | def install_report(self, items: Optional[List[Tuple[str, Optional[str]...
method install (line 187) | def install(self, pid: str, repo_url: str, package_version: Optional[s...
method __get_file_list (line 260) | def __get_file_list(self, pid: str, user_repo: str, package_version: O...
method __download_files (line 294) | def __download_files(self, pid: str, file_list: List[dict], user_repo:...
method __download_and_install_requirements (line 348) | def __download_and_install_requirements(self, requirements_file_info: ...
method __install_dependencies_if_required (line 377) | def __install_dependencies_if_required(self, pid: str) -> Tuple[bool, ...
method __backup_plugin (line 399) | def __backup_plugin(pid: str) -> str:
method __restore_plugin (line 420) | def __restore_plugin(pid: str, backup_dir: str):
method __remove_old_plugin (line 438) | def __remove_old_plugin(pid: str):
method pip_install_with_fallback (line 448) | def pip_install_with_fallback(requirements_file: Path) -> Tuple[bool, ...
method __request_with_fallback (line 501) | def __request_with_fallback(url: str,
method __get_plugin_meta (line 541) | def __get_plugin_meta(self, pid: str, repo_url: str,
method __install_flow_sync (line 554) | def __install_flow_sync(self, pid: str, force_install: bool,
method __install_from_release (line 592) | def __install_from_release(self, pid: str, user_repo: str, release_tag...
method find_missing_dependencies (line 667) | def find_missing_dependencies(self) -> List[str]:
method install_dependencies (line 703) | def install_dependencies(self, dependencies: List[str]) -> Tuple[bool,...
method __get_installed_packages (line 730) | def __get_installed_packages(self) -> Dict[str, Version]:
method __find_plugin_dependencies (line 759) | def __find_plugin_dependencies(self) -> Dict[str, str]:
method __parse_requirements (line 796) | def __parse_requirements(self, requirements_file: Path) -> Dict[str, L...
method __merge_dependencies (line 827) | def __merge_dependencies(dependencies: Dict[str, Set[str]]) -> Dict[st...
method __standardize_pkg_name (line 854) | def __standardize_pkg_name(name: str) -> str:
method async_get_plugin_package_version (line 865) | async def async_get_plugin_package_version(self, pid: str, repo_url: str,
method __async_request_with_fallback (line 883) | async def __async_request_with_fallback(url: str,
method async_get_plugins (line 924) | async def async_get_plugins(self, repo_url: str,
method async_get_statistic (line 955) | async def async_get_statistic(self) -> Dict:
method async_install_reg (line 966) | async def async_install_reg(self, pid: str, repo_url: Optional[str] = ...
method async_install_report (line 987) | async def async_install_report(self, items: Optional[List[Tuple[str, O...
method __async_get_file_list (line 1010) | async def __async_get_file_list(self, pid: str, user_repo: str, packag...
method __async_download_files (line 1044) | async def __async_download_files(self, pid: str, file_list: List[dict]...
method __async_download_and_install_requirements (line 1099) | async def __async_download_and_install_requirements(self, requirements...
method __async_backup_plugin (line 1128) | async def __async_backup_plugin(self, pid: str) -> str:
method __async_restore_plugin (line 1149) | async def __async_restore_plugin(self, pid: str, backup_dir: str):
method __async_remove_old_plugin (line 1168) | async def __async_remove_old_plugin(pid: str):
method _async_copytree (line 1177) | async def _async_copytree(self, src: AsyncPath, dst: AsyncPath):
method __async_install_dependencies_if_required (line 1198) | async def __async_install_dependencies_if_required(self, pid: str) -> ...
method async_install_dependencies (line 1219) | async def async_install_dependencies(self, dependencies: List[str]) ->...
method __async_find_plugin_dependencies (line 1248) | async def __async_find_plugin_dependencies(self) -> Dict[str, str]:
method __async_parse_requirements (line 1287) | async def __async_parse_requirements(self, requirements_file: AsyncPat...
method async_find_missing_dependencies (line 1317) | async def async_find_missing_dependencies(self) -> List[str]:
method async_install (line 1353) | async def async_install(self, pid: str, repo_url: str, package_version...
method __async_get_plugin_meta (line 1426) | async def __async_get_plugin_meta(self, pid: str, repo_url: str,
method __install_flow_async (line 1439) | async def __install_flow_async(self, pid: str, force_install: bool,
method __prepare_content_via_filelist_sync (line 1476) | def __prepare_content_via_filelist_sync(self, pid: str, user_repo: str,
method __prepare_content_via_filelist_async (line 1496) | async def __prepare_content_via_filelist_async(self, pid: str, user_re...
method __async_install_from_release (line 1516) | async def __async_install_from_release(self, pid: str, user_repo: str,...
FILE: app/helper/progress.py
class ProgressHelper (line 8) | class ProgressHelper:
method __init__ (line 13) | def __init__(self, key: Union[ProgressKey, str]):
method __reset (line 19) | def __reset(self):
method start (line 30) | def start(self):
method end (line 41) | def end(self):
method update (line 57) | def update(self, value: Union[float, int] = None, text: Optional[str] ...
method get (line 74) | def get(self) -> dict:
FILE: app/helper/redis.py
function serialize (line 24) | def serialize(value: Any) -> bytes:
function deserialize (line 62) | def deserialize(value: bytes) -> Any:
class RedisHelper (line 75) | class RedisHelper(ConfigReloadMixin, metaclass=Singleton):
method __init__ (line 87) | def __init__(self):
method _connect (line 94) | def _connect(self):
method on_config_changed (line 116) | def on_config_changed(self):
method get_reload_name (line 120) | def get_reload_name(self):
method set_memory_limit (line 123) | def set_memory_limit(self, policy: Optional[str] = "allkeys-lru"):
method __get_region (line 139) | def __get_region(region: Optional[str] = None):
method __make_redis_key (line 145) | def __make_redis_key(self, region: str, key: str) -> str:
method __get_original_key (line 154) | def __get_original_key(redis_key: Union[str, bytes]) -> str:
method set (line 167) | def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 188) | def exists(self, key: str, region: Optional[str] = "DEFAULT") -> bool:
method get (line 204) | def get(self, key: str, region: Optional[str] = "DEFAULT") -> Optional...
method delete (line 223) | def delete(self, key: str, region: Optional[str] = "DEFAULT") -> None:
method clear (line 237) | def clear(self, region: Optional[str] = None) -> None:
method items (line 259) | def items(self, region: Optional[str] = None) -> Generator[Tuple[str, ...
method test (line 283) | def test(self) -> bool:
method close (line 294) | def close(self) -> None:
class AsyncRedisHelper (line 304) | class AsyncRedisHelper(ConfigReloadMixin, metaclass=Singleton):
method __init__ (line 317) | def __init__(self):
method _connect (line 324) | async def _connect(self):
method on_config_changed (line 346) | async def on_config_changed(self):
method get_reload_name (line 350) | def get_reload_name(self):
method set_memory_limit (line 353) | async def set_memory_limit(self, policy: Optional[str] = "allkeys-lru"):
method __get_region (line 369) | def __get_region(region: Optional[str] = "DEFAULT"):
method __make_redis_key (line 375) | def __make_redis_key(self, region: str, key: str) -> str:
method __get_original_key (line 384) | def __get_original_key(redis_key: Union[str, bytes]) -> str:
method set (line 397) | async def set(self, key: str, value: Any, ttl: Optional[int] = None,
method exists (line 418) | async def exists(self, key: str, region: Optional[str] = "DEFAULT") ->...
method get (line 435) | async def get(self, key: str, region: Optional[str] = "DEFAULT") -> Op...
method delete (line 454) | async def delete(self, key: str, region: Optional[str] = "DEFAULT") ->...
method clear (line 468) | async def clear(self, region: Optional[str] = None) -> None:
method items (line 490) | async def items(self, region: Optional[str] = None) -> AsyncGenerator[...
method test (line 514) | async def test(self) -> bool:
method close (line 525) | async def close(self) -> None:
FILE: app/helper/resource.py
class ResourceHelper (line 13) | class ResourceHelper:
method __init__ (line 22) | def __init__(self):
method proxies (line 26) | def proxies(self):
method check (line 29) | def check(self):
FILE: app/helper/rss.py
class RssHelper (line 16) | class RssHelper:
method parse (line 230) | def parse(self, url, proxy: bool = False,
method get_rss_link (line 440) | def get_rss_link(self, url: str, cookie: str, ua: str, proxy: bool = F...
FILE: app/helper/rule.py
class RuleHelper (line 9) | class RuleHelper:
method get_rule_groups (line 15) | def get_rule_groups() -> List[FilterRuleGroup]:
method get_rule_group (line 24) | def get_rule_group(self, group_name: str) -> Optional[FilterRuleGroup]:
method get_rule_group_by_media (line 34) | def get_rule_group_by_media(self, media: MediaInfo = None, group_names...
method get_custom_rules (line 52) | def get_custom_rules() -> List[CustomRule]:
method get_custom_rule (line 61) | def get_custom_rule(self, rule_id: str) -> Optional[CustomRule]:
FILE: app/helper/service.py
class ServiceConfigHelper (line 11) | class ServiceConfigHelper:
method get_configs (line 17) | def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
method get_downloader_configs (line 32) | def get_downloader_configs() -> List[DownloaderConf]:
method get_mediaserver_configs (line 39) | def get_mediaserver_configs() -> List[MediaServerConf]:
method get_notification_configs (line 46) | def get_notification_configs() -> List[NotificationConf]:
method get_notification_switches (line 53) | def get_notification_switches() -> List[NotificationSwitchConf]:
method get_notification_switch (line 60) | def get_notification_switch(mtype: NotificationType) -> Optional[str]:
class ServiceBaseHelper (line 71) | class ServiceBaseHelper(Generic[TConf]):
method __init__ (line 76) | def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf]...
method get_configs (line 82) | def get_configs(self, include_disabled: bool = False) -> Dict[str, TCo...
method get_config (line 96) | def get_config(self, name: str) -> Optional[TConf]:
method iterate_module_instances (line 105) | def iterate_module_instances(self) -> Iterator[ServiceInfo]:
method get_services (line 129) | def get_services(self, type_filter: Optional[str] = None, name_filters...
method get_service (line 149) | def get_service(self, name: str, type_filter: Optional[str] = None) ->...
FILE: app/helper/storage.py
class StorageHelper (line 8) | class StorageHelper:
method get_storagies (line 14) | def get_storagies() -> List[schemas.StorageConf]:
method get_storage (line 23) | def get_storage(self, storage: str) -> Optional[schemas.StorageConf]:
method set_storage (line 33) | def set_storage(self, storage: str, conf: dict):
method add_storage (line 52) | def add_storage(self, storage: str, name: str, conf: dict):
method reset_storage (line 73) | def reset_storage(self, storage: str):
FILE: app/helper/subscribe.py
class SubscribeHelper (line 15) | class SubscribeHelper(metaclass=WeakSingleton):
method __init__ (line 54) | def __init__(self):
method _check_subscribe_share_enabled (line 64) | def _check_subscribe_share_enabled() -> Tuple[bool, str]:
method _validate_subscribe (line 73) | def _validate_subscribe(subscribe) -> Tuple[bool, str]:
method _prepare_subscribe_data (line 82) | def _prepare_subscribe_data(subscribe) -> dict:
method _build_share_payload (line 90) | def _build_share_payload(self, share_title: str, share_comment: str,
method _handle_response (line 103) | def _handle_response(self, res, clear_cache: bool = True) -> Tuple[boo...
method _handle_list_response (line 125) | def _handle_list_response(res) -> List[dict]:
method get_statistic (line 134) | def get_statistic(self, stype: str, page: Optional[int] = 1, count: Op...
method async_get_statistic (line 165) | async def async_get_statistic(self, stype: str, page: Optional[int] = ...
method sub_reg (line 195) | def sub_reg(self, sub: dict) -> bool:
method async_sub_reg (line 209) | async def async_sub_reg(self, sub: dict) -> bool:
method sub_done (line 223) | def sub_done(self, sub: dict) -> bool:
method sub_reg_async (line 237) | def sub_reg_async(self, sub: dict) -> bool:
method sub_done_async (line 245) | def sub_done_async(self, sub: dict) -> bool:
method sub_report (line 253) | def sub_report(self) -> bool:
method sub_share (line 272) | def sub_share(self, subscribe_id: int,
method async_sub_share (line 300) | async def async_sub_share(self, subscribe_id: int,
method share_delete (line 328) | def share_delete(self, share_id: int) -> Tuple[bool, str]:
method async_share_delete (line 343) | async def async_share_delete(self, share_id: int) -> Tuple[bool, str]:
method sub_fork (line 358) | def sub_fork(self, share_id: int) -> Tuple[bool, str]:
method async_sub_fork (line 373) | async def async_sub_fork(self, share_id: int) -> Tuple[bool, str]:
method get_shares (line 389) | def get_shares(self, name: Optional[str] = None, page: Optional[int] =...
method async_get_shares (line 420) | async def async_get_shares(self, name: Optional[str] = None, page: Opt...
method get_share_statistics (line 451) | def get_share_statistics(self) -> List[dict]:
method async_get_share_statistics (line 464) | async def async_get_share_statistics(self) -> List[dict]:
method get_user_uuid (line 476) | def get_user_uuid(self) -> str:
method get_github_user (line 485) | def get_github_user(self) -> str:
method is_admin_user (line 498) | def is_admin_user(self) -> bool:
FILE: app/helper/system.py
class SystemHelper (line 16) | class SystemHelper(ConfigReloadMixin):
method on_config_changed (line 31) | def on_config_changed(self):
method get_reload_name (line 34) | def get_reload_name(self):
method can_restart (line 38) | def can_restart() -> bool:
method _get_container_id (line 48) | def _get_container_id() -> str:
method _check_restart_policy (line 74) | def _check_restart_policy() -> bool:
method restart (line 102) | def restart() -> Tuple[bool, str]:
method _start_graceful_shutdown_monitor (line 131) | def _start_graceful_shutdown_monitor():
method _docker_api_restart (line 150) | def _docker_api_restart() -> Tuple[bool, str]:
method set_system_modified (line 166) | def set_system_modified(self):
method is_system_reset (line 176) | def is_system_reset(self) -> bool:
FILE: app/helper/thread.py
class ThreadHelper (line 7) | class ThreadHelper(metaclass=Singleton):
method __init__ (line 11) | def __init__(self):
method submit (line 14) | def submit(self, func, *args, **kwargs):
method shutdown (line 24) | def shutdown(self):
FILE: app/helper/torrent.py
class TorrentHelper (line 22) | class TorrentHelper:
method __init__ (line 27) | def __init__(self):
method download_torrent (line 30) | def download_torrent(self, url: str,
method get_torrent_info (line 148) | def get_torrent_info(self, torrent_path: Path) -> Tuple[str, List[str]]:
method get_fileinfo_from_torrent (line 165) | def get_fileinfo_from_torrent(torrent: Torrent) -> Tuple[str, List[str]]:
method get_fileinfo_from_torrent_content (line 195) | def get_f
Condensed preview — 471 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,247K chars).
[
{
"path": ".dockerignore",
"chars": 677,
"preview": "# Git\n.github\n.git\n.gitignore\n\n# Documentation\ndocs/\nREADME.md\nLICENSE\n\n# Development files\n.pylintrc\n*.pyc\n__pycache__/"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1945,
"preview": "name: 问题反馈\ndescription: File a bug report\ntitle: \"[错误报告]: 请在此处简单描述你的问题\"\nlabels: [\"bug\"]\nbody:\n - type: markdown\n att"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 309,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: 项目讨论\n url: https://github.com/jxxghp/MoviePilot/discussions/new/"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1020,
"preview": "name: 功能改进\ndescription: Feature Request\ntitle: \"[Feature Request]: \"\nlabels: [\"feature request\"]\nbody:\n - type: markdow"
},
{
"path": ".github/ISSUE_TEMPLATE/rfc.yml",
"chars": 1334,
"preview": "name: 功能提案\ndescription: Request for Comments\ntitle: \"[RFC]\"\nlabels: [\"RFC\"]\nbody:\n - type: markdown\n attributes:\n "
},
{
"path": ".github/workflows/beta.yml",
"chars": 1693,
"preview": "name: MoviePilot Builder Beta\non:\n workflow_dispatch:\n\njobs:\n Docker-build:\n runs-on: ubuntu-latest\n name: Build"
},
{
"path": ".github/workflows/build.yml",
"chars": 2960,
"preview": "name: MoviePilot Builder v2\non:\n workflow_dispatch:\n push:\n branches:\n - v2\n paths:\n - 'version.py'\n\nj"
},
{
"path": ".github/workflows/issues.yml",
"chars": 906,
"preview": "name: Close inactive issues\non:\n workflow_dispatch:\n\n schedule:\n # Github Action 只支持 UTC 时间。\n # '0 18 * * *' 对应 UTC "
},
{
"path": ".github/workflows/pylint.yml",
"chars": 2519,
"preview": "name: Pylint Code Quality Check\n\non:\n # 允许手动触发\n workflow_dispatch:\n\njobs:\n pylint:\n runs-on: ubuntu-latest\n nam"
},
{
"path": ".gitignore",
"chars": 362,
"preview": ".idea/\n*.c\n*.so\n*.pyd\nbuild/\ncython_cache/\ndist/\nnginx/\ntest.py\nsafety_report.txt\napp/helper/sites.py\napp/helper/*.so\nap"
},
{
"path": ".pylintrc",
"chars": 1479,
"preview": "[MASTER]\n# 指定Python路径\ninit-hook='import sys; sys.path.append(\".\")'\n\n# 忽略的文件和目录\nignore=.git,__pycache__,.venv,build,dist,"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 2845,
"preview": "# MoviePilot\n\n\n![GitHub f"
},
{
"path": "app/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "app/agent/__init__.py",
"chars": 20323,
"preview": "import asyncio\nfrom typing import Dict, List, Any, Union\nimport json\nimport tiktoken\n\nfrom langchain.agents import Agent"
},
{
"path": "app/agent/callback/__init__.py",
"chars": 890,
"preview": "import threading\n\nfrom langchain_core.callbacks import AsyncCallbackHandler\n\nfrom app.log import logger\n\n\nclass Streamin"
},
{
"path": "app/agent/memory/__init__.py",
"chars": 11331,
"preview": "\"\"\"对话记忆管理器\"\"\"\n\nimport asyncio\nimport json\nfrom datetime import datetime, timedelta\nfrom typing import Dict, List, Option"
},
{
"path": "app/agent/prompt/Agent Prompt.txt",
"chars": 4835,
"preview": "You are an AI media assistant powered by MoviePilot, specialized in managing home media ecosystems. Your expertise cover"
},
{
"path": "app/agent/prompt/__init__.py",
"chars": 2939,
"preview": "\"\"\"提示词管理器\"\"\"\nfrom pathlib import Path\nfrom typing import Dict\n\nfrom app.log import logger\nfrom app.schemas import Channe"
},
{
"path": "app/agent/tools/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "app/agent/tools/base.py",
"chars": 4618,
"preview": "import json\nimport uuid\nfrom abc import ABCMeta, abstractmethod\nfrom typing import Any, Optional\n\nfrom langchain.tools i"
},
{
"path": "app/agent/tools/factory.py",
"chars": 6805,
"preview": "from typing import List, Callable\n\nfrom app.agent.tools.impl.add_download import AddDownloadTool\nfrom app.agent.tools.im"
},
{
"path": "app/agent/tools/impl/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "app/agent/tools/impl/_torrent_search_utils.py",
"chars": 6556,
"preview": "\"\"\"种子搜索工具辅助函数\"\"\"\n\nimport re\nfrom typing import List, Optional\n\nfrom app.core.context import Context\nfrom app.utils.crypt"
},
{
"path": "app/agent/tools/impl/add_download.py",
"chars": 11581,
"preview": "\"\"\"添加下载工具\"\"\"\n\nimport re\nfrom pathlib import Path\nfrom typing import List, Optional, Type\n\nfrom pydantic import BaseModel"
},
{
"path": "app/agent/tools/impl/add_subscribe.py",
"chars": 7004,
"preview": "\"\"\"添加订阅工具\"\"\"\n\nfrom typing import Optional, Type, List\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base "
},
{
"path": "app/agent/tools/impl/delete_download.py",
"chars": 2657,
"preview": "\"\"\"删除下载任务工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base impo"
},
{
"path": "app/agent/tools/impl/delete_subscribe.py",
"chars": 2277,
"preview": "\"\"\"删除订阅工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base import"
},
{
"path": "app/agent/tools/impl/execute_command.py",
"chars": 2996,
"preview": "\"\"\"执行Shell命令工具\"\"\"\n\nimport asyncio\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.age"
},
{
"path": "app/agent/tools/impl/get_recommendations.py",
"chars": 9211,
"preview": "\"\"\"获取推荐工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools"
},
{
"path": "app/agent/tools/impl/get_search_results.py",
"chars": 5083,
"preview": "\"\"\"获取搜索结果工具\"\"\"\n\nimport json\nimport re\nfrom typing import List, Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfr"
},
{
"path": "app/agent/tools/impl/list_directory.py",
"chars": 5073,
"preview": "\"\"\"查询文件系统目录内容工具\"\"\"\n\nimport json\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Optional, Type"
},
{
"path": "app/agent/tools/impl/query_directory_settings.py",
"chars": 6220,
"preview": "\"\"\"查询系统目录设置工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.t"
},
{
"path": "app/agent/tools/impl/query_download_tasks.py",
"chars": 11201,
"preview": "\"\"\"查询下载工具\"\"\"\n\nimport json\nfrom typing import Optional, Type, List, Union\n\nfrom pydantic import BaseModel, Field\n\nfrom ap"
},
{
"path": "app/agent/tools/impl/query_downloaders.py",
"chars": 1378,
"preview": "\"\"\"查询下载器工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tool"
},
{
"path": "app/agent/tools/impl/query_episode_schedule.py",
"chars": 3922,
"preview": "\"\"\"查询剧集上映时间工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.t"
},
{
"path": "app/agent/tools/impl/query_library_exists.py",
"chars": 5734,
"preview": "\"\"\"查询媒体库工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tool"
},
{
"path": "app/agent/tools/impl/query_library_latest.py",
"chars": 3488,
"preview": "\"\"\"查询媒体服务器最近入库影片工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.ag"
},
{
"path": "app/agent/tools/impl/query_media_detail.py",
"chars": 4944,
"preview": "\"\"\"查询媒体详情工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.too"
},
{
"path": "app/agent/tools/impl/query_popular_subscribes.py",
"chars": 7108,
"preview": "\"\"\"查询热门订阅工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nimport cn2an\nfrom pydantic import BaseModel, Field\n\nfrom "
},
{
"path": "app/agent/tools/impl/query_rule_groups.py",
"chars": 2265,
"preview": "\"\"\"查询规则组工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tool"
},
{
"path": "app/agent/tools/impl/query_schedulers.py",
"chars": 2044,
"preview": "\"\"\"查询定时服务工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.too"
},
{
"path": "app/agent/tools/impl/query_site_userdata.py",
"chars": 6310,
"preview": "\"\"\"查询站点用户数据工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.t"
},
{
"path": "app/agent/tools/impl/query_sites.py",
"chars": 3267,
"preview": "\"\"\"查询站点工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools"
},
{
"path": "app/agent/tools/impl/query_subscribe_history.py",
"chars": 5068,
"preview": "\"\"\"查询订阅历史工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.too"
},
{
"path": "app/agent/tools/impl/query_subscribe_shares.py",
"chars": 4729,
"preview": "\"\"\"查询订阅分享工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.too"
},
{
"path": "app/agent/tools/impl/query_subscribes.py",
"chars": 4638,
"preview": "\"\"\"查询订阅工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools"
},
{
"path": "app/agent/tools/impl/query_transfer_history.py",
"chars": 5466,
"preview": "\"\"\"查询整理历史记录工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nimport jieba\nfrom pydantic import BaseModel, Field\n\nfro"
},
{
"path": "app/agent/tools/impl/query_workflows.py",
"chars": 5508,
"preview": "\"\"\"查询工作流工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tool"
},
{
"path": "app/agent/tools/impl/recognize_media.py",
"chars": 6763,
"preview": "\"\"\"识别媒体信息工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.too"
},
{
"path": "app/agent/tools/impl/run_scheduler.py",
"chars": 1810,
"preview": "\"\"\"运行定时服务工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base impo"
},
{
"path": "app/agent/tools/impl/run_workflow.py",
"chars": 2516,
"preview": "\"\"\"执行工作流工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base impor"
},
{
"path": "app/agent/tools/impl/scrape_metadata.py",
"chars": 4600,
"preview": "\"\"\"刮削媒体元数据工具\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel,"
},
{
"path": "app/agent/tools/impl/search_media.py",
"chars": 4864,
"preview": "\"\"\"搜索媒体工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools"
},
{
"path": "app/agent/tools/impl/search_person.py",
"chars": 3706,
"preview": "\"\"\"搜索人物工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools"
},
{
"path": "app/agent/tools/impl/search_person_credits.py",
"chars": 4068,
"preview": "\"\"\"搜索演员参演作品工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.t"
},
{
"path": "app/agent/tools/impl/search_subscribe.py",
"chars": 5293,
"preview": "\"\"\"搜索订阅缺失剧集工具\"\"\"\n\nimport json\nfrom typing import Optional, Type, List\n\nfrom pydantic import BaseModel, Field\n\nfrom app.a"
},
{
"path": "app/agent/tools/impl/search_torrents.py",
"chars": 4992,
"preview": "\"\"\"搜索种子工具\"\"\"\n\nimport json\nfrom typing import List, Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent"
},
{
"path": "app/agent/tools/impl/search_web.py",
"chars": 6761,
"preview": "import asyncio\nimport json\nimport re\nfrom typing import Optional, Type, List, Dict\n\nimport httpx\nfrom ddgs import DDGS\nf"
},
{
"path": "app/agent/tools/impl/send_message.py",
"chars": 1953,
"preview": "\"\"\"发送消息工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base import"
},
{
"path": "app/agent/tools/impl/test_site.py",
"chars": 1832,
"preview": "\"\"\"测试站点连通性工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.base imp"
},
{
"path": "app/agent/tools/impl/transfer_file.py",
"chars": 6249,
"preview": "\"\"\"整理文件或目录工具\"\"\"\n\nfrom pathlib import Path\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom"
},
{
"path": "app/agent/tools/impl/update_site.py",
"chars": 9004,
"preview": "\"\"\"更新站点工具\"\"\"\n\nimport json\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools"
},
{
"path": "app/agent/tools/impl/update_site_cookie.py",
"chars": 2795,
"preview": "\"\"\"更新站点Cookie和UA工具\"\"\"\n\nfrom typing import Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent.tools.ba"
},
{
"path": "app/agent/tools/impl/update_subscribe.py",
"chars": 11649,
"preview": "\"\"\"更新订阅工具\"\"\"\n\nimport json\nfrom typing import Optional, Type, List\n\nfrom pydantic import BaseModel, Field\n\nfrom app.agent"
},
{
"path": "app/agent/tools/manager.py",
"chars": 9858,
"preview": "import json\nimport uuid\nfrom typing import Any, Dict, List, Optional\n\nfrom app.agent.tools.factory import MoviePilotTool"
},
{
"path": "app/api/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "app/api/apiv1.py",
"chars": 2254,
"preview": "from fastapi import APIRouter\n\nfrom app.api.endpoints import login, user, webhook, message, site, subscribe, \\\n media"
},
{
"path": "app/api/endpoints/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "app/api/endpoints/bangumi.py",
"chars": 2648,
"preview": "from typing import List, Any, Optional\n\nfrom fastapi import APIRouter, Depends\n\nfrom app import schemas\nfrom app.chain.b"
},
{
"path": "app/api/endpoints/dashboard.py",
"chars": 6361,
"preview": "from pathlib import Path\nfrom typing import Any, List, Optional, Annotated\n\nfrom fastapi import APIRouter, Depends\nfrom "
},
{
"path": "app/api/endpoints/discover.py",
"chars": 6033,
"preview": "from typing import Any, List, Optional\n\nfrom fastapi import APIRouter, Depends\n\nfrom app import schemas\nfrom app.chain.b"
},
{
"path": "app/api/endpoints/douban.py",
"chars": 2829,
"preview": "from typing import Any, List, Optional\n\nfrom fastapi import APIRouter, Depends\n\nfrom app import schemas\nfrom app.chain.d"
},
{
"path": "app/api/endpoints/download.py",
"chars": 5313,
"preview": "from typing import Any, List, Annotated, Optional\n\nfrom fastapi import APIRouter, Depends, Body\n\nfrom app import schemas"
},
{
"path": "app/api/endpoints/history.py",
"chars": 4845,
"preview": "from typing import List, Any, Optional\n\nimport jieba\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.ext.asyncio "
},
{
"path": "app/api/endpoints/login.py",
"chars": 2718,
"preview": "from datetime import timedelta\nfrom typing import Any, List, Annotated\n\nfrom fastapi import APIRouter, Depends, Form, HT"
},
{
"path": "app/api/endpoints/mcp.py",
"chars": 10175,
"preview": "from typing import List, Any, Dict, Annotated, Union\n\nfrom fastapi import APIRouter, Depends, HTTPException, Request\nfro"
},
{
"path": "app/api/endpoints/media.py",
"chars": 10253,
"preview": "from pathlib import Path\nfrom typing import List, Any, Union, Annotated, Optional\n\nfrom fastapi import APIRouter, Depend"
},
{
"path": "app/api/endpoints/mediaserver.py",
"chars": 5746,
"preview": "from typing import Any, List, Dict, Optional\n\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.ext.asyncio import "
},
{
"path": "app/api/endpoints/message.py",
"chars": 5986,
"preview": "import json\nfrom typing import Union, Any, List, Optional\n\nfrom fastapi import APIRouter, BackgroundTasks, Depends, Requ"
},
{
"path": "app/api/endpoints/mfa.py",
"chars": 15880,
"preview": "\"\"\"\nMFA (Multi-Factor Authentication) API 端点\n包含 OTP 和 PassKey 相关功能\n\"\"\"\nfrom datetime import timedelta\nfrom typing import"
},
{
"path": "app/api/endpoints/plugin.py",
"chars": 23586,
"preview": "import mimetypes\nimport shutil\nfrom typing import Annotated, Any, List, Optional\n\nimport aiofiles\nfrom anyio import Path"
},
{
"path": "app/api/endpoints/recommend.py",
"chars": 8253,
"preview": "from typing import Any, List, Optional\n\nfrom fastapi import APIRouter, Depends\n\nfrom app import schemas\nfrom app.chain.r"
},
{
"path": "app/api/endpoints/search.py",
"chars": 11714,
"preview": "from typing import List, Any, Optional\n\nfrom fastapi import APIRouter, Depends, Body\n\nfrom app import schemas\nfrom app.c"
},
{
"path": "app/api/endpoints/site.py",
"chars": 15787,
"preview": "from typing import List, Any, Dict, Optional\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy.ext."
},
{
"path": "app/api/endpoints/storage.py",
"chars": 8313,
"preview": "import math\nfrom pathlib import Path\nfrom typing import Any, List, Optional\n\nfrom fastapi import APIRouter, Depends, HTT"
},
{
"path": "app/api/endpoints/subscribe.py",
"chars": 22915,
"preview": "from typing import List, Any, Annotated, Optional\n\nimport cn2an\nfrom fastapi import APIRouter, Request, BackgroundTasks,"
},
{
"path": "app/api/endpoints/system.py",
"chars": 21916,
"preview": "import asyncio\nimport json\nimport re\nfrom collections import deque\nfrom datetime import datetime\nfrom typing import Opti"
},
{
"path": "app/api/endpoints/tmdb.py",
"chars": 4457,
"preview": "from typing import List, Any, Optional\n\nfrom fastapi import APIRouter, Depends\n\nfrom app import schemas\nfrom app.chain.t"
},
{
"path": "app/api/endpoints/torrent.py",
"chars": 7252,
"preview": "from typing import Optional\n\nfrom fastapi import APIRouter, Depends\n\nfrom app import schemas\nfrom app.chain.media import"
},
{
"path": "app/api/endpoints/transfer.py",
"chars": 7675,
"preview": "from pathlib import Path\nfrom typing import Any, List, Annotated, Optional\n\nfrom fastapi import APIRouter, Depends\nfrom "
},
{
"path": "app/api/endpoints/user.py",
"chars": 6415,
"preview": "import base64\nimport re\nfrom typing import Annotated, Any, List, Union\n\nfrom fastapi import APIRouter, Body, Depends, HT"
},
{
"path": "app/api/endpoints/webhook.py",
"chars": 1397,
"preview": "from typing import Any, Annotated\n\nfrom fastapi import APIRouter, BackgroundTasks, Request, Depends\n\nfrom app import sch"
},
{
"path": "app/api/endpoints/workflow.py",
"chars": 11080,
"preview": "import json\nfrom datetime import datetime\nfrom typing import List, Any, Optional\n\nfrom fastapi import APIRouter, Depends"
},
{
"path": "app/api/servarr.py",
"chars": 20298,
"preview": "from typing import Any, List, Annotated\n\nfrom fastapi import APIRouter, HTTPException, Depends\nfrom sqlalchemy.ext.async"
},
{
"path": "app/api/servcookie.py",
"chars": 4175,
"preview": "import gzip\nimport json\nfrom typing import Annotated, Callable, Any, Dict, Optional\n\nimport aiofiles\nfrom anyio import P"
},
{
"path": "app/chain/__init__.py",
"chars": 44234,
"preview": "import copy\nimport inspect\nimport pickle\nimport traceback\nfrom abc import ABCMeta\nfrom collections.abc import Callable\nf"
},
{
"path": "app/chain/ai_recommend.py",
"chars": 10656,
"preview": "import re\nfrom typing import List, Optional, Dict, Any\nimport asyncio\nimport hashlib\nimport json\n\nfrom app.chain import "
},
{
"path": "app/chain/bangumi.py",
"chars": 3460,
"preview": "from typing import Optional, List\n\nfrom app import schemas\nfrom app.chain import ChainBase\nfrom app.core.context import "
},
{
"path": "app/chain/dashboard.py",
"chars": 578,
"preview": "from typing import Optional, List\n\nfrom app import schemas\nfrom app.chain import ChainBase\n\n\nclass DashboardChain(ChainB"
},
{
"path": "app/chain/douban.py",
"chars": 8068,
"preview": "from typing import Optional, List\n\nfrom app import schemas\nfrom app.chain import ChainBase\nfrom app.core.context import "
},
{
"path": "app/chain/download.py",
"chars": 44832,
"preview": "import base64\nimport copy\nimport json\nimport re\nimport time\nfrom pathlib import Path\nfrom typing import List, Optional, "
},
{
"path": "app/chain/media.py",
"chars": 55666,
"preview": "import os\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\nfrom threading import Lock\nfrom typing import"
},
{
"path": "app/chain/mediaserver.py",
"chars": 7838,
"preview": "import threading\nfrom typing import List, Union, Optional, Generator, Any\n\nfrom app.chain import ChainBase\nfrom app.core"
},
{
"path": "app/chain/message.py",
"chars": 44118,
"preview": "import asyncio\nimport re\nimport time\nfrom datetime import datetime, timedelta\nfrom typing import Any, Optional, Dict, Un"
},
{
"path": "app/chain/recommend.py",
"chars": 18704,
"preview": "from typing import List, Optional\n\nimport pillow_avif # noqa 用于自动注册AVIF支持\n\nfrom app.chain import ChainBase\nfrom app.cha"
},
{
"path": "app/chain/search.py",
"chars": 26099,
"preview": "import asyncio\nimport random\nimport time\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom datetime i"
},
{
"path": "app/chain/site.py",
"chars": 33087,
"preview": "import base64\nimport re\nfrom datetime import datetime\nfrom typing import Optional, Tuple, Union, Dict\nfrom urllib.parse "
},
{
"path": "app/chain/storage.py",
"chars": 8058,
"preview": "from pathlib import Path\nfrom typing import Optional, Tuple, List, Dict\n\nfrom app import schemas\nfrom app.chain import C"
},
{
"path": "app/chain/subscribe.py",
"chars": 84812,
"preview": "import copy\nimport json\nimport random\nimport threading\nimport time\nfrom datetime import datetime\nfrom typing import Dict"
},
{
"path": "app/chain/system.py",
"chars": 9866,
"preview": "import json\nimport re\nimport shutil\nfrom pathlib import Path\nfrom typing import Union, Optional\n\nfrom app.chain import C"
},
{
"path": "app/chain/tmdb.py",
"chars": 11844,
"preview": "import random\nfrom typing import Optional, List\n\nfrom app import schemas\nfrom app.chain import ChainBase\nfrom app.core.c"
},
{
"path": "app/chain/torrents.py",
"chars": 14632,
"preview": "import re\nimport traceback\nfrom typing import Dict, List, Union, Optional\n\nfrom app.chain import ChainBase\nfrom app.chai"
},
{
"path": "app/chain/transfer.py",
"chars": 71139,
"preview": "import queue\nimport re\nimport threading\nimport traceback\nfrom copy import deepcopy\nfrom pathlib import Path\nfrom typing "
},
{
"path": "app/chain/tvdb.py",
"chars": 314,
"preview": "from typing import List\n\nfrom app.chain import ChainBase\n\n\nclass TvdbChain(ChainBase):\n \"\"\"\n Tvdb处理链,单例运行\n \"\"\"\n"
},
{
"path": "app/chain/user.py",
"chars": 10707,
"preview": "import secrets\nfrom typing import Optional, Tuple, Union\n\nfrom app.chain import ChainBase\nfrom app.core.config import se"
},
{
"path": "app/chain/webhook.py",
"chars": 501,
"preview": "from typing import Any\n\nfrom app.chain import ChainBase\nfrom app.schemas.types import EventType\n\n\nclass WebhookChain(Cha"
},
{
"path": "app/chain/workflow.py",
"chars": 8372,
"preview": "import base64\nimport pickle\nimport threading\nfrom collections import defaultdict, deque\nfrom concurrent.futures import T"
},
{
"path": "app/command.py",
"chars": 15086,
"preview": "import copy\nimport threading\nimport traceback\nfrom typing import Any, Union, Dict, Optional\n\nfrom app.chain import Chain"
},
{
"path": "app/core/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "app/core/cache.py",
"chars": 41504,
"preview": "import contextvars\nimport inspect\nimport shutil\nimport tempfile\nimport threading\nfrom abc import ABC, abstractmethod\nfro"
},
{
"path": "app/core/config.py",
"chars": 33048,
"preview": "import asyncio\nimport copy\nimport json\nimport os\nimport platform\nimport re\nimport secrets\nimport sys\nimport threading\nfr"
},
{
"path": "app/core/context.py",
"chars": 26460,
"preview": "import re\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import List, Dict, Any, Tup"
},
{
"path": "app/core/event.py",
"chars": 29392,
"preview": "import asyncio\nimport importlib\nimport inspect\nimport random\nimport threading\nimport time\nimport traceback\nimport uuid\nf"
},
{
"path": "app/core/meta/__init__.py",
"chars": 97,
"preview": "from .metabase import MetaBase\nfrom .metavideo import MetaVideo\nfrom .metaanime import MetaAnime\n"
},
{
"path": "app/core/meta/customization.py",
"chars": 1621,
"preview": "import regex as re\n\nfrom app.db.systemconfig_oper import SystemConfigOper\nfrom app.schemas.types import SystemConfigKey\n"
},
{
"path": "app/core/meta/metaanime.py",
"chars": 11877,
"preview": "import re\nimport traceback\n\nimport zhconv\nimport anitopy\nfrom app.core.meta.customization import CustomizationMatcher\nfr"
},
{
"path": "app/core/meta/metabase.py",
"chars": 21061,
"preview": "import traceback\nfrom dataclasses import dataclass\nfrom typing import Union, Optional, List, Self\n\nimport cn2an\nimport r"
},
{
"path": "app/core/meta/metavideo.py",
"chars": 29988,
"preview": "import re\nfrom typing import Optional\n\nfrom Pinyin2Hanzi import is_pinyin\n\nfrom app.core.config import settings\nfrom app"
},
{
"path": "app/core/meta/releasegroup.py",
"chars": 4559,
"preview": "import regex as re\n\nfrom app.db.systemconfig_oper import SystemConfigOper\nfrom app.schemas.types import SystemConfigKey\n"
},
{
"path": "app/core/meta/streamingplatform.py",
"chars": 8908,
"preview": "from typing import Optional, List, Tuple\n\nfrom app.utils.singleton import Singleton\n\n\nclass StreamingPlatforms(metaclass"
},
{
"path": "app/core/meta/words.py",
"chars": 5908,
"preview": "from typing import List, Tuple\n\nimport cn2an\nimport regex as re\n\nfrom app.db.systemconfig_oper import SystemConfigOper\nf"
},
{
"path": "app/core/metainfo.py",
"chars": 7595,
"preview": "from pathlib import Path\nfrom typing import Tuple, List, Optional\n\nimport regex as re\n\nfrom app.core.config import setti"
},
{
"path": "app/core/module.py",
"chars": 5222,
"preview": "import traceback\nfrom typing import Generator, Optional, Tuple, Any, Union, List\n\nfrom app.core.config import settings\nf"
},
{
"path": "app/core/plugin.py",
"chars": 61019,
"preview": "import ast\nimport asyncio\nimport concurrent\nimport concurrent.futures\nimport importlib.util\nimport inspect\nimport os\nimp"
},
{
"path": "app/core/security.py",
"chars": 12742,
"preview": "import base64\nimport datetime\nimport hashlib\nimport hmac\nimport json\nimport os\nimport traceback\nfrom datetime import tim"
},
{
"path": "app/db/__init__.py",
"chars": 14367,
"preview": "import asyncio\nfrom typing import Any, Generator, List, Optional, Self, Tuple, AsyncGenerator, Union\n\nfrom sqlalchemy im"
},
{
"path": "app/db/downloadhistory_oper.py",
"chars": 5271,
"preview": "from typing import List, Optional\n\nfrom app.db import DbOper\nfrom app.db.models.downloadhistory import DownloadHistory, "
},
{
"path": "app/db/init.py",
"chars": 1315,
"preview": "from alembic.command import upgrade\nfrom alembic.config import Config\n\nfrom app.core.config import settings\nfrom app.db "
},
{
"path": "app/db/mediaserver_oper.py",
"chars": 3236,
"preview": "from typing import Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom app.db import DbOper\nfrom app.db.models.mediaserve"
},
{
"path": "app/db/message_oper.py",
"chars": 3295,
"preview": "import time\nfrom typing import Optional, Union\n\nfrom sqlalchemy.orm import Session\n\nfrom app.db import DbOper\nfrom app.d"
},
{
"path": "app/db/models/__init__.py",
"chars": 425,
"preview": "from .downloadhistory import DownloadHistory, DownloadFiles\nfrom .mediaserver import MediaServerItem\nfrom .passkey impor"
},
{
"path": "app/db/models/downloadhistory.py",
"chars": 9057,
"preview": "import time\nfrom typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, JSON, select\nfrom sqlalchemy.ex"
},
{
"path": "app/db/models/mediaserver.py",
"chars": 3831,
"preview": "from datetime import datetime\nfrom typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, JSON\nfrom sql"
},
{
"path": "app/db/models/message.py",
"chars": 1307,
"preview": "from typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, JSON, select\nfrom sqlalchemy.ext.asyncio im"
},
{
"path": "app/db/models/passkey.py",
"chars": 4037,
"preview": "from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, select, ForeignKey\nfrom sqlalchemy.ext.asyncio "
},
{
"path": "app/db/models/plugindata.py",
"chars": 1234,
"preview": "from sqlalchemy import Column, String, JSON\nfrom sqlalchemy.orm import Session\n\nfrom app.db import db_query, db_update, "
},
{
"path": "app/db/models/site.py",
"chars": 3050,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, Column, Integer, String, JSON, select, delete\nfrom sqlalc"
},
{
"path": "app/db/models/siteicon.py",
"chars": 849,
"preview": "from sqlalchemy import Column, String, select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy.orm import"
},
{
"path": "app/db/models/sitestatistic.py",
"chars": 1180,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Column, Integer, String, JSON, select\nfrom sqlalchemy.ext.asyncio "
},
{
"path": "app/db/models/siteuserdata.py",
"chars": 4170,
"preview": "from datetime import datetime\nfrom typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, Float, JSON, "
},
{
"path": "app/db/models/subscribe.py",
"chars": 13205,
"preview": "import time\nfrom typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, Float, JSON, select\nfrom sqlalc"
},
{
"path": "app/db/models/subscribehistory.py",
"chars": 3774,
"preview": "from typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, Float, JSON, select\nfrom sqlalchemy.ext.asy"
},
{
"path": "app/db/models/systemconfig.py",
"chars": 937,
"preview": "from sqlalchemy import Column, String, JSON, select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy.orm "
},
{
"path": "app/db/models/transferhistory.py",
"chars": 12075,
"preview": "import time\nfrom typing import Optional\n\nfrom sqlalchemy import Column, Integer, String, Boolean, func, or_, JSON, selec"
},
{
"path": "app/db/models/user.py",
"chars": 3162,
"preview": "from sqlalchemy import Boolean, Column, JSON, String, select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalc"
},
{
"path": "app/db/models/userconfig.py",
"chars": 1025,
"preview": "from sqlalchemy import Column, String, UniqueConstraint, Index, JSON\nfrom sqlalchemy.orm import Session\n\nfrom app.db imp"
},
{
"path": "app/db/models/workflow.py",
"chars": 7748,
"preview": "from datetime import datetime\nfrom typing import Optional\n\nfrom sqlalchemy import Column, Integer, JSON, String, and_, o"
},
{
"path": "app/db/plugindata_oper.py",
"chars": 1690,
"preview": "from typing import Any, Optional\n\nfrom app.db import DbOper\nfrom app.db.models.plugindata import PluginData\n\n\nclass Plug"
},
{
"path": "app/db/site_oper.py",
"chars": 9596,
"preview": "from datetime import datetime\nfrom typing import List, Tuple, Optional\n\nfrom app.db import DbOper\nfrom app.db.models imp"
},
{
"path": "app/db/subscribe_oper.py",
"chars": 7675,
"preview": "import time\nfrom typing import Tuple, List, Optional\n\nfrom app.core.context import MediaInfo\nfrom app.db import DbOper\nf"
},
{
"path": "app/db/systemconfig_oper.py",
"chars": 3962,
"preview": "import asyncio\nimport copy\nimport threading\nfrom typing import Any, Optional, Union\n\nfrom app.db import DbOper\nfrom app."
},
{
"path": "app/db/transferhistory_oper.py",
"chars": 7446,
"preview": "import time\nfrom typing import Any, List, Optional\n\nfrom app.core.context import MediaInfo\nfrom app.core.meta import Met"
},
{
"path": "app/db/user_oper.py",
"chars": 3541,
"preview": "from typing import Optional, List\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.ext.asyncio import AsyncSe"
},
{
"path": "app/db/userconfig_oper.py",
"chars": 2607,
"preview": "from typing import Any, Union, Dict, Optional\n\nfrom app.db import DbOper\nfrom app.db.models.userconfig import UserConfig"
},
{
"path": "app/db/workflow_oper.py",
"chars": 2620,
"preview": "from typing import List, Tuple, Optional, Any, Coroutine, Sequence\n\nfrom app.db import DbOper\nfrom app.db.models.workflo"
},
{
"path": "app/factory.py",
"chars": 653,
"preview": "from fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\n\nfrom app.core.config import settings\nfro"
},
{
"path": "app/helper/__init__.py",
"chars": 40,
"preview": "from .cloudflare import under_challenge\n"
},
{
"path": "app/helper/browser.py",
"chars": 10122,
"preview": "import uuid\nfrom typing import Callable, Any, Optional\n\nfrom cf_clearance import sync_cf_retry, sync_stealth\nfrom playwr"
},
{
"path": "app/helper/cloudflare.py",
"chars": 1151,
"preview": "import os\n\nfrom pyquery import PyQuery\n\nfrom app.log import logger\n\nCHALLENGE_TITLES = [\n # Cloudflare\n 'Just a mo"
},
{
"path": "app/helper/cookie.py",
"chars": 10168,
"preview": "import base64\nfrom typing import Tuple, Optional\n\nfrom lxml import etree\nfrom playwright.sync_api import Page\n\nfrom app."
},
{
"path": "app/helper/cookiecloud.py",
"chars": 4761,
"preview": "import json\nfrom typing import Any, Dict, Tuple, Optional\n\nfrom app.core.config import settings\nfrom app.log import logg"
},
{
"path": "app/helper/directory.py",
"chars": 5990,
"preview": "import re\nfrom pathlib import Path\nfrom typing import List, Optional, Tuple\n\nfrom app import schemas\nfrom app.core.conte"
},
{
"path": "app/helper/display.py",
"chars": 721,
"preview": "from pyvirtualdisplay import Display\n\nfrom app.log import logger\nfrom app.utils.singleton import Singleton\nfrom app.util"
},
{
"path": "app/helper/doh.py",
"chars": 5048,
"preview": "\"\"\"\ndoh函数的实现。\nauthor: https://github.com/C5H12O5/syno-videoinfo-plugin\n\"\"\"\nimport base64\nimport concurrent\nimport concur"
},
{
"path": "app/helper/downloader.py",
"chars": 1090,
"preview": "from typing import Optional\n\nfrom app.helper.service import ServiceBaseHelper\nfrom app.schemas import DownloaderConf, Se"
},
{
"path": "app/helper/format.py",
"chars": 5258,
"preview": "import re\nfrom typing import Tuple, Optional\n\nimport parse\n\nfrom app.core.meta.metabase import MetaBase\n\n\nclass FormatPa"
},
{
"path": "app/helper/image.py",
"chars": 9358,
"preview": "import io\nfrom pathlib import Path\nfrom typing import Optional, List\n\nfrom PIL import Image\n\nfrom app.chain.mediaserver "
},
{
"path": "app/helper/llm.py",
"chars": 3829,
"preview": "\"\"\"LLM模型相关辅助功能\"\"\"\nfrom typing import List, Optional\n\nfrom app.core.config import settings\nfrom app.log import logger\n\n\nc"
},
{
"path": "app/helper/mediaserver.py",
"chars": 1089,
"preview": "from typing import Optional\n\nfrom app.helper.service import ServiceBaseHelper\nfrom app.schemas import MediaServerConf, S"
},
{
"path": "app/helper/message.py",
"chars": 25461,
"preview": "from __future__ import annotations\n\nimport ast\nimport json\nimport queue\nimport re\nimport threading\nimport time\nfrom date"
},
{
"path": "app/helper/module.py",
"chars": 4507,
"preview": "# -*- coding: utf-8 -*-\nimport importlib\nimport pkgutil\nimport traceback\nfrom pathlib import Path\nfrom typing import Lis"
},
{
"path": "app/helper/nfo.py",
"chars": 568,
"preview": "import xml.etree.ElementTree as ET\nfrom pathlib import Path\nfrom typing import List, Optional\n\n\nclass NfoReader:\n def"
},
{
"path": "app/helper/notification.py",
"chars": 1107,
"preview": "from typing import Optional\n\nfrom app.helper.service import ServiceBaseHelper\nfrom app.schemas import NotificationConf, "
},
{
"path": "app/helper/ocr.py",
"chars": 1173,
"preview": "import base64\nfrom typing import Optional\n\nfrom app.core.config import settings\nfrom app.utils.http import RequestUtils\n"
},
{
"path": "app/helper/passkey.py",
"chars": 12022,
"preview": "\"\"\"\nPassKey WebAuthn 辅助工具类\n\"\"\"\nimport base64\nimport json\nimport binascii\nfrom typing import Optional, Tuple, List, Dict,"
},
{
"path": "app/helper/plugin.py",
"chars": 65380,
"preview": "import importlib\nimport io\nimport json\nimport shutil\nimport site\nimport sys\nimport traceback\nimport zipfile\nfrom pathlib"
},
{
"path": "app/helper/progress.py",
"chars": 1842,
"preview": "from enum import Enum\nfrom typing import Union, Optional\n\nfrom app.core.cache import TTLCache\nfrom app.schemas.types imp"
},
{
"path": "app/helper/redis.py",
"chars": 17304,
"preview": "import json\nimport pickle\nfrom typing import Any, Optional, Generator, Tuple, AsyncGenerator, Union\nfrom urllib.parse im"
},
{
"path": "app/helper/resource.py",
"chars": 5353,
"preview": "import json\nfrom pathlib import Path\n\nfrom app.core.config import settings\nfrom app.helper.sites import SitesHelper # n"
},
{
"path": "app/helper/rss.py",
"chars": 18071,
"preview": "import re\nimport traceback\nfrom typing import List, Tuple, Union, Optional\nfrom urllib.parse import urljoin\n\nimport char"
},
{
"path": "app/helper/rule.py",
"chars": 2146,
"preview": "from typing import List, Optional\n\nfrom app.core.context import MediaInfo\nfrom app.db.systemconfig_oper import SystemCon"
},
{
"path": "app/helper/service.py",
"chars": 5420,
"preview": "from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator\n\nfrom app.core.module import ModuleManager\nfro"
},
{
"path": "app/helper/storage.py",
"chars": 2302,
"preview": "from typing import List, Optional\n\nfrom app import schemas\nfrom app.db.systemconfig_oper import SystemConfigOper\nfrom ap"
},
{
"path": "app/helper/subscribe.py",
"chars": 16668,
"preview": "from threading import Thread\nfrom typing import List, Tuple, Optional\n\nfrom app.core.cache import cached\nfrom app.core.c"
},
{
"path": "app/helper/system.py",
"chars": 5864,
"preview": "import os\nimport signal\nimport threading\nimport time\nfrom pathlib import Path\nfrom typing import Tuple\n\nimport docker\n\nf"
},
{
"path": "app/helper/thread.py",
"chars": 645,
"preview": "from concurrent.futures import ThreadPoolExecutor\n\nfrom app.core.config import settings\nfrom app.utils.singleton import "
},
{
"path": "app/helper/torrent.py",
"chars": 21620,
"preview": "import datetime\nimport re\nfrom pathlib import Path\nfrom typing import Tuple, Optional, List, Union, Dict, Any\nfrom urlli"
},
{
"path": "app/helper/twofa.py",
"chars": 1280,
"preview": "import base64\nimport hashlib\nimport hmac\nimport struct\nimport sys\nimport time\n\nfrom app.log import logger\n\n\nclass TwoFac"
},
{
"path": "app/helper/workflow.py",
"chars": 9032,
"preview": "import json\nfrom typing import List, Tuple, Optional\n\nfrom app.core.cache import cached\nfrom app.core.config import sett"
}
]
// ... and 271 more files (download for full content)
About this extraction
This page contains the full source code of the jxxghp/MoviePilot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 471 files (3.8 MB), approximately 1.0M tokens, and a symbol index with 5429 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.