media-downloader 0.11.13__py2.py3-none-any.whl → 0.11.15__py2.py3-none-any.whl
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/media_downloader.py +74 -94
- media_downloader/media_downloader_mcp.py +22 -9
- media_downloader/version.py +1 -1
- {media_downloader-0.11.13.dist-info → media_downloader-0.11.15.dist-info}/METADATA +3 -3
- media_downloader-0.11.15.dist-info/RECORD +11 -0
- media_downloader-0.11.13.dist-info/RECORD +0 -11
- {media_downloader-0.11.13.dist-info → media_downloader-0.11.15.dist-info}/LICENSE +0 -0
- {media_downloader-0.11.13.dist-info → media_downloader-0.11.15.dist-info}/WHEEL +0 -0
- {media_downloader-0.11.13.dist-info → media_downloader-0.11.15.dist-info}/entry_points.txt +0 -0
- {media_downloader-0.11.13.dist-info → media_downloader-0.11.15.dist-info}/top_level.txt +0 -0
|
@@ -51,61 +51,15 @@ class YtDlpLogger:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class MediaDownloader:
|
|
54
|
-
|
|
55
|
-
def __init__(
|
|
56
|
-
self, links: list = [], download_directory: str = None, audio: bool = False
|
|
57
|
-
):
|
|
58
|
-
self.links = links
|
|
59
|
-
if download_directory:
|
|
60
|
-
self.download_directory = download_directory
|
|
61
|
-
else:
|
|
62
|
-
self.download_directory = f'{os.path.expanduser("~")}/Downloads'
|
|
63
|
-
self.audio = audio
|
|
64
|
-
self.logger = logging.getLogger("MediaDownloader")
|
|
65
|
-
|
|
66
|
-
def open_file(self, file):
|
|
67
|
-
youtube_urls = open(file, "r")
|
|
68
|
-
for url in youtube_urls:
|
|
69
|
-
self.links.append(url)
|
|
70
|
-
self.links = list(dict.fromkeys(self.links))
|
|
71
|
-
|
|
72
|
-
def get_save_path(self) -> str:
|
|
73
|
-
return self.download_directory
|
|
74
|
-
|
|
75
|
-
def set_save_path(self, download_directory):
|
|
76
|
-
self.download_directory = download_directory.replace(os.sep, "/")
|
|
77
|
-
self.logger.debug(f"Set download directory to: {self.download_directory}")
|
|
78
|
-
|
|
79
|
-
def reset_links(self):
|
|
80
|
-
self.logger.debug("Resetting links")
|
|
54
|
+
def __init__(self):
|
|
81
55
|
self.links = []
|
|
56
|
+
self.download_directory = f"{os.path.expanduser('~')}/Downloads"
|
|
57
|
+
self.audio = False
|
|
58
|
+
self.logger = logging.getLogger("MediaDownloader")
|
|
59
|
+
self.progress_callback = None # Store callback for progress updates
|
|
82
60
|
|
|
83
|
-
def
|
|
84
|
-
self.
|
|
85
|
-
self.links.extend(urls)
|
|
86
|
-
self.links = list(dict.fromkeys(self.links))
|
|
87
|
-
|
|
88
|
-
def append_link(self, url):
|
|
89
|
-
self.logger.debug(f"Appending link: {url}")
|
|
90
|
-
self.links.append(url)
|
|
91
|
-
self.links = list(dict.fromkeys(self.links))
|
|
92
|
-
|
|
93
|
-
def get_links(self) -> List[str]:
|
|
94
|
-
return self.links
|
|
95
|
-
|
|
96
|
-
def set_audio(self, audio=False):
|
|
97
|
-
self.audio = audio
|
|
98
|
-
self.logger.debug(f"Audio mode set to: {audio}")
|
|
99
|
-
|
|
100
|
-
def download_all(self):
|
|
101
|
-
self.logger.debug(f"Downloading {len(self.links)} links")
|
|
102
|
-
pool = Pool(processes=os.cpu_count())
|
|
103
|
-
try:
|
|
104
|
-
pool.map(self.download_video, self.links)
|
|
105
|
-
finally:
|
|
106
|
-
pool.close()
|
|
107
|
-
pool.join()
|
|
108
|
-
self.reset_links()
|
|
61
|
+
def set_progress_callback(self, callback):
|
|
62
|
+
self.progress_callback = callback
|
|
109
63
|
|
|
110
64
|
def download_video(self, link):
|
|
111
65
|
self.logger.debug(f"Downloading video: {link}")
|
|
@@ -125,10 +79,10 @@ class MediaDownloader:
|
|
|
125
79
|
ydl_opts = {
|
|
126
80
|
"format": "bestaudio/best" if self.audio else "best",
|
|
127
81
|
"outtmpl": outtmpl,
|
|
128
|
-
"quiet": True,
|
|
129
|
-
"no_warnings": True,
|
|
130
|
-
"
|
|
131
|
-
"logger": YtDlpLogger(self.logger),
|
|
82
|
+
"quiet": True,
|
|
83
|
+
"no_warnings": True,
|
|
84
|
+
"progress_hooks": [self.progress_hook], # Add progress hook
|
|
85
|
+
"logger": YtDlpLogger(self.logger),
|
|
132
86
|
}
|
|
133
87
|
if self.audio:
|
|
134
88
|
ydl_opts["postprocessors"] = [
|
|
@@ -142,7 +96,7 @@ class MediaDownloader:
|
|
|
142
96
|
try:
|
|
143
97
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
144
98
|
info = ydl.extract_info(link, download=True)
|
|
145
|
-
return ydl.prepare_filename(info)
|
|
99
|
+
return ydl.prepare_filename(info)
|
|
146
100
|
except Exception as e:
|
|
147
101
|
self.logger.error(f"Failed to download {link}: {str(e)}")
|
|
148
102
|
try:
|
|
@@ -156,43 +110,18 @@ class MediaDownloader:
|
|
|
156
110
|
return None
|
|
157
111
|
|
|
158
112
|
def get_channel_videos(self, channel, limit=-1):
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self.logger.debug(f"Trying URL: {url}")
|
|
165
|
-
page = requests.get(url).content
|
|
166
|
-
data = str(page).split(" ")
|
|
167
|
-
item = 'href="/watch?'
|
|
168
|
-
vids = [
|
|
169
|
-
line.replace('href="', "youtube.com") for line in data if item in line
|
|
170
|
-
]
|
|
171
|
-
if vids:
|
|
172
|
-
self.logger.debug(f"Found {len(vids)} videos")
|
|
173
|
-
x = 0
|
|
174
|
-
for vid in vids:
|
|
175
|
-
if limit < 0 or x < limit:
|
|
176
|
-
self.append_link(vid)
|
|
177
|
-
x += 1
|
|
178
|
-
return
|
|
179
|
-
else:
|
|
180
|
-
url = f"https://www.youtube.com/c/{channel}/videos"
|
|
113
|
+
self.logger.debug(f"Fetching videos for channel: {channel}, limit: {limit}")
|
|
114
|
+
username = channel
|
|
115
|
+
attempts = 0
|
|
116
|
+
while attempts < 3:
|
|
117
|
+
url = f"https://www.youtube.com/user/{username}/videos"
|
|
181
118
|
self.logger.debug(f"Trying URL: {url}")
|
|
182
119
|
page = requests.get(url).content
|
|
183
120
|
data = str(page).split(" ")
|
|
184
|
-
item = "
|
|
185
|
-
vids = [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
try:
|
|
189
|
-
found = re.search(
|
|
190
|
-
"https://i.ytimg.com/vi/(.+?)/hqdefault.", line
|
|
191
|
-
).group(1)
|
|
192
|
-
vid = f"https://www.youtube.com/watch?v={found}"
|
|
193
|
-
vids.append(vid)
|
|
194
|
-
except AttributeError:
|
|
195
|
-
continue
|
|
121
|
+
item = 'href="/watch?'
|
|
122
|
+
vids = [
|
|
123
|
+
line.replace('href="', "youtube.com") for line in data if item in line
|
|
124
|
+
]
|
|
196
125
|
if vids:
|
|
197
126
|
self.logger.debug(f"Found {len(vids)} videos")
|
|
198
127
|
x = 0
|
|
@@ -201,8 +130,59 @@ class MediaDownloader:
|
|
|
201
130
|
self.append_link(vid)
|
|
202
131
|
x += 1
|
|
203
132
|
return
|
|
204
|
-
|
|
205
|
-
|
|
133
|
+
else:
|
|
134
|
+
url = f"https://www.youtube.com/c/{channel}/videos"
|
|
135
|
+
self.logger.debug(f"Trying URL: {url}")
|
|
136
|
+
page = requests.get(url).content
|
|
137
|
+
data = str(page).split(" ")
|
|
138
|
+
item = "https://i.ytimg.com/vi/"
|
|
139
|
+
vids = []
|
|
140
|
+
for line in data:
|
|
141
|
+
if item in line:
|
|
142
|
+
try:
|
|
143
|
+
found = re.search(
|
|
144
|
+
"https://i.ytimg.com/vi/(.+?)/hqdefault.", line
|
|
145
|
+
).group(1)
|
|
146
|
+
vid = f"https://www.youtube.com/watch?v={found}"
|
|
147
|
+
vids.append(vid)
|
|
148
|
+
except AttributeError:
|
|
149
|
+
continue
|
|
150
|
+
if vids:
|
|
151
|
+
self.logger.debug(f"Found {len(vids)} videos")
|
|
152
|
+
x = 0
|
|
153
|
+
for vid in vids:
|
|
154
|
+
if limit < 0 or x < limit:
|
|
155
|
+
self.append_link(vid)
|
|
156
|
+
x += 1
|
|
157
|
+
return
|
|
158
|
+
attempts += 1
|
|
159
|
+
self.logger.error(f"Could not find user or channel: {channel}")
|
|
160
|
+
|
|
161
|
+
def progress_hook(self, d):
|
|
162
|
+
if self.progress_callback and d["status"] == "downloading":
|
|
163
|
+
if d.get("total_bytes") and d.get("downloaded_bytes"):
|
|
164
|
+
progress = (d["downloaded_bytes"] / d["total_bytes"]) * 100
|
|
165
|
+
self.progress_callback(progress=progress, total=100)
|
|
166
|
+
elif d.get("downloaded_bytes"):
|
|
167
|
+
# Indeterminate progress if total_bytes is unavailable
|
|
168
|
+
self.progress_callback(progress=d["downloaded_bytes"])
|
|
169
|
+
elif d["status"] == "finished":
|
|
170
|
+
if self.progress_callback:
|
|
171
|
+
self.progress_callback(progress=100, total=100)
|
|
172
|
+
|
|
173
|
+
def download_all(self):
|
|
174
|
+
self.logger.debug(f"Downloading {len(self.links)} links")
|
|
175
|
+
pool = Pool(processes=os.cpu_count())
|
|
176
|
+
try:
|
|
177
|
+
results = pool.map(self.download_video, self.links)
|
|
178
|
+
self.reset_links()
|
|
179
|
+
for result in results:
|
|
180
|
+
if result and os.path.exists(result):
|
|
181
|
+
return result
|
|
182
|
+
return None
|
|
183
|
+
finally:
|
|
184
|
+
pool.close()
|
|
185
|
+
pool.join()
|
|
206
186
|
|
|
207
187
|
|
|
208
188
|
def media_downloader(argv):
|
|
@@ -32,28 +32,41 @@ async def download_media(
|
|
|
32
32
|
RuntimeError: If the download fails.
|
|
33
33
|
"""
|
|
34
34
|
logger = logging.getLogger("MediaDownloader")
|
|
35
|
-
logger.debug(
|
|
36
|
-
f"Starting download for URL: {video_url}, directory: {download_directory}, audio_only: {audio_only}"
|
|
37
|
-
)
|
|
35
|
+
logger.debug(f"Starting download for URL: {video_url}, directory: {download_directory}, audio_only: {audio_only}")
|
|
38
36
|
|
|
39
37
|
try:
|
|
40
|
-
# Validate inputs
|
|
41
38
|
if not video_url or not download_directory:
|
|
42
39
|
raise ValueError("video_url and download_directory must not be empty")
|
|
43
|
-
|
|
44
|
-
# Ensure the download directory exists
|
|
45
40
|
os.makedirs(download_directory, exist_ok=True)
|
|
46
41
|
|
|
47
|
-
# Initialize MediaDownloader
|
|
48
42
|
downloader = MediaDownloader(
|
|
49
43
|
download_directory=download_directory, audio=audio_only
|
|
50
44
|
)
|
|
45
|
+
|
|
46
|
+
# Set progress callback for yt_dlp
|
|
47
|
+
async def progress_callback(progress, total=None):
|
|
48
|
+
if ctx:
|
|
49
|
+
await ctx.report_progress(progress=progress, total=total)
|
|
50
|
+
logger.debug(f"Reported progress: {progress}/{total}")
|
|
51
|
+
|
|
52
|
+
downloader.set_progress_callback(progress_callback)
|
|
53
|
+
|
|
54
|
+
# Report initial progress
|
|
55
|
+
if ctx:
|
|
56
|
+
await ctx.report_progress(progress=0, total=100)
|
|
57
|
+
logger.debug("Reported initial progress: 0/100")
|
|
58
|
+
|
|
59
|
+
# Perform the download
|
|
51
60
|
file_path = downloader.download_video(link=video_url)
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
if not file_path:
|
|
62
|
+
if not file_path or not os.path.exists(file_path):
|
|
55
63
|
raise RuntimeError("Download failed or file not found")
|
|
56
64
|
|
|
65
|
+
# Report completion
|
|
66
|
+
if ctx:
|
|
67
|
+
await ctx.report_progress(progress=100, total=100)
|
|
68
|
+
logger.debug("Reported final progress: 100/100")
|
|
69
|
+
|
|
57
70
|
logger.debug(f"Download completed, file path: {file_path}")
|
|
58
71
|
return file_path
|
|
59
72
|
except Exception as e:
|
media_downloader/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: media-downloader
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.15
|
|
4
4
|
Summary: Download audio/videos from the internet!
|
|
5
5
|
Home-page: https://github.com/Knuckles-Team/media-downloader
|
|
6
6
|
Author: Audel Rouhi
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: fastmcp (>=2.11.3)
|
|
24
|
-
Requires-Dist: yt-dlp (>=
|
|
24
|
+
Requires-Dist: yt-dlp (>=2025.8.20)
|
|
25
25
|
|
|
26
26
|
# Media Downloader
|
|
27
27
|
|
|
@@ -45,7 +45,7 @@ Requires-Dist: yt-dlp (>=2023.12.30)
|
|
|
45
45
|

|
|
46
46
|

|
|
47
47
|
|
|
48
|
-
*Version: 0.11.
|
|
48
|
+
*Version: 0.11.15*
|
|
49
49
|
|
|
50
50
|
Download videos and audio from the internet!
|
|
51
51
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
media_downloader/__init__.py,sha256=d0PDULMlfmN1E52NKz20Wprllpq-UYMsBU3-2p4ZEk8,460
|
|
2
|
+
media_downloader/__main__.py,sha256=aHajZBE7fyiC7E5qjV5LMe9nCPkQV2YKu6FU8Ubkdm4,112
|
|
3
|
+
media_downloader/media_downloader.py,sha256=Ihre9gBnaqorHSmOCFXyF_wYhUL_ohS3JFguZW2mAgQ,8894
|
|
4
|
+
media_downloader/media_downloader_mcp.py,sha256=fULxaUQe3-YBveYvOP3eKpaWBa2nrKHaNtr02t4kGU0,3547
|
|
5
|
+
media_downloader/version.py,sha256=D9vc4IaPvZ2ts54UPiI9aLExP3B_yt4LOaTNPUSXy_o,118
|
|
6
|
+
media_downloader-0.11.15.dist-info/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
|
7
|
+
media_downloader-0.11.15.dist-info/METADATA,sha256=3--6QBQ1aSppevqUfntO5O6pG5K_gpBtz3maPxN0SIU,5522
|
|
8
|
+
media_downloader-0.11.15.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
|
|
9
|
+
media_downloader-0.11.15.dist-info/entry_points.txt,sha256=Hjp1vLkHPq_bABsuh4kpL5nddi1wn9Ftota-72_GgW4,142
|
|
10
|
+
media_downloader-0.11.15.dist-info/top_level.txt,sha256=B2OBmgONOm0hIyx2HJ8qFPOI_p5HOeolrYvmslVC1fc,17
|
|
11
|
+
media_downloader-0.11.15.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
media_downloader/__init__.py,sha256=d0PDULMlfmN1E52NKz20Wprllpq-UYMsBU3-2p4ZEk8,460
|
|
2
|
-
media_downloader/__main__.py,sha256=aHajZBE7fyiC7E5qjV5LMe9nCPkQV2YKu6FU8Ubkdm4,112
|
|
3
|
-
media_downloader/media_downloader.py,sha256=IALP6PvypBjvtFbqHyG8FN3B7BONBJpNiz1vsJrPUoY,9139
|
|
4
|
-
media_downloader/media_downloader_mcp.py,sha256=TQge0wu8ORw5qUlJUuS777p6D-D4-16RwRmv_JzcN2M,2979
|
|
5
|
-
media_downloader/version.py,sha256=4Tbx5GpSb_b_vd21XQECp6Vms8kjl-97O7YN9f_prG8,118
|
|
6
|
-
media_downloader-0.11.13.dist-info/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
|
7
|
-
media_downloader-0.11.13.dist-info/METADATA,sha256=abydpky230Z2xrOAvGlmYzrHyOXoqI_C4SLLGyy-7j8,5523
|
|
8
|
-
media_downloader-0.11.13.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
|
|
9
|
-
media_downloader-0.11.13.dist-info/entry_points.txt,sha256=Hjp1vLkHPq_bABsuh4kpL5nddi1wn9Ftota-72_GgW4,142
|
|
10
|
-
media_downloader-0.11.13.dist-info/top_level.txt,sha256=B2OBmgONOm0hIyx2HJ8qFPOI_p5HOeolrYvmslVC1fc,17
|
|
11
|
-
media_downloader-0.11.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|