Skip to content

Commit 5d26570

Browse files
authored
Merge pull request #8 from jslay88/proxy_support
Finalized proxy support and added pyinstaller spec file
2 parents 7de94e7 + 2eb2121 commit 5d26570

File tree

10 files changed

+230
-38
lines changed

10 files changed

+230
-38
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ MANIFEST
3030
# before PyInstaller builds the exe, so as to inject date/other infos into it.
3131
*.manifest
3232
*.spec
33+
*.exe
34+
!pyinstaller.spec
3335

3436
# Installer logs
3537
pip-log.txt
@@ -107,4 +109,5 @@ venv.bak/
107109
.idea/
108110

109111
# Downloaded video/audio
110-
*.mp4
112+
*.mp4
113+
*.webm

README.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# videodownloader
2-
A Simple Script writtten in Python to Download YouTube Videos
2+
A Simple Script written in Python to Download YouTube Videos
33
These videos can also be downloaded as audio
44

55
### Installation
@@ -21,21 +21,28 @@ or use the command line arguments on the script (`script.py`).
2121

2222
python3 gui.py
2323
python3 script.py
24+
python3 script.py -u rfscVS0vtbw -l
2425
python3 script.py -u rfscVS0vtbw -o videos/ -f my_downloaded_video
26+
python3 script.py -u rfscVS0vtbw -i 278 -o videos/ -f my_downloaded_itag_video
2527
python3 script.py -u rfscVS0vtbw -o audio/ -f my_downloaded_audio --audio-only
2628
python3 script.py --help
2729

2830
-u --url YouTube URL or YouTube Video ID to download
31+
-l --list-streams List available streams for this YouTube Video
32+
instead of download. Use -a/--audio-only to list audio streams.
33+
Download specific stream with the
34+
itag ID and -i/--itag argument.
35+
-i --itag Stream ITAG to download for given YouTube Video/ID.
36+
List streams with -l/--list-streams argument.
37+
If ITAG is not provided, default stream will be downloaded.
38+
Downloading with ITAG ignores -a/--audio-only.
2939
-o --output-path Output Directory Path
3040
-f --filename Override the output filename. Does not override file extension
41+
-p --proxy Proxy to use. Ex http://xxx.xxx.xxx:8080. NOTE: You need https proxy for https URL!
3142
-a --audio-only Download Audio Only
3243

3344

