diff --git a/CHANGELOG.md b/CHANGELOG.md index 2017e31..9d862d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Pyutube Changelog +## 1.2.8 + +- Add: if you download half of the playlist, you can resume the others without having to download them again. + It will check the root directory for the file, and if the playlist folder is found it will see its content and remove all the files that already downloaded from the download queue. + ## 1.2.6 - Fix: switch into pytubefix instead of pytube. diff --git a/pyutube/cli.py b/pyutube/cli.py index 6e23581..daa2898 100644 --- a/pyutube/cli.py +++ b/pyutube/cli.py @@ -39,6 +39,7 @@ import os import sys +import re import typer import threading @@ -190,13 +191,10 @@ def handle_playlist(url: str, path: str): Returns: None """ - def get_title(video): - """Function to get the title of a YouTube video.""" - return video.title def fetch_title_thread(video): """Fetch the title of a YouTube video in a separate thread.""" - video_title = video.title + video_title = safe_filename(video.title) video_id = video.video_id playlist_videos.append((video_title, video_id)) @@ -229,19 +227,77 @@ def fetch_title_thread(video): # Now all video titles are stored in the video_titles list console.print(f"\nPlaylist title: {title}\n", style="info") - console.print(f"Total videos: {total}\n\n", style="info") - console.print("Chose what video you want to download") - - videos_selected = ask_playlist_video_names(playlist_videos) + console.print(f"Total videos: {total}\n", style="info") os.makedirs(title, exist_ok=True) new_path = os.path.join(path, title) + # check if there is any video already downloaded in the past + for file in os.listdir(new_path): + for video in playlist_videos: + if file.startswith(video): + playlist_videos.remove(video) + # Exit the inner loop since we found a match + break + + if not playlist_videos: + console.print(f"All playlist are already downloaded in this directory, see '{ + title}' folder", style="info") + sys.exit() + + console.print("Chose what video you want to download") + videos_selected = ask_playlist_video_names(playlist_videos) + for index, video_id in enumerate(videos_selected): url = f"https://www.youtube.com/watch?v={video_id}" + if index == 0: quality = download(url, new_path, is_audio, is_playlist=False) continue - download(url, new_path, is_audio, - quality_choice=quality, is_playlist=False) + quality_choice=quality, + is_playlist=False) + + +def safe_filename(s: str, max_length: int = 255) -> str: + """Sanitize a string making it safe to use as a filename. + + This function was based off the limitations outlined here: + https://en.wikipedia.org/wiki/Filename. + + :param str s: + A string to make safe for use as a file name. + :param int max_length: + The maximum filename character length. + :rtype: str + :returns: + A sanitized string. + """ + # Characters in range 0-31 (0x00-0x1F) are not allowed in ntfs filenames. + ntfs_characters = [chr(i) for i in range(31)] + characters = [ + r'"', + r"\#", + r"\$", + r"\%", + r"'", + r"\*", + r"\,", + r"\.", + r"\/", + r"\:", + r'"', + r"\;", + r"\<", + r"\>", + r"\?", + r"\\", + r"\^", + r"\|", + r"\~", + r"\\\\", + ] + pattern = "|".join(ntfs_characters + characters) + regex = re.compile(pattern, re.UNICODE) + filename = regex.sub("", s) + return filename[:max_length].rsplit(" ", 0)[0] diff --git a/pyutube/downloader.py b/pyutube/downloader.py index 08ee0f3..c234643 100644 --- a/pyutube/downloader.py +++ b/pyutube/downloader.py @@ -130,7 +130,16 @@ def get_video_streams(self, quality: str, streams: YouTube.streams) -> YouTube: The video stream with the specified quality, or the best available stream if no match is found. """ - return streams.filter(res=quality).first() + s = streams.filter(res=quality).first() + + if not s: + available_qualities = [stream.resolution for stream in streams] + available_qualities = list(map(int, available_qualities)) + selected_quality = min(available_qualities, + key=lambda x: abs(int(quality) - x)) + s = streams.filter(res=str(selected_quality)).first() + + return s @yaspin( text=colored("Downloading the audio...", "green"), diff --git a/pyutube/utils.py b/pyutube/utils.py index 1269fcd..62162d6 100644 --- a/pyutube/utils.py +++ b/pyutube/utils.py @@ -16,7 +16,7 @@ from termcolor import colored -__version__ = "1.2.7" +__version__ = "1.2.8" __app__ = "pyutube" ABORTED_PREFIX = "aborted" CANCEL_PREFIX = "cancel" @@ -251,12 +251,14 @@ def ask_rename_file(filename: str) -> str: def ask_playlist_video_names(videos): note = colored("NOTE:", "cyan") + select_one = colored("", "red") select_all = colored("", "red") invert_selection = colored("", "red") restart_selection = colored("", "red") print( - f"{note} Press {select_all} to select all, {invert_selection} to invert selection, and {restart_selection} to restart selection", + f"{note} Press {select_one} to select the videos, {select_all} to select all, { + invert_selection} to invert selection, and {restart_selection} to restart selection", ) questions = [ inquirer.Checkbox( @@ -348,7 +350,8 @@ def check_for_updates() -> None: if latest_version != __version__: console.print( - f"👉 A new version of {__app__} is available: {latest_version}. Update it by running [bold red link=https://github.com/Hetari/pyutube]pip install --upgrade {__app__}[/bold red link]", + f"👉 A new version of {__app__} is available: { + latest_version}. Update it by running [bold red link=https://github.com/Hetari/pyutube]pip install --upgrade {__app__}[/bold red link]", style="warning" ) else: diff --git a/requirements.txt b/requirements.txt index 274a3ad..e23998b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ typer==0.9.0 requests==2.31.0 rich==13.7.1 yaspin==3.0.1 -pytubefix==5.4.2 +pytubefix==5.6.3 inquirer==3.2.4 termcolor==2.4.0 moviepy==1.0.3 \ No newline at end of file