released :P

This commit is contained in:
Pari Malam
2026-06-15 18:44:59 +08:00
commit 2b1869e4a2
18 changed files with 1365 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.vscode/
launcher.go
/build/
o11pro
workspace-69e17692-9d10-431b-adee-503d25eb1a71.tar
+259
View File
@@ -0,0 +1,259 @@
# O11 Pro Cracked - Unpacked Version
**O11 Pro** is a streaming media server capable of proxying, remuxing, and redistributing IPTV/OTT live and VOD streams. It features a built-in web UI, EPG support, HLS/DASH handling, and DRM decryption via CDM integration.
---
## Quick Start
```bash
# Minimum: just set a port
./o11pro_unpacked -p 8080
# With web UI login credentials
./o11pro_unpacked -p 8080 -user admin -password yourpassword
# Headless mode (no web UI)
./o11pro_unpacked -p 8080 -headless
# With separate EPG and streaming ports
./o11pro_unpacked -p 8080 -epgport 8081 -streamport 8082
# Debug mode
./o11pro_unpacked -p 8080 -v 3 -stdout
```
When started without `-user` and `-password`, a temporary admin account is auto-generated and printed to the log:
```
WARN: Use temporary account to login to Web UI: admin / OtoN4Fx0
```
Open `http://<your-ip>:<port>` in a browser to access the web interface.
---
## Command-Line Options
### Server & Network
| Flag | Description | Default |
|------|-------------|---------|
| `-p` | **HTTP port** to listen on (required) | `0` (none - must be set) |
| `-b` | HTTP bind address | `0.0.0.0` (all interfaces) |
| `-https` | Enable HTTPS (requires `server.crt` and `server.key` in the O11 directory) | `false` |
| `-baseurl` | HTTP base URL (for reverse proxy setups) | *(empty)* |
| `-streamport` | Dedicated streaming port. If not set, the HTTP port is used | `0` (shares HTTP port) |
| `-streambind` | Streaming bind address | `0.0.0.0` |
| `-epgport` | EPG (Electronic Program Guide) port | `0` (disabled) |
| `-epgbind` | EPG bind address | `0.0.0.0` |
| `-allow` | Comma-separated list of no-auth admin source IP addresses (e.g. `192.168.1.0/24,10.0.0.1`) | *(empty)* |
### Authentication & Security
| Flag | Description | Default |
|------|-------------|---------|
| `-user` | Static admin username for web UI login | *(empty - temp account generated)* |
| `-password` | Static admin password for web UI login | *(empty - temp account generated)* |
| `-jwtsecret` | JWT secret for login sessions. Default is randomly generated on each start | *(random)* |
### Configuration & Files
| Flag | Description | Default |
|------|-------------|---------|
| `-c` | Main config file path | `o11.cfg` |
| `-job` | Jobs config file path | `o11-job.cfg` |
| `-rec` | Recordings config file path | `o11-rec.cfg` |
| `-path` | Working directory. All sub-directories (HLS, logs, etc.) are created here | current directory |
| `-providers` | Providers directory path | `providers/` |
| `-keys` | File with KID:KEY pairs for decryption fallback | `keys.txt` |
| `-f` | FFmpeg binary path | `ffmpeg` |
| `-tsplay` | Tsplay binary path (for UDP output mode) | `tsplay` |
### Logging
| Flag | Description | Default |
|------|-------------|---------|
| `-v` | Log level: `0`=error, `1`=warning, `2`=info, `3`=debug, `4`=verbose, `5`=trace | `2` (info) |
| `-stdout` | Log to stdout instead of log files | `false` |
| `-logsize` | Max log file size in MB (per file) | `100` |
| `-logscount` | Number of log file rotations to keep | `7` |
| `-logtomain` | Also send stream-specific logs to the main log | `false` |
| `-logtomainonly` | Only send stream logs to main log (no separate stream logs) | `false` |
| `-V` | Module to enable special debug logs from (e.g. `subtitles`) | *(empty)* |
### Runtime Behavior
| Flag | Description | Default |
|------|-------------|---------|
| `-headless` | Run without the web UI | `false` |
| `-noautostart` | Do not auto-start providers/channels on launch | `false` |
| `-flushperiod` | Seconds between config/key flushes to disk. `0` to disable | `300` |
| `-keep` | Keep temp media files after processing (debugging) | `true` |
| `-noramfs` | Allow FFmpeg mode even if `./hls/live` is not a RAMFS | `false` |
| `-remuxondisk` | Write internal remuxer temporary files to disk instead of RAM | `false` |
| `-debugspeed` | Print current fragment download queue per channel/media type | `false` |
| `-defaultprovid` | Default provider ID to display when none specified | *(empty)* |
### Playlist & Stream Naming
| Flag | Description | Default |
|------|-------------|---------|
| `-plstreamname` | Format for stream names in playlists. `%p` = provider name, `%s` = stream name | `[%p] %s` |
| `-alteventdateformat` | Use alternate date format `02 Jan. 2006 15:04` for EPG events | `false` |
| `-novodmetadata` | Disable metadata for VOD tracks | `false` |
| `-pipeoutputcmd` | Pipe output command format (overridable per provider). `%s` = format placeholder | `tsplay -stdin %s` |
### Replay Mode
| Flag | Description | Default |
|------|-------------|---------|
| `-replay` | Local stream path to replay (from `hls/live/`) | *(empty)* |
| `-replaymode` | Replay mode: `ffmpeg`, `internalremuxer`, etc. | `internalremuxer` |
### User-Agent & HTTP Headers
| Flag | Description | Default |
|------|-------------|---------|
| `-a` | Default User-Agent for HTTP connections | Chrome 122 on macOS |
### VOD Downloader
These options are used when downloading and converting VOD (Video On Demand) streams:
| Flag | Description | Default |
|------|-------------|---------|
| `-manifest` | Download and convert this manifest URL to MP4, then exit | *(empty)* |
| `-video` | Video track index to grab | *(empty - all)* |
| `-audio` | Audio track indexes to grab (comma-separated) | *(empty - all)* |
| `-subs` | Subtitle indexes to grab (comma-separated or `all`) | *(empty - none)* |
| `-extrasubs` | Extra subtitle file URL. Can be used multiple times | *(empty)* |
| `-maxsegments` | Max segments to download (`0` = unlimited) | `0` |
| `-dashperiod` | Force DASH period index. `-1` = all non-ad periods | `-2` |
### DRM / CDM Decryption
| Flag | Description | Default |
|------|-------------|---------|
| `-usecdm` | Enable CDM script for DRM decryption | `false` |
| `-cdmtype` | CDM type: `widevine`, `playready`, or `verimatrix` | `widevine` |
| `-cdmmode` | CDM mode: `internal` or `external` | `internal` |
| `-script` | CDM script name | `auto` |
| `-cdmparams` | CDM script parameters | *(empty)* |
| `-key` | KID:KEY pair for decryption. Can be used multiple times | *(empty)* |
| `-doh` | DNS-over-HTTPS URL for CDM requests | *(empty)* |
| `-H` | Custom HTTP header for VOD requests (format: `key:value`). Can be used multiple times | *(empty)* |
---
## Examples
### Basic IPTV Proxy
```bash
./o11pro_unpacked -p 8080 -user admin -password mysecretpass
```
Access the web UI at `http://localhost:8080`, log in with `admin` / `mysecretpass`, and add your providers through the interface.
### Separate Streaming + EPG Ports
```bash
./o11pro_unpacked -p 8080 -streamport 9090 -epgport 9091 -user admin -password pass
```
- **Web UI**: `http://localhost:8080`
- **Streaming**: `http://localhost:9090`
- **EPG**: `http://localhost:9091`
### HTTPS Mode
```bash
# Place your certificates in the working directory first
cp server.crt server.key /path/to/o11/
./o11pro_unpacked -p 8443 -https -user admin -password pass
```
Access via `https://localhost:8443`.
### VOD Download with DRM
```bash
# Download with manual decryption keys
./o11pro_unpacked -manifest "https://example.com/manifest.mpd" \
-key "eb676abbcb345e96bbcf616630f1a3da:234567890abcdef1234567890abcdef1" \
-video 0 -audio 0,1 -subs all
# Download with CDM script
./o11pro_unpacked -manifest "https://example.com/manifest.mpd" \
-usecdm -cdmtype widevine -cdmmode internal
```
### Headless with Custom Config
```bash
./o11pro_unpacked -p 8080 -headless -c /etc/o11/myconfig.cfg \
-path /var/lib/o11 -providers /etc/o11/providers/
```
### Bind to Localhost Only
```bash
./o11pro_unpacked -p 8080 -b 127.0.0.1 -streambind 127.0.0.1
```
### Full Debug Mode
```bash
./o11pro_unpacked -p 8080 -v 5 -stdout -debugspeed
```
### Replay a Local Stream
```bash
./o11pro_unpacked -p 8080 -replay my_channel_stream -replaymode internalremuxer
```
---
## Directory Structure
When using `-path`, O11 creates the following sub-directories:
```
<path>/
├── hls/
│ └── live/ # Live HLS segments (recommend RAMFS)
├── logs/ # Rotating log files
├── providers/ # Provider configuration files
├── keys.txt # KID:KEY decryption fallback
├── o11.cfg # Main configuration
├── o11-job.cfg # Jobs configuration
└── o11-rec.cfg # Recordings configuration
```
---
## Log Levels Reference
| Level | Name | Description |
|-------|------|-------------|
| `0` | Error | Critical errors only |
| `1` | Warning | Warnings and errors |
| `2` | Info | General operational info (default) |
| `3` | Debug | Detailed debug information |
| `4` | Verbose | Very detailed output |
| `5` | Trace | Maximum verbosity, including internal state |
---
## Notes
- resources/ is a embedded content from the unpacked binary
- **The `-p` flag is required.** The binary will not start without an HTTP port.
- Without `-user` / `-password`, a temporary admin account is generated on each start and printed to the log.
- For HTTPS, both `server.crt` and `server.key` must exist in the O11 working directory.
- The `-key` flag accepts KID:KEY pairs in hex format and can be specified multiple times for multiple keys.
- The `-H` flag for custom headers can be repeated: `-H "Authorization:Bearer token" -H "X-Custom:value"`.
- VOD downloader mode (`-manifest`) downloads, converts, and exits - it does not start the server.
---
+359
View File
@@ -0,0 +1,359 @@
#!/usr/bin/python3
import sys
import os
import o11
import base64
import json
import time
from dateutil import parser
from deep_translator import GoogleTranslator
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
user=o11.parse_params(sys.argv, 'user')
password=o11.parse_params(sys.argv, 'password')
device=o11.parse_params(sys.argv, 'device')
pin=o11.parse_params(sys.argv, 'pin')
if user == "" or password == "":
print("no account specificed")
sys.exit(1)
if len(device) != 36:
print("device must be a UUID")
sys.exit(1)
id=o11.parse_params(sys.argv, 'id')
text=o11.parse_params(sys.argv, 'text')
srclang=o11.parse_params(sys.argv, 'srclang')
dstlang=o11.parse_params(sys.argv, 'dstlang')
action=o11.parse_params(sys.argv, 'action')
url=o11.parse_params(sys.argv, 'url')
bind=o11.parse_params(sys.argv, 'bind')
proxy=o11.parse_params(sys.argv, 'proxy')
doh=o11.parse_params(sys.argv, 'doh')
dns=o11.parse_params(sys.argv, 'dns')
worker=o11.parse_params(sys.argv, 'worker')
cdm=o11.parse_params(sys.argv, 'cdm')
drm=o11.parse_params(sys.argv, 'drm')
kid=o11.parse_params(sys.argv, 'kid')
pssh=o11.parse_params(sys.argv, 'pssh')
challenge=o11.parse_params(sys.argv, 'challenge')
licenseurl=o11.parse_params(sys.argv, 'licenseurl')
licenseparams=o11.parse_params(sys.argv, 'licenseparams')
heartbeaturl=o11.parse_params(sys.argv, 'heartbeaturl')
heartbeatparams=o11.parse_params(sys.argv, 'heartbeatparams')
o11Session = o11.session(bind=bind, proxy=proxy, worker=worker, doh=doh, dns=dns)
req = o11Session.get_session()
if doh != "":
o11.dns(doh)
elif dns != "":
o11.dns(dns)
if action == "subtitles":
if srclang == "":
srclang = "auto"
output = {
"Text": GoogleTranslator(source=srclang, target=dstlang).translate(text)
}
print(json.dumps(output))
sys.exit(1)
if challenge == "cert":
challenge = "CAQ="
authFile = '/example_' + user + '.tokens'
headers = {}
def pair():
print("pairing...", flush=True)
response = req.get()
try:
code = response.json()['code']
print("Please go to ... and enter code " + code, flush=True)
except:
print(response.text, flush=True)
sys.exit(1)
print("please enter received code: ", flush=True)
code = input()
while True:
response = req.get()
try:
response.json()['token']
json.dump(response.json(), open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
print("pairing done", flush=True)
break
except:
pass
time.sleep(1)
def login():
print("logging in...", file=sys.stderr)
# Get token
response = req.get()
try:
response.json()['token']
json.dump(response.json(), open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
except:
output = {
"ErrorCode": 401,
"ErrorMessage": "auth failed"
}
print(json.dumps(output))
sys.exit(0)
print("logged in successfully", file=sys.stderr)
def refresh():
print("refreshing tokens...", file=sys.stderr)
try:
auth = json.load(open(os.path.abspath(os.path.dirname(__file__)) + authFile))
except:
return "error"
# Get token
response = req.get()
try:
response.json()['token']
except:
print(response.text, file=sys.stderr)
return "error"
auth.update(response.json())
json.dump(auth, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
print("tokens refreshed successfully", file=sys.stderr)
def do_action():
if action == "pair":
pair()
sys.exit()
elif action == "login":
login()
sys.exit()
try:
auth = json.load(open(os.path.abspath(os.path.dirname(__file__)) + authFile))
except:
return "error"
if action == "channels":
output = {}
output['Channels'] = []
response = req.get()
try:
for chan in response.json():
channel = {}
channel['Name'] = chan['Name']
channel['SortId'] = 0
channel['EpgId'] = ""
channel['Mode'] = "live"
channel['ScriptParams'] = 'id=' + str(chan['Id'])
channel['SessionManifest'] = True
channel['CdmType'] = "widevine"
channel['UseCdm'] = True
channel['Video'] = 'best'
channel['OnDemand'] = True
channel['SpeedUp'] = True
channel['LogoUrl'] = ""
channel['Info'] = "unsubscribed"
output['Channels'].append(channel)
print(json.dumps(output, indent=2))
except:
print(response.text, file=sys.stderr)
return "error"
elif action == "vod":
output = {}
output['Vod'] = []
response = req.get()
try:
for item in response.json():
vod = {}
vod['Name'] = item['Name']
vod['Category'] = ''
vod['Description'] = ''
vod['Mode'] = "vod"
vod['SessionManifest'] = True
vod['ScriptParams'] = 'id=' + str(item['Id'])
vod['CdmType'] = "widevine"
vod['UseCdm'] = True
vod['Video'] = 'best'
output['Vod'].append(vod)
print(json.dumps(output, indent=2))
except:
print(response.text, file=sys.stderr)
return "error"
elif action == "events":
output = {}
output['Events'] = []
response = req.get()
try:
for ev in response.json():
event = {}
event['Name'] = ev['Name']
event['Mode'] = "live"
event['SessionManifest'] = True
event['ScriptParams'] = 'id=' + str(ev['Id'])
event['CdmType'] = "widevine"
event['UseCdm'] = True
event['Video'] = 'best'
event['OnDemand'] = True
event['SpeedUp'] = True
event['Start'] = int(parser.parse(ev['start_date']).timestamp())
event['End'] = int(parser.parse(ev['end_date']).timestamp())
output['Events'].append(event)
print(json.dumps(output, indent=2))
except:
print(response.text, file=sys.stderr)
return "error"
elif action == "epg":
output = {}
output['Epg'] = {}
output['Epg']['Channels'] = []
channels = req.get()
try:
for chan in channels.json():
channel = {}
channel['Name'] = chan['Name']
channel['EpgId'] = ""
channel['Lang'] = 'en'
channel['Entries'] = []
items = req.get()
for item in items.json():
entry = {}
entry['Title'] = item['Title']
entry['Description'] = item['Description']
entry['Lang'] = item['Lang']
entry['Start'] = int(parser.parse(item['start_date']).timestamp())
entry['End'] = int(parser.parse(item['end_date']).timestamp())
channel['Entries'].append(entry)
output['Epg']['Channels'].append(channel)
print(json.dumps(output, indent=2))
except:
print(response.text, file=sys.stderr)
return "error"
elif action == "heartbeat":
output = {
"ManifestUrl": response.json()['mpdUrl'],
"Headers": {
"Manifest": {
'User-Agent': user_agent
},
"Media": {
'User-Agent': user_agent
}
},
"Heartbeat": {
"Url": '',
"Params": '',
"PeriodMs": 5*60*1000
},
}
print(json.dumps(output))
elif action == "manifest":
try:
output = {
"Cdn": [],
"ManifestUrl": response.json()['mpdUrl'],
"ManifestExpiration": 0, # Unix timestamp
"Headers": {
"Manifest": {
'User-Agent': user_agent
},
"Media": {
'User-Agent': user_agent
}
},
"Heartbeat": {
"Url": '',
"Params": '',
"PeriodMs": 5*60*1000
},
"License": {
"Url": '',
"Params": ''
}
}
for cdn in [ '' ]:
response = req.get("", headers=headers)
output['Cdn'].append({ "Name": cdn, "ManifestUrl": mpd_url })
output['ManifestUrl'] = mpd_url
print(json.dumps(output))
except:
print(response.text, file=sys.stderr)
return "error"
elif action == "cdm" and cdm == "internal":
# return license from challenge
response = req.post(licenseurl, headers=headers, data=base64.b64decode(challenge))
response_b64 = str(base64.b64encode(response.content), 'ascii')
if response_b64.startswith('CA'):
output = {
"CdmAnswer": response_b64
}
print(json.dumps(output))
else:
print(response.text, file=sys.stderr)
return "error"
elif action == "cdm" and cdm == "external":
output = {
"CdmAnswer": keys
}
print(json.dumps(output))
elif action == "pssh":
output = {
"ProcessedPssh": pssh
}
print(json.dumps(output))
elif action == "downloadmanifest":
response = req.get(url)
output = {
"ErrorCode": response.status_code,
"Payload": base64.b64encode(response.content).decode()
}
print(json.dumps(output))
elif action == "downloadmedia":
response = req.get(url)
output = {
"ErrorCode": response.status_code,
"Payload": base64.b64encode(response.content).decode()
}
print(json.dumps(output))
else:
print("invalid action: " + action)
if do_action() == "error":
if refresh() == "error":
login()
if do_action() == "error":
sys.exit(1)
+630
View File
@@ -0,0 +1,630 @@
from urllib3.util import connection, parse_url
from dns import message, rdatatype, resolver
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager
from requests_toolbelt.adapters.socket_options import SocketOptionsAdapter
from io import BytesIO
from http.cookies import SimpleCookie
from curl_cffi import requests as cffi_requests, CurlOpt, CurlSslVersion, CurlHttpVersion
import ipaddress
import gzip
import requests
import urllib
import http
import ssl
import socket
import socks
import json
import base64
import os
import sys
import cloudscraper
def parse_params(params, name, default=""):
ret = default
for p in params:
if p.split("=")[0] == name and len(p.split("=")) >= 2:
ret = p[len(p.split("=")[0])+1:]
return ret
def get_local_config():
return json.load(open(os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__)) + '/../providers/' + os.path.splitext(os.path.basename(sys.modules['__main__'].__file__))[0] + '.cfg'))
class dns:
def __init__(self, url):
self.url = url
self._orig_create_connection = connection.create_connection
connection.create_connection = self.patched_create_connection
def resolve_dns(self, host):
if len(self.url.split("/"))>=3 and self.url.split("/")[2] == host:
my_resolver = resolver.Resolver()
my_resolver.nameservers = ['1.1.1.1']
return str(my_resolver.query(host)[0])
try:
ipaddress.ip_address(host)
return host
except:
pass
if self.url.startswith('http'):
headers = {
'accept': 'application/dns-message',
'content-type': 'application/dns-message',
}
q = message.make_query(host, rdatatype.A)
response = requests.post(self.url, data=q.to_wire(), headers=headers)
try:
return message.from_wire(response.content).answer[0].to_rdataset()[0].to_text()
except:
return host
else:
if ':' in self.url:
url = self.url.split(':')[0]
port = self.url.split(':')[1]
else:
url = self.url
port = 53
my_resolver = resolver.Resolver()
my_resolver.nameservers = [url]
my_resolver.port = int(port)
result = resolver.query(host)
return str(my_resolver.query(host)[0])
def patched_create_connection(self, address, *args, **kwargs):
host, port = address
hostname = self.resolve_dns(host)
return self._orig_create_connection((hostname, port), *args, **kwargs)
class Custom_Adapter(HTTPAdapter):
def __init__(self, *args, **kwargs):
self.worker = kwargs.pop('worker', None)
super().__init__(*args, **kwargs)
def init_poolmanager(self, connections, maxsize, block=False, ssl_version=None, source_address=(), socket_options=[]):
if ssl_version:
ssl_context = ssl.create_default_context()
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
ssl_context.maximum_version = ssl.TLSVersion.TLSv1_2
ssl_context.options = ssl.PROTOCOL_TLS & ssl.OP_NO_TLSv1_3
self.poolmanager = PoolManager(num_pools=connections,
maxsize=maxsize,
block=block,
source_address=source_address,
socket_options=socket_options,
ssl_context=ssl_context)
else:
self.poolmanager = PoolManager(num_pools=connections,
maxsize=maxsize,
block=block,
source_address=source_address,
socket_options=socket_options)
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
parsed = parse_url(request.url)
if self.worker and parsed.scheme in ('http', 'https'):
request.headers['original-host'] = parsed.host
if parsed.query:
new_url = f"{parsed.scheme}://{self.worker}{parsed.path}?{parsed.query}"
else:
new_url = f"{parsed.scheme}://{self.worker}{parsed.path}"
request.url = new_url
return super().send(request, stream, timeout, verify, cert, proxies)
class session1:
def __init__(self, bind="", proxy="", worker="", force_tls1_2=False, cloud=False):
if cloud:
self.session = cloudscraper.create_scraper()
else:
self.session = requests.Session()
if proxy != "":
self.session.proxies = { "http": proxy, "https": proxy }
if cloud:
return
self.session.mount('http://', Custom_Adapter(worker=worker))
self.session.mount('https://', Custom_Adapter(worker=worker))
if bind != "":
if "." not in bind:
socket_options = [(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, bind.encode())]
source_address = ("", 0)
else:
socket_options = []
source_address = (bind, 0)
else:
source_address = ("", 0)
socket_options = []
if force_tls1_2:
ssl_version = ssl.PROTOCOL_TLSv1_2
else:
ssl_version = None
self.session.get_adapter('http://').init_poolmanager(
connections=requests.adapters.DEFAULT_POOLSIZE,
maxsize=requests.adapters.DEFAULT_POOLSIZE,
source_address=source_address,
socket_options=socket_options
)
self.session.get_adapter('https://').init_poolmanager(
connections=requests.adapters.DEFAULT_POOLSIZE,
maxsize=requests.adapters.DEFAULT_POOLSIZE,
source_address=source_address,
socket_options=socket_options,
ssl_version=ssl_version
)
def get_session(self):
return self.session
class Cookies:
def __init__(self, cookie_string):
self.cookies = self._parse_cookies(cookie_string)
def _parse_cookies(self, cookie_header):
cookies = {}
if cookie_header:
simple_cookie = SimpleCookie(cookie_header)
for key, morsel in simple_cookie.items():
cookies[key] = morsel.value
return cookies
def get_dict(self):
return self.cookies
def clear(self):
self.cookies.clear()
def update(self, new_cookies):
if isinstance(new_cookies, str):
# Parse the cookie string and update existing cookies
new_cookie_dict = self._parse_cookies(new_cookies)
self.cookies.update(new_cookie_dict)
elif isinstance(new_cookies, dict):
# If it's already a dictionary, just update the existing cookies
self.cookies.update(new_cookies)
else:
raise ValueError("new_cookies must be either a string or a dictionary.")
class HttpResponse:
def __init__(self, response):
self.status = response.status
self.headers = dict(response.getheaders())
self._body = response.read()
for key, value in self.headers.items():
if key.lower() == 'content-encoding' and value.lower() == 'gzip':
compressed_data = self._body
with gzip.GzipFile(fileobj=BytesIO(compressed_data), mode='rb') as f:
self._body = f.read()
self.cookies = Cookies(self.headers.get('Set-Cookie', ''))
@property
def content(self):
"""Returns the raw response content (bytes)."""
return self._body
@property
def text(self):
"""Returns the response content as a string (decoded)."""
try:
return self._body.decode("utf-8")
except:
return self._body
@property
def status_code(self):
"""Returns the response status code."""
return self.status
def json(self):
"""Returns the response content as a parsed JSON object."""
try:
return json.loads(self.text)
except json.JSONDecodeError:
raise ValueError("Response content is not valid JSON")
def resolve_dns(hostname, dns_server, ipv6):
r = resolver.Resolver()
r.nameservers = [dns_server]
reso = 'A'
if ipv6:
reso = 'AAAA'
answer = r.query(hostname, reso)
ip_address = answer[0].address
return ip_address
class session_http_client:
def __init__(self, bind="", proxy="", doh="", dns="", worker="", force_tls1_2=False, force_tls1_3=False, ciphers=None, max_redirects=5, ipv6=False):
self.bind = bind
self.proxy = proxy
self.proxy_username = None
self.proxy_password = None
self.doh = doh
self.dns = dns
self.worker = worker
self.force_tls1_2 = force_tls1_2
self.force_tls1_3 = force_tls1_3
self.max_redirects = max_redirects # Maximum number of redirects
self.cookies = {}
self.ciphers = ciphers
self.ipv6 = ipv6
if self.proxy:
proxy_parsed = urllib.parse.urlparse(proxy)
if proxy_parsed.username and proxy_parsed.password:
self.proxy_username = proxy_parsed.username
self.proxy_password = proxy_parsed.password
self.proxy_host = proxy_parsed.hostname
self.proxy_port = proxy_parsed.port or 80
def get_session(self):
return self
def _create_connection(self, host, port, ssl_context, ipv6):
if ipv6:
print("Using IPV6", file=sys.stderr)
af = socket.AF_INET6
else:
af = socket.AF_INET
# Create a socket object
if self.proxy.startswith('socks'):
sock = socks.socksocket(af, socket.SOCK_STREAM)
sock.set_proxy(socks.SOCKS5, self.proxy_host, self.proxy_port, username=self.proxy_username, password=self.proxy_password)
else:
sock = socket.socket(af, socket.SOCK_STREAM)
if ssl_context:
sock = ssl_context.wrap_socket(sock, server_hostname=host)
# If bind address is specified, create a socket with the bind address
if self.bind:
if "." in self.bind: # IP address bind
sock.bind((self.bind, 0)) # Bind to the specified IP address (port 0 to let OS choose)
else: # Interface bind (e.g., "eth0")
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, self.bind.encode())
# Connect to the host
if self.doh and self.doh.startswith('http'):
headers = {
'accept': 'application/dns-message',
'content-type': 'application/dns-message',
}
q = message.make_query(host, rdatatype.A)
response = requests.post(self.doh, data=q.to_wire(), headers=headers)
try:
host = message.from_wire(response.content).answer[0].to_rdataset()[0].to_text()
print("DOH: resolved ip to", host, file=sys.stderr)
except:
print("DOH failed")
sys.exit(1)
elif self.dns:
host = resolve_dns(host, self.dns, self.ipv6)
print("DNS: resolved ip to", host, file=sys.stderr)
sock.connect((host, port))
if ssl_context and self.proxy.startswith('socks'):
sock = ssl_context.wrap_socket(sock, server_hostname=host)
return sock
def request(self, method, url, params=None, body=None, headers={}, allow_redirects=True, verify=True, cookies=None):
if cookies:
self.cookies.update(cookies)
if params:
if '?' in url:
url += "&" + urllib.parse.urlencode(params)
else:
url += "?" + urllib.parse.urlencode(params)
parsed_url = urllib.parse.urlparse(url)
# Create a connection to the host or proxy if specified
context = None
if parsed_url.scheme == "https":
context = ssl.create_default_context()
if not verify:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if self.force_tls1_2:
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_3
if self.force_tls1_3:
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2
if self.ciphers:
#print("setting ciphers: " + self.ciphers, file=sys.stderr)
context.set_ciphers(self.ciphers)
connection = None
redirects = 0
current_url = url
if isinstance(body, dict):
converted = False
for key, value in headers.items():
if key.lower() == 'content-type':
if 'x-www-form-urlencoded' in value:
body = urllib.parse.urlencode(body, quote_via=urllib.parse.quote)
converted = True
elif 'application/json' in value:
body = json.dumps(body)
converted = True
if not converted:
headers['content-type'] = 'application/x-www-form-urlencoded'
body = urllib.parse.urlencode(body, quote_via=urllib.parse.quote)
while redirects <= self.max_redirects:
host = parsed_url.hostname
port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
path = parsed_url.path
if parsed_url.query:
path += '?' + parsed_url.query
if self.worker and parsed_url.scheme in ('http', 'https'):
headers['original-host'] = parsed.host
host = self.worker
if self.proxy and self.proxy.startswith('http'):
real_host = host
host = self.proxy_host
port = self.proxy_port
if parsed_url.scheme == "https":
connection = http.client.HTTPSConnection(host, port, context=context)
else:
connection = http.client.HTTPConnection(host, port)
if self.proxy and self.proxy.startswith('http'):
if self.proxy_username and self.proxy_password:
auth = base64.b64encode(f"{self.proxy_username}:{self.proxy_password}".encode()).decode()
connection.set_tunnel(real_host, headers={'Proxy-Authorization': 'Basic ' + auth})
else:
connection.set_tunnel(real_host)
if not self.proxy or self.proxy.startswith('socks'):
connection.sock = self._create_connection(host, port, context, self.ipv6)
if self.cookies and self.cookies != {}:
headers['cookie'] = "; ".join([f"{key}={value}" for key, value in self.cookies.items()])
connection.request(method, path, body=body, headers=headers)
#if connection.sock:
# print("selected ciphers: ", connection.sock.cipher(), file=sys.stderr)
response = connection.getresponse()
response = HttpResponse(response)
self.cookies.update(response.cookies.get_dict())
if not allow_redirects:
break
# Check for redirects (HTTP 3xx status codes)
if response.status_code in (301, 302, 303, 307, 308):
if 'location' in response.headers or 'Location' in response.headers:
if 'location' in response.headers:
location = response.headers['location']
else:
location = response.headers['Location']
current_url = urllib.parse.urljoin(current_url, location)
parsed_url = urllib.parse.urlparse(current_url) # Reparse the new URL
redirects += 1
print(f"redirect to {location}", file=sys.stderr)
continue # If a redirect, follow it
# If no redirect, break the loop and return the response
break
return response
def get(self, url, headers={}, params=None, allow_redirects=True, verify=True, cookies=None):
headers = { k.lower(): v for k, v in headers.items() }
return self.request("GET", url, params=params, headers=headers, allow_redirects=allow_redirects, verify=verify, cookies=cookies)
def post(self, url, params=None, data=None, json=None, headers={}, allow_redirects=True, verify=True, cookies=None):
headers = { k.lower(): v for k, v in headers.items() }
if json:
data = json
headers['content-type'] = 'application/json;charset=UTF-8'
return self.request("POST", url, params=params, body=data, headers=headers, allow_redirects=allow_redirects, verify=verify, cookies=cookies)
def put(self, url, params=None, data=None, json=None, headers={}, allow_redirects=True, verify=True, cookies=None):
headers = { k.lower(): v for k, v in headers.items() }
if json:
data = json
headers['content-type'] = 'application/json;charset=UTF-8'
return self.request("PUT", url, params=params, body=data, headers=headers, allow_redirects=allow_redirects, verify=verify, cookies=cookies)
def delete(self, url, params=None, headers={}, allow_redirects=True, verify=True, cookies=None):
headers = { k.lower(): v for k, v in headers.items() }
return self.request("DELETE", url, params=params, headers=headers, allow_redirects=allow_redirects, verify=verify, cookies=cookies)
class session_curl_cffi:
def __init__(self, bind="", proxy="", worker="", doh="", dns="", impersonate="", force_tls1_2=False, force_tls1_3=False, ciphers=None, ipv6=False, verbose=False):
if doh != "":
print("DOH not supported with curl_cffi")
sys.exit(1)
if dns != "":
print("DNS not supported with curl_cffi")
sys.exit(1)
if worker != "":
print("Worker not supported with curl_cffi")
sys.exit(1)
if force_tls1_2:
curl_options = { CurlOpt.SSLVERSION: CurlSslVersion.TLSv1_2 }
elif force_tls1_3:
curl_options = { CurlOpt.SSLVERSION: CurlSslVersion.TLSv1_3 }
else:
curl_options = {}
if verbose:
curl_options[CurlOpt.VERBOSE] = True
self._session = cffi_requests.Session(interface=bind, impersonate=impersonate, curl_options=curl_options)
if proxy != "":
self._session.proxies = {
"http": proxy,
"https": proxy
}
def get_session(self):
return self._session
mydns = dns
class session:
def __init__(self, bind="", proxy="", doh="", dns="", worker="", impersonate="chrome", force_tls1_2=False, force_tls1_3=False, ciphers=None, ipv6=False, alt=False, curl_cffi=False, cloud=False, verbose=False):
if curl_cffi:
print("Using curl_cffi", file=sys.stderr)
s = session_curl_cffi(bind=bind, proxy=proxy, doh=doh, dns=dns, impersonate=impersonate, worker=worker, force_tls1_2=force_tls1_2, force_tls1_3 = force_tls1_3, ciphers=ciphers, ipv6=ipv6, verbose=verbose)
self.session = s.get_session()
elif alt:
print("Using http.client", file=sys.stderr)
s = session_http_client(bind=bind, proxy=proxy, doh=doh, dns=dns, worker=worker, force_tls1_2=force_tls1_2, force_tls1_3 = force_tls1_3, ciphers=ciphers, ipv6=ipv6)
self.session = s.get_session()
else:
if cloud:
print("Using cloudscraper", file=sys.stderr)
if bind != "" or worker != "":
print("cannot use bind and/or worker with cloudscraper")
sys.exit(1)
else:
print("Using requests", file=sys.stderr)
if doh != "":
mydns(doh)
elif dns != "":
mydns(dns)
s = session1(bind=bind, proxy=proxy, worker=worker, force_tls1_2=force_tls1_2, cloud=cloud)
self.session = s.get_session()
def get_session(self):
return self.session
class api:
def __init__(self, url="", user="", password=""):
self.url = url
self.token = ""
if user != "" and password != "":
json_data = {
'Username': user,
'Password': password
}
response = requests.post(url + '/api/login', json=json_data, verify=False)
try:
self.token = response.json()['Token']
except:
print("O11 API login failed")
sys.exit(1)
info = self.get_info()
if "API error" in info:
print("Could not connect to O11 API: " + info)
sys.exit(1)
def get_info(self):
headers = {
'Authorization': self.token
}
json_data = {
'Id': 'local',
}
response = requests.post(self.url + '/api/server/getinfo', headers=headers, json=json_data, verify=False)
if response.status_code != 200:
return f"API error {response.status_code}"
else:
return response.text
def add_stream(self, type="event", provider_id="", name="", id="", autostart=True, start=0, end=0):
headers = {
'Authorization': self.token
}
json_data = {
'ProviderId': provider_id,
'StreamName': name,
'Stream': {
'Type': type,
'Name': name,
'Id': id,
'Autostart': autostart,
'Start': start,
'End': end
},
}
response = requests.post(self.url + '/api/stream/add', headers=headers, json=json_data, verify=False)
print(response.text, file=sys.stderr)
def edit_stream(self, provider_id="", stream_id="", manifest="", keys=[], start=0, end=0):
headers = {
'Authorization': self.token
}
json_data = {
'ProviderId': provider_id,
'StreamId': stream_id,
'Manifest': manifest,
'Keys': keys,
'Start': start,
'End': end
}
response = requests.post(self.url + '/api/stream/edit', headers=headers, json=json_data, verify=False)
print(response.text, file=sys.stderr)
def delete_stream(self, provider_id="", stream_id=""):
headers = {
'Authorization': self.token
}
json_data = {
'ProviderId': provider_id,
'StreamId': stream_id,
}
response = requests.post(self.url + '/api/stream/delete', headers=headers, json=json_data, verify=False)
print(response.text, file=sys.stderr)
def start_stop_stream(self, action="", provider_id="", stream_id=""):
headers = {
'Authorization': self.token
}
json_data = {
'ProviderId': provider_id,
'StreamId': stream_id
}
response = requests.post(self.url + '/api/stream/' + action, headers=headers, json=json_data, verify=False)
print(response.text, file=sys.stderr)
def add_job(self, name, cron, description="", script_name="", script_params="" , script_timeout=30):
headers = {
'Authorization': self.token
}
json_data = {
'Job': {
'Enabled': True,
'Name': name,
'Description': description,
'ScriptName': script_name,
'ScriptParams': script_params,
'ScriptTimeout': script_timeout,
'Cron': cron,
}
}
response = requests.post(self.url + '/api/job/add', headers=headers, json=json_data, verify=False)
print(response.text, file=sys.stderr)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,34 @@
<svg viewBox="0 0 395.52 247.2" height="247.2" width="395.52">
<g transform="matrix(1,0,0,1,39.55199999999999,65.51351621541207)">
<svg viewBox="0 0 316.416 116.17296756917585" height="116.17296756917585"
width="316.416">
<g>
<svg viewBox="0 0 316.416 116.17296756917585" height="116.17296756917585"
width="316.416">
<g>
<svg viewBox="0 0 316.416 116.17296756917585"
height="116.17296756917585" width="316.416">
<g>
<svg viewBox="0 0 316.416 116.17296756917585"
height="116.17296756917585" width="316.416">
<g id="textblocktransform">
<svg viewBox="0 0 316.416 116.17296756917585"
height="116.17296756917585" width="316.416"
id="textblock">
<g>
<svg viewBox="0 0 316.416 116.17296756917585"
height="116.17296756917585"
width="316.416">
<g transform="matrix(1,0,0,1,0,0)">
<svg width="316.416"
viewBox="3.22 -29.59 82.05 30.13"
height="116.17296756917585"
data-palette-color="#f9cc0b">
<path d="M14.99-23.1Q18.33-23.1 21.02-21.58 23.71-20.07 25.23-17.37 26.76-14.67 26.76-11.28L26.76-11.28Q26.76-7.89 25.23-5.19 23.71-2.49 21.02-0.98 18.33 0.54 14.99 0.54L14.99 0.54Q11.65 0.54 8.96-0.98 6.27-2.49 4.75-5.19 3.22-7.89 3.22-11.28L3.22-11.28Q3.22-14.67 4.75-17.37 6.27-20.07 8.96-21.58 11.65-23.1 14.99-23.1L14.99-23.1ZM14.99-19.48Q12.62-19.48 10.83-18.44 9.03-17.41 8.06-15.54 7.08-13.67 7.08-11.28L7.08-11.28Q7.08-8.89 8.06-7.02 9.03-5.15 10.83-4.11 12.62-3.08 14.99-3.08L14.99-3.08Q17.36-3.08 19.15-4.11 20.95-5.15 21.92-7.02 22.9-8.89 22.9-11.28L22.9-11.28Q22.9-13.67 21.92-15.54 20.95-17.41 19.15-18.44 17.36-19.48 14.99-19.48L14.99-19.48ZM53.76-3.32Q54.59-3.32 54.94-2.94 55.3-2.56 55.3-1.66L55.3-1.66Q55.3-0.76 54.94-0.38 54.59 0 53.76 0L53.76 0 37.3 0Q36.47 0 36.12-0.38 35.76-0.76 35.76-1.66L35.76-1.66Q35.76-2.56 36.12-2.94 36.47-3.32 37.3-3.32L37.3-3.32 43.87-3.32 43.87-24.27 37.79-20.48Q37.38-20.24 37.03-20.24L37.03-20.24Q36.25-20.24 35.69-21.24L35.69-21.24Q35.35-21.83 35.35-22.36L35.35-22.36Q35.35-23.19 36.11-23.63L36.11-23.63 45.31-29.3Q45.8-29.59 46.29-29.59L46.29-29.59Q46.82-29.59 47.18-29.24 47.53-28.88 47.53-28.25L47.53-28.25 47.53-3.32 53.76-3.32ZM83.74-3.32Q84.57-3.32 84.92-2.94 85.27-2.56 85.27-1.66L85.27-1.66Q85.27-0.76 84.92-0.38 84.57 0 83.74 0L83.74 0 67.28 0Q66.45 0 66.1-0.38 65.74-0.76 65.74-1.66L65.74-1.66Q65.74-2.56 66.1-2.94 66.45-3.32 67.28-3.32L67.28-3.32 73.85-3.32 73.85-24.27 67.77-20.48Q67.35-20.24 67.01-20.24L67.01-20.24Q66.23-20.24 65.67-21.24L65.67-21.24Q65.33-21.83 65.33-22.36L65.33-22.36Q65.33-23.19 66.08-23.63L66.08-23.63 75.29-29.3Q75.78-29.59 76.26-29.59L76.26-29.59Q76.8-29.59 77.16-29.24 77.51-28.88 77.51-28.25L77.51-28.25 77.51-3.32 83.74-3.32Z"
opacity="1"
transform="matrix(1,0,0,1,0,0)"
fill="#ffffff"
class="wordmark-text-0"
data-fill-palette-color="primary"
id="text-0"></path>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
width="2000" height="1247" viewBox="0 0 2000 1247">
<g transform="matrix(1,0,0,1,-1.2121212121212466,0.504858299595071)">
<svg viewBox="0 0 396 247" data-background-color="#000000" preserveAspectRatio="xMidYMid meet" height="1247"
width="2000" xmlns="http://www.w3.org/2000/svg">
<g id="tight-bounds" transform="matrix(1,0,0,1,0.2400000000000091,-0.09999999999999432)">
<svg viewBox="0 0 395.52 247.2" height="247.2" width="395.52">
<g>
<svg></svg>

