- Versionsanzeige für den Windows-Worker im UI inkl. Prüfung - Absicherung der install.sh gegen fehlendes systemd - Serien-Episoden-Erkennung und Laufzeitabgleich anhand TMDB-Daten (Heuristik) - Umstellung der Windows-Worker-Installation auf SchTasks (Dienst-Ersatz) - Kodi-Bibliotheks-Refresh über JSON-RPC integriert
This commit is contained in:
@@ -91,6 +91,10 @@ else
|
||||
echo " keine API-Schluessel). RIPPY_ENV_SRC=<pfad> setzen." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i '/^RIPPY_VERSION=/d' .env
|
||||
echo "RIPPY_VERSION=$(git rev-parse --short HEAD)" >> .env
|
||||
|
||||
docker compose -p rippy up -d --build $DIENST
|
||||
docker compose -p rippy ps --format 'table {{.Name}}\t{{.Status}}'
|
||||
REMOTE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime).
|
||||
# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime).
|
||||
# Wird über rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur
|
||||
# Python 3.10+ auf der Maschine - kein Docker, kein git.
|
||||
#
|
||||
@@ -606,61 +606,31 @@ Start-Process cmd.exe -ArgumentList '/c timeout /t 4 /nobreak >nul & rd /s /q "'
|
||||
(Join-Path $InstallDir "uninstall.ps1"), $uninstall,
|
||||
(New-Object System.Text.UTF8Encoding $true))
|
||||
|
||||
# 7. Autostart über den Autostart-ORDNER, nicht über schtasks.
|
||||
# 7. Autostart über den Task Scheduler (als Dienst-Ersatz).
|
||||
#
|
||||
# Befund 25.07.2026 (auf dem Commander-PC aufgetreten): schtasks /create
|
||||
# mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER der
|
||||
# Aufgabenplanung an, und das verlangt Administratorrechte. Der
|
||||
# Installer läuft normal ohne. schtasks schrieb deshalb
|
||||
# "FEHLER: Zugriff verweigert." nach stderr, und weil oben
|
||||
# $ErrorActionPreference = "Stop" steht, wurde daraus ein Abbruch der
|
||||
# GESAMTEN Installation - obwohl alles außer dem Autostart fertig war.
|
||||
# Der Nutzer sah nur "FEHLER: FEHLER: Zugriff verweigert" samt dem
|
||||
# irreführenden Tipp, die IP könne falsch sein.
|
||||
#
|
||||
# Der Autostart-Ordner braucht NIE Adminrechte, und der Nutzer kann die
|
||||
# Verknüpfung dort selbst sehen und löschen. Zwei Fliegen.
|
||||
# Commander-Wunsch 26.07.2026: SchTasks.exe statt Startmenü-Verknüpfung.
|
||||
# Da der Installer jetzt zwingend mit Administratorrechten läuft (für
|
||||
# das "Programme"-Verzeichnis), funktioniert schtasks problemlos.
|
||||
# /RL HIGHEST verhindert UAC-Popups beim automatischen Start.
|
||||
# /SC ONLOGON startet den Worker bei der Benutzeranmeldung — das ist
|
||||
# wichtig, damit er Zugriff auf die eingehängten Netzwerklaufwerke
|
||||
# des Benutzers hat (der SYSTEM-Account hätte das bei ONSTART nicht).
|
||||
if ($chkAuto.Checked) {
|
||||
try {
|
||||
# Liegt das Programm unter "Programme", ist es eine Installation
|
||||
# für die ganze Maschine - dann gehört der Autostart in den
|
||||
# Autostart-Ordner ALLER Benutzer. Das passt auch zur Rechtelage:
|
||||
# der Installer läuft dafür ohnehin erhöht, und bei einer
|
||||
# Erhöhung über ein FREMDES Administratorkonto wäre der
|
||||
# benutzereigene Ordner der des Admins - also der falsche.
|
||||
# Bei einer Installation ins eigene Profil bleibt es persönlich.
|
||||
$programme = [Environment]::GetFolderPath("ProgramFiles")
|
||||
$programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86")
|
||||
$maschinenweit = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") `
|
||||
-or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase"))
|
||||
$autostartDir = if ($maschinenweit) {
|
||||
[Environment]::GetFolderPath("CommonStartup")
|
||||
# Der Pfad zur .bat muss in Anführungszeichen, falls Leerzeichen drin sind.
|
||||
# schtasks erwartet diese Escaped-Anführungszeichen: \"C:\Pfad\...\"
|
||||
$aktion = "\`"$InstallDir\start-tray.bat\`""
|
||||
$process = Start-Process schtasks -ArgumentList "/create /tn `"RippyWorker`" /tr $aktion /sc ONLOGON /rl HIGHEST /f" -NoNewWindow -Wait -PassThru
|
||||
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Log "Autostart als Aufgabe (Task Scheduler) eingerichtet (startet bei Anmeldung)."
|
||||
} else {
|
||||
[Environment]::GetFolderPath("Startup")
|
||||
}
|
||||
$lnkPfad = Join-Path $autostartDir "RippyWorker.lnk"
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
$lnk = $wsh.CreateShortcut($lnkPfad)
|
||||
$lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat")
|
||||
$lnk.WorkingDirectory = $InstallDir
|
||||
$lnk.Description = "Rippy Encoding-Worker"
|
||||
# WindowStyle 7 = minimiert. start-tray.bat startet pythonw (kein
|
||||
# Konsolenfenster), aber die .bat selbst blitzt sonst kurz auf.
|
||||
$lnk.WindowStyle = 7
|
||||
$ico = Join-Path $InstallDir "rippy.ico"
|
||||
if (Test-Path $ico) { $lnk.IconLocation = $ico }
|
||||
$lnk.Save()
|
||||
if ($maschinenweit) {
|
||||
Log "Autostart eingerichtet (für alle Benutzer dieser Maschine)."
|
||||
} else {
|
||||
Log "Autostart eingerichtet (für dich, startet bei der Anmeldung)."
|
||||
throw "ExitCode $($process.ExitCode)"
|
||||
}
|
||||
} catch {
|
||||
# Nur eine Warnung: der Worker selbst ist fertig und startbar.
|
||||
Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))."
|
||||
Log "HINWEIS: Autostart-Aufgabe konnte nicht eingerichtet werden ($_)."
|
||||
Log " Der Worker ist trotzdem fertig installiert und läuft."
|
||||
Log " Von Hand: start-tray.bat in den Autostart-Ordner verknüpfen"
|
||||
Log " (Windows-Taste + R, dann shell:startup eingeben)."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ services:
|
||||
# hochgeladene KEYDB.cfg entgegen (Einstellungen → System).
|
||||
- MAKEMKV_DATA_DIR=/app/makemkv-data
|
||||
- LOG_LEVEL=INFO
|
||||
- RIPPY_VERSION=${RIPPY_VERSION:-dev}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\")'"]
|
||||
interval: 30s
|
||||
@@ -102,6 +103,7 @@ services:
|
||||
# Episoden-Laufzeiten der Staffel (TMDB).
|
||||
- API_URL=http://api:8000
|
||||
- LOG_LEVEL=INFO
|
||||
- RIPPY_VERSION=${RIPPY_VERSION:-dev}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /srv/rippy/media
|
||||
|
||||
+46
-4
@@ -777,6 +777,18 @@ async def create_job(request: JobCreateRequest):
|
||||
if request.series and request.series.strip():
|
||||
meta_dict["series"] = request.series.strip()
|
||||
meta_dict["season"] = max(1, int(request.season or 1))
|
||||
# TMDB-Laufzeiten der Episoden holen (für die Erkennung im Worker)
|
||||
if meta_dict.get("id") and meta_dict.get("type") == "tv":
|
||||
try:
|
||||
from clients.tmdb import TMDBClient
|
||||
staffel = TMDBClient().get_tv_season(meta_dict["id"], meta_dict["season"])
|
||||
if staffel and "episodes" in staffel:
|
||||
meta_dict["episode_runtimes"] = [
|
||||
ep.get("runtime") for ep in staffel["episodes"] if ep.get("runtime")
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Holen der Staffel-Laufzeiten: {e}")
|
||||
|
||||
if request.main_feature_only is not None:
|
||||
meta_dict["main_feature_only"] = request.main_feature_only
|
||||
if request.titles:
|
||||
@@ -1230,7 +1242,10 @@ async def capabilities():
|
||||
zeile["node"] = node
|
||||
return zeilen
|
||||
|
||||
return {"workers": await asyncio.to_thread(sammle)}
|
||||
return {
|
||||
"workers": await asyncio.to_thread(sammle),
|
||||
"server_version": os.getenv("RIPPY_VERSION", "dev")
|
||||
}
|
||||
|
||||
|
||||
@app.get("/presets")
|
||||
@@ -1291,8 +1306,8 @@ class MediaServerRefreshRequest(BaseModel):
|
||||
|
||||
@app.post("/mediaserver/refresh")
|
||||
async def mediaserver_refresh(request: MediaServerRefreshRequest):
|
||||
"""Bibliotheks-Scan von Jellyfin/Emby anstoßen — auch als Verbindungs-Test
|
||||
aus den Einstellungen (POST /Library/Refresh, Header X-Emby-Token)."""
|
||||
"""Bibliotheks-Scan von Jellyfin/Emby/Kodi anstoßen — auch als Verbindungs-Test
|
||||
aus den Einstellungen."""
|
||||
import requests as _requests
|
||||
|
||||
url = request.url.strip().rstrip("/")
|
||||
@@ -1300,6 +1315,24 @@ async def mediaserver_refresh(request: MediaServerRefreshRequest):
|
||||
raise HTTPException(status_code=422, detail="Server-URL muss mit http(s):// beginnen")
|
||||
|
||||
def anstossen():
|
||||
# Fallback für die reine API-Test-Funktion, die den Server-Typ nicht kennt:
|
||||
# Wir versuchen Kodi (JSON-RPC) und danach Emby/Jellyfin.
|
||||
# Wenn der User Kodi auswählt, hat er oft Basic Auth konfiguriert.
|
||||
auth = None
|
||||
if ":" in request.api_key:
|
||||
user, pw = request.api_key.split(":", 1)
|
||||
auth = (user, pw)
|
||||
|
||||
try:
|
||||
# Kodi JSON-RPC
|
||||
payload = {"jsonrpc": "2.0", "method": "VideoLibrary.Scan", "id": 1}
|
||||
res = _requests.post(url + "/jsonrpc", json=payload, auth=auth, timeout=5)
|
||||
if res.status_code < 300:
|
||||
return res
|
||||
except:
|
||||
pass
|
||||
|
||||
# Emby / Jellyfin
|
||||
return _requests.post(
|
||||
url + "/Library/Refresh",
|
||||
headers={"X-Emby-Token": request.api_key.strip()}, timeout=15,
|
||||
@@ -1748,9 +1781,12 @@ async def worker_setup_paket():
|
||||
if name.endswith(".py") and not name.startswith(("test_", "conftest")):
|
||||
z.write(os.path.join("worker_dist", name), name)
|
||||
# requirements.txt für die venv, rippy.ico für die Verknüpfung
|
||||
# auf dem Desktop (sonst trägt sie das Batch-Standardsymbol).
|
||||
elif name in ("requirements.txt", "rippy.ico"):
|
||||
z.write(os.path.join("worker_dist", name), name)
|
||||
|
||||
# Write RIPPY_VERSION to version.txt
|
||||
version_str = os.getenv("RIPPY_VERSION", "dev")
|
||||
z.writestr("version.txt", version_str)
|
||||
return puffer.getvalue()
|
||||
|
||||
inhalt = await asyncio.to_thread(baue)
|
||||
@@ -1762,6 +1798,12 @@ async def worker_setup_paket():
|
||||
)
|
||||
|
||||
|
||||
@app.get("/system/version")
|
||||
async def system_version():
|
||||
"""Gibt die aktuelle Rippy-Version des Servers zurück (aus deploy.sh)."""
|
||||
return {"version": os.getenv("RIPPY_VERSION", "dev")}
|
||||
|
||||
|
||||
@app.get("/system/info")
|
||||
async def system_info():
|
||||
"""System-Selbstauskunft (Einstellungen → System): Werkzeug-Versionen der
|
||||
|
||||
@@ -21,6 +21,7 @@ interface WorkerInfo {
|
||||
cpu_kerne?: string
|
||||
cpu_simd?: string
|
||||
handbrake_encoder?: string
|
||||
rippy_version?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ function relativeZeit(iso?: string): string {
|
||||
|
||||
export default function WorkerVerwaltung() {
|
||||
const [workers, setWorkers] = useState<WorkerInfo[]>([])
|
||||
const [serverVersion, setServerVersion] = useState<string>('dev')
|
||||
const [kopiert, setKopiert] = useState(false)
|
||||
const [variante, setVariante] = useState<'linux' | 'windows'>('linux')
|
||||
// BEWUSST leer: kein window.location.hostname-Default. Der füllte früher
|
||||
@@ -68,6 +70,7 @@ export default function WorkerVerwaltung() {
|
||||
api.get('/capabilities')
|
||||
.then(r => {
|
||||
setWorkers(r.data.workers || [])
|
||||
if (r.data.server_version) setServerVersion(r.data.server_version)
|
||||
if (manuell) toast('success', 'Worker-Liste aktualisiert')
|
||||
})
|
||||
.catch(() => { if (manuell) toast('error', 'Worker-Liste konnte nicht geladen werden') })
|
||||
@@ -146,6 +149,11 @@ export default function WorkerVerwaltung() {
|
||||
{w.info?.ip ? ` · IP ${w.info.ip}` : ''}
|
||||
{w.info?.hostname && w.info.hostname !== w.name ? ` · ID ${w.info.hostname}` : ''}
|
||||
</p>
|
||||
{w.info?.rippy_version && (
|
||||
<p className={`text-xs ${w.info.rippy_version !== serverVersion ? 'text-rose-500 font-semibold' : 'text-slate-500 dark:text-slate-400'}`}>
|
||||
Version {w.info.rippy_version} {w.info.rippy_version !== serverVersion ? `(Veraltet! Server hat ${serverVersion})` : ''}
|
||||
</p>
|
||||
)}
|
||||
{(w.info?.cpu_kerne || w.info?.cpu_simd) && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400" title={w.info?.cpu_modell || ''}>
|
||||
{w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''}
|
||||
|
||||
@@ -401,4 +401,16 @@ def werkzeug_versionen() -> dict:
|
||||
info["schluessel"] = str(makemkv_daten.schluesselspeicher_status().get("schluessel", 0))
|
||||
except Exception:
|
||||
info["schluessel"] = "unbekannt"
|
||||
|
||||
# Version für den Windows-Worker (aus version.txt) oder Linux-Worker (aus Env)
|
||||
try:
|
||||
ver_pfad = os.path.join(os.path.dirname(__file__), "version.txt")
|
||||
if os.path.isfile(ver_pfad):
|
||||
with open(ver_pfad, "r", encoding="utf-8") as f:
|
||||
info["rippy_version"] = f.read().strip()
|
||||
else:
|
||||
info["rippy_version"] = os.getenv("RIPPY_VERSION", "dev")
|
||||
except Exception:
|
||||
info["rippy_version"] = "unbekannt"
|
||||
|
||||
return info
|
||||
|
||||
+30
-11
@@ -140,24 +140,43 @@ def episoden_umbenennen(ordner: str, serie: str, staffel: int, zuordnung: list)
|
||||
|
||||
|
||||
def bibliothek_refresh(media_server: str, url: str, api_key: str) -> str:
|
||||
"""Stößt den Bibliotheks-Scan des Media-Servers an (Jellyfin/Emby).
|
||||
"""Stößt den Bibliotheks-Scan des Media-Servers an (Jellyfin/Emby/Kodi).
|
||||
|
||||
API laut jellyfin.org/docs (Emby-kompatibel): POST /Library/Refresh,
|
||||
Auth über den Header X-Emby-Token. Rückgabe: Klartext fürs Log.
|
||||
Jellyfin/Emby: POST /Library/Refresh, Auth über X-Emby-Token.
|
||||
Kodi: POST /jsonrpc, VideoLibrary.Scan, Auth über Basic Auth (api_key = user:pass).
|
||||
Wirft nie — der Refresh ist Komfort, nicht Teil des Rips.
|
||||
"""
|
||||
if media_server not in ("jellyfin", "emby") or not url or not api_key:
|
||||
if not url or media_server not in ("jellyfin", "emby", "kodi"):
|
||||
return ""
|
||||
try:
|
||||
antwort = requests.post(
|
||||
url.rstrip("/") + "/Library/Refresh",
|
||||
headers={"X-Emby-Token": api_key}, timeout=15,
|
||||
)
|
||||
if antwort.status_code < 300:
|
||||
return f"{media_server}-Bibliothek aktualisiert ({url})"
|
||||
return f"Bibliotheks-Refresh: HTTP {antwort.status_code} von {url}"
|
||||
if media_server in ("jellyfin", "emby") and api_key:
|
||||
antwort = requests.post(
|
||||
url.rstrip("/") + "/Library/Refresh",
|
||||
headers={"X-Emby-Token": api_key}, timeout=15,
|
||||
)
|
||||
if antwort.status_code < 300:
|
||||
return f"{media_server.title()}-Bibliothek aktualisiert ({url})"
|
||||
return f"Bibliotheks-Refresh: HTTP {antwort.status_code} von {url}"
|
||||
|
||||
elif media_server == "kodi":
|
||||
auth = None
|
||||
if api_key and ":" in api_key:
|
||||
user, pw = api_key.split(":", 1)
|
||||
auth = (user, pw)
|
||||
|
||||
payload = {"jsonrpc": "2.0", "method": "VideoLibrary.Scan", "id": 1}
|
||||
antwort = requests.post(
|
||||
url.rstrip("/") + "/jsonrpc",
|
||||
json=payload, auth=auth, timeout=15,
|
||||
)
|
||||
if antwort.status_code < 300:
|
||||
return f"Kodi-Bibliothek aktualisiert ({url})"
|
||||
return f"Kodi-Refresh: HTTP {antwort.status_code} von {url}"
|
||||
|
||||
except requests.RequestException as e:
|
||||
return f"Bibliotheks-Refresh fehlgeschlagen: {e}"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def baue_nfo(meta: dict, titel: str, jahr=None) -> str:
|
||||
|
||||
@@ -348,13 +348,45 @@ def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
|
||||
return {"status": "success", "output_dir": output_dir, "files": alle_dateien}
|
||||
|
||||
|
||||
def laengster_titel(dauern: dict):
|
||||
"""Titel-Nummer mit der längsten Laufzeit — der Hauptfilm (pure Funktion)."""
|
||||
def laengster_titel(dauern: dict, meta: dict = None):
|
||||
"""Titel-Nummer für den Hauptfilm (pure Funktion).
|
||||
|
||||
Mit meta['runtime'] wird heuristisch der Titel gesucht, dessen Laufzeit am nächsten
|
||||
an der TMDB-Laufzeit liegt (vermeidet 'Play All'-Tracks, die länger als der Film sind).
|
||||
Ohne runtime wird klassisch der längste Titel gewählt.
|
||||
"""
|
||||
if not dauern:
|
||||
return None
|
||||
|
||||
ziel_s = (meta.get("runtime") or 0) * 60 if meta else 0
|
||||
if ziel_s > 0:
|
||||
bester = min(dauern, key=lambda t: abs(dauern[t] - ziel_s))
|
||||
if abs(dauern[bester] - ziel_s) <= 20 * 60:
|
||||
return bester
|
||||
|
||||
return max(dauern, key=dauern.get)
|
||||
|
||||
|
||||
def episoden_titel(dauern: dict, meta: dict) -> list:
|
||||
"""Findet Episoden-Titel anhand der TMDB-Episoden-Laufzeiten.
|
||||
|
||||
Toleranz: +/- 10 Minuten (600 Sekunden) zum Durchschnitt.
|
||||
Gibt eine sortierte Liste der gefundenen Titelnummern zurück.
|
||||
"""
|
||||
if not dauern or not meta or not meta.get("episode_runtimes"):
|
||||
return []
|
||||
|
||||
runtimes_s = [r * 60 for r in meta["episode_runtimes"]]
|
||||
avg_runtime = sum(runtimes_s) / len(runtimes_s)
|
||||
|
||||
gefundene_titel = []
|
||||
for titel, dauer in dauern.items():
|
||||
if abs(dauer - avg_runtime) <= 10 * 60:
|
||||
gefundene_titel.append(int(titel))
|
||||
|
||||
return sorted(gefundene_titel)
|
||||
|
||||
|
||||
def lies_titel_dauern(device_path: str, timeout: int = 300) -> dict:
|
||||
"""Fragt die Titel-Laufzeiten der Disc ab (makemkvcon info, Robot-Mode)."""
|
||||
try:
|
||||
@@ -832,15 +864,14 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
|
||||
|
||||
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
|
||||
output_dir: str = None, nur_hauptfilm: bool = False,
|
||||
titel_liste: list = None, log_cb=None) -> dict:
|
||||
titel_liste: list = None, log_cb=None, meta: dict = None) -> dict:
|
||||
"""Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
|
||||
|
||||
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc,
|
||||
der hier mit seinem eigenen Fortschritts-Callback durchgreift. output_dir
|
||||
überschreibt das Ziel (Transcode-Fall: Roh-Rip nach /app/temp).
|
||||
titel_liste: exakte Titel-Auswahl aus dem UI (schlägt nur_hauptfilm).
|
||||
nur_hauptfilm: Vorab-Info-Lauf, dann nur den LÄNGSTEN Titel rippen —
|
||||
das löst das eingestellte (vorher wirkungslose) mainFeatureOnly ein.
|
||||
nur_hauptfilm: Vorab-Info-Lauf, dann heuristisch den besten Titel rippen.
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = os.path.join(RIP_OUTPUT_DIR, disc_type, disc_id)
|
||||
@@ -849,10 +880,18 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb, log_cb)
|
||||
|
||||
# Serien-Erkennung: Wenn keine Titel explizit gewählt wurden und es eine Serie ist
|
||||
if meta and meta.get("series") and meta.get("episode_runtimes"):
|
||||
dauern = lies_titel_dauern(device_path)
|
||||
episoden = episoden_titel(dauern, meta)
|
||||
if episoden:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
return rip_titel_auswahl(device_path, output_dir, episoden, progress_cb, log_cb)
|
||||
|
||||
titel = "all"
|
||||
if nur_hauptfilm:
|
||||
dauern = lies_titel_dauern(device_path)
|
||||
haupt = laengster_titel(dauern)
|
||||
haupt = laengster_titel(dauern, meta)
|
||||
if haupt is not None:
|
||||
titel = str(haupt)
|
||||
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten
|
||||
|
||||
+14
-9
@@ -333,9 +333,10 @@ elif [ "$PROPAGATION_NOETIG" = 1 ]; then
|
||||
ok "Propagation eingerichtet"
|
||||
# Neustart-fest machen, sonst ist es nach dem nächsten Reboot wieder weg
|
||||
# und das Einhängen aus dem UI scheitert stillschweigend.
|
||||
UNIT="/etc/systemd/system/$(systemd-escape -p --suffix=mount "$MEDIA_DIR" 2>/dev/null)"
|
||||
if [ -n "${UNIT:-}" ] && [ ! -e "$UNIT" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
cat > "$UNIT" <<UNITEOF
|
||||
if command -v systemd-escape >/dev/null 2>&1 && command -v systemctl >/dev/null 2>&1; then
|
||||
UNIT="/etc/systemd/system/$(systemd-escape -p --suffix=mount "$MEDIA_DIR")"
|
||||
if [ ! -e "$UNIT" ]; then
|
||||
cat > "$UNIT" <<UNITEOF
|
||||
[Unit]
|
||||
Description=Rippy-Ablage als shared mount (für NAS-Freigaben aus dem UI)
|
||||
After=local-fs.target
|
||||
@@ -349,13 +350,17 @@ Options=bind,rshared
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNITEOF
|
||||
systemctl daemon-reload 2>/dev/null
|
||||
if systemctl enable "$(basename "$UNIT")" >/dev/null 2>&1; then
|
||||
ok "neustart-fest gemacht ($(basename "$UNIT"))"
|
||||
else
|
||||
gelb " ! Konnte nicht neustart-fest gemacht werden — nach einem Reboot"
|
||||
gelb " dieses Skript einfach erneut ausführen."
|
||||
systemctl daemon-reload 2>/dev/null
|
||||
if systemctl enable "$(basename "$UNIT")" >/dev/null 2>&1; then
|
||||
ok "neustart-fest gemacht ($(basename "$UNIT"))"
|
||||
else
|
||||
gelb " ! Konnte nicht neustart-fest gemacht werden — nach einem Reboot"
|
||||
gelb " dieses Skript einfach erneut ausführen."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
gelb " ! Kein systemd gefunden — Mount-Propagation ist nicht neustart-fest."
|
||||
gelb " Nach einem Reboot dieses Skript einfach erneut ausführen."
|
||||
fi
|
||||
else
|
||||
gelb " ! Propagation ließ sich nicht einrichten."
|
||||
|
||||
Reference in New Issue
Block a user