feat: support reverse proxy with subpath
This commit is contained in:
parent
037e37a760
commit
8f6acea210
@ -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]
|
||||||
|
@ -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
1
src/blrec/data/webapp/103.bd702fba8239ab1e.js
Normal file
1
src/blrec/data/webapp/103.bd702fba8239ab1e.js
Normal file
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
1
src/blrec/data/webapp/183.fd2e6a1f63815dbf.js
Normal file
1
src/blrec/data/webapp/183.fd2e6a1f63815dbf.js
Normal file
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/237.44684bee585167eb.js
Normal file
1
src/blrec/data/webapp/237.44684bee585167eb.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/548.a9f0c3e1529d6713.js
Normal file
1
src/blrec/data/webapp/548.a9f0c3e1529d6713.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/91.5d33ec6f665fb52d.js
Normal file
1
src/blrec/data/webapp/91.5d33ec6f665fb52d.js
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
1
src/blrec/data/webapp/main.545d90ce6b1d69be.js
Normal file
1
src/blrec/data/webapp/main.545d90ce6b1d69be.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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": [
|
||||||
|
1
src/blrec/data/webapp/runtime.1db847619b50d96c.js
Normal file
1
src/blrec/data/webapp/runtime.1db847619b50d96c.js
Normal 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))})()})();
|
@ -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))})()})();
|
|
@ -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')
|
|
||||||
|
51
src/blrec/web/middlewares/base_herf.py
Normal file
51
src/blrec/web/middlewares/base_herf.py
Normal 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)
|
25
src/blrec/web/middlewares/route_redirect.py
Normal file
25
src/blrec/web/middlewares/route_redirect.py
Normal 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)
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
webapp/src/app/core/services/url.service.spec.ts
Normal file
16
webapp/src/app/core/services/url.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
22
webapp/src/app/core/services/url.service.ts
Normal file
22
webapp/src/app/core/services/url.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user