summaryrefslogtreecommitdiff
path: root/server/szurubooru/func/net.py
blob: c53a62eb3201d26a5b42dbf6667e623670e02901 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import json
import logging
import subprocess
import urllib.error
import urllib.request
from threading import Thread
from typing import Any, Dict, List

from szurubooru import config, errors
from szurubooru.func import mime

logger = logging.getLogger(__name__)
_dl_chunk_size = 2 ** 15


class DownloadError(errors.ProcessingError):
    pass


class DownloadTooLargeError(DownloadError):
    pass


def download(url: str, use_video_downloader: bool = False) -> bytes:
    assert url
    youtube_dl_error = None
    if use_video_downloader:
        try:
            url = _get_youtube_dl_content_url(url) or url
        except errors.ThirdPartyError as ex:
            youtube_dl_error = ex

    request = urllib.request.Request(url)
    if config.config["user_agent"]:
        request.add_header("User-Agent", config.config["user_agent"])
    request.add_header("Referer", url)

    content_buffer = b""
    length_tally = 0
    try:
        with urllib.request.urlopen(request) as handle:
            while chunk := handle.read(_dl_chunk_size):
                length_tally += len(chunk)
                if length_tally > config.config["max_dl_filesize"]:
                    raise DownloadTooLargeError(
                        "Download target exceeds maximum. (%d)"
                        % (config.config["max_dl_filesize"]),
                        extra_fields={"URL": url},
                    )
                content_buffer += chunk
    except urllib.error.HTTPError as ex:
        raise DownloadError(
            "Download target returned HTTP %d. (%s)" % (ex.code, ex.reason),
            extra_fields={"URL": url},
        ) from ex

    if (
        youtube_dl_error
        and mime.get_mime_type(content_buffer) == "application/octet-stream"
    ):
        raise youtube_dl_error

    return content_buffer


def _get_youtube_dl_content_url(url: str) -> str:
    cmd = ["youtube-dl", "--format", "best", "--no-playlist"]
    if config.config["user_agent"]:
        cmd.extend(["--user-agent", config.config["user_agent"]])
    cmd.extend(["--get-url", url])
    try:
        return (
            subprocess.run(cmd, text=True, capture_output=True, check=True)
            .stdout.split("\n")[0]
            .strip()
        )
    except subprocess.CalledProcessError:
        raise errors.ThirdPartyError(
            "Could not extract content location from URL.",
            extra_fields={"URL": url},
        ) from None


def post_to_webhooks(payload: Dict[str, Any]) -> List[Thread]:
    threads = [
        Thread(target=_post_to_webhook, args=(webhook, payload), daemon=False)
        for webhook in (config.config["webhooks"] or [])
    ]
    for thread in threads:
        thread.start()
    return threads


def _post_to_webhook(webhook: str, payload: Dict[str, Any]) -> int:
    req = urllib.request.Request(webhook)
    req.data = json.dumps(
        payload,
        default=lambda x: x.isoformat("T") + "Z",
    ).encode("utf-8")
    req.add_header("Content-Type", "application/json")
    try:
        res = urllib.request.urlopen(req)
        if not 200 <= res.status <= 299:
            logger.warning(
                f"Webhook {webhook} returned {res.status} {res.reason}"
            )
        return res.status
    except urllib.error.URLError as ex:
        logger.warning(f"Unable to call webhook {webhook}: {ex}")
        return 400