feat: support reverse proxy with subpath

This commit is contained in:
acgnhik 2023-03-19 15:08:43 +08:00
parent 037e37a760
commit 8f6acea210
32 changed files with 458 additions and 363 deletions

View File

@ -59,6 +59,7 @@ install_requires =
reactivex >= 4.0.0, < 5.0.0 reactivex >= 4.0.0, < 5.0.0
bitarray >= 2.2.5, < 3.0.0 bitarray >= 2.2.5, < 3.0.0
brotli >= 1.0.9, < 2.0.0 brotli >= 1.0.9, < 2.0.0
brotli-asgi >= 1.3.0, < 1.4.0
uvicorn[standard] >= 0.20.0, < 0.21.0 uvicorn[standard] >= 0.20.0, < 0.21.0
[options.extras_require] [options.extras_require]

View File

@ -1,104 +1,110 @@
import os import logging
import logging import os
from copy import deepcopy from copy import deepcopy
from typing import Optional from typing import Optional
import uvicorn import typer
from uvicorn.config import LOGGING_CONFIG import uvicorn
import typer from uvicorn.config import LOGGING_CONFIG
from .. import __prog__, __version__ from .. import __prog__, __version__
from ..logging import TqdmOutputStream from ..logging import TqdmOutputStream
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
cli = typer.Typer()
cli = typer.Typer()
def version_callback(value: bool) -> None:
def version_callback(value: bool) -> None: if value:
if value: typer.echo(f'Bilibili live streaming recorder {__version__}')
typer.echo(f'Bilibili live streaming recorder {__version__}') raise typer.Exit()
raise typer.Exit()
@cli.command()
@cli.command() def cli_main(
def cli_main( version: Optional[bool] = typer.Option(
version: Optional[bool] = typer.Option( None,
None, '--version',
'--version', callback=version_callback,
callback=version_callback, is_eager=True,
is_eager=True, help=f"show {__prog__}'s version and exit",
help=f"show {__prog__}'s version and exit", ),
), config: str = typer.Option(
config: str = typer.Option( None, '--config', '-c', help='path of settings.toml file'
None, ),
'--config', out_dir: Optional[str] = typer.Option(
'-c', None,
help='path of settings.toml file', '--out-dir',
), '-o',
out_dir: Optional[str] = typer.Option( help='path of directory to store record files (overwrite setting)',
None, ),
'--out-dir', log_dir: Optional[str] = typer.Option(
'-o', None,
help='path of directory to store record files (overwrite setting)' '--log-dir',
), help='path of directory to store log files (overwrite setting)',
log_dir: Optional[str] = typer.Option( ),
None, host: str = typer.Option('localhost', help='webapp host bind'),
'--log-dir', port: int = typer.Option(2233, help='webapp port bind'),
help='path of directory to store log files (overwrite setting)' open: bool = typer.Option(False, help='open webapp in default browser'),
), root_path: str = typer.Option('', help='ASGI root path'),
host: str = typer.Option('localhost', help='webapp host bind'), key_file: Optional[str] = typer.Option(None, help='SSL key file'),
port: int = typer.Option(2233, help='webapp port bind'), cert_file: Optional[str] = typer.Option(None, help='SSL certificate file'),
open: bool = typer.Option(False, help='open webapp in default browser'), api_key: Optional[str] = typer.Option(None, help='web api key'),
key_file: Optional[str] = typer.Option(None, help='SSL key file'), ) -> None:
cert_file: Optional[str] = typer.Option(None, help='SSL certificate file'), """Bilibili live streaming recorder"""
api_key: Optional[str] = typer.Option(None, help='web api key'), if config is not None:
) -> None: os.environ['config'] = config
"""Bilibili live streaming recorder""" if api_key is not None:
if config is not None: os.environ['api_key'] = api_key
os.environ['config'] = config if out_dir is not None:
if api_key is not None: os.environ['out_dir'] = out_dir
os.environ['api_key'] = api_key if log_dir is not None:
if out_dir is not None: os.environ['log_dir'] = log_dir
os.environ['out_dir'] = out_dir
if log_dir is not None: if root_path:
os.environ['log_dir'] = log_dir if not root_path.startswith('/'):
root_path = '/' + root_path
if open: if not root_path.endswith('/'):
typer.launch(f'http://localhost:{port}') root_path += '/'
logging_config = deepcopy(LOGGING_CONFIG) if open:
logging_config['handlers']['default']['stream'] = TqdmOutputStream typer.launch(f'http://localhost:{port}')
logging_config['handlers']['access']['stream'] = TqdmOutputStream
logging_config = deepcopy(LOGGING_CONFIG)
uvicorn.run( logging_config['handlers']['default']['stream'] = TqdmOutputStream
'blrec.web:app', logging_config['handlers']['access']['stream'] = TqdmOutputStream
host=host,
port=port, uvicorn.run(
ssl_keyfile=key_file, 'blrec.web:app',
ssl_certfile=cert_file, host=host,
log_config=logging_config, port=port,
log_level='info', root_path=root_path,
access_log=False, proxy_headers=True,
) forwarded_allow_ips='*',
ssl_keyfile=key_file,
ssl_certfile=cert_file,
def main() -> int: log_config=logging_config,
try: log_level='info',
cli() access_log=False,
except KeyboardInterrupt: )
return 1
except SystemExit:
return 1 def main() -> int:
except BaseException as e: try:
logger.exception(e) cli()
return 2 except KeyboardInterrupt:
else: return 1
return 0 except SystemExit:
finally: return 1
logger.info('Exit') except BaseException as e:
logger.exception(e)
return 2
if __name__ == '__main__': else:
main() return 0
finally:
logger.info('Exit')
if __name__ == '__main__':
main()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,6 @@
<body> <body>
<app-root></app-root> <app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript> <noscript>Please enable JavaScript to continue using this application.</noscript>
<script src="runtime.a904720a2e39ffc3.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.dbd09d2079405adc.js" type="module"></script> <script src="runtime.1db847619b50d96c.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.545d90ce6b1d69be.js" type="module"></script>
</body></html> </body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"configVersion": 1, "configVersion": 1,
"timestamp": 1669447100875, "timestamp": 1677235187781,
"index": "/index.html", "index": "/index.html",
"assetGroups": [ "assetGroups": [
{ {
@ -11,18 +11,18 @@
"ignoreVary": true "ignoreVary": true
}, },
"urls": [ "urls": [
"/103.5b5d2a6e5a8a7479.js", "/103.bd702fba8239ab1e.js",
"/146.5a8902910bda9e87.js", "/183.fd2e6a1f63815dbf.js",
"/183.ee55fc76717674c3.js", "/237.44684bee585167eb.js",
"/45.c90c3cea2bf1a66e.js", "/45.c90c3cea2bf1a66e.js",
"/548.4789e17f7acce023.js", "/548.a9f0c3e1529d6713.js",
"/91.07ca0767ccc21566.js", "/91.5d33ec6f665fb52d.js",
"/common.858f777e9296e6f2.js", "/common.858f777e9296e6f2.js",
"/index.html", "/index.html",
"/main.dbd09d2079405adc.js", "/main.545d90ce6b1d69be.js",
"/manifest.webmanifest", "/manifest.webmanifest",
"/polyfills.4b08448aee19bb22.js", "/polyfills.4b08448aee19bb22.js",
"/runtime.a904720a2e39ffc3.js", "/runtime.1db847619b50d96c.js",
"/styles.2e152d608221c2ee.css" "/styles.2e152d608221c2ee.css"
], ],
"patterns": [] "patterns": []
@ -1634,12 +1634,12 @@
], ],
"dataGroups": [], "dataGroups": [],
"hashTable": { "hashTable": {
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a", "/103.bd702fba8239ab1e.js": "34fa616477a9a519bf0a8cba3013267c8e8c6410",
"/146.5a8902910bda9e87.js": "d9c33c7073662699f00f46f3a384ae5b749fdef9", "/183.fd2e6a1f63815dbf.js": "01e46704e96688183d68029b1343c246f9872398",
"/183.ee55fc76717674c3.js": "2628c996ec80a6c6703d542d34ac95194283bcf8", "/237.44684bee585167eb.js": "c30482253a95da9216e9f4bb87abbd9197fa2c29",
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764", "/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
"/548.4789e17f7acce023.js": "3b8aaf921bd400fb32cc15135dd4de09deb2c824", "/548.a9f0c3e1529d6713.js": "0ac4eecad93f3b8c93e8a3dc92e9f98b61df24d7",
"/91.07ca0767ccc21566.js": "4105beda647cedabf52678640e8fe450671e2e45", "/91.5d33ec6f665fb52d.js": "f6df1e37381abdc03ad85398484e343636b3cef0",
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1", "/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1", "/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
@ -3234,11 +3234,11 @@
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01", "/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068", "/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd", "/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
"/index.html": "9ba0d26d371e607af065904e06d098a0698f75a3", "/index.html": "f4610b8180c8e25908c57d40eb5c1a5d3ffa2771",
"/main.dbd09d2079405adc.js": "2f7284b616ed9fc433b612c9dca53dc06a0f3aa1", "/main.545d90ce6b1d69be.js": "abcd561449fbb227221a982f772e635d407b4400",
"/manifest.webmanifest": "0c4534b4c868d756691b1b4372cecb2efce47c6d", "/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d", "/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
"/runtime.a904720a2e39ffc3.js": "d9eb86363e3840a15e5659af6f04f29e19df9233", "/runtime.1db847619b50d96c.js": "20d27b3ff34cae73b5645baa40cbaed4bb4e57f8",
"/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f" "/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
}, },
"navigationUrls": [ "navigationUrls": [

View File

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,f,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,f,o]=e[n],c=!0,l=0;l<t.length;l++)(!1&o||a>=o)&&Object.keys(r.O).every(p=>r.O[p](t[l]))?t.splice(l--,1):(c=!1,o<a&&(a=o));if(c){e.splice(n--,1);var d=f();void 0!==d&&(i=d)}}return i}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,f,o]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",91:"5d33ec6f665fb52d",103:"bd702fba8239ab1e",183:"fd2e6a1f63815dbf",237:"44684bee585167eb",548:"a9f0c3e1529d6713",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,f,o,n)=>{if(e[t])e[t].push(f);else{var a,c;if(void 0!==o)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+o){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+o),a.src=r.tu(t)),e[t]=[f];var s=(g,p)=>{a.onerror=a.onload=null,clearTimeout(b);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(p)),g)return g(p)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(f,o)=>{var n=r.o(e,f)?e[f]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=f){var a=new Promise((u,s)=>n=e[f]=[u,s]);o.push(n[2]=a);var c=r.p+r.u(f),l=new Error;r.l(c,u=>{if(r.o(e,f)&&(0!==(n=e[f])&&(e[f]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),b=u&&u.target&&u.target.src;l.message="Loading chunk "+f+" failed.\n("+s+": "+b+")",l.name="ChunkLoadError",l.type=s,l.request=b,n[1](l)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var i=(f,o)=>{var l,d,[n,a,c]=o,u=0;if(n.some(b=>0!==e[b])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(f&&f(o);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();

