This repository has been archived by the owner on Aug 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Pneumatic.py
327 lines (261 loc) · 10.9 KB
/
Pneumatic.py
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
# -------------------
# ----- Imports -----
# -------------------
from __future__ import unicode_literals, print_function
from prompt_toolkit import print_formatted_text as print, HTML
from os import system, get_terminal_size
from time import sleep
import youtube_dl as ytdl
from sys import exit
from struct import calcsize
from os.path import dirname, realpath, exists, join, splitext
from shutil import rmtree
from tqdm import tqdm
from requests import get
from io import BytesIO
from zipfile import ZipFile, ZIP_DEFLATED
from re import compile
from readchar import readkey
from tkinter import Tk
from tkinter.filedialog import askdirectory
# -------------------
# ----- Globals -----
# -------------------
# For FFMPEG #
# Evaluates to 64 for x64 architecture, and 32 for x86 architecture.
arch = calcsize("P") * 8
# We derive the architecture in order to then determine which version (32 or 64 bit) of FFMPEG we
# can download.
ffmpeg_filename = f"ffmpeg-latest-win{arch}-static.zip"
ffmpeg_dl_url = f"https://ffmpeg.zeranoe.com/builds/win{arch}/static/{ffmpeg_filename}"
# A global that stores the real directory pointing to the actual python file being run.
current_dir = dirname(realpath(__file__))
# A global that points to where ffmpeg should be if it was downloaded and extracted correctly.
ffmpeg_dir_path = join(current_dir, splitext(ffmpeg_filename)[0])
# Menu options:
# URL/s to MP3
# URL/s to MP4
# Custom CMD
menu_str = [
"",
'<b>`7MM"""Mq. <ansired>mm db </ansired></b>', # noqa
"<b> MM `MM. <ansired>MM </ansired></b>", # noqa
'<b> MM ,M9`7MMpMMMb. .gP"Ya`7MM `7MM <ansired>`7MMpMMMb.pMMMb. ,6"Yb.mmMMmm `7MM ,p6"bo </ansired></b>', # noqa
"<b> MMmmdM9 MM MM ,M' Yb MM MM <ansired>MM MM MM 8) MM MM MM 6M' OO </ansired></b>", # noqa
'<b> MM MM MM 8M"""""" MM MM <ansired>MM MM MM ,pm9MM MM MM 8M </ansired></b>', # noqa
"<b> MM MM MM YM. , MM MM <ansired>MM MM MM 8M MM MM MM YM. ,</ansired></b>", # noqa
"<b>.JMML. .JMML JMML.`Mbmmd' `Mbod\"YML.<ansired>JMML JMML JMML`Moo9^Yo.`Mbmo.JMML.YMbmd' </ansired></b>", # noqa
"",
"<ansiblue>Created by Brittank88 | Inspired by Mahlarian</ansiblue>",
"",
"╭──────────────────────────────────────────────────────╮",
"│ URL >> MP3 URL >> MP4 │",
"├───────────────\\/────────────────────\\/───────────────┤",
"│ Press: 1 Press: 2 │",
"╰──────────────────────────────────────────────────────╯",
" ⁞ <ansired>To quit, press Q / X</ansired> ⁞ ",
" ╰──────────────────────────────────╯ ",
"",
]
# -------------------
# ----- Classes -----
# -------------------
class Logger(object):
def debug(self, msg):
print(HTML(f"<ansibrightgreen>{msg.replace('&','&')}</ansibrightgreen>"))
def warning(self, msg):
print(HTML(f"<ansiyellow>{msg.replace('&','&')}</ansiyellow>"))
def error(self, msg):
print(HTML(f"<ansibrightred>{msg.replace('&','&')}</ansibrightred>"))
# Create an intance of the logger.
logger = Logger()
# ---------------------
# ----- Functions -----
# ---------------------
# Little function to make formatted user input queries easier.
def input_formatted(msg):
print(msg, end="")
return input()
def dl_ffmpeg():
# Checking if the directory exists already.
if exists(ffmpeg_dir_path):
logger.debug(f"{ffmpeg_filename} found. Skipping download.\n")
# Exit the function - there's no point continuing here.
return
else:
logger.warning(f"{ffmpeg_filename} is missing! Downloading & extracting...\n")
# GET request to download the file, in the form of a memory stream we can iterate over.
get_req = get(ffmpeg_dl_url, stream=True)
# The request header tells us how big the file is, which we provide to tqdm to enable its
# predictive features.
total_size = int(get_req.headers["content-length"])
# Create a bytearray object to store the bytes we are downloading in.
zip_bytes = bytearray()
# Creating the loading bar incremented manually as we iterate over byte chunks.
dl_bar = tqdm(total=total_size, unit="iB", unit_scale=True)
# Iterate through 32768 (32 * 1024) byte chunks at a time.
for data in get_req.iter_content(32768):
# Update the bar percentage.
dl_bar.update(len(data))
# Append the new bytes to our bytearray.
zip_bytes.extend(data)
# Parse our completed bytearray as a BytesIO (file-like) object, and extract / close.
with ZipFile(BytesIO(zip_bytes), "r", ZIP_DEFLATED) as zfl:
zfl.extractall(path=current_dir)
zfl.close()
# Close the loading bar instance.
dl_bar.close()
# We want to ensure we downloaded exactly as much as we anticipated
# (content-length from the request header).
if dl_bar.n != total_size:
# Log the error to the console.
logger.error(
"\nSomething went wrong during the download process!\n"
+ f"Expected: {total_size}\n"
+ f"Actual: {dl_bar.n}\n\n"
+ "Please:\n"
+ f"> Download {ffmpeg_filename} from <u>{ffmpeg_dl_url}</u>."
+ f"> Extract {ffmpeg_filename} to the same folder as this application is located.\n"
)
# Delete the directory path, as the files are potentially malformed.
rmtree(ffmpeg_dir_path)
# Allow user to read message before exiting.
input("Press any key to exit.")
# Exit.
exit(-1)
# Credits: http://granitosaurus.rocks/getting-terminal-size.html#making%20it%20work!
def get_cli_size(fallback=(80, 24)):
for i in range(3):
try:
columns, rows = get_terminal_size(i)
except OSError:
continue
break
# Set default if the loop completes which means all failed.
else:
return fallback
return columns, rows
def ytdl_hook(d):
if d["status"] == "finished":
logger.debug(
"\nDownload complete! <b>Please wait for any conversions to finish</b>.\n"
)
def download(opts):
while True:
# Clear screen.
system("cls")
# Get the URL from the user.
url = input_formatted(
HTML(
"""
<b>Paste in your link and hit enter.</b>
Playlists links are supported!
Searching is also supported! Just prefix your query with:
- <b>'ytsearch:'</b>, for <ansired>YouTube</ansired>
- <b>'scsearch:'</b>, for <orange>Soundcloud</orange> (MP3 only!)
<ansired>You can also exit back to the main menu using <b>'Q'</b> or <b>'X'</b>.</ansired>
""" # noqa
)
)
if url.upper() in ("Q", "X"):
break
# Set some opts that should always be set this way. #
# Prefer FFMPEG.
opts["prefer_ffmpeg"] = True
# Points towards the FFMPEG we downloaded.
opts["ffmpeg_location"] = join(ffmpeg_dir_path, "bin")
# Restrict to safe filenames.
opts["restrict_filenames"] = True
# Sets our logger for any information from youtube-dl.
opts["logger"] = logger
# Sets our hook that is called whenever youtube-dl makes any progress downloading a file.
opts["progress_hooks"] = [ytdl_hook]
# Create a root Tkinter window that we will instantly hide.
root = Tk()
# Hide the window.
root.withdraw()
# Ask for a save directory.
opts["outtmpl"] = join(
askdirectory(mustexist=True, initialdir=current_dir), "%(title)s.%(ext)s"
)
# Destroy the hidden root window once we are done with it.
root.destroy()
with ytdl.YoutubeDL(opts) as ydl:
try:
print()
ydl.download([url])
break
except ytdl.utils.DownloadError:
# Wait a little so they can read the above.
sleep(5)
# Reset menu.
continue
def url_mp3():
download(
{
"format": "bestaudio/best",
"postprocessors": [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192",
}
],
}
)
def url_mp4():
download(
{
"format": "bestvideo+bestaudio[ext=m4a]/bestvideo+bestaudio/best",
"merge_output_format": "mp4",
}
)
def close_program():
system("cls")
exit(0)
menu_functions = {"Q": close_program, "X": close_program, "1": url_mp3, "2": url_mp4}
def mainMenuLoop():
while True:
# Clear screen.
system("cls")
# Regex to match against content enclosed in '<>'.
match_tags = compile(r"<.*?>")
# Centre the multiline string line by line.
for line in menu_str:
# Calculate how much length in HTML-like tags we are to account for.
remove_len = len("".join(match_tags.findall(line)))
# Center the line, accounting for the length we just calculated.
print(HTML(line.center(get_cli_size()[0] + remove_len)))
try:
# Little arrow that prefixes the CMD cursor.
print("\t> ", end="")
# Read the key, without requiring an enter press.
usr_in = readkey()
# Lookup the input in our dictionary and call the appropriate command if there is a
# corresponding key.
menu_functions[usr_in.upper()]()
except KeyError:
# Let the user know their input was invalid.
input_quote = f" '{usr_in}'" if usr_in else ""
print(
HTML(
f"<ansired><b>Invalid input{input_quote}. Please try again.</b></ansired>\n"
)
)
# Wait a little so they can read the above.
sleep(2)
# Reset menu.
continue
# -----------------------
# ----- Driver Code -----
# -----------------------
if __name__ == "__main__":
# Clear screen.
system("cls")
# Always check we have FFMPEG on startup.
dl_ffmpeg()
# Wait a second.
sleep(1)
# Display main menu and await input.
# Automatically clears screen.
mainMenuLoop()