Skip to content

Commit 1d1280e

Browse files
authored
Merge pull request #115 from nebulabroadcast/develop
6.0.9
2 parents 3c42f70 + 4f08d5a commit 1d1280e

File tree

208 files changed

+15827
-4925
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

208 files changed

+15827
-4925
lines changed

.github/workflows/dev.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Publish a new version
1+
name: Publish a dev version
22

33
on:
44
push:
@@ -22,7 +22,7 @@ jobs:
2222
uses: SebRollen/[email protected]
2323
with:
2424
file: 'backend/pyproject.toml'
25-
field: 'tool.poetry.version'
25+
field: 'project.version'
2626

2727
- name: Build docker image
2828
uses: docker/build-push-action@v4

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
uses: SebRollen/[email protected]
2323
with:
2424
file: 'backend/pyproject.toml'
25-
field: 'tool.poetry.version'
25+
field: 'project.version'
2626

2727
- name: Build docker image
2828
uses: docker/build-push-action@v4

Dockerfile

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
FROM node:latest AS build
22

3-
RUN mkdir /frontend
3+
WORKDIR /frontend
44

5-
COPY ./frontend/index.html /frontend/index.html
6-
COPY ./frontend/package.json /frontend/package.json
7-
COPY ./frontend/vite.config.js /frontend/vite.config.js
8-
COPY ./frontend/src /frontend/src
5+
COPY ./frontend/index.html .
6+
COPY ./frontend/package.json .
7+
COPY ./frontend/vite.config.ts .
8+
COPY ./frontend/tsconfig.json .
9+
COPY ./frontend/tsconfig.node.json .
910
COPY ./frontend/public /frontend/public
1011

11-
WORKDIR /frontend
12-
RUN yarn install && yarn build
12+
RUN yarn install
13+
COPY ./frontend/src /frontend/src
14+
RUN yarn build
1315

14-
FROM python:3.12-bullseye
16+
FROM python:3.12-slim
1517
ENV PYTHONBUFFERED=1
1618

