|
1 | | -"""Module containing all classes to download YouTube content.""" |
| 1 | +"""A program to download any YouTube video or playlist.""" |
2 | 2 | from __future__ import annotations |
3 | 3 |
|
4 | | -__all__: tuple[str, ...] = ( |
5 | | - "YouTubeDownloader", |
6 | | - "PlaylistDownloader", |
7 | | - "VideoDownloader", |
8 | | - "get_downloader", |
9 | | -) |
| 4 | +__version__: Final[str] = "1.5.1" |
| 5 | +__title__: Final[str] = "YTDownloader" |
| 6 | +__author__: Final[str] = "realshouzy" |
| 7 | +__license__: Final[str] = "MIT" |
| 8 | +__copyright__: Final[str] = "Copyright (c) 2022-present realshouzy" |
10 | 9 |
|
11 | 10 | import re |
12 | 11 | import sys |
13 | 12 | import webbrowser |
14 | 13 | from abc import ABC, abstractmethod |
15 | 14 | from concurrent.futures import ThreadPoolExecutor |
16 | 15 | from pathlib import Path |
17 | | -from typing import TYPE_CHECKING, Any, Final |
| 16 | +from typing import TYPE_CHECKING, Any, Final, NamedTuple |
18 | 17 |
|
19 | 18 | import PySimpleGUI as sg |
20 | 19 | import pytube.exceptions |
21 | 20 | from pytube import Playlist, YouTube |
22 | 21 |
|
23 | | -from YTDownloader.download_options import AUDIO, HD, LD |
24 | | - |
25 | 22 | if sys.version_info >= (3, 12): # pragma: >=3.12 cover |
26 | 23 | from typing import override |
27 | 24 | else: # pragma: <3.12 cover |
|
32 | 29 |
|
33 | 30 | from pytube import Stream |
34 | 31 |
|
35 | | - from YTDownloader.download_options import DownloadOptions |
| 32 | + |
| 33 | +class DownloadOptions(NamedTuple): |
| 34 | + """Tuple-like class holding the download options.""" |
| 35 | + |
| 36 | + resolution: str | None |
| 37 | + type: str |
| 38 | + progressive: bool |
| 39 | + abr: str | None |
| 40 | + |
| 41 | + |
| 42 | +# defining download options |
| 43 | +LD: Final[DownloadOptions] = DownloadOptions( |
| 44 | + resolution="360p", |
| 45 | + type="video", |
| 46 | + progressive=True, |
| 47 | + abr=None, |
| 48 | +) |
| 49 | +HD: Final[DownloadOptions] = DownloadOptions( |
| 50 | + resolution="720p", |
| 51 | + type="video", |
| 52 | + progressive=True, |
| 53 | + abr=None, |
| 54 | +) |
| 55 | +AUDIO: Final[DownloadOptions] = DownloadOptions( |
| 56 | + resolution=None, |
| 57 | + type="audio", |
| 58 | + progressive=False, |
| 59 | + abr="128kbps", |
| 60 | +) |
36 | 61 |
|
37 | 62 | _YOUTUBE_PLAYLIST_URL_PATTERN: Final[re.Pattern[str]] = re.compile( |
38 | 63 | r"^(?:https?:\/\/)?(?:www\.|m\.)?" |
@@ -590,3 +615,113 @@ def _download_complete( |
590 | 615 | self._download_window["-DOWNLOADPROGRESS-"].update(0) |
591 | 616 | self._download_window["-COMPLETED-"].update("") |
592 | 617 | sg.Popup("Downloaded complete") |
| 618 | + |
| 619 | + |
| 620 | +def create_error_window(error_name: str, message: str) -> None: # pragma: no cover |
| 621 | + """Create an error window.""" |
| 622 | + error_layout: list[list[sg.Text | sg.Button]] = [ |
| 623 | + [sg.Text(f"{error_name}: {message}")], |
| 624 | + [sg.Button("Ok", key="-OK-"), sg.Button("Report", key="-REPORT-")], |
| 625 | + ] |
| 626 | + |
| 627 | + error_window: sg.Window = sg.Window( |
| 628 | + "Error", |
| 629 | + layout=error_layout, |
| 630 | + modal=True, |
| 631 | + ) |
| 632 | + |
| 633 | + # error window event loop |
| 634 | + while True: |
| 635 | + event, _ = error_window.read() # type: ignore |
| 636 | + if event in {sg.WIN_CLOSED, "-OK-"}: |
| 637 | + break |
| 638 | + if event == "-REPORT-": |
| 639 | + webbrowser.open("https://github.com/realshouzy/YTDownloader/issues") |
| 640 | + |
| 641 | + error_window.close() |
| 642 | + |
| 643 | + |
| 644 | +# pylint: disable=R0912, W0718 |
| 645 | + |
| 646 | + |
| 647 | +def main() -> int: # noqa: C901 # pragma: no cover |
| 648 | + """Run the program.""" |
| 649 | + exit_code: int = 0 |
| 650 | + |
| 651 | + sg.theme("Darkred1") |
| 652 | + |
| 653 | + # defining layouts |
| 654 | + start_layout: list[list[sg.Input | sg.Button]] = [ |
| 655 | + [sg.Input(key="-LINKINPUT-"), sg.Button("Submit")], |
| 656 | + ] |
| 657 | + start_window: sg.Window = sg.Window("Youtube Downloader", start_layout) |
| 658 | + |
| 659 | + # main event loop |
| 660 | + while True: |
| 661 | + event, values = start_window.read() |
| 662 | + if event == sg.WIN_CLOSED: |
| 663 | + break |
| 664 | + |
| 665 | + if event == "Submit": |
| 666 | + try: |
| 667 | + downloader: PlaylistDownloader | VideoDownloader = get_downloader( |
| 668 | + values["-LINKINPUT-"], |
| 669 | + ) |
| 670 | + downloader.create_window() |
| 671 | + |
| 672 | + except pytube.exceptions.RegexMatchError as re_err: |
| 673 | + if not values["-LINKINPUT-"]: |
| 674 | + create_error_window( |
| 675 | + re_err.__class__.__name__, |
| 676 | + "Please provide link.", |
| 677 | + ) |
| 678 | + else: |
| 679 | + create_error_window(re_err.__class__.__name__, "Invalid link.") |
| 680 | + |
| 681 | + except pytube.exceptions.VideoPrivate as vp_err: |
| 682 | + create_error_window(vp_err.__class__.__name__, "Video is privat.") |
| 683 | + |
| 684 | + except pytube.exceptions.MembersOnly as mo_err: |
| 685 | + create_error_window( |
| 686 | + mo_err.__class__.__name__, |
| 687 | + "Video is for members only.", |
| 688 | + ) |
| 689 | + |
| 690 | + except pytube.exceptions.VideoRegionBlocked as vgb_err: |
| 691 | + create_error_window( |
| 692 | + vgb_err.__class__.__name__, |
| 693 | + "Video is block in your region.", |
| 694 | + ) |
| 695 | + |
| 696 | + except pytube.exceptions.LiveStreamError as ls_err: |
| 697 | + create_error_window( |
| 698 | + ls_err.__class__.__name__, |
| 699 | + "This is an active life stream.", |
| 700 | + ) |
| 701 | + |
| 702 | + except pytube.exceptions.AgeRestrictedError as ar_err: |
| 703 | + create_error_window( |
| 704 | + ar_err.__class__.__name__, |
| 705 | + "This video is age restricted.", |
| 706 | + ) |
| 707 | + |
| 708 | + except pytube.exceptions.VideoUnavailable as vu_err: |
| 709 | + create_error_window(vu_err.__class__.__name__, "Video Unavailable.") |
| 710 | + |
| 711 | + except KeyError as key_err: |
| 712 | + create_error_window( |
| 713 | + key_err.__class__.__name__, |
| 714 | + "Video or playlist is unreachable or invalid.", |
| 715 | + ) |
| 716 | + |
| 717 | + except Exception as err: |
| 718 | + create_error_window(err.__class__.__name__, str(err)) |
| 719 | + exit_code = 1 |
| 720 | + break |
| 721 | + |
| 722 | + start_window.close() |
| 723 | + return exit_code |
| 724 | + |
| 725 | + |
| 726 | +if __name__ == "__main__": |
| 727 | + raise SystemExit(main()) |
0 commit comments