media-downloader 1.0.8__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.
Potentially problematic release.
This version of media-downloader might be problematic. Click here for more details.
- media_downloader-1.0.8/LICENSE +20 -0
- media_downloader-1.0.8/MANIFEST.in +1 -0
- media_downloader-1.0.8/PKG-INFO +204 -0
- media_downloader-1.0.8/README.md +186 -0
- media_downloader-1.0.8/media_downloader/__init__.py +17 -0
- media_downloader-1.0.8/media_downloader/__main__.py +6 -0
- media_downloader-1.0.8/media_downloader/media_downloader.py +245 -0
- media_downloader-1.0.8/media_downloader/media_downloader_mcp.py +147 -0
- media_downloader-1.0.8/media_downloader.egg-info/PKG-INFO +204 -0
- media_downloader-1.0.8/media_downloader.egg-info/SOURCES.txt +16 -0
- media_downloader-1.0.8/media_downloader.egg-info/dependency_links.txt +1 -0
- media_downloader-1.0.8/media_downloader.egg-info/entry_points.txt +3 -0
- media_downloader-1.0.8/media_downloader.egg-info/requires.txt +2 -0
- media_downloader-1.0.8/media_downloader.egg-info/top_level.txt +3 -0
- media_downloader-1.0.8/pyproject.toml +34 -0
- media_downloader-1.0.8/requirements.txt +2 -0
- media_downloader-1.0.8/setup.cfg +4 -0
- media_downloader-1.0.8/tests/test_mcp.py +48 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2012-2023 Audel Rouhi
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md include requirements.txt recursive-include media_downloader *.py
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: media-downloader
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: Download audio/videos from the internet!
|
|
5
|
+
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: License :: Public Domain
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: yt-dlp>=2025.8.20
|
|
16
|
+
Requires-Dist: fastmcp>=2.11.3
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Media Downloader
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
*Version: 1.0.8*
|
|
42
|
+
|
|
43
|
+
Download videos and audio from the internet!
|
|
44
|
+
|
|
45
|
+
MCP Server Support!
|
|
46
|
+
|
|
47
|
+
This repository is actively maintained - Contributions are welcome!
|
|
48
|
+
|
|
49
|
+
### Supports:
|
|
50
|
+
- YouTube
|
|
51
|
+
- Twitter
|
|
52
|
+
- Rumble
|
|
53
|
+
- BitChute
|
|
54
|
+
- Vimeo
|
|
55
|
+
- And More!
|
|
56
|
+
|
|
57
|
+
#### Using an an MCP Server:
|
|
58
|
+
|
|
59
|
+
AI Prompt:
|
|
60
|
+
```text
|
|
61
|
+
Download me this video: https://youtube.com/watch?askdjfa
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
AI Response:
|
|
65
|
+
```text
|
|
66
|
+
Sure thing, the video has been downloaded to:
|
|
67
|
+
|
|
68
|
+
"C:\Users\User\Downloads\YouTube Video - Episode 1.mp4"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary><b>Usage:</b></summary>
|
|
73
|
+
|
|
74
|
+
| Short Flag | Long Flag | Description |
|
|
75
|
+
|------------|-------------|---------------------------------------------|
|
|
76
|
+
| -h | --help | See usage |
|
|
77
|
+
| -a | --audio | Download audio only |
|
|
78
|
+
| -c | --channel | YouTube Channel/User - Downloads all videos |
|
|
79
|
+
| -f | --file | File with video links |
|
|
80
|
+
| -l | --links | Comma separated links |
|
|
81
|
+
| -d | --directory | Location to save videos |
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary><b>Example:</b></summary>
|
|
87
|
+
|
|
88
|
+
### Use in CLI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
media-downloader --file "C:\Users\videos.txt" --directory "C:\Users\Downloads" --channel "WhiteHouse" --links "URL1,URL2,URL3"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Use in Python
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Import library
|
|
98
|
+
from media_downloader import MediaDownloader
|
|
99
|
+
|
|
100
|
+
# Set URL of video/audio here
|
|
101
|
+
url = "https://YootToob.com/video"
|
|
102
|
+
|
|
103
|
+
# Instantiate vide_downloader_instance
|
|
104
|
+
video_downloader_instance = MediaDownloader()
|
|
105
|
+
|
|
106
|
+
# Set the location to save the video
|
|
107
|
+
video_downloader_instance.set_save_path("C:/Users/you/Downloads")
|
|
108
|
+
|
|
109
|
+
# Add URL to download
|
|
110
|
+
video_downloader_instance.append_link(url)
|
|
111
|
+
|
|
112
|
+
# Download all videos appended
|
|
113
|
+
video_downloader_instance.download_all()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Optional - Set Audio to True, Default is False if unspecified.
|
|
118
|
+
video_downloader_instance.set_audio(audio=True)
|
|
119
|
+
|
|
120
|
+
# Optional - Open a file of video/audio URL(s)
|
|
121
|
+
video_downloader_instance.open_file("FILE")
|
|
122
|
+
|
|
123
|
+
# Optional - Enter a YouTube channel name and download their latest videos
|
|
124
|
+
video_downloader_instance.get_channel_videos("YT-Channel Name")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Use with AI
|
|
128
|
+
|
|
129
|
+
Deploy MCP Server as a Service
|
|
130
|
+
```bash
|
|
131
|
+
docker pull knucklessg1/media-downloader:latest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Modify the `compose.yml`
|
|
135
|
+
|
|
136
|
+
```compose
|
|
137
|
+
services:
|
|
138
|
+
media-downloader-mcp:
|
|
139
|
+
image: knucklessg1/media-downloader:latest
|
|
140
|
+
volumes:
|
|
141
|
+
- downloads:/root/Downloads
|
|
142
|
+
environment:
|
|
143
|
+
- HOST=0.0.0.0
|
|
144
|
+
- PORT=8000
|
|
145
|
+
ports:
|
|
146
|
+
- 8000:8000
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Configure `mcp.json`
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcpServers": {
|
|
154
|
+
"media_downloader": {
|
|
155
|
+
"command": "media-downloader-mcp",
|
|
156
|
+
"env": {
|
|
157
|
+
"DOWNLOAD_DIRECTORY": "~/Downloads", // Optional - Can be specified at prompt
|
|
158
|
+
"AUDIO_ONLY": false // Optional - Can be specified at prompt
|
|
159
|
+
},
|
|
160
|
+
"timeout": 300000
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
</details>
|
|
168
|
+
|
|
169
|
+
<details>
|
|
170
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
171
|
+
|
|
172
|
+
Install Python Package
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python -m pip install media-downloader
|
|
176
|
+
```
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
## Geniusbot Application
|
|
180
|
+
|
|
181
|
+
Use with a GUI through Geniusbot
|
|
182
|
+
|
|
183
|
+
Visit our [GitHub](https://github.com/Knuckles-Team/geniusbot) for more information
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary><b>Installation Instructions with Geniusbot:</b></summary>
|
|
187
|
+
|
|
188
|
+
Install Python Package
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
python -m pip install geniusbot
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
</details>
|
|
195
|
+
|
|
196
|
+
<details>
|
|
197
|
+
<summary><b>Repository Owners:</b></summary>
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
201
|
+
|
|
202
|
+

|
|
203
|
+

|
|
204
|
+
</details>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Media Downloader
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
*Version: 1.0.8*
|
|
24
|
+
|
|
25
|
+
Download videos and audio from the internet!
|
|
26
|
+
|
|
27
|
+
MCP Server Support!
|
|
28
|
+
|
|
29
|
+
This repository is actively maintained - Contributions are welcome!
|
|
30
|
+
|
|
31
|
+
### Supports:
|
|
32
|
+
- YouTube
|
|
33
|
+
- Twitter
|
|
34
|
+
- Rumble
|
|
35
|
+
- BitChute
|
|
36
|
+
- Vimeo
|
|
37
|
+
- And More!
|
|
38
|
+
|
|
39
|
+
#### Using an an MCP Server:
|
|
40
|
+
|
|
41
|
+
AI Prompt:
|
|
42
|
+
```text
|
|
43
|
+
Download me this video: https://youtube.com/watch?askdjfa
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
AI Response:
|
|
47
|
+
```text
|
|
48
|
+
Sure thing, the video has been downloaded to:
|
|
49
|
+
|
|
50
|
+
"C:\Users\User\Downloads\YouTube Video - Episode 1.mp4"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary><b>Usage:</b></summary>
|
|
55
|
+
|
|
56
|
+
| Short Flag | Long Flag | Description |
|
|
57
|
+
|------------|-------------|---------------------------------------------|
|
|
58
|
+
| -h | --help | See usage |
|
|
59
|
+
| -a | --audio | Download audio only |
|
|
60
|
+
| -c | --channel | YouTube Channel/User - Downloads all videos |
|
|
61
|
+
| -f | --file | File with video links |
|
|
62
|
+
| -l | --links | Comma separated links |
|
|
63
|
+
| -d | --directory | Location to save videos |
|
|
64
|
+
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary><b>Example:</b></summary>
|
|
69
|
+
|
|
70
|
+
### Use in CLI
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
media-downloader --file "C:\Users\videos.txt" --directory "C:\Users\Downloads" --channel "WhiteHouse" --links "URL1,URL2,URL3"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Use in Python
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Import library
|
|
80
|
+
from media_downloader import MediaDownloader
|
|
81
|
+
|
|
82
|
+
# Set URL of video/audio here
|
|
83
|
+
url = "https://YootToob.com/video"
|
|
84
|
+
|
|
85
|
+
# Instantiate vide_downloader_instance
|
|
86
|
+
video_downloader_instance = MediaDownloader()
|
|
87
|
+
|
|
88
|
+
# Set the location to save the video
|
|
89
|
+
video_downloader_instance.set_save_path("C:/Users/you/Downloads")
|
|
90
|
+
|
|
91
|
+
# Add URL to download
|
|
92
|
+
video_downloader_instance.append_link(url)
|
|
93
|
+
|
|
94
|
+
# Download all videos appended
|
|
95
|
+
video_downloader_instance.download_all()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# Optional - Set Audio to True, Default is False if unspecified.
|
|
100
|
+
video_downloader_instance.set_audio(audio=True)
|
|
101
|
+
|
|
102
|
+
# Optional - Open a file of video/audio URL(s)
|
|
103
|
+
video_downloader_instance.open_file("FILE")
|
|
104
|
+
|
|
105
|
+
# Optional - Enter a YouTube channel name and download their latest videos
|
|
106
|
+
video_downloader_instance.get_channel_videos("YT-Channel Name")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Use with AI
|
|
110
|
+
|
|
111
|
+
Deploy MCP Server as a Service
|
|
112
|
+
```bash
|
|
113
|
+
docker pull knucklessg1/media-downloader:latest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Modify the `compose.yml`
|
|
117
|
+
|
|
118
|
+
```compose
|
|
119
|
+
services:
|
|
120
|
+
media-downloader-mcp:
|
|
121
|
+
image: knucklessg1/media-downloader:latest
|
|
122
|
+
volumes:
|
|
123
|
+
- downloads:/root/Downloads
|
|
124
|
+
environment:
|
|
125
|
+
- HOST=0.0.0.0
|
|
126
|
+
- PORT=8000
|
|
127
|
+
ports:
|
|
128
|
+
- 8000:8000
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Configure `mcp.json`
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"media_downloader": {
|
|
137
|
+
"command": "media-downloader-mcp",
|
|
138
|
+
"env": {
|
|
139
|
+
"DOWNLOAD_DIRECTORY": "~/Downloads", // Optional - Can be specified at prompt
|
|
140
|
+
"AUDIO_ONLY": false // Optional - Can be specified at prompt
|
|
141
|
+
},
|
|
142
|
+
"timeout": 300000
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
<details>
|
|
152
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
153
|
+
|
|
154
|
+
Install Python Package
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
python -m pip install media-downloader
|
|
158
|
+
```
|
|
159
|
+
</details>
|
|
160
|
+
|
|
161
|
+
## Geniusbot Application
|
|
162
|
+
|
|
163
|
+
Use with a GUI through Geniusbot
|
|
164
|
+
|
|
165
|
+
Visit our [GitHub](https://github.com/Knuckles-Team/geniusbot) for more information
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary><b>Installation Instructions with Geniusbot:</b></summary>
|
|
169
|
+
|
|
170
|
+
Install Python Package
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python -m pip install geniusbot
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
</details>
|
|
177
|
+
|
|
178
|
+
<details>
|
|
179
|
+
<summary><b>Repository Owners:</b></summary>
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
183
|
+
|
|
184
|
+

|
|
185
|
+

|
|
186
|
+
</details>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
from media_downloader.media_downloader import (
|
|
5
|
+
media_downloader,
|
|
6
|
+
main,
|
|
7
|
+
setup_logging,
|
|
8
|
+
MediaDownloader,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
media-downloader
|
|
13
|
+
|
|
14
|
+
Download videos and audio from the internet!
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__all__ = ["media_downloader", "main", "setup_logging", "MediaDownloader"]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import re
|
|
7
|
+
import getopt
|
|
8
|
+
import logging
|
|
9
|
+
import requests
|
|
10
|
+
import yt_dlp
|
|
11
|
+
from multiprocessing import Pool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Configure logging
|
|
15
|
+
def setup_logging(is_mcp_server=False, log_file="media_downloader.log"):
|
|
16
|
+
logger = logging.getLogger("MediaDownloader")
|
|
17
|
+
logger.setLevel(logging.DEBUG)
|
|
18
|
+
|
|
19
|
+
# Clear any existing handlers to avoid duplicate logs
|
|
20
|
+
logger.handlers.clear()
|
|
21
|
+
|
|
22
|
+
if is_mcp_server:
|
|
23
|
+
# Log to a file when running as MCP server
|
|
24
|
+
handler = logging.FileHandler(log_file)
|
|
25
|
+
else:
|
|
26
|
+
# Log to console (stdout) when running standalone
|
|
27
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
28
|
+
|
|
29
|
+
handler.setLevel(logging.DEBUG)
|
|
30
|
+
formatter = logging.Formatter(
|
|
31
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
32
|
+
)
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
logger.addHandler(handler)
|
|
35
|
+
return logger
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class YtDlpLogger:
|
|
39
|
+
def __init__(self, logger):
|
|
40
|
+
self.logger = logger
|
|
41
|
+
|
|
42
|
+
def debug(self, msg):
|
|
43
|
+
self.logger.debug(msg)
|
|
44
|
+
|
|
45
|
+
def warning(self, msg):
|
|
46
|
+
self.logger.warning(msg)
|
|
47
|
+
|
|
48
|
+
def error(self, msg):
|
|
49
|
+
self.logger.error(msg)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MediaDownloader:
|
|
53
|
+
def __init__(
|
|
54
|
+
self, links: list = [], download_directory: str = None, audio: bool = False
|
|
55
|
+
):
|
|
56
|
+
self.links = links
|
|
57
|
+
if download_directory:
|
|
58
|
+
self.download_directory = download_directory
|
|
59
|
+
else:
|
|
60
|
+
self.download_directory = f'{os.path.expanduser("~")}/Downloads'
|
|
61
|
+
self.audio = audio
|
|
62
|
+
self.logger = logging.getLogger("MediaDownloader")
|
|
63
|
+
self.progress_callback = None # Store callback for progress updates
|
|
64
|
+
|
|
65
|
+
def set_progress_callback(self, callback):
|
|
66
|
+
self.progress_callback = callback
|
|
67
|
+
|
|
68
|
+
def download_video(self, link):
|
|
69
|
+
self.logger.debug(f"Downloading video: {link}")
|
|
70
|
+
outtmpl = f"{self.download_directory}/%(uploader)s - %(title)s.%(ext)s"
|
|
71
|
+
if "rumble.com" in link:
|
|
72
|
+
self.logger.debug(f"Processing Rumble URL: {link}")
|
|
73
|
+
rumble_url = requests.get(link)
|
|
74
|
+
for rumble_embedded_url in rumble_url.text.split(","):
|
|
75
|
+
if "embedUrl" in rumble_embedded_url:
|
|
76
|
+
rumble_embedded_url = re.sub(
|
|
77
|
+
'"', "", re.sub('"embedUrl":', "", rumble_embedded_url)
|
|
78
|
+
)
|
|
79
|
+
link = rumble_embedded_url
|
|
80
|
+
outtmpl = f"{self.download_directory}/%(title)s.%(ext)s"
|
|
81
|
+
self.logger.debug(f"Updated Rumble URL: {link}")
|
|
82
|
+
|
|
83
|
+
ydl_opts = {
|
|
84
|
+
"format": "bestaudio/best" if self.audio else "best",
|
|
85
|
+
"outtmpl": outtmpl,
|
|
86
|
+
"quiet": True,
|
|
87
|
+
"no_warnings": True,
|
|
88
|
+
"progress_hooks": [self.progress_hook], # Add progress hook
|
|
89
|
+
"logger": YtDlpLogger(self.logger),
|
|
90
|
+
}
|
|
91
|
+
if self.audio:
|
|
92
|
+
ydl_opts["postprocessors"] = [
|
|
93
|
+
{
|
|
94
|
+
"key": "FFmpegExtractAudio",
|
|
95
|
+
"preferredcodec": "mp3",
|
|
96
|
+
"preferredquality": "320",
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
102
|
+
info = ydl.extract_info(link, download=True)
|
|
103
|
+
return ydl.prepare_filename(info)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
self.logger.error(f"Failed to download {link}: {str(e)}")
|
|
106
|
+
try:
|
|
107
|
+
outtmpl = f"{self.download_directory}/%(id)s.%(ext)s"
|
|
108
|
+
ydl_opts["outtmpl"] = outtmpl
|
|
109
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
110
|
+
info = ydl.extract_info(link, download=True)
|
|
111
|
+
return ydl.prepare_filename(info)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
self.logger.error(f"Retry failed for {link}: {str(e)}")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def get_channel_videos(self, channel, limit=-1):
|
|
117
|
+
self.logger.debug(f"Fetching videos for channel: {channel}, limit: {limit}")
|
|
118
|
+
username = channel
|
|
119
|
+
attempts = 0
|
|
120
|
+
while attempts < 3:
|
|
121
|
+
url = f"https://www.youtube.com/user/{username}/videos"
|
|
122
|
+
self.logger.debug(f"Trying URL: {url}")
|
|
123
|
+
page = requests.get(url).content
|
|
124
|
+
data = str(page).split(" ")
|
|
125
|
+
item = 'href="/watch?'
|
|
126
|
+
vids = [
|
|
127
|
+
line.replace('href="', "youtube.com") for line in data if item in line
|
|
128
|
+
]
|
|
129
|
+
if vids:
|
|
130
|
+
self.logger.debug(f"Found {len(vids)} videos")
|
|
131
|
+
x = 0
|
|
132
|
+
for vid in vids:
|
|
133
|
+
if limit < 0 or x < limit:
|
|
134
|
+
self.append_link(vid)
|
|
135
|
+
x += 1
|
|
136
|
+
return
|
|
137
|
+
else:
|
|
138
|
+
url = f"https://www.youtube.com/c/{channel}/videos"
|
|
139
|
+
self.logger.debug(f"Trying URL: {url}")
|
|
140
|
+
page = requests.get(url).content
|
|
141
|
+
data = str(page).split(" ")
|
|
142
|
+
item = "https://i.ytimg.com/vi/"
|
|
143
|
+
vids = []
|
|
144
|
+
for line in data:
|
|
145
|
+
if item in line:
|
|
146
|
+
try:
|
|
147
|
+
found = re.search(
|
|
148
|
+
"https://i.ytimg.com/vi/(.+?)/hqdefault.", line
|
|
149
|
+
).group(1)
|
|
150
|
+
vid = f"https://www.youtube.com/watch?v={found}"
|
|
151
|
+
vids.append(vid)
|
|
152
|
+
except AttributeError:
|
|
153
|
+
continue
|
|
154
|
+
if vids:
|
|
155
|
+
self.logger.debug(f"Found {len(vids)} videos")
|
|
156
|
+
x = 0
|
|
157
|
+
for vid in vids:
|
|
158
|
+
if limit < 0 or x < limit:
|
|
159
|
+
self.append_link(vid)
|
|
160
|
+
x += 1
|
|
161
|
+
return
|
|
162
|
+
attempts += 1
|
|
163
|
+
self.logger.error(f"Could not find user or channel: {channel}")
|
|
164
|
+
|
|
165
|
+
def progress_hook(self, d):
|
|
166
|
+
if self.progress_callback and d["status"] == "downloading":
|
|
167
|
+
if d.get("total_bytes") and d.get("downloaded_bytes"):
|
|
168
|
+
progress = (d["downloaded_bytes"] / d["total_bytes"]) * 100
|
|
169
|
+
self.progress_callback(progress=progress, total=100)
|
|
170
|
+
elif d.get("downloaded_bytes"):
|
|
171
|
+
# Indeterminate progress if total_bytes is unavailable
|
|
172
|
+
self.progress_callback(progress=d["downloaded_bytes"])
|
|
173
|
+
elif d["status"] == "finished":
|
|
174
|
+
if self.progress_callback:
|
|
175
|
+
self.progress_callback(progress=100, total=100)
|
|
176
|
+
|
|
177
|
+
def download_all(self):
|
|
178
|
+
self.logger.debug(f"Downloading {len(self.links)} links")
|
|
179
|
+
pool = Pool(processes=os.cpu_count())
|
|
180
|
+
try:
|
|
181
|
+
results = pool.map(self.download_video, self.links)
|
|
182
|
+
self.links = []
|
|
183
|
+
for result in results:
|
|
184
|
+
if result and os.path.exists(result):
|
|
185
|
+
return result
|
|
186
|
+
return None
|
|
187
|
+
finally:
|
|
188
|
+
pool.close()
|
|
189
|
+
pool.join()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def media_downloader(argv):
|
|
193
|
+
logger = setup_logging(is_mcp_server=False)
|
|
194
|
+
video_downloader_instance = MediaDownloader()
|
|
195
|
+
try:
|
|
196
|
+
opts, args = getopt.getopt(
|
|
197
|
+
argv,
|
|
198
|
+
"hac:d:f:l:",
|
|
199
|
+
["help", "audio", "channel=", "directory=", "file=", "links="],
|
|
200
|
+
)
|
|
201
|
+
except getopt.GetoptError:
|
|
202
|
+
usage()
|
|
203
|
+
logger.error("Incorrect arguments")
|
|
204
|
+
sys.exit(2)
|
|
205
|
+
for opt, arg in opts:
|
|
206
|
+
if opt in ("-h", "--help"):
|
|
207
|
+
usage()
|
|
208
|
+
sys.exit()
|
|
209
|
+
elif opt in ("-a", "--audio"):
|
|
210
|
+
video_downloader_instance.audio = True
|
|
211
|
+
elif opt in ("-c", "--channel"):
|
|
212
|
+
video_downloader_instance.get_channel_videos(arg)
|
|
213
|
+
elif opt in ("-d", "--directory"):
|
|
214
|
+
video_downloader_instance.download_directory = arg
|
|
215
|
+
elif opt in ("-f", "--file"):
|
|
216
|
+
video_downloader_instance.open_file(arg)
|
|
217
|
+
elif opt in ("-l", "--links"):
|
|
218
|
+
url_list = arg.replace(" ", "").split(",")
|
|
219
|
+
for url in url_list:
|
|
220
|
+
video_downloader_instance.links.extend(url_list)
|
|
221
|
+
|
|
222
|
+
video_downloader_instance.download_all()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def usage():
|
|
226
|
+
print(
|
|
227
|
+
"Media-Downloader: A tool to download any video off the internet!\n"
|
|
228
|
+
"\nUsage:\n"
|
|
229
|
+
"-h | --help [ See usage ]\n"
|
|
230
|
+
"-a | --audio [ Download audio only ]\n"
|
|
231
|
+
"-c | --channel [ YouTube Channel/User - Downloads all videos ]\n"
|
|
232
|
+
"-d | --directory [ Location where the images will be saved ]\n"
|
|
233
|
+
"-f | --file [ Text file to read the URLs from ]\n"
|
|
234
|
+
"-l | --links [ Comma separated URLs (No spaces) ]\n"
|
|
235
|
+
"\nExample:\n"
|
|
236
|
+
'media-downloader -f "file_of_urls.txt" -l "URL1,URL2,URL3" -c "WhiteHouse" -d "~/Downloads"\n'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main():
|
|
241
|
+
media_downloader(sys.argv[1:])
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
media_downloader(sys.argv[1:])
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
import getopt
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from media_downloader import MediaDownloader, setup_logging
|
|
9
|
+
from fastmcp import FastMCP, Context
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
# Initialize logging for MCP server (logs to file)
|
|
13
|
+
setup_logging(is_mcp_server=True, log_file="media_downloader_mcp.log")
|
|
14
|
+
|
|
15
|
+
mcp = FastMCP(name="MediaDownloaderServer")
|
|
16
|
+
|
|
17
|
+
def to_boolean(string):
|
|
18
|
+
# Normalize the string: strip whitespace and convert to lowercase
|
|
19
|
+
normalized = str(string).strip().lower()
|
|
20
|
+
|
|
21
|
+
# Define valid true/false values
|
|
22
|
+
true_values = {'t', 'true', 'y', 'yes', '1'}
|
|
23
|
+
false_values = {'f', 'false', 'n', 'no', '0'}
|
|
24
|
+
|
|
25
|
+
if normalized in true_values:
|
|
26
|
+
return True
|
|
27
|
+
elif normalized in false_values:
|
|
28
|
+
return False
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(f"Cannot convert '{string}' to boolean")
|
|
31
|
+
|
|
32
|
+
environment_download_directory = os.environ.get("DOWNLOAD_DIRECTORY", None)
|
|
33
|
+
environment_audio_only = os.environ.get("AUDIO_ONLY", False)
|
|
34
|
+
|
|
35
|
+
if environment_audio_only:
|
|
36
|
+
environment_audio_only = to_boolean(environment_audio_only)
|
|
37
|
+
|
|
38
|
+
@mcp.tool(
|
|
39
|
+
annotations={
|
|
40
|
+
"title": "Download Media",
|
|
41
|
+
"readOnlyHint": False,
|
|
42
|
+
"destructiveHint": False,
|
|
43
|
+
"idempotentHint": True,
|
|
44
|
+
"openWorldHint": False,
|
|
45
|
+
},
|
|
46
|
+
tags={"collection_management"},
|
|
47
|
+
)
|
|
48
|
+
async def download_media(
|
|
49
|
+
video_url: str = Field(description="Video URL to Download", default=None),
|
|
50
|
+
download_directory: Optional[str] = Field(
|
|
51
|
+
description="The directory where the media will be saved. If None, uses default directory.",
|
|
52
|
+
default=environment_download_directory),
|
|
53
|
+
audio_only: Optional[bool] = Field(description="Downloads only the audio", default=environment_audio_only),
|
|
54
|
+
ctx: Context = Field(description="MCP context for progress reporting.", default=None),
|
|
55
|
+
) -> str:
|
|
56
|
+
"""Downloads media from a given URL to the specified directory."""
|
|
57
|
+
logger = logging.getLogger("MediaDownloader")
|
|
58
|
+
logger.debug(
|
|
59
|
+
f"Starting download for URL: {video_url}, directory: {download_directory}, audio_only: {audio_only}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
if not video_url:
|
|
64
|
+
raise ValueError("video_url must not be empty")
|
|
65
|
+
|
|
66
|
+
download_directory = f'{os.path.expanduser("~")}/Downloads'
|
|
67
|
+
os.makedirs(download_directory, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
downloader = MediaDownloader(
|
|
70
|
+
download_directory=download_directory, audio=audio_only
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Set progress callback for yt_dlp
|
|
74
|
+
async def progress_callback(progress, total=None):
|
|
75
|
+
if ctx:
|
|
76
|
+
await ctx.report_progress(progress=progress, total=total)
|
|
77
|
+
logger.debug(f"Reported progress: {progress}/{total}")
|
|
78
|
+
|
|
79
|
+
downloader.set_progress_callback(progress_callback)
|
|
80
|
+
|
|
81
|
+
# Report initial progress
|
|
82
|
+
if ctx:
|
|
83
|
+
await ctx.report_progress(progress=0, total=100)
|
|
84
|
+
logger.debug("Reported initial progress: 0/100")
|
|
85
|
+
|
|
86
|
+
# Perform the download
|
|
87
|
+
file_path = downloader.download_video(link=video_url)
|
|
88
|
+
|
|
89
|
+
if not file_path or not os.path.exists(file_path):
|
|
90
|
+
raise RuntimeError("Download failed or file not found")
|
|
91
|
+
|
|
92
|
+
# Report completion
|
|
93
|
+
if ctx:
|
|
94
|
+
await ctx.report_progress(progress=100, total=100)
|
|
95
|
+
logger.debug("Reported final progress: 100/100")
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Download completed, file path: {file_path}")
|
|
98
|
+
return file_path
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Failed to download media: {str(e)}")
|
|
101
|
+
raise RuntimeError(f"Failed to download media: {str(e)}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def media_downloader_mcp(argv):
|
|
105
|
+
transport = "stdio"
|
|
106
|
+
host = "0.0.0.0"
|
|
107
|
+
port = 8000
|
|
108
|
+
try:
|
|
109
|
+
opts, args = getopt.getopt(
|
|
110
|
+
argv,
|
|
111
|
+
"ht:h:p:",
|
|
112
|
+
["help", "transport=", "host=", "port="],
|
|
113
|
+
)
|
|
114
|
+
except getopt.GetoptError:
|
|
115
|
+
sys.exit(2)
|
|
116
|
+
for opt, arg in opts:
|
|
117
|
+
if opt in ("-h", "--help"):
|
|
118
|
+
sys.exit()
|
|
119
|
+
elif opt in ("-t", "--transport"):
|
|
120
|
+
transport = arg
|
|
121
|
+
elif opt in ("-h", "--host"):
|
|
122
|
+
host = arg
|
|
123
|
+
elif opt in ("-p", "--port"):
|
|
124
|
+
try:
|
|
125
|
+
port = int(arg) # Attempt to convert port to integer
|
|
126
|
+
if not (0 <= port <= 65535): # Valid port range
|
|
127
|
+
print(f"Error: Port {arg} is out of valid range (0-65535).")
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
except ValueError:
|
|
130
|
+
print(f"Error: Port {arg} is not a valid integer.")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
if transport == "stdio":
|
|
133
|
+
mcp.run(transport="stdio")
|
|
134
|
+
elif transport == "http":
|
|
135
|
+
mcp.run(transport="http", host=host, port=port)
|
|
136
|
+
else:
|
|
137
|
+
logger = logging.getLogger("MediaDownloader")
|
|
138
|
+
logger.error("Transport not supported")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
media_downloader_mcp(sys.argv[1:])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
media_downloader_mcp(sys.argv[1:])
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: media-downloader
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: Download audio/videos from the internet!
|
|
5
|
+
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: License :: Public Domain
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: yt-dlp>=2025.8.20
|
|
16
|
+
Requires-Dist: fastmcp>=2.11.3
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Media Downloader
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
*Version: 1.0.8*
|
|
42
|
+
|
|
43
|
+
Download videos and audio from the internet!
|
|
44
|
+
|
|
45
|
+
MCP Server Support!
|
|
46
|
+
|
|
47
|
+
This repository is actively maintained - Contributions are welcome!
|
|
48
|
+
|
|
49
|
+
### Supports:
|
|
50
|
+
- YouTube
|
|
51
|
+
- Twitter
|
|
52
|
+
- Rumble
|
|
53
|
+
- BitChute
|
|
54
|
+
- Vimeo
|
|
55
|
+
- And More!
|
|
56
|
+
|
|
57
|
+
#### Using an an MCP Server:
|
|
58
|
+
|
|
59
|
+
AI Prompt:
|
|
60
|
+
```text
|
|
61
|
+
Download me this video: https://youtube.com/watch?askdjfa
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
AI Response:
|
|
65
|
+
```text
|
|
66
|
+
Sure thing, the video has been downloaded to:
|
|
67
|
+
|
|
68
|
+
"C:\Users\User\Downloads\YouTube Video - Episode 1.mp4"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary><b>Usage:</b></summary>
|
|
73
|
+
|
|
74
|
+
| Short Flag | Long Flag | Description |
|
|
75
|
+
|------------|-------------|---------------------------------------------|
|
|
76
|
+
| -h | --help | See usage |
|
|
77
|
+
| -a | --audio | Download audio only |
|
|
78
|
+
| -c | --channel | YouTube Channel/User - Downloads all videos |
|
|
79
|
+
| -f | --file | File with video links |
|
|
80
|
+
| -l | --links | Comma separated links |
|
|
81
|
+
| -d | --directory | Location to save videos |
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary><b>Example:</b></summary>
|
|
87
|
+
|
|
88
|
+
### Use in CLI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
media-downloader --file "C:\Users\videos.txt" --directory "C:\Users\Downloads" --channel "WhiteHouse" --links "URL1,URL2,URL3"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Use in Python
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Import library
|
|
98
|
+
from media_downloader import MediaDownloader
|
|
99
|
+
|
|
100
|
+
# Set URL of video/audio here
|
|
101
|
+
url = "https://YootToob.com/video"
|
|
102
|
+
|
|
103
|
+
# Instantiate vide_downloader_instance
|
|
104
|
+
video_downloader_instance = MediaDownloader()
|
|
105
|
+
|
|
106
|
+
# Set the location to save the video
|
|
107
|
+
video_downloader_instance.set_save_path("C:/Users/you/Downloads")
|
|
108
|
+
|
|
109
|
+
# Add URL to download
|
|
110
|
+
video_downloader_instance.append_link(url)
|
|
111
|
+
|
|
112
|
+
# Download all videos appended
|
|
113
|
+
video_downloader_instance.download_all()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Optional - Set Audio to True, Default is False if unspecified.
|
|
118
|
+
video_downloader_instance.set_audio(audio=True)
|
|
119
|
+
|
|
120
|
+
# Optional - Open a file of video/audio URL(s)
|
|
121
|
+
video_downloader_instance.open_file("FILE")
|
|
122
|
+
|
|
123
|
+
# Optional - Enter a YouTube channel name and download their latest videos
|
|
124
|
+
video_downloader_instance.get_channel_videos("YT-Channel Name")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Use with AI
|
|
128
|
+
|
|
129
|
+
Deploy MCP Server as a Service
|
|
130
|
+
```bash
|
|
131
|
+
docker pull knucklessg1/media-downloader:latest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Modify the `compose.yml`
|
|
135
|
+
|
|
136
|
+
```compose
|
|
137
|
+
services:
|
|
138
|
+
media-downloader-mcp:
|
|
139
|
+
image: knucklessg1/media-downloader:latest
|
|
140
|
+
volumes:
|
|
141
|
+
- downloads:/root/Downloads
|
|
142
|
+
environment:
|
|
143
|
+
- HOST=0.0.0.0
|
|
144
|
+
- PORT=8000
|
|
145
|
+
ports:
|
|
146
|
+
- 8000:8000
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Configure `mcp.json`
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcpServers": {
|
|
154
|
+
"media_downloader": {
|
|
155
|
+
"command": "media-downloader-mcp",
|
|
156
|
+
"env": {
|
|
157
|
+
"DOWNLOAD_DIRECTORY": "~/Downloads", // Optional - Can be specified at prompt
|
|
158
|
+
"AUDIO_ONLY": false // Optional - Can be specified at prompt
|
|
159
|
+
},
|
|
160
|
+
"timeout": 300000
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
</details>
|
|
168
|
+
|
|
169
|
+
<details>
|
|
170
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
171
|
+
|
|
172
|
+
Install Python Package
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python -m pip install media-downloader
|
|
176
|
+
```
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
## Geniusbot Application
|
|
180
|
+
|
|
181
|
+
Use with a GUI through Geniusbot
|
|
182
|
+
|
|
183
|
+
Visit our [GitHub](https://github.com/Knuckles-Team/geniusbot) for more information
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary><b>Installation Instructions with Geniusbot:</b></summary>
|
|
187
|
+
|
|
188
|
+
Install Python Package
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
python -m pip install geniusbot
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
</details>
|
|
195
|
+
|
|
196
|
+
<details>
|
|
197
|
+
<summary><b>Repository Owners:</b></summary>
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
201
|
+
|
|
202
|
+

|
|
203
|
+

|
|
204
|
+
</details>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
requirements.txt
|
|
6
|
+
media_downloader/__init__.py
|
|
7
|
+
media_downloader/__main__.py
|
|
8
|
+
media_downloader/media_downloader.py
|
|
9
|
+
media_downloader/media_downloader_mcp.py
|
|
10
|
+
media_downloader.egg-info/PKG-INFO
|
|
11
|
+
media_downloader.egg-info/SOURCES.txt
|
|
12
|
+
media_downloader.egg-info/dependency_links.txt
|
|
13
|
+
media_downloader.egg-info/entry_points.txt
|
|
14
|
+
media_downloader.egg-info/requires.txt
|
|
15
|
+
media_downloader.egg-info/top_level.txt
|
|
16
|
+
tests/test_mcp.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.9.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "media-downloader"
|
|
7
|
+
version = "1.0.8"
|
|
8
|
+
description = "Download audio/videos from the internet!\nHost an MCP Server for Agentic AI to download videos!"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Audel Rouhi", email = "knucklessg1@gmail.com" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"License :: Public Domain",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.8"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"yt-dlp>=2025.8.20",
|
|
22
|
+
"fastmcp>=2.11.3"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
media-downloader = "media_downloader.media_downloader:main"
|
|
27
|
+
media-downloader-mcp = "media_downloader.media_downloader_mcp:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
include-package-data = true
|
|
34
|
+
package-data = { "media_downloader" = ["media_downloader"] }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
import subprocess
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_server(
|
|
8
|
+
video_url: str, download_directory: str = ".", audio_only: bool = False
|
|
9
|
+
):
|
|
10
|
+
payload = {
|
|
11
|
+
"tool": "download_media",
|
|
12
|
+
"args": {
|
|
13
|
+
"video_url": video_url,
|
|
14
|
+
"download_directory": download_directory,
|
|
15
|
+
"audio_only": audio_only,
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
try:
|
|
19
|
+
# Run the server as a subprocess and pipe the JSON request
|
|
20
|
+
process = subprocess.run(
|
|
21
|
+
["python", "-m", "media_downloader", "--transport=stdio"],
|
|
22
|
+
input=json.dumps(payload),
|
|
23
|
+
text=True,
|
|
24
|
+
capture_output=True,
|
|
25
|
+
)
|
|
26
|
+
print("Server response:", process.stdout)
|
|
27
|
+
if process.stderr:
|
|
28
|
+
print("Errors:", process.stderr)
|
|
29
|
+
# Parse the response to extract the result
|
|
30
|
+
try:
|
|
31
|
+
response = json.loads(process.stdout)
|
|
32
|
+
if "result" in response:
|
|
33
|
+
print(f"Downloaded file path: {response['result']}")
|
|
34
|
+
elif "error" in response:
|
|
35
|
+
print(f"Error: {response['error']}")
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
print("Invalid JSON response:", process.stdout)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Failed to send request: {e}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
# Replace with a valid URL supported by MediaDownloader
|
|
44
|
+
test_server(
|
|
45
|
+
video_url="https://www.youtube.com/watch?v=Tkv_guk57i0",
|
|
46
|
+
download_directory="./downloads",
|
|
47
|
+
audio_only=False,
|
|
48
|
+
)
|