FocusedWolf

Python: Youtube video downloader

Jul 14th, 2023 (edited)
1,691
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.39 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. # Version 12
  4.  
  5. # Prevent creation of the __pycache__ directory.
  6. # SOURCE: https://github.com/pytest-dev/pytest/issues/200
  7. import sys
  8. sys.dont_write_bytecode = True
  9.  
  10. # -----
  11.  
  12. import ctypes
  13. import uuid
  14. def known_folder(idstr):
  15.     # Convert the GUID string (e.g. '{374DE...}') into a UUID object,
  16.     # then get its little-endian 16-byte representation as required by Windows.
  17.     fid = uuid.UUID(idstr).bytes_le
  18.  
  19.     # Create a real C array of 16 unsigned bytes initialized with the GUID bytes.
  20.     # This produces a stable block of memory that SHGetKnownFolderPath() can read.
  21.     guid = (ctypes.c_byte * 16)(*fid)
  22.  
  23.     # Prepare a wchar* (wide char pointer) to receive the resulting folder path.
  24.     # SHGetKnownFolderPath will allocate and fill this pointer.
  25.     out = ctypes.c_wchar_p()
  26.  
  27.     # Call the Windows API:
  28.     #   HRESULT SHGetKnownFolderPath(
  29.     #       REFKNOWNFOLDERID rfid,  // pointer to our 16-byte GUID
  30.     #       DWORD dwFlags,          // we pass 0 (default behavior)
  31.     #       HANDLE hToken,          // None = current user
  32.     #       PWSTR *ppszPath         // receives allocated wide-string path
  33.     #   )
  34.     ctypes.windll.shell32.SHGetKnownFolderPath(
  35.         guid,             # pointer to 16-byte GUID array
  36.         0,                # flags = 0
  37.         None,             # user token = current user
  38.         ctypes.byref(out) # address of out pointer to receive the path
  39.     )
  40.  
  41.     # Return the actual string value (Python automatically converts wchar*).
  42.     return out.value
  43.  
  44. # Common Known Folders GUIDs (user profile related only).
  45. KnownFolders = {
  46.     'Desktop':               '{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}',
  47.     'Documents':             '{FDD39AD0-238F-46AF-ADB4-6C85480369C7}',
  48.     'Downloads':             '{374DE290-123F-4565-9164-39C4925E467B}',
  49.     'Music':                 '{4BD8D571-6D19-48D3-BE97-422220080E43}',
  50.     'Pictures':              '{33E28130-4E1E-4676-835A-98395C3BC3BB}',
  51.     'Videos':                '{18989B1D-99B5-455B-841C-AB7C74E4DDFC}',
  52.  
  53.     'Favorites':             '{1777F761-68AD-4D8A-87BD-30B759FA33DD}',
  54.     'Links':                 '{BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968}',
  55.  
  56.     'SavedGames':            '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}',
  57.     'SavedSearches':         '{7D1D3A04-DEBB-4115-95CF-2F29DA2920DA}',
  58.     'Contacts':              '{56784854-C6CB-462B-8169-88E350ACB882}',
  59.     'AppDataLocal':          '{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}',
  60.     'AppDataLocalLow':       '{A520A1A4-1780-4FF6-BD18-167343C5AF16}',
  61.     'AppDataRoaming':        '{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}',
  62.  
  63.     'DesktopDirectory':      '{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}', # same as Desktop
  64.     'Profile':               '{5E6C858F-0E22-4760-9AFE-EA3317B67173}', # C:\Users\<User>
  65.     'Public':                '{DFDF76A2-C82A-4D63-906A-5644AC457385}',
  66.  
  67.     'OneDrive':              '{A52BBA46-E9E1-435f-B3D9-28DAA648C0F6}',
  68.     'OneDriveDocuments':     '{24D89E24-2F19-4534-9DDE-6A6671FBB8FE}',
  69.     'OneDrivePictures':      '{339719B5-8C47-4894-94C2-D8F77ADD44A6}',
  70.     'OneDriveMusic':         '{0077D3A4-6F62-46A1-8F79-6BCB9B93D15B}',
  71. }
  72.  
  73. # -----
  74.  
  75. # Winget adds yt-dlp.exe to PATH so don't need to hard code it here.
  76. YT_DLP_PATH = r'yt-dlp'
  77.  
  78. DOWNLOAD_DIRECTORY = known_folder(KnownFolders['Downloads'])
  79.  
  80. # -----
  81.  
  82. import ctypes
  83. import os
  84. import subprocess
  85. import sys
  86.  
  87. def is_admin():
  88.     try:
  89.         if sys.platform == 'win32':
  90.             return ctypes.windll.shell32.IsUserAnAdmin()
  91.         else:
  92.             return os.geteuid() == 0
  93.     except:
  94.         return False
  95.  
  96. def check_permissions():
  97.     if is_admin():
  98.         return
  99.     print('Requesting administrative privileges . . .')
  100.     try:
  101.         if sys.platform == 'win32':
  102.             params = ' '.join([f'"{arg}"' for arg in sys.argv])
  103.             result = ctypes.windll.shell32.ShellExecuteW(None, 'runas', sys.executable, params, None, 1)
  104.             if result <= 32: # SOURCE: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew If the function succeeds, it returns a value greater than 32.
  105.                 sys.exit(1)
  106.         else:
  107.             try:
  108.                 script = os.path.abspath(sys.argv[0])
  109.                 args = [script] + sys.argv[1:]
  110.                 subprocess.run(['sudo', sys.executable] + args)
  111.             except KeyboardInterrupt:
  112.                 pass
  113.     except Exception as e:
  114.         print(f'An error occurred: {e}')
  115.     sys.exit()
  116.  
  117. def update_yt_dlp():
  118.     # Update yt-dlp and wait for it to finish.
  119.     print('Updating yt-dlp . . .')
  120.     try:
  121.         subprocess.run([YT_DLP_PATH, '--update'], check=True)
  122.     except subprocess.CalledProcessError as e:
  123.         print('ERROR: Failed to update yt-dlp.', e)
  124.         check_permissions() # Restart script and try again with administrator privileges.
  125.  
  126.         # If script is already running as administrator.
  127.         wait_for_any_keypress()
  128.         sys.exit(1)
  129.  
  130. def get_video_url():
  131.     url = None
  132.     while True:
  133.         try:
  134.             url = input('Youtube URL: ')
  135.             if not str_is_none_or_whitespace(url):
  136.                break
  137.             clear()
  138.         except ValueError as e:
  139.             print(e)
  140.     return url
  141.  
  142. def str_is_none_or_whitespace(text):
  143.     return not text or text.isspace()
  144.  
  145. import os
  146. def clear():
  147.     os.system('cls' if os.name in ('nt', 'dos') else 'clear')
  148.  
  149. import subprocess
  150. def main():
  151.     update_yt_dlp()
  152.     print()
  153.  
  154.     print('Download Directory:', DOWNLOAD_DIRECTORY)
  155.     print()
  156.  
  157.     url = get_video_url()
  158.     print()
  159.  
  160.     subprocess.run([YT_DLP_PATH, '--list-formats', url])
  161.  
  162.     cmd = [
  163.             YT_DLP_PATH,
  164.              '-f', 'bestvideo+bestaudio', # yt-dlp chooses what it thinks is the "best" combination (sometimes not the absolute largest resolution).
  165.             #'-f', 'bv*+ba*', # explicitly forces maximum resolution video and best audio, no matter what yt-dlp's ranking says.
  166.  
  167.             #'-f', 'bestvideo[vcodec=vp9]+bestaudio', # yt-dlp assumes AV1 is "better" because it's newer and more efficient, but if your goal is maximum resolution + maximum bitrate, you need to override its preference. (VP9) → larger file, higher bitrate, potentially better quality if you want maximum fidelity.
  168.             #'--merge-output-format', 'mp4', # re-encoding may cause quality loss.
  169.  
  170.             '--output', '%(title)s-%(id)s.%(ext)s',
  171.             '--verbose',
  172.         ]
  173.  
  174.     #  cookies_path = os.path.join(os.getcwd(), 'cookies.txt')
  175.     #  if os.path.exists(cookies_path):
  176.     #      cmd += ['--cookies', cookies_path]
  177.     #      print(f'Loading cookies: {cookies_path}')
  178.     #  else:
  179.     #      print('WARNING: cookies.txt not found in the current directory. Proceeding without cookies.')
  180.  
  181.     # SOURCE: https://github.com/yt-dlp/yt-dlp/wiki/FAQ
  182.     # Firefox/Librewolf: User agent from about:support
  183.     # Brave: User agent from brave://version
  184.     # Chrome: User agent from chrome://version
  185.     cmd += ['--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0']
  186.  
  187.     # SOURCE: https://github.com/yt-dlp/yt-dlp/issues/14050
  188.     if os.name in ('nt', 'dos'):
  189.         cmd += ['--cookies-from-browser', 'firefox:%APPDATA%/librewolf']
  190.     else:
  191.         # TODO: Untested
  192.         cmd += ['--cookies-from-browser', 'firefox:${HOME}/.librewolf/']
  193.         #cmd += ['--cookies-from-browser', 'firefox:./.librewolf/']
  194.  
  195.     cmd.append(url)
  196.     try:
  197.         subprocess.run(cmd, cwd=DOWNLOAD_DIRECTORY, check=True)
  198.     except subprocess.CalledProcessError as e:
  199.         print('ERROR: Failed to download video from url:', url)
  200.         wait_for_any_keypress()
  201.         sys.exit(1)
  202.  
  203.     print('Download complete . . .')
  204.     print()
  205.  
  206.     wait_for_any_keypress()
  207.  
  208. # ----- Press any key to continue -----
  209.  
  210. def wait_for_any_keypress():
  211.     import sys
  212.     if sys.platform == 'win32':
  213.         import os
  214.         os.system('pause')
  215.     elif sys.platform.startswith('linux') or sys.platform == 'darwin':
  216.         print('Press any key to continue . . .')
  217.         import termios
  218.         import tty
  219.         stdin_file_desc = sys.stdin.fileno()
  220.         old_stdin_tty_attr = termios.tcgetattr(stdin_file_desc)
  221.         try:
  222.             tty.setraw(stdin_file_desc)
  223.             sys.stdin.read(1)
  224.         finally:
  225.             termios.tcsetattr(stdin_file_desc, termios.TCSADRAIN, old_stdin_tty_attr)
  226.  
  227. if __name__ == '__main__':
  228.     main()
Advertisement
Add Comment
Please, Sign In to add comment