webplayercontrol 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webplayercontrol/__init__.py +3 -0
- webplayercontrol/assets/common.css +12 -0
- webplayercontrol/assets/index.html +14 -0
- webplayercontrol/assets/index.js +26 -0
- webplayercontrol/assets/player.css +43 -0
- webplayercontrol/assets/player.html +29 -0
- webplayercontrol/assets/player.js +114 -0
- webplayercontrol/assets.py +284 -0
- webplayercontrol/dbus.xml +132 -0
- webplayercontrol/main.py +473 -0
- webplayercontrol/mpris.py +698 -0
- webplayercontrol/mpris.xml +97 -0
- webplayercontrol/player.xml +737 -0
- webplayercontrol/utils.py +109 -0
- webplayercontrol-1.0.0.dist-info/METADATA +59 -0
- webplayercontrol-1.0.0.dist-info/RECORD +17 -0
- webplayercontrol-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>WebPlayerControl</title>
|
|
6
|
+
<link href="{{hash-urlsafe:common.css}}" integrity="sha256-{{hash:common.css}}" rel="stylesheet">
|
|
7
|
+
<script type="module" integrity="sha256-{{hash:index.js}}" src="{{hash-urlsafe:index.js}}"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Select a player to control</h1>
|
|
11
|
+
<ul id="player-list">
|
|
12
|
+
</ul>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const onLoad = async function () {
|
|
2
|
+
const playerList = document.getElementById("player-list");
|
|
3
|
+
try {
|
|
4
|
+
// Fetch the list of players from the server.
|
|
5
|
+
const listingResp = await fetch("players/", { headers: { Accept: "application/json" } });
|
|
6
|
+
if (!listingResp.ok) {
|
|
7
|
+
throw new Error(`Fetching player list failed: HTTP error ${listingResp.status}`);
|
|
8
|
+
}
|
|
9
|
+
const listing = await listingResp.json();
|
|
10
|
+
for (const [name, display] of listing) {
|
|
11
|
+
const item = document.createElement("li");
|
|
12
|
+
const link = document.createElement("a");
|
|
13
|
+
link.href = `players/${name}/`;
|
|
14
|
+
link.textContent = display;
|
|
15
|
+
item.append(link);
|
|
16
|
+
playerList.append(item);
|
|
17
|
+
}
|
|
18
|
+
} catch (exp) {
|
|
19
|
+
const para = document.createElement("p");
|
|
20
|
+
para.textContent = exp.toString();
|
|
21
|
+
para.classList.add("error-message");
|
|
22
|
+
playerList.parentElement.replaceChild(para, playerList);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
window.addEventListener("load", onLoad);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
html {
|
|
2
|
+
text-align: center;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
button {
|
|
6
|
+
background: black;
|
|
7
|
+
border: 0;
|
|
8
|
+
font-size: min(10vw, 35vh);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
button:disabled {
|
|
12
|
+
filter: grayscale(100%);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#status {
|
|
16
|
+
font-size: 2rem;
|
|
17
|
+
margin-bottom: 2rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#spinner-wrapper {
|
|
21
|
+
display: inline-block;
|
|
22
|
+
min-height: 2em;
|
|
23
|
+
vertical-align: middle;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#spinner {
|
|
27
|
+
animation: spin 2s linear infinite;
|
|
28
|
+
border: 0.2em solid #333;
|
|
29
|
+
border-top: 0.2em solid #888;
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
display: inline-block;
|
|
32
|
+
width: 1em;
|
|
33
|
+
height: 1em;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#spinner.off {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes spin {
|
|
41
|
+
0% { transform: rotate(0deg); }
|
|
42
|
+
100% { transform: rotate(360deg); }
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>WebPlayerControl</title>
|
|
6
|
+
<link href="../../{{hash-urlsafe:common.css}}" integrity="sha256-{{hash:common.css}}" rel="stylesheet">
|
|
7
|
+
<link href="../../{{hash-urlsafe:player.css}}" integrity="sha256-{{hash:player.css}}" rel="stylesheet">
|
|
8
|
+
<script type="module" integrity="sha256-{{hash:player.js}}" src="../../{{hash-urlsafe:player.js}}"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<h1>WebPlayerControl</h1>
|
|
12
|
+
<a href="../..">← Back to player list</a>
|
|
13
|
+
<div id="status">
|
|
14
|
+
<div id="spinner-wrapper">
|
|
15
|
+
<div id="spinner"></div>
|
|
16
|
+
</div>
|
|
17
|
+
<span id="status-text">Initializing</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="buttons">
|
|
20
|
+
<button id="goPrevious" type="button" disabled>⏮️</button>
|
|
21
|
+
<button id="seekBack" type="button" disabled>⏪️</button>
|
|
22
|
+
<button id="stop" type="button" disabled>⏹️</button>
|
|
23
|
+
<button id="pause" type="button" disabled>⏸️</button>
|
|
24
|
+
<button id="play" type="button" disabled>▶️</button>
|
|
25
|
+
<button id="seekForward" type="button" disabled>⏩️</button>
|
|
26
|
+
<button id="goNext" type="button" disabled>⏭️</button>
|
|
27
|
+
</div>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Capitalizes the first character of a string.
|
|
3
|
+
*/
|
|
4
|
+
const capFirst = function (string) {
|
|
5
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Lowercases the first character of a string.
|
|
10
|
+
*/
|
|
11
|
+
const lowerFirst = function (string) {
|
|
12
|
+
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Display a status message for an error or reconnection.
|
|
17
|
+
*
|
|
18
|
+
* All controls are disabled.
|
|
19
|
+
*/
|
|
20
|
+
const showStatus = function (msg, error) {
|
|
21
|
+
for (const button of document.getElementsByTagName("button")) {
|
|
22
|
+
button.disabled = true;
|
|
23
|
+
}
|
|
24
|
+
const spinner = document.getElementById("spinner");
|
|
25
|
+
const statusText = document.getElementById("status-text");
|
|
26
|
+
statusText.textContent = msg;
|
|
27
|
+
if (error) {
|
|
28
|
+
spinner.classList.add("off");
|
|
29
|
+
statusText.classList.add("error-message");
|
|
30
|
+
} else {
|
|
31
|
+
spinner.classList.remove("off");
|
|
32
|
+
statusText.classList.remove("error-message");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Handles a server-sent event starting with “can”.
|
|
38
|
+
*
|
|
39
|
+
* Such an event enables or disables the indicated button.
|
|
40
|
+
*/
|
|
41
|
+
const onCan = function (evt) {
|
|
42
|
+
const buttonName = lowerFirst(evt.type.slice(3));
|
|
43
|
+
const button = document.getElementById(buttonName);
|
|
44
|
+
button.disabled = evt.data === "0";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Handles a button being clicked.
|
|
49
|
+
*/
|
|
50
|
+
const onClick = async function (evt, source) {
|
|
51
|
+
const buttonName = evt.target.id;
|
|
52
|
+
const result = await (async () => {
|
|
53
|
+
try {
|
|
54
|
+
const resp = await fetch(buttonName, { body: "", method: "POST", mode: "same-origin" });
|
|
55
|
+
return [resp, null];
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof TypeError) {
|
|
58
|
+
return [null, err];
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
const [resp, err] = result;
|
|
64
|
+
if (resp === null) {
|
|
65
|
+
source.close();
|
|
66
|
+
showStatus(`POST to “${buttonName}” failed with ${err.message}`, true);
|
|
67
|
+
} else if (!resp.ok) {
|
|
68
|
+
source.close();
|
|
69
|
+
showStatus(`POST to “${resp.url}” failed with HTTP ${resp.status}`, true);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Handles an error in the server-sent event connection.
|
|
75
|
+
*
|
|
76
|
+
* The problem is displayed and all controls are disabled until the event
|
|
77
|
+
* stream reconnects (if it is going to do so).
|
|
78
|
+
*/
|
|
79
|
+
const onError = function (evt) {
|
|
80
|
+
if (evt.target.readyState === EventSource.CONNECTING) {
|
|
81
|
+
showStatus("SSE reconnecting", false);
|
|
82
|
+
} else {
|
|
83
|
+
showStatus("SSE connection failed", true);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* Handles a server-sent “title” event.
|
|
89
|
+
*
|
|
90
|
+
* The title is displayed for the user. The loading spinner is also hidden,
|
|
91
|
+
* since “title” is always the last event in the initial burst on a new
|
|
92
|
+
* connection.
|
|
93
|
+
*/
|
|
94
|
+
const onTitle = function (evt) {
|
|
95
|
+
const statusText = document.getElementById("status-text");
|
|
96
|
+
statusText.textContent = evt.data;
|
|
97
|
+
const spinner = document.getElementById("spinner");
|
|
98
|
+
spinner.classList.add("off");
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Starts the application.
|
|
103
|
+
*/
|
|
104
|
+
const main = function () {
|
|
105
|
+
const source = new EventSource("status");
|
|
106
|
+
source.addEventListener("error", onError);
|
|
107
|
+
for (const button of document.getElementsByTagName("button")) {
|
|
108
|
+
source.addEventListener(`can${capFirst(button.id)}`, onCan);
|
|
109
|
+
button.addEventListener("click", (evt) => onClick(evt, source));
|
|
110
|
+
}
|
|
111
|
+
source.addEventListener("title", onTitle);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
main();
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Handling of static asset files."""
|
|
2
|
+
|
|
3
|
+
import binascii
|
|
4
|
+
import importlib.resources
|
|
5
|
+
from base64 import b64encode, urlsafe_b64decode, urlsafe_b64encode
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
from logging import getLogger
|
|
9
|
+
from os import environ
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from .utils import acceptable, discard_request, send_error, unmodified
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from asgi_types import (
|
|
17
|
+
ASGIReceiveCallable,
|
|
18
|
+
ASGISendCallable,
|
|
19
|
+
HTTPScope,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class _FileType:
|
|
25
|
+
"""Information about a particular file type."""
|
|
26
|
+
|
|
27
|
+
__slots__ = {
|
|
28
|
+
"mime_type": "The MIME type of files with this extension.",
|
|
29
|
+
"template": "Whether this file type permits template substitutions.",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mime_type: bytes
|
|
33
|
+
template: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_FILE_TYPES = {
|
|
37
|
+
"css": _FileType(mime_type=b"text/css", template=False),
|
|
38
|
+
"html": _FileType(mime_type=b"text/html", template=True),
|
|
39
|
+
"js": _FileType(mime_type=b"text/javascript", template=False),
|
|
40
|
+
}
|
|
41
|
+
"""All known file types, by file extension."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class _Asset:
|
|
46
|
+
"""Information about an individual asset."""
|
|
47
|
+
|
|
48
|
+
__slots__ = {
|
|
49
|
+
"content": "The content of the asset.",
|
|
50
|
+
"etag": "The ETag header value.",
|
|
51
|
+
"extracted": "The path to which the asset was extracted for pathsend.",
|
|
52
|
+
"file_type": "The file type.",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
content: bytes
|
|
56
|
+
etag: bytes
|
|
57
|
+
extracted: Path | None
|
|
58
|
+
file_type: _FileType
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AssetHandler:
|
|
62
|
+
"""A handler that knows how to deal with static asset files."""
|
|
63
|
+
|
|
64
|
+
__slots__ = {
|
|
65
|
+
"_assets": "A mapping from hash value to _Asset object.",
|
|
66
|
+
"_hashes": "A mapping from filename to hash value.",
|
|
67
|
+
"_warned": "Whether the missing pathsend extension warning has been shown yet.",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_assets: dict[bytes, _Asset]
|
|
71
|
+
_hashes: dict[str, bytes]
|
|
72
|
+
_warned: bool
|
|
73
|
+
|
|
74
|
+
def __init__(self) -> None:
|
|
75
|
+
"""Initialize the asset handler."""
|
|
76
|
+
self._warned = False
|
|
77
|
+
|
|
78
|
+
# Load all the assets into memory.
|
|
79
|
+
by_name = {
|
|
80
|
+
i.name: _Asset(
|
|
81
|
+
content=i.read_bytes(),
|
|
82
|
+
etag=b"",
|
|
83
|
+
extracted=None,
|
|
84
|
+
file_type=_FILE_TYPES[i.name.rsplit(".", 1)[-1]],
|
|
85
|
+
)
|
|
86
|
+
for i in (importlib.resources.files(__package__) / "assets").iterdir()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Hash the ones that are not templates, because we already know their content.
|
|
90
|
+
self._hashes = {
|
|
91
|
+
k: sha256(v.content).digest()
|
|
92
|
+
for k, v in by_name.items()
|
|
93
|
+
if not v.file_type.template
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Preprocess the ones that are templates; they can depend on any non-template
|
|
97
|
+
# files, but not other templates since that could create a loop of changing hash
|
|
98
|
+
# values.
|
|
99
|
+
for k, v in by_name.items():
|
|
100
|
+
if v.file_type.template:
|
|
101
|
+
v.content = self._preprocess(k, v.content, self._hashes)
|
|
102
|
+
|
|
103
|
+
# Add the preprocessed templates to the hash dictionary.
|
|
104
|
+
self._hashes.update(
|
|
105
|
+
(
|
|
106
|
+
(k, sha256(v.content).digest())
|
|
107
|
+
for k, v in by_name.items()
|
|
108
|
+
if v.file_type.template
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Create the hash-to-asset dictionary.
|
|
113
|
+
self._assets = {self._hashes[k]: v for k, v in by_name.items()}
|
|
114
|
+
|
|
115
|
+
# Fill in the ETag header values.
|
|
116
|
+
for k, v in self._assets.items():
|
|
117
|
+
v.etag = b'"' + b64encode(k) + b'"'
|
|
118
|
+
|
|
119
|
+
# If an assets directory was specified, extract them.
|
|
120
|
+
if (extract_dir := environ.get("WEBPLAYERCONTROL_ASSETS")) is not None:
|
|
121
|
+
extract_dir = Path(extract_dir)
|
|
122
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
for k, v in self._assets.items():
|
|
124
|
+
dest = extract_dir / urlsafe_b64encode(k).decode("ASCII")
|
|
125
|
+
dest.write_bytes(v.content)
|
|
126
|
+
v.extracted = dest
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _preprocess(name: str, content: bytes, hashes: dict[str, bytes]) -> bytes:
|
|
130
|
+
"""
|
|
131
|
+
Preprocess an asset file.
|
|
132
|
+
|
|
133
|
+
Every occurrence of {{hash:<filename>}} will be replaced with the SHA256 of
|
|
134
|
+
<filename>, encoded with base64. Every occurrence of {{hash-urlsafe:<filename>}}
|
|
135
|
+
will be replaced with the SHA256 of <filename>, encoded with URL-safe base64.
|
|
136
|
+
|
|
137
|
+
:param name: The name of this asset.
|
|
138
|
+
:param content: The content to preprocess.
|
|
139
|
+
:param hashes: The hashes of the assets.
|
|
140
|
+
"""
|
|
141
|
+
for prefix, encode in (
|
|
142
|
+
(b"{{hash:", b64encode),
|
|
143
|
+
(b"{{hash-urlsafe:", urlsafe_b64encode),
|
|
144
|
+
):
|
|
145
|
+
while (pos := content.find(prefix)) >= 0:
|
|
146
|
+
end_pos = content.find(b"}}", pos)
|
|
147
|
+
if end_pos < 0:
|
|
148
|
+
msg = f"Asset {name} contains {prefix} without matching b'}}'"
|
|
149
|
+
raise ValueError(msg)
|
|
150
|
+
target_name = content[pos + len(prefix) : end_pos].decode("UTF-8")
|
|
151
|
+
content = (
|
|
152
|
+
content[:pos] + encode(hashes[target_name]) + content[end_pos + 2 :]
|
|
153
|
+
)
|
|
154
|
+
return content
|
|
155
|
+
|
|
156
|
+
async def maybe_handle_cas(
|
|
157
|
+
self,
|
|
158
|
+
path: str,
|
|
159
|
+
scope: HTTPScope,
|
|
160
|
+
receive: ASGIReceiveCallable,
|
|
161
|
+
send: ASGISendCallable,
|
|
162
|
+
) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Handle a request if it matches the name of a content-addressable asset.
|
|
165
|
+
|
|
166
|
+
:param path: The request path.
|
|
167
|
+
:param scope: The scope.
|
|
168
|
+
:param receive: The event-receive function.
|
|
169
|
+
:param send: The event-send function.
|
|
170
|
+
:return: True if the request matched a content-addressable asset and has been
|
|
171
|
+
handled, or False if it did not match any such name.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
h = urlsafe_b64decode(path.encode("UTF-8"))
|
|
175
|
+
except binascii.Error:
|
|
176
|
+
return False
|
|
177
|
+
if (asset := self._assets.get(h)) is None:
|
|
178
|
+
return False
|
|
179
|
+
await self._handle(
|
|
180
|
+
asset,
|
|
181
|
+
b"max-age=31536000, immutable, stale-while-revalidate=31536000",
|
|
182
|
+
scope,
|
|
183
|
+
receive,
|
|
184
|
+
send,
|
|
185
|
+
)
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
async def handle(
|
|
189
|
+
self,
|
|
190
|
+
asset: str,
|
|
191
|
+
cache_control: bytes,
|
|
192
|
+
scope: HTTPScope,
|
|
193
|
+
receive: ASGIReceiveCallable,
|
|
194
|
+
send: ASGISendCallable,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Handle a request as a static asset.
|
|
198
|
+
|
|
199
|
+
:param asset: The name of the asset to send.
|
|
200
|
+
:param cache_control: The value of the cache-control header.
|
|
201
|
+
:param scope: The scope.
|
|
202
|
+
:param receive: The event-receive function.
|
|
203
|
+
:param send: The event-send function.
|
|
204
|
+
"""
|
|
205
|
+
await self._handle(
|
|
206
|
+
self._assets[self._hashes[asset]],
|
|
207
|
+
cache_control,
|
|
208
|
+
scope,
|
|
209
|
+
receive,
|
|
210
|
+
send,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async def _handle(
|
|
214
|
+
self,
|
|
215
|
+
asset: _Asset,
|
|
216
|
+
cache_control: bytes,
|
|
217
|
+
scope: HTTPScope,
|
|
218
|
+
receive: ASGIReceiveCallable,
|
|
219
|
+
send: ASGISendCallable,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Handle a request as a static asset.
|
|
223
|
+
|
|
224
|
+
:param asset: Which asset to send.
|
|
225
|
+
:param cache_control: The value of the cache-control header.
|
|
226
|
+
:param scope: The scope.
|
|
227
|
+
:param receive: The event-receive function.
|
|
228
|
+
:param send: The event-send function.
|
|
229
|
+
"""
|
|
230
|
+
# Receive and discard any request body.
|
|
231
|
+
await discard_request(receive)
|
|
232
|
+
|
|
233
|
+
# Static assets can only handle GET requests.
|
|
234
|
+
if scope["method"] != "GET":
|
|
235
|
+
await send_error(send, 405, "Method Not Allowed")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
# Check that the asset’s content type is acceptable to the client.
|
|
239
|
+
if not acceptable(asset.file_type.mime_type, scope["headers"]):
|
|
240
|
+
await send_error(send, 406, "Not Acceptable")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# Prepare the headers.
|
|
244
|
+
content_type = asset.file_type.mime_type
|
|
245
|
+
if content_type.startswith(b"text/"):
|
|
246
|
+
content_type += b"; charset=utf-8"
|
|
247
|
+
headers = [
|
|
248
|
+
(b"content-type", content_type),
|
|
249
|
+
(b"content-length", str(len(asset.content)).encode("ASCII")),
|
|
250
|
+
(b"cache-control", cache_control),
|
|
251
|
+
(b"etag", asset.etag),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
# Check if the client is doing a conditional request that should be skipped.
|
|
255
|
+
if unmodified(asset.etag, scope["headers"]):
|
|
256
|
+
await send(
|
|
257
|
+
{
|
|
258
|
+
"type": "http.response.start",
|
|
259
|
+
"status": 304,
|
|
260
|
+
"headers": headers,
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
await send({"type": "http.response.body", "body": b""})
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Send the asset content.
|
|
267
|
+
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
|
268
|
+
if asset.extracted is not None:
|
|
269
|
+
if "http.response.pathsend" in scope.get("extensions", {}):
|
|
270
|
+
await send(
|
|
271
|
+
# asgi_types doesn’t know about pathsend
|
|
272
|
+
{ # pyright: ignore [reportArgumentType]
|
|
273
|
+
"type": "http.response.pathsend",
|
|
274
|
+
"path": str(asset.extracted),
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
return
|
|
278
|
+
if not self._warned:
|
|
279
|
+
getLogger(__name__).warning(
|
|
280
|
+
"WEBPLAYERCONTROL_ASSETS unused; server does not support "
|
|
281
|
+
"http.response.pathsend",
|
|
282
|
+
)
|
|
283
|
+
self._warned = True
|
|
284
|
+
await send({"type": "http.response.body", "body": asset.content})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
|
2
|
+
<node>
|
|
3
|
+
<interface name="org.freedesktop.DBus">
|
|
4
|
+
<method name="Hello">
|
|
5
|
+
<arg direction="out" type="s"/>
|
|
6
|
+
</method>
|
|
7
|
+
<method name="RequestName">
|
|
8
|
+
<arg direction="in" type="s"/>
|
|
9
|
+
<arg direction="in" type="u"/>
|
|
10
|
+
<arg direction="out" type="u"/>
|
|
11
|
+
</method>
|
|
12
|
+
<method name="ReleaseName">
|
|
13
|
+
<arg direction="in" type="s"/>
|
|
14
|
+
<arg direction="out" type="u"/>
|
|
15
|
+
</method>
|
|
16
|
+
<method name="StartServiceByName">
|
|
17
|
+
<arg direction="in" type="s"/>
|
|
18
|
+
<arg direction="in" type="u"/>
|
|
19
|
+
<arg direction="out" type="u"/>
|
|
20
|
+
</method>
|
|
21
|
+
<method name="UpdateActivationEnvironment">
|
|
22
|
+
<arg direction="in" type="a{ss}"/>
|
|
23
|
+
</method>
|
|
24
|
+
<method name="NameHasOwner">
|
|
25
|
+
<arg direction="in" type="s"/>
|
|
26
|
+
<arg direction="out" type="b"/>
|
|
27
|
+
</method>
|
|
28
|
+
<method name="ListNames">
|
|
29
|
+
<arg direction="out" type="as"/>
|
|
30
|
+
</method>
|
|
31
|
+
<method name="ListActivatableNames">
|
|
32
|
+
<arg direction="out" type="as"/>
|
|
33
|
+
</method>
|
|
34
|
+
<method name="AddMatch">
|
|
35
|
+
<arg direction="in" type="s"/>
|
|
36
|
+
</method>
|
|
37
|
+
<method name="RemoveMatch">
|
|
38
|
+
<arg direction="in" type="s"/>
|
|
39
|
+
</method>
|
|
40
|
+
<method name="GetNameOwner">
|
|
41
|
+
<arg direction="in" type="s"/>
|
|
42
|
+
<arg direction="out" type="s"/>
|
|
43
|
+
</method>
|
|
44
|
+
<method name="ListQueuedOwners">
|
|
45
|
+
<arg direction="in" type="s"/>
|
|
46
|
+
<arg direction="out" type="as"/>
|
|
47
|
+
</method>
|
|
48
|
+
<method name="GetConnectionUnixUser">
|
|
49
|
+
<arg direction="in" type="s"/>
|
|
50
|
+
<arg direction="out" type="u"/>
|
|
51
|
+
</method>
|
|
52
|
+
<method name="GetConnectionUnixProcessID">
|
|
53
|
+
<arg direction="in" type="s"/>
|
|
54
|
+
<arg direction="out" type="u"/>
|
|
55
|
+
</method>
|
|
56
|
+
<method name="GetAdtAuditSessionData">
|
|
57
|
+
<arg direction="in" type="s"/>
|
|
58
|
+
<arg direction="out" type="ay"/>
|
|
59
|
+
</method>
|
|
60
|
+
<method name="GetConnectionSELinuxSecurityContext">
|
|
61
|
+
<arg direction="in" type="s"/>
|
|
62
|
+
<arg direction="out" type="ay"/>
|
|
63
|
+
</method>
|
|
64
|
+
<method name="ReloadConfig">
|
|
65
|
+
</method>
|
|
66
|
+
<method name="GetId">
|
|
67
|
+
<arg direction="out" type="s"/>
|
|
68
|
+
</method>
|
|
69
|
+
<method name="GetConnectionCredentials">
|
|
70
|
+
<arg direction="in" type="s"/>
|
|
71
|
+
<arg direction="out" type="a{sv}"/>
|
|
72
|
+
</method>
|
|
73
|
+
<property name="Features" type="as" access="read">
|
|
74
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
|
|
75
|
+
</property>
|
|
76
|
+
<property name="Interfaces" type="as" access="read">
|
|
77
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
|
|
78
|
+
</property>
|
|
79
|
+
<signal name="NameOwnerChanged">
|
|
80
|
+
<arg type="s"/>
|
|
81
|
+
<arg type="s"/>
|
|
82
|
+
<arg type="s"/>
|
|
83
|
+
</signal>
|
|
84
|
+
<signal name="NameLost">
|
|
85
|
+
<arg type="s"/>
|
|
86
|
+
</signal>
|
|
87
|
+
<signal name="NameAcquired">
|
|
88
|
+
<arg type="s"/>
|
|
89
|
+
</signal>
|
|
90
|
+
<signal name="ActivatableServicesChanged">
|
|
91
|
+
</signal>
|
|
92
|
+
</interface>
|
|
93
|
+
<interface name="org.freedesktop.DBus.Properties">
|
|
94
|
+
<method name="Get">
|
|
95
|
+
<arg direction="in" type="s"/>
|
|
96
|
+
<arg direction="in" type="s"/>
|
|
97
|
+
<arg direction="out" type="v"/>
|
|
98
|
+
</method>
|
|
99
|
+
<method name="GetAll">
|
|
100
|
+
<arg direction="in" type="s"/>
|
|
101
|
+
<arg direction="out" type="a{sv}"/>
|
|
102
|
+
</method>
|
|
103
|
+
<method name="Set">
|
|
104
|
+
<arg direction="in" type="s"/>
|
|
105
|
+
<arg direction="in" type="s"/>
|
|
106
|
+
<arg direction="in" type="v"/>
|
|
107
|
+
</method>
|
|
108
|
+
<signal name="PropertiesChanged">
|
|
109
|
+
<arg type="s" name="interface_name"/>
|
|
110
|
+
<arg type="a{sv}" name="changed_properties"/>
|
|
111
|
+
<arg type="as" name="invalidated_properties"/>
|
|
112
|
+
</signal>
|
|
113
|
+
</interface>
|
|
114
|
+
<interface name="org.freedesktop.DBus.Introspectable">
|
|
115
|
+
<method name="Introspect">
|
|
116
|
+
<arg direction="out" type="s"/>
|
|
117
|
+
</method>
|
|
118
|
+
</interface>
|
|
119
|
+
<interface name="org.freedesktop.DBus.Monitoring">
|
|
120
|
+
<method name="BecomeMonitor">
|
|
121
|
+
<arg direction="in" type="as"/>
|
|
122
|
+
<arg direction="in" type="u"/>
|
|
123
|
+
</method>
|
|
124
|
+
</interface>
|
|
125
|
+
<interface name="org.freedesktop.DBus.Peer">
|
|
126
|
+
<method name="GetMachineId">
|
|
127
|
+
<arg direction="out" type="s"/>
|
|
128
|
+
</method>
|
|
129
|
+
<method name="Ping">
|
|
130
|
+
</method>
|
|
131
|
+
</interface>
|
|
132
|
+
</node>
|