From 6743e7825c65f6c29a69e497ffcac561ec184ced Mon Sep 17 00:00:00 2001 From: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:57:02 -0400 Subject: [PATCH] Squashed commit of the following: commit b9393a3a54dab1a4c9e653d3eb100a0d90899bae Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 15:54:51 2024 -0400 its scuffed but it works sometimes to get albums commit 4a5e72fe29fc376f6fa6827ca5a215435353d570 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 14:52:25 2024 -0400 add album and more accurate title/artist metadata commit 6251508a88fd41a43a948e66f73f694ca44d121b Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 14:41:38 2024 -0400 abstract the ffmpeg command commit 986f0fc5010c6a8c60b680aad1c37afce73a7f38 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 14:23:46 2024 -0400 force works now with -av commit 6fd1d7aadec20c07da9bc1cea959f10c1f9e76fe Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 14:13:21 2024 -0400 -d works commit bbba16eeff81209b49cca5bd68e5952305b985f4 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 14:02:50 2024 -0400 clean stuff up commit d97e9ab4f76e41406e51faad9ac833bbac672635 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 14:00:18 2024 -0400 `-av' works now commit 4276e1f90b0604053e9e1e6a8876c65fd9a8b5c9 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 13:33:45 2024 -0400 add force commit 4cc58b49c9ed114094f2efdc80528eef81fa5748 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 13:03:45 2024 -0400 add todo list commit a70f70c9f66ee1779c9420ba17bc2ab1129ab466 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 12:54:58 2024 -0400 thumb + cover art works commit bd659cfa83e6ec652f5f1e7cde36e18f1cc9475c Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 01:07:00 2024 -0400 stuff commit f417ba4e116e4086ef043861ff99b815170d8993 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 00:38:13 2024 -0400 Update .gitignore commit e844629839d17f7dc1a8758856ab6c401b2a7ebe Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon Oct 14 00:37:25 2024 -0400 fix file names with bad chars commit dc884c0826329c35c92541ee283eaf2ce0a663ef Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 23:33:26 2024 -0400 thing for everything commit 0681ff947765e360b8b750404e155eb7efcc787e Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 23:27:56 2024 -0400 thing i gpted commit a249ef27331f88b1631e51e2736decb0d2babdb3 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 23:07:02 2024 -0400 add fix for downloading youtu.be links commit a1e21bd040c2fd8a49de31718c3dd61bf674868c Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 23:05:01 2024 -0400 make things more efficient commit 708109426e3371595bd24ebd20e8f7bf29bf5baf Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 22:56:51 2024 -0400 remove the download folder commit 6cdea0ca8d654a2282b4dba3ce768eed34c54c46 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 22:55:04 2024 -0400 add print statements commit 1537a440bf6873a70c06c3527bc2d69c4c9b8bc9 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 22:51:16 2024 -0400 add views commit c70357473cdfba560b71e7f494c6ede2abdd6ab1 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 22:35:25 2024 -0400 Update .gitignore commit 6d1ae576d8c62ff4dbfe4001dc857457e5f817cb Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 22:34:55 2024 -0400 it works now commit c23523f7d8b5dd4ba9bfe9af1b0a33d1fefe252d Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 21:48:57 2024 -0400 stuff commit a08a41c625656dfe26ab664503ffd2146fa6a174 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 19:24:18 2024 -0400 Update .gitignore commit 1dae0861912de5dbfa22a45801f50135b5786fd4 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 19:24:07 2024 -0400 Delete main.py commit 77eb2a1aa7c9860ab227079ee52f367b0cbf2817 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 19:24:01 2024 -0400 Update .gitignore commit 3afc93e7a87787eb6e6e490bfac47c313764e317 Author: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Sun Oct 13 19:23:45 2024 -0400 Update .gitignore commit d0d5b91b2e41da8141a3e882f8765314a788c14c Author: Skullheadx Date: Sun Oct 13 19:19:18 2024 -0400 everything --- .gitignore | 16 +++ README.md | 20 ++- dependencies.txt | 2 - install.sh | 1 + links.txt | 26 ---- main.py | 87 ------------ requirements.txt | 3 + setup.py | 10 ++ ytdl.egg-info/PKG-INFO | 4 + ytdl.egg-info/SOURCES.txt | 11 ++ ytdl.egg-info/dependency_links.txt | 1 + ytdl.egg-info/entry_points.txt | 2 + ytdl.egg-info/top_level.txt | 1 + ytdl/__init__.py | 1 + ytdl/__main__.py | 39 ++++++ ytdl/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 142 bytes ytdl/__pycache__/__main__.cpython-312.pyc | Bin 0 -> 1904 bytes ytdl/__pycache__/classmodule.cpython-312.pyc | Bin 0 -> 719 bytes ytdl/__pycache__/funcmodule.cpython-312.pyc | Bin 0 -> 7533 bytes ytdl/funcmodule.py | 137 +++++++++++++++++++ 20 files changed, 240 insertions(+), 121 deletions(-) delete mode 100644 dependencies.txt create mode 100644 install.sh delete mode 100644 links.txt delete mode 100644 main.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 ytdl.egg-info/PKG-INFO create mode 100644 ytdl.egg-info/SOURCES.txt create mode 100644 ytdl.egg-info/dependency_links.txt create mode 100644 ytdl.egg-info/entry_points.txt create mode 100644 ytdl.egg-info/top_level.txt create mode 100644 ytdl/__init__.py create mode 100644 ytdl/__main__.py create mode 100644 ytdl/__pycache__/__init__.cpython-312.pyc create mode 100644 ytdl/__pycache__/__main__.cpython-312.pyc create mode 100644 ytdl/__pycache__/classmodule.cpython-312.pyc create mode 100644 ytdl/__pycache__/funcmodule.cpython-312.pyc create mode 100644 ytdl/funcmodule.py diff --git a/.gitignore b/.gitignore index 5207898..1c115a2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,19 @@ links.txt /.venv .DS_Store links.txt +*.egg-info/ +*.pyc + +ytdl.egg-info/dependency_links.txt +ytdl.egg-info/dependency_links.txt +ytdl.egg-info/entry_points.txt +ytdl.egg-info/SOURCES.txt +ytdl.egg-info/top_level.txt +ytdl/__main__.py +ytdl/__pycache__/__init__.cpython-312.pyc +ytdl/__pycache__/__main__.cpython-312.pyc +ytdl/__pycache__/classmodule.cpython-312.pyc +ytdl/__pycache__/funcmodule.cpython-312.pyc +/ytdl.egg-info +/ytdl/__pycache__ +*.mp4 diff --git a/README.md b/README.md index 76e3731..8f6da0b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ -# youtube-downloader +# Youtube Downloader - ytdl -HOW TO USE -- Enter links into the links_file.txt file each on a new line. - - You can even put links to playlists too! -- Run the main.py file using python -- Enjoy your newly downloaded videos in the "downloaded" folder. (audio and video streams are available in the respective folders in downloaded) +## usage +```shell +ytdl "https://www.youtube.com/watch?v=dQw4w9WgXcQ" +``` +downloads the audio and video and stitches it together in the current directory. Automatically detects playlists. + +- `-a` - audio only +- `-v` - video only +- `-av` - audio + video separate +- `-f` - force replace if file exists + +# TODO: +- [ ] figure out why -av takes so long compared to -a and -v \ No newline at end of file diff --git a/dependencies.txt b/dependencies.txt deleted file mode 100644 index 2fd322a..0000000 --- a/dependencies.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytube -ffmpeg-python \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..7895f27 --- /dev/null +++ b/install.sh @@ -0,0 +1 @@ +pip install -e . \ No newline at end of file diff --git a/links.txt b/links.txt deleted file mode 100644 index b00161d..0000000 --- a/links.txt +++ /dev/null @@ -1,26 +0,0 @@ -https://www.youtube.com/watch?app=desktop&v=lW4KseyDqcY -https://www.youtube.com/watch?v=nJ7ZCN0m14A -https://www.youtube.com/watch?app=desktop&v=VvWX3vRRLME -https://www.youtube.com/watch?v=iCx2nBfu54I -https://www.youtube.com/watch?app=desktop&v=V2hrTDS4Ml4 -https://www.youtube.com/watch?app=desktop&v=_Sd11FWbvZ8 -https://www.youtube.com/watch?v=HDOIQZZoABg -https://www.youtube.com/watch?v=j-ttaqEzXKM -https://www.youtube.com/watch?app=desktop&v=UMiW3G1USHg#dialog -https://www.youtube.com/watch?v=uPG77Gtn0Ws -https://www.youtube.com/watch?app=desktop&v=7cKGmeTY9eg -https://www.youtube.com/watch?app=desktop&v=Pfs9yAsRbaU -https://www.youtube.com/watch?v=WxKbU98GLRY -https://www.youtube.com/watch?v=9MzCxt1QpWg -https://www.youtube.com/watch?v=jCaug9SkKEI -https://www.youtube.com/watch?v=V19v3oNPixQ -https://www.youtube.com/watch?v=NscXXbmAggI -https://www.youtube.com/watch?v=iCx2nBfu54I -https://www.youtube.com/watch?v=9pq-G57iSEQ -https://www.youtube.com/watch?v=XVRno3Y1TX8 -https://www.youtube.com/watch?v=codyY_-AiXc -https://www.youtube.com/watch?v=qZ2yWvholHw -https://www.youtube.com/watch?v=wodcSTIxZtw -https://www.youtube.com/watch?v=btKTE4eMPf4 -https://www.youtube.com/watch?v=kW-WE5zrJsg -https://www.youtube.com/watch?v=i5M-WHDhQ4c diff --git a/main.py b/main.py deleted file mode 100644 index 5f5b3a1..0000000 --- a/main.py +++ /dev/null @@ -1,87 +0,0 @@ -from pytubefix import YouTube, Playlist -from pytubefix.exceptions import VideoUnavailable -import ffmpeg - - -RES = ["1440p", "1080p", "720p", "480p", "360p", "240p", "144p"] -ABR = ["160kbps", "128kbps", "70kbps", "50kbps", "48kbps"] -target_res = 0 -target_abr=0 - -failed_download = set() - -if __name__ == "__main__": - - # get list of links from file - links = [] - with open('links.txt', 'r') as f: - links = f.read().split('\n') - if links[-1] == "": - links = links[:-1] - - for link in links: - if "playlist" in link: - p = Playlist(link) - for url in p.video_urls: - links.append(url) - links.remove(link) - - - # download links one by one - for link in links: - target_res = 0 - target_abr = 0 - video_success = True - audio_success = True - - try: - yt = YouTube(link) - yt.streams - - except VideoUnavailable: - print(f'Video {link} is unavaialable, skipping.') - failed_download.add(((yt.title, link))) - else: - video_streams = [] - while len(video_streams) == 0: - video_streams = yt.streams.filter(file_extension='mp4', res=RES[target_res]) # find available streams - if target_res + 1 < len(RES): - target_res = target_res + 1 - else: - video_success = False - break - if not video_success: - print(f"Unable to find video stream for {yt.title}") - failed_download.add(((yt.title, link))) - - break - vstream = video_streams[0] - - # audio - audio_streams = [] - while len(audio_streams) == 0: - audio_streams = yt.streams.filter(only_audio=True, abr=ABR[target_abr]) # find available streams - - if target_abr + 1 < len(RES): - target_abr = target_abr + 1 - else: - audio_success = False - break - if not audio_success: - print(f"Unable to find audio stream for {yt.title}") - failed_download.add(((yt.title, link))) - - break - astream = audio_streams[0] - - vstream.download(output_path="downloaded/video_only") - astream.download(output_path="downloaded/audio_only") - - input_video = ffmpeg.input(f"downloaded/video_only/{vstream.default_filename}") - input_audio = ffmpeg.input(f"downloaded/audio_only/{astream.default_filename}") - - ffmpeg.concat(input_video, input_audio, v=1, a=1).output(f'downloaded/{yt.title}.mp4').run() - -print("Failed Downloading:") -print(failed_download) - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e287554 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pytubefix~=8.0.0 +requests~=2.32.3 +ffmpeg \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b8c2199 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup +setup( + name = 'ytdl', + version = '0.1.0', + packages = ['ytdl'], + entry_points = { + 'console_scripts': [ + 'ytdl = ytdl.__main__:main' + ] + }) \ No newline at end of file diff --git a/ytdl.egg-info/PKG-INFO b/ytdl.egg-info/PKG-INFO new file mode 100644 index 0000000..d54d315 --- /dev/null +++ b/ytdl.egg-info/PKG-INFO @@ -0,0 +1,4 @@ +Metadata-Version: 2.1 +Name: ytdl +Version: 0.1.0 +License-File: LICENSE diff --git a/ytdl.egg-info/SOURCES.txt b/ytdl.egg-info/SOURCES.txt new file mode 100644 index 0000000..3a3b6f7 --- /dev/null +++ b/ytdl.egg-info/SOURCES.txt @@ -0,0 +1,11 @@ +LICENSE +README.md +setup.py +ytdl/__init__.py +ytdl/__main__.py +ytdl/funcmodule.py +ytdl.egg-info/PKG-INFO +ytdl.egg-info/SOURCES.txt +ytdl.egg-info/dependency_links.txt +ytdl.egg-info/entry_points.txt +ytdl.egg-info/top_level.txt \ No newline at end of file diff --git a/ytdl.egg-info/dependency_links.txt b/ytdl.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ytdl.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/ytdl.egg-info/entry_points.txt b/ytdl.egg-info/entry_points.txt new file mode 100644 index 0000000..ab2a57f --- /dev/null +++ b/ytdl.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +ytdl = ytdl.__main__:main diff --git a/ytdl.egg-info/top_level.txt b/ytdl.egg-info/top_level.txt new file mode 100644 index 0000000..763acad --- /dev/null +++ b/ytdl.egg-info/top_level.txt @@ -0,0 +1 @@ +ytdl diff --git a/ytdl/__init__.py b/ytdl/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ytdl/__init__.py @@ -0,0 +1 @@ + diff --git a/ytdl/__main__.py b/ytdl/__main__.py new file mode 100644 index 0000000..bc86369 --- /dev/null +++ b/ytdl/__main__.py @@ -0,0 +1,39 @@ +import sys +from .funcmodule import check_playlist, download +import concurrent.futures + + +def main(): + args = sys.argv[1:] + modes = ["-d", "-a", "-v", "-av"] + + links = [] + mode = "-d" + force = False + assert len(args) > 0, "no args :(" + for arg in args: + if arg in modes: + mode = arg + if arg == '-f': # force + force = True + if "youtube" in arg or "youtu.be" in arg: + links.extend(arg.split(" ")) + + assert len(links) > 0, "Should pass at least one link as arg" + assert mode in modes, f"Mode should be one of {modes}" + print("Processing links") + # remove empty strings + links = list(filter(None, links)) + assert len(links) > 0, "Should not remove all links" + print("Checking for playlists") + links = check_playlist(links) + assert len(links) > 0, "Should be at least one song in playlist" + + # Use ThreadPoolExecutor to run downloads concurrently + with concurrent.futures.ThreadPoolExecutor() as executor: + # Schedule the download_audio_stream function for each audio stream + futures = {executor.submit(download, link, mode, force): link for link in links} + + +if __name__ == '__main__': + main() diff --git a/ytdl/__pycache__/__init__.cpython-312.pyc b/ytdl/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2889eb188a9736c3babb84d0743b378dd882252 GIT binary patch literal 142 zcmX@j%ge>Uz`!6{$&=2+z`*br#6icOSr`}?rZZGBXfpb(WGDiu`wUX^OV`CJ#yztn zqcka|GQYH>G$~a#CBHl`CqFSIwJ4^tBqb*%K0Y%qvm`!Vub}c4hfQvNN@-52T@fn- X0|O%i14A*0@sXL4k+F!Gfq?-43i}|4 literal 0 HcmV?d00001 diff --git a/ytdl/__pycache__/__main__.cpython-312.pyc b/ytdl/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a297c4dd31c12fa39efaaabe4bbf139438379f6 GIT binary patch literal 1904 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 literal 0 HcmV?d00001 diff --git a/ytdl/funcmodule.py b/ytdl/funcmodule.py new file mode 100644 index 0000000..53ddeee --- /dev/null +++ b/ytdl/funcmodule.py @@ -0,0 +1,137 @@ +import glob +import os +import subprocess + +import requests +from pytubefix import YouTube, Playlist + + +def check_playlist(links): + for link in links: + if "playlist" in link: + p = Playlist(link) + for url in p.video_urls: + links.append(url) + links.remove(link) + return links + + +def big_num_format(num): # https://stackoverflow.com/a/579376 + magnitude = 0 + while abs(num) >= 1000: + magnitude += 1 + num /= 1000.0 + return '%.1f%s' % (num, ['', 'K', 'M', 'B'][magnitude]) + + +def fix_filename(filename): + for i in ['/', ':', '*', '?', '"', '<', '>', '|']: + filename = filename.replace(i, '') + return filename + + +def download_thumbnail(thumbnail_url, thumbnail_filename): + data = requests.get(thumbnail_url).content + with open(thumbnail_filename, 'wb') as f: + f.write(data) + + +def download(link, mode, force=False): + yt = YouTube(link) + filename = fix_filename(yt.title) + if ( + ( + (mode == '-av' and + (filename + ' (audio only).mp4' in glob.glob("*.mp4") or + filename + ' (video only).mp4' in glob.glob("*.mp4"))) or + (mode != '-av' and filename + '.mp4' in glob.glob("*.mp4")) + ) and + (not force) + ): + print(f"{yt.title} is already downloaded") + return + yt.check_availability() + + thumbnail_filename = f'{filename}.jpg' + download_thumbnail(yt.thumbnail_url, thumbnail_filename) + + if mode == '-a' or mode == '-v': + download_single_stream(yt, filename, thumbnail_filename, mode) + elif mode == '-av' or mode == '-d': + download_double_stream(yt, filename, thumbnail_filename, mode) + + +def convert_add_metadata(input1, input2, output, yt, m1=1, m2=0): + album = yt.title + if 'keywords' in yt.vid_info['videoDetails'].keys() and len(yt.vid_info['videoDetails']['keywords']) > 2: + album = yt.vid_info['videoDetails']['keywords'][-2] + command = [ + 'ffmpeg', + '-i', input1, + '-i', input2, + '-map', f'{m1}', + '-map', f'{m2}', + '-c', 'copy', + f'-disposition:v:{m2}', 'attached_pic', + '-metadata', f'title={yt.title}', + '-metadata', f'artist={yt.author}', + '-metadata', f'comment={big_num_format(yt.views) + " views"}', + '-metadata', f'date={yt.publish_date}', + '-metadata', f'album={album}', + output + ".mp4", + '-y' + ] + subprocess.run(command) + + +def download_single_stream(yt, filename, thumbnail_filename, mode): + print(f"Fetching stream for {yt.title}") + stream = None + if mode == "-a": + assert len(yt.streams.filter(only_audio=True)) > 0, "No available audio streams" + stream = yt.streams.filter(only_audio=True).order_by("abr").last() + if mode == "-v": + assert len(yt.streams.filter(only_video=True)) > 0, "No available video streams" + stream = yt.streams.filter(only_video=True).order_by("resolution").last() + + assert stream is not None, "mode is not valid" + print(f"Downloading stream for {yt.title}") + default_filename = "default " + fix_filename(stream.default_filename) + stream.download(filename=default_filename, skip_existing=True) + + print(f"Adding metadata to {yt.title}") + convert_add_metadata(default_filename, thumbnail_filename, filename, yt) + + print("Removing temporary files") + os.remove(thumbnail_filename) + os.remove(default_filename) + + +def download_double_stream(yt, filename, thumbnail_filename, mode): + print(f"Fetching streams for {yt.title}") + assert len(yt.streams.filter(only_audio=True)) > 0, "No available audio streams" + audio_stream = yt.streams.filter(only_audio=True).order_by("abr").last() + assert len(yt.streams.filter(only_video=True)) > 0, "No available video streams" + video_stream = yt.streams.filter(only_video=True).order_by("resolution").last() + + print(f"Downloading streams for {yt.title}") + audio_default_filename = "default audio " + fix_filename(audio_stream.default_filename) + video_default_filename = "default video " + fix_filename(video_stream.default_filename) + + audio_stream.download(filename=audio_default_filename, skip_existing=True) + video_stream.download(filename=video_default_filename, skip_existing=True) + + print(f"Adding metadata to {yt.title}") + if mode == '-av': + for suffix, stream in [(" (audio only)", audio_default_filename), (" (video only)", video_default_filename)]: + convert_add_metadata(stream, thumbnail_filename, filename + suffix, yt) + elif mode == '-d': + convert_add_metadata(audio_default_filename, video_default_filename, filename + "tmp", yt, m1=0, m2=1) + convert_add_metadata(filename + "tmp.mp4", thumbnail_filename, filename, yt) + + print("Removing temporary files") + os.remove(thumbnail_filename) + os.remove(audio_default_filename) + os.remove(video_default_filename) + if mode == '-d': + os.remove(filename + "tmp" + ".mp4") -- 2.54.0