feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge, Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen: Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben. Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet. Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck, bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline, Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware. WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte Kompression waren seine Wahl). GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg- Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format, TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength, MakeMKV-Auswahlregel laut forum.makemkv.com t=4386. 348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche folgt im naechsten Schritt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d70a81d194
commit
3f7f962dff
@@ -0,0 +1,148 @@
|
||||
// Tests des Roh-Aufräumens (kern/ablage/rohaufraeumen.ts, 5.4.0): die
|
||||
// Regel, die Frist, die drei Sicherungen — und dass fremde Ordner nur
|
||||
// gezeigt, nie von selbst gelöscht werden.
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
aufraeumen,
|
||||
faelligAm,
|
||||
fremdeOrdner,
|
||||
istUnterRohWurzel,
|
||||
loeschHindernis,
|
||||
ordnerGroesse,
|
||||
regelLesen,
|
||||
regelText,
|
||||
type AufraeumKandidat,
|
||||
type AufraeumZugriff,
|
||||
} from '../src/kern/ablage/rohaufraeumen'
|
||||
|
||||
let wegwerf: string[] = []
|
||||
afterEach(() => {
|
||||
for (const o of wegwerf.splice(0)) rmSync(o, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('regelLesen', () => {
|
||||
it('kennt behalten, sofort und Tage — und die Vorgabe 7', () => {
|
||||
expect(regelLesen('behalten')).toEqual({ art: 'behalten' })
|
||||
expect(regelLesen('sofort')).toEqual({ art: 'sofort' })
|
||||
expect(regelLesen('30')).toEqual({ art: 'tage', tage: 30 })
|
||||
expect(regelLesen(null)).toEqual({ art: 'tage', tage: 7 })
|
||||
expect(regelLesen('')).toEqual({ art: 'tage', tage: 7 })
|
||||
expect(regelLesen('quatsch')).toEqual({ art: 'tage', tage: 7 })
|
||||
expect(regelLesen('-3')).toEqual({ art: 'tage', tage: 7 })
|
||||
expect(regelText(regelLesen('7'))).toContain('7 Tage')
|
||||
})
|
||||
})
|
||||
|
||||
describe('faelligAm', () => {
|
||||
it('Tage nach dem Ende; sofort = am Ende; behalten = nie', () => {
|
||||
expect(faelligAm('2026-09-01T20:00:00.000Z', { art: 'tage', tage: 7 })).toBe('2026-09-08T20:00:00.000Z')
|
||||
expect(faelligAm('2026-09-01T20:00:00.000Z', { art: 'sofort' })).toBe('2026-09-01T20:00:00.000Z')
|
||||
expect(faelligAm('2026-09-01T20:00:00.000Z', { art: 'behalten' })).toBe('')
|
||||
expect(faelligAm(null, { art: 'sofort' })).toBe('')
|
||||
expect(faelligAm('kaputt', { art: 'sofort' })).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
const fertigeDateien = ['E:\\Rippy\\Filme\\Akira (1988)\\title_t00.mkv']
|
||||
const kandidat = (teil: Partial<AufraeumKandidat> = {}): AufraeumKandidat => ({
|
||||
id: 'bluray-1',
|
||||
phase: 'fertig',
|
||||
komprimiert: true,
|
||||
rohOrdner: 'E:\\Rippy\\roh\\bluray-1',
|
||||
rohGeloeschtAm: null,
|
||||
beendet: '2026-09-01T20:00:00.000Z',
|
||||
fertigeDateien,
|
||||
...teil,
|
||||
})
|
||||
const zugriff = (überschreiben: Partial<AufraeumZugriff> = {}): AufraeumZugriff & { geloescht: string[] } => {
|
||||
const geloescht: string[] = []
|
||||
return {
|
||||
existiert: () => true,
|
||||
groesse: () => 1000,
|
||||
ordnerLoeschen: (p) => {
|
||||
geloescht.push(p)
|
||||
},
|
||||
geloescht,
|
||||
...überschreiben,
|
||||
}
|
||||
}
|
||||
const regel7 = { art: 'tage' as const, tage: 7 }
|
||||
const spaeter = new Date('2026-09-10T00:00:00Z')
|
||||
const frueher = new Date('2026-09-03T00:00:00Z')
|
||||
|
||||
describe('loeschHindernis — die drei Sicherungen', () => {
|
||||
it('fertig, komprimiert, Ergebnis da, Frist um: löschbar', () => {
|
||||
expect(loeschHindernis(kandidat(), regel7, spaeter, zugriff())).toBe('')
|
||||
})
|
||||
it('vor der Frist: nicht löschbar, mit Datum', () => {
|
||||
expect(loeschHindernis(kandidat(), regel7, frueher, zugriff())).toContain('Frist läuft bis 2026-09-08')
|
||||
})
|
||||
it('nicht fertig / nicht komprimiert / schon gelöscht / Regel behalten', () => {
|
||||
expect(loeschHindernis(kandidat({ phase: 'fehler' }), regel7, spaeter, zugriff())).toContain('nicht fertig')
|
||||
expect(loeschHindernis(kandidat({ komprimiert: false }), regel7, spaeter, zugriff())).toContain('nicht komprimiert')
|
||||
expect(loeschHindernis(kandidat({ rohGeloeschtAm: 'x' }), regel7, spaeter, zugriff())).toBe('schon gelöscht')
|
||||
expect(loeschHindernis(kandidat(), { art: 'behalten' }, spaeter, zugriff())).toBe('Regel: behalten')
|
||||
})
|
||||
it('die fertige Datei fehlt oder ist leer: NIE löschen', () => {
|
||||
expect(loeschHindernis(kandidat(), regel7, spaeter, zugriff({ existiert: (p) => p.endsWith('bluray-1') }))).toContain('fertige Datei fehlt')
|
||||
expect(loeschHindernis(kandidat(), regel7, spaeter, zugriff({ groesse: () => 0 }))).toContain('ist leer')
|
||||
expect(loeschHindernis(kandidat({ fertigeDateien: [] }), regel7, spaeter, zugriff())).toContain('keine fertige Datei')
|
||||
})
|
||||
it('sofort: gleich nach dem Ende löschbar', () => {
|
||||
expect(loeschHindernis(kandidat(), { art: 'sofort' }, new Date('2026-09-01T20:00:01Z'), zugriff())).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('aufraeumen', () => {
|
||||
it('löscht nur die Fälligen und sagt, warum der Rest blieb', () => {
|
||||
const z = zugriff()
|
||||
const ergebnis = aufraeumen(
|
||||
[kandidat(), kandidat({ id: 'jung', beendet: '2026-09-09T00:00:00.000Z' }), kandidat({ id: 'roh', komprimiert: false })],
|
||||
regel7,
|
||||
spaeter,
|
||||
z,
|
||||
)
|
||||
expect(ergebnis.geloescht).toEqual(['bluray-1'])
|
||||
expect(z.geloescht).toEqual(['E:\\Rippy\\roh\\bluray-1'])
|
||||
expect(ergebnis.uebersprungen.map((u) => u.id)).toEqual(['jung', 'roh'])
|
||||
expect(ergebnis.fehler).toEqual([])
|
||||
})
|
||||
it('ein Ordner, der sich nicht löschen lässt, ist ein lauter Fehler, kein stilles Nichts', () => {
|
||||
const ergebnis = aufraeumen([kandidat()], regel7, spaeter, zugriff({ ordnerLoeschen: () => { throw new Error('EBUSY') } }))
|
||||
expect(ergebnis.geloescht).toEqual([])
|
||||
expect(ergebnis.fehler[0].grund).toContain('EBUSY')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Belegung und fremde Ordner — am echten Dateisystem', () => {
|
||||
it('ordnerGroesse zählt rekursiv; fremdeOrdner kennt die Vorgänge und lässt sie aus', () => {
|
||||
const wurzel = mkdtempSync(join(tmpdir(), 'rippy-roh-'))
|
||||
wegwerf.push(wurzel)
|
||||
const roh = join(wurzel, 'roh')
|
||||
mkdirSync(join(roh, 'bluray-1', 'tief'), { recursive: true })
|
||||
mkdirSync(join(roh, 'alt-vor-540'), { recursive: true })
|
||||
writeFileSync(join(roh, 'bluray-1', 'a.mkv'), 'x'.repeat(100))
|
||||
writeFileSync(join(roh, 'bluray-1', 'tief', 'b.mkv'), 'x'.repeat(50))
|
||||
writeFileSync(join(roh, 'alt-vor-540', 'c.mkv'), 'x'.repeat(7))
|
||||
writeFileSync(join(roh, 'lose.txt'), 'x')
|
||||
expect(ordnerGroesse(join(roh, 'bluray-1'))).toBe(150)
|
||||
expect(ordnerGroesse(join(roh, 'gibt-es-nicht'))).toBe(0)
|
||||
const fremde = fremdeOrdner(roh, new Set([join(roh, 'bluray-1')]))
|
||||
expect(fremde).toEqual([{ ordner: join(roh, 'alt-vor-540'), bytes: 7 }])
|
||||
expect(fremdeOrdner(join(wurzel, 'nix'), new Set())).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('istUnterRohWurzel — nur Ordner DIREKT unter roh\\', () => {
|
||||
it('erlaubt genau eine Ebene', () => {
|
||||
expect(istUnterRohWurzel('E:\\Rippy\\roh', 'E:\\Rippy\\roh\\bluray-1')).toBe(true)
|
||||
expect(istUnterRohWurzel('E:\\Rippy\\roh\\', 'e:\\rippy\\roh\\bluray-1\\')).toBe(true)
|
||||
expect(istUnterRohWurzel('E:\\Rippy\\roh', 'E:\\Rippy\\roh')).toBe(false)
|
||||
expect(istUnterRohWurzel('E:\\Rippy\\roh', 'E:\\Rippy\\roh\\a\\b')).toBe(false)
|
||||
expect(istUnterRohWurzel('E:\\Rippy\\roh', 'E:\\Rippy\\Filme\\Akira')).toBe(false)
|
||||
expect(istUnterRohWurzel('E:\\Rippy\\roh', 'E:\\Rippy\\rohling\\x')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user