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

58 Commits

13 changed files with 1230 additions and 654 deletions

View File

@@ -18,14 +18,14 @@ jobs:
- name: 🐍 Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'
- name: 📦 Install dependencies
run: |
pip install -r requirements.txt
- name: 🛠️ Build package
run: python3 setup.py sdist bdist_wheel
run: python3 -m build
- name: 🚀 Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# Compiled python modules.
# Compiled python modules and cache.
__pycache__/
*.pyc
# Setuptools distribution folder.

View File

135
README.md
View File

@@ -3,23 +3,37 @@
### A Simple CLI Tool to Download Your Favourite YouTube Videos Effortlessly!
[![status](https://img.shields.io/badge/status-active-brightgreen.svg?style=flat)](https://github.com/neosubhamoy/pytubepp/)
[![verion](https://img.shields.io/badge/version-v1.0.5_stable-yellow.svg?style=flat)](https://github.com/neosubhamoy/pytubepp/)
[![python](https://img.shields.io/badge/python-v3.12.x-blue?logo=python&style=flat)](https://www.python.org/downloads/)
[![PypiDownloads](https://img.shields.io/pypi/dm/pytubepp?color=brightgreen)](https://pypi.org/project/pytubepp/)
[![PypiVersion](https://img.shields.io/pypi/v/pytubepp?color=yellow)](https://pypi.org/project/pytubepp/)
[![python](https://img.shields.io/badge/python-v3.13-blue?logo=python&style=flat)](https://www.python.org/downloads/)
[![builds](https://img.shields.io/badge/builds-passing-brightgreen.svg?style=flat)](https://github.com/neosubhamoy/pytubepp/)
[![PRs](https://img.shields.io/badge/PRs-welcome-blue.svg?style=flat)](https://github.com/neosubhamoy/pytubepp/)
😀 GOOD NEWS: If you are Windows(10/11) user and don't want to bother remembering PytubePP Commands! (You are not familier with Command Line Tools). We recently released a Browser Extension that can auto detect YouTube Videos and You can download the Video in one click directly from the browser using PytubePP CLI. Install [PytubePP Helper](https://github.com/neosubhamoy/pytubepp-helper) app in your Computer and add [PytubePP Extension](https://github.com/neosubhamoy/pytubepp-extension) in your Browser to get started.
😀 GOOD NEWS: If you are not a power user and don't want to bother remembering PytubePP Commands! (You are not familier with Command Line Tools). We recently released a Browser Extension that can auto detect YouTube Videos and You can download the Video in one click directly from the browser using PytubePP CLI. Install [PytubePP Helper](https://github.com/neosubhamoy/pytubepp-helper) app in your System and add [PytubePP Extension](https://github.com/neosubhamoy/pytubepp-extension) in your Browser to get started.
> **🥰 Liked this project? Please consider giving it a Star (🌟) on github to show us your appreciation and help the algorythm recommend this project to even more awesome people like you!**
### **💻 Supported Platforms**
- Windows (10 / 11)
- Linux (Debian, Fedora, Arch)
- MacOS
- Android (Termux)
### **🏷️ Features**
* Auto Post-Process & Merge YouTube DASH Streams
* Supports upto 8K 60fps HDR Stream Download
* Supports MP3 Download (with Embeded Thumbnail and Tags)
* Supports Embeded Captions
* Smart Stream Selection
* Highly Configurable and Many More 😉
### **🧩 Dependencies**
### **📎 Pre-Requirements**
* [Python](https://www.python.org/downloads/) (>=3.8)
* [FFmpeg](https://ffmpeg.org/)
* [Node.js](https://nodejs.org/en/download/) (required for auto YT poToken genration which is currently not possible in Python environment)
### **🧩 Python Dependencies**
* [pytubefix](https://pypi.org/project/pytubefix/)
* [FFmpeg (Not Pre-Included)](https://ffmpeg.org/)
* [ffmpy](https://pypi.org/project/ffmpy/)
* [mutagen](https://pypi.org/project/mutagen/)
* [tabulate](https://pypi.org/project/tabulate/)
@@ -29,31 +43,69 @@
* [setuptools](https://pypi.org/project/setuptools/)
### **🛠️ Installation**
You can install pytubePP in your system via PIP by simply running the below command
1. Install Python and PIP
- Linux (Debian): Python is pre-installed install PIP using `sudo apt install python3-pip`<br>
- Linux (Fedora): Python is pre-installed install PIP using `sudo dnf install python3-pip`<br>
- Linux (Arch): Python is pre-installed install PIP using `sudo pacman -S python-pip`<br>
- Windows (10/11): `winget install Python.Python.3.13`<br>
- MacOS (using Homebrew): `brew install python`<br>
- Android (using Termux): `pkg install python`
> You can skip step 2, 3 and auto install them later using the command `pytubepp --postinstall` post installation (works in: Windows, Linux - debian fedora arch, MacOS)
2. Install FFmpeg
- Linux (Debian): `sudo apt install ffmpeg`<br>
- Linux (Fedora) ([enable](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) rpmfusion free+nonfree repos before installing): `sudo dnf install ffmpeg`<br>
- Linux (Arch): `sudo pacman -S ffmpeg`<br>
- Windows (10/11): `winget install ffmpeg`<br>
- MacOS (using Homebrew): `brew install ffmpeg`<br>
- Android (using Termux): `pkg install ffmpeg`
3. Install Node.js
- Linux (Debian): `sudo apt install nodejs`<br>
- Linux (Fedora): `sudo dnf install nodejs`<br>
- Linux (Arch): `sudo pacman -S nodejs-lts-iron npm`<br>
- Windows (10/11): `winget install OpenJS.NodeJS.LTS`<br>
- MacOS (using Homebrew): `brew install node`<br>
- Android (using Termux): `pkg install nodejs`
4. Install PytubePP (using PIP)
> 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
pip install pytubepp
```
**IMPORTANT: Before installing pytubePP make sure FFmpeg is installed in your system and accesable via your cli interface (FFmpeg is Must Required as some of the core features of pytubePP relies on FFmpeg, but due to security reasons we can not ship it with the module)**
**>> Install FFmpeg (If you haven't already!)**
**UPDATE: Always make sure 'PytubePP' and 'Pytubefix' is on the latest version to avoid issues (update them at least once a week) (Use the command below to update)**
Linux (Ubuntu): `apt install ffmpeg`<br>
Windows (10/11): `winget install ffmpeg`<br>
MacOS (using Homebrew): `brew install ffmpeg`<br>
Android (using Termux): `pkg install ffmpeg`
```
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**
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"
* To download a video in maximum available resolution the command will look like:
Using PytubePP is as simple as just supplying it only the YouTube video url as argument!
> 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:
```terminal
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:
```terminal
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:
```terminal
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:
```terminal
pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -s mp3
@@ -65,25 +117,54 @@ pytubepp "https://youtube.com/watch?v=2lAe1cqCOXo" -i
* To cancel/stop an ongoing download press `CTRL` + `C` on keyboard (it is recommended to run the `-ct` flag once after canceling an ongoing download).
* List of all available flags are given below:
| 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 |
| -i | Shows the video information like: Title, Author, Views, Available Download Streams | 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` |
| -df | Set custom download folder path | YES | NO | Use the full path excluding the last trailing slash within double quotes eg(in Linux): `"/path/to/folder"` (Make sure the folder path you enterted is already created and accessable) | Within `Pytube Downloads` folder in your System's `Downloads` folder |
| -r | Reset to default configuration (Download Folder, Default Stream) | NO | NO | No parameters | No default |
| -sc | Show all current user configurations | NO | NO | No parameters | No default |
| -ct | Clear temporary files (audio, video, thumbnail) of the failed, incomplete downloads | NO | NO | No parameters | No default |
| Short Flag | Flag | Usage | Requires Parameter | Requires URL | Parameters | Default |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| -s | --stream | 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 | --caption | Choose preferred caption | YES | YES | All [ISO 639-1 Language Codes](https://www.w3schools.com/tags/ref_language_codes.asp) + auto generated ones + `none` for No Caption (Pass any one of them) eg: `en` for English | Your chosen Default Caption via `-dc` flag |
| -i | --show-info | Shows the video information like: Title, Author, Views, Publication Date, Duration, Available Download Streams and Captions | NO | YES | No parameters | No default |
| -ls | --list-stream | Lists all available streams (video, audio, caption) (only for debuging purposes) | NO | YES | No parameters | No default |
| -ri | --raw-info | Shows the video information in raw json format | NO | YES | No parameters | No default |
| -jp | --json-prettify | Shows raw json output in prettified view (with indentation: 4) (primarily used with -ri flag)| NO | YES | No parameters | No default |
| -ds | --default-stream | Set default download stream | YES | NO | `144p` `240p` `360p` `480p` `720p` `1080p` `1440p` `2160p` `4320p` `mp3` `max` (Pass any one of them) | `max` |
| -dc | --default-caption | Set default caption | YES | NO | All [ISO 639-1 Language Codes](https://www.w3schools.com/tags/ref_language_codes.asp) + auto generated ones + `none` for No Caption (Pass any one of them) eg: `en` for English | `none` |
| -df | --download-folder | Set custom download folder path | YES | NO | Use the full path excluding the last trailing slash within double quotes eg(in Linux): `"/path/to/folder"` (Make sure the folder path you enterted is already created and accessable) | Within `PytubePP Downloads` folder in your System's `Downloads` folder |
| -r | --reset-default | Reset to default configuration (Download Folder, Default Stream) | NO | NO | No parameters | No default |
| -sc | --show-config | Show all current user configurations | NO | NO | No parameters | No default |
| -ct | --clear-temp | Clear temporary files (audio, video, thumbnail) of the failed, incomplete downloads | NO | NO | No parameters | No default |
| -pi | --postinstall | Auto install all external dependencies (FFmpeg, Node.js) (in Windows, Linux - debian fedora arch, MacOS) | NO | NO | No parameters | No default |
### 🛠️ Contributing / Building from Source
Want to be part of this? Feel free to contribute...!! Pull Requests are always welcome...!! (^_^) Follow these simple steps to start building:
* Make sure to install Python, FFmpeg, Node.js and Git before proceeding.
1. Fork this repo in your github account.
2. Git clone the forked repo in your local machine.
> Use `python3` and `pip3` commands instead of `python` and `pip` if you are on Linux or MacOS.
3. Install python dependencies
```terminal
pip install -r requirements.txt
```
4. build, install and test the module
```terminal
python -m build // build the module
pip install .\dist\pytubepp-<version>-py3-none-any.whl // install the module (give the path to the newly genrated whl file based on your OS path style and don't forget to replace the <version> with the actual version number)
```
5. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected)
⭕ Noticed any Bugs? or Want to give me some suggetions? always feel free to open an issue...!!
### 📝 License & Usage
pytubePP - (Pytube Post Processor) is a Fully Open Sourced Project licensed under MIT License. Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions.
PytubePP - (Pytube Post Processor) is a Fully Open Sourced Project licensed under MIT License. Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions.
**🌟 Liked this project? Please consider giving it a star to show me your appreciation**
<br></br>
⚖️ NOTE: YouTube is a trademark of Google LLC. Use of this trademark is subject to Google Permissions. Downloading and using Copyrighted YouTube Content for Commercial pourposes are not allowed by YouTube Terms without proper Permissions from the Creator. We don't promote this kinds of activity, You should use the downloaded contents wisely and at your own responsibility.
****

60
pyproject.toml Normal file
View File

@@ -0,0 +1,60 @@
[build-system]
requires = ["setuptools>=67.4.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pytubepp"
version = "1.1.7"
authors = [
{ name="Subhamoy Biswas", email="hey@neosubhamoy.com" },
]
description = "A Simple CLI Tool to Download Your Favorite YouTube Videos Effortlessly!"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
keywords = ["youtube", "download", "video", "pytube", "cli"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python",
"Topic :: Internet",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Sound/Audio :: Conversion",
"Topic :: Multimedia :: Video :: Conversion",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Terminals",
"Topic :: Utilities",
]
dependencies = [
"pytubefix",
"requests",
"ffmpy",
"mutagen",
"tabulate",
"tqdm",
"appdirs",
"setuptools",
]
[project.scripts]
pytubepp = "pytubepp.main:main"
[project.urls]
"Homepage" = "https://github.com/neosubhamoy/pytubepp"
"Bug Reports" = "https://github.com/neosubhamoy/pytubepp/issues"
[tool.setuptools.packages.find]
include = ["pytubepp*"]
[tool.setuptools]
license-files = ["LICENSE"]

54
pytubepp/config.py Normal file
View File

@@ -0,0 +1,54 @@
import os, json, platform, appdirs, tempfile
def get_download_folder():
system = platform.system()
if system in ["Windows", "Darwin", "Linux"]:
cli_download_dir = os.path.join(os.path.expanduser("~"), "Downloads", "PytubePP Downloads")
os.makedirs(cli_download_dir, exist_ok=True)
return cli_download_dir
else:
cli_download_dir = os.path.join(appdirs.user_download_dir(), "PytubePP Downloads")
os.makedirs(cli_download_dir, exist_ok=True)
return cli_download_dir
DEFAULT_CONFIG = {
'downloadDIR': get_download_folder(),
'defaultStream': 'max',
'defaultCaption': 'none',
}
def get_temporary_directory():
temp_dir = tempfile.gettempdir()
cli_temp_dir = os.path.join(temp_dir, 'pytubepp')
os.makedirs(cli_temp_dir, exist_ok=True)
return cli_temp_dir
def load_config():
config_dir = appdirs.user_config_dir('pytubepp')
config_path = os.path.join(config_dir, 'config.json')
if os.path.exists(config_path):
with open(config_path, 'r') as f:
return json.load(f)
else:
return DEFAULT_CONFIG
def save_config(config):
config_dir = appdirs.user_config_dir('pytubepp')
os.makedirs(config_dir, exist_ok=True)
config_path = os.path.join(config_dir, 'config.json')
with open(config_path, 'w') as f:
json.dump(config, f, indent=4)
def update_config(key, value):
config = load_config()
config[key] = value
save_config(config)
def reset_config():
config_dir = appdirs.user_config_dir('pytubepp')
config_path = os.path.join(config_dir, 'config.json')
if os.path.exists(config_path):
os.remove(config_path)
print('\nConfig reset successful!')
else:
print('\nAlready using the default configs! Not resetting...!')

89
pytubepp/download.py Normal file
View File

@@ -0,0 +1,89 @@
from tqdm import tqdm
from .config import get_temporary_directory, load_config
from .utils import get_unique_filename, postprocess_cleanup, unpack_caption
import os, re, requests, shutil, sys, random, ffmpy
userConfig = load_config()
downloadDIR = userConfig['downloadDIR']
tempDIR = get_temporary_directory()
def download_progressive(stream, itag, title, resolution, file_extention, captions, caption_code=None, tempDIR=tempDIR, downloadDIR=downloadDIR):
global total_filesize, progress_bar
selected_vdo = stream.get_by_itag(itag)
total_filesize = selected_vdo.filesize
progress_bar = tqdm(total=total_filesize, unit='B', unit_scale=True, desc="Downloading Video+Audio")
random_filename = str(random.randint(1000000000, 9999999999))
filename = random_filename + '_vdo.' + file_extention
output_temp_file = os.path.join(tempDIR, filename)
output_file = os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '.' + file_extention)) if not caption_code else os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '_' + caption_code + '.' + file_extention))
selected_vdo.download(output_path=tempDIR, filename=filename)
if caption_code:
print(f'Downloading Caption ({caption_code})...')
caption = captions[caption_code]
_, caption_lang = unpack_caption(caption)
caption_file = os.path.join(tempDIR, random_filename + '_cap.srt')
caption.save_captions(caption_file)
print('Processing...')
devnull = open(os.devnull, 'w')
output_temp_file_with_subs = os.path.join(tempDIR, random_filename + '_merged.' + file_extention)
ff = ffmpy.FFmpeg(
inputs={output_temp_file: None},
outputs={output_temp_file_with_subs: ['-i', caption_file, '-c', 'copy', '-c:s', 'mov_text', '-metadata:s:s:0', f'language={caption_code}', '-metadata:s:s:0', f'title={caption_lang}', '-metadata:s:s:0', f'handler_name={caption_lang}']}
)
ff.run(stdout=devnull, stderr=devnull)
devnull.close()
shutil.move(output_temp_file_with_subs, output_file)
postprocess_cleanup(tempDIR, ['_vdo.' + file_extention, '_cap.srt', '_merged.' + file_extention], random_filename)
print('Done! 🎉')
else:
print('Processing...')
shutil.move(output_temp_file, output_file)
print('Done! 🎉')
def download_nonprogressive(stream, itag_vdo, itag_ado, file_extention, output_path):
global total_filesize, progress_bar
selected_vdo = stream.get_by_itag(itag_vdo)
selected_ado = stream.get_by_itag(itag_ado)
random_filename = str(random.randint(1000000000, 9999999999))
total_filesize = selected_vdo.filesize
progress_bar = tqdm(total=total_filesize, unit='B', unit_scale=True, desc="Downloading Video")
selected_vdo.download(output_path=output_path, filename=random_filename + '_vdo.' + file_extention)
total_filesize = selected_ado.filesize
progress_bar = tqdm(total=total_filesize, unit='B', unit_scale=True, desc="Downloading Audio")
selected_ado.download(output_path=output_path, filename=random_filename + '_ado.' + file_extention)
return random_filename
def download_audio(stream, itag, output_path):
global total_filesize, progress_bar
selected_ado = stream.get_by_itag(itag)
total_filesize = selected_ado.filesize
progress_bar = tqdm(total=total_filesize, unit='B', unit_scale=True, desc="Downloading Audio")
random_filename = str(random.randint(1000000000, 9999999999))
selected_ado.download(output_path=output_path, filename=random_filename + '_ado.mp4')
return random_filename
def download_thumbnail(url, file_path):
print('Downloading thumbnail...')
maxres_url = re.sub(r'/[^/]*\.jpg.*$', '/maxresdefault.jpg', url)
hq_url = re.sub(r'/[^/]*\.jpg.*$', '/hqdefault.jpg', url)
response = requests.get(maxres_url, stream=True)
if response.status_code != 200:
response = requests.get(hq_url, stream=True)
if response.status_code == 200:
with open(file_path, 'wb') as file:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, file)
else:
print('Failed to download thumbnail...!')
sys.exit()
def progress(chunk, file_handle, bytes_remaining):
chunk_size = total_filesize - bytes_remaining
progress_bar.update(chunk_size - progress_bar.n)
if bytes_remaining == 0:
progress_bar.close()

File diff suppressed because it is too large Load Diff

130
pytubepp/postinstaller.py Normal file
View File

@@ -0,0 +1,130 @@
from .utils import ffmpeg_installed, nodejs_installed
import subprocess, platform
def postinstall():
os_type = platform.system().lower()
package_manager = None
print("### PytubePP Post-Install Script ###\n")
print("Checking requirements...")
ffmpeg_needed = not ffmpeg_installed()
nodejs_needed = not nodejs_installed()
if ffmpeg_needed or nodejs_needed:
if os_type == 'windows':
version_info = platform.version().split('.')
if int(version_info[0]) >= 10 and (int(version_info[1]) > 0 or int(version_info[2]) >= 1709):
winget_check = subprocess.run(['winget', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if winget_check.returncode == 0:
print("OS: Windows (winget)")
package_manager = 'winget' # Windows Package Manager
else:
print("OS: Windows (winget not enabled)")
user_input = input("WinGet is not available. Do you want to enable winget? (Make sure to login to Windows before enabling) [yes/no]: ").strip().lower()
if user_input in ['yes', 'y']:
print("Enabling winget...")
subprocess.run(['powershell', '-Command', 'Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe'])
print("WinGet enabled successfully! Please restart your computer and re-run the post install script: pytubepp --postinstall")
return
else:
print("Installation aborted! exiting...!!")
return
else:
print("OS: Windows (winget not supported)")
print("Unsupported Windows version! WinGet requires Windows 10 1709 (build 16299) or later, Please install dependencies manually...!!")
return
elif os_type == 'linux':
# Determine the Linux distribution
if subprocess.run(['command', '-v', 'apt'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0:
print("OS: Linux (apt)")
package_manager = 'apt' # APT for Debian/Ubuntu
elif subprocess.run(['command', '-v', 'dnf'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0:
print("OS: Linux (dnf)")
distro_id = subprocess.run(['grep', '^ID=', '/etc/os-release'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if distro_id.returncode == 0 and 'fedora' in distro_id.stdout.decode().strip() and ffmpeg_needed:
user_input = input("Looks like you are using Fedora. Do you want to enable RPM Fusion free and nonfree repositories? (answer no if already enabled) [yes/no]: ").strip().lower()
if user_input in ['yes', 'y']:
print("Enabling RPM Fusion repositories...")
fedora_version = subprocess.run(['rpm', '-E', '%fedora'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if fedora_version.returncode == 0:
fedora_version_str = fedora_version.stdout.decode().strip()
subprocess.run(['sudo', 'dnf', 'install', f'https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-{fedora_version_str}.noarch.rpm', '-y'], check=True)
subprocess.run(['sudo', 'dnf', 'install', f'https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{fedora_version_str}.noarch.rpm', '-y'], check=True)
else:
print("Failed to retrieve Fedora version. Please install RPM Fusion repositories manually.")
else:
print("RPM Fusion repositories installation skipped...!!")
package_manager = 'dnf' # DNF for Fedora
elif subprocess.run(['command', '-v', 'pacman'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0:
print("OS: Linux (pacman)")
package_manager = 'pacman' # Pacman for Arch Linux
else:
print("OS: Linux (unknown)")
print("Unsupported Linux distribution! Please install dependencies manually...!!")
return
elif os_type == 'darwin':
homebrew_check = subprocess.run(['brew', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if homebrew_check.returncode == 0:
print("OS: MacOS (brew)")
package_manager = 'brew' # Homebrew for macOS
else:
print("OS: MacOS (brew not installed)")
user_input = input("Homebrew is not installed. Do you want to install Homebrew? [yes/no]: ").strip().lower()
if user_input in ['yes', 'y']:
print("Installing Homebrew...")
subprocess.run('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', shell=True)
print("Homebrew installation completed! Please restart your mac and re-run the post install script: pytubepp --postinstall")
return
else:
print("Installation aborted! exiting...!!")
return
else:
print("Unsupported OS! Please install dependencies manually...!!")
return
print("The following packages are about to be installed:")
if ffmpeg_needed:
print("- FFmpeg")
if nodejs_needed:
print("- Node.js")
user_input = input("Do you want to proceed with the installation? [yes/no]: ").strip().lower()
if user_input not in ['yes', 'y']:
print("Installation aborted! exiting...!!")
return
if ffmpeg_needed:
print("Installing FFmpeg...")
install_ffmpeg(package_manager)
if nodejs_needed:
print("Installing Node.js...")
install_nodejs(package_manager)
else:
print("Dependencies already satisfied! exiting...!!")
def install_ffmpeg(package_manager):
if package_manager == 'winget':
subprocess.run(['winget', 'install', 'ffmpeg'], check=True)
elif package_manager == 'apt':
subprocess.run(['sudo', 'apt', 'install', 'ffmpeg', '-y'], check=True)
elif package_manager == 'dnf':
subprocess.run(['sudo', 'dnf', 'install', 'ffmpeg', '-y'], check=True)
elif package_manager == 'pacman':
subprocess.run(['sudo', 'pacman', '-S', 'ffmpeg', '--noconfirm'], check=True)
elif package_manager == 'brew':
subprocess.run(['brew', 'install', 'ffmpeg'], check=True)
print("FFmpeg installation completed")
def install_nodejs(package_manager):
if package_manager == 'winget':
subprocess.run(['winget', 'install', 'OpenJS.NodeJS.LTS'], check=True)
elif package_manager == 'apt':
subprocess.run(['sudo', 'apt', 'install', 'nodejs', '-y'], check=True)
elif package_manager == 'dnf':
subprocess.run(['sudo', 'dnf', 'install', 'nodejs', '-y'], check=True)
elif package_manager == 'pacman':
subprocess.run(['sudo', 'pacman', '-S', 'nodejs-lts-iron', 'npm', '--noconfirm'], check=True)
elif package_manager == 'brew':
subprocess.run(['brew', 'install', 'node'], check=True)
print("Node.js installation completed")

115
pytubepp/postprocess.py Normal file
View File

@@ -0,0 +1,115 @@
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB
from .config import get_temporary_directory, load_config
from .utils import get_unique_filename, postprocess_cleanup, unpack_caption
from .download import download_thumbnail
import os, shutil, ffmpy
userConfig = load_config()
downloadDIR = userConfig['downloadDIR']
tempDIR = get_temporary_directory()
def merge_audio_video(title, resolution, file_extention, random_filename, captions, caption_code=None, tempDIR=tempDIR, downloadDIR=downloadDIR):
video_file = os.path.join(tempDIR, random_filename + '_vdo.' + file_extention)
audio_file = os.path.join(tempDIR, random_filename + '_ado.' + file_extention)
output_temp_file = os.path.join(tempDIR, random_filename + '_merged.' + file_extention)
output_file = os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '.' + file_extention)) if not caption_code else os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '_' + caption_code + '.' + file_extention))
if caption_code:
print(f'Downloading Caption ({caption_code})...')
caption = captions[caption_code]
_, caption_lang = unpack_caption(caption)
srt_file = os.path.join(tempDIR, random_filename + '_cap.srt')
caption.save_captions(srt_file)
vtt_file = os.path.join(tempDIR, random_filename + '_cap.vtt')
print('Processing...')
if file_extention == 'webm':
devnull = open(os.devnull, 'w')
ff_convert = ffmpy.FFmpeg(
inputs={srt_file: None},
outputs={vtt_file: None}
)
ff_convert.run(stdout=devnull, stderr=devnull)
subtitle_file = vtt_file
subtitle_codec = 'webvtt'
else:
subtitle_file = srt_file
subtitle_codec = 'mov_text'
input_params = {video_file: None, audio_file: None}
output_params = {output_temp_file: ['-i', subtitle_file, '-c:v', 'copy', '-c:a', 'copy',
'-c:s', subtitle_codec, '-metadata:s:s:0', f'language={caption_code}',
'-metadata:s:s:0', f'title={caption_lang}', '-metadata:s:s:0', f'handler_name={caption_lang}']}
devnull = open(os.devnull, 'w')
ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params)
ff.run(stdout=devnull, stderr=devnull)
devnull.close()
shutil.move(output_temp_file, output_file)
cleanup_files = ['_vdo.' + file_extention, '_ado.' + file_extention, '_cap.srt', '_merged.' + file_extention]
if file_extention == 'webm':
cleanup_files.append('_cap.vtt')
postprocess_cleanup(tempDIR, cleanup_files, random_filename)
print('Done! 🎉')
else:
input_params = {video_file: None, audio_file: None}
output_params = {output_temp_file: ['-c:v', 'copy', '-c:a', 'copy']}
print('Processing...')
devnull = open(os.devnull, 'w')
ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params)
ff.run(stdout=devnull, stderr=devnull)
devnull.close()
shutil.move(output_temp_file, output_file)
postprocess_cleanup(tempDIR, ['_vdo.' + file_extention, '_ado.' + file_extention, '_merged.' + file_extention], random_filename)
print('Done! 🎉')
def convert_to_mp3(title, thumbnail_url, random_filename, mp3_artist='Unknown', mp3_title='Unknown', mp3_album='Unknown', tempDIR=tempDIR, downloadDIR=downloadDIR):
image_file = os.path.join(tempDIR, random_filename + '_thumbnail.jpg')
download_thumbnail(thumbnail_url, image_file)
audio_file = os.path.join(tempDIR, random_filename + '_ado.mp4')
output_file = os.path.join(downloadDIR, get_unique_filename(title + '_audio.mp3'))
print('Processing...')
devnull = open(os.devnull, 'w')
video_file = os.path.join(tempDIR, random_filename + '_thumbnail.mp4')
ff1 = ffmpy.FFmpeg(
inputs={image_file: '-loop 1 -t 1'},
outputs={video_file: '-vf "scale=1280:720" -r 1 -c:v libx264 -t 1'}
)
ff1.run(stdout=devnull, stderr=devnull)
merged_file = os.path.join(tempDIR, random_filename + '_merged.mp4')
ff2 = ffmpy.FFmpeg(
inputs={video_file: None, audio_file: None},
outputs={merged_file: '-c:v copy -c:a copy'}
)
ff2.run(stdout=devnull, stderr=devnull)
output_temp_file = os.path.join(tempDIR, random_filename + '_merged.mp3')
ff3 = ffmpy.FFmpeg(
inputs={merged_file: None},
outputs={output_temp_file: '-vn -c:a libmp3lame -q:a 2'}
)
ff3.run(stdout=devnull, stderr=devnull)
devnull.close()
audio = ID3(output_temp_file)
audio.add(TIT2(encoding=3, text=mp3_title))
audio.add(TPE1(encoding=3, text=mp3_artist))
audio.add(TALB(encoding=3, text=mp3_album))
with open(image_file, 'rb') as img:
audio.add(APIC(
encoding=3,
mime='image/jpeg',
type=3,
desc=u'Cover',
data=img.read()
))
audio.save()
shutil.move(output_temp_file, output_file)
postprocess_cleanup(tempDIR, ['_thumbnail.jpg', '_thumbnail.mp4', '_ado.mp4', '_merged.mp4'], random_filename)
print('Done! 🎉')

81
pytubepp/utils.py Normal file
View File

@@ -0,0 +1,81 @@
from importlib.metadata import version
from .config import load_config, get_temporary_directory
import os, re, subprocess, platform
userConfig = load_config()
downloadDIR = userConfig['downloadDIR']
tempDIR = get_temporary_directory()
def network_available():
try:
param = '-n' if platform.system().lower() == 'windows' else '-c'
command = ['ping', param, '1', 'youtube.com']
subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
return True
except subprocess.CalledProcessError:
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():
try:
return version('pytubepp')
except Exception as e:
return "Unknown"
def is_valid_url(url):
match = re.search(r"(https?://(?:www\.|music\.)?youtube\.com/(?:watch\?v=[^&]{11}|shorts/[^?&]+)|https?://youtu\.be/[^?&]*(\?si=[^&]*)?)", url)
return match
def get_unique_filename(filename, directory=downloadDIR):
base_name, extension = os.path.splitext(filename)
counter = 1
while os.path.exists(os.path.join(directory, filename)):
filename = f"{base_name} ({counter}){extension}"
counter += 1
return filename
def unpack_caption(caption):
caption_str = str(caption)
code_start = caption_str.find('code="') + 6
code_end = caption_str.find('"', code_start)
lang_start = caption_str.find('lang="') + 6
lang_end = caption_str.find('"', lang_start)
code = caption_str[code_start:code_end]
lang = caption_str[lang_start:lang_end]
return code, lang
def postprocess_cleanup(dir, files, random_filename):
for file in files:
file_path = os.path.join(dir, random_filename + file)
try:
if os.path.isfile(file_path):
os.remove(file_path)
except Exception as e:
print(e)
def clear_temp_files():
if os.listdir(tempDIR) != []:
for file in os.listdir(tempDIR):
file_path = os.path.join(tempDIR, file)
try:
if os.path.isfile(file_path):
os.remove(file_path)
print(f'Removed: {file}')
except Exception as e:
print(e)
else:
print('No temporary files found to clear...!')

View File

@@ -7,4 +7,5 @@ tqdm
appdirs
setuptools
wheel
twine
twine
build

View File

@@ -1,59 +0,0 @@
from setuptools import setup, find_packages
with open('README.md', 'r', encoding='utf8') as file:
readme = file.read()
setup(
name='pytubepp',
version='1.0.5',
description='A Simple CLI Tool to Download Your Favorite YouTube Videos Effortlessly!',
long_description=readme,
long_description_content_type='text/markdown',
author='Subhamoy Biswas',
author_email='hey@neosubhamoy.com',
license='MIT',
packages=find_packages(),
python_requires=">=3.8",
url="https://github.com/neosubhamoy/pytubepp",
entry_points={
'console_scripts': [
'pytubepp=pytubepp.main:main',
],
},
install_requires=[
'pytubefix',
'requests',
'ffmpy',
'mutagen',
'tabulate',
'tqdm',
'appdirs',
'setuptools',
],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python",
"Topic :: Internet",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Sound/Audio :: Conversion",
"Topic :: Multimedia :: Video :: Conversion",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Terminals",
"Topic :: Utilities",
],
keywords=["youtube", "download", "video", "pytube", "cli"],
project_urls={
"Bug Reports": "https://github.com/neosubhamoy/pytubepp/issues",
},
)