// ── Auswahl: WAS soll gerippt werden? (§ 6.2) ─────────────────────────── // Bis zum 01.09.2026 rippte Rippy IMMER alles — Trailer, Werbung und // Alterskennzeichen inklusive. Der Info-Lauf dauert 20–120 s, deshalb // beginnt er erst hier und nicht schon beim Einlegen. // 5.4.0: Sprachen je Titel, Staffel/Folge von Hand, Zielpfad-Vorschau. // 5.5.0: Platz-Prüfung vor dem Rip und der Serien-Merker (Folge von Disc 1 // zählt auf Disc 2 weiter). import { useState } from 'react' import { zielVorschau } from '../../gemeinsam/ablagepfad' import { platzUrteil } from '../../gemeinsam/platz' import type { DiscInfoStand, DiscTitelVorschlag, GeraeteInfo, SpeicherStand, SprachEintrag, TitelRolle, TitelStand, } from '../../gemeinsam/nachrichten' import { DialogRahmen, KNOPF_GOLD, dauerText, gb, gbKurz, sprachenVorbelegt } from '../bausteine' import { ripStarten } from '../kernverbindung' /** Sprach-Chips eines Titels — anklickbar, außerhalb des Auswahl-Labels * (ein Knopf IM Label würde die Checkbox mit umschalten). */ function SprachChips({ name, vorhanden, gewaehlt, aufWechsel, }: { name: string vorhanden: readonly SprachEintrag[] gewaehlt: readonly string[] aufWechsel: (lang: string) => void }) { if (vorhanden.length === 0) return null return ( {name} {vorhanden.map((s) => { const an = gewaehlt.includes(s.lang) return ( ) })} ) } export function TitelWahlDialog({ id, typ, stand, info, einstellungen, regel, speicher, schliessen, }: { id: string typ: GeraeteInfo['typ'] stand: TitelStand | undefined info: DiscInfoStand | undefined einstellungen: Record /** Klartext der Aufbewahrungs-Regel — damit klar ist, was mit dem * Roh-Material passiert. */ regel: string speicher: SpeicherStand | null schliessen: () => void }) { const [gewaehlt, setGewaehlt] = useState | null>(null) // Eigene Rollen-Wahl je Titel; was hier nicht drinsteht, behaelt Rippys // Vorschlag ('hauptfilm' beim Film, 'folge' bei der Serie, sonst 'extra'). const [rollen, setRollen] = useState>({}) // Sprachen je Titel (5.4.0): Was hier nicht drinsteht, ist vorbelegt // (Einstellung, soweit der Titel sie hat — sonst alles, was er hat). const [sprachen, setSprachen] = useState>({}) // Staffel und erste Folge von Hand (5.4.0) — die Staffel kommt von der // Disc, die erste Folge vom Serien-Merker (5.5.0: die vorige Disc dieser // Serie) oder vom Nutzer („auf Disc 2 sind 3–4"). const merker = info?.serienMerker ?? null const merkerGilt = merker !== null && (info?.discNr === null || info === undefined || (info.discNr ?? 1) > 1) const [staffel, setStaffel] = useState( info !== undefined && info.staffel !== null ? String(info.staffel) : merker !== null ? String(merker.staffel) : '', ) const [ersteFolge, setErsteFolge] = useState(merkerGilt && merker !== null ? String(merker.naechsteFolge) : '') // Rippys Vorauswahl gilt, bis der Nutzer sie anfasst. const aktiv = gewaehlt ?? new Set((stand?.titel ?? []).filter((t) => t.gewaehlt).map((t) => t.nr)) const rolleVon = (t: DiscTitelVorschlag): TitelRolle => rollen[t.nr] ?? t.rolle const sprachenVon = (t: DiscTitelVorschlag): { audio: string[]; untertitel: string[] } => sprachen[t.nr] ?? { audio: sprachenVorbelegt(t.sprachen.audio, einstellungen['audioSprachen'] ?? ''), untertitel: sprachenVorbelegt(t.sprachen.untertitel, einstellungen['untertitelSprachen'] ?? ''), } const sprachWechsel = (t: DiscTitelVorschlag, art: 'audio' | 'untertitel', lang: string): void => { const jetzt = sprachenVon(t) const liste = jetzt[art].includes(lang) ? jetzt[art].filter((l) => l !== lang) : [...jetzt[art], lang] setSprachen({ ...sprachen, [t.nr]: { ...jetzt, [art]: liste } }) } const umschalten = (nr: number): void => { const neu = new Set(aktiv) if (neu.has(nr)) neu.delete(nr) else neu.add(nr) setGewaehlt(neu) } const alle = stand?.titel ?? [] const gewaehlteTitel = alle.filter((t) => aktiv.has(t.nr)) const gesamtBytes = gewaehlteTitel.reduce((s, t) => s + t.groesseBytes, 0) const alsSerie = gewaehlteTitel.some((t) => rolleVon(t) === 'folge') const hauptTitel = gewaehlteTitel.filter((t) => rolleVon(t) !== 'extra').length const extras = gewaehlteTitel.filter((t) => rolleVon(t) === 'extra').length const zahlOderNull = (text: string, mindestens: number): number | null => { const n = Number(text.trim()) return text.trim().length > 0 && Number.isInteger(n) && n >= mindestens ? n : null } const staffelZahl = zahlOderNull(staffel, 0) const folgeZahl = zahlOderNull(ersteFolge, 1) const vorschau = zielVorschau({ ablage: einstellungen['ablage'] ?? 'Videos\\Rippy', alsSerie, titel: info?.titel ?? '', jahr: info?.jahr ?? null, staffel: staffelZahl, ersteFolge: folgeZahl, hauptTitel, extras, vorgangsName: `${typ}-`, }) // Platz-Prüfung (5.5.0): gegen den Arbeitsordner — dort landen Roh und Encode. const komprimiert = einstellungen['transcodeEnabled'] !== 'false' const platz = speicher !== null && speicher.arbeit.ok ? platzUrteil(speicher.arbeit.freiBytes, gesamtBytes, komprimiert) : { text: '', hart: false } return (

