-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmainframe.py
More file actions
executable file
·407 lines (325 loc) · 18.5 KB
/
mainframe.py
File metadata and controls
executable file
·407 lines (325 loc) · 18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
def resource_path(relative_path): # a global method to return relative path
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
import os
import threading
import wx
from addmanager import AddManager
from downloadmanager import DownloadManager
from infodialog import VideoInfoDialog
from playlist_manager import PlaylistManager
from playlistdialog import PlaylistDialog
from json_util import JsonUtil
FRAME_WIDTH = 870
FRAME_HEIGHT = 480
BACKGROUND_COLOR = "white"
CONFIGS = "settings.json"
# MainFrame class to handle UI
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, title="YouTube Downloader", size=(FRAME_WIDTH, FRAME_HEIGHT), \
style=wx.DEFAULT_FRAME_STYLE)
self.SetMinSize((FRAME_WIDTH, FRAME_HEIGHT))
self.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_CLOSE, self.__onClose)
panel = wx.Panel(self)
vBox = wx.BoxSizer(wx.VERTICAL)
hBoxes = []
for i in range(5): # 5 boxsizer to place attributes properly
hBoxes.append(wx.BoxSizer(wx.HORIZONTAL))
sourceLabel = wx.StaticText(panel, label="URLs:")
self.__addButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/addButtonIcon.png")), style=wx.NO_BORDER)
self.__addButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickAddButton, self.__addButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanAdd, self.__addButton)
self.__playlistButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/playlistButtonIcon.png")), style=wx.NO_BORDER)
self.__playlistButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickPlaylistButton, self.__playlistButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanAddPlaylist, self.__playlistButton)
addBox = wx.BoxSizer(wx.HORIZONTAL)
addBox.Add(self.__addButton, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=8)
addBox.Add(self.__playlistButton, flag=wx.ALIGN_CENTER_VERTICAL)
# labelGridSizer includes attributes that place on the top
labelGridSizer = wx.GridSizer(cols=3)
labelGridSizer.Add(sourceLabel, 0, wx.ALIGN_LEFT)
labelGridSizer.Add(wx.StaticText(panel, size=(wx.GetDisplaySize().Width, -1)), 0, wx.EXPAND | wx.ALIGN_CENTER)
labelGridSizer.Add(addBox, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT)
hBoxes[0].Add(labelGridSizer, flag=wx.EXPAND)
vBox.Add(hBoxes[0], flag=wx.ALL, border=10)
# text field to input urls
self.__sourceText = wx.TextCtrl(panel, size=(-1, wx.GetDisplaySize().Height), style=wx.TE_MULTILINE)
hBoxes[1].Add(self.__sourceText, proportion=1)
vBox.Add(hBoxes[1], proportion=1, flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=10)
# a button to change download directory
dirBox = wx.BoxSizer(wx.HORIZONTAL)
self.__changeDirButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/changeDirButtonIcon.png")), style=wx.NO_BORDER)
self.__changeDirButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickChangeDirButton, self.__changeDirButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanChangeDir, self.__changeDirButton)
dirBox.Add(self.__changeDirButton, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=8)
defaultDir = ""
# set default download directory
if os.path.exists(CONFIGS): # if the user has already set default directory, read it
conf = JsonUtil(CONFIGS).read()
defaultDir = conf['dir']
if not os.path.exists(defaultDir): # if saved default directory is corrupt, remove it and let user reset it
os.remove(CONFIGS)
os.execl(sys.executable, sys.executable, *sys.argv) # restart this program
else: # otherwise, make the user set default directory
dialog = wx.DirDialog(None)
if dialog.ShowModal() == wx.ID_OK:
defaultDir = dialog.GetPath()
if os.name == "nt": # setting directory for Windows
if not defaultDir.endswith("\\"):
defaultDir += "\\"
else: # for Linux or macOS
if not defaultDir.endswith("/"):
defaultDir += "/"
conf = { 'dir': defaultDir }
JsonUtil(CONFIGS).write(conf)
else: # if the user click cancel, program should be exited
self.Destroy()
dialog.Destroy()
# this text shows currently selected download directory
self.__dirText = wx.TextCtrl(panel, value=defaultDir, size=(300, -1), style=wx.TE_READONLY)
dirBox.Add(self.__dirText)
# a meaningless icon
optBox = wx.BoxSizer(wx.HORIZONTAL)
prefIcon = wx.StaticBitmap(panel, -1, wx.Bitmap(resource_path("images/changePrefIcon.png")))
prefIcon.SetBackgroundColour(BACKGROUND_COLOR)
optBox.Add(prefIcon, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=8)
# a combobox which includes all available stream options that are available on selected video
self.__prefCombobox = wx.ComboBox(panel, size=(200, -1), style=wx.CB_DROPDOWN | wx.TE_READONLY)
self.Bind(wx.EVT_COMBOBOX, self.__onSelectOption, self.__prefCombobox)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanChangeOption, self.__prefCombobox)
optBox.Add(self.__prefCombobox)
# optionGridSizer includes attributes which place on the center
optionGridSizer = wx.GridSizer(cols=3)
optionGridSizer.Add(dirBox, 0, wx.ALIGN_LEFT)
optionGridSizer.Add(wx.StaticText(panel, size=(wx.GetDisplaySize().Width, -1)), 0, wx.EXPAND | wx.ALIGN_CENTER)
optionGridSizer.Add(optBox, 0, wx.ALIGN_RIGHT)
hBoxes[2].Add(optionGridSizer, flag=wx.EXPAND)
vBox.Add(hBoxes[2], flag=wx.LEFT | wx.RIGHT | wx.BOTTOM, border=10)
# a tabled list which includes download list
self.__addedList = wx.ListCtrl(panel, style=wx.LC_REPORT | wx.BORDER_DOUBLE)
cols = [ "제목", "저자", "길이", "옵션", "크기", "속도", "진행률", "남은 시간", "" ] # an empty column not to spoil UI when resizing
columnWidths = [ 230, 80, 70, 180, 70, 85, 60, 70, wx.GetDisplaySize().Width ]
for i in range(len(cols)):
self.__addedList.InsertColumn(i, cols[i], wx.TEXT_ALIGNMENT_RIGHT if i == 0 else wx.TEXT_ALIGNMENT_CENTER)
self.__addedList.SetColumnWidth(i, columnWidths[i])
self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.__onSelectItem, self.__addedList)
hBoxes[3].Add(self.__addedList, flag=wx.EXPAND | wx.LEFT | wx.RIGHT)
vBox.Add(hBoxes[3], flag=wx.LEFT | wx.RIGHT, border=10)
vBox.Add((-1, 10))
# add 6 buttons (start, pause, skip, stop, info, remove)
self.__startButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/startButtonIcon.png")), style=wx.NO_BORDER)
self.__startButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickStartButton, self.__startButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanStart, self.__startButton)
hBoxes[4].Add(self.__startButton, flag=wx.RIGHT, border=12)
self.__pauseButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/pauseButtonIcon.png")), style=wx.NO_BORDER)
self.__pauseButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickPauseButton, self.__pauseButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanPause, self.__pauseButton)
hBoxes[4].Add(self.__pauseButton, flag=wx.RIGHT, border=12)
self.__skipButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/skipButtonIcon.png")), style=wx.NO_BORDER)
self.__skipButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickSkipButton, self.__skipButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanSkip, self.__skipButton)
hBoxes[4].Add(self.__skipButton, flag=wx.RIGHT, border=12)
self.__stopButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/stopButtonIcon.png")), style=wx.NO_BORDER)
self.__stopButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickStopButton, self.__stopButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanStop, self.__stopButton)
hBoxes[4].Add(self.__stopButton, flag=wx.RIGHT, border=12)
self.__infoButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/infoButtonIcon.png")), style=wx.NO_BORDER)
self.__infoButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickInfoButton, self.__infoButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanShowInfo, self.__infoButton)
hBoxes[4].Add(self.__infoButton, flag=wx.RIGHT, border=12)
self.__removeButton = wx.BitmapButton(panel, -1, wx.Bitmap(resource_path("images/removeButtonIcon.png")), style=wx.NO_BORDER)
self.__removeButton.SetBackgroundColour(BACKGROUND_COLOR)
self.Bind(wx.EVT_BUTTON, self.__onClickRemoveButton, self.__removeButton)
self.Bind(wx.EVT_UPDATE_UI, self.__onCheckCanRemove, self.__removeButton)
hBoxes[4].Add(self.__removeButton)
vBox.Add(hBoxes[4], flag=wx.ALIGN_RIGHT | wx.RIGHT, border=10)
vBox.Add((-1, 10))
panel.SetSizer(vBox)
# status bar to show events
self.CreateStatusBar()
self.GetStatusBar().SetBackgroundColour(BACKGROUND_COLOR)
self.SetStatusText("")
self.__downloadList = []
self.__downloading = False
self.__plm = PlaylistManager()
self.__am = None # AddManager for adding urls
self.__dm = None # DownloadManager for downloading videos
self._lock = threading.Lock()
self.Center()
self.Show()
PlaylistDialog(self, self.__plm).autoAddPlaylist()
# stop all threads before force close
def __onClose(self, event):
if self.__am and self.__am.is_alive():
self.__am.stop()
if self.__dm and self.__dm.is_alive():
self.__dm.pause()
self.Destroy()
# UI updater for AddButton
def __onCheckCanAdd(self, event):
event.Enable(not self.__downloading and self.__sourceText.GetValue() != "" and \
(True if self.__am is None else not self.__am.is_alive()))
# UI updater for PlaylistButton
def __onCheckCanAddPlaylist(self, event):
pass # PlaylistButton can always be clickable
# UI updater for ChangeDirButton
def __onCheckCanChangeDir(self, event):
event.Enable(not self.__downloading)
# UI updater for PrefCombobox
def __onCheckCanChangeOption(self, event):
event.Enable(not self.__downloading and self.__addedList.GetSelectedItemCount() == 1)
# UI updater for StartButton
def __onCheckCanStart(self, event):
event.Enable(not self.__downloading and len(self.__downloadList) > 0 and \
self.__am is not None and not self.__am.is_alive())
# UI updater for PauseButton
def __onCheckCanPause(self, event):
event.Enable(self.__downloading)
# UI updater for SkipButton
def __onCheckCanSkip(self, event):
event.Enable(self.__downloading)
# UI updater for StopButton
def __onCheckCanStop(self, event):
event.Enable(self.__downloading and self.__addedList.GetSelectedItemCount() == 1 and \
self.__dm is not None and self.__dm.is_alive() and \
self.__dm.isDownloading(self.__addedList.GetFocusedItem()))
# UI updater for InfoButton
def __onCheckCanShowInfo(self, event):
event.Enable(self.__addedList.GetSelectedItemCount() > 0)
# UI updater for RemoveButton
def __onCheckCanRemove(self, event):
event.Enable(not self.__downloading and self.__addedList.GetSelectedItemCount() > 0 and \
self.__am is not None and not self.__am.is_alive())
# event handler for AddButton
def __onClickAddButton(self, event):
urls = []
for i in range(self.__sourceText.GetNumberOfLines()):
if self.__sourceText.GetLineText(i) != "": # skip blank
urls.append(self.__sourceText.GetLineText(i))
self.__am = AddManager(self, self.__plm, urls, self._lock)
self.__am.start()
self.__sourceText.Clear()
# event handler for PlaylistButton
def __onClickPlaylistButton(self, event):
PlaylistDialog(self, self.__plm).show(self.GetPosition().x + self.GetSize().Width, \
self.GetPosition().y)
# event handler for StartButton
def __onClickStartButton(self, event):
self.__dm = DownloadManager(self, self.__plm, self.__downloadList, self.__dirText.GetValue(), self._lock)
self.__dm.start()
self.__downloading = True
self.__prefCombobox.Clear()
self.SetStatusText("Downloading...")
# event handler for PauseButton
def __onClickPauseButton(self, event):
self.__dm.pause()
self.SetStatusText("Paused.")
self.__downloading = False
# event handler for SkipButton
def __onClickSkipButton(self, event):
self.__dm.skip()
# event handler for StopButton
def __onClickStopButton(self, event):
self.__dm.stop(self.__addedList.GetFirstSelected())
# event handler for InfoButton
def __onClickInfoButton(self, event):
selectedItem = self.__addedList.GetFirstSelected()
x = 0 # x-coordinate to move info dialog if multiple items are selected
y = 0 # y-coordinate to move info dialog if multiple items are selected
while True: # do-while loop to show information dialogs for multiple selected items
VideoInfoDialog(self.__downloadList[selectedItem].video, x, y).show()
selectedItem = self.__addedList.GetNextSelected(selectedItem)
x += 30
y += 30
if selectedItem < 0:
break
# event handler for RemoveButton
def __onClickRemoveButton(self, event):
selectedItem = self.__addedList.GetFirstSelected()
removeList = []
while selectedItem >= 0: # get every index to remove
removeList.append((selectedItem, self.__downloadList[selectedItem]))
selectedItem = self.__addedList.GetNextSelected(selectedItem)
removeList.sort(reverse=True) # sort remove list reversely to remove safely by starting from latest index
for itemTuple in removeList:
self.SetStatusText(itemTuple[1].video.title + " has been removed.")
self.__downloadList.remove(itemTuple[1])
self.__addedList.DeleteItem(itemTuple[0])
# event handler for ChangeDirButton
def __onClickChangeDirButton(self, event):
dialog = wx.DirDialog(None, defaultPath=self.__dirText.GetValue())
if dialog.ShowModal() == wx.ID_OK:
defaultDir = dialog.GetPath()
if os.name == "nt": # setting directory for Windows
self.__dirText.SetValue(defaultDir + "\\" \
if not defaultDir.endswith("\\") else defaultDir)
else: # for Linux or macOS
self.__dirText.SetValue(defaultDir + "/" \
if not defaultDir.endswith("/") else defaultDir)
conf = JsonUtil(CONFIGS).read()
conf['dir'] = self.__dirText.GetValue()
JsonUtil(CONFIGS).write(conf)
dialog.Destroy()
# event handler for selecting an item -> update PrefCombobox list
def __onSelectItem(self, event):
self.__prefCombobox.Clear()
if not self.__downloading and self.__addedList.GetSelectedItemCount() == 1:
selectedItem = self.__downloadList[self.__addedList.GetFocusedItem()]
self.__prefCombobox.AppendItems(selectedItem.options)
self.__prefCombobox.SetValue(selectedItem.selectedExt)
# event handler for selecting an option in PrefCombobox -> update selected item's selected extension
def __onSelectOption(self, event):
selectedItem = self.__addedList.GetFocusedItem()
self.__downloadList[selectedItem].setSelectedExt(self.__prefCombobox.GetStringSelection())
self.__addedList.SetItem(selectedItem, 3, self.__downloadList[selectedItem].selectedExt)
self.__addedList.SetItem(selectedItem, 4, self.__downloadList[selectedItem].filesize)
# add item to download list
def addToDownloadList(self, item): # if video is already in download list, skip it
if item.video.title not in [ d.video.title for d in self.__downloadList ]:
self.__downloadList.append(item)
num_items = self.__addedList.GetItemCount()
self.__addedList.InsertItem(num_items, item.video.title)
self.__addedList.SetItem(num_items, 1, item.video.author)
self.__addedList.SetItem(num_items, 2, item.video.duration)
self.__addedList.SetItem(num_items, 3, item.selectedExt)
self.__addedList.SetItem(num_items, 4, item.filesize)
self.SetStatusText(item.video.title + " has been added.")
else:
self.SetStatusText(item.video.title + " is already in download list.")
# add playlist to download list
def addPlaylist(self, playlist):
self.__am = AddManager(self, self.__plm, playlist, self._lock)
self.__am.start()
# update status of downloading item
def updateStatus(self, item, rate, progress, eta):
selectedItem = self.__downloadList.index(item)
self.__addedList.SetItem(selectedItem, 5, rate)
self.__addedList.SetItem(selectedItem, 6, progress)
self.__addedList.SetItem(selectedItem, 7, eta)
# remove item from list when downloaded
def removeFinishedItem(self, item):
self.__addedList.DeleteItem(self.__downloadList.index(item))
self.__downloadList.remove(item)
# set finished when all videos are downloaded
def setFinished(self):
self.SetStatusText("Finished")
self.__downloading = False
# check it's possible to add playlist
def isAddable(self):
return not self.__downloading and (True if self.__am is None else not self.__am.is_alive())