34-
35-
If you would like to suggest changes feel free to fork this repo and create PR or submit an issue
36-
37-
Feel Free to DM on twitter if you have any questions
38-
[twitter](http://www.twitter.com/muhammad_o7)
45+
**NOTE:** If you are using a proxy, you need https proxy for https URL!
3946

4047
![GUI Screenshot](assets/gui_screenshot.png)![GUI Screenshot Linux](assets/gui_screenshot_linux.png)
4148

@@ -45,3 +52,18 @@ Here's the demo on how to use it
4552
<a href="https://vimeo.com/281200561"><img width="60%" src="assets/img.png" alt="view demo"></a><br>
4653
</div>
4754

55+
### Building Windows EXE
56+
57+
If you want to build the Windows EXE manually, make sure you have pywin32 installed,
58+
as well as pyinstaller. Then run the following command from the script directory.
59+
60+
pyinstaller --clean --onefile pyinstaller.spec
61+
62+
The EXE will be put into the `dist` directory within the script directory.
63+
64+
### Contributing
65+
66+
If you would like to suggest changes feel free to fork this repo and create PR or submit an issue
67+
68+
Feel Free to DM on twitter if you have any questions
69+
[twitter](http://www.twitter.com/muhammad_o7)

assets/gui_screenshot.png

10.7 KB
Loading

assets/gui_screenshot_linux.png

35.5 KB
Loading

assets/ytdl.ico

66.1 KB
Binary file not shown.

download_youtube_video.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from os import makedirs
22
from pytube import YouTube
3+
from pytube.helpers import safe_filename
34

45

5-
def download_youtube_video(url, audio_only=False, output_path=None,
6+
def download_youtube_video(url, itag=None, audio_only=False, output_path=None,
67
filename=None, filename_prefix=None,
7-
proxies=None):
8+
proxies=None, progress_callback=None):
89
"""
910
Download a YouTube Video.
1011
:param url: Full URL to YouTube Video or YouTube Video ID
1112
:type url: str
13+
:param itag: YouTube Stream ITAG to Download
14+
:type itag: int
1215
:param audio_only: Download only the audio for the video. Takes longer than video.
1316
:type audio_only: bool
1417
:param output_path: Path to folder to output file.
@@ -24,18 +27,24 @@ def download_youtube_video(url, audio_only=False, output_path=None,
2427
"""
2528
if output_path:
2629
makedirs(output_path, exist_ok=True)
27-
if 'https' not in url:
30+
if 'http' not in url:
2831
url = 'https://www.youtube.com/watch?v=%s' % url
2932
if proxies:
30-
print(proxies)
3133
video = YouTube(url, proxies=proxies)
3234
else:
3335
video = YouTube(url)
34-
if audio_only:
35-
stream = video.streams.filter(only_audio=True).first()
36+
if progress_callback:
37+
video.register_on_progress_callback(progress_callback)
38+
if itag:
39+
itag = int(itag)
40+
stream = video.streams.get_by_itag(itag)
3641
else:
37-
stream = video.streams.first()
42+
stream = video.streams.filter(only_audio=audio_only).first()
3843
print('Download Started: %s' % video.title)
44+
if filename:
45+
filename = safe_filename(filename)
3946
stream.download(output_path=output_path, filename=filename)
40-
print('Download Complete: %s' % video.title)
41-
return video.title + '.mp4' if filename is None else filename + '.mp4'
47+
file_type = '.' + stream.mime_type.split('/')[1]
48+
filename = stream.default_filename if filename is None else filename + file_type
49+
print('Download Complete! Saved to file: %s' % filename)
50+
return filename

gui.py

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import os.path
44

55

6+
from pytube import YouTube
67
from threading import Thread
7-
from tkinter import filedialog, messagebox
8+
from tkinter import filedialog, messagebox, ttk
89
from download_youtube_video import download_youtube_video
910
from pytube.exceptions import PytubeError, RegexMatchError
1011

@@ -14,8 +15,10 @@ class YouTubeDownloadGUI(tk.Frame):
1415
def __init__(self, master=None):
1516
super().__init__(master)
1617
self.pack()
18+
self.label_video_title = None
1719
self.btn_download = None
1820
self.btn_output_browse = None
21+
self.btn_check_id = None
1922
self.text_url = None
2023
self.text_output_path = None
2124
self.text_filename_override = None
@@ -25,15 +28,25 @@ def __init__(self, master=None):
2528
self.output_path = tk.StringVar(self)
2629
self.filename_override = tk.StringVar(self)
2730
self.proxy = tk.StringVar(self)
31+
self.video = None
32+
self.stream = tk.IntVar(self)
33+
self.streams = []
34+
self.stream_widgets = []
35+
self.file_size = 0
36+
self.progress_bar = None
2837

29-
self.quit = None
38+
self.last_row = 0
3039

3140
self.create_widgets()
3241

3342
def create_widgets(self):
3443
tk.Label(self, text='YouTube URL/ID').grid(row=0, column=0)
3544
self.text_url = tk.Entry(self, width=60)
3645
self.text_url.grid(row=0, column=1, columnspan=2)
46+
self.btn_check_id = tk.Button(self)
47+
self.btn_check_id['text'] = 'Check Video'
48+
self.btn_check_id['command'] = self.check_video
49+
self.btn_check_id.grid(row=0, column=3)
3750

3851
tk.Label(self, text='Output Directory').grid(row=1, column=0)
3952
self.text_output_path = tk.Entry(self, width=60, textvariable=self.output_path)
@@ -51,43 +64,107 @@ def create_widgets(self):
5164
self.text_proxy = tk.Entry(self, width=60, textvariable=self.proxy)
5265
self.text_proxy.grid(row=3, column=1, columnspan=2)
5366

54-
self.radio_video_audio.append(tk.Radiobutton(self, text='Video', variable=self.audio_only, value=False))
67+
tk.Label(self, text='Media Type').grid(row=4, column=0)
68+
self.radio_video_audio.append(tk.Radiobutton(self, text='Video', variable=self.audio_only,
69+
value=False, command=self.check_video))
5570
self.radio_video_audio.append(tk.Radiobutton(self, text='Audio (Takes Longer)', variable=self.audio_only,
56-
value=True))
57-
71+
value=True, command=self.check_video))
5872
self.radio_video_audio[0].grid(row=4, column=1)
5973
self.radio_video_audio[1].grid(row=4, column=2)
6074

