def main(): """Command line application to download youtube videos.""" # noinspection PyTypeChecker parser = argparse.ArgumentParser(description=main.__doc__) args = _parse_args(parser) if args.verbosity: log_level = min(args.verbosity, 4) * 10 setup_logger(logging.FATAL - log_level) if not args.url or "youtu" not in args.url: parser.print_help() sys.exit(1) if "/playlist" in args.url: print("Loading playlist...") playlist = Playlist(args.url) if not args.target: args.target = safe_filename(playlist.title()) for youtube_video in playlist.videos: try: _perform_args_on_youtube(youtube_video, args) except PytubeError as e: print(f"There was an error with video: {youtube_video}") print(e) else: print("Loading video...") youtube = YouTubeItem(args.url) _perform_args_on_youtube(youtube, args)
def _ffmpeg_downloader(audio_stream: Stream, video_stream: Stream, target: str) -> None: """ Given a YouTube Stream object, finds the correct audio stream, downloads them both giving them a unique name, them uses ffmpeg to create a new file with the audio and video from the previously downloaded files. Then deletes the original adaptive streams, leaving the combination. :param Stream audio_stream: A valid Stream object representing the audio to download :param Stream video_stream: A valid Stream object representing the video to download :param Path target: A valid Path object """ video_unique_name = _unique_name(safe_filename(video_stream.title), video_stream.subtype, "video", target=target) audio_unique_name = _unique_name(safe_filename(video_stream.title), audio_stream.subtype, "audio", target=target) _download(stream=video_stream, target=target, filename=video_unique_name) print("Loading audio...") _download(stream=audio_stream, target=target, filename=audio_unique_name) video_path = os.path.join(target, f"{video_unique_name}.{video_stream.subtype}") audio_path = os.path.join(target, f"{audio_unique_name}.{audio_stream.subtype}") final_path = os.path.join( target, f"{safe_filename(video_stream.title)}.{video_stream.subtype}") subprocess.run( # nosec [ "ffmpeg", "-i", video_path, "-i", audio_path, "-codec", "copy", final_path, ]) os.unlink(video_path) os.unlink(audio_path)
def default_filename(self) -> str: """Generate filename based on the video title. :rtype: str :returns: An os file system compatible filename. """ filename = safe_filename(self.title) return f"{filename}.{self.subtype}"
def download( self, title: str, srt: bool = True, output_path: Optional[str] = None, filename_prefix: Optional[str] = None, ) -> str: """Write the media stream to disk. :param title: Output filename (stem only) for writing media file. If one is not specified, the default filename is used. :type title: str :param srt: Set to True to download srt, false to download xml. Defaults to True. :type srt bool :param output_path: (optional) Output path for writing media file. If one is not specified, defaults to the current working directory. :type output_path: str or None :param filename_prefix: (optional) A string that will be prepended to the filename. For example a number in a playlist or the name of a series. If one is not specified, nothing will be prepended This is separate from filename so you can use the default filename but still add a prefix. :type filename_prefix: str or None :rtype: str """ if title.endswith(".srt") or title.endswith(".xml"): filename = ".".join(title.split(".")[:-1]) else: filename = title if filename_prefix: filename = f"{safe_filename(filename_prefix)}{filename}" filename = safe_filename(filename) filename += f" ({self.code})" if srt: filename += ".srt" else: filename += ".xml" file_path = os.path.join(target_directory(output_path), filename) with open(file_path, "w", encoding="utf-8") as file_handle: if srt: file_handle.write(self.generate_srt_captions()) else: file_handle.write(self.xml_captions) return file_path
def test_safe_filename(): """Unsafe characters get stripped from generated filename""" assert helpers.safe_filename("abc1245$$") == "abc1245" assert helpers.safe_filename("abc##") == "abc"