From: Skullheadx <94652084+Skullheadx@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:57:02 +0000 (-0400) Subject: Squashed commit of the following: X-Git-Url: http://git.skullheadx.com/nixos/README?a=commitdiff_plain;h=6743e7825c65f6c29a69e497ffcac561ec184ced;p=youtube-downloader.git 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 --- 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 0000000..a2889eb Binary files /dev/null and b/ytdl/__pycache__/__init__.cpython-312.pyc differ diff --git a/ytdl/__pycache__/__main__.cpython-312.pyc b/ytdl/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000..0a297c4 Binary files /dev/null and b/ytdl/__pycache__/__main__.cpython-312.pyc differ diff --git a/ytdl/__pycache__/classmodule.cpython-312.pyc b/ytdl/__pycache__/classmodule.cpython-312.pyc new file mode 100644 index 0000000..626cbfd Binary files /dev/null and b/ytdl/__pycache__/classmodule.cpython-312.pyc differ diff --git a/ytdl/__pycache__/funcmodule.cpython-312.pyc b/ytdl/__pycache__/funcmodule.cpython-312.pyc new file mode 100644 index 0000000..91e1dc7 Binary files /dev/null and b/ytdl/__pycache__/funcmodule.cpython-312.pyc differ 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")