v3.0.0: Docker-Migration mit CI-Pipeline und neuen Features
Build Release Docker Image / build-arm64 (release) Successful in 1m0s
Build Release Docker Image / build-amd64 (release) Successful in 2m50s
Build Release Docker Image / publish-release-manifest (release) Successful in 7s

- Gitea CI-Workflows fuer Multi-Arch Docker Builds (amd64 + arm64)
- Beschreibungsfeld und mehrtaegige Zeitplanung fuer Jobs
- Benachrichtigungen auf Deutsch
- docker-compose nutzt Registry-Image

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Asmus
2026-09-16 09:40:40 +02:00
co-authored by Claude Opus 4.6
parent 9fa589789e
commit b446ce9b3e
9 changed files with 298 additions and 28 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Build Dev Docker Image
on:
workflow_dispatch:
push:
branches: [main, dev]
paths-ignore:
- 'docs/**'
- 'README.md'
- 'LICENSE'
env:
REGISTRY: git.techniverse.net
IMAGE: git.techniverse.net/scriptos/stream-recorder
jobs:
build-amd64:
runs-on: linux-amd64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Gitea Container Registry
run: echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push amd64 image
run: |
docker build --pull --platform linux/amd64 \
-t "${IMAGE}:dev-amd64" \
-t "${IMAGE}:dev-${GITHUB_SHA}-amd64" \
.
docker push "${IMAGE}:dev-amd64"
docker push "${IMAGE}:dev-${GITHUB_SHA}-amd64"
build-arm64:
runs-on: linux-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Gitea Container Registry
run: echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push arm64 image
run: |
docker build --pull --platform linux/arm64 \
-t "${IMAGE}:dev-arm64" \
-t "${IMAGE}:dev-${GITHUB_SHA}-arm64" \
.
docker push "${IMAGE}:dev-arm64"
docker push "${IMAGE}:dev-${GITHUB_SHA}-arm64"
publish-dev-manifest:
runs-on: linux-amd64
needs: [build-amd64, build-arm64]
steps:
- name: Login to Gitea Container Registry
run: echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Publish dev manifests
run: |
docker manifest create "${IMAGE}:dev-latest" \
--amend "${IMAGE}:dev-amd64" \
--amend "${IMAGE}:dev-arm64"
docker manifest push "${IMAGE}:dev-latest"
docker manifest create "${IMAGE}:dev-${GITHUB_SHA}" \
--amend "${IMAGE}:dev-${GITHUB_SHA}-amd64" \
--amend "${IMAGE}:dev-${GITHUB_SHA}-arm64"
docker manifest push "${IMAGE}:dev-${GITHUB_SHA}"
+87
View File
@@ -0,0 +1,87 @@
name: Build Release Docker Image
on:
release:
types: [published]
env:
REGISTRY: git.techniverse.net
IMAGE: git.techniverse.net/scriptos/stream-recorder
jobs:
build-amd64:
runs-on: linux-amd64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Gitea Container Registry
run: echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push amd64 release image
run: |
RELEASE_TAG="${GITHUB_REF_NAME}"
VERSION="${RELEASE_TAG#v}"
docker build --pull --platform linux/amd64 \
-t "${IMAGE}:${RELEASE_TAG}-amd64" \
-t "${IMAGE}:${VERSION}-amd64" \
.
docker push "${IMAGE}:${RELEASE_TAG}-amd64"
docker push "${IMAGE}:${VERSION}-amd64"
build-arm64:
runs-on: linux-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Gitea Container Registry
run: echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push arm64 release image
run: |
RELEASE_TAG="${GITHUB_REF_NAME}"
VERSION="${RELEASE_TAG#v}"
docker build --pull --platform linux/arm64 \
-t "${IMAGE}:${RELEASE_TAG}-arm64" \
-t "${IMAGE}:${VERSION}-arm64" \
.
docker push "${IMAGE}:${RELEASE_TAG}-arm64"
docker push "${IMAGE}:${VERSION}-arm64"
publish-release-manifest:
runs-on: linux-amd64
needs: [build-amd64, build-arm64]
steps:
- name: Login to Gitea Container Registry
run: echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${GITHUB_ACTOR}" --password-stdin
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Publish release manifests
run: |
RELEASE_TAG="${GITHUB_REF_NAME}"
VERSION="${RELEASE_TAG#v}"
docker manifest create "${IMAGE}:${RELEASE_TAG}" \
--amend "${IMAGE}:${RELEASE_TAG}-amd64" \
--amend "${IMAGE}:${RELEASE_TAG}-arm64"
docker manifest push "${IMAGE}:${RELEASE_TAG}"
if [ "${VERSION}" != "${RELEASE_TAG}" ]; then
docker manifest create "${IMAGE}:${VERSION}" \
--amend "${IMAGE}:${VERSION}-amd64" \
--amend "${IMAGE}:${VERSION}-arm64"
docker manifest push "${IMAGE}:${VERSION}"
fi
docker manifest create "${IMAGE}:latest" \
--amend "${IMAGE}:${RELEASE_TAG}-amd64" \
--amend "${IMAGE}:${RELEASE_TAG}-arm64"
docker manifest push "${IMAGE}:latest"
+17 -1
View File
@@ -49,12 +49,28 @@ docker compose up -d
Das Web-UI ist unter `http://localhost:8484` erreichbar. Die Konfiguration wird beim ersten Start automatisch in `data/config.yml` erstellt. Das Web-UI ist unter `http://localhost:8484` erreichbar. Die Konfiguration wird beim ersten Start automatisch in `data/config.yml` erstellt.
## Container Image
Release-Images werden automatisch in der Gitea Container Registry veröffentlicht:
```bash
docker pull git.techniverse.net/scriptos/stream-recorder:latest
docker pull git.techniverse.net/scriptos/stream-recorder:v3.0.0
docker pull git.techniverse.net/scriptos/stream-recorder:3.0.0
```
Dev-Images entstehen bei Pushes auf `main` oder `dev`:
```bash
docker pull git.techniverse.net/scriptos/stream-recorder:dev-latest
```
## Docker Compose ## Docker Compose
```yaml ```yaml
services: services:
stream-recorder: stream-recorder:
image: stream-recorder:latest image: git.techniverse.net/scriptos/stream-recorder:latest
container_name: stream-recorder container_name: stream-recorder
restart: unless-stopped restart: unless-stopped
ports: ports:
+24 -1
View File
@@ -97,6 +97,8 @@ class JobCreate(BaseModel):
schedule_days: str = "*" schedule_days: str = "*"
schedule_start: str = "" schedule_start: str = ""
schedule_stop: str = "" schedule_stop: str = ""
schedule_end_date: str = ""
description: str = ""
ntfy_enabled: bool = False ntfy_enabled: bool = False
plik_enabled: bool = False plik_enabled: bool = False
delete_after_upload: bool = False delete_after_upload: bool = False
@@ -136,6 +138,8 @@ async def list_jobs():
"schedule_days": j.schedule_days, "schedule_days": j.schedule_days,
"schedule_start": j.schedule_start, "schedule_start": j.schedule_start,
"schedule_stop": j.schedule_stop, "schedule_stop": j.schedule_stop,
"schedule_end_date": j.schedule_end_date,
"description": j.description,
"ntfy_enabled": j.ntfy_enabled, "ntfy_enabled": j.ntfy_enabled,
"plik_enabled": j.plik_enabled, "plik_enabled": j.plik_enabled,
"delete_after_upload": j.delete_after_upload, "delete_after_upload": j.delete_after_upload,
@@ -178,6 +182,7 @@ async def get_job(job_id: int):
"schedule_enabled": job.schedule_enabled, "schedule_once": job.schedule_once, "schedule_enabled": job.schedule_enabled, "schedule_once": job.schedule_once,
"schedule_date": job.schedule_date, "schedule_days": job.schedule_days, "schedule_date": job.schedule_date, "schedule_days": job.schedule_days,
"schedule_start": job.schedule_start, "schedule_stop": job.schedule_stop, "schedule_start": job.schedule_start, "schedule_stop": job.schedule_stop,
"schedule_end_date": job.schedule_end_date, "description": job.description,
"ntfy_enabled": job.ntfy_enabled, "ntfy_enabled": job.ntfy_enabled,
"plik_enabled": job.plik_enabled, "delete_after_upload": job.delete_after_upload, "plik_enabled": job.plik_enabled, "delete_after_upload": job.delete_after_upload,
"metadata_monitor_enabled": job.metadata_monitor_enabled, "metadata_monitor_enabled": job.metadata_monitor_enabled,
@@ -226,11 +231,29 @@ async def start_job_recording(job_id: int):
job = session.query(Job).filter_by(id=job_id).first() job = session.query(Job).filter_by(id=job_id).first()
if not job: if not job:
raise HTTPException(404, "Job nicht gefunden") raise HTTPException(404, "Job nicht gefunden")
duration = job.max_duration
if not duration and job.schedule_start and job.schedule_stop:
if job.schedule_end_date and job.schedule_date:
start_dt = datetime.datetime.strptime(f"{job.schedule_date} {job.schedule_start}", "%Y-%m-%d %H:%M")
end_dt = datetime.datetime.strptime(f"{job.schedule_end_date} {job.schedule_stop}", "%Y-%m-%d %H:%M")
duration = max(0, int((end_dt - start_dt).total_seconds()))
else:
def _time_to_minutes(t: str) -> int:
parts = t.split(":")
return int(parts[0]) * 60 + int(parts[1])
start_min = _time_to_minutes(job.schedule_start)
stop_min = _time_to_minutes(job.schedule_stop)
if stop_min > start_min:
duration = (stop_min - start_min) * 60
else:
duration = (1440 - start_min + stop_min) * 60
job_data = { job_data = {
"name": job.name, "stream_url": job.stream_url, "name": job.name, "stream_url": job.stream_url,
"stream_type": job.stream_type, "output_format": job.output_format, "stream_type": job.stream_type, "output_format": job.output_format,
"max_duration": job.max_duration, "extra_ffmpeg_args": job.extra_ffmpeg_args, "max_duration": duration, "extra_ffmpeg_args": job.extra_ffmpeg_args,
"segment_duration": job.segment_duration, "segment_duration": job.segment_duration,
"description": job.description,
"metadata_monitor_enabled": job.metadata_monitor_enabled, "metadata_monitor_enabled": job.metadata_monitor_enabled,
"metadata_pattern": job.metadata_pattern, "metadata_pattern": job.metadata_pattern,
"metadata_grace_period": job.metadata_grace_period or 300, "metadata_grace_period": job.metadata_grace_period or 300,
+3
View File
@@ -23,6 +23,9 @@ class Job(Base):
schedule_days = Column(String(50), default="*") schedule_days = Column(String(50), default="*")
schedule_start = Column(String(5), default="") schedule_start = Column(String(5), default="")
schedule_stop = Column(String(5), default="") schedule_stop = Column(String(5), default="")
schedule_end_date = Column(String(10), default="")
description = Column(Text, default="")
plik_enabled = Column(Boolean, default=False) plik_enabled = Column(Boolean, default=False)
delete_after_upload = Column(Boolean, default=False) delete_after_upload = Column(Boolean, default=False)
+10 -4
View File
@@ -98,6 +98,9 @@ class RecordingManager:
ntfy_enabled = False ntfy_enabled = False
if rec_id and rec_id in self._rec_options: if rec_id and rec_id in self._rec_options:
ntfy_enabled = self._rec_options[rec_id].get("ntfy_enabled", False) ntfy_enabled = self._rec_options[rec_id].get("ntfy_enabled", False)
desc = self._rec_options[rec_id].get("description", "")
if desc:
message = f"{message}\n{desc}"
self._get_ntfy().notify(event, title, message, job_ntfy_enabled=ntfy_enabled, priority=priority) self._get_ntfy().notify(event, title, message, job_ntfy_enabled=ntfy_enabled, priority=priority)
def _log(self, level: str, message: str, job_name: str = None): def _log(self, level: str, message: str, job_name: str = None):
@@ -120,6 +123,7 @@ class RecordingManager:
extra_ffmpeg_args: str = "", extra_ffmpeg_args: str = "",
segment_duration: Optional[int] = None, segment_duration: Optional[int] = None,
is_scheduled: bool = False, is_scheduled: bool = False,
description: str = "",
metadata_monitor_enabled: bool = False, metadata_monitor_enabled: bool = False,
metadata_pattern: str = "", metadata_pattern: str = "",
metadata_grace_period: int = 300, metadata_grace_period: int = 300,
@@ -186,10 +190,11 @@ class RecordingManager:
"plik_enabled": plik_enabled, "plik_enabled": plik_enabled,
"delete_after_upload": delete_after_upload, "delete_after_upload": delete_after_upload,
"segment_mode": segment_mode, "segment_mode": segment_mode,
"description": description,
} }
self._log("INFO", f"Aufnahme gestartet: {name} [{stream_type}]", name) self._log("INFO", f"Aufnahme gestartet: {name}", name)
self._notify("start", "Aufnahme gestartet", f"{name} [{stream_type}]", rec_id) self._notify("start", "Aufnahme gestartet", name, rec_id)
if metadata_monitor_enabled and metadata_pattern: if metadata_monitor_enabled and metadata_pattern:
self._get_metadata_monitor().start_monitoring( self._get_metadata_monitor().start_monitoring(
@@ -251,9 +256,10 @@ class RecordingManager:
if stop_event.is_set() or retcode == 0: if stop_event.is_set() or retcode == 0:
status = "completed" if retcode == 0 else "stopped" status = "completed" if retcode == 0 else "stopped"
status_de = "abgeschlossen" if status == "completed" else "gestoppt"
self._update_recording(rec_id, status=status) self._update_recording(rec_id, status=status)
self._log("INFO", f"Aufnahme {status}: {job_name}", job_name) self._log("INFO", f"Aufnahme {status_de}: {job_name}", job_name)
self._notify("stop", f"Aufnahme {status}", job_name, rec_id) self._notify("stop", f"Aufnahme {status_de}", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode) self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
return return
+29 -5
View File
@@ -85,12 +85,13 @@ class StreamScheduler:
finally: finally:
session.close() session.close()
def _get_active_recording_for_job(self, job_id: int): def _get_active_scheduled_recording_for_job(self, job_id: int):
session = get_session() session = get_session()
try: try:
rec = session.query(Recording).filter( rec = session.query(Recording).filter(
Recording.job_id == job_id, Recording.job_id == job_id,
Recording.status.in_(["running", "starting"]), Recording.status.in_(["running", "starting"]),
Recording.is_scheduled == True,
).first() ).first()
return rec.id if rec else None return rec.id if rec else None
finally: finally:
@@ -114,7 +115,8 @@ class StreamScheduler:
"segment_duration": j.segment_duration, "segment_duration": j.segment_duration,
"schedule_once": j.schedule_once, "schedule_date": j.schedule_date, "schedule_once": j.schedule_once, "schedule_date": j.schedule_date,
"schedule_days": j.schedule_days, "schedule_start": j.schedule_start, "schedule_days": j.schedule_days, "schedule_start": j.schedule_start,
"schedule_stop": j.schedule_stop, "schedule_stop": j.schedule_stop, "schedule_end_date": j.schedule_end_date,
"description": j.description,
"metadata_monitor_enabled": j.metadata_monitor_enabled, "metadata_monitor_enabled": j.metadata_monitor_enabled,
"metadata_pattern": j.metadata_pattern, "metadata_pattern": j.metadata_pattern,
"metadata_grace_period": j.metadata_grace_period, "metadata_grace_period": j.metadata_grace_period,
@@ -131,6 +133,8 @@ class StreamScheduler:
continue continue
is_today = False is_today = False
multi_day = bool(job.get("schedule_end_date") and job["schedule_once"])
if job["schedule_once"]: if job["schedule_once"]:
if job["schedule_date"] == today_date: if job["schedule_date"] == today_date:
is_today = True is_today = True
@@ -139,8 +143,20 @@ class StreamScheduler:
is_today = True is_today = True
in_window = False in_window = False
is_cross_midnight = False
if is_today: if is_today:
if job["schedule_stop"]: if multi_day:
now_min = _time_to_minutes(now_time)
start_min = _time_to_minutes(job["schedule_start"])
in_window = now_min >= start_min
elif job["schedule_stop"]:
start_min = _time_to_minutes(job["schedule_start"])
stop_min = _time_to_minutes(job["schedule_stop"])
if stop_min <= start_min:
is_cross_midnight = True
now_min = _time_to_minutes(now_time)
in_window = now_min >= start_min
else:
in_window = _in_time_window(now_time, job["schedule_start"], job["schedule_stop"]) in_window = _in_time_window(now_time, job["schedule_start"], job["schedule_stop"])
else: else:
now_min = _time_to_minutes(now_time) now_min = _time_to_minutes(now_time)
@@ -153,6 +169,13 @@ class StreamScheduler:
if occ_key not in self._started_keys and not self._is_job_recording(job["id"]): if occ_key not in self._started_keys and not self._is_job_recording(job["id"]):
duration = job["max_duration"] duration = job["max_duration"]
if not duration and job["schedule_stop"]: if not duration and job["schedule_stop"]:
if multi_day:
start_dt = datetime.datetime.strptime(
f"{job['schedule_date']} {job['schedule_start']}", "%Y-%m-%d %H:%M")
end_dt = datetime.datetime.strptime(
f"{job['schedule_end_date']} {job['schedule_stop']}", "%Y-%m-%d %H:%M")
duration = max(0, int((end_dt - start_dt).total_seconds()))
else:
start_min = _time_to_minutes(job["schedule_start"]) start_min = _time_to_minutes(job["schedule_start"])
stop_min = _time_to_minutes(job["schedule_stop"]) stop_min = _time_to_minutes(job["schedule_stop"])
if stop_min > start_min: if stop_min > start_min:
@@ -170,6 +193,7 @@ class StreamScheduler:
extra_ffmpeg_args=job["extra_ffmpeg_args"] or "", extra_ffmpeg_args=job["extra_ffmpeg_args"] or "",
segment_duration=job["segment_duration"], segment_duration=job["segment_duration"],
is_scheduled=True, is_scheduled=True,
description=job.get("description", ""),
metadata_monitor_enabled=job.get("metadata_monitor_enabled", False), metadata_monitor_enabled=job.get("metadata_monitor_enabled", False),
metadata_pattern=job.get("metadata_pattern", ""), metadata_pattern=job.get("metadata_pattern", ""),
metadata_grace_period=job.get("metadata_grace_period") or 300, metadata_grace_period=job.get("metadata_grace_period") or 300,
@@ -180,8 +204,8 @@ class StreamScheduler:
) )
self._started_keys.add(occ_key) self._started_keys.add(occ_key)
else: else:
if job["schedule_stop"] and self._is_job_recording(job["id"]): if not multi_day and not is_cross_midnight and job["schedule_stop"] and self._is_job_recording(job["id"]):
rec_id = self._get_active_recording_for_job(job["id"]) rec_id = self._get_active_scheduled_recording_for_job(job["id"])
if rec_id: if rec_id:
self.recorder.stop_recording(rec_id) self.recorder.stop_recording(rec_id)
self._started_keys.discard(occ_key) self._started_keys.discard(occ_key)
+39 -3
View File
@@ -450,6 +450,10 @@
<label>Name *</label> <label>Name *</label>
<input type="text" id="job-name" placeholder="Mein Stream"> <input type="text" id="job-name" placeholder="Mein Stream">
</div> </div>
<div class="form-group">
<label>Beschreibung (optional)</label>
<input type="text" id="job-description" placeholder="z.B. Freitagssendung, Live-Event...">
</div>
<div class="form-group"> <div class="form-group">
<label>Typ</label> <label>Typ</label>
<select id="job-type"> <select id="job-type">
@@ -498,9 +502,13 @@
</div> </div>
</div> </div>
<div class="form-group" id="scheduleDateGroup" style="display:none"> <div class="form-group" id="scheduleDateGroup" style="display:none">
<label>Datum</label> <label>Startdatum</label>
<input type="date" id="job-schedule-date"> <input type="date" id="job-schedule-date">
</div> </div>
<div class="form-group" id="scheduleEndDateGroup" style="display:none">
<label>Enddatum (leer = gleicher Tag)</label>
<input type="date" id="job-schedule-end-date" oninput="updateDurationField()">
</div>
<div class="form-group" id="scheduleDaysGroup"> <div class="form-group" id="scheduleDaysGroup">
<label>Tage (z.B. Mo-Fr, Sa,So, *)</label> <label>Tage (z.B. Mo-Fr, Sa,So, *)</label>
<input type="text" id="job-schedule-days" value="*" placeholder="*"> <input type="text" id="job-schedule-days" value="*" placeholder="*">
@@ -721,6 +729,7 @@ async function loadJobs() {
<button class="btn btn-sm btn-danger" onclick="deleteJob(${j.id})">Löschen</button> <button class="btn btn-sm btn-danger" onclick="deleteJob(${j.id})">Löschen</button>
</div> </div>
</div> </div>
${j.description ? `<div class="meta" style="margin-bottom:4px"><span class="meta-item" style="color:var(--text)">${esc(j.description)}</span></div>` : ''}
<div class="meta"> <div class="meta">
<span class="meta-item">${esc(j.stream_type || 'auto')}</span> <span class="meta-item">${esc(j.stream_type || 'auto')}</span>
<span class="meta-item">${esc(j.stream_url)}</span> <span class="meta-item">${esc(j.stream_url)}</span>
@@ -744,6 +753,7 @@ function openJobModal(job = null) {
document.getElementById('job-url').value = job ? job.stream_url : ''; document.getElementById('job-url').value = job ? job.stream_url : '';
document.getElementById('job-name').value = job ? job.name : ''; document.getElementById('job-name').value = job ? job.name : '';
document.getElementById('job-type').value = job ? job.stream_type : 'auto'; document.getElementById('job-type').value = job ? job.stream_type : 'auto';
document.getElementById('job-description').value = job ? job.description || '' : '';
document.getElementById('job-format').value = job ? job.output_format || '' : ''; document.getElementById('job-format').value = job ? job.output_format || '' : '';
document.getElementById('job-duration').value = job && job.max_duration ? Math.round(job.max_duration / 60) : ''; document.getElementById('job-duration').value = job && job.max_duration ? Math.round(job.max_duration / 60) : '';
document.getElementById('job-segment').value = job && job.segment_duration ? Math.round(job.segment_duration / 60) : ''; document.getElementById('job-segment').value = job && job.segment_duration ? Math.round(job.segment_duration / 60) : '';
@@ -751,6 +761,7 @@ function openJobModal(job = null) {
document.getElementById('job-schedule-enabled').checked = job ? job.schedule_enabled : false; document.getElementById('job-schedule-enabled').checked = job ? job.schedule_enabled : false;
document.getElementById('job-schedule-once').checked = job ? job.schedule_once : false; document.getElementById('job-schedule-once').checked = job ? job.schedule_once : false;
document.getElementById('job-schedule-date').value = job ? job.schedule_date || '' : ''; document.getElementById('job-schedule-date').value = job ? job.schedule_date || '' : '';
document.getElementById('job-schedule-end-date').value = job ? job.schedule_end_date || '' : '';
document.getElementById('job-schedule-days').value = job ? job.schedule_days || '*' : '*'; document.getElementById('job-schedule-days').value = job ? job.schedule_days || '*' : '*';
document.getElementById('job-schedule-start').value = job ? job.schedule_start || '' : ''; document.getElementById('job-schedule-start').value = job ? job.schedule_start || '' : '';
document.getElementById('job-schedule-stop').value = job ? job.schedule_stop || '' : ''; document.getElementById('job-schedule-stop').value = job ? job.schedule_stop || '' : '';
@@ -791,6 +802,8 @@ async function saveJob() {
schedule_days: document.getElementById('job-schedule-days').value || '*', schedule_days: document.getElementById('job-schedule-days').value || '*',
schedule_start: document.getElementById('job-schedule-start').value, schedule_start: document.getElementById('job-schedule-start').value,
schedule_stop: document.getElementById('job-schedule-stop').value, schedule_stop: document.getElementById('job-schedule-stop').value,
schedule_end_date: document.getElementById('job-schedule-end-date').value,
description: document.getElementById('job-description').value,
ntfy_enabled: document.getElementById('job-ntfy-enabled').checked, ntfy_enabled: document.getElementById('job-ntfy-enabled').checked,
plik_enabled: document.getElementById('job-plik-enabled').checked, plik_enabled: document.getElementById('job-plik-enabled').checked,
delete_after_upload: document.getElementById('job-delete-after').checked, delete_after_upload: document.getElementById('job-delete-after').checked,
@@ -844,27 +857,50 @@ function updateDurationField() {
const scheduleOn = document.getElementById('job-schedule-enabled').checked; const scheduleOn = document.getElementById('job-schedule-enabled').checked;
const start = document.getElementById('job-schedule-start').value; const start = document.getElementById('job-schedule-start').value;
const stop = document.getElementById('job-schedule-stop').value; const stop = document.getElementById('job-schedule-stop').value;
const once = document.getElementById('job-schedule-once').checked;
const startDate = document.getElementById('job-schedule-date').value;
const endDate = document.getElementById('job-schedule-end-date').value;
const hasScheduleStop = scheduleOn && start && stop; const hasScheduleStop = scheduleOn && start && stop;
document.getElementById('durationGroup').style.display = hasScheduleStop ? 'none' : ''; document.getElementById('durationGroup').style.display = hasScheduleStop ? 'none' : '';
document.getElementById('durationAutoGroup').style.display = hasScheduleStop ? '' : 'none'; document.getElementById('durationAutoGroup').style.display = hasScheduleStop ? '' : 'none';
if (hasScheduleStop) { if (hasScheduleStop) {
let text;
if (once && startDate && endDate && endDate !== startDate) {
const startDt = new Date(`${startDate}T${start}`);
const endDt = new Date(`${endDate}T${stop}`);
let diffMin = Math.round((endDt - startDt) / 60000);
if (diffMin <= 0) { text = 'Enddatum muss nach Startdatum liegen'; }
else {
const d = Math.floor(diffMin / 1440);
const h = Math.floor((diffMin % 1440) / 60);
const m = diffMin % 60;
const parts = [];
if (d > 0) parts.push(`${d} Tag${d > 1 ? 'e' : ''}`);
if (h > 0) parts.push(`${h} Std.`);
if (m > 0) parts.push(`${m} Min.`);
text = `${parts.join(' ')} (mehrtägig, aus Zeitplan)`;
}
} else {
const [sh, sm] = start.split(':').map(Number); const [sh, sm] = start.split(':').map(Number);
const [eh, em] = stop.split(':').map(Number); const [eh, em] = stop.split(':').map(Number);
let diff = (eh * 60 + em) - (sh * 60 + sm); let diff = (eh * 60 + em) - (sh * 60 + sm);
if (diff <= 0) diff += 1440; if (diff <= 0) diff += 1440;
const h = Math.floor(diff / 60); const h = Math.floor(diff / 60);
const m = diff % 60; const m = diff % 60;
const text = h > 0 ? `${h} Std. ${m > 0 ? m + ' Min.' : ''}` : `${m} Min.`; text = (h > 0 ? `${h} Std. ${m > 0 ? m + ' Min.' : ''}` : `${m} Min.`) + ' (automatisch aus Zeitplan)';
document.getElementById('durationAutoText').textContent = `${text} (automatisch aus Zeitplan)`; }
document.getElementById('durationAutoText').textContent = text;
} }
} }
function toggleScheduleOnce() { function toggleScheduleOnce() {
const once = document.getElementById('job-schedule-once').checked; const once = document.getElementById('job-schedule-once').checked;
document.getElementById('scheduleDateGroup').style.display = once ? 'block' : 'none'; document.getElementById('scheduleDateGroup').style.display = once ? 'block' : 'none';
document.getElementById('scheduleEndDateGroup').style.display = once ? 'block' : 'none';
document.getElementById('scheduleDaysGroup').style.display = once ? 'none' : 'block'; document.getElementById('scheduleDaysGroup').style.display = once ? 'none' : 'block';
updateDurationField();
} }
function togglePlik() { function togglePlik() {
+1 -2
View File
@@ -1,7 +1,6 @@
services: services:
stream-recorder: stream-recorder:
build: . image: git.techniverse.net/scriptos/stream-recorder:latest
image: stream-recorder:latest
container_name: stream-recorder container_name: stream-recorder
restart: unless-stopped restart: unless-stopped
ports: ports: