v3.0.0: Docker-Migration mit CI-Pipeline und neuen Features
- 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:
co-authored by
Claude Opus 4.6
parent
9fa589789e
commit
b446ce9b3e
@@ -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}"
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
```yaml
|
||||
services:
|
||||
stream-recorder:
|
||||
image: stream-recorder:latest
|
||||
image: git.techniverse.net/scriptos/stream-recorder:latest
|
||||
container_name: stream-recorder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
+24
-1
@@ -97,6 +97,8 @@ class JobCreate(BaseModel):
|
||||
schedule_days: str = "*"
|
||||
schedule_start: str = ""
|
||||
schedule_stop: str = ""
|
||||
schedule_end_date: str = ""
|
||||
description: str = ""
|
||||
ntfy_enabled: bool = False
|
||||
plik_enabled: bool = False
|
||||
delete_after_upload: bool = False
|
||||
@@ -136,6 +138,8 @@ async def list_jobs():
|
||||
"schedule_days": j.schedule_days,
|
||||
"schedule_start": j.schedule_start,
|
||||
"schedule_stop": j.schedule_stop,
|
||||
"schedule_end_date": j.schedule_end_date,
|
||||
"description": j.description,
|
||||
"ntfy_enabled": j.ntfy_enabled,
|
||||
"plik_enabled": j.plik_enabled,
|
||||
"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_date": job.schedule_date, "schedule_days": job.schedule_days,
|
||||
"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,
|
||||
"plik_enabled": job.plik_enabled, "delete_after_upload": job.delete_after_upload,
|
||||
"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()
|
||||
if not job:
|
||||
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 = {
|
||||
"name": job.name, "stream_url": job.stream_url,
|
||||
"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,
|
||||
"description": job.description,
|
||||
"metadata_monitor_enabled": job.metadata_monitor_enabled,
|
||||
"metadata_pattern": job.metadata_pattern,
|
||||
"metadata_grace_period": job.metadata_grace_period or 300,
|
||||
|
||||
@@ -23,6 +23,9 @@ class Job(Base):
|
||||
schedule_days = Column(String(50), default="*")
|
||||
schedule_start = 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)
|
||||
delete_after_upload = Column(Boolean, default=False)
|
||||
|
||||
+10
-4
@@ -98,6 +98,9 @@ class RecordingManager:
|
||||
ntfy_enabled = False
|
||||
if rec_id and rec_id in self._rec_options:
|
||||
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)
|
||||
|
||||
def _log(self, level: str, message: str, job_name: str = None):
|
||||
@@ -120,6 +123,7 @@ class RecordingManager:
|
||||
extra_ffmpeg_args: str = "",
|
||||
segment_duration: Optional[int] = None,
|
||||
is_scheduled: bool = False,
|
||||
description: str = "",
|
||||
metadata_monitor_enabled: bool = False,
|
||||
metadata_pattern: str = "",
|
||||
metadata_grace_period: int = 300,
|
||||
@@ -186,10 +190,11 @@ class RecordingManager:
|
||||
"plik_enabled": plik_enabled,
|
||||
"delete_after_upload": delete_after_upload,
|
||||
"segment_mode": segment_mode,
|
||||
"description": description,
|
||||
}
|
||||
|
||||
self._log("INFO", f"Aufnahme gestartet: {name} [{stream_type}]", name)
|
||||
self._notify("start", "Aufnahme gestartet", f"{name} [{stream_type}]", rec_id)
|
||||
self._log("INFO", f"Aufnahme gestartet: {name}", name)
|
||||
self._notify("start", "Aufnahme gestartet", name, rec_id)
|
||||
|
||||
if metadata_monitor_enabled and metadata_pattern:
|
||||
self._get_metadata_monitor().start_monitoring(
|
||||
@@ -251,9 +256,10 @@ class RecordingManager:
|
||||
|
||||
if stop_event.is_set() or retcode == 0:
|
||||
status = "completed" if retcode == 0 else "stopped"
|
||||
status_de = "abgeschlossen" if status == "completed" else "gestoppt"
|
||||
self._update_recording(rec_id, status=status)
|
||||
self._log("INFO", f"Aufnahme {status}: {job_name}", job_name)
|
||||
self._notify("stop", f"Aufnahme {status}", job_name, rec_id)
|
||||
self._log("INFO", f"Aufnahme {status_de}: {job_name}", job_name)
|
||||
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)
|
||||
return
|
||||
|
||||
|
||||
@@ -85,12 +85,13 @@ class StreamScheduler:
|
||||
finally:
|
||||
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()
|
||||
try:
|
||||
rec = session.query(Recording).filter(
|
||||
Recording.job_id == job_id,
|
||||
Recording.status.in_(["running", "starting"]),
|
||||
Recording.is_scheduled == True,
|
||||
).first()
|
||||
return rec.id if rec else None
|
||||
finally:
|
||||
@@ -114,7 +115,8 @@ class StreamScheduler:
|
||||
"segment_duration": j.segment_duration,
|
||||
"schedule_once": j.schedule_once, "schedule_date": j.schedule_date,
|
||||
"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_pattern": j.metadata_pattern,
|
||||
"metadata_grace_period": j.metadata_grace_period,
|
||||
@@ -131,6 +133,8 @@ class StreamScheduler:
|
||||
continue
|
||||
|
||||
is_today = False
|
||||
multi_day = bool(job.get("schedule_end_date") and job["schedule_once"])
|
||||
|
||||
if job["schedule_once"]:
|
||||
if job["schedule_date"] == today_date:
|
||||
is_today = True
|
||||
@@ -139,8 +143,20 @@ class StreamScheduler:
|
||||
is_today = True
|
||||
|
||||
in_window = False
|
||||
is_cross_midnight = False
|
||||
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"])
|
||||
else:
|
||||
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"]):
|
||||
duration = job["max_duration"]
|
||||
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"])
|
||||
stop_min = _time_to_minutes(job["schedule_stop"])
|
||||
if stop_min > start_min:
|
||||
@@ -170,6 +193,7 @@ class StreamScheduler:
|
||||
extra_ffmpeg_args=job["extra_ffmpeg_args"] or "",
|
||||
segment_duration=job["segment_duration"],
|
||||
is_scheduled=True,
|
||||
description=job.get("description", ""),
|
||||
metadata_monitor_enabled=job.get("metadata_monitor_enabled", False),
|
||||
metadata_pattern=job.get("metadata_pattern", ""),
|
||||
metadata_grace_period=job.get("metadata_grace_period") or 300,
|
||||
@@ -180,8 +204,8 @@ class StreamScheduler:
|
||||
)
|
||||
self._started_keys.add(occ_key)
|
||||
else:
|
||||
if job["schedule_stop"] and self._is_job_recording(job["id"]):
|
||||
rec_id = self._get_active_recording_for_job(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_scheduled_recording_for_job(job["id"])
|
||||
if rec_id:
|
||||
self.recorder.stop_recording(rec_id)
|
||||
self._started_keys.discard(occ_key)
|
||||
|
||||
@@ -450,6 +450,10 @@
|
||||
<label>Name *</label>
|
||||
<input type="text" id="job-name" placeholder="Mein Stream">
|
||||
</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">
|
||||
<label>Typ</label>
|
||||
<select id="job-type">
|
||||
@@ -498,9 +502,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="scheduleDateGroup" style="display:none">
|
||||
<label>Datum</label>
|
||||
<label>Startdatum</label>
|
||||
<input type="date" id="job-schedule-date">
|
||||
</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">
|
||||
<label>Tage (z.B. Mo-Fr, Sa,So, *)</label>
|
||||
<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>
|
||||
</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">
|
||||
<span class="meta-item">${esc(j.stream_type || 'auto')}</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-name').value = job ? job.name : '';
|
||||
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-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) : '';
|
||||
@@ -751,6 +761,7 @@ function openJobModal(job = null) {
|
||||
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-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-start').value = job ? job.schedule_start || '' : '';
|
||||
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_start: document.getElementById('job-schedule-start').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,
|
||||
plik_enabled: document.getElementById('job-plik-enabled').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 start = document.getElementById('job-schedule-start').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;
|
||||
|
||||
document.getElementById('durationGroup').style.display = hasScheduleStop ? 'none' : '';
|
||||
document.getElementById('durationAutoGroup').style.display = hasScheduleStop ? '' : 'none';
|
||||
|
||||
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 [eh, em] = stop.split(':').map(Number);
|
||||
let diff = (eh * 60 + em) - (sh * 60 + sm);
|
||||
if (diff <= 0) diff += 1440;
|
||||
const h = Math.floor(diff / 60);
|
||||
const m = diff % 60;
|
||||
const text = h > 0 ? `${h} Std. ${m > 0 ? m + ' Min.' : ''}` : `${m} Min.`;
|
||||
document.getElementById('durationAutoText').textContent = `${text} (automatisch aus Zeitplan)`;
|
||||
text = (h > 0 ? `${h} Std. ${m > 0 ? m + ' Min.' : ''}` : `${m} Min.`) + ' (automatisch aus Zeitplan)';
|
||||
}
|
||||
document.getElementById('durationAutoText').textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleScheduleOnce() {
|
||||
const once = document.getElementById('job-schedule-once').checked;
|
||||
document.getElementById('scheduleDateGroup').style.display = once ? 'block' : 'none';
|
||||
document.getElementById('scheduleEndDateGroup').style.display = once ? 'block' : 'none';
|
||||
document.getElementById('scheduleDaysGroup').style.display = once ? 'none' : 'block';
|
||||
updateDurationField();
|
||||
}
|
||||
|
||||
function togglePlik() {
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
services:
|
||||
stream-recorder:
|
||||
build: .
|
||||
image: stream-recorder:latest
|
||||
image: git.techniverse.net/scriptos/stream-recorder:latest
|
||||
container_name: stream-recorder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
Reference in New Issue
Block a user