1
1
mirror of https://github.com/neosubhamoy/pytubepp.git synced 2026-02-04 10:22:21 +05:30

8 Commits

4 changed files with 119 additions and 27 deletions

View File

@@ -62,6 +62,8 @@
> Use `pip3` command instead of `pip` if you are on Linux or MacOS. > Use `pip3` command instead of `pip` if you are on Linux or MacOS.
> Use `--break-system-packages` flag to install 'PytubePP' in global environment if you get `error: externally-managed-environment` while installing in Linux or MacOS (Don't worry it will not break your system packages, it's just a security mesure)
```terminal ```terminal
pip install pytubepp pip install pytubepp
``` ```
@@ -72,21 +74,29 @@ pip install pytubepp
pip install pytubefix pytubepp --upgrade pip install pytubefix pytubepp --upgrade
``` ```
**UNINSTALL: If you want to uninstall PytubePP (Use the command below to uninstall) NOTE: it will only remove the 'PytubePP' python package**
```
pip uninstall pytubepp -y
```
### **📌 Commands and Flags** ### **📌 Commands and Flags**
Using PytubePP is as simple as just supplying it only the YouTube video url as argument! Using PytubePP is as simple as just supplying it only the YouTube video url as argument!
** Before Starting Please NOTE: PytubePP follows a simple rule - "Use the Default Download Configuration if No Flags are Passed" > Before starting please NOTE: PytubePP follows a simple principle -> `Use Default Configuration if No Flags are Passed`
* To download a video in default configuration (maximum resolution and without any caption by default) the command will look like: * To download a video in default configuration (maximum resolution and without any caption by default) the command will look like:
```terminal ```terminal
pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo"
``` ```
> NOTE: This command will behave differently if you have changed default configurations
* To download the video in a specific resolution (suppose 480p) the command will be: * To download the video in a specific resolution (suppose 480p) the command will be:
```terminal ```terminal
pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -s 480p pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -s 480p
``` ```
> NOTE: PytubePP always uses default configuration of flags if they are not passed for example if you only pass `-s` flag then it will use the default caption along with it, if you only pass `-c` then it will use default stream and vice versa
* To download the video with embeded caption (suppose en - English) the command will be: * To download the video with embeded caption (suppose en - English) the command will be:
```terminal ```terminal
pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -c en pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -c en
``` ```
> NOTE: You can override and disable default caption for the current video if you pass `-c none`
* To download the video in audio-only MP3 format the command will be: * To download the video in audio-only MP3 format the command will be:
```terminal ```terminal
pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -s mp3 pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -s mp3
@@ -101,8 +111,9 @@ pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -i
| Flag | Usage | Requires Parameter | Requires URL | Parameters | Default | | Flag | Usage | Requires Parameter | Requires URL | Parameters | Default |
| :--- | :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- | :--- |
| -s | Choose preferred download stream | YES | YES | `144` `144p` `240` `240p` `360` `360p` `480` `480p` `720` `720p` `hd` `1080` `1080p` `fhd` `1440` `1440p` `2k` `2160` `2160p` `4k` `4320` `4320p` `8k` `mp3` (Pass any one of them) | Your chosen Default Stream via `-ds` flag | | -s | Choose preferred download stream | YES | YES | `144` `144p` `240` `240p` `360` `360p` `480` `480p` `720` `720p` `hd` `1080` `1080p` `fhd` `1440` `1440p` `2k` `2160` `2160p` `4k` `4320` `4320p` `8k` `mp3` (Pass any one of them) | Your chosen Default Stream via `-ds` flag |
| -c | Choose preferred caption | YES | YES | All [ISO 639-1 Language Codes](https://www.w3schools.com/tags/ref_language_codes.asp) + some others (Pass any one of them) eg: `en` for English | Your chosen Default Caption via `-dc` flag | | -c | Choose preferred caption | YES | YES | All [ISO 639-1 Language Codes](https://www.w3schools.com/tags/ref_language_codes.asp) + some others (Pass any one of them) + `none` for No Caption eg: `en` for English | Your chosen Default Caption via `-dc` flag |
| -i | Shows the video information like: Title, Author, Views, Publication Date, Duration, Available Download Streams | NO | YES | No parameters | No default | | -i | Shows the video information like: Title, Author, Views, Publication Date, Duration, Available Download Streams | NO | YES | No parameters | No default |
| -ls | Lists all available streams (video, audio, caption) (only for debuging purposes) | NO | YES | No parameters | No default |
| -ri | Shows the video information in raw json format | NO | YES | No parameters | No default | | -ri | Shows the video information in raw json format | NO | YES | No parameters | No default |
| -jp | Shows raw json output in prettified view (with indentation: 4) (primarily used with -ri flag)| NO | YES | No parameters | No default | | -jp | Shows raw json output in prettified view (with indentation: 4) (primarily used with -ri flag)| NO | YES | No parameters | No default |
| -ds | Set default download stream | YES | NO | `144p` `240p` `360p` `480p` `720p` `1080p` `1440p` `2160p` `4320p` `mp3` `max` (Pass any one of them) | `max` | | -ds | Set default download stream | YES | NO | `144p` `240p` `360p` `480p` `720p` `1080p` `1440p` `2160p` `4320p` `mp3` `max` (Pass any one of them) | `max` |

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pytubepp" name = "pytubepp"
version = "1.1.3" version = "1.1.5"
authors = [ authors = [
{ name="Subhamoy Biswas", email="hey@neosubhamoy.com" }, { name="Subhamoy Biswas", email="hey@neosubhamoy.com" },
] ]

View File

@@ -3,7 +3,7 @@ from tabulate import tabulate
from .config import get_temporary_directory, load_config, update_config, reset_config from .config import get_temporary_directory, load_config, update_config, reset_config
from .download import download_progressive, download_nonprogressive, download_audio, progress from .download import download_progressive, download_nonprogressive, download_audio, progress
from .postprocess import merge_audio_video, convert_to_mp3 from .postprocess import merge_audio_video, convert_to_mp3
from .utils import get_version, clear_temp_files, is_valid_url, network_available from .utils import get_version, clear_temp_files, is_valid_url, network_available, ffmpeg_installed, nodejs_installed
import appdirs, os, re, sys, argparse, json import appdirs, os, re, sys, argparse, json
class YouTubeDownloader: class YouTubeDownloader:
@@ -42,10 +42,15 @@ class YouTubeDownloader:
if not network_available(): if not network_available():
print('\nRequest timeout! Please check your network and try again...!!') print('\nRequest timeout! Please check your network and try again...!!')
sys.exit() sys.exit()
if not nodejs_installed():
print("\nWarning: Node.js is not installed or not found in PATH!")
print("BotGuard poToken generation will not work properly without Node.js environment")
print("Please install Node.js from https://nodejs.org/en/download\n")
if is_valid_url(link): if is_valid_url(link):
link = is_valid_url(link).group(1) link = is_valid_url(link).group(1)
self.video = YouTube(link, 'ANDROID', on_progress_callback=progress) self.video = YouTube(link, 'WEB', on_progress_callback=progress)
self.author = self.video.author self.author = self.video.author
self.title = re.sub(r'[\\/*?:"<>|]', '_', self.author + ' - ' + self.video.title) self.title = re.sub(r'[\\/*?:"<>|]', '_', self.author + ' - ' + self.video.title)
self.thumbnail = self.video.thumbnail_url self.thumbnail = self.video.thumbnail_url
@@ -90,7 +95,7 @@ class YouTubeDownloader:
'ado_bitrate': matching_stream.abr 'ado_bitrate': matching_stream.abr
} }
else: else:
_select_suitable_audio_stream = lambda stream: 139 if stream.itag in [160, 133] else (251 if stream.mime_type == 'video/webm' else 140) _select_suitable_audio_stream = lambda stream: 251 if stream.mime_type == 'video/webm' else 140
# Check for HDR variants first # Check for HDR variants first
hdr_stream = None hdr_stream = None
if res in ['4320p', '2160p', '1440p', '1080p', '720p']: if res in ['4320p', '2160p', '1440p', '1080p', '720p']:
@@ -152,6 +157,24 @@ class YouTubeDownloader:
else: else:
print('\nInvalid video link! Please enter a valid video url...!!') print('\nInvalid video link! Please enter a valid video url...!!')
def show_all_streams(self, link):
if self.set_video_info(link):
print(f"Available Streams({len(self.stream)}):")
if self.stream:
for stream in self.stream:
print(stream)
else:
print('No stream available!')
print(f"\nAvailable Captions({len(self.captions)}):")
if self.captions:
for caption in self.captions:
print(caption)
else:
print('No caption available!')
else:
print('\nInvalid video link! Please enter a valid video url...!!')
def show_raw_info(self, link, prettify=False): def show_raw_info(self, link, prettify=False):
if self.set_video_info(link): if self.set_video_info(link):
streams_list = [] streams_list = []
@@ -217,22 +240,38 @@ class YouTubeDownloader:
print('\nInvalid video link! Please enter a valid video url...!!') print('\nInvalid video link! Please enter a valid video url...!!')
return [] return []
def print_short_info(self, chosen_stream): def print_short_info(self, chosen_stream, chosen_caption=None):
resolution_map = { print(f'\nTitle: {self.title}')
'4320': '4320p (8K)', '4320p': '4320p (8K)', '8k': '4320p (8K)',
'2160': '2160p (4K)', '2160p': '2160p (4K)', '4k': '2160p (4K)', if chosen_stream == 'mp3':
'1440': '1440p (2K)', '1440p': '1440p (2K)', '2k': '1440p (2K)', print(f'Selected: Audio [128kbps (140)]')
'1080': '1080p (FHD)', '1080p': '1080p (FHD)', 'fhd': '1080p (FHD)', return
'720': '720p (HD)', '720p': '720p (HD)', 'hd': '720p (HD)',
'480': '480p (SD)', '480p': '480p (SD)', if chosen_stream in ['360', '360p']:
'360': '360p (SD)', '360p': '360p (SD)', print(f"Selected: Video [360p (18)] + Audio [96kbps (18)]{f' + Caption [{chosen_caption}]' if chosen_caption else ''}")
'240': '240p (LD)', '240p': '240p (LD)', return
'144': '144p (LD)', '144p': '144p (LD)',
'mp3': 'mp3 (Audio)' _select_suitable_audio_stream = lambda stream: 251 if stream.mime_type == 'video/webm' else 140
} res = next((k for k, v in self.stream_resolutions.items() if chosen_stream in v['allowed_streams']), None)
print(f'\nTitle: {self.title}\nSelected Stream: {resolution_map.get(chosen_stream, "Unknown")}\n')
if res:
hdr_stream = None
if res in ['4320p', '2160p', '1440p', '1080p', '720p']:
hdr_itags = {'4320p': 702, '2160p': 701, '1440p': 700, '1080p': 699, '720p': 698}
hdr_stream = self.stream.get_by_itag(hdr_itags.get(res))
matching_stream = hdr_stream if hdr_stream else self.stream.filter(res=res).first()
audio_stream = self.stream.get_by_itag(_select_suitable_audio_stream(matching_stream))
print(f"Selected: Video [{res} ({matching_stream.itag})] + Audio [{audio_stream.abr} ({audio_stream.itag})]{f' + Caption [{chosen_caption}]' if chosen_caption else ''}")
def download_stream(self, link, chosen_stream, chosen_caption=None): def download_stream(self, link, chosen_stream, chosen_caption=None):
if not ffmpeg_installed():
print("\nWarning: FFmpeg is not installed or not found in PATH!")
print("Some core functionalities like video processing will not work properly without FFmpeg")
print("Please install FFmpeg, read https://github.com/neosubhamoy/pytubepp#%EF%B8%8F-installation for instructions\n")
sys.exit()
if self.set_video_info(link): if self.set_video_info(link):
allowed_streams = self.get_allowed_streams(link) allowed_streams = self.get_allowed_streams(link)
allowed_captions = self.get_allowed_captions(link) allowed_captions = self.get_allowed_captions(link)
@@ -242,7 +281,7 @@ class YouTubeDownloader:
sys.exit() sys.exit()
if chosen_stream in allowed_streams: if chosen_stream in allowed_streams:
self.print_short_info(chosen_stream) self.print_short_info(chosen_stream, chosen_caption)
if chosen_stream in ['360', '360p']: if chosen_stream in ['360', '360p']:
download_progressive(self.stream, 18, self.title, '360p', 'mp4', self.captions, chosen_caption) download_progressive(self.stream, 18, self.title, '360p', 'mp4', self.captions, chosen_caption)
elif chosen_stream in ['1080', '1080p', 'fhd']: elif chosen_stream in ['1080', '1080p', 'fhd']:
@@ -252,9 +291,9 @@ class YouTubeDownloader:
elif chosen_stream in ['480', '480p']: elif chosen_stream in ['480', '480p']:
merge_audio_video(self.title, '480p', 'mp4', download_nonprogressive(self.stream, 135, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) merge_audio_video(self.title, '480p', 'mp4', download_nonprogressive(self.stream, 135, 140, 'mp4', self.temp_dir), self.captions, chosen_caption)
elif chosen_stream in ['240', '240p']: elif chosen_stream in ['240', '240p']:
merge_audio_video(self.title, '240p', 'mp4', download_nonprogressive(self.stream, 133, 139, 'mp4', self.temp_dir), self.captions, chosen_caption) merge_audio_video(self.title, '240p', 'mp4', download_nonprogressive(self.stream, 133, 140, 'mp4', self.temp_dir), self.captions, chosen_caption)
elif chosen_stream in ['144', '144p']: elif chosen_stream in ['144', '144p']:
merge_audio_video(self.title, '144p', 'mp4', download_nonprogressive(self.stream, 160, 139, 'mp4', self.temp_dir), self.captions, chosen_caption) merge_audio_video(self.title, '144p', 'mp4', download_nonprogressive(self.stream, 160, 140, 'mp4', self.temp_dir), self.captions, chosen_caption)
elif chosen_stream in ['4320', '4320p', '8k']: elif chosen_stream in ['4320', '4320p', '8k']:
self._handle_4320p_download(chosen_caption) self._handle_4320p_download(chosen_caption)
elif chosen_stream in ['2160', '2160p', '4k']: elif chosen_stream in ['2160', '2160p', '4k']:
@@ -315,8 +354,9 @@ def main():
parser.add_argument('-ds', '--default-stream', default=argparse.SUPPRESS, help='set default download stream (default: max) [available arguments: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p, 4320p, mp3, max]') parser.add_argument('-ds', '--default-stream', default=argparse.SUPPRESS, help='set default download stream (default: max) [available arguments: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p, 4320p, mp3, max]')
parser.add_argument('-dc', '--default-caption', default=argparse.SUPPRESS, help='set default caption (default: none) [available arguments: all language codes, none]') parser.add_argument('-dc', '--default-caption', default=argparse.SUPPRESS, help='set default caption (default: none) [available arguments: all language codes, none]')
parser.add_argument('-s', '--stream', default=argparse.SUPPRESS, help='choose download stream for the current video (default: your chosen --default-stream) [available arguments: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p, 4320p, 144, 240, 360, 480, 720, 1080, 1440, 2160, 4320, mp3, hd, fhd, 2k, 4k, 8k]') parser.add_argument('-s', '--stream', default=argparse.SUPPRESS, help='choose download stream for the current video (default: your chosen --default-stream) [available arguments: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p, 4320p, 144, 240, 360, 480, 720, 1080, 1440, 2160, 4320, mp3, hd, fhd, 2k, 4k, 8k]')
parser.add_argument('-c', '--caption', default=argparse.SUPPRESS, help='choose caption to embed for the current video (default: none)') parser.add_argument('-c', '--caption', default=argparse.SUPPRESS, help='choose caption to embed for the current video (default: your chosen --default-caption) [available arguments: all language codes, none]')
parser.add_argument('-i', '--show-info', action='store_true', help='show video info (title, author, views and available_streams)') parser.add_argument('-i', '--show-info', action='store_true', help='show video info (title, author, views and available_streams)')
parser.add_argument('-ls', '--list-stream', action='store_true', help='list all available streams (video, audio, caption) (only for debuging purposes)')
parser.add_argument('-ri', '--raw-info', action='store_true', help='show video info in raw json format') parser.add_argument('-ri', '--raw-info', action='store_true', help='show video info in raw json format')
parser.add_argument('-jp', '--json-prettify', action='store_true', help='show json in prettified indented view') parser.add_argument('-jp', '--json-prettify', action='store_true', help='show json in prettified indented view')
parser.add_argument('-sc', '--show-config', action='store_true', help='show all current user config settings') parser.add_argument('-sc', '--show-config', action='store_true', help='show all current user config settings')
@@ -352,6 +392,8 @@ def main():
# Handle info display flags # Handle info display flags
if args.show_info: if args.show_info:
downloader.show_video_info(args.url) downloader.show_video_info(args.url)
if args.list_stream:
downloader.show_all_streams(args.url)
if args.raw_info: if args.raw_info:
downloader.show_raw_info(args.url, args.json_prettify) downloader.show_raw_info(args.url, args.json_prettify)
if args.json_prettify and not args.raw_info: if args.json_prettify and not args.raw_info:
@@ -360,9 +402,11 @@ def main():
# Handle download cases # Handle download cases
if hasattr(args, 'stream') and hasattr(args, 'caption'): if hasattr(args, 'stream') and hasattr(args, 'caption'):
if downloader.set_video_info(args.url): if downloader.set_video_info(args.url):
if args.caption not in downloader.captions.keys(): if (args.caption not in downloader.captions.keys()) and (args.caption != 'none'):
print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)') print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)')
sys.exit() sys.exit()
elif args.caption == 'none':
downloader.download_stream(args.url, args.stream)
elif args.stream == 'mp3' and downloader.stream.get_by_itag(140): elif args.stream == 'mp3' and downloader.stream.get_by_itag(140):
print(f'\nYou have chosen to download mp3 stream! ( Captioning audio files is not supported )') print(f'\nYou have chosen to download mp3 stream! ( Captioning audio files is not supported )')
answer = input('Do you still want to continue downloading ? [yes/no]\n') answer = input('Do you still want to continue downloading ? [yes/no]\n')
@@ -403,9 +447,29 @@ def main():
print('Download cancelled! exiting...!!') print('Download cancelled! exiting...!!')
elif hasattr(args, 'caption'): elif hasattr(args, 'caption'):
if downloader.set_video_info(args.url): if downloader.set_video_info(args.url):
if args.caption not in downloader.captions.keys(): if (args.caption not in downloader.captions.keys()) and (args.caption != 'none'):
print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)') print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)')
sys.exit() sys.exit()
elif args.caption == 'none':
if downloader.default_stream == 'max' and downloader.maxres:
downloader.download_stream(args.url, downloader.maxres)
elif downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140):
downloader.download_stream(args.url, downloader.default_stream)
elif downloader.default_stream != 'max' and downloader.stream.filter(res=downloader.default_stream):
downloader.download_stream(args.url, downloader.default_stream)
else:
if downloader.maxres:
print(f'\nDefault stream not available! ( Default: {downloader.default_stream} | Available: {downloader.maxres} )')
answer = input('Do you want to download the maximum available stream ? [yes/no]\n')
while answer not in ['yes', 'y', 'no', 'n']:
print('Invalid answer! try again...!! answer with: [yes/y/no/n]')
answer = input('Do you want to download the maximum available stream ? [yes/no]\n')
if answer in ['yes', 'y']:
downloader.download_stream(args.url, downloader.maxres)
else:
print('Download cancelled! exiting...!!')
else:
print('Sorry, No downloadable video stream found....!!!')
elif downloader.default_stream == 'max' and downloader.maxres: elif downloader.default_stream == 'max' and downloader.maxres:
downloader.download_stream(args.url, downloader.maxres, args.caption) downloader.download_stream(args.url, downloader.maxres, args.caption)
elif downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140): elif downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140):
@@ -433,7 +497,7 @@ def main():
print('Download cancelled! exiting...!!') print('Download cancelled! exiting...!!')
else: else:
print('Sorry, No downloadable video stream found....!!!') print('Sorry, No downloadable video stream found....!!!')
elif not any([args.show_info, args.raw_info, args.json_prettify]): # If no info flags are set elif not any([args.show_info, args.raw_info, args.json_prettify, args.list_stream]): # If no info flags are set
if downloader.set_video_info(args.url): if downloader.set_video_info(args.url):
if downloader.default_stream == 'max' and downloader.maxres: if downloader.default_stream == 'max' and downloader.maxres:
if downloader.default_caption == 'none': if downloader.default_caption == 'none':
@@ -546,6 +610,9 @@ def main():
if args.show_info: if args.show_info:
print('\nNo video url supplied! exiting...!!') print('\nNo video url supplied! exiting...!!')
if args.list_stream:
print('\nNo video url supplied! exiting...!!')
if args.raw_info: if args.raw_info:
print('\nNo video url supplied! exiting...!!') print('\nNo video url supplied! exiting...!!')

View File

@@ -14,6 +14,20 @@ def network_available():
return True return True
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return False return False
def nodejs_installed():
try:
subprocess.run(['node', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def ffmpeg_installed():
try:
subprocess.run(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def get_version(): def get_version():
try: try: