Test Failed
Push — dev ( 3a28f6...0451e7 )
by Konstantinos
05:08
created

()   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 11
nop 1
dl 0
loc 13
ccs 0
cts 9
cp 0
crap 12
rs 9.85
c 0
b 0
f 0
1 1
import json
2 1
import logging
3 1
import re
4 1
from abc import ABC
5
from pathlib import Path
6 1
from time import sleep
7
from typing import Union
8 1
9
from pytube import YouTube
10
11
logger = logging.getLogger(__name__)
12
13
14
class CMDYoutubeDownloader:
15
    __instance = None
16
17
    def __new__(cls, *args, **kwargs):
18
        if not cls.__instance:
19
            cls.__instance = super(CMDYoutubeDownloader, cls).__new__(cls)
20
        return cls.__instance
21
22
    def download(self, video_url: str, directory: Union[str, Path], **kwargs):
23
        self._download(video_url, directory)
24
25
    @classmethod
26
    def _download(cls, video_url, directory):
27
        # output dir where to store the stream
28 1
        output_dir = Path(directory)
29 1
30
        yt = YouTube(video_url)
31
32
        # get avaialbe streams
33
        streams = yt.streams
34
35 1
        # filter streams by audio only
36 1
        audio_streams = streams.filter(only_audio=True)
37 1
38
        # find highest quality audio stream
39 1
        # we currently judge quality by bitrate (higher is better)
40 1
        best_audio_stream = audio_streams.order_by('bitrate')[-1]
41
42 1
        # highest_quality_audio_stream = audio_streams.order_by('abr').desc().first()
43
44
        # find highest quality audio stream
45 1
        # find audio only stream with highest reported kbps (as quality measure)
46
        # best_audio_stream = yt.streams.filter(only_audio=True).order_by('bitrate')[-1]
47
48
        # Download the audio stream
49
        local_file = best_audio_stream.download(
50
            output_path=str(output_dir),
51
            filename=f'{yt.title}.mp4',  # since this is an audio-only stream, the file will be mp4
52
            filename_prefix=None,
53
            skip_existing=True,  # Skip existing files, defaults to True
54
            timeout=None,  # Request timeout length in seconds. Uses system default
55
            max_retries=3,  # Number of retries to attempt after socket timeout. Defaults to 0
56
        )
57
        logger.error(
58
            "Downloaded from Youtube: %s",
59
            json.dumps(
60 1
                {
61 1
                    'title': yt.title,
62 1
                    'local_file': str(local_file),
63
                },
64 1
                indent=4,
65
                sort_keys=True,
66
            ),
67
        )
68
        return local_file
69 1
70
    def download_trials(self, video_url, directory, times=10, delay=1, **kwargs):
71
        i = 0
72 1
        while i < times - 1:
73
            try:
74
                return self._download(video_url, directory)
75
            except TooManyRequestsError as e:
76
                logger.info(e)
77
                i += 1
78
                sleep(delay)
79
        return self._download(video_url, directory)
80
81
82
class YoutubeDownloaderErrorFactory(object):
83
    @staticmethod
84
    def create_with_message(msg):
85
        return Exception(msg)
86
87
    @staticmethod
88
    def create_from_stderr(stderror, video_url):
89 1
        exception_classes = (
90
            UploaderIDExtractionError,
91
            TokenParameterNotInVideoInfoError,
92
            InvalidUrlError,
93
            UnavailableVideoError,
94
            TooManyRequestsError,
95
            CertificateVerificationError,
96
            HTTPForbiddenError,
97
        )
98
        for subclass in exception_classes:
99
            if subclass.reg.search(stderror):
100
                return subclass(video_url, stderror)
101
        s = "NOTE: None of the predesinged exceptions' regexs [{}] matched. Perhaps you want to derive a new subclass from AbstractYoutubeDownloaderError to account for this youtube-dl exception with string to parse <S>{}</S>'".format(
102 1
            ', '.join(['"{}"'.format(_.reg) for _ in exception_classes]), stderror
103 1
        )
104
        return Exception(AbstractYoutubeDownloaderError(video_url, stderror)._msg + '\n' + s)
105
106
107 1
#### EXCEPTIONS
108
109
110
class AbstractYoutubeDownloaderError(ABC):
111
    def __init__(self, *args, **kwargs):
112
        super(AbstractYoutubeDownloaderError, self).__init__()
113
        if len(args) > 1:
114
            self.video_url = args[0]
115
            self.stderr = args[1]
116
        elif len(args) > 0:
