File size: 3,378 Bytes
97962d4
 
 
 
 
 
 
 
 
 
 
 
 
 
214a06a
97962d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a061382
 
 
 
97962d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a061382
 
 
 
97962d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from __future__ import annotations

import os
import glob
from typing import Optional, Literal

from pydantic import BaseModel, Field, HttpUrl

from fastmcp import FastMCP


mcp = FastMCP(
    name="youtube-audio",
    host="0.0.0.0",
    port=7860,
)


class DownloadResult(BaseModel):
    url: HttpUrl = Field(..., description="Original YouTube URL")
    title: Optional[str] = Field(None, description="Video title if available")
    filepath: str = Field(..., description="Absolute path to the downloaded audio file in the container")
    ext: str = Field(..., description="Audio file extension, e.g., mp3")


@mcp.tool(description="Download audio from a YouTube video URL and return the local file path inside the container.")
def download_youtube_audio(url: HttpUrl, audio_format: Literal["mp3", "m4a", "wav", "aac", "opus"] = "mp3") -> DownloadResult:
    """
    - url: A direct YouTube video URL
    - audio_format: Desired audio format (requires ffmpeg in the container)

    The file will be saved under /app/downloads. Ensure the container has write access.

    Networking:
    - If outbound DNS is restricted, set env DNS_SERVERS (space-separated, e.g. "8.8.8.8 1.1.1.1").
    - To route via a proxy, set env YT_PROXY (e.g. http://user:pass@proxy:port).
    """
    # Ensure output directory exists
    output_dir = "/app/downloads"
    os.makedirs(output_dir, exist_ok=True)

    try:
        import yt_dlp as ytdlp
    except Exception:
        raise RuntimeError("yt-dlp is required. Ensure it is listed in requirements.txt and installed.")

    ydl_opts = {
        "format": "bestaudio/best",
        "outtmpl": os.path.join(output_dir, "%(title).200s [%(id)s].%(ext)s"),
        "postprocessors": [
            {
                "key": "FFmpegExtractAudio",
                "preferredcodec": audio_format,
                "preferredquality": "0",
            }
        ],
        "noplaylist": True,
        "quiet": True,
        "nocheckcertificate": True,
    }

    proxy_url = os.environ.get("YT_PROXY")
    if proxy_url:
        ydl_opts["proxy"] = proxy_url

    info_title: Optional[str] = None
    downloaded_id: Optional[str] = None

    with ytdlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(str(url), download=True)
        info_title = info.get("title") if isinstance(info, dict) else None
        downloaded_id = info.get("id") if isinstance(info, dict) else None

    # Resolve the final filename after post-processing
    final_path: Optional[str] = None
    if downloaded_id:
        pattern = os.path.join(output_dir, f"*[{downloaded_id}].{audio_format}")
        matches = glob.glob(pattern)
        if matches:
            final_path = os.path.abspath(matches[0])

    if not final_path:
        # Fallback: best-effort to find any file with the selected extension modified recently
        candidates = sorted(
            glob.glob(os.path.join(output_dir, f"*.{audio_format}")),
            key=lambda p: os.path.getmtime(p),
            reverse=True,
        )
        if candidates:
            final_path = os.path.abspath(candidates[0])

    if not final_path:
        raise RuntimeError("Audio file not found after download. Check logs and ffmpeg availability.")

    return DownloadResult(url=url, title=info_title, filepath=final_path, ext=audio_format)


if __name__ == "__main__":
    mcp.run(transport="http")