rbx 3.18.4__tar.gz → 3.18.4.dev164__tar.gz
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.
- {rbx-3.18.4 → rbx-3.18.4.dev164}/PKG-INFO +1 -8
- {rbx-3.18.4 → rbx-3.18.4.dev164}/pyproject.toml +2 -12
- rbx-3.18.4.dev164/rbx/__init__.py +1 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx.egg-info/PKG-INFO +1 -8
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx.egg-info/SOURCES.txt +0 -6
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx.egg-info/entry_points.txt +0 -1
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx.egg-info/requires.txt +0 -9
- rbx-3.18.4/rbx/__init__.py +0 -1
- rbx-3.18.4/rbx/toolkit/__init__.py +0 -79
- rbx-3.18.4/rbx/toolkit/browser.py +0 -200
- rbx-3.18.4/rbx/toolkit/cli.py +0 -108
- rbx-3.18.4/rbx/toolkit/exporter.py +0 -53
- rbx-3.18.4/rbx/toolkit/media.py +0 -45
- rbx-3.18.4/rbx/toolkit/utils.py +0 -72
- {rbx-3.18.4 → rbx-3.18.4.dev164}/LICENSE +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/README.md +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/auth/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/auth/decorators.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/auth/keystore.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/auth/mock.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/aws/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/aws/s3.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/cli.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/tasks/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/tasks/apprunner.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/tasks/ec2.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/tasks/image.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/buildtools/tasks/misc.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/adsquare.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/broadsign.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/oxr.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/panels.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/reporting.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/clients/retry.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/exceptions.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/gcp/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/gcp/cloud_tasks.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/gcp/pubsub.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/gcp/storage.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/logging.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/settings.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/utils/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/utils/mdm.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/utils/vast.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/web/__init__.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx/web/handlers.py +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx.egg-info/dependency_links.txt +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/rbx.egg-info/top_level.txt +0 -0
- {rbx-3.18.4 → rbx-3.18.4.dev164}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rbx
|
|
3
|
-
Version: 3.18.4
|
|
3
|
+
Version: 3.18.4.dev164
|
|
4
4
|
Summary: A collection of common tools for Scoota services.
|
|
5
5
|
Author-email: The Scoota Engineering Team <engineering@scoota.com>
|
|
6
6
|
License:
|
|
@@ -249,19 +249,12 @@ Requires-Dist: check-manifest; extra == "buildtools"
|
|
|
249
249
|
Requires-Dist: fabric~=3.2.0; extra == "buildtools"
|
|
250
250
|
Requires-Dist: Jinja2==3.1.2; extra == "buildtools"
|
|
251
251
|
Requires-Dist: twine; extra == "buildtools"
|
|
252
|
-
Provides-Extra: exporter
|
|
253
|
-
Requires-Dist: starlette==0.38.2; extra == "exporter"
|
|
254
252
|
Provides-Extra: notifications
|
|
255
253
|
Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "notifications"
|
|
256
254
|
Provides-Extra: storage
|
|
257
255
|
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "storage"
|
|
258
256
|
Provides-Extra: tasks
|
|
259
257
|
Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "tasks"
|
|
260
|
-
Provides-Extra: toolkit
|
|
261
|
-
Requires-Dist: boto3; extra == "toolkit"
|
|
262
|
-
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "toolkit"
|
|
263
|
-
Requires-Dist: playwright~=1.51.0; extra == "toolkit"
|
|
264
|
-
Requires-Dist: sh~=2.0.6; extra == "toolkit"
|
|
265
258
|
Provides-Extra: web
|
|
266
259
|
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "web"
|
|
267
260
|
Dynamic: license-file
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rbx"
|
|
7
|
-
version = "3.18.4"
|
|
7
|
+
version = "3.18.4.dev164"
|
|
8
8
|
description = "A collection of common tools for Scoota services."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "The Scoota Engineering Team", email = "engineering@scoota.com" }
|
|
@@ -59,9 +59,6 @@ buildtools = [
|
|
|
59
59
|
"Jinja2==3.1.2",
|
|
60
60
|
"twine",
|
|
61
61
|
]
|
|
62
|
-
exporter = [
|
|
63
|
-
"starlette==0.38.2",
|
|
64
|
-
]
|
|
65
62
|
notifications = [
|
|
66
63
|
"google-cloud-pubsub>=2.9,<3"
|
|
67
64
|
]
|
|
@@ -71,26 +68,19 @@ storage = [
|
|
|
71
68
|
tasks = [
|
|
72
69
|
"google-cloud-tasks>=2.7,<3"
|
|
73
70
|
]
|
|
74
|
-
toolkit = [
|
|
75
|
-
"boto3",
|
|
76
|
-
"google-cloud-storage>=2.1,<3",
|
|
77
|
-
"playwright~=1.51.0",
|
|
78
|
-
"sh~=2.0.6",
|
|
79
|
-
]
|
|
80
71
|
web = [
|
|
81
72
|
"google-cloud-error-reporting>=1.9.2,<2"
|
|
82
73
|
]
|
|
83
74
|
|
|
84
75
|
[project.scripts]
|
|
85
76
|
buildtools = "rbx.buildtools.cli:cli [buildtools]"
|
|
86
|
-
toolkit = "rbx.toolkit.cli:cli [toolkit]"
|
|
87
77
|
|
|
88
78
|
[project.urls]
|
|
89
79
|
homepage = "https://github.com/rockabox/rbx"
|
|
90
80
|
repository = "https://github.com/rockabox/rbx.git"
|
|
91
81
|
|
|
92
82
|
[tool.bumpversion]
|
|
93
|
-
current_version = "3.18.4"
|
|
83
|
+
current_version = "3.18.4.dev164"
|
|
94
84
|
commit = true
|
|
95
85
|
parse = """
|
|
96
86
|
(?P<major>\\d+)\\.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.18.4.dev164"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rbx
|
|
3
|
-
Version: 3.18.4
|
|
3
|
+
Version: 3.18.4.dev164
|
|
4
4
|
Summary: A collection of common tools for Scoota services.
|
|
5
5
|
Author-email: The Scoota Engineering Team <engineering@scoota.com>
|
|
6
6
|
License:
|
|
@@ -249,19 +249,12 @@ Requires-Dist: check-manifest; extra == "buildtools"
|
|
|
249
249
|
Requires-Dist: fabric~=3.2.0; extra == "buildtools"
|
|
250
250
|
Requires-Dist: Jinja2==3.1.2; extra == "buildtools"
|
|
251
251
|
Requires-Dist: twine; extra == "buildtools"
|
|
252
|
-
Provides-Extra: exporter
|
|
253
|
-
Requires-Dist: starlette==0.38.2; extra == "exporter"
|
|
254
252
|
Provides-Extra: notifications
|
|
255
253
|
Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "notifications"
|
|
256
254
|
Provides-Extra: storage
|
|
257
255
|
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "storage"
|
|
258
256
|
Provides-Extra: tasks
|
|
259
257
|
Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "tasks"
|
|
260
|
-
Provides-Extra: toolkit
|
|
261
|
-
Requires-Dist: boto3; extra == "toolkit"
|
|
262
|
-
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "toolkit"
|
|
263
|
-
Requires-Dist: playwright~=1.51.0; extra == "toolkit"
|
|
264
|
-
Requires-Dist: sh~=2.0.6; extra == "toolkit"
|
|
265
258
|
Provides-Extra: web
|
|
266
259
|
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "web"
|
|
267
260
|
Dynamic: license-file
|
|
@@ -35,12 +35,6 @@ rbx/gcp/__init__.py
|
|
|
35
35
|
rbx/gcp/cloud_tasks.py
|
|
36
36
|
rbx/gcp/pubsub.py
|
|
37
37
|
rbx/gcp/storage.py
|
|
38
|
-
rbx/toolkit/__init__.py
|
|
39
|
-
rbx/toolkit/browser.py
|
|
40
|
-
rbx/toolkit/cli.py
|
|
41
|
-
rbx/toolkit/exporter.py
|
|
42
|
-
rbx/toolkit/media.py
|
|
43
|
-
rbx/toolkit/utils.py
|
|
44
38
|
rbx/utils/__init__.py
|
|
45
39
|
rbx/utils/mdm.py
|
|
46
40
|
rbx/utils/vast.py
|
|
@@ -29,9 +29,6 @@ fabric~=3.2.0
|
|
|
29
29
|
Jinja2==3.1.2
|
|
30
30
|
twine
|
|
31
31
|
|
|
32
|
-
[exporter]
|
|
33
|
-
starlette==0.38.2
|
|
34
|
-
|
|
35
32
|
[notifications]
|
|
36
33
|
google-cloud-pubsub<3,>=2.9
|
|
37
34
|
|
|
@@ -41,11 +38,5 @@ google-cloud-storage<3,>=2.1
|
|
|
41
38
|
[tasks]
|
|
42
39
|
google-cloud-tasks<3,>=2.7
|
|
43
40
|
|
|
44
|
-
[toolkit]
|
|
45
|
-
boto3
|
|
46
|
-
google-cloud-storage<3,>=2.1
|
|
47
|
-
playwright~=1.51.0
|
|
48
|
-
sh~=2.0.6
|
|
49
|
-
|
|
50
41
|
[web]
|
|
51
42
|
google-cloud-error-reporting<2,>=1.9.2
|
rbx-3.18.4/rbx/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.18.4"
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
__all__ = ["Options", "run"]
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import tempfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import NamedTuple, Optional
|
|
7
|
-
|
|
8
|
-
from .browser import record, screenshot
|
|
9
|
-
from .utils import upload
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Options(NamedTuple):
|
|
15
|
-
url: str
|
|
16
|
-
width: int
|
|
17
|
-
height: int
|
|
18
|
-
format: str
|
|
19
|
-
duration: Optional[int] = 0
|
|
20
|
-
path: Optional[str] = "/tmp"
|
|
21
|
-
output: Optional[str] = "."
|
|
22
|
-
filename: Optional[str] = None
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def filename(output, filename):
|
|
26
|
-
if output.startswith("s3://"):
|
|
27
|
-
return "s3://" + str(Path(output[5:]) / filename)
|
|
28
|
-
elif output.startswith("gs://"):
|
|
29
|
-
return "gs://" + str(Path(output[5:]) / filename)
|
|
30
|
-
return str(Path(output) / filename)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
async def capture(filename: str, options: Options, path: Path) -> None:
|
|
34
|
-
await record(
|
|
35
|
-
dirname=str(path),
|
|
36
|
-
filename=str(path / filename),
|
|
37
|
-
duration=options.duration,
|
|
38
|
-
height=options.height,
|
|
39
|
-
url=options.url,
|
|
40
|
-
width=options.width,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
async def screengrab(filename: str, options: Options, path: Path) -> None:
|
|
45
|
-
await screenshot(
|
|
46
|
-
filename=str(path / filename),
|
|
47
|
-
height=options.height,
|
|
48
|
-
url=options.url,
|
|
49
|
-
width=options.width,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
async def run(options: Options) -> None:
|
|
54
|
-
with tempfile.TemporaryDirectory(dir=options.path) as dirname:
|
|
55
|
-
path = Path(dirname)
|
|
56
|
-
logger.debug(f"Working directory: '{path}'")
|
|
57
|
-
if options.format == "video":
|
|
58
|
-
asset = options.filename or "video.mp4"
|
|
59
|
-
output = filename(options.output, asset)
|
|
60
|
-
if options.duration:
|
|
61
|
-
logger.info(
|
|
62
|
-
f"Capturing '{options.url}' [{options.width}x{options.height}]"
|
|
63
|
-
f" for {options.duration}ms to '{output}'",
|
|
64
|
-
)
|
|
65
|
-
else:
|
|
66
|
-
logger.info(
|
|
67
|
-
f"Capturing '{options.url}' [{options.width}x{options.height}] to '{output}'",
|
|
68
|
-
)
|
|
69
|
-
await capture(filename=asset, options=options, path=path)
|
|
70
|
-
else:
|
|
71
|
-
asset = options.filename or "screenshot.png"
|
|
72
|
-
output = filename(options.output, asset)
|
|
73
|
-
logger.info(
|
|
74
|
-
f"Taking screenshot of '{options.url}' [{options.width}x{options.height}]"
|
|
75
|
-
f" to '{output}'"
|
|
76
|
-
)
|
|
77
|
-
await screengrab(filename=asset, options=options, path=path)
|
|
78
|
-
|
|
79
|
-
upload(path / asset, output)
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import signal
|
|
5
|
-
import subprocess
|
|
6
|
-
import time
|
|
7
|
-
import uuid
|
|
8
|
-
|
|
9
|
-
from playwright.async_api import async_playwright
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_free_display(base=99, max_tries=100):
|
|
15
|
-
for i in range(max_tries):
|
|
16
|
-
display = f":{base + i}"
|
|
17
|
-
if not os.path.exists(f"/tmp/.X{base + i}-lock"):
|
|
18
|
-
return display
|
|
19
|
-
|
|
20
|
-
raise RuntimeError("No free display found")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class RecordingSession:
|
|
24
|
-
def __init__(self, dirname, width, height):
|
|
25
|
-
self.display = get_free_display()
|
|
26
|
-
self.dirname = dirname
|
|
27
|
-
self.width = width
|
|
28
|
-
self.height = height
|
|
29
|
-
self.resolution = f"{self.width}x{self.height}"
|
|
30
|
-
self.session_id = str(uuid.uuid4())[:8]
|
|
31
|
-
self.sink_name = f"rec_sink_{self.session_id}"
|
|
32
|
-
self.pulse_dir = f"{dirname}/pulseaudio"
|
|
33
|
-
self.pulse_socket = os.path.join(self.pulse_dir, "pulse-socket")
|
|
34
|
-
self.proc = []
|
|
35
|
-
|
|
36
|
-
async def __aenter__(self):
|
|
37
|
-
return self
|
|
38
|
-
|
|
39
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
40
|
-
logger.debug("Stopping processes...")
|
|
41
|
-
|
|
42
|
-
# PulseAudio needs to be terminated via the kill command.
|
|
43
|
-
subprocess.Popen(
|
|
44
|
-
["pulseaudio", "--kill"],
|
|
45
|
-
env={
|
|
46
|
-
"PULSE_SERVER": f"unix:{self.pulse_socket}",
|
|
47
|
-
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
48
|
-
},
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
for proc in reversed(self.proc):
|
|
52
|
-
if proc and proc.poll() is None:
|
|
53
|
-
try:
|
|
54
|
-
proc.send_signal(signal.SIGINT)
|
|
55
|
-
proc.wait(timeout=3)
|
|
56
|
-
except subprocess.TimeoutExpired:
|
|
57
|
-
proc.kill()
|
|
58
|
-
|
|
59
|
-
def start_xvfb(self):
|
|
60
|
-
logger.debug(f"Starting Xvfb [{self.display}]...")
|
|
61
|
-
# fmt: off
|
|
62
|
-
self.proc.append(
|
|
63
|
-
subprocess.Popen(
|
|
64
|
-
[
|
|
65
|
-
"Xvfb", self.display, "-screen", "0", f"{self.resolution}x24",
|
|
66
|
-
# These options are needed when running in a container
|
|
67
|
-
"-ac", "-nolisten", "tcp", "-nolisten", "unix",
|
|
68
|
-
]
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
# fmt: on
|
|
72
|
-
time.sleep(1)
|
|
73
|
-
|
|
74
|
-
logger.debug(f"Starting unclutter [{self.display}]...")
|
|
75
|
-
self.proc.append(
|
|
76
|
-
subprocess.Popen(
|
|
77
|
-
["unclutter", "-idle", "0"],
|
|
78
|
-
env={"DISPLAY": self.display},
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
def start_pulseaudio(self):
|
|
83
|
-
logger.debug(f"Starting PulseAudio [{self.pulse_dir}.{self.sink_name}]...")
|
|
84
|
-
os.makedirs(self.pulse_dir, exist_ok=True)
|
|
85
|
-
os.chmod(self.pulse_dir, 0o700) # PulseAudio requires strict permissions
|
|
86
|
-
|
|
87
|
-
subprocess.Popen(
|
|
88
|
-
[
|
|
89
|
-
"pulseaudio",
|
|
90
|
-
"--start",
|
|
91
|
-
"--disable-shm",
|
|
92
|
-
"--exit-idle-time=300",
|
|
93
|
-
f"--load=module-native-protocol-unix socket={self.pulse_socket}",
|
|
94
|
-
f"--load=module-null-sink sink_name={self.sink_name}"
|
|
95
|
-
f" sink_properties=device.description={self.sink_name}",
|
|
96
|
-
],
|
|
97
|
-
env={
|
|
98
|
-
"DISPLAY": self.display,
|
|
99
|
-
"PULSE_NO_SIMD": "1",
|
|
100
|
-
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
101
|
-
},
|
|
102
|
-
)
|
|
103
|
-
time.sleep(2)
|
|
104
|
-
|
|
105
|
-
def start_ffmpeg(self, duration, output_path):
|
|
106
|
-
logger.debug(f"Starting FFmpeg recording [{self.display}:{self.sink_name}]...")
|
|
107
|
-
# fmt: off
|
|
108
|
-
self.proc.append(
|
|
109
|
-
subprocess.Popen(
|
|
110
|
-
[
|
|
111
|
-
"ffmpeg", "-y",
|
|
112
|
-
"-t", str(duration),
|
|
113
|
-
"-video_size", self.resolution,
|
|
114
|
-
"-f", "x11grab",
|
|
115
|
-
"-i", f"{self.display}.0",
|
|
116
|
-
"-f", "pulse",
|
|
117
|
-
"-i", f"{self.sink_name}.monitor",
|
|
118
|
-
"-c:v", "libx264",
|
|
119
|
-
"-preset", "slow",
|
|
120
|
-
"-crf", "8",
|
|
121
|
-
"-pix_fmt", "yuv420p",
|
|
122
|
-
"-c:a", "aac",
|
|
123
|
-
"-b:a", "256k",
|
|
124
|
-
"-movflags",
|
|
125
|
-
"+faststart",
|
|
126
|
-
output_path,
|
|
127
|
-
],
|
|
128
|
-
stdout=subprocess.DEVNULL,
|
|
129
|
-
stderr=subprocess.STDOUT,
|
|
130
|
-
env={
|
|
131
|
-
"PULSE_SERVER": f"unix:{self.pulse_socket}",
|
|
132
|
-
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
133
|
-
},
|
|
134
|
-
)
|
|
135
|
-
)
|
|
136
|
-
# fmt: on
|
|
137
|
-
|
|
138
|
-
async def capture(self, url, duration, output_path):
|
|
139
|
-
self.start_xvfb()
|
|
140
|
-
self.start_pulseaudio()
|
|
141
|
-
|
|
142
|
-
async with async_playwright() as p:
|
|
143
|
-
logger.debug(
|
|
144
|
-
f"Capturing Chromium session [{self.display}:{self.sink_name}]..."
|
|
145
|
-
)
|
|
146
|
-
context = await p.chromium.launch_persistent_context(
|
|
147
|
-
user_data_dir=f"{self.dirname}/user-data",
|
|
148
|
-
headless=False,
|
|
149
|
-
channel="chrome",
|
|
150
|
-
args=[
|
|
151
|
-
"--no-sandbox",
|
|
152
|
-
"--disable-gpu",
|
|
153
|
-
"--disable-software-rasterizer",
|
|
154
|
-
f"--display={self.display}",
|
|
155
|
-
"--autoplay-policy=no-user-gesture-required",
|
|
156
|
-
"--use-fake-ui-for-media-stream",
|
|
157
|
-
f"--alsa-output-device={self.sink_name}",
|
|
158
|
-
"--start-fullscreen",
|
|
159
|
-
"--window-position=0,0",
|
|
160
|
-
f"--window-size={self.width},{self.height}",
|
|
161
|
-
],
|
|
162
|
-
ignore_default_args=["--enable-automation"],
|
|
163
|
-
no_viewport=True,
|
|
164
|
-
env={
|
|
165
|
-
"PULSE_SERVER": f"unix:{self.pulse_socket}",
|
|
166
|
-
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
167
|
-
},
|
|
168
|
-
)
|
|
169
|
-
page = await context.new_page()
|
|
170
|
-
await page.goto(url, wait_until="commit")
|
|
171
|
-
|
|
172
|
-
logger.info(
|
|
173
|
-
f"Recording session [{self.display}:{self.sink_name}] for {duration}ms"
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
self.start_ffmpeg(duration, output_path)
|
|
177
|
-
|
|
178
|
-
await page.wait_for_timeout(duration) # Wait for the duration
|
|
179
|
-
await context.close()
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
async def record(
|
|
183
|
-
dirname: str, filename: str, duration: int, height: int, url: str, width: int
|
|
184
|
-
) -> None:
|
|
185
|
-
async with RecordingSession(dirname, width, height) as session:
|
|
186
|
-
await session.capture(url, duration, filename)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
async def screenshot(filename: str, height: int, url: str, width: int) -> None:
|
|
190
|
-
async with async_playwright() as p:
|
|
191
|
-
browser = await p.chromium.launch()
|
|
192
|
-
page = await browser.new_page()
|
|
193
|
-
await page.set_viewport_size({"width": width, "height": height})
|
|
194
|
-
await page.goto(url, wait_until="networkidle")
|
|
195
|
-
await page.screenshot(path=filename)
|
|
196
|
-
await browser.close()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def to_ms(timedelta: datetime.timedelta) -> int:
|
|
200
|
-
return round(1000 * timedelta.total_seconds())
|
rbx-3.18.4/rbx/toolkit/cli.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import datetime
|
|
3
|
-
import logging
|
|
4
|
-
import logging.config
|
|
5
|
-
import os
|
|
6
|
-
|
|
7
|
-
import click
|
|
8
|
-
|
|
9
|
-
from . import Options, run
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ColourFormatter(logging.Formatter):
|
|
13
|
-
def format(self, record):
|
|
14
|
-
message = super().format(record)
|
|
15
|
-
colours = {
|
|
16
|
-
logging.INFO: {"bold": True},
|
|
17
|
-
logging.WARNING: {"fg": "yellow"},
|
|
18
|
-
logging.ERROR: {"fg": "bright_red"},
|
|
19
|
-
logging.CRITICAL: {"fg": "bright_white", "bg": "red"},
|
|
20
|
-
}
|
|
21
|
-
try:
|
|
22
|
-
message = click.style(message, **colours[record.levelno])
|
|
23
|
-
except KeyError:
|
|
24
|
-
pass
|
|
25
|
-
return message
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@click.group()
|
|
29
|
-
@click.version_option(message="%(version)s")
|
|
30
|
-
def cli():
|
|
31
|
-
"""Toolkit Creative Converter."""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def validate_filename(ctx, param, value):
|
|
35
|
-
if value is None:
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
if ctx.params["format"] == "image" and not value.endswith(".png"):
|
|
39
|
-
raise click.BadParameter(f"{param.name} must use the '.png' extension")
|
|
40
|
-
elif ctx.params["format"] == "video" and not value.endswith(".mp4"):
|
|
41
|
-
raise click.BadParameter(f"{param.name} must use the '.mp4' extension")
|
|
42
|
-
|
|
43
|
-
return value
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@cli.command(context_settings={"show_default": True})
|
|
47
|
-
@click.argument("url")
|
|
48
|
-
@click.option("--width", "-w", type=click.INT, default=1080)
|
|
49
|
-
@click.option("--height", "-h", type=click.INT, default=1920)
|
|
50
|
-
@click.option(
|
|
51
|
-
"--format",
|
|
52
|
-
type=click.Choice(["image", "video"], case_sensitive=False),
|
|
53
|
-
default="image",
|
|
54
|
-
)
|
|
55
|
-
@click.option("--duration", "-d", type=click.INT, default=0)
|
|
56
|
-
@click.option("--path", "-p", default="/tmp")
|
|
57
|
-
@click.option("--output-dir", "-o", type=click.Path(file_okay=False), default=".")
|
|
58
|
-
@click.option("--filename", "-f", callback=validate_filename)
|
|
59
|
-
def export(url, width, height, format, duration, path, output_dir, filename):
|
|
60
|
-
log_level = os.getenv("LOG_LEVEL", "INFO")
|
|
61
|
-
logging.config.dictConfig(
|
|
62
|
-
{
|
|
63
|
-
"version": 1,
|
|
64
|
-
"formatters": {
|
|
65
|
-
"coloured": {
|
|
66
|
-
"class": "rbx.toolkit.cli.ColourFormatter",
|
|
67
|
-
"format": "[%(asctime)s] %(message)s",
|
|
68
|
-
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
"handlers": {
|
|
72
|
-
"console": {
|
|
73
|
-
"level": "DEBUG",
|
|
74
|
-
"class": "logging.StreamHandler",
|
|
75
|
-
"formatter": "coloured",
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
"loggers": {
|
|
79
|
-
"rbx.toolkit": {
|
|
80
|
-
"level": log_level,
|
|
81
|
-
"handlers": ["console"],
|
|
82
|
-
"propagate": False,
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
start_timestamp = datetime.datetime.now()
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
asyncio.run(
|
|
92
|
-
run(
|
|
93
|
-
options=Options(
|
|
94
|
-
url=url,
|
|
95
|
-
width=width,
|
|
96
|
-
height=height,
|
|
97
|
-
format=format,
|
|
98
|
-
duration=duration,
|
|
99
|
-
path=path,
|
|
100
|
-
output=output_dir,
|
|
101
|
-
filename=filename,
|
|
102
|
-
)
|
|
103
|
-
)
|
|
104
|
-
)
|
|
105
|
-
except RuntimeError as e:
|
|
106
|
-
click.secho(e, fg="red", bold=True)
|
|
107
|
-
|
|
108
|
-
click.secho(f"Finished in {datetime.datetime.now() - start_timestamp}", bold=True)
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from starlette.applications import Starlette
|
|
4
|
-
from starlette.config import Config
|
|
5
|
-
from starlette.responses import JSONResponse
|
|
6
|
-
from starlette.routing import Route
|
|
7
|
-
|
|
8
|
-
from . import Options, run
|
|
9
|
-
|
|
10
|
-
config = Config()
|
|
11
|
-
logging.basicConfig(level=config("LOG_LEVEL", default="INFO"))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def handler(request):
|
|
15
|
-
payload = await request.json()
|
|
16
|
-
|
|
17
|
-
errors = []
|
|
18
|
-
for param in ("url", "width", "height", "format", "name"):
|
|
19
|
-
if param not in payload.keys():
|
|
20
|
-
errors.append(f"Missing required '{param}' parameter")
|
|
21
|
-
|
|
22
|
-
if payload.get("format") == "video" and "duration" not in payload.keys():
|
|
23
|
-
errors.append("Missing required 'duration' parameter")
|
|
24
|
-
|
|
25
|
-
if errors:
|
|
26
|
-
return JSONResponse({"errors": errors}, status_code=400)
|
|
27
|
-
|
|
28
|
-
ext = "mp4" if payload["format"] == "video" else "png"
|
|
29
|
-
filename = f"{payload['name']}.{ext}"
|
|
30
|
-
project_id = config("GOOGLE_CLOUD_PROJECT", default="dev-platform-eu")
|
|
31
|
-
output = f"gs://{project_id}.appspot.com/toolkit/exports/"
|
|
32
|
-
|
|
33
|
-
await run(
|
|
34
|
-
options=Options(
|
|
35
|
-
url=payload["url"],
|
|
36
|
-
width=payload["width"],
|
|
37
|
-
height=payload["height"],
|
|
38
|
-
format=payload["format"],
|
|
39
|
-
duration=int(payload.get("duration", 0)),
|
|
40
|
-
output=output,
|
|
41
|
-
filename=filename,
|
|
42
|
-
)
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
return JSONResponse({"path": f"{output}{filename}"})
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def create_app() -> Starlette:
|
|
49
|
-
return Starlette(
|
|
50
|
-
routes=[
|
|
51
|
-
Route("/", endpoint=handler, methods=["POST"]),
|
|
52
|
-
]
|
|
53
|
-
)
|
rbx-3.18.4/rbx/toolkit/media.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
import sh
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def convert(
|
|
7
|
-
infile: str,
|
|
8
|
-
outfile: str,
|
|
9
|
-
delay: Optional[int] = 0,
|
|
10
|
-
duration: Optional[int] = 0,
|
|
11
|
-
opts: Optional[list] = None,
|
|
12
|
-
):
|
|
13
|
-
"""Convert `infile` to `outfile`.
|
|
14
|
-
|
|
15
|
-
For WebM to MP4 conversion, see
|
|
16
|
-
https://blog.addpipe.com/converting-webm-to-mp4-with-ffmpeg/
|
|
17
|
-
|
|
18
|
-
"""
|
|
19
|
-
args = ["-y"]
|
|
20
|
-
args.extend(["-i", infile])
|
|
21
|
-
if delay:
|
|
22
|
-
args.extend(["-ss", milliseconds_to_duration(delay)])
|
|
23
|
-
if duration:
|
|
24
|
-
args.extend(["-to", milliseconds_to_duration(delay + duration)])
|
|
25
|
-
if delay or duration:
|
|
26
|
-
args.extend(["-c:v", "libx264", "-preset", "slow", "-crf", "8"])
|
|
27
|
-
if opts:
|
|
28
|
-
args.extend(opts)
|
|
29
|
-
args.append(outfile)
|
|
30
|
-
ffmpg(args)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def ffmpg(args):
|
|
34
|
-
try:
|
|
35
|
-
sh.ffmpeg(*args)
|
|
36
|
-
except sh.ErrorReturnCode as e:
|
|
37
|
-
raise RuntimeError(
|
|
38
|
-
f"Command {e.full_cmd} exited with {e.exit_code}\n\n{e.stderr.decode()}"
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def milliseconds_to_duration(milliseconds):
|
|
43
|
-
minutes, rem = divmod(milliseconds / 1000.0, 60)
|
|
44
|
-
seconds, ms = divmod(1000 * rem, 1000)
|
|
45
|
-
return f"00:{int(minutes):02d}:{int(seconds):02d}.{int(ms)}"
|
rbx-3.18.4/rbx/toolkit/utils.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
import shutil
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import boto3
|
|
7
|
-
from botocore.exceptions import ClientError
|
|
8
|
-
from google.cloud import storage
|
|
9
|
-
from google.cloud.exceptions import GoogleCloudError
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def upload(filename: Path, target: str):
|
|
13
|
-
if target.startswith(("gs://", "s3://")):
|
|
14
|
-
service, _, target = str(target).partition("://")
|
|
15
|
-
parts = target.split("/")
|
|
16
|
-
bucket = parts.pop(0)
|
|
17
|
-
object_name = "/".join(parts)
|
|
18
|
-
if service == "s3":
|
|
19
|
-
upload_to_s3(filename, bucket, object_name)
|
|
20
|
-
elif service == "gs":
|
|
21
|
-
upload_to_storage(filename, bucket, object_name)
|
|
22
|
-
else:
|
|
23
|
-
logging.error(f"Unknown upload service '{service}'")
|
|
24
|
-
|
|
25
|
-
else:
|
|
26
|
-
shutil.copy(filename, target)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def upload_to_s3(filename, bucket, object_name=None):
|
|
30
|
-
"""Upload a file to an S3 bucket
|
|
31
|
-
|
|
32
|
-
:param file_name: File to upload
|
|
33
|
-
:param bucket: Bucket to upload to
|
|
34
|
-
:param object_name: S3 object name. If not specified then file_name is used
|
|
35
|
-
:return: True if file was uploaded, else False
|
|
36
|
-
"""
|
|
37
|
-
if object_name is None:
|
|
38
|
-
object_name = os.path.basename(filename)
|
|
39
|
-
|
|
40
|
-
s3_client = boto3.client("s3")
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
s3_client.upload_file(filename, bucket, object_name)
|
|
44
|
-
except ClientError as e:
|
|
45
|
-
logging.error(e)
|
|
46
|
-
return False
|
|
47
|
-
|
|
48
|
-
return True
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def upload_to_storage(filename, bucket, object_name=None):
|
|
52
|
-
"""Upload a file to a Cloud Storage bucket
|
|
53
|
-
|
|
54
|
-
:param file_name: File to upload
|
|
55
|
-
:param bucket: Bucket to upload to
|
|
56
|
-
:param object_name: object name. If not specified then file_name is used
|
|
57
|
-
:return: True if file was uploaded, else False
|
|
58
|
-
"""
|
|
59
|
-
if object_name is None:
|
|
60
|
-
object_name = os.path.basename(filename)
|
|
61
|
-
|
|
62
|
-
client = storage.Client()
|
|
63
|
-
bucket = client.bucket(bucket)
|
|
64
|
-
blob = bucket.blob(object_name)
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
blob.upload_from_filename(filename)
|
|
68
|
-
except GoogleCloudError as e:
|
|
69
|
-
logging.error(e)
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|