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.

@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/media-downloader)
22
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/media-downloader)
23
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/media-downloader)
24
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/media-downloader)
25
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/media-downloader)
26
+ ![PyPI - License](https://img.shields.io/pypi/l/media-downloader)
27
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/media-downloader)
28
+
29
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/media-downloader)
30
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/media-downloader)
31
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/media-downloader)
32
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/media-downloader)
33
+
34
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/media-downloader)
35
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/media-downloader)
36
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/media-downloader)
37
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/media-downloader)
38
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/media-downloader)
39
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/media-downloader)
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
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
203
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
204
+ </details>
@@ -0,0 +1,186 @@
1
+ # Media Downloader
2
+
3
+ ![PyPI - Version](https://img.shields.io/pypi/v/media-downloader)
4
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/media-downloader)
5
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/media-downloader)
6
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/media-downloader)
7
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/media-downloader)
8
+ ![PyPI - License](https://img.shields.io/pypi/l/media-downloader)
9
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/media-downloader)
10
+
11
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/media-downloader)
12
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/media-downloader)
13
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/media-downloader)
14
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/media-downloader)
15
+
16
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/media-downloader)
17
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/media-downloader)
18
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/media-downloader)
19
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/media-downloader)
20
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/media-downloader)
21
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/media-downloader)
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
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
185
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
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,6 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ from .media_downloader_mcp import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/media-downloader)
22
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/media-downloader)
23
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/media-downloader)
24
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/media-downloader)
25
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/media-downloader)
26
+ ![PyPI - License](https://img.shields.io/pypi/l/media-downloader)
27
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/media-downloader)
28
+
29
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/media-downloader)
30
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/media-downloader)
31
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/media-downloader)
32
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/media-downloader)
33
+
34
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/media-downloader)
35
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/media-downloader)
36
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/media-downloader)
37
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/media-downloader)
38
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/media-downloader)
39
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/media-downloader)
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
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
203
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
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,3 @@
1
+ [console_scripts]
2
+ media-downloader = media_downloader.media_downloader:main
3
+ media-downloader-mcp = media_downloader.media_downloader_mcp:main
@@ -0,0 +1,2 @@
1
+ yt-dlp>=2025.8.20
2
+ fastmcp>=2.11.3
@@ -0,0 +1,3 @@
1
+ dist
2
+ media_downloader
3
+ tests
@@ -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,2 @@
1
+ yt-dlp>=2025.8.20
2
+ fastmcp>=2.11.3
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )