released :P
@@ -0,0 +1,5 @@
|
|||||||
|
.vscode/
|
||||||
|
launcher.go
|
||||||
|
/build/
|
||||||
|
o11pro
|
||||||
|
workspace-69e17692-9d10-431b-adee-503d25eb1a71.tar
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 578 B |
|
After Width: | Height: | Size: 833 B |
|
After Width: | Height: | Size: 8.0 KiB |
@@ -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 |
@@ -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 |
@@ -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>
|
||||||