diff --git a/objects/Encoder.php b/objects/Encoder.php index 6dff7f5e..285ee211 100644 --- a/objects/Encoder.php +++ b/objects/Encoder.php @@ -543,12 +543,88 @@ static function isPythonAndPytubeInstalled() return true; } - public static function downloadWithPytube($video_url, $filename) + public static function getTitleFromLinkWithPytube($video_url) + { + global $global; + + $downloadWithPytubeFilename = 'video_download_' . md5($video_url); + $metadataFile = "{$global['systemRootPath']}videos/pytube/{$downloadWithPytubeFilename}/metadata.json"; + if(!file_exists($metadataFile)){ + $response = self::downloadWithPytube($video_url, $downloadWithPytubeFilename, 'metadata'); + } + + if(file_exists($metadataFile)){ + $content = file_get_contents($metadataFile); + $json = json_decode($content); + return $json->title; + } + return false; + } + + public static function getDescriptionFromLinkWithPytube($video_url) + { + global $global; + + $downloadWithPytubeFilename = 'video_download_' . md5($video_url); + $metadataFile = "{$global['systemRootPath']}videos/pytube/{$downloadWithPytubeFilename}/metadata.json"; + if(!file_exists($metadataFile)){ + $response = self::downloadWithPytube($video_url, $downloadWithPytubeFilename, 'metadata'); + } + + if(file_exists($metadataFile)){ + $content = file_get_contents($metadataFile); + $json = json_decode($content); + return $json->description; + } + return false; + } + + public static function getDurationFromLinkWithPytube($video_url) + { + global $global; + + $downloadWithPytubeFilename = 'video_download_' . md5($video_url); + $metadataFile = "{$global['systemRootPath']}videos/pytube/{$downloadWithPytubeFilename}/metadata.json"; + if(!file_exists($metadataFile)){ + $response = self::downloadWithPytube($video_url, $downloadWithPytubeFilename, 'metadata'); + } + + if(file_exists($metadataFile)){ + $content = file_get_contents($metadataFile); + $json = json_decode($content); + return $json->duration_seconds; + } + return false; + } + + public static function getThumbsFromLinkWithPytube($video_url, $returnFileName = false) + { + global $global; + + $downloadWithPytubeFilename = 'video_download_' . md5($video_url); + $File = "{$global['systemRootPath']}videos/pytube/{$downloadWithPytubeFilename}/thumbs.jpg"; + if(!file_exists($File)){ + $response = self::downloadWithPytube($video_url, $downloadWithPytubeFilename, 'thumbnail'); + } + + if(file_exists($File)){ + if ($returnFileName) { + return $File; + } else { + $content = url_get_contents($File); + //unlink($returnTmpfname); + return $content; + } + } + return false; + } + + public static function downloadWithPytube($video_url, $filename, $action = 'video') { global $global; $pythonScript = $global['systemRootPath'] . "objects/youtube.py"; - $command = escapeshellcmd("python3 $pythonScript " . escapeshellarg($video_url) . " " . escapeshellarg($filename)); + $command = escapeshellcmd("python3 $pythonScript " . escapeshellarg($video_url) . " " . escapeshellarg($filename)." {$action}"); _error_log("downloadWithPytube($video_url, $filename) " . $command); exec($command, $output, $return_var); @@ -556,6 +632,10 @@ public static function downloadWithPytube($video_url, $filename) $response->command = $command; $response->output = $output; $response->error = $return_var !== 0; + $response->filename = $filename; + $response->metadata = "{$global['systemRootPath']}videos/pytube/{$response->filename}/metadata.json"; + $response->thumbnail = "{$global['systemRootPath']}videos/pytube/{$response->filename}/thumbs.jpg"; + $response->video = "{$global['systemRootPath']}videos/pytube/{$response->filename}/video.mp4"; if ($response->error) { $response->msg = "Error downloading video. Check progress.json for details."; @@ -2704,13 +2784,20 @@ public static function getReverseVideosJsonListFromLink($link, $streamers_id, $a public static function getTitleFromLink($link, $streamers_id, $addOauthFromProvider = '') { + if (self::isPythonAndPytubeInstalled() && isYouTubeUrl($link)) { + $resp = self::getTitleFromLinkWithPytube($link); + if(!empty($resp)){ + return array('error' => false, 'output' => $resp); + } + } $prepend = ''; if (!isWindows()) { $prepend = 'LC_ALL=en_US.UTF-8 '; } + $link = str_replace("'", '', $link); $link = escapeshellarg($link); - $response = array('error' => true, 'output' => array()); + $response = array('error' => true, 'output' => ''); $cmd = $prepend . self::getYouTubeDLCommand($addOauthFromProvider, $streamers_id) . " --no-check-certificate --no-playlist --force-ipv4 --skip-download -e {$link}"; exec($cmd . " 2>&1", $output, $return_val); if ($return_val !== 0) { @@ -2730,6 +2817,12 @@ public static function getTitleFromLink($link, $streamers_id, $addOauthFromProvi public static function getDurationFromLink($link, $streamers_id, $addOauthFromProvider = '') { + if (self::isPythonAndPytubeInstalled() && isYouTubeUrl($link)) { + $resp = self::getDurationFromLinkWithPytube($link); + if(!empty($resp)){ + return static::parseSecondsToDuration($resp); + } + } $link = escapeshellarg($link); $cmd = self::getYouTubeDLCommand($addOauthFromProvider, $streamers_id) . " --no-check-certificate --no-playlist --force-ipv4 --get-duration --skip-download {$link}"; exec($cmd . " 2>&1", $output, $return_val); @@ -2751,6 +2844,13 @@ public static function getDurationFromLink($link, $streamers_id, $addOauthFromPr public static function getThumbsFromLink($link, $streamers_id, $returnFileName = false, $addOauthFromProvider = '') { + if (self::isPythonAndPytubeInstalled() && isYouTubeUrl($link)) { + $resp = self::getThumbsFromLinkWithPytube($link, $returnFileName); + if(!empty($resp)){ + return $resp; + } + } + $link = str_replace(array('"', "'"), array('', ''), $link); $link = escapeshellarg($link); @@ -2796,6 +2896,13 @@ public static function getDescriptionFromLink($link, $streamers_id, $addOauthFro if (empty($link)) { return ''; } + + if (self::isPythonAndPytubeInstalled() && isYouTubeUrl($link)) { + $resp = self::getDescriptionFromLinkWithPytube($link); + if(!empty($resp)){ + return $resp; + } + } $link = escapeshellarg($link); $tmpfname = _get_temp_file('thumbs'); $cmd = self::getYouTubeDLCommand($addOauthFromProvider, $streamers_id) . " --no-check-certificate --no-playlist --force-ipv4 --write-description --skip-download -o \"{$tmpfname}\" {$link}"; @@ -2836,6 +2943,11 @@ public static function streamerHasOauth($OauthFromProvider, $streamers_id = 0) public static function getYouTubeDLCommand($addOauthFromProvider = '', $streamers_id = 0, $forceYoutubeDL = false) { global $global; + $cacheDir = '/var/www/.cache/'; + if(!is_dir($cacheDir)){ + @mkdir($cacheDir); + } + $ytdl = "youtube-dl "; if (!empty($global['youtube-dl'])) { $ytdl = $global['youtube-dl'] . ' '; diff --git a/objects/youtube.py b/objects/youtube.py index 0d9d2c94..7d909315 100644 --- a/objects/youtube.py +++ b/objects/youtube.py @@ -5,6 +5,7 @@ import sys import subprocess import urllib.request +from datetime import datetime, timedelta # Function to ensure pytube is installed def ensure_pytube_installed(): @@ -64,23 +65,50 @@ def patched_get_throttling_function_name(js: str) -> str: def save_metadata(yt, folder): metadata = { - "title": yt.title, - "description": yt.description, - "url": yt.watch_url + "title": yt.title if yt.title else "No Title", + "description": yt.description if yt.description else "No Description", + "url": yt.watch_url, + "duration_seconds": yt.length, # Add duration in seconds + "created_date": datetime.now().isoformat() # Track creation time } - with open(os.path.join(folder, "metadata.json"), "w") as meta_file: + os.makedirs(folder, exist_ok=True) + metadata_file_path = os.path.join(folder, "metadata.json") + with open(metadata_file_path, "w") as meta_file: json.dump(metadata, meta_file, indent=4) + print(f"Metadata saved successfully to '{metadata_file_path}'.") def save_thumbnail(yt, folder): - thumbnail_url = yt.thumbnail_url + """Save the highest resolution thumbnail available.""" + video_id = yt.video_id + thumbnail_urls = [ + f"https://img.youtube.com/vi/{video_id}/maxresdefault.jpg", # Highest resolution + f"https://img.youtube.com/vi/{video_id}/sddefault.jpg", # Standard definition + f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg", # High quality + f"https://img.youtube.com/vi/{video_id}/mqdefault.jpg", # Medium quality + yt.thumbnail_url # Default thumbnail + ] + thumbnail_path = os.path.join(folder, "thumbs.jpg") - urllib.request.urlretrieve(thumbnail_url, thumbnail_path) + os.makedirs(folder, exist_ok=True) + + for url in thumbnail_urls: + try: + urllib.request.urlretrieve(url, thumbnail_path) + print(f"Thumbnail downloaded successfully to '{thumbnail_path}' from URL: {url}") + return # Exit the loop on success + except Exception as e: + print(f"Failed to download thumbnail from '{url}': {e}") + + print(f"Could not download any thumbnails for video '{yt.title}'.") + + def download_video(yt, folder): video_stream = yt.streams.get_highest_resolution() video_path = os.path.join(folder, "video.mp4") yt.register_on_progress_callback(lambda stream, chunk, bytes_remaining: save_progress(stream, bytes_remaining, folder)) video_stream.download(output_path=folder, filename="video.mp4") + print(f"Video downloaded successfully to '{video_path}'.") def save_progress(stream, bytes_remaining, folder): total_size = stream.filesize @@ -90,28 +118,63 @@ def save_progress(stream, bytes_remaining, folder): "downloaded": downloaded, "progress": round((downloaded / total_size) * 100, 2) } - with open(os.path.join(folder, "progress.json"), "w") as progress_file: + os.makedirs(folder, exist_ok=True) + progress_file_path = os.path.join(folder, "progress.json") + with open(progress_file_path, "w") as progress_file: json.dump(progress, progress_file, indent=4) + print(f"Progress saved to '{progress_file_path}'.") + +def clean_old_folders(base_folder, days=7): + """Delete folders older than a specified number of days.""" + now = datetime.now() + cutoff = now - timedelta(days=days) + + for folder in os.listdir(base_folder): + folder_path = os.path.join(base_folder, folder) + if os.path.isdir(folder_path): + metadata_path = os.path.join(folder_path, "metadata.json") + if os.path.exists(metadata_path): + try: + with open(metadata_path, "r") as meta_file: + metadata = json.load(meta_file) + created_date = datetime.fromisoformat(metadata.get("created_date")) + if created_date < cutoff: + print(f"Deleting folder '{folder_path}' (created on {created_date})") + subprocess.call(["rm", "-rf", folder_path]) + except Exception as e: + print(f"Error processing folder '{folder_path}': {e}") def main(): - if len(sys.argv) != 3: - print("Usage: python yt_downloader.py ") + if len(sys.argv) < 3: + print("Usage: python yt_downloader.py [metadata|thumbnail|video|all]") sys.exit(1) + # Get the directory where the script is located + script_dir = os.path.dirname(os.path.abspath(__file__)) + base_folder = os.path.join(script_dir, '../videos/pytube/') url = sys.argv[1] - folder_name = '../videos/pytube/'+sys.argv[2] + folder_name = os.path.join(base_folder, sys.argv[2]) + action = sys.argv[3].lower() if len(sys.argv) > 3 else "video" - # Create the folder os.makedirs(folder_name, exist_ok=True) try: - # Download YouTube Video yt = YouTube(url) - save_metadata(yt, folder_name) - save_thumbnail(yt, folder_name) - download_video(yt, folder_name) - - print(f"Download completed. Files saved in '{folder_name}'.") + if action == "metadata": + save_metadata(yt, folder_name) + elif action == "thumbnail": + save_thumbnail(yt, folder_name) + elif action == "video": + download_video(yt, folder_name) + elif action == "all": + save_metadata(yt, folder_name) + save_thumbnail(yt, folder_name) + download_video(yt, folder_name) + else: + print("Invalid action specified. Use 'metadata', 'thumbnail', 'video', or 'all'.") + + # Clean old folders after the operation + clean_old_folders(base_folder) except Exception as e: print(f"Error: {e}")