├── 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 | [![PyPI version](https://img.shields.io/pypi/v/yby6-video-mcp-server.svg)](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 | image 22 | image 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 | 当前最新版本: [![PyPI version](https://img.shields.io/pypi/v/yby6-video-mcp-server.svg)](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 | [![PyPI version](https://img.shields.io/pypi/v/yby6-video-mcp-server.svg)](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 | image 21 | image 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: [![PyPI version](https://img.shields.io/pypi/v/yby6-video-mcp-server.svg)](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. --------------------------------------------------------------------------------