61-
self.btn_download = tk.Button(self)
62-
self.btn_download['text'] = 'Download'
63-
self.btn_download['command'] = self.download
64-
self.btn_download.grid(row=5, column=1, columnspan=2)
65-
66-
self.quit = tk.Button(self, text="QUIT", fg="red",
67-
command=root.destroy)
68-
self.quit.grid(row=6, column=1, columnspan=2)
75+
self.label_video_title = tk.Label(self)
76+
self.label_video_title.grid(row=5, column=0, columnspan=4)
77+
self.last_row = 5
6978

7079
def browse_output_path(self):
7180
self.output_path.set(filedialog.askdirectory(initialdir='/', title='Select Output Folder'))
72-
self.text_output_path.delete(0, tk.END)
73-
self.text_output_path.insert(0, self.output_path.get())
81+
82+
def check_video(self):
83+
self.btn_check_id['text'] = 'Checking...'
84+
self.btn_check_id.config(state=tk.DISABLED)
85+
Thread(target=self.threaded_check_video).start()
86+
87+
def threaded_check_video(self):
88+
self.last_row = 5
89+
self.stream.set(0)
90+
[radio_button.destroy() for radio_button in self.stream_widgets]
91+
if self.btn_download:
92+
self.progress_bar.destroy()
93+
self.btn_download.destroy()
94+
url = self.text_url.get()
95+
if 'https' not in url:
96+
url = 'https://www.youtube.com/watch?v=%s' % url
97+
try:
98+
if self.proxy.get() != '':
99+
self.video = YouTube(url, proxies={self.proxy.get().split(':')[0]: self.proxy.get()})
100+
else:
101+
self.video = YouTube(url)
102+
self.label_video_title['text'] = self.video.title
103+
self.streams = self.video.streams.filter(only_audio=self.audio_only.get()).all()
104+
105+
for stream in self.streams:
106+
if self.audio_only.get():
107+
text = f'Codec: {stream.audio_codec}, ' \
108+
f'ABR: {stream.abr} ' \
109+
f'File Type: {stream.mime_type.split("/")[1]}, Size: {stream.filesize // 1024} KB'
110+
else:
111+
if stream.video_codec is None:
112+
continue
113+
text = f'Res: {stream.resolution}, FPS: {stream.fps},' \
114+
f' Video Codec: {stream.video_codec}, Audio Codec: {stream.audio_codec}, ' \
115+
f'File Type: {stream.mime_type.split("/")[1]}, Size: {stream.filesize // 1024} KB'
116+
radio_button = tk.Radiobutton(self, text=text, variable=self.stream, value=stream.itag)
117+
self.last_row += 1
118+
radio_button.grid(row=self.last_row, column=0, columnspan=4)
119+
self.stream_widgets.append(radio_button)
120+
self.last_row += 1
121+
self.progress_bar = ttk.Progressbar(self, orient='horizontal', length=350, mode='determinate')
122+
self.progress_bar.grid(row=self.last_row, column=1, columnspan=2)
123+
124+
self.progress_bar['value'] = 0
125+
self.progress_bar['maximum'] = 100
126+
127+
self.last_row += 1
128+
self.btn_download = tk.Button(self)
129+
self.btn_download['text'] = 'Download'
130+
self.btn_download['command'] = self.download
131+
self.btn_download.config(state=tk.NORMAL)
132+
self.btn_download.grid(row=self.last_row, column=1, columnspan=2)
133+
except PytubeError as e:
134+
messagebox.showerror('Something went wrong...', e)
135+
except RegexMatchError as e:
136+
messagebox.showerror('Something went wrong...', e)
137+
finally:
138+
self.btn_check_id['text'] = 'Check Video'
139+
self.btn_check_id.config(state=tk.NORMAL)
74140

