Completed
Push — dev ( 232b11...6092be )
by Konstantinos
04:15 queued 01:32
created

music_album_creation.downloading   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 175
Duplicated Lines 0 %

Test Coverage

Coverage 40.4%

Importance

Changes 0
Metric Value
eloc 110
dl 0
loc 175
ccs 40
cts 99
cp 0.404
rs 10
c 0
b 0
f 0
wmc 28

15 Methods

Rating   Name   Duplication   Size   Complexity  
A AbstractYoutubeDL.download() 0 2 1
A AbstractYoutubeDownloader.download() 0 3 1
A CMDYoutubeDownloader.download_trials() 0 11 3
A YoutubeDownloaderErrorFactory.create_from_stderr() 0 8 3
A AbstractYoutubeDL.update_backend() 0 13 3
A CertificateVerificationError.__init__() 0 7 1
A YoutubeDownloaderErrorFactory.create_with_message() 0 3 1
A TooManyRequestsError.__init__() 0 3 1
A InvalidUrlError.__init__() 0 3 1
A CMDYoutubeDownloader.download() 0 2 1
A UnavailableVideoError.__init__() 0 3 1
A AbstractYoutubeDownloaderError.__init__() 0 11 5
A CMDYoutubeDownloader.__new__() 0 4 2
A TokenParameterNotInVideoInfoError.__init__() 0 3 1
A CMDYoutubeDownloader._download() 0 12 3
1 1
import logging
2 1
import re
3 1
import subprocess
4
from abc import ABCMeta, abstractmethod
5 1
from time import sleep
6
7 1
logger = logging.getLogger(__name__)
8
9
# # Create handlers
10
# c_handler = logging.StreamHandler()
11
# f_handler = logging.FileHandler('file.log')
12
# c_handler.setLevel(logging.INFO)
13
# f_handler.setLevel(logging.DEBUG)
14
#
15
# # Create formatters and add it to handlers
16
# c_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
17
# f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
# c_handler.setFormatter(c_format)
19
# f_handler.setFormatter(f_format)
20
#
21
# # Add handlers to the logger
22
# logger.addHandler(c_handler)
23
# logger.addHandler(f_handler)
24
25
26
27 1
class AbstractYoutubeDownloader(metaclass=ABCMeta):
28
29
    @abstractmethod
30
    def download(self, video_url, directory, **kwargs):
31
        raise NotImplementedError
32
33
34 1
class AbstractYoutubeDL(AbstractYoutubeDownloader):
35 1
    update_command_args = ('sudo', 'python' '-m', 'pip', 'install', '--upgrade', 'youtube-dl')
36 1
    update_backend_command = ' '.join(update_command_args)
37
38 1
    already_up_to_date_reg = re.compile(r'python\d[\d.]*/(site-packages \(\d[\d.]*\))',)
39 1
    updated_reg = re.compile(r'Collecting [\w\-_]+==(\d[\d.]*)')
40
41 1
    def download(self, video_url, directory, **kwargs):
42
        raise NotImplementedError
43
44 1
    @classmethod
45
    def update_backend(cls):
46
        args = ['python', '-m', 'pip', 'install', '--user', '--upgrade', 'youtube-dl']
47
        output = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
48
        stdout = str(output.stdout, encoding='utf-8')
49
        if output.returncode == 0:
50
            match = cls.requirement_dir_reg.search(stdout)
51
            if match:
52
                logger.info("Backend 'youtube-dl' already up-to-date in '{}'".format(match.group(1)))
53
            else:
54
                logger.info("Updated with command '{}' to version {}".format(' '.join(args), cls.updated_reg.search(stdout)))
55
        else:
56
            logging.error("Something not documented happened while attempting to update youtube_dl: {}".format(str(output.stderr, encoding='utf-8')))
57
58
59 1
class CMDYoutubeDownloader(AbstractYoutubeDL):
60 1
    _args = ['youtube-dl', '--extract-audio', '--audio-quality', '0', '--audio-format', 'mp3', '-o', '%(title)s.%(ext)s']
61 1
    __instance = None
62
63 1
    def __new__(cls, *args, **kwargs):
64
        if not cls.__instance:
65
            cls.__instance = super().__new__(cls)
66
        return cls.__instance
67
68 1
    def download(self, video_url, directory, suppress_certificate_validation=False, **kwargs):
69
        self._download(video_url, directory, suppress_certificate_validation=suppress_certificate_validation)
70
71 1
    @classmethod
72
    def _download(cls, video_url, directory, **kwargs):
73
        template = kwargs.get('template', '%(title)s.%(ext)s')
74
        args = ['youtube-dl', '--extract-audio', '--audio-quality', '0', '--audio-format', 'mp3', '-o', '{}/{}'.format(directory, template), video_url]
75
        # If suppress HTTPS certificate validation
76
        if kwargs.get('suppress_certificate_validation', False):
77
            args.insert(1, '--no-check-certificate')
78
        logger.info("Executing '{}'".format(' '.join(args)))
