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 +60 -0
- ytdlm-1.0.0/README.md +43 -0
- ytdlm-1.0.0/discord_py_bot.py +59 -0
- ytdlm-1.0.0/pyproject.toml +29 -0
- ytdlm-1.0.0/setup.cfg +4 -0
- ytdlm-1.0.0/ytdlm.egg-info/PKG-INFO +60 -0
- ytdlm-1.0.0/ytdlm.egg-info/SOURCES.txt +8 -0
- ytdlm-1.0.0/ytdlm.egg-info/dependency_links.txt +1 -0
- ytdlm-1.0.0/ytdlm.egg-info/top_level.txt +2 -0
- ytdlm-1.0.0/ytdlm.py +179 -0
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,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 @@
|
|
|
1
|
+
|
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
|