├── yby6_video_mcp_server
├── functionality
│ ├── README.md
│ ├── __pycache__
│ │ ├── base.cpython-310.pyc
│ │ ├── huya.cpython-310.pyc
│ │ ├── acfun.cpython-310.pyc
│ │ ├── doupai.cpython-310.pyc
│ │ ├── douyin.cpython-310.pyc
│ │ ├── haokan.cpython-310.pyc
│ │ ├── lvzhou.cpython-310.pyc
│ │ ├── meipai.cpython-310.pyc
│ │ ├── pipixia.cpython-310.pyc
│ │ ├── quanmin.cpython-310.pyc
│ │ ├── redbook.cpython-310.pyc
│ │ ├── sixroom.cpython-310.pyc
│ │ ├── weibo.cpython-310.pyc
│ │ ├── weishi.cpython-310.pyc
│ │ ├── xigua.cpython-310.pyc
│ │ ├── zuiyou.cpython-310.pyc
│ │ ├── __init__.cpython-310.pyc
│ │ ├── kuaishou.cpython-310.pyc
│ │ ├── lishipin.cpython-310.pyc
│ │ ├── pipigaoxiao.cpython-310.pyc
│ │ ├── quanminkge.cpython-310.pyc
│ │ └── xinpianchang.cpython-310.pyc
│ ├── doupai.py
│ ├── haokan.py
│ ├── lvzhou.py
│ ├── zuiyou.py
│ ├── weishi.py
│ ├── huya.py
│ ├── lishipin.py
│ ├── quanminkge.py
│ ├── sixroom.py
│ ├── quanmin.py
│ ├── pipigaoxiao.py
│ ├── weibo.py
│ ├── xinpianchang.py
│ ├── acfun.py
│ ├── base.py
│ ├── pipixia.py
│ ├── xigua.py
│ ├── redbook.py
│ ├── meipai.py
│ ├── kuaishou.py
│ ├── video_processor.py
│ ├── douyin.py
│ └── __init__.py
├── __pycache__
│ ├── server.cpython-310.pyc
│ └── __init__.cpython-310.pyc
├── utils
│ ├── __pycache__
│ │ ├── __init__.cpython-310.pyc
│ │ ├── __init__.cpython-311.pyc
│ │ └── __init__.cpython-312.pyc
│ ├── constants.py
│ ├── responses.py
│ ├── __init__.py
│ ├── helpers.py
│ ├── config.py
│ └── tools.py
├── __init__.py
└── server.py
├── .gitignore
├── .dockerignore
├── Dockerfile.base
├── Dockerfile.mcp
├── requirements.txt
├── LICENSE
├── pyproject.toml
├── script
├── deployBase.bat
├── deployMcp.bat
└── deployMcp.sh
├── README.md
└── README.en.md
/yby6_video_mcp_server/functionality/ README.md:
--------------------------------------------------------------------------------
1 | # 这里存放视频MCP服务器的功能实现
--------------------------------------------------------------------------------
/yby6_video_mcp_server/__pycache__/server.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/__pycache__/server.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/utils/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/__pycache__/__init__.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/utils/__pycache__/__init__.cpython-311.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/__pycache__/__init__.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/utils/__pycache__/__init__.cpython-312.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/base.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/base.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/huya.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/huya.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/acfun.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/acfun.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/doupai.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/doupai.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/douyin.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/douyin.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/haokan.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/haokan.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/lvzhou.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/lvzhou.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/meipai.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/meipai.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/pipixia.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/pipixia.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/quanmin.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/quanmin.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/redbook.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/redbook.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/sixroom.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/sixroom.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/weibo.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/weibo.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/weishi.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/weishi.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/xigua.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/xigua.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/zuiyou.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/zuiyou.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/kuaishou.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/kuaishou.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/lishipin.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/lishipin.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/pipigaoxiao.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/pipigaoxiao.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/quanminkge.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/quanminkge.cpython-310.pyc
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__pycache__/xinpianchang.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangbuyiya/yby6-crawling-short-video-mcp/HEAD/yby6_video_mcp_server/functionality/__pycache__/xinpianchang.cpython-310.pyc
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .pypirc
2 | .venv
3 | venv
4 | .env
5 | .DS_Store
6 | __pycache__
7 | *.pyc
8 | *.pyo
9 | *.pyd
10 | *.pyw
11 | *.pyz
12 | upload_pypi
13 | scripts/.local-config
14 | .local-config
15 | dist
16 | .idea
17 | uv.lock
18 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 全网短视频去水印链接提取 MCP服务
3 | """
4 |
5 | __version__ = "1.0.2"
6 | __author__ = "yangbuyiya"
7 | __email__ = "yangbuyiya@duck.com"
8 | __url__ = "https://github.com/yangbuyiya/yby6-crawling-short-video-mcp"
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/constants.py:
--------------------------------------------------------------------------------
1 | # 常量定义模块
2 |
3 | # URL正则表达式模式
4 | URL_REGEX_PATTERN = r"http[s]?:\/\/[\w.-]+[\w\/-]*[\w.-]*\??[\w=&:\-\+\%]*[/]*"
5 |
6 | # 默认响应代码
7 | DEFAULT_RESPONSE_CODES = {
8 | 'SUCCESS': 200,
9 | 'ERROR': 500
10 | }
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/responses.py:
--------------------------------------------------------------------------------
1 | # 响应处理模块
2 |
3 | from typing import Dict, Any
4 | from .constants import DEFAULT_RESPONSE_CODES
5 |
6 |
7 | def create_success_response(msg: str, data: Any) -> Dict[str, Any]:
8 | """创建成功响应"""
9 | return {
10 | "code": DEFAULT_RESPONSE_CODES['SUCCESS'],
11 | "msg": msg,
12 | "data": data
13 | }
14 |
15 |
16 | def create_error_response(msg: str) -> Dict[str, Any]:
17 | """创建错误响应"""
18 | return {
19 | "code": DEFAULT_RESPONSE_CODES['ERROR'],
20 | "msg": msg
21 | }
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Git
2 | .git
3 | .gitignore
4 |
5 | # Python
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 | *.so
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | venv/
27 |
28 | # Docker
29 | Dockerfile
30 | Dockerfile.combined
31 | docker-compose.yml
32 | .dockerignore
33 |
34 | # IDE
35 | .idea/
36 | .vscode/
37 | *.swp
38 | *.swo
39 |
40 | # 其他
41 | .DS_Store
42 | *.log
43 | result.html
44 | *.md
45 | LICENSE
--------------------------------------------------------------------------------
/Dockerfile.base:
--------------------------------------------------------------------------------
1 | # Dockerfile.base
2 | FROM python:3.11-slim
3 |
4 | # 创建新的 sources.list 文件,使用阿里云镜像源
5 | RUN echo 'deb http://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware' > /etc/apt/sources.list && \
6 | echo 'deb http://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware' >> /etc/apt/sources.list && \
7 | echo 'deb http://mirrors.aliyun.com/debian-security/ bookworm-security main contrib non-free non-free-firmware' >> /etc/apt/sources.list
8 |
9 | # 安装 FFmpeg(只在基础镜像中执行一次)
10 | RUN apt-get update && \
11 | apt-get install -y --no-install-recommends ffmpeg && \
12 | rm -rf /var/lib/apt/lists/*
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import parse_qs, urlparse
2 |
3 | # 导入新创建的模块
4 | from .constants import URL_REGEX_PATTERN, DEFAULT_RESPONSE_CODES
5 | from .responses import create_success_response, create_error_response
6 | from .helpers import extract_url_from_text, get_val_from_url_by_query_key
7 | from .config import get_api_configuration
8 | from .tools import share_url_parse_tool, video_id_parse_tool, share_text_parse_tool
9 |
10 | # 导出所有公共接口
11 | __all__ = [
12 | 'URL_REGEX_PATTERN',
13 | 'DEFAULT_RESPONSE_CODES',
14 | 'create_success_response',
15 | 'create_error_response',
16 | 'extract_url_from_text',
17 | 'get_api_configuration',
18 | 'share_url_parse_tool',
19 | 'video_id_parse_tool',
20 | 'share_text_parse_tool',
21 | 'get_val_from_url_by_query_key',
22 | ]
23 |
--------------------------------------------------------------------------------
/Dockerfile.mcp:
--------------------------------------------------------------------------------
1 | # 从自定义基础镜像继承(包含预安装的 FFmpeg、python:3.11-slim)
2 | FROM registry.cn-hangzhou.aliyuncs.com/yby6/ffmpeg-python-base:1.0.0
3 |
4 | # 设置工作目录
5 | WORKDIR /app
6 |
7 | # 1. 先只复制 requirements.txt 分步 COPY,利用 Docker 缓存
8 | COPY ./requirements.txt /app/
9 |
10 | # 2. 安装依赖(只要 requirements.txt 没变,这一步就会用缓存)
11 | RUN pip install --no-cache-dir -v -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
12 |
13 | # 验证ffmpeg-python安装成功
14 | RUN python -c "import ffmpeg; print('ffmpeg-python installed successfully')"
15 |
16 | # 3. 只复制必要的代码文件
17 | COPY ./yby6_video_mcp_server /app/yby6_video_mcp_server/
18 | COPY ./pyproject.toml /app/
19 |
20 | # 暴露 MCP 服务的端口
21 | EXPOSE 8637
22 |
23 | # 启动 MCP 服务 外部传递参数 例如:--transport http --host 0.0.0.0 --port 8637
24 | CMD ["python", "-m", "yby6_video_mcp_server.server", "--transport", "sse", "--host", "0.0.0.0", "--port", "8637"]
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/helpers.py:
--------------------------------------------------------------------------------
1 | # 辅助函数模块
2 |
3 | import re
4 | from typing import Optional
5 | from urllib.parse import parse_qs, urlparse
6 | from .constants import URL_REGEX_PATTERN
7 |
8 |
9 | def extract_url_from_text(text: str) -> Optional[str]:
10 | """从文本中提取URL"""
11 | url_reg = re.compile(URL_REGEX_PATTERN)
12 | match = url_reg.search(text)
13 | return match.group() if match else None
14 |
15 |
16 | def get_val_from_url_by_query_key(url: str, query_key: str) -> str:
17 | """
18 | 从url的query参数中解析出query_key对应的值
19 | :param url: url地址
20 | :param query_key: query参数的key
21 | :return:
22 | """
23 | url_res = urlparse(url)
24 | url_query = parse_qs(url_res.query, keep_blank_values=True)
25 |
26 | try:
27 | query_val = url_query[query_key][0]
28 | except KeyError:
29 | raise KeyError(f"url中不存在query参数: {query_key}")
30 |
31 | if len(query_val) == 0:
32 | raise ValueError(f"url中query参数值长度为0: {query_key}")
33 |
34 | return url_query[query_key][0]
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==23.2.1
2 | annotated-types==0.6.0
3 | anyio>=4.5
4 | black==24.3.0
5 | certifi==2024.2.2
6 | cfgv==3.4.0
7 | click==8.1.7
8 | colorama==0.4.6
9 | cssselect==1.2.0
10 | distlib==0.3.8
11 | exceptiongroup>=1.2.2
12 | fake-useragent==1.5.1
13 | Faker==24.4.0
14 | filelock==3.13.3
15 | flake8==7.0.0
16 | ffmpeg-python==0.2.0
17 | h11==0.14.0
18 | httpcore==1.0.5
19 | httpx>=0.28.1
20 | identify==2.5.35
21 | idna==3.6
22 | isort==5.13.2
23 | Jinja2==3.1.3
24 | jmespath==1.0.1
25 | lxml==5.4.0
26 | markdown-it-py==3.0.0
27 | MarkupSafe==2.1.5
28 | mccabe==0.7.0
29 | mdurl==0.1.2
30 | mypy-extensions==1.0.0
31 | nodeenv==1.8.0
32 | packaging==24.0
33 | parsel==1.9.0
34 | pathspec==0.12.1
35 | platformdirs==4.2.0
36 | pre-commit==3.7.0
37 | pycodestyle==2.11.1
38 | pydantic>=2.11.7
39 | pyflakes==3.2.0
40 | Pygments==2.17.2
41 | PyJWT==2.8.0
42 | python-dateutil==2.9.0.post0
43 | python-dotenv>=1.1.0
44 | PyYAML==6.0.1
45 | rich>=13.9.4
46 | shellingham==1.5.4
47 | six==1.16.0
48 | sniffio==1.3.1
49 | tomli==2.0.1
50 | typer>=0.15.2
51 | virtualenv==20.25.1
52 | w3lib==2.1.2
53 | fastmcp==2.10.2
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 yangbuyiya
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/doupai.py:
--------------------------------------------------------------------------------
1 | import httpx
2 |
3 | from ..utils.helpers import get_val_from_url_by_query_key
4 |
5 | from .base import BaseParser, VideoAuthor, VideoInfo
6 |
7 |
8 | class DouPai(BaseParser):
9 | """
10 | 逗拍
11 | """
12 |
13 | async def parse_share_url(self, share_url: str) -> VideoInfo:
14 | video_id = get_val_from_url_by_query_key(share_url, "id")
15 | return await self.parse_video_id(video_id)
16 |
17 | async def parse_video_id(self, video_id: str) -> VideoInfo:
18 | req_url = f"https://v2.doupai.cc/topic/{video_id}.json"
19 | async with httpx.AsyncClient() as client:
20 | response = await client.get(req_url, headers=self.get_default_headers())
21 | response.raise_for_status()
22 |
23 | json_data = response.json()
24 | data = json_data["data"]
25 |
26 | video_info = VideoInfo(
27 | video_url=data["videoUrl"],
28 | cover_url=data["imageUrl"],
29 | title=data["name"],
30 | author=VideoAuthor(
31 | uid=data["userId"]["id"],
32 | name=data["userId"]["name"],
33 | avatar=data["userId"]["avatar"],
34 | ),
35 | )
36 | return video_info
37 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/haokan.py:
--------------------------------------------------------------------------------
1 | import httpx
2 |
3 | from ..utils.helpers import get_val_from_url_by_query_key
4 |
5 | from .base import BaseParser, VideoAuthor, VideoInfo
6 |
7 |
8 | class HaoKan(BaseParser):
9 | """
10 | 好看视频
11 | """
12 |
13 | async def parse_share_url(self, share_url: str) -> VideoInfo:
14 | video_id = get_val_from_url_by_query_key(share_url, "vid")
15 | return await self.parse_video_id(video_id)
16 |
17 | async def parse_video_id(self, video_id: str) -> VideoInfo:
18 | req_url = f"https://haokan.baidu.com/v?_format=json&vid={video_id}"
19 | async with httpx.AsyncClient() as client:
20 | response = await client.get(req_url, headers=self.get_default_headers())
21 | response.raise_for_status()
22 |
23 | json_data = response.json()
24 | # 接口返回错误
25 | if json_data["errno"] != 0:
26 | raise Exception(json_data["error"])
27 |
28 | video_data = json_data["data"]["apiData"]["curVideoMeta"]
29 | user_data = video_data["mth"]
30 |
31 | video_info = VideoInfo(
32 | video_url=video_data["playurl"],
33 | cover_url=video_data["poster"],
34 | title=video_data["title"],
35 | author=VideoAuthor(
36 | uid=user_data["mthid"],
37 | name=user_data["author_name"],
38 | avatar=user_data["author_photo"],
39 | ),
40 | )
41 | return video_info
42 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/lvzhou.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import httpx
4 | from parsel import Selector
5 |
6 | from .base import BaseParser, VideoAuthor, VideoInfo
7 |
8 |
9 | class LvZhou(BaseParser):
10 | """
11 | 绿洲
12 | """
13 |
14 | async def parse_share_url(self, share_url: str) -> VideoInfo:
15 | async with httpx.AsyncClient() as client:
16 | response = await client.get(share_url, headers=self.get_default_headers())
17 | response.raise_for_status()
18 |
19 | sel = Selector(response.text)
20 |
21 | video_url = sel.css("video::attr(src)").get()
22 | author_avatar = sel.css("a.avatar img::attr(src)").get()
23 | video_cover_style = sel.css("div.video-cover::attr(style)").get(default="")
24 |
25 | cover_url = ""
26 | if video_cover_style:
27 | match = re.search(r"background-image:url\((.*)\)", video_cover_style)
28 | if match:
29 | cover_url = match.group(1)
30 |
31 | title = sel.css("div.status-title::text").get()
32 | author_name = sel.css("div.nickname::text").get()
33 |
34 | return VideoInfo(
35 | video_url=video_url,
36 | cover_url=cover_url,
37 | title=title,
38 | author=VideoAuthor(
39 | name=author_name,
40 | avatar=author_avatar,
41 | ),
42 | )
43 |
44 | async def parse_video_id(self, video_id: str) -> VideoInfo:
45 | share_url = f"https://m.oasis.weibo.cn/v1/h5/share?sid={video_id}"
46 | return await self.parse_share_url(share_url)
47 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/zuiyou.py:
--------------------------------------------------------------------------------
1 | import httpx
2 |
3 | from ..utils.helpers import get_val_from_url_by_query_key
4 |
5 | from .base import BaseParser, VideoAuthor, VideoInfo
6 |
7 |
8 | class ZuiYou(BaseParser):
9 | """
10 | 最右
11 | """
12 |
13 | async def parse_share_url(self, share_url: str) -> VideoInfo:
14 | video_id = get_val_from_url_by_query_key(share_url, "pid")
15 | return await self.parse_video_id(video_id)
16 |
17 | async def parse_video_id(self, video_id: str) -> VideoInfo:
18 | int_video_id = int(video_id)
19 | req_url = "https://share.xiaochuankeji.cn/planck/share/post/detail_h5"
20 | post_data = {
21 | "h_av": "5.2.13.011",
22 | "pid": int_video_id,
23 | }
24 | async with httpx.AsyncClient(follow_redirects=True) as client:
25 | response = await client.post(
26 | req_url, headers=self.get_default_headers(), json=post_data
27 | )
28 | response.raise_for_status()
29 |
30 | json_data = response.json()
31 | data = json_data["data"]["post"]
32 | video_key = str(data["imgs"][0]["id"])
33 |
34 | video_info = VideoInfo(
35 | video_url=data["videos"][video_key]["url"],
36 | cover_url="",
37 | title=data["content"],
38 | author=VideoAuthor(
39 | uid=str(data["member"]["id"]),
40 | name=data["member"]["name"],
41 | avatar=data["member"]["avatar_urls"]["origin"]["urls"][0],
42 | ),
43 | )
44 | return video_info
45 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/weishi.py:
--------------------------------------------------------------------------------
1 | import httpx
2 |
3 | from ..utils.helpers import get_val_from_url_by_query_key
4 |
5 | from .base import BaseParser, VideoAuthor, VideoInfo
6 |
7 |
8 | class WeiShi(BaseParser):
9 | """
10 | 微视
11 | """
12 |
13 | async def parse_share_url(self, share_url: str) -> VideoInfo:
14 | video_id = get_val_from_url_by_query_key(share_url, "id")
15 | return await self.parse_video_id(video_id)
16 |
17 | async def parse_video_id(self, video_id: str) -> VideoInfo:
18 | req_url = (
19 | "https://h5.weishi.qq.com/webapp/json/weishi/WSH5GetPlayPage"
20 | f"?feedid={video_id}"
21 | )
22 | async with httpx.AsyncClient() as client:
23 | response = await client.get(req_url, headers=self.get_default_headers())
24 | response.raise_for_status()
25 |
26 | json_data = response.json()
27 | # 接口返回错误
28 | if json_data["ret"] != 0:
29 | raise Exception(json_data["msg"])
30 | # 视频状态错误
31 | if len(json_data["data"]["errmsg"]) > 0:
32 | raise Exception(json_data["data"]["errmsg"])
33 |
34 | data = json_data["data"]["feeds"][0]
35 |
36 | video_info = VideoInfo(
37 | video_url=data["video_url"],
38 | cover_url=data["images"][0]["url"],
39 | title=data["feed_desc_withat"],
40 | author=VideoAuthor(
41 | uid=data["id"],
42 | name=data["poster"]["nick"],
43 | avatar=data["poster"]["avatar"],
44 | ),
45 | )
46 | return video_info
47 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/huya.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import fake_useragent
4 | import httpx
5 |
6 | from .base import BaseParser, VideoAuthor, VideoInfo
7 |
8 |
9 | class HuYa(BaseParser):
10 | """
11 | 虎牙
12 | """
13 |
14 | async def parse_share_url(self, share_url: str) -> VideoInfo:
15 | re_pattern = r"\/(\d+).html"
16 | re_result = re.search(re_pattern, share_url)
17 |
18 | if not re_result:
19 | raise Exception("parse video_id from share url fail")
20 |
21 | video_id = re_result.group(1)
22 | return await self.parse_video_id(video_id)
23 |
24 | async def parse_video_id(self, video_id: str) -> VideoInfo:
25 | req_url = f"https://liveapi.huya.com/moment/getMomentContent?videoId={video_id}"
26 | async with httpx.AsyncClient() as client:
27 | headers = {
28 | "User-Agent": fake_useragent.UserAgent(os=["windows"]).random,
29 | "Referer": "https://v.huya.com/",
30 | }
31 | response = await client.get(req_url, headers=headers)
32 | response.raise_for_status()
33 |
34 | json_data = response.json()
35 | data = json_data["data"]["moment"]["videoInfo"]
36 | if data["uid"] == 0:
37 | raise Exception("video not found")
38 |
39 | video_info = VideoInfo(
40 | video_url=data["definitions"][0]["url"],
41 | cover_url=data["videoCover"],
42 | title=data["videoTitle"],
43 | author=VideoAuthor(
44 | uid=str(data["uid"]),
45 | name=data["actorNick"],
46 | avatar=data["actorAvatarUrl"],
47 | ),
48 | )
49 | return video_info
50 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/lishipin.py:
--------------------------------------------------------------------------------
1 | import time
2 | from urllib.parse import urlparse
3 |
4 | import fake_useragent
5 | import httpx
6 |
7 | from .base import BaseParser, VideoInfo
8 |
9 |
10 | class LiShiPin(BaseParser):
11 | """
12 | 梨视频
13 | """
14 |
15 | async def parse_share_url(self, share_url: str) -> VideoInfo:
16 | url_res = urlparse(share_url)
17 |
18 | video_id = url_res.path.replace("/detail_", "")
19 | if len(video_id) == 0:
20 | raise ValueError("parse video_id from share url fail")
21 |
22 | return await self.parse_video_id(video_id)
23 |
24 | async def parse_video_id(self, video_id: str) -> VideoInfo:
25 | now = int(time.time())
26 | req_url = (
27 | f"https://www.pearvideo.com/videoStatus.jsp?contId={video_id}&mrd={now}"
28 | )
29 |
30 | async with httpx.AsyncClient() as client:
31 | headers = {
32 | "Referer": f"https://www.pearvideo.com/detail_{video_id}",
33 | "User-Agent": fake_useragent.UserAgent(os=["windows"]).random,
34 | }
35 | response = await client.get(req_url, headers=headers)
36 |
37 | if response.status_code != 200:
38 | raise Exception("failed to fetch data")
39 |
40 | json_data = response.json()
41 |
42 | # 获取 videoInfo 字段的值
43 | video_src_url = json_data["videoInfo"]["videos"]["srcUrl"]
44 | timer = json_data["systemTime"]
45 | video_url = video_src_url.replace(timer, f"cont-{video_id}")
46 | cover_url = json_data["videoInfo"]["video_image"]
47 |
48 | return VideoInfo(
49 | video_url=video_url,
50 | cover_url=cover_url,
51 | )
52 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/quanminkge.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | import fake_useragent
5 | import httpx
6 |
7 | from ..utils.helpers import get_val_from_url_by_query_key
8 |
9 | from .base import BaseParser, VideoAuthor, VideoInfo
10 |
11 |
12 | class QuanMinKGe(BaseParser):
13 | """
14 | 全民K歌
15 | """
16 |
17 | async def parse_share_url(self, share_url: str) -> VideoInfo:
18 | video_id = get_val_from_url_by_query_key(share_url, "s")
19 | return await self.parse_video_id(video_id)
20 |
21 | async def parse_video_id(self, video_id: str) -> VideoInfo:
22 | req_url = f"https://kg.qq.com/node/play?s={video_id}"
23 | async with httpx.AsyncClient() as client:
24 | headers = {
25 | "User-Agent": fake_useragent.UserAgent(os="windows").random,
26 | }
27 | response = await client.get(req_url, headers=headers)
28 | response.raise_for_status()
29 |
30 | re_pattern = r"window.__DATA__ = (.*?); "
31 | re_result = re.search(re_pattern, response.text)
32 |
33 | if not re_result or len(re_result.groups()) < 1:
34 | raise Exception("failed to parse video JSON info from HTML")
35 |
36 | json_text = re_result.group(1).strip()
37 | json_data = json.loads(json_text)
38 | data = json_data["detail"]
39 |
40 | video_info = VideoInfo(
41 | video_url=data["playurl_video"],
42 | cover_url=data["cover"],
43 | title=data["content"],
44 | author=VideoAuthor(
45 | uid=data["uid"],
46 | name=data["nick"],
47 | avatar=data["avatar"],
48 | ),
49 | )
50 | return video_info
51 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "yby6_video_mcp_server"
7 | version = "1.0.2"
8 | description = "全网短视频/图集去水印链接提取、视频文本内容提取MCP服务 @yangbuyiya"
9 | readme = "README.md"
10 | requires-python = ">=3.10"
11 | license = {text = "MIT"}
12 | authors = [
13 | {name = "yangbuyiya", email = "yangbuyiya@duck.com"}
14 | ]
15 | dependencies = [
16 | "aiofiles>=23.2.1",
17 | "annotated-types>=0.6.0",
18 | "anyio>=4.5",
19 | "certifi>=2024.2.2",
20 | "click>=8.1.7",
21 | "colorama>=0.4.6",
22 | "cssselect>=1.2.0",
23 | "exceptiongroup>=1.2.2",
24 | "fake-useragent>=1.5.1",
25 | "Faker>=24.4.0",
26 | "fastapi>=0.110.1",
27 | "ffmpeg-python>=0.2.0",
28 | "h11>=0.14.0",
29 | "httpcore>=1.0.5",
30 | "httptools>=0.6.1",
31 | "httpx>=0.28.1",
32 | "idna>=3.6",
33 | "Jinja2>=3.1.3",
34 | "jmespath>=1.0.1",
35 | "lxml>=5.2.1",
36 | "markdown-it-py>=3.0.0",
37 | "MarkupSafe>=2.1.5",
38 | "mdurl>=0.1.2",
39 | "parsel>=1.9.0",
40 | "pydantic>=2.11.7",
41 | "Pygments>=2.17.2",
42 | "PyJWT>=2.8.0",
43 | "python-dateutil>=2.9.0.post0",
44 | "python-dotenv>=1.1.0",
45 | "PyYAML>=6.0.1",
46 | "rich>=13.9.4",
47 | "shellingham>=1.5.4",
48 | "six>=1.16.0",
49 | "sniffio>=1.3.1",
50 | "starlette>=0.37.2",
51 | "typer>=0.15.2",
52 | "uvicorn>=0.29.0",
53 | "w3lib>=2.1.2",
54 | "watchfiles>=0.21.0",
55 | "websockets>=12.0",
56 | "fastmcp>=2.10.2"
57 | ]
58 |
59 | [project.scripts]
60 | yby6_video_mcp_server = "yby6_video_mcp_server.server:main"
61 |
62 | [tool.hatch.build.targets.wheel]
63 | packages = ["yby6_video_mcp_server"]
64 |
65 | [project.urls]
66 | "Homepage" = "https://github.com/yangbuyiya/yby6-crawling-short-video-mcp"
67 | "Repository" = "https://github.com/yangbuyiya/yby6-crawling-short-video-mcp"
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/sixroom.py:
--------------------------------------------------------------------------------
1 | import fake_useragent
2 | import httpx
3 |
4 | from ..utils.helpers import get_val_from_url_by_query_key
5 |
6 | from .base import BaseParser, VideoAuthor, VideoInfo
7 |
8 |
9 | class SixRoom(BaseParser):
10 | """
11 | 六间房
12 | """
13 |
14 | async def parse_share_url(self, share_url: str) -> VideoInfo:
15 | if "watchMini.php?vid=" in share_url:
16 | video_id = get_val_from_url_by_query_key(share_url, "vid")
17 | else:
18 | video_id = share_url.split("?")[0].strip("/").split("/")[-1]
19 |
20 | if len(video_id) == 0:
21 | raise Exception("parse video id from share url failed")
22 |
23 | return await self.parse_video_id(video_id)
24 |
25 | async def parse_video_id(self, video_id: str) -> VideoInfo:
26 | req_url = (
27 | f"https://v.6.cn/coop/mobile/index.php?"
28 | f"padapi=minivideo-watchVideo.php&av=3.0"
29 | f"&encpass=&logiuid=&isnew=1&from=0&vid={video_id}"
30 | )
31 | headers = {
32 | "Referer": f"https://m.6.cn/v/{video_id}",
33 | "User-Agent": fake_useragent.UserAgent(os=["ios"]).random,
34 | }
35 | async with httpx.AsyncClient(follow_redirects=True) as client:
36 | response = await client.get(req_url, headers=headers)
37 | response.raise_for_status()
38 |
39 | json_data = response.json()
40 | data = json_data["content"]
41 |
42 | video_info = VideoInfo(
43 | video_url=data["playurl"],
44 | cover_url=data["picurl"],
45 | title=data["title"],
46 | author=VideoAuthor(
47 | uid="",
48 | name=data["alias"],
49 | avatar=data["picuser"],
50 | ),
51 | )
52 | return video_info
53 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/quanmin.py:
--------------------------------------------------------------------------------
1 | import httpx
2 |
3 | from ..utils.helpers import get_val_from_url_by_query_key
4 |
5 | from .base import BaseParser, VideoAuthor, VideoInfo
6 |
7 |
8 | class QuanMin(BaseParser):
9 | """
10 | 度小视(原 全民小视频)
11 | """
12 |
13 | async def parse_share_url(self, share_url: str) -> VideoInfo:
14 | video_id = get_val_from_url_by_query_key(share_url, "vid")
15 | return await self.parse_video_id(video_id)
16 |
17 | async def parse_video_id(self, video_id: str) -> VideoInfo:
18 | req_url = (
19 | "https://quanmin.hao222.com/wise/growth/api/sv/immerse"
20 | f"?source=share-h5&pd=qm_share_mvideo&_format=json&vid={video_id}"
21 | )
22 | async with httpx.AsyncClient() as client:
23 | response = await client.get(req_url, headers=self.get_default_headers())
24 | response.raise_for_status()
25 |
26 | json_data = response.json()
27 | data = json_data["data"]
28 | # 接口返回错误
29 | if json_data["errno"] != 0:
30 | raise Exception(json_data["error"])
31 | # 视频状态错误
32 | if len(data["meta"]["statusText"]) > 0:
33 | raise Exception(data["meta"]["statusText"])
34 |
35 | # 获取视频标题,如果没有则使用分享标题
36 | video_title = data["meta"]["title"]
37 | if len(video_title) == 0:
38 | video_title = data["shareInfo"]["title"]
39 |
40 | video_info = VideoInfo(
41 | video_url=data["meta"]["video_info"]["clarityUrl"][1]["url"],
42 | cover_url=data["meta"]["image"],
43 | title=video_title,
44 | author=VideoAuthor(
45 | uid=data["author"]["id"],
46 | name=data["author"]["name"],
47 | avatar=data["author"]["icon"],
48 | ),
49 | )
50 | return video_info
51 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/pipigaoxiao.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urlparse
2 |
3 | import fake_useragent
4 | import httpx
5 |
6 | from .base import BaseParser, VideoInfo
7 |
8 |
9 | class PiPiGaoXiao(BaseParser):
10 | """
11 | 皮皮搞笑
12 | """
13 |
14 | async def parse_share_url(self, share_url: str) -> VideoInfo:
15 | url_res = urlparse(share_url)
16 |
17 | video_id = url_res.path.replace("/pp/post/", "")
18 | if len(video_id) == 0:
19 | raise ValueError("parse video_id from share url fail")
20 |
21 | return await self.parse_video_id(video_id)
22 |
23 | async def parse_video_id(self, video_id: str) -> VideoInfo:
24 | req_url = "https://share.ippzone.com/ppapi/share/fetch_content"
25 | async with httpx.AsyncClient() as client:
26 | headers = {
27 | "Referer": req_url,
28 | "Content-Type": "text/plain;charset=UTF-8",
29 | "User-Agent": fake_useragent.UserAgent(os=["windows"]).random,
30 | }
31 | # pid需要是数字,这里直接拼接json字符串,不用json.dumps
32 | post_content = '{"pid":' + video_id + ',"type":"post","mid":null}'
33 | response = await client.post(req_url, headers=headers, content=post_content)
34 | response.raise_for_status()
35 |
36 | json_data = response.json()
37 | # 接口返回错误
38 | if "msg" in json_data:
39 | raise Exception(json_data["msg"])
40 |
41 | data = json_data["data"]["post"]
42 | img_id = data["imgs"][0]["id"]
43 | video_url = data["videos"][str(img_id)]["url"]
44 | cover_url = f"https://file.ippzone.com/img/view/id/{img_id}"
45 |
46 | video_info = VideoInfo(
47 | video_url=video_url,
48 | cover_url=cover_url,
49 | title=data["content"],
50 | )
51 | return video_info
52 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/config.py:
--------------------------------------------------------------------------------
1 | # 配置处理模块
2 |
3 | import os
4 | import logging
5 | from typing import Optional
6 |
7 | from fastmcp.server.context import Context
8 | from fastmcp.server.dependencies import get_http_request, get_http_headers
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | def get_api_configuration(ctx: Optional[Context], api_base_url: Optional[str], model: Optional[str]) -> tuple:
14 | """获取API配置信息"""
15 | api_key = None
16 |
17 | try:
18 | # 尝试从HTTP请求中获取API密钥
19 | headers = get_http_headers(include_all=True)
20 | logger.debug(f"请求头信息: {headers}")
21 |
22 | if "apikey" in headers:
23 | api_key = headers["apikey"]
24 | if ctx:
25 | ctx.info(f"使用请求头中的 API 密钥")
26 | logger.info("使用请求头中的 API 密钥")
27 |
28 | # 尝试从查询参数获取API密钥
29 | if not api_key:
30 | request = get_http_request()
31 | logger.debug(f"查询参数: {request.query_params}")
32 | query_api_key = request.query_params.get("apikey")
33 | if query_api_key:
34 | api_key = query_api_key
35 | if ctx:
36 | ctx.info(f"使用 URL 查询参数中的 API 密钥")
37 | logger.info("使用 URL 查询参数中的 API 密钥")
38 |
39 | except RuntimeError:
40 | logger.debug("非 HTTP 请求上下文,无法获取请求信息")
41 |
42 | # 从环境变量获取配置
43 | if not api_key:
44 | api_key = os.getenv("API_KEY")
45 | if api_key and ctx:
46 | ctx.info(f"使用环境变量中的 API 密钥")
47 | if api_key:
48 | logger.info("使用环境变量中的 API 密钥")
49 |
50 | if not api_base_url:
51 | api_base_url = os.getenv("API_BASE_URL")
52 | if api_base_url and ctx:
53 | ctx.info(f"使用环境变量中的 API 基础 URL: {api_base_url}")
54 | if api_base_url:
55 | logger.info(f"使用环境变量中的 API 基础 URL: {api_base_url}")
56 |
57 | if not model:
58 | model = os.getenv("MODEL")
59 | if model and ctx:
60 | ctx.info(f"使用环境变量中的模型: {model}")
61 | if model:
62 | logger.info(f"使用环境变量中的模型: {model}")
63 |
64 | return api_key, api_base_url, model
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/weibo.py:
--------------------------------------------------------------------------------
1 | import fake_useragent
2 | import httpx
3 |
4 | from ..utils.helpers import get_val_from_url_by_query_key
5 |
6 | from .base import BaseParser, VideoAuthor, VideoInfo
7 |
8 |
9 | class WeiBo(BaseParser):
10 | """
11 | 微博
12 | """
13 |
14 | async def parse_share_url(self, share_url: str) -> VideoInfo:
15 | if "show?fid=" in share_url:
16 | video_id = get_val_from_url_by_query_key(share_url, "fid")
17 | else:
18 | video_id = share_url.split("?")[0].strip("/").split("/")[-1]
19 |
20 | if len(video_id) == 0:
21 | raise Exception("parse video id from share url failed")
22 |
23 | return await self.parse_video_id(video_id)
24 |
25 | async def parse_video_id(self, video_id: str) -> VideoInfo:
26 | req_url = f"https://h5.video.weibo.com/api/component?page=/show/{video_id}"
27 | headers = {
28 | "Referer": f"https://h5.video.weibo.com/show/{video_id}",
29 | "Content-Type": "application/x-www-form-urlencoded",
30 | "User-Agent": fake_useragent.UserAgent(os=["ios"]).random,
31 | }
32 | post_content = 'data={"Component_Play_Playinfo":{"oid":"' + video_id + '"}}'
33 | async with httpx.AsyncClient(follow_redirects=True) as client:
34 | response = await client.post(req_url, headers=headers, content=post_content)
35 | response.raise_for_status()
36 |
37 | json_data = response.json()
38 | data = json_data["data"]["Component_Play_Playinfo"]
39 |
40 | video_url = data["stream_url"]
41 | if len(data["urls"]) > 0:
42 | # stream_url码率最低,urls中第一条码率最高
43 | _, first_mp4_url = next(iter(data["urls"].items()))
44 | video_url = f"https:{first_mp4_url}"
45 |
46 | video_info = VideoInfo(
47 | video_url=video_url,
48 | cover_url="https:" + data["cover_image"],
49 | title=data["title"],
50 | author=VideoAuthor(
51 | uid=str(data["user"]["id"]),
52 | name=data["author"],
53 | avatar="https:" + data["avatar"],
54 | ),
55 | )
56 | return video_info
57 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/xinpianchang.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import fake_useragent
4 | import httpx
5 | from parsel import Selector
6 |
7 | from .base import BaseParser, VideoAuthor, VideoInfo
8 |
9 |
10 | class XinPianChang(BaseParser):
11 | """
12 | 新片场
13 | """
14 |
15 | async def parse_share_url(self, share_url: str) -> VideoInfo:
16 | headers = {
17 | "User-Agent": fake_useragent.UserAgent(os=["windows"]).random,
18 | "Upgrade-Insecure-Requests": "1",
19 | "Referer": "https://www.xinpianchang.com/",
20 | }
21 | async with httpx.AsyncClient(follow_redirects=True) as client:
22 | response = await client.get(share_url, headers=headers)
23 | response.raise_for_status()
24 |
25 | sel = Selector(response.text)
26 | json_text = sel.css("script#__NEXT_DATA__::text").get()
27 | json_data = json.loads(json_text)
28 | data = json_data["props"]["pageProps"]["detail"]
29 |
30 | # 获取 appKey 和 media_id, 另外调用接口获取mp4视频地址
31 | app_key = data["video"]["appKey"]
32 | media_id = data["media_id"]
33 | req_mp4_url = (
34 | f"https://mod-api.xinpianchang.com/mod/api/v2/media/{media_id}"
35 | f"?appKey={app_key}&extend=userInfo%2CuserStatus"
36 | )
37 | async with httpx.AsyncClient(follow_redirects=True) as client:
38 | mp4_response = await client.get(req_mp4_url, headers=headers)
39 | mp4_response.raise_for_status()
40 | mp4_data = mp4_response.json()
41 | video_url = mp4_data["data"]["resource"]["progressive"][0]["url"]
42 |
43 | video_info = VideoInfo(
44 | video_url=video_url,
45 | cover_url=data["cover"],
46 | title=data["title"],
47 | author=VideoAuthor(
48 | uid=str(data["author"]["userinfo"]["id"]),
49 | name=data["author"]["userinfo"]["username"],
50 | avatar=data["author"]["userinfo"]["avatar"],
51 | ),
52 | )
53 |
54 | return video_info
55 |
56 | async def parse_video_id(self, video_id: str) -> VideoInfo:
57 | raise NotImplementedError("新片场暂不支持直接解析视频ID")
58 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/acfun.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | import httpx
5 | from parsel import Selector
6 |
7 | from .base import BaseParser, VideoAuthor, VideoInfo
8 |
9 |
10 | class AcFun(BaseParser):
11 | """
12 | A站:视频地址是m3u8, 可以使用网站 https://tools.thatwind.com/tool/m3u8downloader 下载
13 | """
14 |
15 | async def parse_share_url(self, share_url: str) -> VideoInfo:
16 | async with httpx.AsyncClient(follow_redirects=True) as client:
17 | response = await client.get(share_url, headers=self.get_default_headers())
18 | response.raise_for_status()
19 |
20 | re_video_pattern = r"var videoInfo =\s(.*?);"
21 | re_video_result = re.search(re_video_pattern, response.text)
22 | if not re_video_result or len(re_video_result.groups()) < 1:
23 | raise Exception("failed to parse video JSON info from HTML")
24 |
25 | video_text = re_video_result.group(1).strip()
26 | video_data = json.loads(video_text)
27 |
28 | # 解析视频播放地址
29 | re_play_info_pattern = r"var playInfo =\s(.*?);"
30 | re_play_info_result = re.search(re_play_info_pattern, response.text)
31 | if not re_play_info_result or len(re_play_info_result.groups()) < 1:
32 | raise Exception("failed to parse play info JSON info from HTML")
33 |
34 | play_info_text = re_play_info_result.group(1).strip()
35 | play_info_data = json.loads(play_info_text)
36 |
37 | # 解析用户信息
38 | sel = Selector(response.text)
39 | uid = (
40 | sel.css("div.up-info > a.info-item1::attr(href)")
41 | .get(default="")
42 | .replace("/upPage/", "")
43 | )
44 | name = sel.css("div.up-info span.up-name::text").get(default="")
45 | avatar = sel.css("div.up-info span.up-avatar > img::attr(src)").get(default="")
46 |
47 | video_info = VideoInfo(
48 | video_url=play_info_data["streams"][0]["playUrls"][0],
49 | cover_url=video_data["cover"],
50 | title=video_data["title"],
51 | author=VideoAuthor(
52 | uid=uid,
53 | name=name,
54 | avatar=avatar,
55 | ),
56 | )
57 | return video_info
58 |
59 | async def parse_video_id(self, video_id: str) -> VideoInfo:
60 | # acid, 格式: ac36935385
61 | req_url = f"https://www.acfun.cn/v/{video_id}"
62 | return await self.parse_share_url(req_url)
63 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/base.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from abc import ABC, abstractmethod
3 | from enum import Enum
4 | from typing import Dict, List
5 |
6 | import fake_useragent
7 |
8 |
9 | class VideoSource(Enum):
10 | """
11 | 视频来源:douiyin,kuaishou...
12 | """
13 |
14 | DouYin = "douyin" # 抖音 / 抖音火山版(原 火山小视频)
15 | KuaiShou = "kuaishou" # 快手
16 | PiPiXia = "pipixia" # 皮皮虾
17 | WeiBo = "weibo" # 微博
18 | WeiShi = "weishi" # 微视
19 | LvZhou = "lvzhou" # 绿洲
20 | ZuiYou = "zuiyou" # 最右
21 | QuanMin = "quanmin" # 度小视(原 全民小视频)
22 | XiGua = "xigua" # 西瓜
23 | LiShiPin = "lishipin" # 梨视频
24 | PiPiGaoXiao = "pipigaoxiao" # 皮皮搞笑
25 | HuYa = "huya" # 虎牙
26 | AcFun = "acfun" # A站
27 | DouPai = "doupai" # 逗拍
28 | MeiPai = "meipai" # 美拍
29 | QuanMinKGe = "quanminkge" # 全民K歌
30 | SixRoom = "sixroom" # 六间房
31 | XinPianChang = "xinpianchang" # 新片场
32 | HaoKan = "haokan" # 好看视频
33 | RedBook = "redbook" # 小红书
34 |
35 |
36 | @dataclasses.dataclass
37 | class VideoAuthor:
38 | """
39 | 视频作者信息
40 | """
41 |
42 | # 作者ID
43 | uid: str = ""
44 |
45 | # 作者昵称
46 | name: str = ""
47 |
48 | # 作者头像
49 | avatar: str = ""
50 |
51 |
52 | @dataclasses.dataclass
53 | class ImgInfo:
54 | """
55 | 图集图片信息
56 | """
57 |
58 | # 图片url
59 | url: str = ""
60 |
61 | # livephoto 视频地址
62 | live_photo_url: str = ""
63 |
64 |
65 | @dataclasses.dataclass
66 | class VideoInfo:
67 | """
68 | 视频信息
69 | """
70 |
71 | # 视频播放地址
72 | video_url: str
73 |
74 | # 视频封面地址
75 | cover_url: str
76 |
77 | # 视频标题
78 | title: str = ""
79 |
80 | # 音乐播放地址
81 | music_url: str = ""
82 |
83 | # 图集图片地址列表
84 | images: List[ImgInfo] = dataclasses.field(default_factory=list)
85 |
86 | # 视频作者信息
87 | author: VideoAuthor = dataclasses.field(default_factory=VideoAuthor)
88 |
89 |
90 | class BaseParser(ABC):
91 | @staticmethod
92 | def get_default_headers() -> Dict[str, str]:
93 | return {
94 | "User-Agent": fake_useragent.UserAgent(os=["ios"]).random,
95 | }
96 |
97 | @abstractmethod
98 | async def parse_share_url(self, share_url: str) -> VideoInfo:
99 | """
100 | 解析分享链接, 获取视频信息
101 | :param share_url: 视频分享链接
102 | :return: VideoInfo
103 | """
104 | pass
105 |
106 | @abstractmethod
107 | async def parse_video_id(self, video_id: str) -> VideoInfo:
108 | """
109 | 解析视频ID, 获取视频信息
110 | :param video_id: 视频ID
111 | :return:
112 | """
113 | pass
114 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/pipixia.py:
--------------------------------------------------------------------------------
1 | import httpx
2 |
3 | from .base import BaseParser, ImgInfo, VideoAuthor, VideoInfo
4 |
5 |
6 | class PiPiXia(BaseParser):
7 | """
8 | 皮皮虾
9 | """
10 |
11 | async def parse_share_url(self, share_url: str) -> VideoInfo:
12 | async with httpx.AsyncClient(follow_redirects=False) as client:
13 | response = await client.get(share_url, headers=self.get_default_headers())
14 | location_url = response.headers.get("location", "")
15 | if len(location_url) <= 0:
16 | raise Exception("failed to get location url from share url")
17 |
18 | video_id = location_url.split("?")[0].split("/")[-1]
19 |
20 | return await self.parse_video_id(video_id)
21 |
22 | async def parse_video_id(self, video_id: str) -> VideoInfo:
23 | req_url = (
24 | "https://api.pipix.com/bds/cell/cell_comment/"
25 | + f"?offset=0&cell_type=1&api_version=1&cell_id={video_id}"
26 | + "&ac=wifi&channel=huawei_1319_64&aid=1319&app_name=super"
27 | )
28 | async with httpx.AsyncClient(follow_redirects=False) as client:
29 | response = await client.get(req_url, headers=self.get_default_headers())
30 | response.raise_for_status()
31 |
32 | json_data = response.json()
33 | if json_data["status_code"] != 0:
34 | raise Exception(f"获取作品信息失败:prompt={json_data['prompt']}")
35 | data = json_data["data"]["cell_comments"][0]["comment_info"]["item"]
36 |
37 | author_id = data["author"]["id"]
38 |
39 | # 获取图集图片地址
40 | images = []
41 | # 如果data含有 images,并且 images 是一个列表
42 | if data.get("note") is not None:
43 | for img in data["note"]["multi_image"]:
44 | images.append(ImgInfo(url=img["url_list"][0]["url"]))
45 |
46 | video_url = ""
47 | if data.get("video") is not None:
48 | video_url = data["video"]["video_high"]["url_list"][0][
49 | "url"
50 | ] # 备用视频地址, 可能有水印
51 | # comments中可能带有不带水印视频, 但是comments可能为空
52 | for comment in data.get("comments", []):
53 | if (
54 | comment["item"]["author"]["id"] == author_id
55 | and comment["item"]["video"]["video_high"]["url_list"][0]["url"]
56 | ):
57 | video_url = comment["item"]["video"]["video_high"]["url_list"][0][
58 | "url"
59 | ]
60 | break
61 |
62 | video_info = VideoInfo(
63 | video_url=video_url,
64 | cover_url=data["cover"]["url_list"][0]["url"],
65 | title=data["content"],
66 | images=images,
67 | author=VideoAuthor(
68 | uid=str(author_id),
69 | name=data["author"]["name"],
70 | avatar=data["author"]["avatar"]["download_list"][0]["url"],
71 | ),
72 | )
73 |
74 | return video_info
75 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/xigua.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | import fake_useragent
5 | import httpx
6 |
7 | from .base import BaseParser, VideoAuthor, VideoInfo
8 |
9 |
10 | class XiGua(BaseParser):
11 | """
12 | 西瓜视频
13 | """
14 |
15 | async def parse_share_url(self, share_url: str) -> VideoInfo:
16 | headers = {
17 | "User-Agent": fake_useragent.UserAgent(os=["android"]).random,
18 | }
19 | if share_url.startswith("https://www.ixigua.com/"):
20 | # 支持电脑网页版链接 https://www.ixigua.com/xxxxxx
21 | video_id = share_url.strip("/").split("/")[-1]
22 | return await self.parse_video_id(video_id)
23 |
24 | async with httpx.AsyncClient(follow_redirects=False) as client:
25 | response = await client.get(share_url, headers=headers)
26 |
27 | location_url = response.headers.get("location", "")
28 | video_id = location_url.split("?")[0].strip("/").split("/")[-1]
29 | if len(video_id) <= 0:
30 | raise Exception("failed to get video_id from share URL")
31 |
32 | return await self.parse_video_id(video_id)
33 |
34 | async def parse_video_id(self, video_id: str) -> VideoInfo:
35 | # 注意: url中的 video_id 后面不要有 /, 否则返回格式不一样
36 | req_url = (
37 | f"https://m.ixigua.com/douyin/share/video/{video_id}"
38 | f"?aweme_type=107&schema_type=1&utm_source=copy"
39 | f"&utm_campaign=client_share&utm_medium=android&app=aweme"
40 | )
41 |
42 | async with httpx.AsyncClient(follow_redirects=True) as client:
43 | response = await client.get(req_url, headers=self.get_default_headers())
44 | response.raise_for_status()
45 |
46 | pattern = re.compile(
47 | pattern=r"window\._ROUTER_DATA\s*=\s*(.*?)",
48 | flags=re.DOTALL,
49 | )
50 | find_res = pattern.search(response.text)
51 |
52 | if not find_res or not find_res.group(1):
53 | raise ValueError("parse video json info from html fail")
54 |
55 | json_data = json.loads(find_res.group(1).strip())
56 | original_video_info = json_data["loaderData"]["video_(id)/page"]["videoInfoRes"]
57 |
58 | # 如果没有视频信息,获取并抛出异常
59 | if len(original_video_info["item_list"]) == 0:
60 | err_detail_msg = "failed to parse video info from HTML"
61 | if len(filter_list := original_video_info["filter_list"]) > 0:
62 | err_detail_msg = filter_list[0]["detail_msg"]
63 | raise Exception(err_detail_msg)
64 |
65 | data = original_video_info["item_list"][0]
66 | video_url = data["video"]["play_addr"]["url_list"][0].replace("playwm", "play")
67 |
68 | video_info = VideoInfo(
69 | video_url=video_url,
70 | cover_url=data["video"]["cover"]["url_list"][0],
71 | title=data["desc"],
72 | author=VideoAuthor(
73 | uid=data["author"]["unique_id"],
74 | name=data["author"]["nickname"],
75 | avatar=data["author"]["avatar_thumb"]["url_list"][0],
76 | ),
77 | )
78 | return video_info
79 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/redbook.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import fake_useragent
4 | import httpx
5 | import yaml
6 |
7 | from .base import BaseParser, ImgInfo, VideoAuthor, VideoInfo
8 |
9 |
10 | class RedBook(BaseParser):
11 | """
12 | 小红书
13 | """
14 |
15 | async def parse_share_url(self, share_url: str) -> VideoInfo:
16 | headers = {
17 | "User-Agent": fake_useragent.UserAgent(os=["windows"]).random,
18 | }
19 | async with httpx.AsyncClient(follow_redirects=True) as client:
20 | response = await client.get(share_url, headers=headers)
21 | response.raise_for_status()
22 |
23 | pattern = re.compile(
24 | pattern=r"window\.__INITIAL_STATE__\s*=\s*(.*?)",
25 | flags=re.DOTALL,
26 | )
27 | find_res = pattern.search(response.text)
28 |
29 | if not find_res or not find_res.group(1):
30 | raise ValueError("parse video json info from html fail")
31 |
32 | json_data = yaml.safe_load(find_res.group(1))
33 |
34 | note_id = json_data["note"]["currentNoteId"]
35 | # 验证返回:小红书的分享链接有有效期,过期后会返回 undefined
36 | if note_id == "undefined":
37 | raise Exception("parse fail: note id in response is undefined")
38 | data = json_data["note"]["noteDetailMap"][note_id]["note"]
39 |
40 | # 视频地址
41 | video_url = ""
42 | h264_data = (
43 | data.get("video", {}).get("media", {}).get("stream", {}).get("h264", [])
44 | )
45 | if len(h264_data) > 0:
46 | video_url = h264_data[0].get("masterUrl", "")
47 |
48 | # 获取图集图片地址
49 | images = []
50 | if len(video_url) <= 0:
51 | for img_item in data["imageList"]:
52 | # 个别图片有水印, 替换图片域名
53 | image_id = img_item["urlDefault"].split("/")[-1].split("!")[0]
54 | # 如果链接中带有 spectrum/ , 替换域名时需要带上
55 | spectrum_str = (
56 | "spectrum/" if "spectrum" in img_item["urlDefault"] else ""
57 | )
58 | new_url = (
59 | "https://ci.xiaohongshu.com/notes_pre_post/"
60 | + f"{spectrum_str}{image_id}"
61 | + "?imageView2/format/jpg"
62 | )
63 | img_info = ImgInfo(url=new_url)
64 | # 是否有 livephoto 视频地址
65 | if img_item.get("livePhoto", False) and (
66 | h264_data := img_item.get("stream", {}).get("h264", [])
67 | ):
68 | img_info.live_photo_url = h264_data[0]["masterUrl"]
69 | images.append(img_info)
70 |
71 | video_info = VideoInfo(
72 | video_url=video_url,
73 | cover_url=data["imageList"][0]["urlDefault"],
74 | title=data["title"],
75 | images=images,
76 | author=VideoAuthor(
77 | uid=data["user"]["userId"],
78 | name=data["user"]["nickname"],
79 | avatar=data["user"]["avatar"],
80 | ),
81 | )
82 | return video_info
83 |
84 | async def parse_video_id(self, video_id: str) -> VideoInfo:
85 | raise NotImplementedError("小红书暂不支持直接解析视频ID")
86 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/meipai.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from typing import Dict, List
3 |
4 | import fake_useragent
5 | import httpx
6 | from parsel import Selector
7 |
8 | from .base import BaseParser, VideoAuthor, VideoInfo
9 |
10 |
11 | class MeiPai(BaseParser):
12 | """
13 | 美拍
14 | """
15 |
16 | async def parse_share_url(self, share_url: str) -> VideoInfo:
17 | async with httpx.AsyncClient() as client:
18 | headers = {
19 | "User-Agent": fake_useragent.UserAgent(os=["windows"]).random,
20 | }
21 | response = await client.get(share_url, headers=headers)
22 | response.raise_for_status()
23 |
24 | sel = Selector(response.text)
25 | video_bs64 = sel.css("#shareMediaBtn::attr(data-video)").get(default="")
26 | video_url = self.parse_video_bs64(video_bs64)
27 |
28 | video_info = VideoInfo(
29 | video_url=video_url,
30 | cover_url=sel.css("#detailVideo img::attr(src)").get(default=""),
31 | title=sel.css(".detail-cover-title::text").get(default="").strip(),
32 | author=VideoAuthor(
33 | uid=sel.css(".detail-name a::attr(href)")
34 | .get(default="")
35 | .split("/")[-1],
36 | name=sel.css(".detail-avatar::attr(alt)").get(default=""),
37 | avatar="https:" + sel.css(".detail-avatar::attr(src)").get(default=""),
38 | ),
39 | )
40 | return video_info
41 |
42 | async def parse_video_id(self, video_id: str) -> VideoInfo:
43 | req_url = f"https://www.meipai.com/video/{video_id}"
44 | return await self.parse_share_url(req_url)
45 |
46 | def parse_video_bs64(self, video_bs64: str) -> str:
47 | hex_val = self.get_hex(video_bs64)
48 | dec_val = self.get_dec(hex_val["hex_1"])
49 | d_val = self.sub_str(hex_val["str_1"], dec_val["pre"])
50 | p_val = self.get_pos(d_val, dec_val["tail"])
51 | kk_val = self.sub_str(d_val, p_val)
52 | decode_bs64 = base64.b64decode(kk_val)
53 | video_url = "https:" + decode_bs64.decode("utf-8")
54 | return video_url
55 |
56 | def get_hex(self, s: str) -> Dict[str, str]:
57 | hex_val = s[:4]
58 | str_val = s[4:]
59 | return {"hex_1": self.reverse_string(hex_val), "str_1": str_val}
60 |
61 | @staticmethod
62 | def get_dec(hex_val: str) -> Dict[str, List[int]]:
63 | int_n = int(hex_val, 16)
64 | str_n = str(int_n)
65 | length = len(str_n)
66 | pre = [int(str_n[i]) for i in range(length) if i < length - 2]
67 | tail = [int(str_n[i]) for i in range(length) if i >= length - 2]
68 | return {"pre": pre, "tail": tail}
69 |
70 | @staticmethod
71 | def sub_str(s: str, b: List[int]) -> str:
72 | index_1 = b[0]
73 | index_2 = b[0] + b[1]
74 | c = s[:index_1]
75 | d = s[index_1:index_2]
76 | temp = s[index_2:].replace(d, "")
77 | return c + temp
78 |
79 | @staticmethod
80 | def get_pos(s: str, b: List[int]) -> List[int]:
81 | b[0] = len(s) - b[0] - b[1]
82 | return b
83 |
84 | @staticmethod
85 | def reverse_string(s: str) -> str:
86 | return s[::-1]
87 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/kuaishou.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | import fake_useragent
5 | import httpx
6 |
7 | from .base import BaseParser, ImgInfo, VideoAuthor, VideoInfo
8 |
9 |
10 | class KuaiShou(BaseParser):
11 | """
12 | 快手
13 | """
14 |
15 | async def parse_share_url(self, share_url: str) -> VideoInfo:
16 | user_agent = fake_useragent.UserAgent(os=["ios"]).random
17 |
18 | # 获取跳转前的信息, 从中获取跳转url, cookie
19 | async with httpx.AsyncClient(follow_redirects=False) as client:
20 | share_response = await client.get(
21 | share_url,
22 | headers={
23 | "User-Agent": user_agent,
24 | "Referer": "https://v.kuaishou.com/",
25 | },
26 | )
27 |
28 | location_url = share_response.headers.get("location", "")
29 | if len(location_url) <= 0:
30 | raise Exception("failed to get location url from share url")
31 |
32 | # /fw/long-video/ 返回结果不一样, 统一替换为 /fw/photo/ 请求
33 | location_url = location_url.replace("/fw/long-video/", "/fw/photo/")
34 |
35 | async with httpx.AsyncClient(follow_redirects=True) as client:
36 | response = await client.get(
37 | location_url,
38 | headers=share_response.headers,
39 | cookies=share_response.cookies,
40 | )
41 |
42 | re_pattern = r"window.INIT_STATE\s*=\s*(.*?)"
43 | re_result = re.search(re_pattern, response.text)
44 |
45 | if not re_result or len(re_result.groups()) < 1:
46 | raise Exception("failed to parse video JSON info from HTML")
47 |
48 | json_text = re_result.group(1).strip()
49 | json_data = json.loads(json_text)
50 |
51 | photo_data = {}
52 | for json_item in json_data.values():
53 | if "result" in json_item and "photo" in json_item:
54 | photo_data = json_item
55 | break
56 |
57 | if not photo_data:
58 | raise Exception("failed to parse photo info from INIT_STATE")
59 |
60 | # 判断result状态
61 | if (result_code := photo_data["result"]) != 1:
62 | raise Exception(f"获取作品信息失败:result={result_code}")
63 |
64 | data = photo_data["photo"]
65 |
66 | # 获取视频地址
67 | video_url = ""
68 | if "mainMvUrls" in data and len(data["mainMvUrls"]) > 0:
69 | video_url = data["mainMvUrls"][0]["url"]
70 |
71 | # 获取图集
72 | ext_params_atlas = data.get("ext_params", {}).get("atlas", {})
73 | atlas_cdn_list = ext_params_atlas.get("cdn", [])
74 | atlas_list = ext_params_atlas.get("list", [])
75 | images = []
76 | if len(atlas_cdn_list) > 0 and len(atlas_list) > 0:
77 | for atlas in atlas_list:
78 | images.append(ImgInfo(url=f"https://{atlas_cdn_list[0]}/{atlas}"))
79 |
80 | video_info = VideoInfo(
81 | video_url=video_url,
82 | cover_url=data["coverUrls"][0]["url"],
83 | title=data["caption"],
84 | author=VideoAuthor(
85 | uid="",
86 | name=data["userName"],
87 | avatar=data["headUrl"],
88 | ),
89 | images=images,
90 | )
91 | return video_info
92 |
93 | async def parse_video_id(self, video_id: str) -> VideoInfo:
94 | raise NotImplementedError("快手暂不支持直接解析视频ID")
95 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/server.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Optional, Dict, Any
3 | from .utils import (
4 | share_url_parse_tool,
5 | video_id_parse_tool,
6 | share_text_parse_tool
7 | )
8 | from fastmcp import FastMCP, Context
9 |
10 | # 直接导入版本号,避免循环导入
11 | from . import __version__
12 |
13 | # 配置日志
14 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
15 | logger = logging.getLogger(__name__)
16 |
17 | # 创建FastMCP实例
18 | mcp = FastMCP(f"全网短视频去水印链接提取 MCP服务 V{__version__}")
19 |
20 |
21 | @mcp.tool(
22 | description="""
23 | 解析视频分享链接,获取视频信息
24 | 参数:
25 | - url: 视频分享链接
26 | 返回:
27 | - code: 状态码
28 | - msg: 状态信息
29 | - data: 视频信息
30 | """
31 | )
32 | async def share_url_parse_tool_wrapper(url: str) -> Dict[str, Any]:
33 | """解析视频分享链接,获取视频信息"""
34 | return await share_url_parse_tool(url)
35 |
36 |
37 | @mcp.tool(
38 | description="""
39 | 根据视频来源和ID解析视频信息
40 | 参数:
41 | - source: 视频来源
42 | - video_id: 视频ID
43 | 返回:
44 | - code: 状态码
45 | - msg: 状态信息
46 | - data: 视频信息
47 | """
48 | )
49 | async def video_id_parse_tool_wrapper(source: str, video_id: str) -> Dict[str, Any]:
50 | """根据视频来源和ID解析视频信息"""
51 | return await video_id_parse_tool(source, video_id)
52 |
53 |
54 | @mcp.tool(
55 | description="""
56 | 提取视频内容,需要传递apikey,否则无法使用视频内容提取功能!
57 | 参数:
58 | - text: 抖音分享文本,包含分享链接
59 | - api_base_url: API基础URL,默认使用siliconflow.cn
60 | - model: 语音识别模型,默认使用FunAudioLLM/SenseVoiceSmall
61 | """
62 | )
63 | async def share_text_parse_tool_wrapper(
64 | text: str,
65 | api_base_url: Optional[str] = None,
66 | model: Optional[str] = None,
67 | ctx: Context = None,
68 | ) -> Dict[str, Any]:
69 | """
70 | 解析抖音分享链接,提取无水印视频地址
71 | 下载无水印视频
72 | 提取音频
73 | 转换音频为文本
74 | 清理临时文件
75 |
76 | 参数:
77 | - text: 抖音分享文本,包含分享链接
78 | - api_base_url: API基础URL,默认使用SiliconFlow
79 | - model: 语音识别模型,默认使用SenseVoiceSmall
80 | """
81 | return await share_text_parse_tool(text, api_base_url, model, ctx)
82 |
83 |
84 | def main():
85 | """作为Python包入口点的主函数"""
86 | import argparse
87 |
88 | parser = argparse.ArgumentParser(description="视频解析MCP服务器 @yangbuyiya")
89 | parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
90 | parser.add_argument(
91 | "--transport",
92 | type=str,
93 | default="stdio",
94 | choices=["stdio", "sse", "http"],
95 | help="传输方式: stdio, sse, http",
96 | )
97 | parser.add_argument("--host", type=str, default="0.0.0.0", help="主机地址")
98 | parser.add_argument("--port", type=int, default=8637, help="端口号")
99 | parser.add_argument("--path", type=str, default=None, help="自定义请求路径")
100 |
101 | args = parser.parse_args()
102 |
103 | print("启动视频解析MCP服务器...")
104 |
105 | # 根据命令行参数选择传输方式
106 | if args.transport == "http":
107 | path = args.path if args.path else "/mcp"
108 | print(f"使用 Streamable HTTP 传输方式: http://{args.host}:{args.port}{path}")
109 | mcp.run(transport="http", host=args.host, port=args.port, path=path)
110 | elif args.transport == "sse":
111 | path = args.path if args.path else "/sse/"
112 | print(f"使用 SSE 传输方式: http://{args.host}:{args.port}{path}")
113 | mcp.run(transport="sse", host=args.host, port=args.port, path=path)
114 | else:
115 | print("使用 STDIO 传输方式")
116 | mcp.run(transport="stdio")
117 |
118 |
119 | if __name__ == "__main__":
120 | main()
121 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/video_processor.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | import httpx
4 | import tempfile
5 | from pathlib import Path
6 | from typing import Optional
7 | import ffmpeg
8 |
9 | # 请求头,模拟移动端访问
10 | HEADERS = {
11 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/121.0.2277.107 Version/17.0 Mobile/15E148 Safari/604.1'
12 | }
13 |
14 | # 默认 API 配置
15 | DEFAULT_API_BASE_URL = "https://api.siliconflow.cn/v1/audio/transcriptions"
16 | DEFAULT_MODEL = "FunAudioLLM/SenseVoiceSmall"
17 |
18 |
19 | class VideoProcessor:
20 | """视频处理器"""
21 |
22 | def __init__(self, api_key: str, api_base_url: Optional[str] = None, model: Optional[str] = None):
23 | self.api_key = api_key
24 | self.api_base_url = api_base_url or DEFAULT_API_BASE_URL
25 | self.model = model or DEFAULT_MODEL
26 | self.temp_dir = Path(tempfile.mkdtemp())
27 |
28 | def __del__(self):
29 | """清理临时目录"""
30 | import shutil
31 | if hasattr(self, 'temp_dir') and self.temp_dir.exists():
32 | shutil.rmtree(self.temp_dir, ignore_errors=True)
33 |
34 | async def download_video(self, video_info: dict) -> Path:
35 | """异步下载视频到临时目录"""
36 | print(f"下载视频信息: {video_info}")
37 | # 如果 video_info['video_id'] 是空, 则使用当前时间戳作为视频ID
38 | if not video_info['video_id']:
39 | video_info['video_id'] = str(int(time.time()))
40 | filename = f"{video_info['video_id']}.mp4"
41 | filepath = self.temp_dir / filename
42 |
43 | with httpx.Client() as client:
44 | response = client.get(video_info['url'], headers=HEADERS, follow_redirects=True)
45 | response.raise_for_status()
46 |
47 | # 下载文件
48 | with open(filepath, 'wb') as f:
49 | for chunk in response.iter_bytes(chunk_size=8192):
50 | if chunk:
51 | f.write(chunk)
52 |
53 | return filepath
54 |
55 | def extract_audio(self, video_path: Path) -> Path:
56 | """从视频文件中提取音频"""
57 | print(f"准备提取的视频文件: {video_path}")
58 | audio_path = video_path.with_suffix('.mp3')
59 |
60 | try:
61 | (
62 | ffmpeg
63 | .input(str(video_path))
64 | .output(str(audio_path), acodec='libmp3lame', q=0)
65 | .run(capture_stdout=True, capture_stderr=True, overwrite_output=True)
66 | )
67 | return audio_path
68 | except Exception as e:
69 | raise Exception(f"提取音频时出错: {str(e)}")
70 |
71 | def extract_text_from_audio(self, audio_path: Path) -> str:
72 | """从音频文件中提取文字"""
73 | print(f"准备提取的音频文件: {audio_path}")
74 | files = {
75 | 'file': (audio_path.name, open(audio_path, 'rb'), 'audio/mpeg'),
76 | 'model': (None, self.model)
77 | }
78 |
79 | headers = {
80 | "Authorization": f"Bearer {self.api_key}"
81 | }
82 |
83 | try:
84 | # 设置较长的超时时间,单位为秒
85 | timeout = httpx.Timeout(300.0) # 5分钟超时
86 | with httpx.Client(timeout=timeout) as client:
87 | response = client.post(self.api_base_url, files=files, headers=headers)
88 | response.raise_for_status()
89 |
90 | # 解析响应
91 | result = response.json()
92 | if 'text' in result:
93 | return result['text']
94 | else:
95 | return response.text
96 |
97 | except Exception as e:
98 | raise Exception(f"提取文字时出错: {str(e)}")
99 | finally:
100 | files['file'][1].close()
101 |
102 | def cleanup_files(self, *file_paths: Path):
103 | """清理指定的文件"""
104 | for file_path in file_paths:
105 | if file_path.exists():
106 | file_path.unlink()
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/douyin.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | import httpx
5 |
6 | from .base import BaseParser, ImgInfo, VideoAuthor, VideoInfo
7 |
8 | # 模拟手机端请求头
9 | header = {
10 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/121.0.2277.107 Version/17.0 Mobile/15E148 Safari/604.1'
11 | }
12 |
13 |
14 | class DouYin(BaseParser):
15 | """
16 | 抖音 / 抖音火山版
17 | """
18 |
19 | async def parse_share_url(self, share_text: str) -> VideoInfo:
20 | """从分享文本中提取无水印视频链接"""
21 | # 提取分享链接
22 | urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', share_text)
23 | if not urls:
24 | raise ValueError("未找到有效的分享链接")
25 |
26 | share_url = urls[0]
27 | # 获取video_id
28 | async with httpx.AsyncClient(follow_redirects=True) as client:
29 | share_response = await client.get(share_url, headers=header)
30 | video_id = str(share_response.url).split("?")[0].strip("/").split("/")[-1]
31 | detail_url = f'https://www.iesdouyin.com/share/video/{video_id}'
32 | response = await client.get(detail_url, headers=header)
33 | response.raise_for_status()
34 |
35 | pattern = re.compile(
36 | pattern=r"window\._ROUTER_DATA\s*=\s*(.*?)",
37 | flags=re.DOTALL,
38 | )
39 | find_res = pattern.search(response.text)
40 |
41 | if not find_res or not find_res.group(1):
42 | raise ValueError("从HTML中解析视频信息失败")
43 |
44 | # 解析JSON数据
45 | json_data = json.loads(find_res.group(1).strip())
46 |
47 | # 获取链接返回json数据进行视频和图集判断,如果指定类型不存在,抛出异常
48 | # 返回的json数据中,视频字典类型为 video_(id)/page
49 | VIDEO_ID_PAGE_KEY = "video_(id)/page"
50 | # 返回的json数据中,视频字典类型为 note_(id)/page
51 | NOTE_ID_PAGE_KEY = "note_(id)/page"
52 | if VIDEO_ID_PAGE_KEY in json_data["loaderData"]:
53 | original_video_info = json_data["loaderData"][VIDEO_ID_PAGE_KEY]["videoInfoRes"]
54 | elif NOTE_ID_PAGE_KEY in json_data["loaderData"]:
55 | original_video_info = json_data["loaderData"][NOTE_ID_PAGE_KEY]["videoInfoRes"]
56 | else:
57 | raise Exception("无法从JSON中解析视频或图集信息")
58 |
59 | data = original_video_info["item_list"][0]
60 |
61 | # 获取图集图片地址
62 | images = []
63 | # 如果data含有 images,并且 images 是一个列表
64 | if "images" in data and isinstance(data["images"], list):
65 | # 获取每个图片的url_list中的第一个元素,非空时添加到images列表中
66 | for img in data["images"]:
67 | if (
68 | "url_list" in img
69 | and isinstance(img["url_list"], list)
70 | and len(img["url_list"]) > 0
71 | and len(img["url_list"][0]) > 0
72 | ):
73 | images.append(ImgInfo(url=img["url_list"][0]))
74 |
75 | # 获取视频播放地址(snssdk.com直链)
76 | video_url = data["video"]["play_addr"]["url_list"][0].replace("playwm", "play")
77 | # 如果图集地址不为空时,因为没有视频,上面抖音返回的视频地址无法访问,置空处理
78 | if len(images) > 0:
79 | video_url = ""
80 |
81 | # 组装VideoInfo
82 | video_info = VideoInfo(
83 | video_url=video_url,
84 | cover_url=data["video"]["cover"]["url_list"][0],
85 | title=data.get("desc", "").strip() or f"douyin_{video_id}",
86 | images=images,
87 | author=VideoAuthor(
88 | uid=data["author"]["sec_uid"],
89 | name=data["author"]["nickname"],
90 | avatar=data["author"]["avatar_thumb"]["url_list"][0],
91 | ),
92 | )
93 | return video_info
94 |
95 | async def get_video_redirect_url(self, video_url: str) -> str:
96 | async with httpx.AsyncClient(follow_redirects=False) as client:
97 | response = await client.get(video_url, headers=header)
98 | # 返回重定向后的地址,如果没有重定向则返回原地址(抖音中的西瓜视频,重定向地址为空)
99 | return response.headers.get("location") or video_url
100 |
101 | async def parse_video_id(self, video_id: str) -> VideoInfo:
102 | req_url = self._get_request_url_by_video_id(video_id)
103 | return await self.parse_share_url(req_url)
104 |
105 | def _get_request_url_by_video_id(self, video_id) -> str:
106 | return f"https://www.iesdouyin.com/share/video/{video_id}/"
107 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/functionality/__init__.py:
--------------------------------------------------------------------------------
1 | from .acfun import AcFun
2 | from .base import VideoInfo, VideoSource
3 | from .doupai import DouPai
4 | from .douyin import DouYin
5 | from .haokan import HaoKan
6 | from .huya import HuYa
7 | from .kuaishou import KuaiShou
8 | from .lishipin import LiShiPin
9 | from .lvzhou import LvZhou
10 | from .meipai import MeiPai
11 | from .pipigaoxiao import PiPiGaoXiao
12 | from .pipixia import PiPiXia
13 | from .quanmin import QuanMin
14 | from .quanminkge import QuanMinKGe
15 | from .redbook import RedBook
16 | from .sixroom import SixRoom
17 | from .weibo import WeiBo
18 | from .weishi import WeiShi
19 | from .xigua import XiGua
20 | from .xinpianchang import XinPianChang
21 | from .zuiyou import ZuiYou
22 | from .video_processor import VideoProcessor, HEADERS, DEFAULT_API_BASE_URL, DEFAULT_MODEL
23 |
24 | # 视频来源与解析器的映射关系
25 | video_source_info_mapping = {
26 | VideoSource.AcFun: {
27 | "domain_list": ["www.acfun.cn"],
28 | "parser": AcFun,
29 | },
30 | VideoSource.DouPai: {
31 | "domain_list": ["doupai.cc"],
32 | "parser": DouPai,
33 | },
34 | VideoSource.DouYin: {
35 | "domain_list": ["v.douyin.com", "www.iesdouyin.com", "www.douyin.com"],
36 | "parser": DouYin,
37 | },
38 | VideoSource.HaoKan: {
39 | "domain_list": [
40 | "haokan.baidu.com",
41 | "haokan.hao123.com",
42 | ],
43 | "parser": HaoKan,
44 | },
45 | VideoSource.HuYa: {
46 | "domain_list": ["v.huya.com"],
47 | "parser": HuYa,
48 | },
49 | VideoSource.KuaiShou: {
50 | "domain_list": ["v.kuaishou.com"],
51 | "parser": KuaiShou,
52 | },
53 | VideoSource.LiShiPin: {
54 | "domain_list": ["www.pearvideo.com"],
55 | "parser": LiShiPin,
56 | },
57 | VideoSource.LvZhou: {
58 | "domain_list": ["weibo.cn"],
59 | "parser": LvZhou,
60 | },
61 | VideoSource.MeiPai: {
62 | "domain_list": ["meipai.com"],
63 | "parser": MeiPai,
64 | },
65 | VideoSource.PiPiGaoXiao: {
66 | "domain_list": ["h5.pipigx.com"],
67 | "parser": PiPiGaoXiao,
68 | },
69 | VideoSource.PiPiXia: {
70 | "domain_list": ["h5.pipix.com"],
71 | "parser": PiPiXia,
72 | },
73 | VideoSource.QuanMin: {
74 | "domain_list": ["xspshare.baidu.com"],
75 | "parser": QuanMin,
76 | },
77 | VideoSource.QuanMinKGe: {
78 | "domain_list": ["kg.qq.com"],
79 | "parser": QuanMinKGe,
80 | },
81 | VideoSource.SixRoom: {
82 | "domain_list": ["6.cn"],
83 | "parser": SixRoom,
84 | },
85 | VideoSource.WeiBo: {
86 | "domain_list": ["weibo.com"],
87 | "parser": WeiBo,
88 | },
89 | VideoSource.WeiShi: {
90 | "domain_list": ["isee.weishi.qq.com"],
91 | "parser": WeiShi,
92 | },
93 | VideoSource.XiGua: {
94 | "domain_list": ["v.ixigua.com", "www.ixigua.com"],
95 | "parser": XiGua,
96 | },
97 | VideoSource.XinPianChang: {
98 | "domain_list": ["xinpianchang.com"],
99 | "parser": XinPianChang,
100 | },
101 | VideoSource.ZuiYou: {
102 | "domain_list": ["share.xiaochuankeji.cn"],
103 | "parser": ZuiYou,
104 | },
105 | VideoSource.RedBook: {
106 | "domain_list": [
107 | "www.xiaohongshu.com",
108 | "xhslink.com",
109 | ],
110 | "parser": RedBook,
111 | },
112 | }
113 |
114 |
115 | async def parse_video_share_url(share_url: str) -> VideoInfo:
116 | """
117 | 解析分享链接, 获取视频信息
118 | :param share_url: 视频分享链接
119 | :return:
120 | """
121 | source = ""
122 | for item_source, item_source_info in video_source_info_mapping.items():
123 | for item_url_domain in item_source_info["domain_list"]:
124 | if item_url_domain in share_url:
125 | source = item_source
126 | break
127 | if source:
128 | break
129 |
130 | if not source:
131 | raise ValueError(f"share url [{share_url}] does not have source config")
132 |
133 | url_parser = video_source_info_mapping[source]["parser"]
134 | if not url_parser:
135 | raise ValueError(f"source {source} has no video parser")
136 |
137 | _obj = url_parser()
138 | video_info = await _obj.parse_share_url(share_url)
139 |
140 | return video_info
141 |
142 |
143 | async def parse_video_id(source: VideoSource, video_id: str) -> VideoInfo:
144 | """
145 | 解析视频ID, 获取视频信息
146 | :param source: 视频来源
147 | :param video_id: 视频id
148 | :return:
149 | """
150 | if not video_id or not source:
151 | raise ValueError("video_id or source is empty")
152 |
153 | id_parser = video_source_info_mapping[source]["parser"]
154 | if not id_parser:
155 | raise ValueError(f"source {source} has no video parser")
156 |
157 | _obj = id_parser()
158 | video_info = await _obj.parse_video_id(video_id)
159 |
160 | return video_info
161 |
162 |
--------------------------------------------------------------------------------
/yby6_video_mcp_server/utils/tools.py:
--------------------------------------------------------------------------------
1 | # MCP工具函数模块
2 |
3 | import time
4 | import logging
5 | from typing import Optional, Dict, Any
6 |
7 | from fastmcp.server.context import Context
8 | from ..functionality import (
9 | VideoSource,
10 | parse_video_id,
11 | parse_video_share_url,
12 | VideoProcessor
13 | )
14 | from .responses import create_success_response, create_error_response
15 | from .helpers import extract_url_from_text
16 | from .config import get_api_configuration
17 | from .constants import DEFAULT_RESPONSE_CODES
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | async def share_url_parse_tool(url: str) -> Dict[str, Any]:
23 | """解析视频分享链接,获取视频信息"""
24 | if not url or not isinstance(url, str):
25 | return create_error_response("URL参数无效")
26 |
27 | try:
28 | video_share_url = extract_url_from_text(url)
29 | if not video_share_url:
30 | return create_error_response("无法从输入文本中提取有效的URL")
31 |
32 | video_info = await parse_video_share_url(video_share_url)
33 | logger.info(f"成功解析视频URL: {video_share_url}")
34 | return create_success_response("解析成功", video_info.__dict__)
35 | except ValueError as err:
36 | logger.error(f"URL解析失败: {err}")
37 | return create_error_response(str(err))
38 | except Exception as err:
39 | logger.error(f"未知错误: {err}")
40 | return create_error_response(f"解析失败: {str(err)}")
41 |
42 |
43 | async def video_id_parse_tool(source: str, video_id: str) -> Dict[str, Any]:
44 | """根据视频来源和ID解析视频信息"""
45 | if not source or not video_id:
46 | return create_error_response("视频来源和ID参数不能为空")
47 |
48 | try:
49 | video_source = VideoSource(source)
50 | video_info = await parse_video_id(video_source, video_id)
51 | logger.info(f"成功解析视频 - 来源: {source}, ID: {video_id}")
52 | return create_success_response("解析成功", video_info.__dict__)
53 | except ValueError as err:
54 | logger.error(f"视频ID解析失败: {err}")
55 | return create_error_response(f"无效的视频来源或ID: {str(err)}")
56 | except Exception as err:
57 | logger.error(f"未知错误: {err}")
58 | return create_error_response(f"解析失败: {str(err)}")
59 |
60 |
61 | async def share_text_parse_tool(
62 | text: str,
63 | api_base_url: Optional[str] = None,
64 | model: Optional[str] = None,
65 | ctx: Context = None,
66 | ) -> Dict[str, Any]:
67 | """
68 | 解析抖音分享链接,提取无水印视频地址
69 | 下载无水印视频
70 | 提取音频
71 | 转换音频为文本
72 | 清理临时文件
73 |
74 | 参数:
75 | - text: 抖音分享文本,包含分享链接
76 | - api_base_url: API基础URL,默认使用SiliconFlow
77 | - model: 语音识别模型,默认使用SenseVoiceSmall
78 | """
79 | if not text or not isinstance(text, str):
80 | return create_error_response("文本参数不能为空")
81 |
82 | try:
83 | # 获取配置参数
84 | api_key, api_base_url, model = get_api_configuration(ctx, api_base_url, model)
85 |
86 | if not api_key:
87 | error_msg = "没有传递apikey,请通过参数传入apikey或请求头传入apikey,或者设置环境变量API_KEY,否则无法使用视频内容提取功能!"
88 | logger.error(error_msg)
89 | return create_error_response(error_msg)
90 |
91 | processor = VideoProcessor(api_key, api_base_url, model)
92 |
93 | # 解析视频链接
94 | video_share_url = extract_url_from_text(text)
95 | if not video_share_url:
96 | error_msg = f"无法从文本中提取视频链接: {text}"
97 | logger.error(error_msg)
98 | return create_error_response(error_msg)
99 |
100 | video_obj = await parse_video_share_url(video_share_url)
101 | video_info = create_success_response("解析成功", video_obj.__dict__)
102 |
103 | # 处理视频下载和文本提取
104 | try:
105 | download_video_info = {
106 | "url": video_info["data"]["video_url"],
107 | "title": video_info["data"]["title"],
108 | "video_id": str(int(time.time())),
109 | }
110 |
111 | logger.info(f"开始下载视频: {download_video_info['title']}")
112 | video_path = await processor.download_video(download_video_info)
113 | ctx.info(f"视频下载地址: {video_path}")
114 |
115 | logger.info("开始提取音频")
116 | audio_path = processor.extract_audio(video_path)
117 | ctx.info(f"音频提取地址: {audio_path}")
118 |
119 | logger.info("开始转换音频为文本")
120 | text_content = processor.extract_text_from_audio(audio_path)
121 | ctx.info(f"文本提取内容: {text_content}")
122 |
123 | logger.info("清理临时文件")
124 | processor.cleanup_files(video_path, audio_path)
125 | ctx.info(f"临时文件清理: {video_path}, {audio_path}")
126 |
127 | return {
128 | "code": DEFAULT_RESPONSE_CODES['SUCCESS'],
129 | "msg": "解析成功",
130 | "data": video_info["data"],
131 | "text_content": text_content,
132 | }
133 | except Exception as processing_err:
134 | logger.error(f"视频处理失败: {processing_err}")
135 | # 确保清理可能存在的临时文件
136 | try:
137 | if 'video_path' in locals() and 'audio_path' in locals():
138 | processor.cleanup_files(video_path, audio_path)
139 | except Exception:
140 | pass
141 | raise processing_err
142 | except ValueError as err:
143 | logger.error(f"参数错误: {err}")
144 | if ctx:
145 | ctx.error(f"参数错误: {str(err)}")
146 | return create_error_response(str(err))
147 | except Exception as err:
148 | logger.error(f"未知错误: {err}")
149 | if ctx:
150 | ctx.error(f"解析失败: {str(err)}")
151 | return create_error_response(f"解析失败: {str(err)}")
--------------------------------------------------------------------------------
/script/deployBase.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | chcp 65001
3 | setlocal enabledelayedexpansion
4 |
5 | :: 设置颜色代码
6 | set "GREEN=[92m"
7 | set "YELLOW=[93m"
8 | set "RED=[91m"
9 | set "BLUE=[94m"
10 | set "MAGENTA=[95m"
11 | set "CYAN=[96m"
12 | set "WHITE=[97m"
13 | set "RESET=[0m"
14 |
15 | :: 打印标题
16 | echo.
17 | echo %BLUE%╔════════════════════════════════════════════════════════════╗%RESET%
18 | echo %BLUE%║%RESET% %CYAN% Docker 部署工具 v1.0.0 %BLUE%║%RESET%
19 | echo %BLUE%╚════════════════════════════════════════════════════════════╝%RESET%
20 | echo.
21 |
22 | :: ==================== 配置变量区域 ====================
23 | :: 切换到项目根目录
24 | cd /d "%~dp0.."
25 | echo %WHITE%[信息]%RESET% %CYAN%切换到项目根目录: %CD%%RESET%
26 |
27 | :: 设置代理
28 | set http_proxy=http://127.0.0.1:7890
29 | set https_proxy=http://127.0.0.1:7890
30 |
31 | :: Docker镜像相关配置
32 | set DOCKER_IMAGE_NAME=yby6/ffmpeg-python-base
33 | set DOCKER_IMAGE_TAG=1.0.0
34 | set DOCKER_PLATFORM=linux/amd64
35 | set DOCKERFILE_PATH=Dockerfile.base
36 |
37 |
38 | :: 阿里云配置
39 | set ALIYUN_REGISTRY=registry.cn-hangzhou.aliyuncs.com
40 | set NAMESPACE=yby6
41 | set IMAGE_NAME=ffmpeg-python-base
42 | set IMAGE_TAG=1.0.0
43 |
44 | :: Docker构建参数
45 | set BUILD_ARGS=--platform %DOCKER_PLATFORM% --load --progress plain
46 |
47 | :: ==================== 执行区域 ====================
48 | :: 检查 Docker 环境
49 | echo %WHITE%[1/6]%RESET% %YELLOW%检查 Docker 环境...%RESET%
50 | where docker >nul 2>nul
51 | if %errorlevel% neq 0 (
52 | echo %RED% ✗ Docker 未安装或未添加到环境变量中%RESET%
53 | echo %RED% 请先安装 Docker 并确保添加到环境变量中%RESET%
54 | pause
55 | exit /b 1
56 | )
57 | echo %GREEN% ✓ Docker 环境检查通过%RESET%
58 | echo.
59 |
60 | :: 检查 Docker 服务
61 | echo %WHITE%[2/6]%RESET% %YELLOW%检查 Docker 服务...%RESET%
62 | docker info >nul 2>nul
63 | if %errorlevel% neq 0 (
64 | echo %RED% ✗ Docker 服务未运行,请启动 Docker Desktop%RESET%
65 | pause
66 | exit /b 1
67 | )
68 | echo %GREEN% ✓ Docker 服务检查通过%RESET%
69 | echo.
70 |
71 | :: 检查配置文件
72 | echo %WHITE%[3/6]%RESET% %YELLOW%检查配置文件...%RESET%
73 | if not exist "%~dp0.local-config" (
74 | echo %RED% ✗ 配置文件不存在%RESET%
75 | echo %YELLOW% 请创建 script/.local-config 文件并填写以下内容:%RESET%
76 | echo %WHITE% ALIYUN_USERNAME=你的阿里云账号%RESET%
77 | echo %WHITE% ALIYUN_PASSWORD=你的阿里云密码%RESET%
78 | pause
79 | exit /b 1
80 | )
81 | echo %GREEN% ✓ 配置文件检查通过%RESET%
82 | echo.
83 |
84 | :: 读取认证信息
85 | echo %WHITE%[4/6]%RESET% %YELLOW%读取认证信息...%RESET%
86 | for /f "tokens=1,2 delims==" %%a in (%~dp0.local-config) do (
87 | if "%%a"=="ALIYUN_USERNAME" set ALIYUN_USERNAME=%%b
88 | if "%%a"=="ALIYUN_PASSWORD" set ALIYUN_PASSWORD=%%b
89 | )
90 |
91 | if "%ALIYUN_USERNAME%"=="" (
92 | echo %RED% ✗ ALIYUN_USERNAME 未设置%RESET%
93 | pause
94 | exit /b 1
95 | )
96 | if "%ALIYUN_PASSWORD%"=="" (
97 | echo %RED% ✗ ALIYUN_PASSWORD 未设置%RESET%
98 | pause
99 | exit /b 1
100 | )
101 |
102 | :: 移除引号
103 | set ALIYUN_USERNAME=%ALIYUN_USERNAME:"=%
104 | set ALIYUN_PASSWORD=%ALIYUN_PASSWORD:"=%
105 | echo %GREEN% ✓ 认证信息读取成功%RESET%
106 | echo.
107 |
108 | :: 构建 Docker 镜像
109 | echo %WHITE%[5/6]%RESET% %YELLOW%开始构建 Docker 镜像...%RESET%
110 | echo %WHITE% 镜像名称: %RESET%%CYAN%%DOCKER_IMAGE_NAME%:%DOCKER_IMAGE_TAG%%RESET%
111 | echo %WHITE% 构建平台: %RESET%%CYAN%%DOCKER_PLATFORM%%RESET%
112 |
113 | if not exist "%DOCKERFILE_PATH%" (
114 | echo %RED% ✗ Dockerfile不存在: %DOCKERFILE_PATH%%RESET%
115 | echo %RED% 当前目录: %CD%%RESET%
116 | pause
117 | exit /b 1
118 | )
119 |
120 | docker buildx build -f "%DOCKERFILE_PATH%" %BUILD_ARGS% -t %DOCKER_IMAGE_NAME%:%DOCKER_IMAGE_TAG% .
121 | if %errorlevel% neq 0 (
122 | echo %RED% ✗ Docker构建失败!错误代码: %errorlevel%%RESET%
123 | pause
124 | exit /b 1
125 | )
126 | echo %GREEN% ✓ 构建完成%RESET%
127 | echo.
128 |
129 | :: 推送 Docker 镜像到阿里云
130 | echo %WHITE%[6/6]%RESET% %YELLOW%推送 Docker 镜像到阿里云...%RESET%
131 |
132 | :: 登录到阿里云容器镜像服务
133 | echo %WHITE% 登录阿里云容器镜像服务...%RESET%
134 | docker login --username="%ALIYUN_USERNAME%" --password="%ALIYUN_PASSWORD%" %ALIYUN_REGISTRY% >nul 2>&1
135 | if %errorlevel% neq 0 (
136 | echo %RED% ✗ 登录失败%RESET%
137 | echo %RED% 请检查用户名和密码是否正确%RESET%
138 | pause
139 | exit /b 1
140 | )
141 | echo %GREEN% ✓ 登录成功%RESET%
142 |
143 | :: 为 Docker 镜像打标签
144 | echo %WHITE% 为 Docker 镜像打标签...%RESET%
145 | docker tag %NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG% %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG% >nul 2>&1
146 | if %errorlevel% neq 0 (
147 | echo %RED% ✗ 打标签失败%RESET%
148 | echo %RED% 请检查本地镜像是否存在:%RESET%
149 | echo %WHITE% docker images ^| findstr %IMAGE_NAME%%RESET%
150 | pause
151 | exit /b 1
152 | )
153 | echo %GREEN% ✓ 标签设置成功%RESET%
154 |
155 | :: 推送镜像
156 | echo %WHITE% 推送 Docker 镜像...%RESET%
157 | docker push %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%
158 | if %errorlevel% neq 0 (
159 | echo %RED% ✗ 推送失败%RESET%
160 | echo %RED% 请检查权限和网络连接%RESET%
161 | pause
162 | exit /b 1
163 | )
164 | echo %GREEN% ✓ 推送完成%RESET%
165 |
166 | :: 登出阿里云容器镜像服务
167 | echo %WHITE% 登出阿里云容器镜像服务...%RESET%
168 | docker logout %ALIYUN_REGISTRY% >nul 2>&1
169 | echo %GREEN% ✓ 已安全登出%RESET%
170 | echo.
171 |
172 | :: 打印完成信息
173 | echo %GREEN%╔════════════════════════════════════════════════════════════╗%RESET%
174 | echo %GREEN%║%RESET% %CYAN% 部署完成! %GREEN%║%RESET%
175 | echo %GREEN%╚════════════════════════════════════════════════════════════╝%RESET%
176 | echo.
177 |
178 | :: 显示镜像信息
179 | echo %MAGENTA%镜像信息:%RESET%
180 | echo %WHITE% 仓库地址:%RESET%%CYAN%%ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%%RESET%
181 | echo %WHITE% 检出命令:%RESET%%CYAN%docker pull %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%%RESET%
182 | echo %WHITE% 标签设置:%RESET%%CYAN%docker tag %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG% %NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%%RESET%
183 | echo.
184 |
185 | pause
--------------------------------------------------------------------------------
/script/deployMcp.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | chcp 65001
3 | setlocal enabledelayedexpansion
4 |
5 | :: 设置颜色代码
6 | set "GREEN=[92m"
7 | set "YELLOW=[93m"
8 | set "RED=[91m"
9 | set "BLUE=[94m"
10 | set "MAGENTA=[95m"
11 | set "CYAN=[96m"
12 | set "WHITE=[97m"
13 | set "RESET=[0m"
14 |
15 | :: 打印标题
16 | echo.
17 | echo %BLUE%╔════════════════════════════════════════════════════════════╗%RESET%
18 | echo %BLUE%║%RESET% %CYAN% Docker 部署工具 v1.0.0 %BLUE%║%RESET%
19 | echo %BLUE%╚════════════════════════════════════════════════════════════╝%RESET%
20 | echo.
21 |
22 | :: ==================== 配置变量区域 ====================
23 | :: 切换到项目根目录
24 | cd /d "%~dp0.."
25 | echo %WHITE%[信息]%RESET% %CYAN%切换到项目根目录: %CD%%RESET%
26 |
27 | :: 设置代理
28 | set http_proxy=http://127.0.0.1:7890
29 | set https_proxy=http://127.0.0.1:7890
30 |
31 | :: Docker镜像相关配置
32 | set DOCKER_IMAGE_NAME=yby6/yby6_video_mcp_server
33 | set DOCKER_IMAGE_TAG=1.0.2
34 | set DOCKER_PLATFORM=linux/amd64
35 | set DOCKERFILE_PATH=Dockerfile.mcp
36 |
37 |
38 | :: 阿里云配置
39 | set ALIYUN_REGISTRY=registry.cn-hangzhou.aliyuncs.com
40 | set NAMESPACE=yby6
41 | set IMAGE_NAME=yby6_video_mcp_server
42 | set IMAGE_TAG=1.0.2
43 |
44 | :: Docker构建参数
45 | set BUILD_ARGS=--platform %DOCKER_PLATFORM% --load --progress plain
46 |
47 | :: ==================== 执行区域 ====================
48 | :: 检查 Docker 环境
49 | echo %WHITE%[1/6]%RESET% %YELLOW%检查 Docker 环境...%RESET%
50 | where docker >nul 2>nul
51 | if %errorlevel% neq 0 (
52 | echo %RED% ✗ Docker 未安装或未添加到环境变量中%RESET%
53 | echo %RED% 请先安装 Docker 并确保添加到环境变量中%RESET%
54 | pause
55 | exit /b 1
56 | )
57 | echo %GREEN% ✓ Docker 环境检查通过%RESET%
58 | echo.
59 |
60 | :: 检查 Docker 服务
61 | echo %WHITE%[2/6]%RESET% %YELLOW%检查 Docker 服务...%RESET%
62 | docker info >nul 2>nul
63 | if %errorlevel% neq 0 (
64 | echo %RED% ✗ Docker 服务未运行,请启动 Docker Desktop%RESET%
65 | pause
66 | exit /b 1
67 | )
68 | echo %GREEN% ✓ Docker 服务检查通过%RESET%
69 | echo.
70 |
71 | :: 检查配置文件
72 | echo %WHITE%[3/6]%RESET% %YELLOW%检查配置文件...%RESET%
73 | if not exist "%~dp0.local-config" (
74 | echo %RED% ✗ 配置文件不存在%RESET%
75 | echo %YELLOW% 请创建 script/.local-config 文件并填写以下内容:%RESET%
76 | echo %WHITE% ALIYUN_USERNAME=你的阿里云账号%RESET%
77 | echo %WHITE% ALIYUN_PASSWORD=你的阿里云密码%RESET%
78 | pause
79 | exit /b 1
80 | )
81 | echo %GREEN% ✓ 配置文件检查通过%RESET%
82 | echo.
83 |
84 | :: 读取认证信息
85 | echo %WHITE%[4/6]%RESET% %YELLOW%读取认证信息...%RESET%
86 | for /f "tokens=1,2 delims==" %%a in (%~dp0.local-config) do (
87 | if "%%a"=="ALIYUN_USERNAME" set ALIYUN_USERNAME=%%b
88 | if "%%a"=="ALIYUN_PASSWORD" set ALIYUN_PASSWORD=%%b
89 | )
90 |
91 | if "%ALIYUN_USERNAME%"=="" (
92 | echo %RED% ✗ ALIYUN_USERNAME 未设置%RESET%
93 | pause
94 | exit /b 1
95 | )
96 | if "%ALIYUN_PASSWORD%"=="" (
97 | echo %RED% ✗ ALIYUN_PASSWORD 未设置%RESET%
98 | pause
99 | exit /b 1
100 | )
101 |
102 | :: 移除引号
103 | set ALIYUN_USERNAME=%ALIYUN_USERNAME:"=%
104 | set ALIYUN_PASSWORD=%ALIYUN_PASSWORD:"=%
105 | echo %GREEN% ✓ 认证信息读取成功%RESET%
106 | echo.
107 |
108 | :: 构建 Docker 镜像
109 | echo %WHITE%[5/6]%RESET% %YELLOW%开始构建 Docker 镜像...%RESET%
110 | echo %WHITE% 镜像名称: %RESET%%CYAN%%DOCKER_IMAGE_NAME%:%DOCKER_IMAGE_TAG%%RESET%
111 | echo %WHITE% 构建平台: %RESET%%CYAN%%DOCKER_PLATFORM%%RESET%
112 |
113 | if not exist "%DOCKERFILE_PATH%" (
114 | echo %RED% ✗ Dockerfile不存在: %DOCKERFILE_PATH%%RESET%
115 | echo %RED% 当前目录: %CD%%RESET%
116 | pause
117 | exit /b 1
118 | )
119 |
120 | docker buildx build -f "%DOCKERFILE_PATH%" %BUILD_ARGS% -t %DOCKER_IMAGE_NAME%:%DOCKER_IMAGE_TAG% .
121 | if %errorlevel% neq 0 (
122 | echo %RED% ✗ Docker构建失败!错误代码: %errorlevel%%RESET%
123 | pause
124 | exit /b 1
125 | )
126 | echo %GREEN% ✓ 构建完成%RESET%
127 | echo.
128 |
129 | :: 推送 Docker 镜像到阿里云
130 | echo %WHITE%[6/6]%RESET% %YELLOW%推送 Docker 镜像到阿里云...%RESET%
131 |
132 | :: 登录到阿里云容器镜像服务
133 | echo %WHITE% 登录阿里云容器镜像服务...%RESET%
134 | docker login --username="%ALIYUN_USERNAME%" --password="%ALIYUN_PASSWORD%" %ALIYUN_REGISTRY% >nul 2>&1
135 | if %errorlevel% neq 0 (
136 | echo %RED% ✗ 登录失败%RESET%
137 | echo %RED% 请检查用户名和密码是否正确%RESET%
138 | pause
139 | exit /b 1
140 | )
141 | echo %GREEN% ✓ 登录成功%RESET%
142 |
143 | :: 为 Docker 镜像打标签
144 | echo %WHITE% 为 Docker 镜像打标签...%RESET%
145 | docker tag %NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG% %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG% >nul 2>&1
146 | if %errorlevel% neq 0 (
147 | echo %RED% ✗ 打标签失败%RESET%
148 | echo %RED% 请检查本地镜像是否存在:%RESET%
149 | echo %WHITE% docker images ^| findstr %IMAGE_NAME%%RESET%
150 | pause
151 | exit /b 1
152 | )
153 | echo %GREEN% ✓ 标签设置成功%RESET%
154 |
155 | :: 推送镜像
156 | echo %WHITE% 推送 Docker 镜像...%RESET%
157 | docker push %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%
158 | if %errorlevel% neq 0 (
159 | echo %RED% ✗ 推送失败%RESET%
160 | echo %RED% 请检查权限和网络连接%RESET%
161 | pause
162 | exit /b 1
163 | )
164 | echo %GREEN% ✓ 推送完成%RESET%
165 |
166 | :: 登出阿里云容器镜像服务
167 | echo %WHITE% 登出阿里云容器镜像服务...%RESET%
168 | docker logout %ALIYUN_REGISTRY% >nul 2>&1
169 | echo %GREEN% ✓ 已安全登出%RESET%
170 | echo.
171 |
172 | :: 打印完成信息
173 | echo %GREEN%╔════════════════════════════════════════════════════════════╗%RESET%
174 | echo %GREEN%║%RESET% %CYAN% 部署完成! %GREEN%║%RESET%
175 | echo %GREEN%╚════════════════════════════════════════════════════════════╝%RESET%
176 | echo.
177 |
178 | :: 显示镜像信息
179 | echo %MAGENTA%镜像信息:%RESET%
180 | echo %WHITE% 仓库地址:%RESET%%CYAN%%ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%%RESET%
181 | echo %WHITE% 检出命令:%RESET%%CYAN%docker pull %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%%RESET%
182 | echo %WHITE% 标签设置:%RESET%%CYAN%docker tag %ALIYUN_REGISTRY%/%NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG% %NAMESPACE%/%IMAGE_NAME%:%IMAGE_TAG%%RESET%
183 | echo.
184 |
185 | pause
--------------------------------------------------------------------------------
/script/deployMcp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 设置编码为 UTF-8
4 | export LANG=UTF-8
5 |
6 | # 设置颜色代码
7 | GREEN='\033[92m'
8 | YELLOW='\033[93m'
9 | RED='\033[91m'
10 | BLUE='\033[94m'
11 | MAGENTA='\033[95m'
12 | CYAN='\033[96m'
13 | WHITE='\033[97m'
14 | RESET='\033[0m'
15 |
16 | # 打印标题
17 | echo ""
18 | echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${RESET}"
19 | echo -e "${BLUE}║${RESET} ${CYAN} Docker 部署工具 v1.0.0 ${BLUE}║${RESET}"
20 | echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${RESET}"
21 | echo ""
22 |
23 | # ==================== 配置变量区域 ====================
24 | # 切换到项目根目录
25 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
27 | cd "$PROJECT_ROOT"
28 | echo -e "${WHITE}[信息]${RESET} ${CYAN}切换到项目根目录: $(pwd)${RESET}"
29 |
30 | # 设置代理
31 | export http_proxy=http://127.0.0.1:7890
32 | export https_proxy=http://127.0.0.1:7890
33 |
34 | # Docker镜像相关配置
35 | DOCKER_IMAGE_NAME="yby6/yby6_video_mcp_server"
36 | DOCKER_IMAGE_TAG="1.0.2"
37 | DOCKER_PLATFORM="linux/amd64,linux/arm64"
38 | DOCKERFILE_PATH="Dockerfile.mcp"
39 |
40 | # 阿里云配置
41 | ALIYUN_REGISTRY="registry.cn-hangzhou.aliyuncs.com"
42 | NAMESPACE="yby6"
43 | IMAGE_NAME="yby6_video_mcp_server"
44 | IMAGE_TAG="1.0.2"
45 |
46 | # Docker构建参数
47 | BUILD_ARGS="--platform $DOCKER_PLATFORM --load --progress plain"
48 |
49 | # ==================== 执行区域 ====================
50 | # 检查 Docker 环境
51 | echo -e "${WHITE}[1/6]${RESET} ${YELLOW}检查 Docker 环境...${RESET}"
52 | if ! command -v docker &> /dev/null; then
53 | echo -e "${RED} ✗ Docker 未安装或未添加到环境变量中${RESET}"
54 | echo -e "${RED} 请先安装 Docker 并确保添加到环境变量中${RESET}"
55 | exit 1
56 | fi
57 | echo -e "${GREEN} ✓ Docker 环境检查通过${RESET}"
58 | echo ""
59 |
60 | # 检查 Docker 服务
61 | echo -e "${WHITE}[2/6]${RESET} ${YELLOW}检查 Docker 服务...${RESET}"
62 | if ! docker info &> /dev/null; then
63 | echo -e "${RED} ✗ Docker 服务未运行,请启动 Docker${RESET}"
64 | exit 1
65 | fi
66 | echo -e "${GREEN} ✓ Docker 服务检查通过${RESET}"
67 | echo ""
68 |
69 | # 检查配置文件
70 | echo -e "${WHITE}[3/6]${RESET} ${YELLOW}检查配置文件...${RESET}"
71 | CONFIG_FILE="$SCRIPT_DIR/.local-config"
72 | if [ ! -f "$CONFIG_FILE" ]; then
73 | echo -e "${RED} ✗ 配置文件不存在${RESET}"
74 | echo -e "${YELLOW} 请创建 script/.local-config 文件并填写以下内容:${RESET}"
75 | echo -e "${WHITE} ALIYUN_USERNAME=你的阿里云账号${RESET}"
76 | echo -e "${WHITE} ALIYUN_PASSWORD=你的阿里云密码${RESET}"
77 | exit 1
78 | fi
79 | echo -e "${GREEN} ✓ 配置文件检查通过${RESET}"
80 | echo ""
81 |
82 | # 读取认证信息
83 | echo -e "${WHITE}[4/6]${RESET} ${YELLOW}读取认证信息...${RESET}"
84 |
85 | # 读取配置文件
86 | while IFS='=' read -r key value; do
87 | # 跳过注释和空行
88 | [[ $key =~ ^[[:space:]]*# ]] && continue
89 | [[ -z "$key" ]] && continue
90 |
91 | # 移除前后空格和引号
92 | key=$(echo "$key" | xargs)
93 | value=$(echo "$value" | xargs | sed 's/^["'\'']*//;s/["'\'']*$//')
94 |
95 | case "$key" in
96 | "ALIYUN_USERNAME")
97 | ALIYUN_USERNAME="$value"
98 | ;;
99 | "ALIYUN_PASSWORD")
100 | ALIYUN_PASSWORD="$value"
101 | ;;
102 | esac
103 | done < "$CONFIG_FILE"
104 |
105 | if [ -z "$ALIYUN_USERNAME" ]; then
106 | echo -e "${RED} ✗ ALIYUN_USERNAME 未设置${RESET}"
107 | exit 1
108 | fi
109 | if [ -z "$ALIYUN_PASSWORD" ]; then
110 | echo -e "${RED} ✗ ALIYUN_PASSWORD 未设置${RESET}"
111 | exit 1
112 | fi
113 |
114 | echo -e "${GREEN} ✓ 认证信息读取成功${RESET}"
115 | echo ""
116 |
117 | # 构建 Docker 镜像
118 | echo -e "${WHITE}[5/6]${RESET} ${YELLOW}开始构建 Docker 镜像...${RESET}"
119 | echo -e "${WHITE} 镜像名称: ${RESET}${CYAN}$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG${RESET}"
120 | echo -e "${WHITE} 构建平台: ${RESET}${CYAN}$DOCKER_PLATFORM${RESET}"
121 |
122 | if [ ! -f "$DOCKERFILE_PATH" ]; then
123 | echo -e "${RED} ✗ Dockerfile不存在: $DOCKERFILE_PATH${RESET}"
124 | echo -e "${RED} 当前目录: $(pwd)${RESET}"
125 | exit 1
126 | fi
127 |
128 | if ! docker buildx build -f "$DOCKERFILE_PATH" $BUILD_ARGS -t "$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG" .; then
129 | echo -e "${RED} ✗ Docker构建失败!错误代码: $?${RESET}"
130 | exit 1
131 | fi
132 | echo -e "${GREEN} ✓ 构建完成${RESET}"
133 | echo ""
134 |
135 | # 推送 Docker 镜像到阿里云
136 | echo -e "${WHITE}[6/6]${RESET} ${YELLOW}推送 Docker 镜像到阿里云...${RESET}"
137 |
138 | # 登录到阿里云容器镜像服务
139 | echo -e "${WHITE} 登录阿里云容器镜像服务...${RESET}"
140 | if ! echo "$ALIYUN_PASSWORD" | docker login --username="$ALIYUN_USERNAME" --password-stdin "$ALIYUN_REGISTRY" &> /dev/null; then
141 | echo -e "${RED} ✗ 登录失败${RESET}"
142 | echo -e "${RED} 请检查用户名和密码是否正确${RESET}"
143 | exit 1
144 | fi
145 | echo -e "${GREEN} ✓ 登录成功${RESET}"
146 |
147 | # 为 Docker 镜像打标签
148 | echo -e "${WHITE} 为 Docker 镜像打标签...${RESET}"
149 | if ! docker tag "$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG" "$ALIYUN_REGISTRY/$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG" &> /dev/null; then
150 | echo -e "${RED} ✗ 打标签失败${RESET}"
151 | echo -e "${RED} 请检查本地镜像是否存在:${RESET}"
152 | echo -e "${WHITE} docker images | grep $IMAGE_NAME${RESET}"
153 | exit 1
154 | fi
155 | echo -e "${GREEN} ✓ 标签设置成功${RESET}"
156 |
157 | # 推送镜像
158 | echo -e "${WHITE} 推送 Docker 镜像...${RESET}"
159 | if ! docker push "$ALIYUN_REGISTRY/$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG"; then
160 | echo -e "${RED} ✗ 推送失败${RESET}"
161 | echo -e "${RED} 请检查权限和网络连接${RESET}"
162 | exit 1
163 | fi
164 | echo -e "${GREEN} ✓ 推送完成${RESET}"
165 |
166 | # 登出阿里云容器镜像服务
167 | echo -e "${WHITE} 登出阿里云容器镜像服务...${RESET}"
168 | docker logout "$ALIYUN_REGISTRY" &> /dev/null
169 | echo -e "${GREEN} ✓ 已安全登出${RESET}"
170 | echo ""
171 |
172 | # 打印完成信息
173 | echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${RESET}"
174 | echo -e "${GREEN}║${RESET} ${CYAN} 部署完成! ${GREEN}║${RESET}"
175 | echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${RESET}"
176 | echo ""
177 |
178 | # 显示镜像信息
179 | echo -e "${MAGENTA}镜像信息:${RESET}"
180 | echo -e "${WHITE} 仓库地址:${RESET}${CYAN}$ALIYUN_REGISTRY/$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG${RESET}"
181 | echo -e "${WHITE} 检出命令:${RESET}${CYAN}docker pull $ALIYUN_REGISTRY/$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG${RESET}"
182 | echo -e "${WHITE} 标签设置:${RESET}${CYAN}docker tag $ALIYUN_REGISTRY/$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG $NAMESPACE/$IMAGE_NAME:$IMAGE_TAG${RESET}"
183 | echo ""
184 |
185 | echo "按回车键继续..."
186 | read -r
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 全网短视频去水印链接提取 MCP服务
2 |
3 | ## 项目简介
4 |
5 | 本项目是一个基于 FastMCP 的全网短视频去水印解析服务,支持多平台视频分享链接的解析,自动提取视频真实地址及相关信息。
6 | 适用于需要批量解析、去水印、采集短视频的场景。本项目还支持视频内容文本提取功能,可以通过语音识别将视频内容转为文本。
7 |
8 | [](https://pypi.org/project/yby6-video-mcp-server/)
9 |
10 | ## 主要特性
11 |
12 | - 支持20+种短视频平台(抖音、快手、小红书、微博、西瓜视频等)
13 | - 一键解析视频分享链接,获取无水印视频地址
14 | - 支持多种传输方式(stdio、SSE、HTTP)
15 | - 支持视频内容文本提取功能(需要FFmpeg和API Key、默认使用硅基流动:https://cloud.siliconflow.cn/i/tbvUltCF )
16 | - 支持Docker容器化部署
17 | - 代码结构清晰,易于扩展
18 |
19 |
20 | ## 演示
21 |
22 |
23 |
24 |
25 | ## 支持平台
26 |
27 | 目前支持以下短视频平台的解析:
28 |
29 | - 抖音(DouYin)
30 | - 快手(KuaiShou)
31 | - 小红书(RedBook)
32 | - 微博(WeiBo)
33 | - 皮皮虾(PiPiXia)
34 | - 微视(WeiShi)
35 | - 绿洲(LvZhou)
36 | - 最右(ZuiYou)
37 | - 度小视/全民小视频(QuanMin)
38 | - 西瓜视频(XiGua)
39 | - 梨视频(LiShiPin)
40 | - 皮皮搞笑(PiPiGaoXiao)
41 | - 虎牙(HuYa)
42 | - AcFun(AcFun)
43 | - 逗拍(DouPai)
44 | - 美拍(MeiPai)
45 | - 全民K歌(QuanMinKGe)
46 | - 六间房(SixRoom)
47 | - 新片场(XinPianChang)
48 | - 好看视频(HaoKan)
49 |
50 |
51 | ## 目录结构
52 |
53 | ```
54 | crawling-short-video-mcp/
55 | ├── yby6_video_mcp_server/
56 | │ ├── server.py # 主服务入口
57 | │ ├── functionality/ # 各平台解析功能模块
58 | │ │ ├── base.py # 基础类和枚举定义
59 | │ │ ├── douyin.py # 抖音解析实现
60 | │ │ ├── kuaishou.py # 快手解析实现
61 | │ │ ├── ... # 其他平台实现
62 | │ │ └── video_processor.py # 视频处理和文本提取
63 | │ └── utils/ # 工具函数
64 | ├── Dockerfile.base # 基础镜像构建文件
65 | ├── Dockerfile.mcp # MCP服务镜像构建文件
66 | ├── requirements.txt # 依赖包列表
67 | ├── pyproject.toml # 项目配置和元数据
68 | └── README.md # 项目说明
69 | ```
70 |
71 | ## 安装方法
72 |
73 | ### 前置依赖
74 |
75 | #### FFmpeg 安装(视频文本内容提取必需)
76 |
77 | **Ubuntu/Debian:**
78 | ```bash
79 | sudo apt update
80 | sudo apt install ffmpeg
81 | ```
82 |
83 | **CentOS/RHEL:**
84 | ```bash
85 | sudo yum install epel-release
86 | sudo yum install ffmpeg ffmpeg-devel
87 | ```
88 |
89 | **macOS:**
90 | ```bash
91 | brew install ffmpeg
92 | ```
93 |
94 | **Windows:**
95 | 1. 下载 FFmpeg: https://ffmpeg.org/download.html#build-windows
96 | 2. 解压到指定目录,如 `C:\ffmpeg`
97 | 3. 添加到系统环境变量 PATH: `C:\ffmpeg\bin`
98 | 4. 重启命令行或系统以使环境变量生效
99 |
100 | 验证安装:
101 | ```bash
102 | ffmpeg -version
103 | ```
104 |
105 | ### 方法一:pypi 使用 pip 安装 (推荐)
106 | 当前最新版本: [](https://pypi.org/project/yby6-video-mcp-server/)
107 |
108 | ```bash
109 | # 安装最新版本
110 | pip install -i https://pypi.org/simple yby6-video-mcp-server
111 |
112 | # 或指定版本安装
113 | pip install -i https://pypi.org/simple yby6-video-mcp-server==1.0.2
114 | ```
115 |
116 | 安装完成后,可以通过以下命令验证安装:
117 |
118 | ```bash
119 | yby6_video_mcp_server --version
120 | ```
121 |
122 | ## MCP 配置使用
123 |
124 | ```json
125 | "yby6_video_mcp_server": {
126 | "command": "uv",
127 | "args": ["yby6_video_mcp_server"],
128 | "env": {
129 | "API_KEY": "前往获取免费apikey: https://cloud.siliconflow.cn/i/tbvUltCF"
130 | }
131 | }
132 | ```
133 |
134 | ### 方法二:从源码安装
135 |
136 | 1. 克隆本项目
137 |
138 | ```bash
139 | git clone https://github.com/yangbuyiya/yby6-crawling-short-video-mcp.git
140 | cd yby6-crawling-short-video-mcp
141 | ```
142 |
143 | 2. 安装依赖
144 |
145 | 推荐使用 Python 3.10+,并建议使用虚拟环境:
146 |
147 | **macOS/Linux:**
148 | ```bash
149 | python -m venv venv
150 | source venv/bin/activate
151 | pip install -r requirements.txt
152 | ```
153 |
154 | **Windows:**
155 | ```bash
156 | python -m venv venv
157 | venv\Scripts\activate
158 | pip install -r requirements.txt
159 | ```
160 |
161 | ### 方法三:使用 Docker 部署
162 |
163 | 本项目提供了 Docker 支持, 快速部署运行
164 |
165 | 1. 运行容器 拉取镜像 sse 模式
166 |
167 | ```bash
168 | docker run -d -p 8637:8637 registry.cn-hangzhou.aliyuncs.com/yby6/yby6_video_mcp_server:1.0.2
169 | ```
170 |
171 |
172 | 可以通过以下命令快速构建部署:
173 |
174 | 1. 构建基础镜像(包含 FFmpeg 和 Python 环境)
175 |
176 | ```bash
177 | docker build -t ffmpeg-python-base:1.0.2 -f Dockerfile.base .
178 | ```
179 |
180 | 2. 构建 MCP 服务镜像
181 |
182 | ```bash
183 | docker build -t yby6-video-mcp:latest -f Dockerfile.mcp .
184 | ```
185 |
186 | 3. 运行容器
187 |
188 | ```bash
189 | docker run -d -p 8637:8637 yby6-video-mcp:latest
190 | ```
191 |
192 | ## 使用方法
193 |
194 | ### stdio MCP启动服务
195 |
196 | ```json
197 | // pypi 拉取运行
198 | "yby6_video_mcp_server": {
199 | "command": "uv",
200 | "args": ["yby6_video_mcp_server"],
201 | "env": {
202 | "API_KEY": "sk-xcazqbgbnoagddpyaorhqhioxazvqdtednppksiqaotjsboe"
203 | }
204 | },
205 |
206 | // 从源码运行
207 | "yby6_video_mcp_server": {
208 | "command": "uv",
209 | "args": [
210 | "--directory",
211 | "to/path/yby6-crawling-short-video-mcp/yby6_video_mcp_server",
212 | "run",
213 | "-m",
214 | "yby6_video_mcp_server.server"
215 | ],
216 | "env": {
217 | "API_KEY": "sk-xcazqbgbnoagddpyaorhqhioxazvqdtednppksiqaotjsboe"
218 | }
219 | },
220 | ```
221 |
222 | ### SSE or HTTP MCP服务地址
223 |
224 | - 支持请求头配置apikey
225 | - 支持请求参数配置apikey
226 |
227 | ```json
228 |
229 | "yby6_video_mcp_server": {
230 | "url": "http://127.0.0.1:8637/sse?apikey=xxxxxx",
231 | }
232 |
233 | ```
234 |
235 | ### 启动服务
236 |
237 | ```bash
238 | # 使用pip安装后
239 | yby6_video_mcp_server --transport http --host 0.0.0.0 --port 8637
240 |
241 | # 或从源码运行
242 | python -m yby6_video_mcp_server.server --transport http --host 0.0.0.0 --port 8637
243 | ```
244 |
245 | 参数说明:
246 | - `--transport` 传输方式,可选:`stdio`、`sse`、`http`(推荐 http)
247 | - `--host` 主机地址,默认 `0.0.0.0`
248 | - `--port` 端口号,默认 `8000`
249 | - `--path` 自定义MCP请求路径(可选)
250 |
251 | ### API 接口说明
252 |
253 | #### 1. 解析视频分享链接
254 |
255 | **接口名称:** `share_url_parse_tool`
256 |
257 | **请求参数:**
258 |
259 | | 参数名 | 类型 | 必填 | 说明 |
260 | |--------|--------|------|------------------|
261 | | url | string | 是 | 视频分享链接 |
262 |
263 | **返回示例:**
264 |
265 | ```json
266 | {
267 | "code": 200,
268 | "msg": "解析成功",
269 | "data": {
270 | "video_url": "https://xxx.com/xxx.mp4",
271 | "cover_url": "https://xxx.com/cover.jpg",
272 | "title": "视频标题",
273 | "music_url": "https://xxx.com/music.mp3",
274 | "images": [],
275 | "author": {
276 | "uid": "用户ID",
277 | "name": "用户名",
278 | "avatar": "头像URL"
279 | }
280 | }
281 | }
282 | ```
283 |
284 | #### 2. 根据视频ID解析
285 |
286 | **接口名称:** `video_id_parse_tool`
287 |
288 | **请求参数:**
289 |
290 | | 参数名 | 类型 | 必填 | 说明 |
291 | |-----------|--------|------|--------------------------------------|
292 | | source | string | 是 | 视频来源,如 douyin、kuaishou 等 |
293 | | video_id | string | 是 | 视频ID |
294 |
295 | **返回示例:** 同上
296 |
297 | #### 3. 视频内容文本提取
298 |
299 | **接口名称:** `share_text_parse_tool`
300 |
301 | **请求参数:**
302 |
303 | | 参数名 | 类型 | 必填 | 说明 |
304 | |--------------|--------|------|------------------------------------------|
305 | | share_link | string | 是 | 抖音分享链接或包含链接的文本 |
306 | | api_base_url | string | 否 | API基础URL,默认使用SiliconFlow |
307 | | model | string | 否 | 语音识别模型,默认使用SenseVoiceSmall |
308 |
309 | > 链接 sse、Streamable HTTP模式的时候只需要将 apikey 带入请求参数当中: http://127.0.0.1:8637/sse?apikey=xxxxxx
310 | > 使用的大模型是硅基流动前往获取apikey即可:https://cloud.siliconflow.cn/i/tbvUltCF
311 |
312 | **返回示例:**
313 |
314 | ```json
315 | {
316 | "code": 200,
317 | "msg": "解析成功",
318 | "data": {
319 | "video_url": "https://xxx.com/xxx.mp4",
320 | "cover_url": "https://xxx.com/cover.jpg",
321 | "title": "视频标题",
322 | "author": {
323 | "uid": "用户ID",
324 | "name": "用户名",
325 | "avatar": "头像URL"
326 | }
327 | },
328 | "text_content": "视频中的语音文本内容"
329 | }
330 | ```
331 |
332 | ## 依赖说明
333 |
334 | 主要依赖包括:
335 |
336 | - fastmcp: MCP服务框架
337 | - httpx: 异步HTTP客户端
338 | - ffmpeg-python: 视频处理
339 | - lxml & parsel: HTML解析
340 | - fake-useragent: 模拟浏览器请求
341 | - pydantic: 数据验证
342 |
343 | ## Docker 部署
344 |
345 | 项目提供了两个Dockerfile:
346 | - `Dockerfile.base`: 构建基础镜像,包含Python环境和FFmpeg
347 | - `Dockerfile.mcp`: 构建MCP服务镜像
348 |
349 | 使用脚本快速部署:
350 | ```bash
351 | # Windows
352 | .\script\deployBase.bat
353 | .\script\deployMcp.bat
354 |
355 | # Linux/macOS
356 | bash script/deployBase.sh
357 | bash script/deployMcp.sh
358 | ```
359 |
360 | 运行容器 拉取镜像 sse 模式
361 | ```bash
362 | docker run -d -p 8637:8637 registry.cn-hangzhou.aliyuncs.com/yby6/yby6_video_mcp_server:1.0.2
363 | ```
364 |
365 | ## 贡献与反馈
366 |
367 | 欢迎提交 issue 或 PR 参与项目改进!
368 |
369 | - 项目地址: https://github.com/yangbuyiya/yby6-crawling-short-video-mcp
370 | - 作者邮箱: yangbuyiya@duck.com
371 |
372 | ---
373 |
374 | 本项目站在巨人的肩膀上二次开发,感谢以下项目:
375 |
376 | - [parse-video-py](https://github.com/wujunwei928/parse-video-py)
377 | - [fastmcp](https://github.com/jlowin/fastmcp)
378 |
379 | # ⚠️ 免责声明
380 |
381 | 1. 本项目为开源工具,仅供学习与研究用途。用户在使用本项目过程中产生的任何风险、损失或法律责任,均由用户本人承担,作者及贡献者不承担任何直接或间接责任。
382 | 2. 本项目的功能和代码基于现有技术实现,作者不承诺其完全正确、无缺陷或持续可用。因项目缺陷或不可用导致的任何后果,作者概不负责。
383 | 3. 本项目依赖的第三方库、插件或服务,均遵循其原有协议。用户需自行查阅、遵守相关协议,因违反第三方协议产生的责任由用户自负。
384 | 4. 用户应确保自身的使用行为合法、合规,不得利用本项目从事任何违法、违规或侵犯他人权益的活动。因违法违规使用本项目产生的后果,均由用户自行承担。
385 | 5. 严禁将本项目用于侵犯知识产权、传播违法信息、商业破解、数据爬取等非法用途。作者坚决反对并不支持任何非法用途。
386 | 6. 用户在处理数据时,应确保符合相关法律法规(如数据合规、隐私保护等),因违规操作产生的责任由用户承担。
387 | 7. 作者、贡献者与用户的具体使用行为无关,不承担任何连带责任。基于本项目的二次开发、修改、分发等行为,均与作者无关,相关责任由行为人自负。
388 | 8. 本项目不授予任何专利许可。因专利纠纷或侵权产生的风险由用户承担。未经作者书面授权,禁止将本项目用于商业宣传、再授权或其他商业用途。
389 | 9. 作者有权随时终止对违规用户的服务,并要求其删除相关代码和数据。
390 | 10. 作者有权随时更新本免责声明,用户继续使用即视为接受最新条款。如不同意本声明,请立即停止使用本项目。
391 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | # Video Watermark Removal & Link Extraction MCP Service
2 |
3 | ## Project Introduction
4 |
5 | This project is a FastMCP-based service for parsing short video sharing links from multiple platforms. It automatically extracts the original video URL and related information without watermarks.
6 | Suitable for scenarios requiring batch parsing, watermark removal, and short video collection. The project also supports video content text extraction through speech recognition.
7 |
8 | [](https://pypi.org/project/yby6-video-mcp-server/)
9 |
10 | ## Main Features
11 |
12 | - Supports 20+ short video platforms (Douyin, Kuaishou, Xiaohongshu, Weibo, Xigua Video, etc.)
13 | - One-click parsing of video sharing links to obtain watermark-free video URLs
14 | - Supports multiple transport methods (stdio, SSE, HTTP)
15 | - Supports video content text extraction (requires FFmpeg and API Key, uses SiliconFlow by default: https://cloud.siliconflow.cn/i/tbvUltCF)
16 | - Supports Docker containerized deployment
17 | - Clear code structure, easy to extend
18 |
19 | ## Demo
20 |
21 |
22 |
23 | ## Supported Platforms
24 |
25 | Currently supports parsing from the following short video platforms:
26 |
27 | - Douyin
28 | - Kuaishou
29 | - Xiaohongshu (RED)
30 | - Weibo
31 | - Pipixia
32 | - Weishi
33 | - Lvzhou
34 | - Zuiyou
35 | - Quanmin Video
36 | - Xigua Video
37 | - Lishipin
38 | - Pipigaoxiao
39 | - Huya
40 | - AcFun
41 | - Doupai
42 | - Meipai
43 | - Quanmin K Song
44 | - Sixroom
45 | - Xinpianchang
46 | - Haokan Video
47 |
48 | ## Directory Structure
49 |
50 | ```
51 | crawling-short-video-mcp/
52 | ├── yby6_video_mcp_server/
53 | │ ├── server.py # Main service entry
54 | │ ├── functionality/ # Platform-specific parsing modules
55 | │ │ ├── base.py # Base classes and enum definitions
56 | │ │ ├── douyin.py # Douyin implementation
57 | │ │ ├── kuaishou.py # Kuaishou implementation
58 | │ │ ├── ... # Other platform implementations
59 | │ │ └── video_processor.py # Video processing and text extraction
60 | │ └── utils/ # Utility functions
61 | ├── Dockerfile.base # Base image build file
62 | ├── Dockerfile.mcp # MCP service image build file
63 | ├── requirements.txt # Dependencies list
64 | ├── pyproject.toml # Project configuration and metadata
65 | └── README.md # Project documentation
66 | ```
67 |
68 | ## Installation
69 |
70 | ### Prerequisites
71 |
72 | #### FFmpeg Installation (Required for video text content extraction)
73 |
74 | **Ubuntu/Debian:**
75 | ```bash
76 | sudo apt update
77 | sudo apt install ffmpeg
78 | ```
79 |
80 | **CentOS/RHEL:**
81 | ```bash
82 | sudo yum install epel-release
83 | sudo yum install ffmpeg ffmpeg-devel
84 | ```
85 |
86 | **macOS:**
87 | ```bash
88 | brew install ffmpeg
89 | ```
90 |
91 | **Windows:**
92 | 1. Download FFmpeg: https://ffmpeg.org/download.html#build-windows
93 | 2. Extract to a directory, e.g., `C:\ffmpeg`
94 | 3. Add to system PATH: `C:\ffmpeg\bin`
95 | 4. Restart command line or system for environment variables to take effect
96 |
97 | Verify installation:
98 | ```bash
99 | ffmpeg -version
100 | ```
101 |
102 | ### Method 1: Install via pip from PyPI (may not have the latest version)
103 | Current latest version: [](https://pypi.org/project/yby6-video-mcp-server/)
104 |
105 | ```bash
106 | # Install latest version
107 | pip install -i https://pypi.org/simple yby6-video-mcp-server
108 |
109 | # Or install specific version
110 | pip install -i https://pypi.org/simple yby6-video-mcp-server==1.0.1
111 | ```
112 |
113 | After installation, verify with:
114 |
115 | ```bash
116 | yby6_video_mcp_server --version
117 | ```
118 |
119 | ## MCP Configuration
120 |
121 | ```json
122 | "yby6_video_mcp_server": {
123 | "command": "uv",
124 | "args": ["yby6_video_mcp_server"],
125 | "env": {
126 | "API_KEY": "Get your free API key at: https://cloud.siliconflow.cn/i/tbvUltCF"
127 | }
128 | }
129 | ```
130 |
131 | ### Method 2: Install from source (recommended for latest version)
132 |
133 | 1. Clone this project
134 |
135 | ```bash
136 | git clone https://github.com/yangbuyiya/yby6-crawling-short-video-mcp.git
137 | cd yby6-crawling-short-video-mcp
138 | ```
139 |
140 | 2. Install dependencies
141 |
142 | Recommended to use Python 3.10+ with a virtual environment:
143 |
144 | **macOS/Linux:**
145 | ```bash
146 | python -m venv venv
147 | source venv/bin/activate
148 | pip install -r requirements.txt
149 | ```
150 |
151 | **Windows:**
152 | ```bash
153 | python -m venv venv
154 | venv\Scripts\activate
155 | pip install -r requirements.txt
156 | ```
157 |
158 | ### Method 3: Deploy with Docker
159 |
160 | This project provides Docker support for quick deployment
161 |
162 | 1. Run container with SSE mode
163 |
164 | ```bash
165 | docker run -d -p 8637:8637 registry.cn-hangzhou.aliyuncs.com/yby6/yby6_video_mcp_server:1.0.0
166 | ```
167 |
168 | You can also quickly build and deploy with:
169 |
170 | 1. Build base image (with FFmpeg and Python environment)
171 |
172 | ```bash
173 | docker build -t ffmpeg-python-base:1.0.0 -f Dockerfile.base .
174 | ```
175 |
176 | 2. Build MCP service image
177 |
178 | ```bash
179 | docker build -t yby6-video-mcp:latest -f Dockerfile.mcp .
180 | ```
181 |
182 | 3. Run container
183 |
184 | ```bash
185 | docker run -d -p 8637:8637 yby6-video-mcp:latest
186 | ```
187 |
188 | ## Usage
189 |
190 | ### stdio MCP Service Start
191 |
192 | ```json
193 | // Run from PyPI
194 | "yby6_video_mcp_server": {
195 | "command": "uv",
196 | "args": ["yby6_video_mcp_server"],
197 | "env": {
198 | "API_KEY": "sk-xcazqbgbnoagddpyaorhqhioxazvqdtednppksiqaotjsboe"
199 | }
200 | },
201 |
202 | // Run from source
203 | "yby6_video_mcp_server": {
204 | "command": "uv",
205 | "args": [
206 | "--directory",
207 | "to/path/yby6-crawling-short-video-mcp/yby6_video_mcp_server",
208 | "run",
209 | "-m",
210 | "yby6_video_mcp_server.server"
211 | ],
212 | "env": {
213 | "API_KEY": "sk-xcazqbgbnoagddpyaorhqhioxazvqdtednppksiqaotjsboe"
214 | }
215 | },
216 | ```
217 |
218 | ### SSE or HTTP MCP Service Address
219 |
220 | - Supports API key configuration in request headers
221 | - Supports API key configuration in request parameters
222 |
223 | ```json
224 | "yby6_video_mcp_server": {
225 | "url": "http://127.0.0.1:8637/sse?apikey=xxxxxx",
226 | }
227 | ```
228 |
229 | ### Starting the Service
230 |
231 | ```bash
232 | # After pip installation
233 | yby6_video_mcp_server --transport http --host 0.0.0.0 --port 8637
234 |
235 | # Or run from source
236 | python -m yby6_video_mcp_server.server --transport http --host 0.0.0.0 --port 8637
237 | ```
238 |
239 | Parameters:
240 | - `--transport`: Transport method, options: `stdio`, `sse`, `http` (recommended)
241 | - `--host`: Host address, default `0.0.0.0`
242 | - `--port`: Port number, default `8000`
243 | - `--path`: Custom MCP request path (optional)
244 |
245 | ### API Documentation
246 |
247 | #### 1. Parse Video Share Link
248 |
249 | **Endpoint:** `share_url_parse_tool`
250 |
251 | **Parameters:**
252 |
253 | | Parameter | Type | Required | Description |
254 | |-----------|--------|----------|-----------------|
255 | | url | string | Yes | Video share URL |
256 |
257 | **Response Example:**
258 |
259 | ```json
260 | {
261 | "code": 200,
262 | "msg": "Success",
263 | "data": {
264 | "video_url": "https://xxx.com/xxx.mp4",
265 | "cover_url": "https://xxx.com/cover.jpg",
266 | "title": "Video Title",
267 | "music_url": "https://xxx.com/music.mp3",
268 | "images": [],
269 | "author": {
270 | "uid": "User ID",
271 | "name": "Username",
272 | "avatar": "Avatar URL"
273 | }
274 | }
275 | }
276 | ```
277 |
278 | #### 2. Parse by Video ID
279 |
280 | **Endpoint:** `video_id_parse_tool`
281 |
282 | **Parameters:**
283 |
284 | | Parameter | Type | Required | Description |
285 | |-----------|--------|----------|-------------------------------------------|
286 | | source | string | Yes | Video source, e.g., douyin, kuaishou, etc.|
287 | | video_id | string | Yes | Video ID |
288 |
289 | **Response Example:** Same as above
290 |
291 | #### 3. Video Content Text Extraction
292 |
293 | **Endpoint:** `share_text_parse_tool`
294 |
295 | **Parameters:**
296 |
297 | | Parameter | Type | Required | Description |
298 | |--------------|--------|----------|--------------------------------------------------|
299 | | share_link | string | Yes | Douyin share link or text containing the link |
300 | | api_base_url | string | No | API base URL, defaults to SiliconFlow |
301 | | model | string | No | Speech recognition model, defaults to SenseVoiceSmall |
302 |
303 | > For SSE and Streamable HTTP modes, just include the apikey in the request parameters: http://127.0.0.1:8637/sse?apikey=xxxxxx
304 | > The LLM used is SiliconFlow, get your apikey at: https://cloud.siliconflow.cn/i/tbvUltCF
305 |
306 | **Response Example:**
307 |
308 | ```json
309 | {
310 | "code": 200,
311 | "msg": "Success",
312 | "data": {
313 | "video_url": "https://xxx.com/xxx.mp4",
314 | "cover_url": "https://xxx.com/cover.jpg",
315 | "title": "Video Title",
316 | "author": {
317 | "uid": "User ID",
318 | "name": "Username",
319 | "avatar": "Avatar URL"
320 | }
321 | },
322 | "text_content": "Transcribed speech content from the video"
323 | }
324 | ```
325 |
326 | ## Dependencies
327 |
328 | Main dependencies include:
329 |
330 | - fastmcp: MCP service framework
331 | - httpx: Asynchronous HTTP client
332 | - ffmpeg-python: Video processing
333 | - lxml & parsel: HTML parsing
334 | - fake-useragent: Browser request simulation
335 | - pydantic: Data validation
336 |
337 | ## Docker Deployment
338 |
339 | The project provides two Dockerfiles:
340 | - `Dockerfile.base`: Builds base image with Python environment and FFmpeg
341 | - `Dockerfile.mcp`: Builds MCP service image
342 |
343 | Quick deployment scripts:
344 | ```bash
345 | # Windows
346 | .\script\deployBase.bat
347 | .\script\deployMcp.bat
348 |
349 | # Linux/macOS
350 | bash script/deployBase.sh
351 | bash script/deployMcp.sh
352 | ```
353 |
354 | Run container with SSE mode:
355 | ```bash
356 | docker run -d -p 8637:8637 registry.cn-hangzhou.aliyuncs.com/yby6/yby6_video_mcp_server:1.0.0
357 | ```
358 |
359 | ## Contributions and Feedback
360 |
361 | Issues and PRs are welcome to improve the project!
362 |
363 | - Project URL: https://github.com/yangbuyiya/yby6-crawling-short-video-mcp
364 | - Author Email: yangbuyiya@duck.com
365 |
366 | ---
367 |
368 | This project is built upon the shoulders of giants. Thanks to:
369 |
370 | - [parse-video-py](https://github.com/wujunwei928/parse-video-py)
371 | - [fastmcp](https://github.com/jlowin/fastmcp)
372 |
373 | # ⚠️ Disclaimer
374 |
375 | 1. This project is an open-source tool for learning and research purposes only. Users assume all risks, losses, or legal liabilities arising from the use of this project. The author and contributors bear no direct or indirect responsibility.
376 | 2. The functionality and code of this project are implemented based on existing technologies. The author does not guarantee its complete correctness, flawlessness, or continuous availability. The author is not responsible for any consequences resulting from project defects or unavailability.
377 | 3. Third-party libraries, plugins, or services used by this project follow their original agreements. Users should consult and comply with relevant agreements. Users are responsible for any liability arising from violations of third-party agreements.
378 | 4. Users should ensure their usage is legal and compliant, and should not use this project for any illegal, non-compliant activities, or activities that infringe on others' rights. Users bear all consequences arising from illegal or non-compliant use.
379 | 5. It is strictly prohibited to use this project for purposes such as intellectual property infringement, dissemination of illegal information, commercial cracking, or data scraping. The author firmly opposes and does not support any illegal use.
380 | 6. When processing data, users should ensure compliance with relevant laws and regulations (such as data compliance and privacy protection). Users are responsible for any liability arising from non-compliant operations.
381 | 7. The author and contributors are not related to specific user behaviors and bear no joint liability. Secondary development, modification, distribution, and other behaviors based on this project are unrelated to the author, and the relevant responsibilities are borne by the actors themselves.
382 | 8. This project does not grant any patent licenses. Users bear the risks arising from patent disputes or infringements. Without written authorization from the author, it is prohibited to use this project for commercial promotion, re-authorization, or other commercial purposes.
383 | 9. The author reserves the right to terminate services to non-compliant users at any time and require them to delete relevant code and data.
384 | 10. The author reserves the right to update this disclaimer at any time. Continued use is deemed acceptance of the latest terms. If you do not agree with this statement, please stop using this project immediately.
--------------------------------------------------------------------------------