media-downloader 0.11.14__py2.py3-none-any.whl → 0.11.16__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.
- media_downloader/media_downloader.py +72 -87
- media_downloader/media_downloader_mcp.py +23 -10
- media_downloader/version.py +1 -1
- {media_downloader-0.11.14.dist-info → media_downloader-0.11.16.dist-info}/METADATA +3 -3
- media_downloader-0.11.16.dist-info/RECORD +11 -0
- media_downloader-0.11.14.dist-info/RECORD +0 -11
- {media_downloader-0.11.14.dist-info → media_downloader-0.11.16.dist-info}/LICENSE +0 -0
- {media_downloader-0.11.14.dist-info → media_downloader-0.11.16.dist-info}/WHEEL +0 -0
- {media_downloader-0.11.14.dist-info → media_downloader-0.11.16.dist-info}/entry_points.txt +0 -0
- {media_downloader-0.11.14.dist-info → media_downloader-0.11.16.dist-info}/top_level.txt +0 -0
|
@@ -51,10 +51,9 @@ class YtDlpLogger:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class MediaDownloader:
|
|
54
|
-
|
|
55
54
|
def __init__(
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
self, links: list = [], download_directory: str = None, audio: bool = False
|
|
56
|
+
):
|
|
58
57
|
self.links = links
|
|
59
58
|
if download_directory:
|
|
60
59
|
self.download_directory = download_directory
|
|
@@ -62,50 +61,10 @@ class MediaDownloader:
|
|
|
62
61
|
self.download_directory = f'{os.path.expanduser("~")}/Downloads'
|
|
63
62
|
self.audio = audio
|
|
64
63
|
self.logger = logging.getLogger("MediaDownloader")
|
|
64
|
+
self.progress_callback = None # Store callback for progress updates
|
|
65
65
|
|
|
66
|
-
def
|
|
67
|
-
|
|
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")
|
|
81
|
-
self.links = []
|
|
82
|
-
|
|
83
|
-
def extend_links(self, urls):
|
|
84
|
-
self.logger.debug(f"Extending links: {urls}")
|
|
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()
|
|
66
|
+
def set_progress_callback(self, callback):
|
|
67
|
+
self.progress_callback = callback
|
|
109
68
|
|
|
110
69
|
def download_video(self, link):
|
|
111
70
|
self.logger.debug(f"Downloading video: {link}")
|
|
@@ -125,10 +84,10 @@ class MediaDownloader:
|
|
|
125
84
|
ydl_opts = {
|
|
126
85
|
"format": "bestaudio/best" if self.audio else "best",
|
|
127
86
|
"outtmpl": outtmpl,
|
|
128
|
-
"quiet": True,
|
|
129
|
-
"no_warnings": True,
|
|
130
|
-
"
|
|
131
|
-
"logger": YtDlpLogger(self.logger),
|
|
87
|
+
"quiet": True,
|
|
88
|
+
"no_warnings": True,
|
|
89
|
+
"progress_hooks": [self.progress_hook], # Add progress hook
|
|
90
|
+
"logger": YtDlpLogger(self.logger),
|
|
132
91
|
}
|
|
133
92
|
if self.audio:
|
|
134
93
|
ydl_opts["postprocessors"] = [
|
|
@@ -142,7 +101,7 @@ class MediaDownloader:
|
|
|
142
101
|
try:
|
|
143
102
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
144
103
|
info = ydl.extract_info(link, download=True)
|
|
145
|
-
return ydl.prepare_filename(info)
|
|
104
|
+
return ydl.prepare_filename(info)
|
|
146
105
|
except Exception as e:
|
|
147
106
|
self.logger.error(f"Failed to download {link}: {str(e)}")
|
|
148
107
|
try:
|
|
@@ -156,43 +115,18 @@ class MediaDownloader:
|
|
|
156
115
|
return None
|
|
157
116
|
|
|
158
117
|
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"
|
|
118
|
+
self.logger.debug(f"Fetching videos for channel: {channel}, limit: {limit}")
|
|
119
|
+
username = channel
|
|
120
|
+
attempts = 0
|
|
121
|
+
while attempts < 3:
|
|
122
|
+
url = f"https://www.youtube.com/user/{username}/videos"
|
|
181
123
|
self.logger.debug(f"Trying URL: {url}")
|
|
182
124
|
page = requests.get(url).content
|
|
183
125
|
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
|
|
126
|
+
item = 'href="/watch?'
|
|
127
|
+
vids = [
|
|
128
|
+
line.replace('href="', "youtube.com") for line in data if item in line
|
|
129
|
+
]
|
|
196
130
|
if vids:
|
|
197
131
|
self.logger.debug(f"Found {len(vids)} videos")
|
|
198
132
|
x = 0
|
|
@@ -201,8 +135,59 @@ class MediaDownloader:
|
|
|
201
135
|
self.append_link(vid)
|
|
202
136
|
x += 1
|
|
203
137
|
return
|
|
204
|
-
|
|
205
|
-
|
|
138
|
+
else:
|
|
139
|
+
url = f"https://www.youtube.com/c/{channel}/videos"
|
|
140
|
+
self.logger.debug(f"Trying URL: {url}")
|
|
141
|
+
page = requests.get(url).content
|
|
142
|
+
data = str(page).split(" ")
|
|
143
|
+
item = "https://i.ytimg.com/vi/"
|
|
144
|
+
vids = []
|
|
145
|
+
for line in data:
|
|
146
|
+
if item in line:
|
|
147
|
+
try:
|
|
148
|
+
found = re.search(
|
|
149
|
+
"https://i.ytimg.com/vi/(.+?)/hqdefault.", line
|
|
150
|
+
).group(1)
|
|
151
|
+
vid = f"https://www.youtube.com/watch?v={found}"
|
|
152
|
+
vids.append(vid)
|
|
153
|
+
except AttributeError:
|
|
154
|
+
continue
|
|
155
|
+
if vids:
|
|
156
|
+
self.logger.debug(f"Found {len(vids)} videos")
|
|
157
|
+
x = 0
|
|
158
|
+
for vid in vids:
|
|
159
|
+
if limit < 0 or x < limit:
|
|
160
|
+
self.append_link(vid)
|
|
161
|
+
x += 1
|
|
162
|
+
return
|
|
163
|
+
attempts += 1
|
|
164
|
+
self.logger.error(f"Could not find user or channel: {channel}")
|
|
165
|
+
|
|
166
|
+
def progress_hook(self, d):
|
|
167
|
+
if self.progress_callback and d["status"] == "downloading":
|
|
168
|
+
if d.get("total_bytes") and d.get("downloaded_bytes"):
|
|
169
|
+
progress = (d["downloaded_bytes"] / d["total_bytes"]) * 100
|
|
170
|
+
self.progress_callback(progress=progress, total=100)
|
|
171
|
+
elif d.get("downloaded_bytes"):
|
|
172
|
+
# Indeterminate progress if total_bytes is unavailable
|
|
173
|
+
self.progress_callback(progress=d["downloaded_bytes"])
|
|
174
|
+
elif d["status"] == "finished":
|
|
175
|
+
if self.progress_callback:
|
|
176
|
+
self.progress_callback(progress=100, total=100)
|
|
177
|
+
|
|
178
|
+
def download_all(self):
|
|
179
|
+
self.logger.debug(f"Downloading {len(self.links)} links")
|
|
180
|
+
pool = Pool(processes=os.cpu_count())
|
|
181
|
+
try:
|
|
182
|
+
results = pool.map(self.download_video, self.links)
|
|
183
|
+
self.reset_links()
|
|
184
|
+
for result in results:
|
|
185
|
+
if result and os.path.exists(result):
|
|
186
|
+
return result
|
|
187
|
+
return None
|
|
188
|
+
finally:
|
|
189
|
+
pool.close()
|
|
190
|
+
pool.join()
|
|
206
191
|
|
|
207
192
|
|
|
208
193
|
def media_downloader(argv):
|
|
@@ -32,27 +32,40 @@ 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
|
-
|
|
55
|
-
|
|
62
|
+
if not file_path or not os.path.exists(file_path):
|
|
63
|
+
raise RuntimeError("Download failed or file not found")
|
|
64
|
+
|
|
65
|
+
# Report completion
|
|
66
|
+
if ctx:
|
|
67
|
+
await ctx.report_progress(progress=100, total=100)
|
|
68
|
+
logger.debug("Reported final progress: 100/100")
|
|
56
69
|
|
|
57
70
|
logger.debug(f"Download completed, file path: {file_path}")
|
|
58
71
|
return file_path
|
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.16
|
|
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.16*
|
|
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=ufxRK8H2oc_bGxkewdLtfI8a3vkUI0NMJzCcIlc1NAg,9096
|
|
4
|
+
media_downloader/media_downloader_mcp.py,sha256=fULxaUQe3-YBveYvOP3eKpaWBa2nrKHaNtr02t4kGU0,3547
|
|
5
|
+
media_downloader/version.py,sha256=ZUpZiwSnYHH6PW485NRWDuEN5qgCB8keyTbrdCF9Rmw,118
|
|
6
|
+
media_downloader-0.11.16.dist-info/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
|
7
|
+
media_downloader-0.11.16.dist-info/METADATA,sha256=BsXpOE3APx7ksUZRK8WUAHVflXlp_5D1GLBtD1OcDrk,5522
|
|
8
|
+
media_downloader-0.11.16.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
|
|
9
|
+
media_downloader-0.11.16.dist-info/entry_points.txt,sha256=Hjp1vLkHPq_bABsuh4kpL5nddi1wn9Ftota-72_GgW4,142
|
|
10
|
+
media_downloader-0.11.16.dist-info/top_level.txt,sha256=B2OBmgONOm0hIyx2HJ8qFPOI_p5HOeolrYvmslVC1fc,17
|
|
11
|
+
media_downloader-0.11.16.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=zFjamIgQznRQvRdF98XWgdDeWN3A59dyaugXnHxBZrI,2983
|
|
5
|
-
media_downloader/version.py,sha256=lsEyJBBq_52dtz5_4fHeohUPAKr4Tf_LbF6XI2Xid5s,118
|
|
6
|
-
media_downloader-0.11.14.dist-info/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
|
7
|
-
media_downloader-0.11.14.dist-info/METADATA,sha256=FQ-gxkQ9yOqP-BXqwcZXZl0xDqkYq48elNYmFKPktHk,5523
|
|
8
|
-
media_downloader-0.11.14.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
|
|
9
|
-
media_downloader-0.11.14.dist-info/entry_points.txt,sha256=Hjp1vLkHPq_bABsuh4kpL5nddi1wn9Ftota-72_GgW4,142
|
|
10
|
-
media_downloader-0.11.14.dist-info/top_level.txt,sha256=B2OBmgONOm0hIyx2HJ8qFPOI_p5HOeolrYvmslVC1fc,17
|
|
11
|
-
media_downloader-0.11.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|