After

Width:  |  Height:  |  Size: 604 B

+51
View File
@@ -0,0 +1,51 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="2000.000000pt" height="2000.000000pt" viewBox="0 0 2000.000000 2000.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,2000.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M18 16233 c-4 -7 -2 -12426 1 -12460 1 -10 2005 -13 9984 -13 l9982
0 0 6240 0 6240 -9982 0 c-5490 0 -9983 -3 -9985 -7z m19437 -6223 c1 -3132
-1 -5701 -3 -5707 -6 -19 -18898 -19 -18904 0 -3 10 -3 11378 1 11400 0 4
4255 6 9453 5 l9453 -3 0 -5695z"/>
<path d="M10315 12923 c-45 -11 -107 -47 -495 -286 -223 -138 -417 -257 -432
-266 -248 -145 -1012 -626 -1044 -658 -104 -104 -97 -280 17 -448 92 -136 210
-182 333 -130 31 13 253 148 446 270 19 13 73 46 120 75 47 30 114 72 150 95
36 23 97 61 135 84 60 36 242 150 340 212 17 10 33 19 36 19 3 0 5 -916 5
-2035 l1 -2035 -51 0 c-793 0 -1336 -8 -1371 -19 -22 -7 -58 -32 -80 -55 -55
-57 -74 -120 -74 -255 0 -129 25 -206 85 -258 71 -62 -8 -60 1839 -59 l1680 1
56 26 c107 50 141 129 136 313 -4 136 -23 194 -79 241 -74 62 -41 60 -762 62
-363 2 -661 5 -663 6 -1 2 -3 1106 -4 2453 -1 2687 4 2504 -61 2577 -47 53
-97 74 -177 75 -36 0 -75 -2 -86 -5z"/>
<path d="M16145 12921 c-16 -4 -41 -13 -55 -21 -60 -31 -1786 -1091 -1807
-1109 -7 -6 -16 -11 -21 -11 -21 0 -114 -95 -132 -136 -40 -88 -30 -196 30
-316 67 -133 161 -209 262 -214 61 -2 131 27 252 105 53 33 99 61 101 61 3 0
11 5 18 11 15 12 434 275 462 289 11 5 128 77 259 160 132 83 242 147 245 141
7 -11 6 -4049 -1 -4056 -3 -3 -315 -6 -694 -8 -677 -2 -690 -2 -734 -23 -110
-52 -146 -133 -143 -325 3 -132 45 -224 121 -263 15 -8 52 -19 82 -24 76 -14
3304 -14 3385 -1 69 12 125 40 153 78 68 92 76 329 15 447 -27 53 -85 90 -158
103 -36 6 -263 10 -575 10 -283 0 -564 1 -625 1 l-110 0 0 2458 c0 2354 -1
2459 -18 2505 -36 93 -118 147 -222 146 -33 -1 -73 -4 -90 -8z"/>
<path d="M4060 11659 c-36 -4 -87 -11 -115 -14 -84 -10 -275 -52 -375 -83
-388 -121 -734 -343 -990 -635 -45 -51 -84 -96 -88 -102 -72 -97 -98 -134
-145 -213 -62 -104 -123 -226 -167 -335 -31 -79 -96 -288 -105 -340 -4 -17
-10 -50 -15 -72 -4 -22 -11 -62 -15 -90 -3 -27 -8 -57 -10 -65 -8 -32 -18
-201 -18 -325 -2 -374 60 -692 194 -998 39 -89 105 -216 138 -267 9 -14 21
-32 26 -41 154 -246 368 -463 626 -634 96 -64 358 -203 393 -209 6 -1 43 -14
81 -29 107 -40 270 -83 395 -103 14 -2 39 -6 55 -9 17 -2 53 -7 80 -10 28 -3
66 -8 85 -10 41 -5 380 -5 426 0 99 10 152 16 174 20 14 3 39 7 55 10 374 61
754 233 1047 473 141 116 249 229 355 372 61 84 84 119 130 195 47 78 158 318
187 406 55 162 76 249 101 414 41 262 38 648 -5 845 -5 25 -12 61 -15 80 -33
213 -156 521 -303 758 -231 372 -599 679 -1016 846 -214 87 -347 120 -636 162
-68 9 -456 12 -530 3z m425 -703 c78 -8 97 -11 174 -25 82 -16 233 -60 245
-72 6 -5 16 -9 24 -9 24 0 192 -88 282 -148 105 -70 279 -242 347 -342 194
-290 287 -610 287 -995 0 -92 -10 -246 -18 -291 -4 -20 -19 -105 -21 -114 -12
-77 -77 -265 -127 -370 -201 -420 -553 -690 -1021 -785 -111 -22 -192 -29
-342 -29 -144 -1 -257 8 -328 24 -26 5 -64 13 -85 16 -73 11 -246 75 -350 129
-191 98 -345 229 -471 400 -72 98 -77 107 -131 210 -161 305 -222 742 -159
1130 75 462 307 829 664 1052 193 121 448 203 682 219 48 4 89 7 90 8 5 4 203
-2 258 -8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="#0c0c0c">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<script type="module" crossorigin src="/static/index-BX-yLeHZ.js"></script>
<link rel="modulepreload" crossorigin href="/static/vendor-modules-BcUHiSD3.js">
<link rel="stylesheet" crossorigin href="/static/index-Bj6KzdKF.css">
</head>
<body style="background-color: #070707">
<div id="app"></div>
</body>
</html>