View File

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},m={};function r(e){var f=m[e];if(void 0!==f)return f.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(f,t,i,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,o]=e[n],c=!0,l=0;l<t.length;l++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(c=!1,o<a&&(a=o));if(c){e.splice(n--,1);var d=i();void 0!==d&&(f=d)}}return f}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,i,o]},r.n=e=>{var f=e&&e.__esModule?()=>e.default:()=>e;return r.d(f,{a:f}),f},r.d=(e,f)=>{for(var t in f)r.o(f,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:f[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((f,t)=>(r.f[t](e,f),f),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",91:"07ca0767ccc21566",103:"5b5d2a6e5a8a7479",146:"5a8902910bda9e87",183:"ee55fc76717674c3",548:"4789e17f7acce023",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),(()=>{var e={},f="blrec:";r.l=(t,i,o,n)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==o)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==f+o){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",f+o),a.src=r.tu(t)),e[t]=[i];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=f=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(f))})(),r.p="",(()=>{var e={666:0};r.f.j=(i,o)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=i){var a=new Promise((u,s)=>n=e[i]=[u,s]);o.push(n[2]=a);var c=r.p+r.u(i),l=new Error;r.l(c,u=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+i+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,l.request=p,n[1](l)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var f=(i,o)=>{var l,d,[n,a,c]=o,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(i&&i(o);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

View File

@ -1,163 +1,149 @@
import os import logging
import logging import os
from typing import Optional, Tuple from typing import Optional, Tuple
from fastapi import FastAPI, status, Request, Depends from brotli_asgi import BrotliMiddleware
from fastapi.responses import JSONResponse from fastapi import Depends, FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.responses import JSONResponse
from starlette.responses import Response from fastapi.staticfiles import StaticFiles
from pydantic import ValidationError from pkg_resources import resource_filename
from pkg_resources import resource_filename from pydantic import ValidationError
from starlette.responses import Response
from . import security
from .routers import ( from blrec.exception import ExistsError, ForbiddenError, NotFoundError
tasks, settings, application, validation, websockets, update from blrec.path.helpers import create_file, file_exists
) from blrec.setting import EnvSettings, Settings
from .schemas import ResponseMessage from blrec.web.middlewares.base_herf import BaseHrefMiddleware
from ..setting import EnvSettings, Settings from blrec.web.middlewares.route_redirect import RouteRedirectMiddleware
from ..application import Application
from ..exception import NotFoundError, ExistsError, ForbiddenError from ..application import Application
from ..path.helpers import file_exists, create_file from . import security
from .routers import application, settings, tasks, update, validation, websockets
from .schemas import ResponseMessage
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
_env_settings = EnvSettings()
_path = os.path.abspath(os.path.expanduser(_env_settings.settings_file)) _env_settings = EnvSettings()
if not file_exists(_path): _path = os.path.abspath(os.path.expanduser(_env_settings.settings_file))
create_file(_path) if not file_exists(_path):
_env_settings.settings_file = _path create_file(_path)
_env_settings.settings_file = _path
_settings = Settings.load(_env_settings.settings_file)
_settings.update_from_env_settings(_env_settings) _settings = Settings.load(_env_settings.settings_file)
_settings.update_from_env_settings(_env_settings)
app = Application(_settings)
app = Application(_settings)
if _env_settings.api_key is None:
_dependencies = None if _env_settings.api_key is None:
else: _dependencies = None
security.api_key = _env_settings.api_key else:
_dependencies = [Depends(security.authenticate)] security.api_key = _env_settings.api_key
_dependencies = [Depends(security.authenticate)]
api = FastAPI(
title='Bilibili live streaming recorder web API', api = FastAPI(
description='Web API to communicate with the backend application', title='Bilibili live streaming recorder web API',
version='v1', description='Web API to communicate with the backend application',
dependencies=_dependencies, version='v1',
) dependencies=_dependencies,
)
api.add_middleware(
CORSMiddleware, api.add_middleware(BaseHrefMiddleware)
allow_origins=[ api.add_middleware(BrotliMiddleware)
'http://localhost:4200', # angular development api.add_middleware(
], CORSMiddleware,
allow_credentials=True, allow_origins=['http://localhost:4200'], # angular development
allow_methods=['*'], allow_credentials=True,
allow_headers=['*'], allow_methods=['*'],
) allow_headers=['*'],
)
api.add_middleware(RouteRedirectMiddleware)
@api.exception_handler(NotFoundError)
async def not_found_error_handler(
request: Request, exc: NotFoundError @api.exception_handler(NotFoundError)
) -> JSONResponse: async def not_found_error_handler(request: Request, exc: NotFoundError) -> JSONResponse:
return JSONResponse( return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
content=dict(ResponseMessage( content=dict(ResponseMessage(code=status.HTTP_404_NOT_FOUND, message=str(exc))),
code=status.HTTP_404_NOT_FOUND, )
message=str(exc),
)),
) @api.exception_handler(ForbiddenError)
async def forbidden_error_handler(
request: Request, exc: ForbiddenError
@api.exception_handler(ForbiddenError) ) -> JSONResponse:
async def forbidden_error_handler( return JSONResponse(
request: Request, exc: ForbiddenError status_code=status.HTTP_403_FORBIDDEN,
) -> JSONResponse: content=dict(ResponseMessage(code=status.HTTP_403_FORBIDDEN, message=str(exc))),
return JSONResponse( )
status_code=status.HTTP_403_FORBIDDEN,
content=dict(ResponseMessage(
code=status.HTTP_403_FORBIDDEN, @api.exception_handler(ExistsError)
message=str(exc), async def exists_error_handler(request: Request, exc: ExistsError) -> JSONResponse:
)), return JSONResponse(
) status_code=status.HTTP_409_CONFLICT,
content=dict(ResponseMessage(code=status.HTTP_409_CONFLICT, message=str(exc))),
)
@api.exception_handler(ExistsError)
async def exists_error_handler(
request: Request, exc: ExistsError @api.exception_handler(ValidationError)
) -> JSONResponse: async def validation_error_handler(
return JSONResponse( request: Request, exc: ValidationError
status_code=status.HTTP_409_CONFLICT, ) -> JSONResponse:
content=dict(ResponseMessage( return JSONResponse(
code=status.HTTP_409_CONFLICT, status_code=status.HTTP_406_NOT_ACCEPTABLE,
message=str(exc), content=dict(
)), ResponseMessage(code=status.HTTP_406_NOT_ACCEPTABLE, message=str(exc))
) ),
)
@api.exception_handler(ValidationError)
async def validation_error_handler( @api.on_event('startup')
request: Request, exc: ValidationError async def on_startup() -> None:
) -> JSONResponse: await app.launch()
return JSONResponse(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
content=dict(ResponseMessage( @api.on_event('shutdown')
code=status.HTTP_406_NOT_ACCEPTABLE, async def on_shuntdown() -> None:
message=str(exc), _settings.dump()
)), await app.exit()
)
tasks.app = app
@api.on_event('startup') settings.app = app
async def on_startup() -> None: application.app = app
await app.launch() validation.app = app
websockets.app = app
update.app = app
@api.on_event('shutdown') api.include_router(tasks.router)
async def on_shuntdown() -> None: api.include_router(settings.router)
_settings.dump() api.include_router(application.router)
await app.exit() api.include_router(validation.router)
api.include_router(websockets.router)
api.include_router(update.router)
tasks.app = app
settings.app = app
application.app = app class WebAppFiles(StaticFiles):
validation.app = app def lookup_path(self, path: str) -> Tuple[str, Optional[os.stat_result]]:
websockets.app = app if path == '404.html':
update.app = app path = 'index.html'
api.include_router(tasks.router) return super().lookup_path(path)
api.include_router(settings.router)
api.include_router(application.router) def file_response(self, full_path: str, *args, **kwargs) -> Response: # type: ignore # noqa
api.include_router(validation.router) # ignore MIME types from Windows registry
api.include_router(websockets.router) # workaround for https://github.com/acgnhiki/blrec/issues/12
api.include_router(update.router) response = super().file_response(full_path, *args, **kwargs)
if full_path.endswith('.js'):
js_media_type = 'application/javascript'
class WebAppFiles(StaticFiles): if response.media_type != js_media_type:
def lookup_path( response.media_type = js_media_type
self, path: str headers = response.headers
) -> Tuple[str, Optional[os.stat_result]]: headers['content-type'] = js_media_type
if path == '404.html': response.raw_headers = headers.raw
path = 'index.html' del response._headers
return super().lookup_path(path) return response
def file_response(self, full_path: str, *args, **kwargs) -> Response: # type: ignore # noqa
# ignore MIME types from Windows registry directory = resource_filename(__name__, '../data/webapp')
# workaround for https://github.com/acgnhiki/blrec/issues/12 api.mount('/', WebAppFiles(directory=directory, html=True), name='webapp')
response = super().file_response(full_path, *args, **kwargs)
if full_path.endswith('.js'):
js_media_type = 'application/javascript'
if response.media_type != js_media_type:
response.media_type = js_media_type
headers = response.headers
headers['content-type'] = js_media_type
response.raw_headers = headers.raw
del response._headers
return response
directory = resource_filename(__name__, '../data/webapp')
api.mount('/', WebAppFiles(directory=directory, html=True), name='webapp')

View File

@ -0,0 +1,51 @@
from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
class BaseHrefMiddleware:
def __init__(self, app: ASGIApp) -> None:
self._app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if (
scope['type'] != 'http'
or scope.get('method', '') != 'GET'
or scope.get('path', '') != '/'
or scope.get('root_path', '') == ''
):
await self._app(scope, receive, send)
return
initial_message: Message = {}
async def _send(msg: Message) -> None:
nonlocal initial_message
msg_type = msg['type']
if msg_type == 'http.response.start':
headers = Headers(raw=msg['headers'])
# the body must not been compressed
assert 'content-encoding' not in headers
initial_message = msg
elif msg_type == 'http.response.body':
body = msg.get('body', b'')
# the body should not be empty
assert body != b''
more_body = msg.get('more_body', False)
# the body should not been read in streaming
assert more_body is False
# replace base href
root_path = scope.get('root_path', '') or '/'
body = body.replace(
b'<base href="/">', f'<base href="{root_path}">'.encode(), 1
)
msg['body'] = body
# update content length
headers = MutableHeaders(raw=initial_message['headers'])
headers['Content-Length'] = str(len(body))
# send messages
await send(initial_message)
await send(msg)
# clean up
del initial_message
await self._app(scope, receive, _send)

View File

@ -0,0 +1,25 @@
import http
import re
from starlette.responses import RedirectResponse
from starlette.types import ASGIApp, Receive, Scope, Send
class RouteRedirectMiddleware:
def __init__(self, app: ASGIApp) -> None:
self._app = app
self._pattern = re.compile(r'^/(tasks|settings|about)($|/.*$)')
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope['type'] != 'http':
await self._app(scope, receive, send)
return
path = scope.get('path', '')
if self._pattern.match(path):
status_code = http.HTTPStatus.MOVED_PERMANENTLY.value
response = RedirectResponse('/', status_code=status_code)
await response(scope, receive, send)
return
await self._app(scope, receive, send)

View File

@ -3,34 +3,32 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment'; import { UrlService } from './url.service';
import { AppInfo, appStatus } from '../models/app.models'; import { AppInfo, appStatus } from '../models/app.models';
const apiUrl = environment.apiUrl;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AppService { export class AppService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private url: UrlService) {}
getAppInfo(): Observable<AppInfo> { getAppInfo(): Observable<AppInfo> {
const url = apiUrl + `/api/v1/app/info`; const url = this.url.makeApiUrl(`/api/v1/app/info`);
return this.http.get<AppInfo>(url); return this.http.get<AppInfo>(url);
} }
getAppStatus(): Observable<appStatus> { getAppStatus(): Observable<appStatus> {
const url = apiUrl + `/api/v1/app/status`; const url = this.url.makeApiUrl(`/api/v1/app/status`);
return this.http.get<appStatus>(url); return this.http.get<appStatus>(url);
} }
restartApp(): Observable<undefined> { restartApp(): Observable<undefined> {
const url = apiUrl + `/api/v1/app/restart`; const url = this.url.makeApiUrl(`/api/v1/app/restart`);
return this.http.post<undefined>(url, null); return this.http.post<undefined>(url, null);
} }
exitApp(): Observable<undefined> { exitApp(): Observable<undefined> {
const url = apiUrl + `/api/v1/app/exit`; const url = this.url.makeApiUrl(`/api/v1/app/exit`);
return this.http.post<undefined>(url, null); return this.http.post<undefined>(url, null);
} }
} }

View File

@ -2,10 +2,8 @@ import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { environment } from 'src/environments/environment';
import { Event } from '../models/event.model'; import { Event } from '../models/event.model';
import { UrlService } from './url.service';
const webSocketUrl = environment.webSocketUrl;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -13,11 +11,11 @@ const webSocketUrl = environment.webSocketUrl;
export class EventService { export class EventService {
private eventSubject?: WebSocketSubject<Event>; private eventSubject?: WebSocketSubject<Event>;
constructor() {} constructor(private url: UrlService) {}
get events() { get events() {
if (!this.eventSubject) { if (!this.eventSubject) {
this.eventSubject = webSocket(webSocketUrl + '/ws/v1/events'); this.eventSubject = webSocket(this.url.makeWebSocketUrl('/ws/v1/events'));
} }
return this.eventSubject; return this.eventSubject;
} }

View File

@ -2,22 +2,22 @@ import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { environment } from 'src/environments/environment';
import { Event } from '../models/event.model'; import { Event } from '../models/event.model';
import { UrlService } from './url.service';
const webSocketUrl = environment.webSocketUrl;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ExceptionService { export class ExceptionService {
private exceptionSubject?: WebSocketSubject<Event>; private exceptionSubject?: WebSocketSubject<Event>;
constructor() {} constructor(private url: UrlService) {}
get exceptions() { get exceptions() {
if (!this.exceptionSubject) { if (!this.exceptionSubject) {
this.exceptionSubject = webSocket(webSocketUrl + '/ws/v1/exceptions'); this.exceptionSubject = webSocket(
this.url.makeWebSocketUrl('/ws/v1/exceptions')
);
} }
return this.exceptionSubject; return this.exceptionSubject;
} }

View File

@ -3,18 +3,16 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment'; import { UrlService } from './url.service';
const apiUrl = environment.apiUrl;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class UpdateService { export class UpdateService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private url: UrlService) {}
getLatestVerisonString(): Observable<string> { getLatestVerisonString(): Observable<string> {
const url = apiUrl + `/api/v1/update/version/latest`; const url = this.url.makeApiUrl(`/api/v1/update/version/latest`);
return this.http.get<string>(url); return this.http.get<string>(url);
} }
} }

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { UrlService } from './url.service';
describe('UrlService', () => {
let service: UrlService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UrlService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { environment } from 'src/environments/environment';
const API_BASE_URL = environment.apiBaseUrl;
const WEB_SOCKET_BASE_URL = environment.webSocketBaseUrl;
@Injectable({
providedIn: 'root',
})
export class UrlService {
constructor(private location: Location) {}
makeApiUrl(uri: string): string {
return API_BASE_URL + this.location.prepareExternalUrl(uri);
}
makeWebSocketUrl(uri: string): string {
return WEB_SOCKET_BASE_URL + this.location.prepareExternalUrl(uri);
}
}

View File

@ -3,19 +3,17 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { ResponseMessage } from 'src/app/shared/api.models'; import { ResponseMessage } from 'src/app/shared/api.models';
import { UrlService } from './url.service';
const apiUrl = environment.apiUrl;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class ValidationService { export class ValidationService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private url: UrlService) {}
validateDir(path: string): Observable<ResponseMessage> { validateDir(path: string): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/validation/dir`; const url = this.url.makeApiUrl(`/api/v1/validation/dir`);
return this.http.post<ResponseMessage>(url, { path }); return this.http.post<ResponseMessage>(url, { path });
} }
} }

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment'; import { UrlService } from 'src/app/core/services/url.service';
import { import {
Settings, Settings,
TaskOptions, TaskOptions,
@ -12,19 +12,17 @@ import {
SettingsOut, SettingsOut,
} from '../setting.model'; } from '../setting.model';
const apiUrl = environment.apiUrl;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class SettingService { export class SettingService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private url: UrlService) {}
getSettings( getSettings(
include: Array<keyof Settings> | null = null, include: Array<keyof Settings> | null = null,
exclude: Array<keyof Settings> | null = null exclude: Array<keyof Settings> | null = null
): Observable<Settings> { ): Observable<Settings> {
const url = apiUrl + `/api/v1/settings`; const url = this.url.makeApiUrl(`/api/v1/settings`);
return this.http.get<Settings>(url, { return this.http.get<Settings>(url, {
params: { params: {
include: include ?? [], include: include ?? [],
@ -45,12 +43,12 @@ export class SettingService {
* @returns settings of the application * @returns settings of the application
*/ */
changeSettings(settings: SettingsIn): Observable<SettingsOut> { changeSettings(settings: SettingsIn): Observable<SettingsOut> {
const url = apiUrl + `/api/v1/settings`; const url = this.url.makeApiUrl(`/api/v1/settings`);
return this.http.patch<SettingsOut>(url, settings); return this.http.patch<SettingsOut>(url, settings);
} }
getTaskOptions(roomId: number): Observable<TaskOptions> { getTaskOptions(roomId: number): Observable<TaskOptions> {
const url = apiUrl + `/api/v1/settings/tasks/${roomId}`; const url = this.url.makeApiUrl(`/api/v1/settings/tasks/${roomId}`);
return this.http.get<TaskOptions>(url); return this.http.get<TaskOptions>(url);
} }
@ -70,7 +68,7 @@ export class SettingService {
roomId: number, roomId: number,
options: TaskOptionsIn options: TaskOptionsIn
): Observable<TaskOptions> { ): Observable<TaskOptions> {
const url = apiUrl + `/api/v1/settings/tasks/${roomId}`; const url = this.url.makeApiUrl(`/api/v1/settings/tasks/${roomId}`);
return this.http.patch<TaskOptions>(url, options); return this.http.patch<TaskOptions>(url, options);
} }
} }

View File

@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import { UrlService } from 'src/app/core/services/url.service';
import { ResponseMessage } from '../../../shared/api.models'; import { ResponseMessage } from '../../../shared/api.models';
import { import {
TaskData, TaskData,
@ -17,83 +17,81 @@ import {
DanmakuFileDetail, DanmakuFileDetail,
} from '../task.model'; } from '../task.model';
const apiUrl = environment.apiUrl;
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TaskService { export class TaskService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private url: UrlService) {}
getAllTaskData( getAllTaskData(
select: DataSelection = DataSelection.ALL select: DataSelection = DataSelection.ALL
): Observable<TaskData[]> { ): Observable<TaskData[]> {
const url = apiUrl + '/api/v1/tasks/data'; const url = this.url.makeApiUrl('/api/v1/tasks/data');
return this.http.get<TaskData[]>(url, { params: { select } }); return this.http.get<TaskData[]>(url, { params: { select } });
} }
getTaskData(roomId: number): Observable<TaskData> { getTaskData(roomId: number): Observable<TaskData> {
const url = apiUrl + `/api/v1/tasks/${roomId}/data`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/data`);
return this.http.get<TaskData>(url); return this.http.get<TaskData>(url);
} }
getVideoFileDetails(roomId: number): Observable<VideoFileDetail[]> { getVideoFileDetails(roomId: number): Observable<VideoFileDetail[]> {
const url = apiUrl + `/api/v1/tasks/${roomId}/videos`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/videos`);
return this.http.get<VideoFileDetail[]>(url); return this.http.get<VideoFileDetail[]>(url);
} }
getDanmakuFileDetails(roomId: number): Observable<DanmakuFileDetail[]> { getDanmakuFileDetails(roomId: number): Observable<DanmakuFileDetail[]> {
const url = apiUrl + `/api/v1/tasks/${roomId}/danmakus`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/danmakus`);
return this.http.get<DanmakuFileDetail[]>(url); return this.http.get<DanmakuFileDetail[]>(url);
} }
getTaskParam(roomId: number): Observable<TaskParam> { getTaskParam(roomId: number): Observable<TaskParam> {
const url = apiUrl + `/api/v1/tasks/${roomId}/param`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/param`);
return this.http.get<TaskParam>(url); return this.http.get<TaskParam>(url);
} }
getMetadata(roomId: number): Observable<Metadata | null> { getMetadata(roomId: number): Observable<Metadata | null> {
const url = apiUrl + `/api/v1/tasks/${roomId}/metadata`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/metadata`);
return this.http.get<Metadata | null>(url); return this.http.get<Metadata | null>(url);
} }
getStreamProfile(roomId: number): Observable<StreamProfile> { getStreamProfile(roomId: number): Observable<StreamProfile> {
const url = apiUrl + `/api/v1/tasks/${roomId}/profile`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/profile`);
return this.http.get<StreamProfile>(url); return this.http.get<StreamProfile>(url);
} }
updateAllTaskInfos(): Observable<ResponseMessage> { updateAllTaskInfos(): Observable<ResponseMessage> {
const url = apiUrl + '/api/v1/tasks/info'; const url = this.url.makeApiUrl('/api/v1/tasks/info');
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
updateTaskInfo(roomId: number): Observable<ResponseMessage> { updateTaskInfo(roomId: number): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/info`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/info`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
addTask(roomId: number): Observable<AddTaskResult> { addTask(roomId: number): Observable<AddTaskResult> {
const url = apiUrl + `/api/v1/tasks/${roomId}`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}`);
return this.http.post<AddTaskResult>(url, null); return this.http.post<AddTaskResult>(url, null);
} }
removeTask(roomId: number): Observable<ResponseMessage> { removeTask(roomId: number): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}`);
return this.http.delete<ResponseMessage>(url); return this.http.delete<ResponseMessage>(url);
} }
removeAllTasks(): Observable<ResponseMessage> { removeAllTasks(): Observable<ResponseMessage> {
const url = apiUrl + '/api/v1/tasks'; const url = this.url.makeApiUrl('/api/v1/tasks');
return this.http.delete<ResponseMessage>(url); return this.http.delete<ResponseMessage>(url);
} }
startTask(roomId: number): Observable<ResponseMessage> { startTask(roomId: number): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/start`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/start`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
startAllTasks(): Observable<ResponseMessage> { startAllTasks(): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/start`; const url = this.url.makeApiUrl(`/api/v1/tasks/start`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
@ -102,7 +100,7 @@ export class TaskService {
force: boolean = false, force: boolean = false,
background: boolean = false background: boolean = false
): Observable<ResponseMessage> { ): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/stop`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/stop`);
return this.http.post<ResponseMessage>(url, { force, background }); return this.http.post<ResponseMessage>(url, { force, background });
} }
@ -110,17 +108,17 @@ export class TaskService {
force: boolean = false, force: boolean = false,
background: boolean = false background: boolean = false
): Observable<ResponseMessage> { ): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/stop`; const url = this.url.makeApiUrl(`/api/v1/tasks/stop`);
return this.http.post<ResponseMessage>(url, { force, background }); return this.http.post<ResponseMessage>(url, { force, background });
} }
enableTaskMonitor(roomId: number): Observable<ResponseMessage> { enableTaskMonitor(roomId: number): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/monitor/enable`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/monitor/enable`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
enableAllMonitors(): Observable<ResponseMessage> { enableAllMonitors(): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/monitor/enable`; const url = this.url.makeApiUrl(`/api/v1/tasks/monitor/enable`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
@ -128,22 +126,22 @@ export class TaskService {
roomId: number, roomId: number,
background: boolean = false background: boolean = false
): Observable<ResponseMessage> { ): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/monitor/disable`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/monitor/disable`);
return this.http.post<ResponseMessage>(url, { background }); return this.http.post<ResponseMessage>(url, { background });
} }
disableAllMonitors(background: boolean = false): Observable<ResponseMessage> { disableAllMonitors(background: boolean = false): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/monitor/disable`; const url = this.url.makeApiUrl(`/api/v1/tasks/monitor/disable`);
return this.http.post<ResponseMessage>(url, { background }); return this.http.post<ResponseMessage>(url, { background });
} }
enableTaskRecorder(roomId: number): Observable<ResponseMessage> { enableTaskRecorder(roomId: number): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/recorder/enable`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/recorder/enable`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
enableAllRecorders(): Observable<ResponseMessage> { enableAllRecorders(): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/recorder/enable`; const url = this.url.makeApiUrl(`/api/v1/tasks/recorder/enable`);
return this.http.post<ResponseMessage>(url, null); return this.http.post<ResponseMessage>(url, null);
} }
@ -152,7 +150,7 @@ export class TaskService {
force: boolean = false, force: boolean = false,
background: boolean = false background: boolean = false
): Observable<ResponseMessage> { ): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/${roomId}/recorder/disable`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/recorder/disable`);
return this.http.post<ResponseMessage>(url, { force, background }); return this.http.post<ResponseMessage>(url, { force, background });
} }
@ -160,19 +158,19 @@ export class TaskService {
force: boolean = false, force: boolean = false,
background: boolean = false background: boolean = false
): Observable<ResponseMessage> { ): Observable<ResponseMessage> {
const url = apiUrl + `/api/v1/tasks/recorder/disable`; const url = this.url.makeApiUrl(`/api/v1/tasks/recorder/disable`);
return this.http.post<ResponseMessage>(url, { force, background }); return this.http.post<ResponseMessage>(url, { force, background });
} }
canCutStream(roomId: number) { canCutStream(roomId: number) {
const url = apiUrl + `/api/v1/tasks/${roomId}/cut`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/cut`);
return this.http return this.http
.get<{ data: { result: boolean } }>(url) .get<{ data: { result: boolean } }>(url)
.pipe(map((response) => response.data.result)); .pipe(map((response) => response.data.result));
} }
cutStream(roomId: number) { cutStream(roomId: number) {
const url = apiUrl + `/api/v1/tasks/${roomId}/cut`; const url = this.url.makeApiUrl(`/api/v1/tasks/${roomId}/cut`);
return this.http.post<null>(url, null); return this.http.post<null>(url, null);
} }
} }

View File

@ -2,8 +2,8 @@ import { NgxLoggerLevel } from 'ngx-logger';
export const environment = { export const environment = {
production: true, production: true,
apiUrl: '', apiBaseUrl: '',
webSocketUrl: '', webSocketBaseUrl: '',
ngxLoggerLevel: NgxLoggerLevel.DEBUG, ngxLoggerLevel: NgxLoggerLevel.DEBUG,
traceRouterScrolling: false, traceRouterScrolling: false,
} as const; } as const;

View File

@ -6,8 +6,8 @@ import { NgxLoggerLevel } from 'ngx-logger';
export const environment = { export const environment = {
production: false, production: false,
apiUrl: 'http://localhost:2233', apiBaseUrl: 'http://localhost:2233',
webSocketUrl: 'ws://localhost:2233', webSocketBaseUrl: 'ws://localhost:2233',
ngxLoggerLevel: NgxLoggerLevel.TRACE, ngxLoggerLevel: NgxLoggerLevel.TRACE,
traceRouterScrolling: true, traceRouterScrolling: true,
} as const; } as const;