79
        ro = subprocess.run(args, stderr=subprocess.PIPE)  # stdout gets streamed in terminal
80
        if ro.returncode != 0:
81
            stderr = str(ro.stderr, encoding='utf-8')
82
            raise YoutubeDownloaderErrorFactory.create_from_stderr(stderr, video_url)
83
84 1
    def download_trials(self, video_url, directory, times=10, delay=1, **kwargs):
85
        i = 0
86
        while i < times - 1:
87
            try:
88
                self.download(video_url, directory, **kwargs)
89
                return
90
            except TooManyRequestsError as e:
91
                logger.info(e)
92
                i += 1
93
                sleep(delay)
94
        self.download(video_url, directory, **kwargs)
95
96
97 1
class YoutubeDownloaderErrorFactory:
98 1
    @staticmethod
99
    def create_with_message(msg):
100
        return Exception(msg)
101
102 1
    @staticmethod
103
    def create_from_stderr(stderror, video_url):
104
        exception_classes = (TokenParameterNotInVideoInfoError, InvalidUrlError, UnavailableVideoError, TooManyRequestsError, CertificateVerificationError)
105
        for subclass in exception_classes:
106
            if subclass.reg.search(stderror):
107
                return subclass(video_url, stderror)
108
        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(', '.join(['"{}"'.format(_.reg) for _ in exception_classes]), stderror)
109
        return Exception(AbstractYoutubeDownloaderError(video_url, stderror)._msg + '\n' + s)
110
111
112
#### EXCEPTIONS
113
114 1
class AbstractYoutubeDownloaderError(metaclass=ABCMeta):
115 1
    def __init__(self, *args, **kwargs):
116
        super().__init__()
117
        if len(args) > 1:
118
            self.video_url = args[0]
119
            self.stderr = args[1]
120
        elif len(args) > 0:
121
            self.video_url = args[0]
122
        self._msg = "YoutubeDownloader generic error."
123
        self._short_msg = kwargs.get('msg')
124
        if args or 'msg' in kwargs:
125
            self._msg = '\n'.join([_ for _ in [kwargs.get('msg', ''), getattr(self, 'stderr', '')] if _])
126
127
128 1
class TokenParameterNotInVideoInfoError(Exception, AbstractYoutubeDownloaderError):
129
    """Token error"""
130 1
    reg = re.compile('"token" parameter not in video info for unknown reason')
131
132 1
    def __init__(self, video_url, stderror):
133
        AbstractYoutubeDownloaderError.__init__(self, video_url, stderror)
134
        Exception.__init__(self, self._msg)
135
136 1
class InvalidUrlError(Exception, AbstractYoutubeDownloaderError):
137
    """Invalid url error"""
138 1
    reg = re.compile(r'is not a valid URL\.')
139
140 1
    def __init__(self, video_url, stderror):
141
        AbstractYoutubeDownloaderError.__init__(self, video_url, stderror, msg="Invalid url '{}'.".format(video_url))
142
        Exception.__init__(self, self._short_msg)
143
144 1
class UnavailableVideoError(Exception, AbstractYoutubeDownloaderError):
145
    """Wrong url error"""
146 1
    reg = re.compile(r'ERROR: This video is unavailable\.')
147
148 1
    def __init__(self, video_url, stderror):
149
        AbstractYoutubeDownloaderError.__init__(self, video_url, stderror, msg="Unavailable video at '{}'.".format(video_url))
150
        Exception.__init__(self, self._msg)
151
152 1
class TooManyRequestsError(Exception, AbstractYoutubeDownloaderError):
153
    """Too many requests (for youtube) to serve"""
154 1
    reg = re.compile(r"(?:ERROR: Unable to download webpage: HTTP Error 429: Too Many Requests|WARNING: unable to download video info webpage: HTTP Error 429)")
155
156 1
    def __init__(self, video_url, stderror):
157
        AbstractYoutubeDownloaderError.__init__(self, video_url, stderror, msg="Too many requests for youtube at the moment.".format(video_url))
158
        Exception.__init__(self, self._msg)
159
160 1
class CertificateVerificationError(Exception, AbstractYoutubeDownloaderError):
161
    """This can happen when downloading is requested from a server like scrutinizer.io\n
162
    ERROR: Unable to download webpage: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get
163
    local issuer certificate (_ssl.c:1056)> (caused by URLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
164
    certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)')))
165
    """
166 1
    reg = re.compile(r"ERROR: Unable to download webpage: <urlopen error \[SSL: CERTIFICATE_VERIFY_FAILED\]")
167
168 1
    def __init__(self, video_url, stderror):
169
        AbstractYoutubeDownloaderError.__init__(self, video_url, stderror,
170
                                                msg="Unable to download webpage because ssl certificate verification failed:\n[SSL: CERTIFICATE_VERIFY_FAILED] certificate "
171
                                                    "verify failed: unable to get local issuer certificate (_ssl.c:1056)> (caused by "
172
                                                    "URLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: "
173
                                                    "unable to get local issuer certificate (_ssl.c:1056)')))")
174
        Exception.__init__(self, self._msg)
175