75141
def download(self):
76142
self.btn_download['text'] = 'Downloading...'
77143
self.btn_download.config(state=tk.DISABLED)
144+
self.btn_check_id.config(state=tk.DISABLED)
145+
self.btn_output_browse.config(state=tk.DISABLED)
146+
[radio_button.config(state=tk.DISABLED) for radio_button in self.radio_video_audio]
78147
Thread(target=self.threaded_download).start()
79148

149+
def update_progress_bar(self, stream, chunk, file_handle, bytes_remaining):
150+
percentage = ((self.file_size - bytes_remaining) / self.file_size) * 100
151+
self.progress_bar['value'] = percentage
152+
80153
def threaded_download(self):
81154
try:
82155
if self.proxy.get() != '':
83156
proxy = {self.proxy.get().split(':')[0]: self.proxy.get()}
84157
else:
85158
proxy = None
86-
filename = download_youtube_video(self.text_url.get(), audio_only=self.audio_only.get(),
159+
for search_stream in self.streams:
160+
if int(search_stream.itag) == int(self.stream.get()):
161+
self.file_size = search_stream.filesize
162+
break
163+
filename = download_youtube_video(self.text_url.get(), itag=self.stream.get(),
87164
output_path=self.output_path.get(),
88165
filename=self.filename_override.get()
89166
if self.filename_override.get() != '' else None,
90-
proxies=proxy)
167+
proxies=proxy, progress_callback=self.update_progress_bar)
91168
messagebox.showinfo('Download Complete!', 'Download Complete!\n%s' % filename)
92169
except PytubeError as e:
93170
messagebox.showerror('Something went wrong...', e)
@@ -100,11 +177,21 @@ def threaded_download(self):
100177
finally:
101178
self.btn_download['text'] = 'Download'
102179
self.btn_download.config(state=tk.NORMAL)
180+
self.btn_check_id.config(state=tk.NORMAL)
181+
self.btn_output_browse.config(state=tk.NORMAL)
182+
[radio_button.config(state=tk.NORMAL) for radio_button in self.radio_video_audio]
183+
184+
185+
def resource_path(relative_path):
186+
import sys
187+
if hasattr(sys, '_MEIPASS'):
188+
return os.path.join(sys._MEIPASS, relative_path)
189+
return os.path.join(os.path.abspath('.'), relative_path)
103190

104191

105192
if __name__ == '__main__':
106193
root = tk.Tk()
107194
app = YouTubeDownloadGUI(master=root)
108195
app.master.title('YouTube Video/Audio Downloader')
109-
app.master.tk.call('wm', 'iconphoto', app.master._w, tk.PhotoImage(file=os.path.abspath('assets/ytdl.png')))
196+
app.master.tk.call('wm', 'iconphoto', app.master._w, tk.PhotoImage(file=resource_path('assets/ytdl.png')))
110197
app.mainloop()

pyinstaller.spec

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
block_cipher = None
2+
3+
a = Analysis(['gui.py'],
4+
binaries=None,
5+
datas=None,
6+
hiddenimports=[],
7+
hookspath=None,
8+
runtime_hooks=None,
9+
excludes=None,
10+
win_no_prefer_redirects=None,
11+
win_private_assemblies=None,
12+
cipher=block_cipher)
13+
14+
a.datas += [('assets\\ytdl.png', '.\\assets\\ytdl.png', 'DATA')]
15+
16+
pyz = PYZ(a.pure, a.zipped_data,
17+
cipher=block_cipher)
18+
exe = EXE(pyz,
19+
a.scripts,
20+
a.binaries,
21+
a.zipfiles,
22+
a.datas,
23+
a.binaries,
24+
name='YouTube Video & Audio Downloader',
25+
debug=False,
26+
strip=None,
27+
upx=True,
28+
console=False,
29+
icon='assets\\ytdl.ico')

0 commit comments

Comments
 (0)