ytdlm 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ytdlm-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: ytdlm
3
+ Version: 1.0.0
4
+ Summary: Python SDK for ytdlm — Real-Time SABR UMP WebM/Opus Audio Streamer for Discord Bots
5
+ Author-email: ytdlm <341343882+ytdlm@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ytdlm/ytdlm
8
+ Project-URL: Repository, https://github.com/ytdlm/ytdlm.git
9
+ Project-URL: Issues, https://github.com/ytdlm/ytdlm/issues
10
+ Keywords: audio,streaming,opus,webm,ump,sabr,discord,music-bot
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Sound/Audio
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ytdlm (Python SDK)
19
+
20
+ Python SDK and `discord.py` voice helper for **[ytdlm](https://github.com/ytdlm/ytdlm)** — a Real-Time SABR UMP WebM/Opus (`251` / `774`) Audio Streaming & Seekable Downloading Engine.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install ytdlm
26
+ ```
27
+
28
+ *(Requires the `ytdlm` Node.js CLI or HTTP Gateway: `npm install -g ytdlm`)*
29
+
30
+ ## Quick Start
31
+
32
+ ```python
33
+ from ytdlm import YtdlmClient
34
+
35
+ client = YtdlmClient(gateway_url="http://127.0.0.1:8787")
36
+
37
+ # 1. Resolve track metadata
38
+ info = client.info("aqz-KE-bpKQ")
39
+ print(info.title, info.duration_sec)
40
+
41
+ # 2. Build direct HTTP stream URL for FFmpegOpusAudio (with instant seek & loop)
42
+ stream_url = client.get_stream_url("aqz-KE-bpKQ", start_sec=30, loop=True)
43
+ ```
44
+
45
+ ### `discord.py` Bot Integration
46
+
47
+ ```python
48
+ import discord
49
+ from ytdlm import YtdlmClient
50
+
51
+ ytdlm = YtdlmClient("http://127.0.0.1:8787")
52
+ source = await discord.FFmpegOpusAudio.from_probe(
53
+ ytdlm.get_stream_url("aqz-KE-bpKQ", start_sec=0)
54
+ )
55
+ voice_client.play(source)
56
+ ```
57
+
58
+ ## License
59
+
60
+ [MIT License](https://github.com/ytdlm/ytdlm/blob/main/LICENSE) — See also [Legal Disclaimer](https://github.com/ytdlm/ytdlm/blob/main/DISCLAIMER.md).
ytdlm-1.0.0/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # ytdlm (Python SDK)
2
+
3
+ Python SDK and `discord.py` voice helper for **[ytdlm](https://github.com/ytdlm/ytdlm)** — a Real-Time SABR UMP WebM/Opus (`251` / `774`) Audio Streaming & Seekable Downloading Engine.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ytdlm
9
+ ```
10
+
11
+ *(Requires the `ytdlm` Node.js CLI or HTTP Gateway: `npm install -g ytdlm`)*
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from ytdlm import YtdlmClient
17
+
18
+ client = YtdlmClient(gateway_url="http://127.0.0.1:8787")
19
+
20
+ # 1. Resolve track metadata
21
+ info = client.info("aqz-KE-bpKQ")
22
+ print(info.title, info.duration_sec)
23
+
24
+ # 2. Build direct HTTP stream URL for FFmpegOpusAudio (with instant seek & loop)
25
+ stream_url = client.get_stream_url("aqz-KE-bpKQ", start_sec=30, loop=True)
26
+ ```
27
+
28
+ ### `discord.py` Bot Integration
29
+
30
+ ```python
31
+ import discord
32
+ from ytdlm import YtdlmClient
33
+
34
+ ytdlm = YtdlmClient("http://127.0.0.1:8787")
35
+ source = await discord.FFmpegOpusAudio.from_probe(
36
+ ytdlm.get_stream_url("aqz-KE-bpKQ", start_sec=0)
37
+ )
38
+ voice_client.play(source)
39
+ ```
40
+
41
+ ## License
42
+
43
+ [MIT License](https://github.com/ytdlm/ytdlm/blob/main/LICENSE) — See also [Legal Disclaimer](https://github.com/ytdlm/ytdlm/blob/main/DISCLAIMER.md).
@@ -0,0 +1,59 @@
1
+ """
2
+ Example: Discord.py Music Bot using `ytdlm` Real-Time SABR UMP Pipe
3
+ ===================================================================
4
+ Demonstrates both:
5
+ 1. Subprocess Pipe Mode (`pipe=True` with `-c:a copy` so FFmpeg only remuxes Opus packets without CPU re-encoding).
6
+ 2. HTTP Gateway Mode (`http://127.0.0.1:8787/stream?v=...`).
7
+ """
8
+
9
+ import discord
10
+ from discord.ext import commands
11
+ from ytdlm import YtdlmClient
12
+
13
+ intents = discord.Intents.default()
14
+ intents.message_content = True
15
+ bot = commands.Bot(command_prefix="!", intents=intents)
16
+
17
+ # Initialize ytdlm (Guest TVHTML5 ITAG 251 by default)
18
+ ytdlm = YtdlmClient(default_itag=251, force_guest=True)
19
+
20
+
21
+ @bot.command(name="play")
22
+ async def play(ctx: commands.Context, *, query: str):
23
+ if not ctx.author.voice or not ctx.author.voice.channel:
24
+ return await ctx.send("Please join a voice channel first!")
25
+
26
+ voice_channel = ctx.author.voice.channel
27
+ vc: discord.VoiceClient = ctx.voice_client or await voice_channel.connect()
28
+
29
+ if vc.is_playing():
30
+ vc.stop()
31
+
32
+ track = ytdlm.get_info(query)
33
+ proc = ytdlm.open_stream_pipe(track.video_id, itag=track.itag)
34
+
35
+ # Because ytdlm outputs native 48kHz stereo WebM/Opus, `-c:a copy` copies Opus frames directly!
36
+ audio_source = discord.FFmpegOpusAudio(
37
+ proc.stdout,
38
+ pipe=True,
39
+ codec="copy",
40
+ )
41
+
42
+ def after_play(err):
43
+ try:
44
+ proc.terminate()
45
+ except Exception:
46
+ pass
47
+ if err:
48
+ print(f"[ytdlm] Playback error: {err}")
49
+
50
+ vc.play(audio_source, after=after_play)
51
+ await ctx.send(
52
+ f"▶️ Now streaming (**{track.mode}**, `{track.abr_kbps}kbps` Opus): **{track.title}** — *{track.author}*"
53
+ )
54
+
55
+
56
+ if __name__ == "__main__":
57
+ # Replace with your Discord Bot Token:
58
+ # bot.run("YOUR_DISCORD_BOT_TOKEN")
59
+ pass
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ytdlm"
7
+ version = "1.0.0"
8
+ description = "Python SDK for ytdlm — Real-Time SABR UMP WebM/Opus Audio Streamer for Discord Bots"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "ytdlm", email = "341343882+ytdlm@users.noreply.github.com" }
14
+ ]
15
+ keywords = ["audio", "streaming", "opus", "webm", "ump", "sabr", "discord", "music-bot"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Multimedia :: Sound/Audio",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/ytdlm/ytdlm"
25
+ Repository = "https://github.com/ytdlm/ytdlm.git"
26
+ Issues = "https://github.com/ytdlm/ytdlm/issues"
27
+
28
+ [tool.setuptools]
29
+ py-modules = ["ytdlm", "discord_py_bot"]
ytdlm-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: ytdlm
3
+ Version: 1.0.0
4
+ Summary: Python SDK for ytdlm — Real-Time SABR UMP WebM/Opus Audio Streamer for Discord Bots
5
+ Author-email: ytdlm <341343882+ytdlm@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ytdlm/ytdlm
8
+ Project-URL: Repository, https://github.com/ytdlm/ytdlm.git
9
+ Project-URL: Issues, https://github.com/ytdlm/ytdlm/issues
10
+ Keywords: audio,streaming,opus,webm,ump,sabr,discord,music-bot
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Sound/Audio
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ytdlm (Python SDK)
19
+
20
+ Python SDK and `discord.py` voice helper for **[ytdlm](https://github.com/ytdlm/ytdlm)** — a Real-Time SABR UMP WebM/Opus (`251` / `774`) Audio Streaming & Seekable Downloading Engine.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install ytdlm
26
+ ```
27
+
28
+ *(Requires the `ytdlm` Node.js CLI or HTTP Gateway: `npm install -g ytdlm`)*
29
+
30
+ ## Quick Start
31
+
32
+ ```python
33
+ from ytdlm import YtdlmClient
34
+
35
+ client = YtdlmClient(gateway_url="http://127.0.0.1:8787")
36
+
37
+ # 1. Resolve track metadata
38
+ info = client.info("aqz-KE-bpKQ")
39
+ print(info.title, info.duration_sec)
40
+
41
+ # 2. Build direct HTTP stream URL for FFmpegOpusAudio (with instant seek & loop)
42
+ stream_url = client.get_stream_url("aqz-KE-bpKQ", start_sec=30, loop=True)
43
+ ```
44
+
45
+ ### `discord.py` Bot Integration
46
+
47
+ ```python
48
+ import discord
49
+ from ytdlm import YtdlmClient
50
+
51
+ ytdlm = YtdlmClient("http://127.0.0.1:8787")
52
+ source = await discord.FFmpegOpusAudio.from_probe(
53
+ ytdlm.get_stream_url("aqz-KE-bpKQ", start_sec=0)
54
+ )
55
+ voice_client.play(source)
56
+ ```
57
+
58
+ ## License
59
+
60
+ [MIT License](https://github.com/ytdlm/ytdlm/blob/main/LICENSE) — See also [Legal Disclaimer](https://github.com/ytdlm/ytdlm/blob/main/DISCLAIMER.md).
@@ -0,0 +1,8 @@
1
+ README.md
2
+ discord_py_bot.py
3
+ pyproject.toml
4
+ ytdlm.py
5
+ ytdlm.egg-info/PKG-INFO
6
+ ytdlm.egg-info/SOURCES.txt
7
+ ytdlm.egg-info/dependency_links.txt
8
+ ytdlm.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ discord_py_bot
2
+ ytdlm
ytdlm-1.0.0/ytdlm.py ADDED
@@ -0,0 +1,179 @@
1
+ """
2
+ ytdlm Python SDK — Real-Time YouTube SABR UMP Audio Streamer & Client
3
+ =====================================================================
4
+ Supports two zero-latency modes for Python apps and `discord.py` / `py-cord` / `nextcord` bots:
5
+ 1. Subprocess Pipe Mode (`open_stream_pipe`): Spawns `ytdlm stream <url> -o -` and yields a live
6
+ stdout binary pipe of WebM/Opus audio chunks (starts in ~300ms).
7
+ 2. HTTP Gateway Mode (`get_stream_url`): Connects to `ytdlm serve` HTTP micro-server (`http://127.0.0.1:8787`)
8
+ so FFmpeg or `discord.FFmpegOpusAudio` can stream directly via HTTP URL.
9
+ """
10
+
11
+ from __future__ import annotations
12
+ import json
13
+ import shutil
14
+ import subprocess
15
+ import urllib.parse
16
+ import urllib.request
17
+ from dataclasses import dataclass
18
+ from typing import Any, Dict, List, Optional
19
+
20
+
21
+ @dataclass
22
+ class TrackInfo:
23
+ video_id: str
24
+ title: str
25
+ author: str
26
+ duration_sec: int
27
+ duration_ms: int
28
+ url: str
29
+ thumbnail: Optional[str]
30
+ itag: int
31
+ abr_kbps: int
32
+ sample_rate: int
33
+ channels: int
34
+ is_guest: bool
35
+ mode: str
36
+ stream_url: Optional[str] = None
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: Dict[str, Any]) -> "TrackInfo":
40
+ return cls(
41
+ video_id=data.get("videoId") or data.get("id", ""),
42
+ title=data.get("title", "YouTube Audio"),
43
+ author=data.get("author") or data.get("uploader", ""),
44
+ duration_sec=int(data.get("durationSec") or data.get("duration") or 0),
45
+ duration_ms=int(data.get("durationMs") or 0),
46
+ url=data.get("url") or data.get("webpage_url", ""),
47
+ thumbnail=data.get("thumbnail"),
48
+ itag=int(data.get("itag") or 251),
49
+ abr_kbps=int(data.get("abr") or round(int(data.get("bitrate", 135000)) / 1000)),
50
+ sample_rate=int(data.get("sampleRate") or data.get("asr") or 48000),
51
+ channels=int(data.get("channels") or data.get("audio_channels") or 2),
52
+ is_guest=bool(data.get("isGuest", True)),
53
+ mode=data.get("mode", "TV_GUEST_SABR_251"),
54
+ stream_url=data.get("streamUrl"),
55
+ )
56
+
57
+
58
+ class YtdlmClient:
59
+ def __init__(
60
+ self,
61
+ binary_path: Optional[str] = None,
62
+ server_url: Optional[str] = None,
63
+ default_itag: int = 251,
64
+ force_guest: bool = True,
65
+ ):
66
+ """
67
+ :param binary_path: Path to `ytdlm` CLI or `node /path/to/ytdlm/bin/ytdlm.js`.
68
+ :param server_url: Optional URL of running `ytdlm serve` instance (e.g. "http://127.0.0.1:8787").
69
+ :param default_itag: 251 (Guest ~160kbps Opus) or 774 (TV Premium ~256kbps Opus).
70
+ :param force_guest: Force anonymous Guest TVHTML5 mode (default True for 251).
71
+ """
72
+ self.binary_path = binary_path or shutil.which("ytdlm") or "ytdlm"
73
+ self.server_url = server_url.rstrip("/") if server_url else None
74
+ self.default_itag = default_itag
75
+ self.force_guest = force_guest
76
+
77
+ def _build_cmd(self, *args: str) -> List[str]:
78
+ if self.binary_path.endswith(".js"):
79
+ return ["node", self.binary_path, *args]
80
+ return [self.binary_path, *args]
81
+
82
+ def get_info(self, url_or_query: str, itag: Optional[int] = None) -> TrackInfo:
83
+ """Fetches track metadata and SABR format details."""
84
+ target_itag = itag or self.default_itag
85
+ if self.server_url:
86
+ params = urllib.parse.urlencode(
87
+ {
88
+ "v": url_or_query,
89
+ "itag": str(target_itag),
90
+ "guest": "1" if self.force_guest else "0",
91
+ }
92
+ )
93
+ with urllib.request.urlopen(f"{self.server_url}/info?{params}", timeout=15) as resp:
94
+ data = json.loads(resp.read().decode("utf-8"))
95
+ return TrackInfo.from_dict(data)
96
+
97
+ cmd = self._build_cmd("info", url_or_query, "-f", str(target_itag), "-j", "-q")
98
+ if self.force_guest:
99
+ cmd.append("--guest")
100
+ out = subprocess.check_output(cmd, text=True, encoding="utf-8")
101
+ return TrackInfo.from_dict(json.loads(out))
102
+
103
+ def search(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
104
+ """Searches YouTube in anonymous Guest mode."""
105
+ if self.server_url:
106
+ params = urllib.parse.urlencode({"q": query, "limit": str(limit)})
107
+ with urllib.request.urlopen(f"{self.server_url}/search?{params}", timeout=15) as resp:
108
+ return json.loads(resp.read().decode("utf-8")).get("results", [])
109
+
110
+ cmd = self._build_cmd("search", query, "-n", str(limit), "-j", "-q")
111
+ out = subprocess.check_output(cmd, text=True, encoding="utf-8")
112
+ return json.loads(out)
113
+
114
+ def open_stream_pipe(
115
+ self,
116
+ url_or_query: str,
117
+ itag: Optional[int] = None,
118
+ start_ms: int = 0,
119
+ loop: bool = False,
120
+ ) -> subprocess.Popen:
121
+ """
122
+ Spawns `ytdlm stream` and returns a `Popen` process whose `.stdout` is a live
123
+ real-time WebM/Opus byte stream suitable for `discord.FFmpegOpusAudio(..., pipe=True)`.
124
+ """
125
+ target_itag = itag or self.default_itag
126
+ cmd = self._build_cmd(
127
+ "stream",
128
+ url_or_query,
129
+ "-f",
130
+ str(target_itag),
131
+ "--start",
132
+ str(start_ms),
133
+ "-o",
134
+ "-",
135
+ "-q",
136
+ )
137
+ if self.force_guest:
138
+ cmd.append("--guest")
139
+ if loop:
140
+ cmd.append("--loop")
141
+
142
+ return subprocess.Popen(
143
+ cmd,
144
+ stdout=subprocess.PIPE,
145
+ stderr=subprocess.DEVNULL,
146
+ bufsize=64 * 1024,
147
+ )
148
+
149
+ def get_stream_url(
150
+ self,
151
+ url_or_query: str,
152
+ itag: Optional[int] = None,
153
+ start_ms: int = 0,
154
+ loop: bool = False,
155
+ ) -> str:
156
+ """
157
+ Returns an HTTP streaming URL when using `ytdlm serve` mode (default http://127.0.0.1:8787).
158
+ """
159
+ base = self.server_url or "http://127.0.0.1:8787"
160
+ target_itag = itag or self.default_itag
161
+ params = urllib.parse.urlencode(
162
+ {
163
+ "v": url_or_query,
164
+ "itag": str(target_itag),
165
+ "guest": "1" if self.force_guest else "0",
166
+ "start": str(start_ms),
167
+ "loop": "1" if loop else "0",
168
+ }
169
+ )
170
+ return f"{base}/stream?{params}"
171
+
172
+ def download(self, url_or_query: str, output_path: str, itag: Optional[int] = None) -> str:
173
+ """Downloads a 100% seekable `.opus` file with patched Duration, SeekHead & Cues."""
174
+ target_itag = itag or self.default_itag
175
+ cmd = self._build_cmd("download", url_or_query, "-f", str(target_itag), "-o", output_path, "-q")
176
+ if self.force_guest:
177
+ cmd.append("--guest")
178
+ subprocess.check_call(cmd)
179
+ return output_path