mirror of
https://github.com/neosubhamoy/pytubepp.git
synced 2026-02-05 02:32:23 +05:30
Compare commits
16 Commits
v1.1.4-sta
...
v1.1.7-sta
62
README.md
62
README.md
@@ -13,6 +13,12 @@
|
|||||||
|
|
||||||
> **🥰 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!**
|
> **🥰 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**
|
### **🏷️ Features**
|
||||||
* Auto Post-Process & Merge YouTube DASH Streams
|
* Auto Post-Process & Merge YouTube DASH Streams
|
||||||
* Supports upto 8K 60fps HDR Stream Download
|
* Supports upto 8K 60fps HDR Stream Download
|
||||||
@@ -44,49 +50,62 @@
|
|||||||
- Windows (10/11): `winget install Python.Python.3.13`<br>
|
- Windows (10/11): `winget install Python.Python.3.13`<br>
|
||||||
- MacOS (using Homebrew): `brew install python`<br>
|
- MacOS (using Homebrew): `brew install python`<br>
|
||||||
- Android (using Termux): `pkg install python`
|
- 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
|
2. Install FFmpeg
|
||||||
- Linux (Debian): `sudo apt install ffmpeg`<br>
|
- Linux (Debian): `sudo apt install ffmpeg`<br>
|
||||||
- Linux (Fedora): `sudo dnf install ffmpeg-free`<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>
|
- Linux (Arch): `sudo pacman -S ffmpeg`<br>
|
||||||
- Windows (10/11): `winget install ffmpeg`<br>
|
- Windows (10/11): `winget install ffmpeg`<br>
|
||||||
- MacOS (using Homebrew): `brew install ffmpeg`<br>
|
- MacOS (using Homebrew): `brew install ffmpeg`<br>
|
||||||
- Android (using Termux): `pkg install ffmpeg`
|
- Android (using Termux): `pkg install ffmpeg`
|
||||||
3. Install Node.js
|
3. Install Node.js
|
||||||
- Linux (Debian): `curl -o- https://fnm.vercel.app/install | bash && fnm install --lts && fnm use lts`<br>
|
- Linux (Debian): `sudo apt install nodejs`<br>
|
||||||
- Linux (Fedora): `curl -o- https://fnm.vercel.app/install | bash && fnm install --lts && fnm use lts`<br>
|
- Linux (Fedora): `sudo dnf install nodejs`<br>
|
||||||
- Linux (Arch): `curl -o- https://fnm.vercel.app/install | bash && fnm install --lts && fnm use lts`<br>
|
- Linux (Arch): `sudo pacman -S nodejs-lts-iron npm`<br>
|
||||||
- Windows (10/11): `winget install Schniz.fnm;fnm install --lts;fnm use lts`<br>
|
- Windows (10/11): `winget install OpenJS.NodeJS.LTS`<br>
|
||||||
- MacOS (using Homebrew): `brew install node`<br>
|
- MacOS (using Homebrew): `brew install node`<br>
|
||||||
- Android (using Termux): `pkg install nodejs`
|
- Android (using Termux): `pkg install nodejs`
|
||||||
4. Install PytubePP (using PIP)
|
4. Install PytubePP (using PIP)
|
||||||
|
|
||||||
> 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
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE: 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)**
|
**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)**
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
@@ -98,20 +117,21 @@ 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).
|
* 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:
|
* List of all available flags are given below:
|
||||||
|
|
||||||
| Flag | Usage | Requires Parameter | Requires URL | Parameters | Default |
|
| Short Flag | 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 | --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 | 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 | --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 | Shows the video information like: Title, Author, Views, Publication Date, Duration, Available Download Streams | NO | YES | No parameters | No default |
|
| -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 | Lists all available streams (video, audio, caption) (only for debuging purposes) | 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 | Shows the video information in raw json format | NO | YES | No parameters | No default |
|
| -ri | --raw-info | 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 | --json-prettify | 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 | --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 | Set default caption | YES | NO | All [ISO 639-1 Language Codes](https://www.w3schools.com/tags/ref_language_codes.asp) + some others + `none` for No Caption (Pass any one of them) eg: `en` for English | `none` |
|
| -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 | 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 |
|
| -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 to default configuration (Download Folder, Default Stream) | NO | NO | No parameters | No default |
|
| -r | --reset-default | 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 |
|
| -sc | --show-config | 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 |
|
| -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
|
### 🛠️ Contributing / Building from Source
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pytubepp"
|
name = "pytubepp"
|
||||||
version = "1.1.4"
|
version = "1.1.7"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Subhamoy Biswas", email="hey@neosubhamoy.com" },
|
{ name="Subhamoy Biswas", email="hey@neosubhamoy.com" },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from .config import get_temporary_directory, load_config
|
from .config import get_temporary_directory, load_config
|
||||||
from .utils import get_unique_filename, postprocess_cleanup
|
from .utils import get_unique_filename, postprocess_cleanup, unpack_caption
|
||||||
import os, re, requests, shutil, sys, random, ffmpy
|
import os, re, requests, shutil, sys, random, ffmpy
|
||||||
|
|
||||||
userConfig = load_config()
|
userConfig = load_config()
|
||||||
@@ -21,6 +21,7 @@ def download_progressive(stream, itag, title, resolution, file_extention, captio
|
|||||||
if caption_code:
|
if caption_code:
|
||||||
print(f'Downloading Caption ({caption_code})...')
|
print(f'Downloading Caption ({caption_code})...')
|
||||||
caption = captions[caption_code]
|
caption = captions[caption_code]
|
||||||
|
_, caption_lang = unpack_caption(caption)
|
||||||
caption_file = os.path.join(tempDIR, random_filename + '_cap.srt')
|
caption_file = os.path.join(tempDIR, random_filename + '_cap.srt')
|
||||||
caption.save_captions(caption_file)
|
caption.save_captions(caption_file)
|
||||||
print('Processing...')
|
print('Processing...')
|
||||||
@@ -28,7 +29,7 @@ def download_progressive(stream, itag, title, resolution, file_extention, captio
|
|||||||
output_temp_file_with_subs = os.path.join(tempDIR, random_filename + '_merged.' + file_extention)
|
output_temp_file_with_subs = os.path.join(tempDIR, random_filename + '_merged.' + file_extention)
|
||||||
ff = ffmpy.FFmpeg(
|
ff = ffmpy.FFmpeg(
|
||||||
inputs={output_temp_file: None},
|
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_code}', '-metadata:s:s:0', f'handler_name={caption_code}']}
|
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)
|
ff.run(stdout=devnull, stderr=devnull)
|
||||||
devnull.close()
|
devnull.close()
|
||||||
|
|||||||
131
pytubepp/main.py
131
pytubepp/main.py
@@ -3,7 +3,8 @@ 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, unpack_caption
|
||||||
|
from .postinstaller import postinstall
|
||||||
import appdirs, os, re, sys, argparse, json
|
import appdirs, os, re, sys, argparse, json
|
||||||
|
|
||||||
class YouTubeDownloader:
|
class YouTubeDownloader:
|
||||||
@@ -42,6 +43,11 @@ 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, by running: pytubepp --postinstall or read https://github.com/neosubhamoy/pytubepp#%EF%B8%8F-installation for manual instructions\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)
|
||||||
@@ -99,6 +105,13 @@ class YouTubeDownloader:
|
|||||||
|
|
||||||
# Use HDR stream if available, otherwise use the original stream
|
# Use HDR stream if available, otherwise use the original stream
|
||||||
final_stream = hdr_stream if hdr_stream else matching_stream
|
final_stream = hdr_stream if hdr_stream else matching_stream
|
||||||
|
|
||||||
|
# For 720p, check if HigherFps MP4 version exists and prefer it
|
||||||
|
if res == '720p' and not hdr_stream:
|
||||||
|
higher_fps_stream = self.stream.get_by_itag(298)
|
||||||
|
if higher_fps_stream:
|
||||||
|
final_stream = higher_fps_stream
|
||||||
|
|
||||||
audio_stream = self.stream.get_by_itag(_select_suitable_audio_stream(final_stream))
|
audio_stream = self.stream.get_by_itag(_select_suitable_audio_stream(final_stream))
|
||||||
|
|
||||||
total_size = final_stream.filesize + audio_stream.filesize
|
total_size = final_stream.filesize + audio_stream.filesize
|
||||||
@@ -146,9 +159,19 @@ class YouTubeDownloader:
|
|||||||
print('Sorry, No video streams found....!!!')
|
print('Sorry, No video streams found....!!!')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
print(f'\nTitle: {self.video.title}\nAuthor: {self.author}\nPublished On: {self.video.publish_date.strftime("%d/%m/%Y")}\nDuration: {f"{self.video.length//3600:02}:{(self.video.length%3600)//60:02}:{self.video.length%60:02}" if self.video.length >= 3600 else f"{(self.video.length%3600)//60:02}:{self.video.length%60:02}"}\nViews: {self.views}\nCaptions: {[caption.code for caption in self.captions.keys()] or "Unavailable"}\n')
|
print(f'\nTitle: {self.video.title}\nAuthor: {self.author}\nPublished On: {self.video.publish_date.strftime("%d/%m/%Y")}\nDuration: {f"{self.video.length//3600:02}:{(self.video.length%3600)//60:02}:{self.video.length%60:02}" if self.video.length >= 3600 else f"{(self.video.length%3600)//60:02}:{self.video.length%60:02}"}\nViews: {self.views}\nCaptions: {"Available" if self.captions else "Unavailable"}')
|
||||||
|
|
||||||
|
print('\n')
|
||||||
print(tabulate(table, headers=['Stream', 'Alias (for -s flag)', 'Format', 'Size', 'FrameRate', 'V-Codec', 'A-Codec', 'V-BitRate', 'A-BitRate']))
|
print(tabulate(table, headers=['Stream', 'Alias (for -s flag)', 'Format', 'Size', 'FrameRate', 'V-Codec', 'A-Codec', 'V-BitRate', 'A-BitRate']))
|
||||||
print('\n')
|
print('\n')
|
||||||
|
|
||||||
|
if self.captions:
|
||||||
|
caption_table = []
|
||||||
|
for caption in self.captions:
|
||||||
|
cap_code, cap_lang = unpack_caption(caption)
|
||||||
|
caption_table.append([cap_lang, cap_code])
|
||||||
|
print(tabulate(caption_table, headers=['Caption', 'CaptionCode (for -c flag)']))
|
||||||
|
print('\n')
|
||||||
else:
|
else:
|
||||||
print('\nInvalid video link! Please enter a valid video url...!!')
|
print('\nInvalid video link! Please enter a valid video url...!!')
|
||||||
|
|
||||||
@@ -199,6 +222,15 @@ class YouTubeDownloader:
|
|||||||
print('Sorry, No video streams found....!!!')
|
print('Sorry, No video streams found....!!!')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
captions_list = []
|
||||||
|
if self.captions:
|
||||||
|
for caption in self.captions:
|
||||||
|
cap_code, cap_lang = unpack_caption(caption)
|
||||||
|
captions_list.append({
|
||||||
|
'code': cap_code,
|
||||||
|
'lang': cap_lang
|
||||||
|
})
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
'id': self.video.video_id,
|
'id': self.video.video_id,
|
||||||
'title': self.video.title,
|
'title': self.video.title,
|
||||||
@@ -208,7 +240,7 @@ class YouTubeDownloader:
|
|||||||
'published_on': self.video.publish_date.strftime('%d/%m/%Y'),
|
'published_on': self.video.publish_date.strftime('%d/%m/%Y'),
|
||||||
'duration': self.video.length,
|
'duration': self.video.length,
|
||||||
'streams': streams_list,
|
'streams': streams_list,
|
||||||
'captions': [caption.code for caption in self.captions.keys()] or None
|
'captions': captions_list or None
|
||||||
}
|
}
|
||||||
|
|
||||||
print(json.dumps(output, indent=4 if prettify else None))
|
print(json.dumps(output, indent=4 if prettify else None))
|
||||||
@@ -235,22 +267,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, by running: pytubepp --postinstall or read https://github.com/neosubhamoy/pytubepp#%EF%B8%8F-installation for manual 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)
|
||||||
@@ -260,7 +308,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']:
|
||||||
@@ -341,6 +389,7 @@ def main():
|
|||||||
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')
|
||||||
parser.add_argument('-r', '--reset-default', action='store_true', help='reset to default settings (download_folder and default_stream)')
|
parser.add_argument('-r', '--reset-default', action='store_true', help='reset to default settings (download_folder and default_stream)')
|
||||||
parser.add_argument('-ct', '--clear-temp', action='store_true', help='clear temporary files (audio, video, thumbnail files of the failed, incomplete downloads)')
|
parser.add_argument('-ct', '--clear-temp', action='store_true', help='clear temporary files (audio, video, thumbnail files of the failed, incomplete downloads)')
|
||||||
|
parser.add_argument('-pi', '--postinstall', action='store_true', help='auto install external dependencies (supported os: windows, linux - debian fedora arch, macos)')
|
||||||
parser.add_argument('-v', '--version', action='store_true', help='show version number')
|
parser.add_argument('-v', '--version', action='store_true', help='show version number')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -367,11 +416,15 @@ def main():
|
|||||||
print('\nVideo url supplied! ignoreing -ct flag...!!')
|
print('\nVideo url supplied! ignoreing -ct flag...!!')
|
||||||
if args.show_config:
|
if args.show_config:
|
||||||
print('\nVideo url supplied! ignoreing -sc flag...!!')
|
print('\nVideo url supplied! ignoreing -sc flag...!!')
|
||||||
|
if args.postinstall:
|
||||||
|
print('\nVideo url supplied! ignoreing -pi flag...!!')
|
||||||
|
|
||||||
# Handle info display flags
|
# Handle info display flags
|
||||||
if args.show_info:
|
if args.show_info:
|
||||||
|
print('Loading...')
|
||||||
downloader.show_video_info(args.url)
|
downloader.show_video_info(args.url)
|
||||||
if args.list_stream:
|
if args.list_stream:
|
||||||
|
print('Loading...')
|
||||||
downloader.show_all_streams(args.url)
|
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)
|
||||||
@@ -380,10 +433,13 @@ def main():
|
|||||||
|
|
||||||
# Handle download cases
|
# Handle download cases
|
||||||
if hasattr(args, 'stream') and hasattr(args, 'caption'):
|
if hasattr(args, 'stream') and hasattr(args, 'caption'):
|
||||||
|
print('Loading...')
|
||||||
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')
|
||||||
@@ -397,6 +453,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
downloader.download_stream(args.url, args.stream, args.caption)
|
downloader.download_stream(args.url, args.stream, args.caption)
|
||||||
elif hasattr(args, 'stream'):
|
elif hasattr(args, 'stream'):
|
||||||
|
print('Loading...')
|
||||||
if downloader.set_video_info(args.url):
|
if downloader.set_video_info(args.url):
|
||||||
if downloader.default_caption == 'none':
|
if downloader.default_caption == 'none':
|
||||||
downloader.download_stream(args.url, args.stream)
|
downloader.download_stream(args.url, args.stream)
|
||||||
@@ -423,10 +480,31 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print('Download cancelled! exiting...!!')
|
print('Download cancelled! exiting...!!')
|
||||||
elif hasattr(args, 'caption'):
|
elif hasattr(args, 'caption'):
|
||||||
|
print('Loading...')
|
||||||
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):
|
||||||
@@ -455,6 +533,7 @@ def main():
|
|||||||
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, args.list_stream]): # 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
|
||||||
|
print('Loading...')
|
||||||
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':
|
||||||
@@ -544,8 +623,13 @@ def main():
|
|||||||
|
|
||||||
if hasattr(args, 'default_caption'):
|
if hasattr(args, 'default_caption'):
|
||||||
if args.default_caption != downloader.default_caption:
|
if args.default_caption != downloader.default_caption:
|
||||||
if not all(c.isalpha() or c in '.-' for c in args.default_caption) or len(args.default_caption) > 10:
|
if not (re.match(r'^[a-z]{2}(-[A-Za-z]+)?$', args.default_caption) or
|
||||||
print('\nInvalid caption code! Only a-z, A-Z, dash (-) and dot (.) are allowed with maximum 10 characters...!!')
|
re.match(r'^a\.[a-z]{2}(-[A-Za-z]+)?$', args.default_caption) or
|
||||||
|
re.match(r'^none$', args.default_caption)):
|
||||||
|
print('\nInvalid caption code! Allowed formats are:\n'
|
||||||
|
'- ISO 639-1 language codes (e.g: en, zh-Hans)\n'
|
||||||
|
'- Auto-generated variants: a.ISO639-1LanguageCode (e.g: a.en, a.zh-Hans)\n'
|
||||||
|
'- none\n')
|
||||||
else:
|
else:
|
||||||
update_config('defaultCaption', args.default_caption)
|
update_config('defaultCaption', args.default_caption)
|
||||||
print(f'\nDefault caption updated to: {args.default_caption}')
|
print(f'\nDefault caption updated to: {args.default_caption}')
|
||||||
@@ -561,6 +645,9 @@ def main():
|
|||||||
if args.show_config:
|
if args.show_config:
|
||||||
print(f'\ntempDIR: {downloader.temp_dir} (Unchangeable) \nconfigDIR: {downloader.config_dir} (Unchangeable)\ndownloadDIR: {downloader.download_dir}\ndefaultStream: {downloader.default_stream}\ndefaultCaption: {downloader.default_caption}\n')
|
print(f'\ntempDIR: {downloader.temp_dir} (Unchangeable) \nconfigDIR: {downloader.config_dir} (Unchangeable)\ndownloadDIR: {downloader.download_dir}\ndefaultStream: {downloader.default_stream}\ndefaultCaption: {downloader.default_caption}\n')
|
||||||
|
|
||||||
|
if args.postinstall:
|
||||||
|
postinstall()
|
||||||
|
|
||||||
if args.version:
|
if args.version:
|
||||||
print(f'pytubepp {downloader.version}')
|
print(f'pytubepp {downloader.version}')
|
||||||
|
|
||||||
|
|||||||
130
pytubepp/postinstaller.py
Normal file
130
pytubepp/postinstaller.py
Normal 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")
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB
|
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB
|
||||||
from .config import get_temporary_directory, load_config
|
from .config import get_temporary_directory, load_config
|
||||||
from .utils import get_unique_filename, postprocess_cleanup
|
from .utils import get_unique_filename, postprocess_cleanup, unpack_caption
|
||||||
from .download import download_thumbnail
|
from .download import download_thumbnail
|
||||||
import os, shutil, ffmpy
|
import os, shutil, ffmpy
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ def merge_audio_video(title, resolution, file_extention, random_filename, captio
|
|||||||
if caption_code:
|
if caption_code:
|
||||||
print(f'Downloading Caption ({caption_code})...')
|
print(f'Downloading Caption ({caption_code})...')
|
||||||
caption = captions[caption_code]
|
caption = captions[caption_code]
|
||||||
|
_, caption_lang = unpack_caption(caption)
|
||||||
srt_file = os.path.join(tempDIR, random_filename + '_cap.srt')
|
srt_file = os.path.join(tempDIR, random_filename + '_cap.srt')
|
||||||
caption.save_captions(srt_file)
|
caption.save_captions(srt_file)
|
||||||
vtt_file = os.path.join(tempDIR, random_filename + '_cap.vtt')
|
vtt_file = os.path.join(tempDIR, random_filename + '_cap.vtt')
|
||||||
@@ -38,7 +39,7 @@ def merge_audio_video(title, resolution, file_extention, random_filename, captio
|
|||||||
input_params = {video_file: None, audio_file: None}
|
input_params = {video_file: None, audio_file: None}
|
||||||
output_params = {output_temp_file: ['-i', subtitle_file, '-c:v', 'copy', '-c:a', 'copy',
|
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}',
|
'-c:s', subtitle_codec, '-metadata:s:s:0', f'language={caption_code}',
|
||||||
'-metadata:s:s:0', f'title={caption_code}', '-metadata:s:s:0', f'handler_name={caption_code}']}
|
'-metadata:s:s:0', f'title={caption_lang}', '-metadata:s:s:0', f'handler_name={caption_lang}']}
|
||||||
|
|
||||||
devnull = open(os.devnull, 'w')
|
devnull = open(os.devnull, 'w')
|
||||||
ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params)
|
ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params)
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -22,7 +36,7 @@ def get_version():
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
def is_valid_url(url):
|
def is_valid_url(url):
|
||||||
match = re.search(r"(https?://(?:www\.|music\.)?youtube\.com/watch\?v=[^&]{11}|https?://youtu\.be/[^?&]*(\?si=[^&]*)?)", url)
|
match = re.search(r"(https?://(?:www\.|music\.)?youtube\.com/(?:watch\?v=[^&]{11}|shorts/[^?&]+)|https?://youtu\.be/[^?&]*(\?si=[^&]*)?)", url)
|
||||||
return match
|
return match
|
||||||
|
|
||||||
def get_unique_filename(filename, directory=downloadDIR):
|
def get_unique_filename(filename, directory=downloadDIR):
|
||||||
@@ -33,6 +47,17 @@ def get_unique_filename(filename, directory=downloadDIR):
|
|||||||
counter += 1
|
counter += 1
|
||||||
return filename
|
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):
|
def postprocess_cleanup(dir, files, random_filename):
|
||||||
for file in files:
|
for file in files:
|
||||||
file_path = os.path.join(dir, random_filename + file)
|
file_path = os.path.join(dir, random_filename + file)
|
||||||
|
|||||||
Reference in New Issue
Block a user