updated
This commit is contained in:
parent
cf86cdd868
commit
1775cb5acd
@ -1310,10 +1310,12 @@ func postworkAudioSidecarCandidatesForOutput(out string) []string {
|
|||||||
|
|
||||||
// Explizite Fallbacks, falls canonicalAssetIDFromName anders normalisiert.
|
// Explizite Fallbacks, falls canonicalAssetIDFromName anders normalisiert.
|
||||||
if base != "" {
|
if base != "" {
|
||||||
|
candidates = append(candidates, filepath.Join(dir, base+".audio.m4s"))
|
||||||
candidates = append(candidates, filepath.Join(dir, "."+base+".audio.m4s"))
|
candidates = append(candidates, filepath.Join(dir, "."+base+".audio.m4s"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if canonical := strings.TrimSpace(canonicalAssetIDFromName(base)); canonical != "" {
|
if canonical := strings.TrimSpace(canonicalAssetIDFromName(base)); canonical != "" {
|
||||||
|
candidates = append(candidates, filepath.Join(dir, canonical+".audio.m4s"))
|
||||||
candidates = append(candidates, filepath.Join(dir, "."+canonical+".audio.m4s"))
|
candidates = append(candidates, filepath.Join(dir, "."+canonical+".audio.m4s"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -747,10 +747,19 @@ func findPostworkAudioSidecarForTS(tsPath string) (string, bool) {
|
|||||||
|
|
||||||
dir := filepath.Dir(tsPath)
|
dir := filepath.Dir(tsPath)
|
||||||
base := strings.TrimSuffix(filepath.Base(tsPath), filepath.Ext(tsPath))
|
base := strings.TrimSuffix(filepath.Base(tsPath), filepath.Ext(tsPath))
|
||||||
candidates = append(candidates,
|
if base != "" {
|
||||||
filepath.Join(dir, "."+base+".audio.m4s"),
|
candidates = append(candidates,
|
||||||
filepath.Join(dir, "."+canonicalAssetIDFromName(base)+".audio.m4s"),
|
filepath.Join(dir, base+".audio.m4s"),
|
||||||
)
|
filepath.Join(dir, "."+base+".audio.m4s"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if canonical := strings.TrimSpace(canonicalAssetIDFromName(base)); canonical != "" {
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.Join(dir, canonical+".audio.m4s"),
|
||||||
|
filepath.Join(dir, "."+canonical+".audio.m4s"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
"ffmpegPath": "",
|
"ffmpegPath": "",
|
||||||
"autoAddToDownloadList": true,
|
"autoAddToDownloadList": true,
|
||||||
"autoStartAddedDownloads": true,
|
"autoStartAddedDownloads": true,
|
||||||
"enableConcurrentDownloadsLimit": true,
|
"enableConcurrentDownloadsLimit": false,
|
||||||
"maxConcurrentDownloads": 80,
|
"maxConcurrentDownloads": 80,
|
||||||
"useProviderApis": true,
|
"useProviderApis": true,
|
||||||
"useChaturbateApi": true,
|
"useChaturbateApi": true,
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -29,9 +31,45 @@ func startPostworkLeftoverScanOnStartup() {
|
|||||||
time.Sleep(postworkLeftoverInitialDelay)
|
time.Sleep(postworkLeftoverInitialDelay)
|
||||||
|
|
||||||
runPostworkLeftoverScan := func(reason string) {
|
runPostworkLeftoverScan := func(reason string) {
|
||||||
removedMuxing, skippedMuxing := cleanupStaleMuxingTempFilesFromRecordDir()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
jobID := newTaskID(postworkLeftoverTaskPrefix(reason))
|
||||||
|
createCleanupJob(jobID, "Wartet…", cancel)
|
||||||
|
|
||||||
|
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
||||||
|
st.Queued = false
|
||||||
|
st.Running = true
|
||||||
|
st.StartedAt = time.Now()
|
||||||
|
st.Done = 0
|
||||||
|
st.Total = 2
|
||||||
|
st.CurrentFile = ""
|
||||||
|
st.Text = postworkLeftoverRunningText(reason)
|
||||||
|
st.Error = ""
|
||||||
|
st.FinishedAt = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
removedMuxing, skippedMuxing, err := cleanupStaleMuxingTempFilesFromRecordDir(ctx)
|
||||||
|
if err != nil {
|
||||||
|
finishCleanupJob(jobID, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
||||||
|
st.Done = 1
|
||||||
|
st.Total = 2
|
||||||
|
st.Text = "Suche offene Postwork-Dateien…"
|
||||||
|
st.CurrentFile = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
queued, skipped, err := enqueuePostworkLeftoversFromRecordDir(ctx)
|
||||||
|
if err != nil {
|
||||||
|
finishCleanupJob(jobID, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := postworkLeftoverScanSummary(queued, skipped, removedMuxing, skippedMuxing)
|
||||||
|
|
||||||
queued, skipped := enqueuePostworkLeftoversFromRecordDir()
|
|
||||||
if queued > 0 || skipped > 0 || removedMuxing > 0 || skippedMuxing > 0 {
|
if queued > 0 || skipped > 0 || removedMuxing > 0 || skippedMuxing > 0 {
|
||||||
appLogln(
|
appLogln(
|
||||||
"🧩 [startup-postwork] leftover scan fertig:",
|
"🧩 [startup-postwork] leftover scan fertig:",
|
||||||
@ -42,6 +80,14 @@ func startPostworkLeftoverScanOnStartup() {
|
|||||||
"skippedMuxing=", skippedMuxing,
|
"skippedMuxing=", skippedMuxing,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
||||||
|
st.Done = 2
|
||||||
|
st.Total = 2
|
||||||
|
st.Text = summary
|
||||||
|
st.CurrentFile = ""
|
||||||
|
})
|
||||||
|
finishCleanupJob(jobID, summary, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
runPostworkLeftoverScan("startup")
|
runPostworkLeftoverScan("startup")
|
||||||
@ -55,7 +101,72 @@ func startPostworkLeftoverScanOnStartup() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupStaleMuxingTempFilesFromRecordDir() (removed int, skipped int) {
|
func postworkLeftoverTaskPrefix(reason string) string {
|
||||||
|
switch strings.TrimSpace(strings.ToLower(reason)) {
|
||||||
|
case "startup":
|
||||||
|
return "cleanup-leftovers-startup"
|
||||||
|
case "interval":
|
||||||
|
return "cleanup-leftovers-hourly"
|
||||||
|
default:
|
||||||
|
return "cleanup-leftovers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postworkLeftoverRunningText(reason string) string {
|
||||||
|
switch strings.TrimSpace(strings.ToLower(reason)) {
|
||||||
|
case "startup":
|
||||||
|
return "Startup-Cleanup: prüfe Record-Reste…"
|
||||||
|
case "interval":
|
||||||
|
return "Stündlicher Cleanup: prüfe Record-Reste…"
|
||||||
|
default:
|
||||||
|
return "Prüfe Record-Reste…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sleepContext(ctx context.Context, d time.Duration) error {
|
||||||
|
if d <= 0 {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := time.NewTimer(d)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postworkLeftoverScanSummary(queued, skipped, removedMuxing, skippedMuxing int) string {
|
||||||
|
parts := make([]string, 0, 4)
|
||||||
|
|
||||||
|
if queued > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Postwork eingereiht: %d", queued))
|
||||||
|
}
|
||||||
|
if removedMuxing > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Muxing-Temps gelöscht: %d", removedMuxing))
|
||||||
|
}
|
||||||
|
if skipped > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Postwork übersprungen: %d", skipped))
|
||||||
|
}
|
||||||
|
if skippedMuxing > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("Muxing-Temps übersprungen: %d", skippedMuxing))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "Keine Record-Reste gefunden."
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " | ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupStaleMuxingTempFilesFromRecordDir(ctx context.Context) (removed int, skipped int, err error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
s := getSettings()
|
s := getSettings()
|
||||||
|
|
||||||
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
||||||
@ -66,18 +177,22 @@ func cleanupStaleMuxingTempFilesFromRecordDir() (removed int, skipped int) {
|
|||||||
recordAbs = strings.TrimSpace(recordAbs)
|
recordAbs = strings.TrimSpace(recordAbs)
|
||||||
if recordAbs == "" {
|
if recordAbs == "" {
|
||||||
appLogln("⚠️ [startup-postwork] recordDir ist leer")
|
appLogln("⚠️ [startup-postwork] recordDir ist leer")
|
||||||
return 0, 0
|
return 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(recordAbs)
|
entries, err := os.ReadDir(recordAbs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err)
|
appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err)
|
||||||
return 0, 0
|
return 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return removed, skipped, err
|
||||||
|
}
|
||||||
|
|
||||||
if e == nil || e.IsDir() {
|
if e == nil || e.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -123,7 +238,11 @@ func cleanupStaleMuxingTempFilesFromRecordDir() (removed int, skipped int) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !singleFileStable(path) {
|
stable, err := singleFileStable(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return removed, skipped, err
|
||||||
|
}
|
||||||
|
if !stable {
|
||||||
skipped++
|
skipped++
|
||||||
appLogln("⏭️ [startup-postwork] .muxing.ts nicht stabil:", name)
|
appLogln("⏭️ [startup-postwork] .muxing.ts nicht stabil:", name)
|
||||||
continue
|
continue
|
||||||
@ -146,7 +265,7 @@ func cleanupStaleMuxingTempFilesFromRecordDir() (removed int, skipped int) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return removed, skipped
|
return removed, skipped, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMuxingTempTSName(name string) bool {
|
func isMuxingTempTSName(name string) bool {
|
||||||
@ -160,10 +279,10 @@ func isMuxingTempTSName(name string) bool {
|
|||||||
return strings.HasSuffix(lowerName, ".muxing.ts")
|
return strings.HasSuffix(lowerName, ".muxing.ts")
|
||||||
}
|
}
|
||||||
|
|
||||||
func singleFileStable(path string) bool {
|
func singleFileStable(ctx context.Context, path string) (bool, error) {
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeOf := func(p string) (int64, bool) {
|
sizeOf := func(p string) (int64, bool) {
|
||||||
@ -177,20 +296,26 @@ func singleFileStable(path string) bool {
|
|||||||
|
|
||||||
size1, ok1 := sizeOf(path)
|
size1, ok1 := sizeOf(path)
|
||||||
if !ok1 {
|
if !ok1 {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(1500 * time.Millisecond)
|
if err := sleepContext(ctx, 1500*time.Millisecond); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
size2, ok2 := sizeOf(path)
|
size2, ok2 := sizeOf(path)
|
||||||
if !ok2 {
|
if !ok2 {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return size1 == size2
|
return size1 == size2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
|
func enqueuePostworkLeftoversFromRecordDir(ctx context.Context) (queued int, skipped int, err error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
s := getSettings()
|
s := getSettings()
|
||||||
|
|
||||||
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
||||||
@ -200,13 +325,13 @@ func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
|
|||||||
recordAbs = strings.TrimSpace(recordAbs)
|
recordAbs = strings.TrimSpace(recordAbs)
|
||||||
if recordAbs == "" {
|
if recordAbs == "" {
|
||||||
appLogln("⚠️ [startup-postwork] recordDir ist leer")
|
appLogln("⚠️ [startup-postwork] recordDir ist leer")
|
||||||
return 0, 0
|
return 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(recordAbs)
|
entries, err := os.ReadDir(recordAbs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err)
|
appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err)
|
||||||
return 0, 0
|
return 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -225,6 +350,10 @@ func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return queued, skipped, err
|
||||||
|
}
|
||||||
|
|
||||||
if e == nil || e.IsDir() {
|
if e == nil || e.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -311,7 +440,11 @@ func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !postworkLeftoverFilesStable(tsPath, audioPath) {
|
stable, err := postworkLeftoverFilesStable(ctx, tsPath, audioPath)
|
||||||
|
if err != nil {
|
||||||
|
return queued, skipped, err
|
||||||
|
}
|
||||||
|
if !stable {
|
||||||
skip(name, "files not stable", filepath.Base(audioPath))
|
skip(name, "files not stable", filepath.Base(audioPath))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -323,7 +456,7 @@ func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return queued, skipped
|
return queued, skipped, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func postworkLeftoverAlreadyTracked(tsPath string) bool {
|
func postworkLeftoverAlreadyTracked(tsPath string) bool {
|
||||||
@ -373,11 +506,11 @@ func postworkLeftoverDoneMP4Exists(tsPath string) bool {
|
|||||||
return err == nil && fi != nil && !fi.IsDir() && fi.Size() > 0
|
return err == nil && fi != nil && !fi.IsDir() && fi.Size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func postworkLeftoverFilesStable(tsPath string, audioPath string) bool {
|
func postworkLeftoverFilesStable(ctx context.Context, tsPath string, audioPath string) (bool, error) {
|
||||||
tsPath = strings.TrimSpace(tsPath)
|
tsPath = strings.TrimSpace(tsPath)
|
||||||
audioPath = strings.TrimSpace(audioPath)
|
audioPath = strings.TrimSpace(audioPath)
|
||||||
if tsPath == "" || audioPath == "" {
|
if tsPath == "" || audioPath == "" {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeOf := func(p string) (int64, bool) {
|
sizeOf := func(p string) (int64, bool) {
|
||||||
@ -391,18 +524,20 @@ func postworkLeftoverFilesStable(tsPath string, audioPath string) bool {
|
|||||||
ts1, ok1 := sizeOf(tsPath)
|
ts1, ok1 := sizeOf(tsPath)
|
||||||
audio1, ok2 := sizeOf(audioPath)
|
audio1, ok2 := sizeOf(audioPath)
|
||||||
if !ok1 || !ok2 {
|
if !ok1 || !ok2 {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(1500 * time.Millisecond)
|
if err := sleepContext(ctx, 1500*time.Millisecond); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
ts2, ok1 := sizeOf(tsPath)
|
ts2, ok1 := sizeOf(tsPath)
|
||||||
audio2, ok2 := sizeOf(audioPath)
|
audio2, ok2 := sizeOf(audioPath)
|
||||||
if !ok1 || !ok2 {
|
if !ok1 || !ok2 {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return ts1 == ts2 && audio1 == audio2
|
return ts1 == ts2 && audio1 == audio2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func enqueuePostworkLeftover(tsPath string, tsInfo os.FileInfo) bool {
|
func enqueuePostworkLeftover(tsPath string, tsInfo os.FileInfo) bool {
|
||||||
|
|||||||
@ -62,6 +62,33 @@ type cleanupTaskJob struct {
|
|||||||
var cleanupJobsMu sync.Mutex
|
var cleanupJobsMu sync.Mutex
|
||||||
var cleanupJobs = map[string]*cleanupTaskJob{}
|
var cleanupJobs = map[string]*cleanupTaskJob{}
|
||||||
|
|
||||||
|
func createCleanupJob(jobID string, text string, cancel context.CancelFunc) CleanupTaskState {
|
||||||
|
st := CleanupTaskState{
|
||||||
|
ID: jobID,
|
||||||
|
Queued: true,
|
||||||
|
Running: false,
|
||||||
|
Cancellable: cancel != nil,
|
||||||
|
QueuedAt: time.Now(),
|
||||||
|
StartedAt: time.Time{},
|
||||||
|
FinishedAt: nil,
|
||||||
|
Text: strings.TrimSpace(text),
|
||||||
|
Error: "",
|
||||||
|
CurrentFile: "",
|
||||||
|
Done: 0,
|
||||||
|
Total: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupJobsMu.Lock()
|
||||||
|
cleanupJobs[jobID] = &cleanupTaskJob{
|
||||||
|
State: st,
|
||||||
|
Cancel: cancel,
|
||||||
|
}
|
||||||
|
cleanupJobsMu.Unlock()
|
||||||
|
|
||||||
|
publishTaskState()
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
func snapshotCleanupJobs() []CleanupTaskState {
|
func snapshotCleanupJobs() []CleanupTaskState {
|
||||||
cleanupJobsMu.Lock()
|
cleanupJobsMu.Lock()
|
||||||
defer cleanupJobsMu.Unlock()
|
defer cleanupJobsMu.Unlock()
|
||||||
@ -184,6 +211,7 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ID: jobID,
|
ID: jobID,
|
||||||
Queued: true,
|
Queued: true,
|
||||||
Running: false,
|
Running: false,
|
||||||
|
Cancellable: true,
|
||||||
QueuedAt: time.Now(),
|
QueuedAt: time.Now(),
|
||||||
StartedAt: time.Time{},
|
StartedAt: time.Time{},
|
||||||
FinishedAt: nil,
|
FinishedAt: nil,
|
||||||
@ -467,16 +495,14 @@ func cleanupTSPathForAudioSidecar(audioPath string) string {
|
|||||||
|
|
||||||
dir := filepath.Dir(audioPath)
|
dir := filepath.Dir(audioPath)
|
||||||
name := filepath.Base(audioPath)
|
name := filepath.Base(audioPath)
|
||||||
|
lowName := strings.ToLower(name)
|
||||||
|
|
||||||
if !strings.HasPrefix(name, ".") {
|
if !strings.HasSuffix(lowName, ".audio.m4s") {
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(strings.ToLower(name), ".audio.m4s") {
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
base := strings.TrimPrefix(name, ".")
|
base := name[:len(name)-len(".audio.m4s")]
|
||||||
base = strings.TrimSuffix(base, ".audio.m4s")
|
base = strings.TrimPrefix(base, ".")
|
||||||
|
|
||||||
if strings.TrimSpace(base) == "" {
|
if strings.TrimSpace(base) == "" {
|
||||||
return ""
|
return ""
|
||||||
@ -543,7 +569,7 @@ func cleanupDeleteRecordOrphanFile(ctx context.Context, jobID string, path strin
|
|||||||
|
|
||||||
if strings.HasSuffix(lowName, ".muxing.ts") {
|
if strings.HasSuffix(lowName, ".muxing.ts") {
|
||||||
resp.DeletedMuxingTemps++
|
resp.DeletedMuxingTemps++
|
||||||
} else if strings.HasPrefix(name, ".") && strings.HasSuffix(lowName, ".audio.m4s") {
|
} else if strings.HasSuffix(lowName, ".audio.m4s") {
|
||||||
resp.DeletedAudioOrphans++
|
resp.DeletedAudioOrphans++
|
||||||
} else if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") {
|
} else if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") {
|
||||||
resp.DeletedTSOrphans++
|
resp.DeletedTSOrphans++
|
||||||
@ -637,7 +663,7 @@ func cleanupRecordDirOrphanAVFiles(ctx context.Context, jobID string, recordAbs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fall 1:
|
// Fall 1:
|
||||||
// foo.ts existiert, aber .foo.audio.m4s fehlt.
|
// foo.ts existiert, aber kein passendes foo.audio.m4s/.foo.audio.m4s.
|
||||||
if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") {
|
if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") {
|
||||||
resp.ScannedFiles++
|
resp.ScannedFiles++
|
||||||
|
|
||||||
@ -650,8 +676,8 @@ func cleanupRecordDirOrphanAVFiles(ctx context.Context, jobID string, recordAbs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fall 2:
|
// Fall 2:
|
||||||
// .foo.audio.m4s existiert, aber foo.ts fehlt.
|
// foo.audio.m4s/.foo.audio.m4s existiert, aber foo.ts fehlt.
|
||||||
if strings.HasPrefix(name, ".") && strings.HasSuffix(low, ".audio.m4s") {
|
if strings.HasSuffix(low, ".audio.m4s") {
|
||||||
resp.ScannedFiles++
|
resp.ScannedFiles++
|
||||||
|
|
||||||
tsPath := cleanupTSPathForAudioSidecar(full)
|
tsPath := cleanupTSPathForAudioSidecar(full)
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type CleanupTaskState struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Queued bool `json:"queued"`
|
Queued bool `json:"queued"`
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
|
Cancellable bool `json:"cancellable"`
|
||||||
QueuedAt time.Time `json:"queuedAt"`
|
QueuedAt time.Time `json:"queuedAt"`
|
||||||
StartedAt time.Time `json:"startedAt"`
|
StartedAt time.Time `json:"startedAt"`
|
||||||
FinishedAt *time.Time `json:"finishedAt,omitempty"`
|
FinishedAt *time.Time `json:"finishedAt,omitempty"`
|
||||||
|
|||||||
@ -38,6 +38,8 @@ import LastUpdatedText from './components/ui/LastUpdatedText'
|
|||||||
import TrainingTab from './components/ui/TrainingTab'
|
import TrainingTab from './components/ui/TrainingTab'
|
||||||
|
|
||||||
const COOKIE_STORAGE_KEY = 'record_cookies'
|
const COOKIE_STORAGE_KEY = 'record_cookies'
|
||||||
|
const MODELS_OVERVIEW_DEBOUNCE_MS = 750
|
||||||
|
const MODELS_OVERVIEW_COOLDOWN_MS = 5000
|
||||||
|
|
||||||
function normalizeCookies(obj: Record<string, string> | null | undefined): Record<string, string> {
|
function normalizeCookies(obj: Record<string, string> | null | undefined): Record<string, string> {
|
||||||
const input = obj ?? {}
|
const input = obj ?? {}
|
||||||
@ -215,6 +217,20 @@ function mergeStoredModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storedModelShallowEqual(a?: StoredModel | null, b?: StoredModel | null) {
|
||||||
|
if (a === b) return true
|
||||||
|
if (!a || !b) return false
|
||||||
|
|
||||||
|
const keys = new Set([...Object.keys(a), ...Object.keys(b)])
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!Object.is((a as any)[key], (b as any)[key])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePendingShow(v: unknown): 'public' | 'private' | 'hidden' | 'away' | 'offline' | 'unknown' {
|
function normalizePendingShow(v: unknown): 'public' | 'private' | 'hidden' | 'away' | 'offline' | 'unknown' {
|
||||||
const s = String(v ?? '').trim().toLowerCase()
|
const s = String(v ?? '').trim().toLowerCase()
|
||||||
|
|
||||||
@ -1747,6 +1763,8 @@ export default function App() {
|
|||||||
const overviewLastSigRef = useRef('')
|
const overviewLastSigRef = useRef('')
|
||||||
const overviewLastAtRef = useRef(0)
|
const overviewLastAtRef = useRef(0)
|
||||||
const overviewTimerRef = useRef<number | null>(null)
|
const overviewTimerRef = useRef<number | null>(null)
|
||||||
|
const overviewAbortRef = useRef<AbortController | null>(null)
|
||||||
|
const overviewPendingKeysRef = useRef<string[] | null>(null)
|
||||||
|
|
||||||
const loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
|
const loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
|
||||||
const uniqKeys = Array.from(
|
const uniqKeys = Array.from(
|
||||||
@ -1760,30 +1778,51 @@ export default function App() {
|
|||||||
const sig = uniqKeys.join('|')
|
const sig = uniqKeys.join('|')
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
if (overviewInFlightRef.current) return
|
if (overviewInFlightRef.current) {
|
||||||
if (overviewLastSigRef.current === sig && now - overviewLastAtRef.current < 1500) return
|
overviewPendingKeysRef.current = uniqKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overviewLastSigRef.current === sig && now - overviewLastAtRef.current < MODELS_OVERVIEW_COOLDOWN_MS) return
|
||||||
|
|
||||||
overviewInFlightRef.current = true
|
overviewInFlightRef.current = true
|
||||||
overviewLastSigRef.current = sig
|
overviewLastSigRef.current = sig
|
||||||
overviewLastAtRef.current = now
|
overviewLastAtRef.current = now
|
||||||
|
overviewAbortRef.current?.abort()
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
overviewAbortRef.current = controller
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await apiJSON<AppModelsSnapshot>('/api/models/overview', {
|
const data = await apiJSON<AppModelsSnapshot>('/api/models/overview', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
cache: 'no-store' as any,
|
cache: 'no-store' as any,
|
||||||
|
signal: controller.signal,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
keys: uniqKeys,
|
keys: uniqKeys,
|
||||||
includeWatched: true,
|
includeWatched: true,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
setHeaderStats({
|
setHeaderStats((prev) => {
|
||||||
totalCount: Number(data?.totalCount ?? 0),
|
const next = {
|
||||||
onlineModelsCount: Number(data?.onlineModelsCount ?? 0),
|
totalCount: Number(data?.totalCount ?? 0),
|
||||||
onlineWatchedModelsCount: Number(data?.onlineWatchedModelsCount ?? 0),
|
onlineModelsCount: Number(data?.onlineModelsCount ?? 0),
|
||||||
onlineFavCount: Number(data?.onlineFavCount ?? 0),
|
onlineWatchedModelsCount: Number(data?.onlineWatchedModelsCount ?? 0),
|
||||||
onlineLikedCount: Number(data?.onlineLikedCount ?? 0),
|
onlineFavCount: Number(data?.onlineFavCount ?? 0),
|
||||||
|
onlineLikedCount: Number(data?.onlineLikedCount ?? 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
prev.totalCount === next.totalCount &&
|
||||||
|
prev.onlineModelsCount === next.onlineModelsCount &&
|
||||||
|
prev.onlineWatchedModelsCount === next.onlineWatchedModelsCount &&
|
||||||
|
prev.onlineFavCount === next.onlineFavCount &&
|
||||||
|
prev.onlineLikedCount === next.onlineLikedCount
|
||||||
|
)
|
||||||
|
? prev
|
||||||
|
: next
|
||||||
})
|
})
|
||||||
|
|
||||||
const safeList = Array.isArray(data?.models) ? data.models : []
|
const safeList = Array.isArray(data?.models) ? data.models : []
|
||||||
@ -1791,17 +1830,40 @@ export default function App() {
|
|||||||
|
|
||||||
setModelsByKey((prev) => {
|
setModelsByKey((prev) => {
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
|
let changed = false
|
||||||
|
|
||||||
for (const [k, incoming] of Object.entries(built)) {
|
for (const [k, incoming] of Object.entries(built)) {
|
||||||
next[k] = mergeStoredModel(prev[k], incoming)
|
const merged = mergeStoredModel(prev[k], incoming)
|
||||||
|
if (!storedModelShallowEqual(prev[k], merged)) {
|
||||||
|
next[k] = merged
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next
|
return changed ? next : prev
|
||||||
})
|
})
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
|
if (error?.name === 'AbortError') return
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
overviewInFlightRef.current = false
|
overviewInFlightRef.current = false
|
||||||
|
if (overviewAbortRef.current === controller) {
|
||||||
|
overviewAbortRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingKeys = overviewPendingKeysRef.current
|
||||||
|
overviewPendingKeysRef.current = null
|
||||||
|
|
||||||
|
if (pendingKeys && pendingKeys.join('|') !== sig) {
|
||||||
|
if (overviewTimerRef.current != null) {
|
||||||
|
window.clearTimeout(overviewTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
overviewTimerRef.current = window.setTimeout(() => {
|
||||||
|
overviewTimerRef.current = null
|
||||||
|
void loadAppModelsSnapshot(pendingKeys)
|
||||||
|
}, MODELS_OVERVIEW_DEBOUNCE_MS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [buildModelsByKey])
|
}, [buildModelsByKey])
|
||||||
|
|
||||||
@ -2172,7 +2234,7 @@ export default function App() {
|
|||||||
|
|
||||||
overviewTimerRef.current = window.setTimeout(() => {
|
overviewTimerRef.current = window.setTimeout(() => {
|
||||||
void loadAppModelsSnapshot(requiredModelKeys)
|
void loadAppModelsSnapshot(requiredModelKeys)
|
||||||
}, 250)
|
}, MODELS_OVERVIEW_DEBOUNCE_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
||||||
@ -2242,6 +2304,11 @@ export default function App() {
|
|||||||
window.clearTimeout(overviewTimerRef.current)
|
window.clearTimeout(overviewTimerRef.current)
|
||||||
overviewTimerRef.current = null
|
overviewTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overviewPendingKeysRef.current = null
|
||||||
|
overviewAbortRef.current?.abort()
|
||||||
|
overviewAbortRef.current = null
|
||||||
|
overviewInFlightRef.current = false
|
||||||
}
|
}
|
||||||
}, [authed, loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
}, [authed, loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||||
|
|
||||||
|
|||||||
@ -144,6 +144,143 @@ function useMobilePreviewConstrained() {
|
|||||||
return constrained
|
return constrained
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEASER_MISSING_TTL_MS = 5 * 60 * 1000
|
||||||
|
const TEASER_MISSING_CACHE_MAX = 600
|
||||||
|
const MAX_TEASER_PREWARMS = 4
|
||||||
|
|
||||||
|
type TeaserMissingEntry = {
|
||||||
|
failedAt: number
|
||||||
|
assetNonce: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const teaserMissingById = new Map<string, TeaserMissingEntry>()
|
||||||
|
const teaserPrewarmSubscribers = new Set<() => void>()
|
||||||
|
let activeTeaserPrewarms = 0
|
||||||
|
|
||||||
|
function teaserCacheKey(id: string) {
|
||||||
|
return String(id ?? '').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAssetNonce(value: unknown) {
|
||||||
|
const n = Number(value ?? 0)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneTeaserMissingCache(now = Date.now()) {
|
||||||
|
for (const [key, entry] of teaserMissingById.entries()) {
|
||||||
|
if (now - entry.failedAt > TEASER_MISSING_TTL_MS) {
|
||||||
|
teaserMissingById.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teaserMissingById.size <= TEASER_MISSING_CACHE_MAX) return
|
||||||
|
|
||||||
|
const oldKeys = Array.from(teaserMissingById.entries())
|
||||||
|
.sort((a, b) => a[1].failedAt - b[1].failedAt)
|
||||||
|
.slice(0, teaserMissingById.size - TEASER_MISSING_CACHE_MAX)
|
||||||
|
.map(([key]) => key)
|
||||||
|
|
||||||
|
for (const key of oldKeys) teaserMissingById.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTeaserMissingCached(id: string, assetNonce: unknown) {
|
||||||
|
const key = teaserCacheKey(id)
|
||||||
|
if (!key) return false
|
||||||
|
|
||||||
|
const entry = teaserMissingById.get(key)
|
||||||
|
if (!entry) return false
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - entry.failedAt > TEASER_MISSING_TTL_MS) {
|
||||||
|
teaserMissingById.delete(key)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.assetNonce !== normalizeAssetNonce(assetNonce)) {
|
||||||
|
teaserMissingById.delete(key)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function markTeaserMissing(id: string, assetNonce: unknown) {
|
||||||
|
const key = teaserCacheKey(id)
|
||||||
|
if (!key) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
pruneTeaserMissingCache(now)
|
||||||
|
teaserMissingById.set(key, {
|
||||||
|
failedAt: now,
|
||||||
|
assetNonce: normalizeAssetNonce(assetNonce),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTeaserMissing(id: string) {
|
||||||
|
const key = teaserCacheKey(id)
|
||||||
|
if (!key) return
|
||||||
|
teaserMissingById.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyTeaserPrewarmSlotChanged() {
|
||||||
|
for (const fn of teaserPrewarmSubscribers) fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryAcquireTeaserPrewarmSlot() {
|
||||||
|
if (activeTeaserPrewarms >= MAX_TEASER_PREWARMS) return false
|
||||||
|
activeTeaserPrewarms += 1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseTeaserPrewarmSlot() {
|
||||||
|
if (activeTeaserPrewarms > 0) {
|
||||||
|
activeTeaserPrewarms -= 1
|
||||||
|
}
|
||||||
|
notifyTeaserPrewarmSlotChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTeaserPrewarmSlot(enabled: boolean, key: string) {
|
||||||
|
const [hasSlot, setHasSlot] = useState(false)
|
||||||
|
const hasSlotRef = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !key) {
|
||||||
|
if (hasSlotRef.current) {
|
||||||
|
hasSlotRef.current = false
|
||||||
|
releaseTeaserPrewarmSlot()
|
||||||
|
}
|
||||||
|
setHasSlot(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let alive = true
|
||||||
|
|
||||||
|
const acquire = () => {
|
||||||
|
if (!alive || hasSlotRef.current) return
|
||||||
|
if (!tryAcquireTeaserPrewarmSlot()) return
|
||||||
|
|
||||||
|
hasSlotRef.current = true
|
||||||
|
setHasSlot(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
teaserPrewarmSubscribers.add(acquire)
|
||||||
|
acquire()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
alive = false
|
||||||
|
teaserPrewarmSubscribers.delete(acquire)
|
||||||
|
|
||||||
|
if (hasSlotRef.current) {
|
||||||
|
hasSlotRef.current = false
|
||||||
|
releaseTeaserPrewarmSlot()
|
||||||
|
}
|
||||||
|
setHasSlot(false)
|
||||||
|
}
|
||||||
|
}, [enabled, key])
|
||||||
|
|
||||||
|
return hasSlot
|
||||||
|
}
|
||||||
|
|
||||||
export default function FinishedVideoPreview({
|
export default function FinishedVideoPreview({
|
||||||
job,
|
job,
|
||||||
getFileName,
|
getFileName,
|
||||||
@ -172,6 +309,7 @@ export default function FinishedVideoPreview({
|
|||||||
inlineNonce = 0,
|
inlineNonce = 0,
|
||||||
inlineControls = false,
|
inlineControls = false,
|
||||||
inlineLoop = true,
|
inlineLoop = true,
|
||||||
|
assetNonce = 0,
|
||||||
muted = DEFAULT_INLINE_MUTED,
|
muted = DEFAULT_INLINE_MUTED,
|
||||||
popoverMuted = DEFAULT_INLINE_MUTED,
|
popoverMuted = DEFAULT_INLINE_MUTED,
|
||||||
alwaysLoadStill = false,
|
alwaysLoadStill = false,
|
||||||
@ -352,6 +490,8 @@ export default function FinishedVideoPreview({
|
|||||||
|
|
||||||
const [teaserReady, setTeaserReady] = useState(false)
|
const [teaserReady, setTeaserReady] = useState(false)
|
||||||
const [teaserOk, setTeaserOk] = useState(true)
|
const [teaserOk, setTeaserOk] = useState(true)
|
||||||
|
const [teaserCacheVersion, setTeaserCacheVersion] = useState(0)
|
||||||
|
const [teaserPrewarmDone, setTeaserPrewarmDone] = useState(false)
|
||||||
|
|
||||||
// ✅ verhindert doppelte Parent-Callbacks (z.B. Count/Meta-Refresh im Parent)
|
// ✅ verhindert doppelte Parent-Callbacks (z.B. Count/Meta-Refresh im Parent)
|
||||||
const emittedDurationRef = useRef<string>('')
|
const emittedDurationRef = useRef<string>('')
|
||||||
@ -619,13 +759,14 @@ export default function FinishedVideoPreview({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTeaserReady(false)
|
setTeaserReady(false)
|
||||||
setTeaserOk(true)
|
setTeaserPrewarmDone(false)
|
||||||
|
setTeaserOk(!isTeaserMissingCached(previewId, assetNonce))
|
||||||
|
|
||||||
emittedDurationRef.current = ''
|
emittedDurationRef.current = ''
|
||||||
emittedResolutionRef.current = ''
|
emittedResolutionRef.current = ''
|
||||||
teaserSoundForcedRef.current = false
|
teaserSoundForcedRef.current = false
|
||||||
setMetaLoaded(false)
|
setMetaLoaded(false)
|
||||||
}, [previewId])
|
}, [previewId, assetNonce])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onRelease = (ev: any) => {
|
const onRelease = (ev: any) => {
|
||||||
@ -736,10 +877,34 @@ export default function FinishedVideoPreview({
|
|||||||
return `/api/preview?id=${encodeURIComponent(previewId)}&t=${thumbTimeSec}`
|
return `/api/preview?id=${encodeURIComponent(previewId)}&t=${thumbTimeSec}`
|
||||||
}, [previewId, thumbTimeSec])
|
}, [previewId, thumbTimeSec])
|
||||||
|
|
||||||
|
const teaserKnownMissing = useMemo(
|
||||||
|
() => isTeaserMissingCached(previewId, assetNonce),
|
||||||
|
[previewId, assetNonce, teaserCacheVersion]
|
||||||
|
)
|
||||||
|
|
||||||
const teaserSrc = useMemo(() => {
|
const teaserSrc = useMemo(() => {
|
||||||
if (!previewId) return ''
|
if (!previewId) return ''
|
||||||
|
if (teaserKnownMissing) return ''
|
||||||
return `/api/generated/teaser?id=${encodeURIComponent(previewId)}&noGenerate=1`
|
return `/api/generated/teaser?id=${encodeURIComponent(previewId)}&noGenerate=1`
|
||||||
}, [previewId])
|
}, [previewId, teaserKnownMissing])
|
||||||
|
|
||||||
|
const markTeaserAvailable = useCallback(() => {
|
||||||
|
if (isTeaserMissingCached(previewId, assetNonce)) {
|
||||||
|
clearTeaserMissing(previewId)
|
||||||
|
setTeaserCacheVersion((v) => v + 1)
|
||||||
|
}
|
||||||
|
setTeaserOk(true)
|
||||||
|
setTeaserReady(true)
|
||||||
|
setTeaserPrewarmDone(true)
|
||||||
|
}, [previewId, assetNonce])
|
||||||
|
|
||||||
|
const markTeaserUnavailable = useCallback(() => {
|
||||||
|
markTeaserMissing(previewId, assetNonce)
|
||||||
|
setTeaserCacheVersion((v) => v + 1)
|
||||||
|
setTeaserOk(false)
|
||||||
|
setTeaserReady(false)
|
||||||
|
setTeaserPrewarmDone(true)
|
||||||
|
}, [previewId, assetNonce])
|
||||||
|
|
||||||
// --- Inline Video sichtbar?
|
// --- Inline Video sichtbar?
|
||||||
const showingInlineVideo =
|
const showingInlineVideo =
|
||||||
@ -864,23 +1029,33 @@ export default function FinishedVideoPreview({
|
|||||||
lastMountedRef.current.clips = null
|
lastMountedRef.current.clips = null
|
||||||
}, [previewId])
|
}, [previewId])
|
||||||
|
|
||||||
if (!videoSrc) {
|
|
||||||
return <div className={[sizeClass, 'rounded bg-gray-100 dark:bg-white/5'].join(' ')} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ aktive Asset-Nutzung (z.B. poster etc.)
|
|
||||||
const shouldLoadAssets =
|
const shouldLoadAssets =
|
||||||
shouldPreloadAnimatedAssets || effectiveInView || showingInlineVideo || teaserActive
|
shouldPreloadAnimatedAssets || effectiveInView || showingInlineVideo || teaserActive
|
||||||
|
|
||||||
const teaserCanPrewarm =
|
const teaserCanPrewarm =
|
||||||
animated &&
|
animated &&
|
||||||
!mobilePreviewConstrained &&
|
!mobilePreviewConstrained &&
|
||||||
animatedMode === 'teaser' &&
|
animatedMode === 'teaser' &&
|
||||||
Boolean(teaserSrc) &&
|
Boolean(teaserSrc) &&
|
||||||
|
!teaserKnownMissing &&
|
||||||
|
!teaserPrewarmDone &&
|
||||||
!showingInlineVideo &&
|
!showingInlineVideo &&
|
||||||
shouldPreloadAnimatedAssets &&
|
shouldPreloadAnimatedAssets &&
|
||||||
animatedTrigger !== 'hover'
|
animatedTrigger !== 'hover'
|
||||||
|
|
||||||
|
const teaserPrewarmWanted =
|
||||||
|
teaserCanPrewarm && !(teaserActive && animatedMode === 'teaser')
|
||||||
|
|
||||||
|
const teaserPrewarmHasSlot = useTeaserPrewarmSlot(
|
||||||
|
teaserPrewarmWanted,
|
||||||
|
`${previewId}:${normalizeAssetNonce(assetNonce)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!videoSrc) {
|
||||||
|
return <div className={[sizeClass, 'rounded bg-gray-100 dark:bg-white/5'].join(' ')} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ aktive Asset-Nutzung (z.B. poster etc.)
|
||||||
const showStillImage =
|
const showStillImage =
|
||||||
shouldLoadStill &&
|
shouldLoadStill &&
|
||||||
!showingInlineVideo &&
|
!showingInlineVideo &&
|
||||||
@ -1464,26 +1639,17 @@ export default function FinishedVideoPreview({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* ✅ Teaser prewarm (lädt Datei/Metadata vor, bleibt unsichtbar) */}
|
{/* ✅ Teaser prewarm (lädt Datei/Metadata vor, bleibt unsichtbar) */}
|
||||||
{!showingInlineVideo && teaserCanPrewarm && !(teaserActive && animatedMode === 'teaser') ? (
|
{teaserPrewarmHasSlot ? (
|
||||||
<video
|
<video
|
||||||
key={`teaser-prewarm-${previewId}`}
|
key={`teaser-prewarm-${previewId}-${normalizeAssetNonce(assetNonce)}`}
|
||||||
src={teaserSrc}
|
src={teaserSrc}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
muted
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
onLoadedData={() => {
|
onLoadedData={markTeaserAvailable}
|
||||||
setTeaserOk(true)
|
onCanPlay={markTeaserAvailable}
|
||||||
setTeaserReady(true)
|
onError={markTeaserUnavailable}
|
||||||
}}
|
|
||||||
onCanPlay={() => {
|
|
||||||
setTeaserOk(true)
|
|
||||||
setTeaserReady(true)
|
|
||||||
}}
|
|
||||||
onError={() => {
|
|
||||||
setTeaserOk(false)
|
|
||||||
setTeaserReady(false)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -1510,18 +1676,9 @@ export default function FinishedVideoPreview({
|
|||||||
loop
|
loop
|
||||||
preload={mobilePreviewConstrained ? 'metadata' : 'auto'}
|
preload={mobilePreviewConstrained ? 'metadata' : 'auto'}
|
||||||
poster={shouldLoadAssets ? (thumbSrc || undefined) : undefined}
|
poster={shouldLoadAssets ? (thumbSrc || undefined) : undefined}
|
||||||
onLoadedData={() => {
|
onLoadedData={markTeaserAvailable}
|
||||||
setTeaserOk(true)
|
onCanPlay={markTeaserAvailable}
|
||||||
setTeaserReady(true)
|
onError={markTeaserUnavailable}
|
||||||
}}
|
|
||||||
onCanPlay={() => {
|
|
||||||
setTeaserOk(true)
|
|
||||||
setTeaserReady(true)
|
|
||||||
}}
|
|
||||||
onError={() => {
|
|
||||||
setTeaserOk(false)
|
|
||||||
setTeaserReady(false)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@ -484,6 +484,7 @@ function mapCheckVideosJobToTask(job: any): TaskItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapCleanupJobToTask(job: any): TaskItem {
|
function mapCleanupJobToTask(job: any): TaskItem {
|
||||||
|
const rawId = String(job?.id || '')
|
||||||
const err = String(job?.error || '').trim()
|
const err = String(job?.error || '').trim()
|
||||||
let status: TaskItem['status'] = 'idle'
|
let status: TaskItem['status'] = 'idle'
|
||||||
|
|
||||||
@ -493,20 +494,29 @@ function mapCleanupJobToTask(job: any): TaskItem {
|
|||||||
else if (job?.finishedAt) status = 'done'
|
else if (job?.finishedAt) status = 'done'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: cleanupTaskDomId(String(job?.id || '')),
|
id: cleanupTaskDomId(rawId),
|
||||||
status,
|
status,
|
||||||
title: 'Aufräumen',
|
title: cleanupTaskTitle(rawId),
|
||||||
text: String(job?.text || job?.currentFile || ''),
|
text: String(job?.text || job?.currentFile || ''),
|
||||||
done: Number(job?.done ?? 0),
|
done: Number(job?.done ?? 0),
|
||||||
total: Number(job?.total ?? 0),
|
total: Number(job?.total ?? 0),
|
||||||
err: status === 'error' ? err : undefined,
|
err: status === 'error' ? err : undefined,
|
||||||
cancellable: status === 'queued' || status === 'running',
|
cancellable: job?.cancellable !== false && (status === 'queued' || status === 'running'),
|
||||||
fading: false,
|
fading: false,
|
||||||
queuedAtMs: parseTaskTimeMs(job?.queuedAt),
|
queuedAtMs: parseTaskTimeMs(job?.queuedAt),
|
||||||
startedAtMs: parseTaskTimeMs(job?.startedAt),
|
startedAtMs: parseTaskTimeMs(job?.startedAt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupTaskTitle(rawId: string) {
|
||||||
|
const id = String(rawId || '').trim().toLowerCase()
|
||||||
|
|
||||||
|
if (id.startsWith('cleanup-leftovers-startup')) return 'Startup-Cleanup'
|
||||||
|
if (id.startsWith('cleanup-leftovers-hourly')) return 'Stündlicher Cleanup'
|
||||||
|
if (id.startsWith('cleanup-leftovers')) return 'Record-Reste prüfen'
|
||||||
|
return 'Aufräumen'
|
||||||
|
}
|
||||||
|
|
||||||
const SETTINGS_RATING_STAR_PATH =
|
const SETTINGS_RATING_STAR_PATH =
|
||||||
'M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 9.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z'
|
'M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 9.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z'
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user