1719
RUN \
1820
apt-get update \
19-
&& apt-get -yqq upgrade \
2021
&& apt-get -yqq install \
21-
cifs-utils
22+
curl \
23+
cifs-utils \
24+
procps \
25+
&& apt-get clean \
26+
&& rm -rf /var/lib/apt/lists/*
2227

23-
RUN mkdir /backend
2428
WORKDIR /backend
25-
COPY ./backend/pyproject.toml /backend/pyproject.toml
26-
27-
RUN \
28-
pip install -U pip && \
29-
pip install poetry && \
30-
poetry config virtualenvs.create false && \
31-
poetry install --no-interaction --no-ansi --only main
29+
COPY ./backend/pyproject.toml /backend/uv.lock .
30+
RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \
31+
uv pip install -r pyproject.toml --system
3232

33-
COPY ./backend /backend
33+
COPY ./backend .
3434
COPY --from=build /frontend/dist/ /frontend
3535

3636
CMD ["/bin/bash", "manage", "start"]

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
IMAGE_NAME=nebulabroadcast/nebula-server:dev
2-
VERSION=$(shell cd backend && poetry run python -c 'import nebula' --version)
2+
VERSION=$(shell cd backend && uv run python -c 'import nebula' --version)
33

44
check:
55
cd frontend && \
66
yarn format
77

88
cd backend && \
9-
poetry version $(VERSION) && \
10-
poetry run ruff format . && \
11-
poetry run ruff check --fix . && \
12-
poetry run mypy .
9+
sed -i "s/^version = \".*\"/version = \"$(VERSION)\"/" pyproject.toml && \
10+
uv run ruff format . && \
11+
uv run ruff check --fix . && \
12+
uv run mypy .
1313

14-
build: check
14+
build:
1515
docker build -t $(IMAGE_NAME) .
1616

1717
dist: build

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ NEBULA
22
======
33

44
![GitHub release (latest by date)](https://img.shields.io/github/v/release/nebulabroadcast/nebula?style=for-the-badge)
5-
![Maintenance](https://img.shields.io/maintenance/yes/2024?style=for-the-badge)
5+
![Maintenance](https://img.shields.io/maintenance/yes/2025?style=for-the-badge)
66
![Last commit](https://img.shields.io/github/last-commit/nebulabroadcast/nebula?style=for-the-badge)
77
![Python version](https://img.shields.io/badge/python-3.11-blue?style=for-the-badge)
88

@@ -26,13 +26,16 @@ Key features
2626

2727
### Media Asset Management
2828

29+
![Metadata editor](https://nebulabroadcast.com/screenshots/nb_screenshot_browser.jpg?)
30+
2931
Simple and fast media catalog based on [EBU Core](https://tech.ebu.ch/MetadataEbuCore) includes a description of asset
3032
genre, editorial format, atmosphere, rights, relations, and technical metadata,
3133
while its very fast search engine makes navigation among media files very easy.
3234

35+
3336
The low-resolution preview allows for editorial review, trimming, and the creation of sub-clips.
3437

35-
![Metadata editor](https://nebulabroadcast.com/static/img/nebula-metadata-editor.webp)
38+
![Video preview](https://nebulabroadcast.com/screenshots/nb_screenshot_preview.jpg?)
3639

3740
### Video and audio cross-conversion and normalization
3841

@@ -43,11 +46,13 @@ smart frame rate and size normalization and [EBU R128](https://tech.ebu.ch/docs/
4346
Automatic cross-conversion servers transcode files for playout, web, low-res proxies, customer previews, etc.
4447
For **h.264** and **HEVC**, Nebula can take advantage of NVIDIA nvenc and leverage the speed of transcoding using GPUs.
4548

49+
![Jonbs view](https://nebulabroadcast.com/screenshots/nb_screenshot_jobs.jpg?)
50+
4651
It is possible to start conversions automatically (rule-based) or trigger them from the user interface.
4752

4853
### Linear scheduling
4954

50-
Firefly client provides a simple and user-friendly way to schedule linear broadcasting.
55+
Nebula provides a simple and user-friendly way to schedule linear broadcasting.
5156
Macro- and micro-scheduling patterns are finished intuitively using drag&drop, including live events.
5257

5358
Nebula has also the ability to schedule for playback assets, which aren't finished yet.
@@ -59,8 +64,7 @@ depending on the particular broadcast scheme, Dramatica selects and automaticall
5964
It is the way to create a playlist for a music station where an algorithm automatically creates a playlist based on a predefined scheme.
6065
Each clip in the rundown is picked by its editorial format, genre, tempo, atmosphere, etc.
6166

62-
![Detail of a scheduler panel in the Firefly application](https://nebulabroadcast.com/static/img/nebula-scheduler.webp)
63-
67+
![Detail of a scheduler panel in the web interface](https://nebulabroadcast.com/screenshots/nb_screenshot_scheduling.jpg?)
6468

6569
### Playout control
6670

@@ -69,12 +73,12 @@ For linear broadcasting, Nebula can control
6973
Broadcasting can run autonomously with and option of starting blocks at a specified time.
7074

7175
Users - master control room operators - can interfere with the rundown using [Firefly client](https://github.com/nebulabroadcast/firefly),
72-
executing graphics or change run order until the last moment.
76+
or the web interface, executing graphics or change run order until the last moment.
7377

7478
Playout control module offers a plug-in interface for secondary events execution such as CG, router or studio control,
7579
recorders control and so on. Right at the operator's fingertips.
7680

77-
![Detail of a rundown panel with playout control interface](https://nebulabroadcast.com/static/img/nebula-playout-control.webp)
81+
![Detail of a rundown panel with playout control interface](https://nebulabroadcast.com/screenshots/nb_screenshot_rundown.jpg?)
7882

7983
### Publishing
8084

@@ -123,7 +127,7 @@ See the GNU General Public License for more details.
123127
Need help?
124128
----------
125129

126-
- Join [Open Source Broadcasting](https://discord.gg/WXaaHYGQ) group on Discord
130+
- Join [Open Source Broadcasting](https://discord.gg/3UxJ4WKfy9) group on Discord
127131
- Professional support for Nebula is provided by [Nebula Broadcast](https://nebulabroadcast.com)
128132
- User documentation is available on [our website](https://nebulabroadcast.com/doc/nebula)
129133
- Found a bug? Please [create an issue](https://github.com/nebulabroadcast/nebula/issues)

backend/api/auth/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
__all__ = ["LoginRequest", "LogoutRequest", "SetPasswordRequest"]
1+
__all__ = [
2+
"LoginRequest",
3+
"LogoutRequest",
4+
"SetPasswordRequest",
5+
"SSOLoginRequest",
6+
"SSOLoginCallback",
7+
"TokenExchangeRequest",
8+
]
29

310
from .login_request import LoginRequest
411
from .logout_request import LogoutRequest
512
from .set_password_request import SetPasswordRequest
13+
from .sso import SSOLoginCallback, SSOLoginRequest
14+
from .token_exchange import TokenExchangeRequest

backend/api/auth/login_request.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,14 @@
11
import time
22

33
from fastapi import Request
4-
from pydantic import Field
54

65
import nebula
76
from server.clientinfo import get_real_ip
8-
from server.models import RequestModel, ResponseModel
7+
from server.models.login import LoginRequestModel, LoginResponseModel
98
from server.request import APIRequest
109
from server.session import Session
1110

1211

13-
class LoginRequestModel(RequestModel):
14-
username: str = Field(
15-
...,
16-
title="Username",
17-
examples=["admin"],
18-
pattern=r"^[a-zA-Z0-9_\-\.]{2,}$",
19-
)
20-
password: str = Field(
21-
...,
22-
title="Password",
23-
description="Password in plain text",
24-
examples=["Password.123"],
25-
)
26-
27-
28-
class LoginResponseModel(ResponseModel):
29-
access_token: str = Field(
30-
...,
31-
title="Access token",
32-
description="Access token to be used in Authorization header"
33-
"for the subsequent requests",
34-
)
35-
36-
3712
async def check_failed_login(ip_address: str) -> None:
3813
banned_until = await nebula.redis.get("banned-ip-until", ip_address)
3914
if banned_until is None:

backend/api/auth/sso.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from urllib.parse import urlparse
2+
3+
from fastapi import Request
4+
from fastapi.responses import RedirectResponse
5+
6+
import nebula
7+
from server.request import APIRequest
8+
from server.session import Session
9+
from server.sso import NebulaSSO
10+
11+
12+
class SSOLoginRequest(APIRequest):
13+
name = "sso_login"
14+
path = "/api/sso/login/{provider}"
15+
methods = ["GET"]
16+
17+
async def handle(self, request: Request, provider: str) -> RedirectResponse:
18+
client = NebulaSSO.client(provider)
19+
20+
referer = request.headers.get("referer")
21+
if referer:
22+
parsed_url = urlparse(referer)
23+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
24+
else:
25+
base_url = "http://localhost:4455"
26+
27+
# We cannot use request.url_for here because it screws the frontend
28+
# dev server proxy
29+
30+
redirect_uri = f"{base_url}/api/sso/callback/{provider}"
31+
nebula.log.debug(f"Redirect URI: {redirect_uri}")
32+
return await client.authorize_redirect(request, redirect_uri)
33+
34+
35+
class SSOLoginCallback(APIRequest):
36+
name = "sso_callback"
37+
path = "/api/sso/callback/{provider}"
38+
methods = ["GET"]
39+
40+
async def handle(self, request: Request, provider: str) -> RedirectResponse:
41+
remote = NebulaSSO.client(provider)
42+
if not remote:
43+
return RedirectResponse("/?error=Invalid provider")
44+
45+
code = request.query_params.get("code")
46+
id_token = request.query_params.get("id_token")
47+
oauth_verifier = request.query_params.get("oauth_verifier")
48+
49+
user_info = {}
50+
51+
if code:
52+
token = await remote.authorize_access_token(request)
53+
user_info = token.get("userinfo", {})
54+
55+
if id_token and not user_info:
56+
token = {"id_token": id_token}
57+
user_info = await remote.parse_id_token(request, token)
58+
59+
if oauth_verifier and not user_info:
60+
token = await remote.authorize_access_token(request)
61+
62+
if token and not user_info:
63+
user_info = await remote.userinfo(token=token)
64+
65+
if not user_info:
66+
return RedirectResponse("/?error=Invalid response from provider")
67+
68+
email = user_info.get("email")
69+
70+
if not email:
71+
return RedirectResponse("/?error=User email not found")
72+
73+
try:
74+
user = await nebula.User.by_email(email)
75+
except nebula.NotFoundException:
76+
return RedirectResponse("/?error=User not found")
77+
session = await Session.create(user, request, transient=True)
78+
return RedirectResponse(f"/?authorize={session.token}")

backend/api/auth/token_exchange.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import Request
2+
3+
import nebula
4+
from server.models.login import LoginResponseModel, TokenExchangeRequestModel
5+
from server.request import APIRequest
6+
from server.session import Session
7+
8+
9+
class TokenExchangeRequest(APIRequest):
10+
"""Exachange a transient access token for a normal one
11+
12+
This request will exchange an access token for a new one.
13+
The original access token will be invalidated.
14+
"""
15+
16+
name: str = "token-exchange"
17+
response_model = LoginResponseModel
18+
19+
async def handle(
20+
self,
21+
request: Request,
22+
payload: TokenExchangeRequestModel,
23+
) -> LoginResponseModel:
24+
session = await Session.check(payload.access_token, request, transient=True)
25+
if not session:
26+
raise nebula.UnauthorizedException("Invalid token")
27+
user_id = session.user["id"]
28+
user = await nebula.User.load(user_id)
29+
session = await Session.create(user, request)
30+
nebula.log.debug(f"{user} token exchanged")
31+
await Session.delete(payload.access_token)
32+
return LoginResponseModel(access_token=session.token)

backend/api/browse.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ def sanitize_value(value: SerializableValue) -> str:
115115
def build_conditions(conditions: list[ConditionModel]) -> list[str]:
116116
cond_list: list[str] = []
117117
for condition in conditions:
118-
assert (
119-
condition.key in nebula.settings.metatypes
120-
), f"Invalid meta key {condition.key}"
118+
assert condition.key in nebula.settings.metatypes, (
119+
f"Invalid meta key {condition.key}"
120+
)
121121
condition.value = normalize_meta(condition.key, condition.value)
122122
if condition.operator in ["IN", "NOT IN"]:
123123
assert isinstance(condition.value, list), "Value must be a list"

0 commit comments

Comments
 (0)