Was soll gerippt werden?

{stand === undefined || stand.laeuft ? (

Rippy liest die Titel der Disc … das dauert bis zu zwei Minuten. Solange dreht das Laufwerk.

) : stand.fehler.length > 0 ? (

✗ {stand.fehler}

) : ( <>

{stand.unsicher ? '⚠ ' : ''} {stand.einordnung}

{alle.map((t) => { const an = aktiv.has(t.nr) const wahl = sprachenVon(t) return (
{/* Sprachen je Titel (5.4.0): Was der Info-Lauf an DIESEM Titel gemessen hat — anhaken, was in die Datei soll. Der Commander-Fall: japanischer Ton, deutsche Untertitel. */} {an && (t.sprachen.audio.length > 0 || t.sprachen.untertitel.length > 0) && (
sprachWechsel(t, 'audio', lang)} /> sprachWechsel(t, 'untertitel', lang)} /> {wahl.audio.length === 0 && t.sprachen.audio.length > 0 && ( kein Ton gewählt — HandBrake nimmt die erste Spur )}
)}
) })} {alle.length === 0 &&

Die Disc gab keine Titel her.

}
{alsSerie && (

{merkerGilt && merker !== null ? `Serien-Merker: Die vorige Disc dieser Serie endete mit Folge ${merker.naechsteFolge - 1} (${merker.wann.slice(0, 10)}) — Rippy zählt ab ${merker.naechsteFolge} weiter. Stimmt das nicht, ändere die Zahl.` : 'Leer heißt: Rippy ordnet die Folgen über die TMDb-Laufzeiten zu, und nur, wenn es eindeutig ist. Weißt du es besser („auf Disc 2 sind die Folgen 3 und 4"), trag es hier ein — dann zählt Rippy fortlaufend in Disc-Reihenfolge.'} {info !== undefined && info.discNr !== null && ` Die Disc nennt sich selbst Disc ${info.discNr}.`}

)} {/* Vorschau (5.4.0): Rolle, Serie, Staffel und Extras entscheiden einiges — zwei Zeilen Zielpfad ersparen Überraschungen. */}

LANDET IN

{vorschau[0]}

{vorschau[1]}

{/* Platz-Prüfung (5.5.0): 40 Minuten Lesen sollen nicht an einem vollen Laufwerk scheitern. */} {speicher !== null && speicher.arbeit.ok && (

0 ? 'text-warn' : 'text-nebel'}`}> {platz.text.length > 0 ? `${platz.hart ? '✗' : '⚠'} ${platz.text}` : `Platz: ${gbKurz(speicher.arbeit.freiBytes)} frei im Arbeitsordner — reicht für ${gb(gesamtBytes)} verlustfrei${komprimiert ? ' plus Kompression' : ''}.`}

)}

{aktiv.size} von {alle.length} gewählt · zusammen {gb(gesamtBytes)} verlustfrei {regel.length > 0 ? ` · ${regel}` : ''}

{alsSerie ? 'Folgen landen unter Serien/… /Season NN. Ohne Vorgabe setzt Rippy die Episoden-Nummern nur, wenn die Laufzeiten eindeutig passen — sonst behalten die Dateien ihre Namen.' : 'Der Hauptfilm landet in seinem eigenen Ordner, Extras daneben im Unterordner „extras" — so lesen Jellyfin, Emby und Kodi sie als Zugaben.'}{' '} Das Laufwerk ist frei, sobald die Roh-Dateien liegen — die Kompression läuft danach im Hintergrund.

)}
) }