Jofthomas commited on
Commit
97962d4
·
verified ·
1 Parent(s): f27af2a

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +21 -0
  2. app.py +91 -0
  3. fastmcp.json +10 -0
  4. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # System dependencies for audio processing
4
+ RUN apt-get update \
5
+ && apt-get install -y --no-install-recommends ffmpeg ca-certificates \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ # Set the working directory in the container
9
+ WORKDIR /app
10
+
11
+ COPY requirements.txt .
12
+
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY . .
16
+
17
+ EXPOSE 7862
18
+
19
+ ENV PORT=7862
20
+
21
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import glob
5
+ from typing import Optional, Literal
6
+
7
+ from pydantic import BaseModel, Field, HttpUrl
8
+
9
+ from fastmcp import FastMCP
10
+
11
+
12
+ mcp = FastMCP(
13
+ name="youtube-audio",
14
+ host="0.0.0.0",
15
+ port=7862,
16
+ )
17
+
18
+
19
+ class DownloadResult(BaseModel):
20
+ url: HttpUrl = Field(..., description="Original YouTube URL")
21
+ title: Optional[str] = Field(None, description="Video title if available")
22
+ filepath: str = Field(..., description="Absolute path to the downloaded audio file in the container")
23
+ ext: str = Field(..., description="Audio file extension, e.g., mp3")
24
+
25
+
26
+ @mcp.tool(description="Download audio from a YouTube video URL and return the local file path inside the container.")
27
+ def download_youtube_audio(url: HttpUrl, audio_format: Literal["mp3", "m4a", "wav", "aac", "opus"] = "mp3") -> DownloadResult:
28
+ """
29
+ - url: A direct YouTube video URL
30
+ - audio_format: Desired audio format (requires ffmpeg in the container)
31
+
32
+ The file will be saved under /app/downloads. Ensure the container has write access.
33
+ """
34
+ # Ensure output directory exists
35
+ output_dir = "/app/downloads"
36
+ os.makedirs(output_dir, exist_ok=True)
37
+
38
+ try:
39
+ import yt_dlp as ytdlp
40
+ except Exception:
41
+ raise RuntimeError("yt-dlp is required. Ensure it is listed in requirements.txt and installed.")
42
+
43
+ ydl_opts = {
44
+ "format": "bestaudio/best",
45
+ "outtmpl": os.path.join(output_dir, "%(title).200s [%(id)s].%(ext)s"),
46
+ "postprocessors": [
47
+ {
48
+ "key": "FFmpegExtractAudio",
49
+ "preferredcodec": audio_format,
50
+ "preferredquality": "0",
51
+ }
52
+ ],
53
+ "noplaylist": True,
54
+ "quiet": True,
55
+ "nocheckcertificate": True,
56
+ }
57
+
58
+ info_title: Optional[str] = None
59
+ downloaded_id: Optional[str] = None
60
+
61
+ with ytdlp.YoutubeDL(ydl_opts) as ydl:
62
+ info = ydl.extract_info(str(url), download=True)
63
+ info_title = info.get("title") if isinstance(info, dict) else None
64
+ downloaded_id = info.get("id") if isinstance(info, dict) else None
65
+
66
+ # Resolve the final filename after post-processing
67
+ final_path: Optional[str] = None
68
+ if downloaded_id:
69
+ pattern = os.path.join(output_dir, f"*[{downloaded_id}].{audio_format}")
70
+ matches = glob.glob(pattern)
71
+ if matches:
72
+ final_path = os.path.abspath(matches[0])
73
+
74
+ if not final_path:
75
+ # Fallback: best-effort to find any file with the selected extension modified recently
76
+ candidates = sorted(
77
+ glob.glob(os.path.join(output_dir, f"*.{audio_format}")),
78
+ key=lambda p: os.path.getmtime(p),
79
+ reverse=True,
80
+ )
81
+ if candidates:
82
+ final_path = os.path.abspath(candidates[0])
83
+
84
+ if not final_path:
85
+ raise RuntimeError("Audio file not found after download. Check logs and ffmpeg availability.")
86
+
87
+ return DownloadResult(url=url, title=info_title, filepath=final_path, ext=audio_format)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ mcp.run(transport="http")
fastmcp.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "youtube-audio",
3
+ "description": "MCP server that downloads audio from a YouTube video URL and returns the file path",
4
+ "entrypoint": "app.py",
5
+ "transport": "streamable-http",
6
+ "http": {
7
+ "host": "0.0.0.0",
8
+ "port": 7862
9
+ }
10
+ }
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastmcp>=2.12.2
2
+ pydantic>=2.7.0
3
+ yt-dlp>=2024.4.9