2015-04-10 21:45:41 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2017-03-16 04:17:35 +01:00
|
|
|
# Copyright 2014-2017 Mike Fährmann
|
2015-04-10 21:45:41 +02:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License version 2 as
|
|
|
|
# published by the Free Software Foundation.
|
|
|
|
|
2017-06-30 19:38:14 +02:00
|
|
|
"""Downloader module for http:// and https:// URLs"""
|
2015-04-10 21:45:41 +02:00
|
|
|
|
2017-12-02 01:47:26 +01:00
|
|
|
import time
|
2016-09-30 12:32:48 +02:00
|
|
|
import mimetypes
|
2017-12-06 22:35:05 +01:00
|
|
|
from requests.exceptions import ConnectionError, Timeout
|
2017-10-24 12:53:03 +02:00
|
|
|
from .common import DownloaderBase
|
2018-04-20 14:53:21 +02:00
|
|
|
from .. import text, exception
|
2017-10-24 23:33:44 +02:00
|
|
|
|
|
|
|
|
2017-10-24 12:53:03 +02:00
|
|
|
class Downloader(DownloaderBase):
|
2017-10-26 22:11:36 +02:00
|
|
|
scheme = "http"
|
2017-03-26 18:24:46 +02:00
|
|
|
|
2017-06-30 19:38:14 +02:00
|
|
|
def __init__(self, session, output):
|
2017-10-24 12:53:03 +02:00
|
|
|
DownloaderBase.__init__(self, session, output)
|
|
|
|
self.response = None
|
2017-10-26 22:11:36 +02:00
|
|
|
self.retries = self.config("retries", 5)
|
|
|
|
self.timeout = self.config("timeout", 30)
|
|
|
|
self.verify = self.config("verify", True)
|
2017-12-02 01:47:26 +01:00
|
|
|
self.rate = self.config("rate")
|
|
|
|
self.chunk_size = 16384
|
|
|
|
|
|
|
|
if self.rate:
|
2018-04-20 14:53:21 +02:00
|
|
|
self.rate = text.parse_bytes(self.rate)
|
2017-12-02 01:47:26 +01:00
|
|
|
if not self.rate:
|
|
|
|
self.log.warning("Invalid rate limit specified")
|
|
|
|
elif self.rate < self.chunk_size:
|
|
|
|
self.chunk_size = self.rate
|
2017-10-24 12:53:03 +02:00
|
|
|
|
|
|
|
def connect(self, url, offset):
|
|
|
|
headers = {}
|
|
|
|
if offset:
|
|
|
|
headers["Range"] = "bytes={}-".format(offset)
|
|
|
|
|
2017-12-06 22:35:05 +01:00
|
|
|
try:
|
|
|
|
self.response = self.session.request(
|
|
|
|
"GET", url, stream=True, headers=headers, allow_redirects=True,
|
|
|
|
timeout=self.timeout, verify=self.verify)
|
|
|
|
except (ConnectionError, Timeout) as exc:
|
|
|
|
raise exception.DownloadRetry(exc)
|
2017-10-24 12:53:03 +02:00
|
|
|
|
|
|
|
code = self.response.status_code
|
2017-11-10 21:35:53 +01:00
|
|
|
if code == 200: # OK
|
2017-10-24 12:53:03 +02:00
|
|
|
offset = 0
|
|
|
|
size = self.response.headers.get("Content-Length")
|
2017-11-10 21:35:53 +01:00
|
|
|
elif code == 206: # Partial Content
|
2017-10-24 12:53:03 +02:00
|
|
|
size = self.response.headers["Content-Range"].rpartition("/")[2]
|
2017-11-10 21:35:53 +01:00
|
|
|
elif code == 416: # Requested Range Not Satisfiable
|
|
|
|
raise exception.DownloadComplete()
|
2017-12-06 22:35:05 +01:00
|
|
|
elif code == 429 or 500 <= code < 600: # Server Error
|
|
|
|
raise exception.DownloadRetry(
|
|
|
|
"{} Server Error: {} for url: {}".format(
|
2017-11-10 21:35:53 +01:00
|
|
|
code, self.response.reason, url))
|
2017-10-24 12:53:03 +02:00
|
|
|
else:
|
|
|
|
self.response.raise_for_status()
|
|
|
|
|
2018-04-20 14:53:21 +02:00
|
|
|
return offset, text.parse_int(size)
|
2017-10-24 12:53:03 +02:00
|
|
|
|
|
|
|
def receive(self, file):
|
2017-12-02 01:47:26 +01:00
|
|
|
if self.rate:
|
|
|
|
total = 0 # total amount of bytes received
|
|
|
|
start = time.time() # start time
|
|
|
|
|
|
|
|
for data in self.response.iter_content(self.chunk_size):
|
2017-10-24 12:53:03 +02:00
|
|
|
file.write(data)
|
|
|
|
|
2017-12-02 01:47:26 +01:00
|
|
|
if self.rate:
|
|
|
|
total += len(data)
|
|
|
|
expected = total / self.rate # expected elapsed time
|
|
|
|
delta = time.time() - start # actual elapsed time since start
|
|
|
|
if delta < expected:
|
|
|
|
# sleep if less time passed than expected
|
|
|
|
time.sleep(expected - delta)
|
|
|
|
|
2017-10-24 12:53:03 +02:00
|
|
|
def reset(self):
|
|
|
|
if self.response:
|
|
|
|
self.response.close()
|
|
|
|
self.response = None
|
|
|
|
|
|
|
|
def get_extension(self):
|
|
|
|
mtype = self.response.headers.get("Content-Type", "image/jpeg")
|
|
|
|
mtype = mtype.partition(";")[0]
|
2017-11-30 22:30:01 +01:00
|
|
|
|
|
|
|
if mtype in MIMETYPE_MAP:
|
|
|
|
return MIMETYPE_MAP[mtype]
|
|
|
|
|
2017-10-24 12:53:03 +02:00
|
|
|
exts = mimetypes.guess_all_extensions(mtype, strict=False)
|
|
|
|
if exts:
|
|
|
|
exts.sort()
|
|
|
|
return exts[-1][1:]
|
2017-11-30 22:30:01 +01:00
|
|
|
|
2017-10-24 12:53:03 +02:00
|
|
|
self.log.warning(
|
|
|
|
"No filename extension found for MIME type '%s'", mtype)
|
|
|
|
return "txt"
|
2017-11-30 22:30:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
MIMETYPE_MAP = {
|
|
|
|
"image/jpeg": "jpg",
|
|
|
|
"image/jpg": "jpg",
|
|
|
|
"image/png": "png",
|
|
|
|
"image/gif": "gif",
|
|
|
|
"image/bmp": "bmp",
|
|
|
|
"image/webp": "webp",
|
|
|
|
"image/svg+xml": "svg",
|
|
|
|
|
|
|
|
"video/webm": "webm",
|
|
|
|
"video/ogg": "ogg",
|
|
|
|
"video/mp4": "mp4",
|
|
|
|
|
|
|
|
"audio/wav": "wav",
|
|
|
|
"audio/x-wav": "wav",
|
|
|
|
"audio/webm": "webm",
|
|
|
|
"audio/ogg": "ogg",
|
|
|
|
"audio/mpeg": "mp3",
|
|
|
|
|
|
|
|
"application/ogg": "ogg",
|
|
|
|
"application/octet-stream": "bin",
|
|
|
|
}
|