117
            self.video_url = args[0]
118
        self._msg = "YoutubeDownloader generic error."
119 1
        self._short_msg = kwargs.get('msg')
120 1
        if args or 'msg' in kwargs:
121
            self._msg = '\n'.join(
122 1
                [_ for _ in [kwargs.get('msg', ''), getattr(self, 'stderr', '')] if _]
123
            )
124
125
126
class TokenParameterNotInVideoInfoError(Exception, AbstractYoutubeDownloaderError):
127
    """Token error"""
128
129
    reg = re.compile('"token" parameter not in video info for unknown reason')
130
131
    def __init__(self, video_url, stderror):
132
        AbstractYoutubeDownloaderError.__init__(self, video_url, stderror)
133
        Exception.__init__(self, self._msg)
134
135 1
136
class InvalidUrlError(Exception, AbstractYoutubeDownloaderError):
137 1
    """Invalid url error"""
138
139 1
    reg = re.compile(r'is not a valid URL\.')
140
141
    def __init__(self, video_url, stderror):
142
        AbstractYoutubeDownloaderError.__init__(
143 1
            self, video_url, stderror, msg="Invalid url '{}'.".format(video_url)
144
        )
145 1
        Exception.__init__(self, self._short_msg)
146
147 1
148
class UnavailableVideoError(Exception, AbstractYoutubeDownloaderError):
149
    """Wrong url error"""
150
151 1
    reg = re.compile(r'ERROR: Video unavailable')
152
153 1
    def __init__(self, video_url, stderror):
154
        AbstractYoutubeDownloaderError.__init__(
155 1
            self, video_url, stderror, msg="Unavailable video at '{}'.".format(video_url)
156
        )
157
        Exception.__init__(self, self._msg)
158
159 1
160
class TooManyRequestsError(Exception, AbstractYoutubeDownloaderError):
161 1
    """Too many requests (for youtube) to serve"""
162
163 1
    reg = re.compile(
164
        r"(?:ERROR: Unable to download webpage: HTTP Error 429: Too Many Requests|WARNING: unable to download video info webpage: HTTP Error 429)"
165
    )
166
167 1
    def __init__(self, video_url, stderror):
168
        AbstractYoutubeDownloaderError.__init__(
169
            self,
170
            video_url,
171
            stderror,
172
            msg="Too many requests for youtube at the moment.".format(video_url),
173 1
        )
174
        Exception.__init__(self, self._msg)
175 1
176
177
class CertificateVerificationError(Exception, AbstractYoutubeDownloaderError):
178
    """This can happen when downloading is requested from a server like scrutinizer.io\n
179
    ERROR: Unable to download webpage: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get
180
    local issuer certificate (_ssl.c:1056)> (caused by URLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
181
    certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)')))
182
    """
183
184
    reg = re.compile(
185
        r"ERROR: Unable to download webpage: <urlopen error \[SSL: CERTIFICATE_VERIFY_FAILED\]"
186
    )
187
188
    def __init__(self, video_url, stderror):
189
        AbstractYoutubeDownloaderError.__init__(
190
            self,
191
            video_url,
192
            stderror,
193
            msg="Unable to download webpage because ssl certificate verification failed:\n[SSL: CERTIFICATE_VERIFY_FAILED] certificate "
194
            "verify failed: unable to get local issuer certificate (_ssl.c:1056)> (caused by "
195
            "URLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: "
196
            "unable to get local issuer certificate (_ssl.c:1056)')))",
197
        )
198
        Exception.__init__(self, self._msg)
199
200
201
class HTTPForbiddenError(Exception, AbstractYoutubeDownloaderError):
202
    reg = re.compile(r"ERROR: unable to download video data: HTTP Error 403: Forbidden")
203
204
    def __init__(self, video_url, stderror):
205
        AbstractYoutubeDownloaderError.__init__(
206
            self,
207
            video_url,
208
            stderror,
209
            msg="HTTP 403 Forbidden for some reason.".format(video_url),
210
        )
211
        Exception.__init__(self, self._msg)
212
213
214
class UploaderIDExtractionError(Exception, AbstractYoutubeDownloaderError):
215
    reg = re.compile(r"ERROR: Unable to extract uploader id")
216
217
    def __init__(self, video_url, stderror):
218
        AbstractYoutubeDownloaderError.__init__(
219
            self,
220
            video_url,
221
            stderror,
222
            msg="Maybe update the youtube-dl binary/executable.".format(video_url),
223
        )
224
        Exception.__init__(self, self._msg)
225