bugfixes
This commit is contained in:
parent
43449405e8
commit
d7e22952a6
Binary file not shown.
@ -159,9 +159,33 @@ func shouldAutoSelectAnalyzeHit(label string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isIgnoredNSFWLabel(label) || isPersonSegmentLabel(label) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
labels := autoSelectedAILabelSet()
|
labels := autoSelectedAILabelSet()
|
||||||
_, ok := labels[label]
|
if _, ok := labels[label]; ok {
|
||||||
return ok
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robuster Fallback:
|
||||||
|
// Auch wenn trainingGroupedLabels()/detection_labels.json gerade leer,
|
||||||
|
// veraltet oder nicht ladbar ist, sollen bekannte Rating-/Analyze-Labels
|
||||||
|
// trotzdem durchkommen.
|
||||||
|
if bodyPartSeverityWeight(label) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if objectSeverityWeight(label) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if clothingSeverityWeight(label) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isKnownPositionLabel(label) && positionSeverityWeight(label) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var nsfwIgnoredLabels = map[string]struct{}{
|
var nsfwIgnoredLabels = map[string]struct{}{
|
||||||
@ -986,6 +1010,7 @@ func recordAnalyzeVideo(w http.ResponseWriter, r *http.Request) {
|
|||||||
ai := &aiAnalysisMeta{
|
ai := &aiAnalysisMeta{
|
||||||
Goal: "highlights",
|
Goal: "highlights",
|
||||||
Mode: "video",
|
Mode: "video",
|
||||||
|
Completed: true,
|
||||||
Hits: hits,
|
Hits: hits,
|
||||||
Segments: segments,
|
Segments: segments,
|
||||||
Rating: rating,
|
Rating: rating,
|
||||||
@ -1271,8 +1296,41 @@ func highlightComboPartOrder(label string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func predictionHasAnyAnalyzeSignal(pred TrainingPrediction) bool {
|
||||||
|
sexPosition := strings.ToLower(strings.TrimSpace(pred.SexPosition))
|
||||||
|
if sexPosition != "" && sexPosition != "unknown" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pred.BodyPartsPresent) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(pred.ObjectsPresent) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(pred.ClothingPresent) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(pred.Boxes) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func predictionUsableForAnalyze(pred TrainingPrediction) bool {
|
||||||
|
if pred.ModelAvailable {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback:
|
||||||
|
// Einige AI-Server/Antworten liefern verwertbare Labels/Boxes,
|
||||||
|
// setzen modelAvailable aber nicht sauber.
|
||||||
|
return predictionHasAnyAnalyzeSignal(pred)
|
||||||
|
}
|
||||||
|
|
||||||
func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64) (analyzeHit, bool) {
|
func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64) (analyzeHit, bool) {
|
||||||
if !pred.ModelAvailable {
|
if !predictionUsableForAnalyze(pred) {
|
||||||
return analyzeHit{}, false
|
return analyzeHit{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1471,7 +1529,7 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildHighlightHitsFromPrediction(pred TrainingPrediction, t float64) []analyzeHit {
|
func buildHighlightHitsFromPrediction(pred TrainingPrediction, t float64) []analyzeHit {
|
||||||
if !pred.ModelAvailable {
|
if !predictionUsableForAnalyze(pred) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1938,6 +1996,19 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
sample := samples[startIdx+i]
|
sample := samples[startIdx+i]
|
||||||
pred := predictions[i]
|
pred := predictions[i]
|
||||||
|
|
||||||
|
if !pred.ModelAvailable && predictionHasAnyAnalyzeSignal(pred) {
|
||||||
|
appLogf(
|
||||||
|
"⚠️ [analyze] prediction has signals but modelAvailable=false file=%q t=%.3f sex=%q body=%d objects=%d clothing=%d boxes=%d",
|
||||||
|
file,
|
||||||
|
sample.Time,
|
||||||
|
strings.TrimSpace(pred.SexPosition),
|
||||||
|
len(pred.BodyPartsPresent),
|
||||||
|
len(pred.ObjectsPresent),
|
||||||
|
len(pred.ClothingPresent),
|
||||||
|
len(pred.Boxes),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, sample.Time)
|
highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, sample.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -185,6 +185,7 @@ func ensureAnalyzeAllGoalsForVideoCtxForce(
|
|||||||
ai := &aiAnalysisMeta{
|
ai := &aiAnalysisMeta{
|
||||||
Goal: "highlights",
|
Goal: "highlights",
|
||||||
Mode: "video",
|
Mode: "video",
|
||||||
|
Completed: true,
|
||||||
Hits: hits,
|
Hits: hits,
|
||||||
Segments: segments,
|
Segments: segments,
|
||||||
Rating: rating,
|
Rating: rating,
|
||||||
@ -1412,24 +1413,46 @@ func hasAIAnalysisForOutputGoal(outPath string, goal string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eine Analyse ist erst dann fertig, wenn wirklich ein Rating-Objekt
|
// Neuer sauberer Marker:
|
||||||
// mit typischen Rating-Feldern gespeichert wurde.
|
// Analyse wurde wirklich ausgeführt und abgeschlossen.
|
||||||
|
// Das gilt auch dann, wenn es keine Hits/Segmente gab.
|
||||||
|
if completed, ok := aiMap["completed"].(bool); ok && completed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treffer/Segmente sind ebenfalls ein sicherer Hinweis.
|
||||||
|
rawHits, hasHits := aiMap["hits"].([]any)
|
||||||
|
if hasHits && len(rawHits) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSegs, hasSegs := aiMap["segments"].([]any)
|
||||||
|
if hasSegs && len(rawSegs) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy-Fallback:
|
||||||
|
// Positive Rating-Werte zählen, aber stars alleine NICHT.
|
||||||
|
// stars=1 kann nur der Default sein.
|
||||||
rating, ok := aiMap["rating"].(map[string]any)
|
rating, ok := aiMap["rating"].(map[string]any)
|
||||||
if !ok || rating == nil {
|
if !ok || rating == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonFieldExists(rating, "score") ||
|
if isPositiveJSONNumber(rating["segments"]) {
|
||||||
jsonFieldExists(rating, "stars") ||
|
return true
|
||||||
jsonFieldExists(rating, "segments") {
|
}
|
||||||
|
if isPositiveJSONNumber(rating["score"]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isPositiveJSONNumber(rating["flaggedSeconds"]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isPositiveJSONNumber(rating["weightedFlaggedSeconds"]) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy-Fallback: alte Dateien ohne Rating, aber mit Hits/Segments.
|
return false
|
||||||
rawHits, hasHits := aiMap["hits"].([]any)
|
|
||||||
rawSegs, hasSegs := aiMap["segments"].([]any)
|
|
||||||
|
|
||||||
return (hasHits && len(rawHits) > 0) || (hasSegs && len(rawSegs) > 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonFieldExists(m map[string]any, key string) bool {
|
func jsonFieldExists(m map[string]any, key string) bool {
|
||||||
|
|||||||
3
backend/data/pending-autostart/admin.json
Normal file
3
backend/data/pending-autostart/admin.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
@ -84,6 +84,11 @@ type postworkCompletedMeta struct {
|
|||||||
Analyze bool `json:"analyze,omitempty"`
|
Analyze bool `json:"analyze,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type postworkMeta struct {
|
||||||
|
Completed postworkCompletedMeta `json:"completed,omitempty"`
|
||||||
|
History []postworkHistoryEntry `json:"history,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type videoMeta struct {
|
type videoMeta struct {
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
UpdatedAtUnix int64 `json:"updatedAtUnix"`
|
UpdatedAtUnix int64 `json:"updatedAtUnix"`
|
||||||
@ -92,6 +97,7 @@ type videoMeta struct {
|
|||||||
Media videoMediaMeta `json:"media"`
|
Media videoMediaMeta `json:"media"`
|
||||||
Preview previewMeta `json:"preview,omitempty"`
|
Preview previewMeta `json:"preview,omitempty"`
|
||||||
Analysis analysisMeta `json:"analysis,omitempty"`
|
Analysis analysisMeta `json:"analysis,omitempty"`
|
||||||
|
Postwork *postworkMeta `json:"postwork,omitempty"`
|
||||||
Validation map[string]any `json:"validation,omitempty"`
|
Validation map[string]any `json:"validation,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,12 +117,29 @@ type aiSegmentMeta struct {
|
|||||||
type aiAnalysisMeta struct {
|
type aiAnalysisMeta struct {
|
||||||
Goal string `json:"goal,omitempty"`
|
Goal string `json:"goal,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
|
Completed bool `json:"completed,omitempty"`
|
||||||
Hits []analyzeHit `json:"hits,omitempty"`
|
Hits []analyzeHit `json:"hits,omitempty"`
|
||||||
Segments []aiSegmentMeta `json:"segments,omitempty"`
|
Segments []aiSegmentMeta `json:"segments,omitempty"`
|
||||||
Rating *aiRatingMeta `json:"rating,omitempty"`
|
Rating *aiRatingMeta `json:"rating,omitempty"`
|
||||||
AnalyzedAtUnix int64 `json:"analyzedAtUnix,omitempty"`
|
AnalyzedAtUnix int64 `json:"analyzedAtUnix,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isZeroPostworkCompletedMeta(v postworkCompletedMeta) bool {
|
||||||
|
return !v.Meta &&
|
||||||
|
!v.Teaser &&
|
||||||
|
!v.Thumb &&
|
||||||
|
!v.Sprites &&
|
||||||
|
!v.Analyze
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZeroPostworkMeta(v *postworkMeta) bool {
|
||||||
|
if v == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isZeroPostworkCompletedMeta(v.Completed) && len(v.History) == 0
|
||||||
|
}
|
||||||
|
|
||||||
func readVideoMeta(metaPath string, fi os.FileInfo) (dur float64, w int, h int, fps float64, ok bool) {
|
func readVideoMeta(metaPath string, fi os.FileInfo) (dur float64, w int, h int, fps float64, ok bool) {
|
||||||
m, ok := readVideoMetaIfValid(metaPath, fi)
|
m, ok := readVideoMetaIfValid(metaPath, fi)
|
||||||
if !ok || m == nil {
|
if !ok || m == nil {
|
||||||
@ -455,6 +478,7 @@ func writeVideoMeta(
|
|||||||
m.Preview = existing.Preview
|
m.Preview = existing.Preview
|
||||||
m.Analysis = existing.Analysis
|
m.Analysis = existing.Analysis
|
||||||
m.Validation = existing.Validation
|
m.Validation = existing.Validation
|
||||||
|
m.Postwork = existing.Postwork
|
||||||
|
|
||||||
if m.File.SourceURL == "" {
|
if m.File.SourceURL == "" {
|
||||||
m.File.SourceURL = existing.File.SourceURL
|
m.File.SourceURL = existing.File.SourceURL
|
||||||
@ -473,6 +497,10 @@ func writeVideoMeta(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isZeroPostworkMeta(m.Postwork) {
|
||||||
|
m.Postwork = nil
|
||||||
|
}
|
||||||
|
|
||||||
buf, err := json.MarshalIndent(m, "", " ")
|
buf, err := json.MarshalIndent(m, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -522,6 +550,7 @@ func writeVideoMetaAI(
|
|||||||
if existing != nil {
|
if existing != nil {
|
||||||
m.Preview = existing.Preview
|
m.Preview = existing.Preview
|
||||||
m.Validation = existing.Validation
|
m.Validation = existing.Validation
|
||||||
|
m.Postwork = existing.Postwork
|
||||||
|
|
||||||
if m.Media.Video.Width <= 0 {
|
if m.Media.Video.Width <= 0 {
|
||||||
m.Media.Video.Width = existing.Media.Video.Width
|
m.Media.Video.Width = existing.Media.Video.Width
|
||||||
@ -552,6 +581,10 @@ func writeVideoMetaAI(
|
|||||||
m.Analysis.Highlights = ai
|
m.Analysis.Highlights = ai
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isZeroPostworkMeta(m.Postwork) {
|
||||||
|
m.Postwork = nil
|
||||||
|
}
|
||||||
|
|
||||||
buf, err := json.MarshalIndent(m, "", " ")
|
buf, err := json.MarshalIndent(m, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -596,6 +629,7 @@ func writeVideoMetaWithPreviewClips(metaPath string, fi os.FileInfo, dur float64
|
|||||||
|
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
m.Analysis = existing.Analysis
|
m.Analysis = existing.Analysis
|
||||||
|
m.Postwork = existing.Postwork
|
||||||
|
|
||||||
if existing.Preview.Sprite != nil && m.Preview.Sprite == nil {
|
if existing.Preview.Sprite != nil && m.Preview.Sprite == nil {
|
||||||
m.Preview.Sprite = existing.Preview.Sprite
|
m.Preview.Sprite = existing.Preview.Sprite
|
||||||
@ -620,6 +654,10 @@ func writeVideoMetaWithPreviewClips(metaPath string, fi os.FileInfo, dur float64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isZeroPostworkMeta(m.Postwork) {
|
||||||
|
m.Postwork = nil
|
||||||
|
}
|
||||||
|
|
||||||
buf, err := json.Marshal(m)
|
buf, err := json.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -675,6 +713,7 @@ func writeVideoMetaWithPreviewClipsAndSprite(
|
|||||||
|
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
m.Analysis = existing.Analysis
|
m.Analysis = existing.Analysis
|
||||||
|
m.Postwork = existing.Postwork
|
||||||
|
|
||||||
m.Preview.Sprite = mergePreviewSpriteMeta(existing.Preview.Sprite, m.Preview.Sprite)
|
m.Preview.Sprite = mergePreviewSpriteMeta(existing.Preview.Sprite, m.Preview.Sprite)
|
||||||
|
|
||||||
@ -704,6 +743,10 @@ func writeVideoMetaWithPreviewClipsAndSprite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isZeroPostworkMeta(m.Postwork) {
|
||||||
|
m.Postwork = nil
|
||||||
|
}
|
||||||
|
|
||||||
buf, err := json.Marshal(m)
|
buf, err := json.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -32,7 +32,6 @@ func setModelStore(store *ModelStore) {
|
|||||||
modelStoreRef = store
|
modelStoreRef = store
|
||||||
setCoverModelStore(store)
|
setCoverModelStore(store)
|
||||||
setChaturbateOnlineModelStore(store)
|
setChaturbateOnlineModelStore(store)
|
||||||
setMyFreeCamsOnlineModelStore(store)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ umbenannt, damit es nicht mit models.go kollidiert
|
// ✅ umbenannt, damit es nicht mit models.go kollidiert
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,6 +20,97 @@ const myfreecamsBioContextURLFmt = "https://api-edge.myfreecams.com/usernameLook
|
|||||||
// getrennt von Chaturbate, damit die Datei unabhängig bleibt
|
// getrennt von Chaturbate, damit die Datei unabhängig bleibt
|
||||||
const myfreecamsBioCacheMaxAge = 10 * time.Minute
|
const myfreecamsBioCacheMaxAge = 10 * time.Minute
|
||||||
|
|
||||||
|
const mfcLookupBusyCooldown = 10 * time.Minute
|
||||||
|
|
||||||
|
var (
|
||||||
|
mfcLookupPauseMu sync.Mutex
|
||||||
|
mfcLookupPausedUntil time.Time
|
||||||
|
mfcLookupPauseReason string
|
||||||
|
)
|
||||||
|
|
||||||
|
func isMFCServersBusyMessage(msg string) bool {
|
||||||
|
msg = strings.ToLower(strings.TrimSpace(msg))
|
||||||
|
return strings.Contains(msg, "servers busy")
|
||||||
|
}
|
||||||
|
|
||||||
|
func pauseMFCLookups(reason string, d time.Duration) {
|
||||||
|
if d <= 0 {
|
||||||
|
d = mfcLookupBusyCooldown
|
||||||
|
}
|
||||||
|
|
||||||
|
reason = strings.TrimSpace(reason)
|
||||||
|
if reason == "" {
|
||||||
|
reason = "servers busy"
|
||||||
|
}
|
||||||
|
|
||||||
|
until := time.Now().Add(d)
|
||||||
|
|
||||||
|
mfcLookupPauseMu.Lock()
|
||||||
|
changed := until.After(mfcLookupPausedUntil)
|
||||||
|
if changed {
|
||||||
|
mfcLookupPausedUntil = until
|
||||||
|
mfcLookupPauseReason = reason
|
||||||
|
}
|
||||||
|
mfcLookupPauseMu.Unlock()
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
appLogln(
|
||||||
|
"⏸️ [myfreecams] usernameLookup pausiert bis",
|
||||||
|
until.Format("15:04:05"),
|
||||||
|
"Grund:",
|
||||||
|
reason,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mfcLookupsPaused() (until time.Time, reason string, paused bool) {
|
||||||
|
mfcLookupPauseMu.Lock()
|
||||||
|
defer mfcLookupPauseMu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if mfcLookupPausedUntil.IsZero() || now.After(mfcLookupPausedUntil) {
|
||||||
|
mfcLookupPausedUntil = time.Time{}
|
||||||
|
mfcLookupPauseReason = ""
|
||||||
|
return time.Time{}, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return mfcLookupPausedUntil, mfcLookupPauseReason, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func mfcLookupPausedError() error {
|
||||||
|
until, reason, paused := mfcLookupsPaused()
|
||||||
|
if !paused {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if reason == "" {
|
||||||
|
reason = "servers busy"
|
||||||
|
}
|
||||||
|
|
||||||
|
return appErrorf(
|
||||||
|
"mfc usernameLookup pausiert bis %s: %s",
|
||||||
|
until.Format("15:04:05"),
|
||||||
|
reason,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearMFCLookupPause(reason string) {
|
||||||
|
reason = strings.TrimSpace(reason)
|
||||||
|
if reason == "" {
|
||||||
|
reason = "manual refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
mfcLookupPauseMu.Lock()
|
||||||
|
wasPaused := !mfcLookupPausedUntil.IsZero()
|
||||||
|
mfcLookupPausedUntil = time.Time{}
|
||||||
|
mfcLookupPauseReason = ""
|
||||||
|
mfcLookupPauseMu.Unlock()
|
||||||
|
|
||||||
|
if wasPaused {
|
||||||
|
appLogln("▶️ [myfreecams] usernameLookup Pause aufgehoben. Grund:", reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MyFreeCamsBioContextResp struct {
|
type MyFreeCamsBioContextResp struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
FetchedAt time.Time `json:"fetchedAt"`
|
FetchedAt time.Time `json:"fetchedAt"`
|
||||||
@ -38,6 +130,8 @@ type MyFreeCamsBioContextResp struct {
|
|||||||
Gender string `json:"gender,omitempty"`
|
Gender string `json:"gender,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mfcMaybeString string
|
||||||
|
|
||||||
type mfcBioLookupResp struct {
|
type mfcBioLookupResp struct {
|
||||||
Result struct {
|
Result struct {
|
||||||
Success int `json:"success"`
|
Success int `json:"success"`
|
||||||
@ -90,6 +184,31 @@ type mfcBioLookupResp struct {
|
|||||||
Err int `json:"err"`
|
Err int `json:"err"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *mfcMaybeString) UnmarshalJSON(b []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(b))
|
||||||
|
|
||||||
|
switch raw {
|
||||||
|
case "", "null", "false", "true":
|
||||||
|
*s = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var text string
|
||||||
|
if err := json.Unmarshal(b, &text); err == nil {
|
||||||
|
*s = mfcMaybeString(strings.TrimSpace(text))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MFC liefert gelegentlich unerwartete Typen.
|
||||||
|
// Nicht den ganzen usernameLookup scheitern lassen.
|
||||||
|
*s = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s mfcMaybeString) String() string {
|
||||||
|
return strings.TrimSpace(string(s))
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeMFCBioRoomStatus(vstate int, hasSession bool) string {
|
func normalizeMFCBioRoomStatus(vstate int, hasSession bool) string {
|
||||||
if !hasSession {
|
if !hasSession {
|
||||||
return "offline"
|
return "offline"
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
// backend/myfreecams_lookup_pause.go
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const mfcLookupBusyCooldown = 10 * time.Minute
|
|
||||||
|
|
||||||
var (
|
|
||||||
mfcLookupPauseMu sync.Mutex
|
|
||||||
mfcLookupPausedUntil time.Time
|
|
||||||
mfcLookupPauseReason string
|
|
||||||
)
|
|
||||||
|
|
||||||
func isMFCServersBusyMessage(msg string) bool {
|
|
||||||
msg = strings.ToLower(strings.TrimSpace(msg))
|
|
||||||
return strings.Contains(msg, "servers busy")
|
|
||||||
}
|
|
||||||
|
|
||||||
func pauseMFCLookups(reason string, d time.Duration) {
|
|
||||||
if d <= 0 {
|
|
||||||
d = mfcLookupBusyCooldown
|
|
||||||
}
|
|
||||||
|
|
||||||
reason = strings.TrimSpace(reason)
|
|
||||||
if reason == "" {
|
|
||||||
reason = "servers busy"
|
|
||||||
}
|
|
||||||
|
|
||||||
until := time.Now().Add(d)
|
|
||||||
|
|
||||||
mfcLookupPauseMu.Lock()
|
|
||||||
changed := until.After(mfcLookupPausedUntil)
|
|
||||||
if changed {
|
|
||||||
mfcLookupPausedUntil = until
|
|
||||||
mfcLookupPauseReason = reason
|
|
||||||
}
|
|
||||||
mfcLookupPauseMu.Unlock()
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
appLogln(
|
|
||||||
"⏸️ [myfreecams] usernameLookup pausiert bis",
|
|
||||||
until.Format("15:04:05"),
|
|
||||||
"Grund:",
|
|
||||||
reason,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mfcLookupsPaused() (until time.Time, reason string, paused bool) {
|
|
||||||
mfcLookupPauseMu.Lock()
|
|
||||||
defer mfcLookupPauseMu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if mfcLookupPausedUntil.IsZero() || now.After(mfcLookupPausedUntil) {
|
|
||||||
mfcLookupPausedUntil = time.Time{}
|
|
||||||
mfcLookupPauseReason = ""
|
|
||||||
return time.Time{}, "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return mfcLookupPausedUntil, mfcLookupPauseReason, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func mfcLookupPausedError() error {
|
|
||||||
until, reason, paused := mfcLookupsPaused()
|
|
||||||
if !paused {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if reason == "" {
|
|
||||||
reason = "servers busy"
|
|
||||||
}
|
|
||||||
|
|
||||||
return appErrorf(
|
|
||||||
"mfc usernameLookup pausiert bis %s: %s",
|
|
||||||
until.Format("15:04:05"),
|
|
||||||
reason,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearMFCLookupPause(reason string) {
|
|
||||||
reason = strings.TrimSpace(reason)
|
|
||||||
if reason == "" {
|
|
||||||
reason = "manual refresh"
|
|
||||||
}
|
|
||||||
|
|
||||||
mfcLookupPauseMu.Lock()
|
|
||||||
wasPaused := !mfcLookupPausedUntil.IsZero()
|
|
||||||
mfcLookupPausedUntil = time.Time{}
|
|
||||||
mfcLookupPauseReason = ""
|
|
||||||
mfcLookupPauseMu.Unlock()
|
|
||||||
|
|
||||||
if wasPaused {
|
|
||||||
appLogln("▶️ [myfreecams] usernameLookup Pause aufgehoben. Grund:", reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,736 +0,0 @@
|
|||||||
// backend/myfreecams_online.go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const myfreecamsOnlineLookupURLFmt = "https://api-edge.myfreecams.com/usernameLookup/%s"
|
|
||||||
|
|
||||||
const (
|
|
||||||
mfcOnlinePollInterval = 5 * time.Minute
|
|
||||||
mfcOnlineMaxConcurrent = 1
|
|
||||||
mfcOnlinePerModelTimeout = 8 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type mfcMaybeString string
|
|
||||||
|
|
||||||
func (s *mfcMaybeString) UnmarshalJSON(b []byte) error {
|
|
||||||
raw := strings.TrimSpace(string(b))
|
|
||||||
|
|
||||||
switch raw {
|
|
||||||
case "", "null", "false", "true":
|
|
||||||
*s = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var text string
|
|
||||||
if err := json.Unmarshal(b, &text); err == nil {
|
|
||||||
*s = mfcMaybeString(strings.TrimSpace(text))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MFC liefert gelegentlich unerwartete Typen.
|
|
||||||
// Nicht den ganzen usernameLookup scheitern lassen.
|
|
||||||
*s = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s mfcMaybeString) String() string {
|
|
||||||
return strings.TrimSpace(string(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
type MyFreeCamsOnlineRoomLite struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
CurrentShow string `json:"current_show"` // public/private/offline/unknown
|
|
||||||
ChatRoomURL string `json:"chat_room_url"`
|
|
||||||
ImageURL string `json:"image_url"`
|
|
||||||
|
|
||||||
Gender string `json:"gender,omitempty"`
|
|
||||||
Country string `json:"country,omitempty"`
|
|
||||||
RoomTopic string `json:"room_topic,omitempty"`
|
|
||||||
RoomCount int `json:"room_count,omitempty"`
|
|
||||||
Tags []string `json:"tags,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type myfreecamsOnlineCache struct {
|
|
||||||
LiteByUser map[string]MyFreeCamsOnlineRoomLite
|
|
||||||
|
|
||||||
FetchedAt time.Time
|
|
||||||
LastAttempt time.Time
|
|
||||||
LastErr string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
mfcOnlineHTTP = &http.Client{Timeout: mfcOnlinePerModelTimeout + 2*time.Second}
|
|
||||||
|
|
||||||
mfcOnlineMu sync.RWMutex
|
|
||||||
mfcOnline myfreecamsOnlineCache
|
|
||||||
|
|
||||||
mfcOnlineModelStore *ModelStore
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mfcOnlineRefreshMu sync.Mutex
|
|
||||||
mfcOnlineRefreshInFlight bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func setMyFreeCamsOnlineModelStore(store *ModelStore) {
|
|
||||||
mfcOnlineModelStore = store
|
|
||||||
}
|
|
||||||
|
|
||||||
type mfcOnlineLookupResp struct {
|
|
||||||
Result struct {
|
|
||||||
Success int `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
|
|
||||||
User struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
UserID int `json:"user_id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Avatar mfcMaybeString `json:"avatar"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
|
|
||||||
Sessions []struct {
|
|
||||||
SessionID int `json:"session_id"`
|
|
||||||
VState int `json:"vstate"`
|
|
||||||
RoomCount int `json:"room_count"`
|
|
||||||
RoomTopic string `json:"room_topic"`
|
|
||||||
ServerName string `json:"server_name"`
|
|
||||||
Phase string `json:"phase"`
|
|
||||||
SnapURL string `json:"snap_url"`
|
|
||||||
} `json:"sessions"`
|
|
||||||
|
|
||||||
Profile struct {
|
|
||||||
Gender string `json:"gender"`
|
|
||||||
Country string `json:"country"`
|
|
||||||
} `json:"profile"`
|
|
||||||
} `json:"user"`
|
|
||||||
} `json:"result"`
|
|
||||||
|
|
||||||
Err int `json:"err"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func mfcOnlineDigitsOnly(s string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, r := range strings.TrimSpace(s) {
|
|
||||||
if r >= '0' && r <= '9' {
|
|
||||||
b.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeMFCOnlineImageURL(raw string) string {
|
|
||||||
raw = strings.TrimSpace(raw)
|
|
||||||
if raw == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(raw, "//") {
|
|
||||||
raw = "https:" + raw
|
|
||||||
} else if strings.HasPrefix(raw, "/") {
|
|
||||||
raw = "https://snap.mfcimg.com" + raw
|
|
||||||
} else if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
|
|
||||||
raw = "https://snap.mfcimg.com/" + strings.TrimLeft(raw, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(raw)
|
|
||||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("no-cache", strconv.FormatInt(time.Now().UnixNano(), 10))
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMFCOnlineSnapimgURL(serverName string, rawUserID int) string {
|
|
||||||
serverID := mfcOnlineDigitsOnly(serverName)
|
|
||||||
if serverID == "" || rawUserID <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeMFCOnlineImageURL(fmt.Sprintf(
|
|
||||||
"https://snap.mfcimg.com/snapimg/%s/853x480/mfc_%d",
|
|
||||||
serverID,
|
|
||||||
rawUserID,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeMFCOnlineRoomStatus(hasSession bool, vstate int) string {
|
|
||||||
if !hasSession {
|
|
||||||
return "offline"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deine Beispiele:
|
|
||||||
// vstate=0 => public
|
|
||||||
// vstate=14 => private show
|
|
||||||
if vstate == 0 {
|
|
||||||
return "public"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "private"
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectMFCOnlineImageURL(userID int, avatarURL string, sessServerName string, sessSnapURL string) string {
|
|
||||||
// Bevorzugt die stabile Live-Snap-URL:
|
|
||||||
// https://snap.mfcimg.com/snapimg/<server>/853x480/mfc_<userID>
|
|
||||||
if img := buildMFCOnlineSnapimgURL(sessServerName, userID); img != "" {
|
|
||||||
return img
|
|
||||||
}
|
|
||||||
|
|
||||||
if img := normalizeMFCOnlineImageURL(sessSnapURL); img != "" {
|
|
||||||
return img
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeMFCOnlineImageURL(avatarURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchMyFreeCamsOnlineModel(ctx context.Context, model string) (MyFreeCamsOnlineRoomLite, []byte, error) {
|
|
||||||
model = strings.TrimSpace(model)
|
|
||||||
if model == "" {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, nil, appErrorf("mfc model leer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mfcLookupPausedError(); err != nil {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreamURL := fmt.Sprintf(myfreecamsOnlineLookupURLFmt, url.PathEscape(model))
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstreamURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
|
|
||||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
|
||||||
req.Header.Set("Referer", "https://www.myfreecams.com/#"+model)
|
|
||||||
req.Header.Set("Origin", "https://www.myfreecams.com")
|
|
||||||
|
|
||||||
resp, err := mfcOnlineHTTP.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf(
|
|
||||||
"mfc usernameLookup %s HTTP %d: %s",
|
|
||||||
model,
|
|
||||||
resp.StatusCode,
|
|
||||||
strings.TrimSpace(string(raw)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data mfcOnlineLookupResp
|
|
||||||
if err := json.Unmarshal(raw, &data); err != nil {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf("mfc usernameLookup json %s: %w", model, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user := data.Result.User
|
|
||||||
avatarURL := user.Avatar.String()
|
|
||||||
username := strings.TrimSpace(user.Username)
|
|
||||||
if username == "" {
|
|
||||||
username = model
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Result.Success == 0 {
|
|
||||||
msg := strings.TrimSpace(data.Result.Message)
|
|
||||||
if msg == "" {
|
|
||||||
msg = "lookup failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
if isMFCServersBusyMessage(msg) {
|
|
||||||
pauseMFCLookups(msg, mfcLookupBusyCooldown)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf(
|
|
||||||
"mfc usernameLookup %s: %s",
|
|
||||||
model,
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.ID <= 0 {
|
|
||||||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf(
|
|
||||||
"mfc usernameLookup %s: missing user.id",
|
|
||||||
model,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSession := len(user.Sessions) > 0
|
|
||||||
|
|
||||||
var (
|
|
||||||
vstate int
|
|
||||||
roomCount int
|
|
||||||
roomTopic string
|
|
||||||
serverName string
|
|
||||||
snapURL string
|
|
||||||
)
|
|
||||||
|
|
||||||
if hasSession {
|
|
||||||
sess := user.Sessions[0]
|
|
||||||
vstate = sess.VState
|
|
||||||
roomCount = sess.RoomCount
|
|
||||||
roomTopic = strings.TrimSpace(sess.RoomTopic)
|
|
||||||
serverName = strings.TrimSpace(sess.ServerName)
|
|
||||||
snapURL = strings.TrimSpace(sess.SnapURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
status := normalizeMFCOnlineRoomStatus(hasSession, vstate)
|
|
||||||
imageURL := selectMFCOnlineImageURL(user.ID, avatarURL, serverName, snapURL)
|
|
||||||
|
|
||||||
return MyFreeCamsOnlineRoomLite{
|
|
||||||
Username: username,
|
|
||||||
CurrentShow: status,
|
|
||||||
ChatRoomURL: "https://www.myfreecams.com/#" + username,
|
|
||||||
ImageURL: imageURL,
|
|
||||||
Gender: strings.TrimSpace(user.Profile.Gender),
|
|
||||||
Country: strings.TrimSpace(user.Profile.Country),
|
|
||||||
RoomTopic: roomTopic,
|
|
||||||
RoomCount: roomCount,
|
|
||||||
Tags: user.Tags,
|
|
||||||
}, raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listMyFreeCamsStoreModels(store *ModelStore) []StoredModel {
|
|
||||||
if store == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
all := store.List()
|
|
||||||
out := make([]StoredModel, 0, len(all))
|
|
||||||
seen := map[string]bool{}
|
|
||||||
|
|
||||||
for _, m := range all {
|
|
||||||
host := canonicalHost(m.Host)
|
|
||||||
if host != "myfreecams.com" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
modelKey := strings.TrimSpace(m.ModelKey)
|
|
||||||
if modelKey == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
k := strings.ToLower(modelKey)
|
|
||||||
if seen[k] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[k] = true
|
|
||||||
|
|
||||||
out = append(out, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshMyFreeCamsSnapshotNow(ctx context.Context, store *ModelStore) (time.Time, int, error) {
|
|
||||||
if store == nil {
|
|
||||||
return time.Time{}, 0, appErrorf("mfc online: model store nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
mfcOnlineMu.Lock()
|
|
||||||
mfcOnline.LastAttempt = time.Now()
|
|
||||||
mfcOnlineMu.Unlock()
|
|
||||||
|
|
||||||
models := listMyFreeCamsStoreModels(store)
|
|
||||||
fetchedAt := time.Now().UTC()
|
|
||||||
|
|
||||||
if err := mfcLookupPausedError(); err != nil {
|
|
||||||
mfcOnlineMu.Lock()
|
|
||||||
mfcOnline.LastAttempt = time.Now()
|
|
||||||
mfcOnline.LastErr = err.Error()
|
|
||||||
mfcOnlineMu.Unlock()
|
|
||||||
|
|
||||||
return fetchedAt, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
liteByUser := make(map[string]MyFreeCamsOnlineRoomLite, len(models))
|
|
||||||
|
|
||||||
// Vorherigen Snapshot behalten, damit einzelne Timeout-Fehler nicht sofort
|
|
||||||
// alle bekannten MFC-Online-Daten aus dem Cache löschen.
|
|
||||||
mfcOnlineMu.RLock()
|
|
||||||
for k, v := range mfcOnline.LiteByUser {
|
|
||||||
liteByUser[k] = v
|
|
||||||
}
|
|
||||||
mfcOnlineMu.RUnlock()
|
|
||||||
|
|
||||||
if len(models) == 0 {
|
|
||||||
mfcOnlineMu.Lock()
|
|
||||||
mfcOnline.LiteByUser = liteByUser
|
|
||||||
mfcOnline.FetchedAt = fetchedAt
|
|
||||||
mfcOnline.LastErr = ""
|
|
||||||
mfcOnlineMu.Unlock()
|
|
||||||
|
|
||||||
return fetchedAt, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type result struct {
|
|
||||||
model StoredModel
|
|
||||||
room MyFreeCamsOnlineRoomLite
|
|
||||||
raw []byte
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
sem := make(chan struct{}, mfcOnlineMaxConcurrent)
|
|
||||||
resCh := make(chan result, len(models))
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for _, m := range models {
|
|
||||||
m := m
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
resCh <- result{model: m, err: ctx.Err()}
|
|
||||||
return
|
|
||||||
case sem <- struct{}{}:
|
|
||||||
}
|
|
||||||
defer func() { <-sem }()
|
|
||||||
|
|
||||||
if err := mfcLookupPausedError(); err != nil {
|
|
||||||
resCh <- result{model: m, err: err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reqCtx, reqCancel := context.WithTimeout(ctx, mfcOnlinePerModelTimeout)
|
|
||||||
room, raw, err := fetchMyFreeCamsOnlineModel(reqCtx, m.ModelKey)
|
|
||||||
reqCancel()
|
|
||||||
resCh <- result{
|
|
||||||
model: m,
|
|
||||||
room: room,
|
|
||||||
raw: raw,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(resCh)
|
|
||||||
|
|
||||||
var (
|
|
||||||
firstErr error
|
|
||||||
okCount int
|
|
||||||
failedCount int
|
|
||||||
timeoutCount int
|
|
||||||
)
|
|
||||||
|
|
||||||
for res := range resCh {
|
|
||||||
modelKey := strings.TrimSpace(res.model.ModelKey)
|
|
||||||
if modelKey == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.err != nil {
|
|
||||||
failedCount++
|
|
||||||
|
|
||||||
if errors.Is(res.err, context.DeadlineExceeded) {
|
|
||||||
timeoutCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if firstErr == nil {
|
|
||||||
firstErr = res.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kein Log pro Model mehr, sonst spammt es bei vielen Timeouts.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
room := res.room
|
|
||||||
if strings.TrimSpace(room.Username) == "" {
|
|
||||||
room.Username = modelKey
|
|
||||||
}
|
|
||||||
|
|
||||||
key := strings.ToLower(strings.TrimSpace(room.Username))
|
|
||||||
if key == "" {
|
|
||||||
key = strings.ToLower(modelKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
liteByUser[key] = room
|
|
||||||
okCount++
|
|
||||||
|
|
||||||
isOnline := room.CurrentShow != "offline" && room.CurrentShow != "unknown"
|
|
||||||
|
|
||||||
_ = store.SetChaturbateRoomState(
|
|
||||||
"myfreecams.com",
|
|
||||||
modelKey,
|
|
||||||
room.CurrentShow,
|
|
||||||
isOnline,
|
|
||||||
room.ChatRoomURL,
|
|
||||||
room.ImageURL,
|
|
||||||
fetchedAt,
|
|
||||||
)
|
|
||||||
|
|
||||||
_ = store.SetLastSeenOnline(
|
|
||||||
"myfreecams.com",
|
|
||||||
modelKey,
|
|
||||||
isOnline,
|
|
||||||
fetchedAt.Format(time.RFC3339Nano),
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(res.raw) > 0 {
|
|
||||||
_ = store.SetBioContext(
|
|
||||||
"myfreecams.com",
|
|
||||||
modelKey,
|
|
||||||
string(res.raw),
|
|
||||||
fetchedAt.Format(time.RFC3339Nano),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(room.ImageURL) != "" {
|
|
||||||
_ = store.SetProfileImageURLOnly(
|
|
||||||
"myfreecams.com",
|
|
||||||
modelKey,
|
|
||||||
room.ImageURL,
|
|
||||||
fetchedAt.Format(time.RFC3339Nano),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr := ""
|
|
||||||
if firstErr != nil {
|
|
||||||
lastErr = fmt.Sprintf(
|
|
||||||
"%d/%d lookups failed (%d timeouts); first error: %v",
|
|
||||||
failedCount,
|
|
||||||
len(models),
|
|
||||||
timeoutCount,
|
|
||||||
firstErr,
|
|
||||||
)
|
|
||||||
|
|
||||||
appLogln("⚠️ [myfreecams] online refresh partial:", lastErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
mfcOnlineMu.Lock()
|
|
||||||
mfcOnline.LiteByUser = liteByUser
|
|
||||||
mfcOnline.FetchedAt = fetchedAt
|
|
||||||
mfcOnline.LastErr = lastErr
|
|
||||||
mfcOnlineMu.Unlock()
|
|
||||||
|
|
||||||
// Nur hart fehlschlagen, wenn wirklich gar kein Model erfolgreich geprüft wurde.
|
|
||||||
if okCount == 0 && firstErr != nil {
|
|
||||||
return fetchedAt, okCount, firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchedAt, okCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startMyFreeCamsOnlinePoller(store *ModelStore) {
|
|
||||||
const interval = mfcOnlinePollInterval
|
|
||||||
|
|
||||||
if store != nil {
|
|
||||||
setMyFreeCamsOnlineModelStore(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastLoggedErr := ""
|
|
||||||
|
|
||||||
refreshOnce := func(trigger string) {
|
|
||||||
if !myFreeCamsProviderAPIEnabled() {
|
|
||||||
if verboseLogs() {
|
|
||||||
appLogln("ℹ️ [myfreecams] online refresh skipped: provider API disabled")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mfcOnlineRefreshMu.Lock()
|
|
||||||
if mfcOnlineRefreshInFlight {
|
|
||||||
mfcOnlineRefreshMu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mfcOnlineRefreshInFlight = true
|
|
||||||
mfcOnlineRefreshMu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
mfcOnlineRefreshMu.Lock()
|
|
||||||
mfcOnlineRefreshInFlight = false
|
|
||||||
mfcOnlineRefreshMu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
fetchedAt, count, err := refreshMyFreeCamsSnapshotNow(context.Background(), store)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
msg := err.Error()
|
|
||||||
|
|
||||||
if strings.Contains(msg, "usernameLookup pausiert") {
|
|
||||||
if verboseLogs() && msg != lastLoggedErr {
|
|
||||||
appLogln("⏸️ [myfreecams] online refresh skipped:", msg)
|
|
||||||
lastLoggedErr = msg
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg != lastLoggedErr {
|
|
||||||
appLogln("⚠️ [myfreecams] online refresh failed:", msg)
|
|
||||||
lastLoggedErr = msg
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastLoggedErr != "" {
|
|
||||||
appLogln("✅ [myfreecams] online refresh recovered")
|
|
||||||
lastLoggedErr = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if verboseLogs() {
|
|
||||||
appLogln(
|
|
||||||
"✅ [myfreecams] checked models:",
|
|
||||||
count,
|
|
||||||
"snapshot",
|
|
||||||
fetchedAt.Format(time.RFC3339),
|
|
||||||
"trigger="+trigger,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshOnce("startup")
|
|
||||||
|
|
||||||
ticker := time.NewTicker(interval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for range ticker.C {
|
|
||||||
refreshOnce("ticker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type mfcOnlineResponse struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
FetchedAt time.Time `json:"fetchedAt"`
|
|
||||||
LastAttempt time.Time `json:"lastAttempt"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
LastError string `json:"lastError"`
|
|
||||||
Rooms []MyFreeCamsOnlineRoomLite `json:"rooms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func myfreecamsOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Nur GET/POST erlaubt", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wantRefresh := false
|
|
||||||
var users []string
|
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
|
||||||
var req struct {
|
|
||||||
Q []string `json:"q"`
|
|
||||||
Refresh bool `json:"refresh"`
|
|
||||||
}
|
|
||||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
wantRefresh = req.Refresh
|
|
||||||
for _, u := range req.Q {
|
|
||||||
u = strings.ToLower(strings.TrimSpace(u))
|
|
||||||
if u != "" {
|
|
||||||
users = append(users, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qRefresh := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("refresh")))
|
|
||||||
wantRefresh = qRefresh == "1" || qRefresh == "true" || qRefresh == "yes"
|
|
||||||
|
|
||||||
qUsers := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
||||||
if qUsers != "" {
|
|
||||||
for _, u := range strings.Split(qUsers, ",") {
|
|
||||||
u = strings.ToLower(strings.TrimSpace(u))
|
|
||||||
if u != "" {
|
|
||||||
users = append(users, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
store := mfcOnlineModelStore
|
|
||||||
if store == nil {
|
|
||||||
store = getModelStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
if wantRefresh && store != nil {
|
|
||||||
if !myFreeCamsProviderAPIEnabled() {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
|
||||||
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{
|
|
||||||
Enabled: false,
|
|
||||||
LastError: "MyFreeCams API ist in den Einstellungen deaktiviert",
|
|
||||||
Rooms: []MyFreeCamsOnlineRoomLite{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mfcOnlineRefreshMu.Lock()
|
|
||||||
if !mfcOnlineRefreshInFlight {
|
|
||||||
mfcOnlineRefreshInFlight = true
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
mfcOnlineRefreshMu.Lock()
|
|
||||||
mfcOnlineRefreshInFlight = false
|
|
||||||
mfcOnlineRefreshMu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, _, _ = refreshMyFreeCamsSnapshotNow(context.Background(), store)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
mfcOnlineRefreshMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
mfcOnlineMu.RLock()
|
|
||||||
fetchedAt := mfcOnline.FetchedAt
|
|
||||||
lastAttempt := mfcOnline.LastAttempt
|
|
||||||
lastErr := mfcOnline.LastErr
|
|
||||||
liteByUser := mfcOnline.LiteByUser
|
|
||||||
mfcOnlineMu.RUnlock()
|
|
||||||
|
|
||||||
total := 0
|
|
||||||
if liteByUser != nil {
|
|
||||||
total = len(liteByUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
rooms := make([]MyFreeCamsOnlineRoomLite, 0)
|
|
||||||
|
|
||||||
if len(users) > 0 {
|
|
||||||
for _, u := range users {
|
|
||||||
if rm, ok := liteByUser[u]; ok {
|
|
||||||
rooms = append(rooms, rm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fetchedAt.IsZero() && lastErr == "" {
|
|
||||||
lastErr = "warming up"
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{
|
|
||||||
Enabled: true,
|
|
||||||
FetchedAt: fetchedAt,
|
|
||||||
LastAttempt: lastAttempt,
|
|
||||||
Count: len(rooms),
|
|
||||||
Total: total,
|
|
||||||
LastError: lastErr,
|
|
||||||
Rooms: rooms,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1030,60 +1030,28 @@ func applyFinishedPhaseTruthToRecordJobMeta(j *RecordJob) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := canonicalAssetIDFromName(outPath)
|
id := strings.TrimSpace(canonicalAssetIDFromName(outPath))
|
||||||
id = strings.TrimSpace(id)
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
truth := finishedPhaseTruthForID(id)
|
truth := finishedPhaseTruthForID(id)
|
||||||
|
|
||||||
rv := reflect.ValueOf(j)
|
if j.Meta == nil {
|
||||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
j.Meta = &videoMeta{}
|
||||||
return
|
|
||||||
}
|
|
||||||
sv := rv.Elem()
|
|
||||||
if !sv.IsValid() || sv.Kind() != reflect.Struct {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fv := sv.FieldByName("Meta")
|
if j.Meta.Postwork == nil {
|
||||||
if !fv.IsValid() || !fv.CanSet() {
|
j.Meta.Postwork = &postworkMeta{}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var raw any
|
j.Meta.Postwork.Completed = postworkCompletedMeta{
|
||||||
switch fv.Kind() {
|
Meta: truth.MetaReady,
|
||||||
case reflect.Interface:
|
Teaser: truth.TeaserReady,
|
||||||
if fv.IsNil() {
|
Thumb: truth.ThumbReady,
|
||||||
raw = nil
|
Sprites: truth.SpritesReady,
|
||||||
} else {
|
Analyze: truth.AnalyzeReady,
|
||||||
raw = fv.Interface()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
raw = fv.Interface()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := metaMapFromAny(raw)
|
|
||||||
if meta == nil {
|
|
||||||
meta = map[string]any{}
|
|
||||||
}
|
|
||||||
|
|
||||||
postworkMap, _ := meta["postwork"].(map[string]any)
|
|
||||||
if postworkMap == nil {
|
|
||||||
postworkMap = map[string]any{}
|
|
||||||
}
|
|
||||||
|
|
||||||
postworkMap["completed"] = map[string]any{
|
|
||||||
"meta": truth.MetaReady,
|
|
||||||
"teaser": truth.TeaserReady,
|
|
||||||
"thumb": truth.ThumbReady,
|
|
||||||
"sprites": truth.SpritesReady,
|
|
||||||
"analyze": truth.AnalyzeReady,
|
|
||||||
}
|
|
||||||
|
|
||||||
meta["postwork"] = postworkMap
|
|
||||||
setStructFieldJSONMap(fv, meta)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Handlers ----------------
|
// ---------------- Handlers ----------------
|
||||||
|
|||||||
@ -104,7 +104,6 @@ func registerRoutes(mux *http.ServeMux, auth *AuthManager) *ModelStore {
|
|||||||
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
||||||
api.HandleFunc("/api/chaturbate/biocontext", chaturbateBioContextHandler)
|
api.HandleFunc("/api/chaturbate/biocontext", chaturbateBioContextHandler)
|
||||||
|
|
||||||
api.HandleFunc("/api/myfreecams/online", myfreecamsOnlineHandler)
|
|
||||||
api.HandleFunc("/api/myfreecams/biocontext", myfreecamsBioContextHandler)
|
api.HandleFunc("/api/myfreecams/biocontext", myfreecamsBioContextHandler)
|
||||||
|
|
||||||
api.HandleFunc("/api/generated/teaser", generatedTeaser)
|
api.HandleFunc("/api/generated/teaser", generatedTeaser)
|
||||||
@ -135,7 +134,6 @@ func registerRoutes(mux *http.ServeMux, auth *AuthManager) *ModelStore {
|
|||||||
setCoverModelStore(store)
|
setCoverModelStore(store)
|
||||||
RegisterModelAPI(api, store)
|
RegisterModelAPI(api, store)
|
||||||
setChaturbateOnlineModelStore(store)
|
setChaturbateOnlineModelStore(store)
|
||||||
setMyFreeCamsOnlineModelStore(store)
|
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// 4) Mount Protected API
|
// 4) Mount Protected API
|
||||||
|
|||||||
@ -844,7 +844,6 @@ func main() {
|
|||||||
// Hintergrund-Worker
|
// Hintergrund-Worker
|
||||||
go startDiskSpaceGuard()
|
go startDiskSpaceGuard()
|
||||||
go startChaturbateOnlinePoller(store)
|
go startChaturbateOnlinePoller(store)
|
||||||
go startMyFreeCamsOnlinePoller(store)
|
|
||||||
go startChaturbateAutoStartWorker(store)
|
go startChaturbateAutoStartWorker(store)
|
||||||
go startMyFreeCamsAutoStartWorker(store)
|
go startMyFreeCamsAutoStartWorker(store)
|
||||||
|
|
||||||
|
|||||||
@ -560,7 +560,6 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if newStore != nil {
|
if newStore != nil {
|
||||||
setModelStore(newStore)
|
setModelStore(newStore)
|
||||||
setChaturbateOnlineModelStore(newStore)
|
setChaturbateOnlineModelStore(newStore)
|
||||||
setMyFreeCamsOnlineModelStore(newStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ffmpeg/ffprobe nach Änderungen neu bestimmen
|
// ✅ ffmpeg/ffprobe nach Änderungen neu bestimmen
|
||||||
|
|||||||
@ -1304,7 +1304,7 @@ export default function FinishedVideoPreview({
|
|||||||
<div
|
<div
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className={[
|
className={[
|
||||||
'group bg-gray-100 dark:bg-white/5 overflow-hidden relative',
|
'group bg-gray-100 rounded-t-lg dark:bg-white/5 overflow-hidden relative',
|
||||||
'select-none touch-manipulation',
|
'select-none touch-manipulation',
|
||||||
sizeClass,
|
sizeClass,
|
||||||
className ?? '',
|
className ?? '',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user