Passed
Push — master ( 6092be...7ebd98 )
by Konstantinos
09:12 queued 05:48
created

music_album_creation.downloading   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Test Coverage

Coverage 40.94%

Importance

Changes 0
Metric Value
eloc 116
dl 0
loc 182
ccs 43
cts 105
cp 0.4094
rs 10
c 0
b 0
f 0
wmc 29

15 Methods

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