updated
This commit is contained in:
parent
d578d4e6aa
commit
0fac07f620
@ -29,14 +29,14 @@ type Model struct {
|
|||||||
Liked *bool `json:"liked"` // nil = keine Angabe
|
Liked *bool `json:"liked"` // nil = keine Angabe
|
||||||
}
|
}
|
||||||
|
|
||||||
type modelStore struct {
|
type jsonModelStore struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
path string
|
path string
|
||||||
loaded bool
|
loaded bool
|
||||||
items []Model
|
items []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
var models = &modelStore{
|
var models = &jsonModelStore{
|
||||||
path: filepath.Join("data", "models.json"),
|
path: filepath.Join("data", "models.json"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -262,7 +262,12 @@ func RegisterModelAPI(mux *http.ServeMux, store *ModelStore) {
|
|||||||
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
modelsWriteJSON(w, http.StatusOK, store.List())
|
|
||||||
|
// ✅ Wenn du List() als ([]T, error) hast -> Fehler sichtbar machen:
|
||||||
|
// Falls List() aktuell nur []T zurückgibt, siehe Schritt 2 unten.
|
||||||
|
list := store.List()
|
||||||
|
|
||||||
|
modelsWriteJSON(w, http.StatusOK, list)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ Profilbild-Blob aus DB ausliefern
|
// ✅ Profilbild-Blob aus DB ausliefern
|
||||||
|
|||||||
@ -3,7 +3,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@ -25,6 +27,11 @@ type StoredModel struct {
|
|||||||
LastSeenOnline *bool `json:"lastSeenOnline,omitempty"` // nil = unbekannt
|
LastSeenOnline *bool `json:"lastSeenOnline,omitempty"` // nil = unbekannt
|
||||||
LastSeenOnlineAt string `json:"lastSeenOnlineAt,omitempty"` // RFC3339Nano
|
LastSeenOnlineAt string `json:"lastSeenOnlineAt,omitempty"` // RFC3339Nano
|
||||||
|
|
||||||
|
// ✅ Chaturbate Online Snapshot (persistiert aus chaturbate_online.go)
|
||||||
|
CbOnlineJSON string `json:"cbOnlineJson,omitempty"`
|
||||||
|
CbOnlineFetchedAt string `json:"cbOnlineFetchedAt,omitempty"`
|
||||||
|
CbOnlineLastError string `json:"cbOnlineLastError,omitempty"`
|
||||||
|
|
||||||
ProfileImageURL string `json:"profileImageUrl,omitempty"`
|
ProfileImageURL string `json:"profileImageUrl,omitempty"`
|
||||||
ProfileImageCached string `json:"profileImageCached,omitempty"` // z.B. /api/models/image?id=...
|
ProfileImageCached string `json:"profileImageCached,omitempty"` // z.B. /api/models/image?id=...
|
||||||
ProfileImageUpdatedAt string `json:"profileImageUpdatedAt,omitempty"` // RFC3339Nano
|
ProfileImageUpdatedAt string `json:"profileImageUpdatedAt,omitempty"` // RFC3339Nano
|
||||||
@ -776,15 +783,18 @@ UPDATE models SET
|
|||||||
|
|
||||||
-- ✅ last_stream ist timestamptz: nur setzen, wenn aktuell NULL und wir einen gültigen Wert haben
|
-- ✅ last_stream ist timestamptz: nur setzen, wenn aktuell NULL und wir einen gültigen Wert haben
|
||||||
last_stream = CASE
|
last_stream = CASE
|
||||||
WHEN last_stream IS NULL AND $6 IS NOT NULL THEN $6
|
WHEN last_stream IS NULL AND $6::timestamptz IS NOT NULL THEN $6::timestamptz
|
||||||
ELSE last_stream
|
ELSE last_stream
|
||||||
END,
|
END,
|
||||||
|
|
||||||
watching = CASE WHEN $7=true THEN true ELSE watching END,
|
watching = CASE WHEN $7=true THEN true ELSE watching END,
|
||||||
favorite = CASE WHEN $8=true THEN true ELSE favorite END,
|
favorite = CASE WHEN $8=true THEN true ELSE favorite END,
|
||||||
hot = CASE WHEN $9=true THEN true ELSE hot END,
|
hot = CASE WHEN $9=true THEN true ELSE hot END,
|
||||||
keep = CASE WHEN $10=true THEN true ELSE keep END,
|
keep = CASE WHEN $10=true THEN true ELSE keep END,
|
||||||
liked = CASE WHEN liked IS NULL AND $11 IS NOT NULL THEN $11 ELSE liked END,
|
liked = CASE
|
||||||
|
WHEN liked IS NULL AND $11::boolean IS NOT NULL THEN $11::boolean
|
||||||
|
ELSE liked
|
||||||
|
END,
|
||||||
|
|
||||||
updated_at = CASE WHEN updated_at < $12 THEN $12 ELSE updated_at END
|
updated_at = CASE WHEN updated_at < $12 THEN $12 ELSE updated_at END
|
||||||
WHERE id = $13;
|
WHERE id = $13;
|
||||||
@ -817,12 +827,21 @@ func (s *ModelStore) List() []StoredModel {
|
|||||||
return []StoredModel{}
|
return []StoredModel{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ last_stream ist TIMESTAMPTZ -> direkt lesen (NullTime), niemals COALESCE(...,'')
|
q1 := `
|
||||||
rows, err := s.db.Query(`
|
|
||||||
SELECT
|
SELECT
|
||||||
id,input,is_url,host,path,model_key,
|
id,
|
||||||
tags, last_stream,
|
COALESCE(input,'') as input,
|
||||||
last_seen_online, last_seen_online_at,
|
is_url,
|
||||||
|
COALESCE(host,'') as host,
|
||||||
|
COALESCE(path,'') as path,
|
||||||
|
COALESCE(model_key,'') as model_key,
|
||||||
|
COALESCE(tags,'') as tags,
|
||||||
|
last_stream,
|
||||||
|
last_seen_online,
|
||||||
|
last_seen_online_at,
|
||||||
|
COALESCE(cb_online_json,''),
|
||||||
|
cb_online_fetched_at,
|
||||||
|
COALESCE(cb_online_last_error,''), -- optional
|
||||||
COALESCE(profile_image_url,''),
|
COALESCE(profile_image_url,''),
|
||||||
profile_image_updated_at,
|
profile_image_updated_at,
|
||||||
CASE WHEN profile_image_blob IS NOT NULL AND octet_length(profile_image_blob) > 0 THEN 1 ELSE 0 END as has_profile_image,
|
CASE WHEN profile_image_blob IS NOT NULL AND octet_length(profile_image_blob) > 0 THEN 1 ELSE 0 END as has_profile_image,
|
||||||
@ -830,9 +849,41 @@ SELECT
|
|||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
FROM models
|
FROM models
|
||||||
ORDER BY updated_at DESC;
|
ORDER BY updated_at DESC;
|
||||||
`)
|
`
|
||||||
|
|
||||||
|
q2 := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
COALESCE(input,'') as input,
|
||||||
|
is_url,
|
||||||
|
COALESCE(host,'') as host,
|
||||||
|
COALESCE(path,'') as path,
|
||||||
|
COALESCE(model_key,'') as model_key,
|
||||||
|
COALESCE(tags,'') as tags,
|
||||||
|
last_stream,
|
||||||
|
last_seen_online,
|
||||||
|
last_seen_online_at,
|
||||||
|
COALESCE(cb_online_json,''),
|
||||||
|
cb_online_fetched_at,
|
||||||
|
''::text as cb_online_last_error, -- fallback dummy
|
||||||
|
COALESCE(profile_image_url,''),
|
||||||
|
profile_image_updated_at,
|
||||||
|
CASE WHEN profile_image_blob IS NOT NULL AND octet_length(profile_image_blob) > 0 THEN 1 ELSE 0 END as has_profile_image,
|
||||||
|
watching,favorite,hot,keep,liked,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM models
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := s.db.Query(q1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []StoredModel{}
|
// ✅ genau dein Fall: "Spalte existiert nicht" -> fallback
|
||||||
|
fmt.Println("models List query err (q1):", err)
|
||||||
|
rows, err = s.db.Query(q2)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("models List query err (q2):", err)
|
||||||
|
return []StoredModel{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
@ -841,14 +892,17 @@ ORDER BY updated_at DESC;
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var (
|
var (
|
||||||
id, input, host, path, modelKey, tags string
|
id, input, host, path, modelKey, tags string
|
||||||
|
isURL bool
|
||||||
isURL bool
|
|
||||||
|
|
||||||
lastStream sql.NullTime
|
lastStream sql.NullTime
|
||||||
|
|
||||||
lastSeenOnline sql.NullBool
|
lastSeenOnline sql.NullBool
|
||||||
lastSeenOnlineAt sql.NullTime
|
lastSeenOnlineAt sql.NullTime
|
||||||
|
|
||||||
|
cbOnlineJSON string
|
||||||
|
cbOnlineFetchedAt sql.NullTime
|
||||||
|
cbOnlineLastError string
|
||||||
|
|
||||||
profileImageURL string
|
profileImageURL string
|
||||||
profileImageUpdatedAt sql.NullTime
|
profileImageUpdatedAt sql.NullTime
|
||||||
hasProfileImage int64
|
hasProfileImage int64
|
||||||
@ -863,6 +917,7 @@ ORDER BY updated_at DESC;
|
|||||||
&id, &input, &isURL, &host, &path, &modelKey,
|
&id, &input, &isURL, &host, &path, &modelKey,
|
||||||
&tags, &lastStream,
|
&tags, &lastStream,
|
||||||
&lastSeenOnline, &lastSeenOnlineAt,
|
&lastSeenOnline, &lastSeenOnlineAt,
|
||||||
|
&cbOnlineJSON, &cbOnlineFetchedAt, &cbOnlineLastError,
|
||||||
&profileImageURL, &profileImageUpdatedAt, &hasProfileImage,
|
&profileImageURL, &profileImageUpdatedAt, &hasProfileImage,
|
||||||
&watching, &favorite, &hot, &keep, &liked,
|
&watching, &favorite, &hot, &keep, &liked,
|
||||||
&createdAt, &updatedAt,
|
&createdAt, &updatedAt,
|
||||||
@ -882,6 +937,10 @@ ORDER BY updated_at DESC;
|
|||||||
LastSeenOnline: ptrBoolFromNullBool(lastSeenOnline),
|
LastSeenOnline: ptrBoolFromNullBool(lastSeenOnline),
|
||||||
LastSeenOnlineAt: fmtNullTime(lastSeenOnlineAt),
|
LastSeenOnlineAt: fmtNullTime(lastSeenOnlineAt),
|
||||||
|
|
||||||
|
CbOnlineJSON: cbOnlineJSON,
|
||||||
|
CbOnlineFetchedAt: fmtNullTime(cbOnlineFetchedAt),
|
||||||
|
CbOnlineLastError: cbOnlineLastError,
|
||||||
|
|
||||||
Watching: watching,
|
Watching: watching,
|
||||||
Favorite: favorite,
|
Favorite: favorite,
|
||||||
Hot: hot,
|
Hot: hot,
|
||||||
@ -919,6 +978,166 @@ func (s *ModelStore) Meta() ModelsMeta {
|
|||||||
return ModelsMeta{Count: count, UpdatedAt: fmtNullTime(updatedAt)}
|
return ModelsMeta{Count: count, UpdatedAt: fmtNullTime(updatedAt)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChaturbateOnlineSnapshot struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
CurrentShow string `json:"current_show,omitempty"` // public/private/hidden/away
|
||||||
|
RoomSubject string `json:"room_subject,omitempty"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
SpokenLanguages string `json:"spoken_languages,omitempty"`
|
||||||
|
Gender string `json:"gender,omitempty"`
|
||||||
|
|
||||||
|
NumUsers int `json:"num_users,omitempty"`
|
||||||
|
NumFollowers int `json:"num_followers,omitempty"`
|
||||||
|
IsHD bool `json:"is_hd,omitempty"`
|
||||||
|
IsNew bool `json:"is_new,omitempty"`
|
||||||
|
Age int `json:"age,omitempty"`
|
||||||
|
SecondsOnline int `json:"seconds_online,omitempty"`
|
||||||
|
|
||||||
|
ImageURL string `json:"image_url,omitempty"`
|
||||||
|
ImageURL360 string `json:"image_url_360x270,omitempty"`
|
||||||
|
ChatRoomURL string `json:"chat_room_url,omitempty"`
|
||||||
|
ChatRoomURLRS string `json:"chat_room_url_revshare,omitempty"`
|
||||||
|
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelStore) ListModelKeysByHost(host string) ([]string, error) {
|
||||||
|
if err := s.ensureInit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
host = canonicalHost(host)
|
||||||
|
if host == "" {
|
||||||
|
return nil, errors.New("host fehlt")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.db.Query(`
|
||||||
|
SELECT model_key
|
||||||
|
FROM models
|
||||||
|
WHERE lower(trim(host)) = lower(trim($1));
|
||||||
|
`, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]string, 0, 128)
|
||||||
|
for rows.Next() {
|
||||||
|
var k string
|
||||||
|
if err := rows.Scan(&k); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k = strings.ToLower(strings.TrimSpace(k))
|
||||||
|
if k != "" {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelStore) SetChaturbateOnlineSnapshot(host, modelKey string, snap *ChaturbateOnlineSnapshot, fetchedAt string, lastErr string) error {
|
||||||
|
if err := s.ensureInit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
host = canonicalHost(host)
|
||||||
|
key := strings.TrimSpace(modelKey)
|
||||||
|
if host == "" || key == "" {
|
||||||
|
return errors.New("host/modelKey fehlt")
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonStr string
|
||||||
|
if snap != nil {
|
||||||
|
b, err := json.Marshal(snap)
|
||||||
|
if err == nil {
|
||||||
|
jsonStr = strings.TrimSpace(string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ft := parseRFC3339Nano(fetchedAt)
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// NOTE: cb_online_last_error nur updaten, wenn Spalte existiert.
|
||||||
|
// Wenn du die optionale Spalte nicht anlegst: entferne die beiden Stellen.
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
UPDATE models
|
||||||
|
SET
|
||||||
|
cb_online_json=$1,
|
||||||
|
cb_online_fetched_at=$2,
|
||||||
|
cb_online_last_error=$3,
|
||||||
|
updated_at=$4
|
||||||
|
WHERE lower(trim(host)) = lower(trim($5))
|
||||||
|
AND lower(trim(model_key)) = lower(trim($6));
|
||||||
|
`, nullableStringArg(jsonStr), nullableTimeArg(ft), strings.TrimSpace(lastErr), now, host, key)
|
||||||
|
if err != nil {
|
||||||
|
// falls cb_online_last_error nicht existiert -> fallback ohne die Spalte
|
||||||
|
_, err2 := s.db.Exec(`
|
||||||
|
UPDATE models
|
||||||
|
SET
|
||||||
|
cb_online_json=$1,
|
||||||
|
cb_online_fetched_at=$2,
|
||||||
|
updated_at=$3
|
||||||
|
WHERE lower(trim(host)) = lower(trim($4))
|
||||||
|
AND lower(trim(model_key)) = lower(trim($5));
|
||||||
|
`, nullableStringArg(jsonStr), nullableTimeArg(ft), now, host, key)
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelStore) GetChaturbateOnlineSnapshot(host, modelKey string) (*ChaturbateOnlineSnapshot, string, bool, error) {
|
||||||
|
if err := s.ensureInit(); err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
host = canonicalHost(host)
|
||||||
|
key := strings.TrimSpace(modelKey)
|
||||||
|
if host == "" || key == "" {
|
||||||
|
return nil, "", false, errors.New("host/modelKey fehlt")
|
||||||
|
}
|
||||||
|
|
||||||
|
var js sql.NullString
|
||||||
|
var fetchedAt sql.NullTime
|
||||||
|
|
||||||
|
err := s.db.QueryRow(`
|
||||||
|
SELECT cb_online_json, cb_online_fetched_at
|
||||||
|
FROM models
|
||||||
|
WHERE lower(trim(host)) = lower(trim($1))
|
||||||
|
AND lower(trim(model_key)) = lower(trim($2))
|
||||||
|
LIMIT 1;
|
||||||
|
`, host, key).Scan(&js, &fetchedAt)
|
||||||
|
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, "", false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := strings.TrimSpace(js.String)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, fmtNullTime(fetchedAt), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var snap ChaturbateOnlineSnapshot
|
||||||
|
if err := json.Unmarshal([]byte(raw), &snap); err != nil {
|
||||||
|
return nil, fmtNullTime(fetchedAt), false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &snap, fmtNullTime(fetchedAt), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullableStringArg(s string) any {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// hostFilter: z.B. "chaturbate.com" (leer => alle Hosts)
|
// hostFilter: z.B. "chaturbate.com" (leer => alle Hosts)
|
||||||
func (s *ModelStore) ListWatchedLite(hostFilter string) []WatchedModelLite {
|
func (s *ModelStore) ListWatchedLite(hostFilter string) []WatchedModelLite {
|
||||||
if err := s.ensureInit(); err != nil {
|
if err := s.ensureInit(); err != nil {
|
||||||
@ -933,14 +1152,14 @@ func (s *ModelStore) ListWatchedLite(hostFilter string) []WatchedModelLite {
|
|||||||
)
|
)
|
||||||
if hostFilter == "" {
|
if hostFilter == "" {
|
||||||
rows, err = s.db.Query(`
|
rows, err = s.db.Query(`
|
||||||
SELECT id,input,host,model_key,watching
|
SELECT id,input,COALESCE(host,'') as host,model_key,watching
|
||||||
FROM models
|
FROM models
|
||||||
WHERE watching = true
|
WHERE watching = true
|
||||||
ORDER BY updated_at DESC;
|
ORDER BY updated_at DESC;
|
||||||
`)
|
`)
|
||||||
} else {
|
} else {
|
||||||
rows, err = s.db.Query(`
|
rows, err = s.db.Query(`
|
||||||
SELECT id,input,host,model_key,watching
|
SELECT id,input,COALESCE(host,'') as host,model_key,watching
|
||||||
FROM models
|
FROM models
|
||||||
WHERE watching = true AND host = $1
|
WHERE watching = true AND host = $1
|
||||||
ORDER BY updated_at DESC;
|
ORDER BY updated_at DESC;
|
||||||
@ -1200,6 +1419,10 @@ func (s *ModelStore) getByID(id string) (StoredModel, error) {
|
|||||||
lastSeenOnline sql.NullBool
|
lastSeenOnline sql.NullBool
|
||||||
lastSeenOnlineAt sql.NullTime
|
lastSeenOnlineAt sql.NullTime
|
||||||
|
|
||||||
|
cbOnlineJSON string
|
||||||
|
cbOnlineFetchedAt sql.NullTime
|
||||||
|
cbOnlineLastError string
|
||||||
|
|
||||||
profileImageURL string
|
profileImageURL string
|
||||||
profileImageUpdatedAt sql.NullTime
|
profileImageUpdatedAt sql.NullTime
|
||||||
hasProfileImage int64
|
hasProfileImage int64
|
||||||
@ -1210,11 +1433,21 @@ func (s *ModelStore) getByID(id string) (StoredModel, error) {
|
|||||||
createdAt, updatedAt time.Time
|
createdAt, updatedAt time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
err := s.db.QueryRow(`
|
// q1: mit optionaler Spalte cb_online_last_error
|
||||||
|
q1 := `
|
||||||
SELECT
|
SELECT
|
||||||
input,is_url,host,path,model_key,
|
COALESCE(input,'') as input,
|
||||||
tags, last_stream,
|
is_url,
|
||||||
last_seen_online, last_seen_online_at,
|
COALESCE(host,'') as host,
|
||||||
|
COALESCE(path,'') as path,
|
||||||
|
COALESCE(model_key,'') as model_key,
|
||||||
|
COALESCE(tags,'') as tags,
|
||||||
|
last_stream,
|
||||||
|
last_seen_online,
|
||||||
|
last_seen_online_at,
|
||||||
|
COALESCE(cb_online_json,''),
|
||||||
|
cb_online_fetched_at,
|
||||||
|
COALESCE(cb_online_last_error,''),
|
||||||
COALESCE(profile_image_url,''),
|
COALESCE(profile_image_url,''),
|
||||||
profile_image_updated_at,
|
profile_image_updated_at,
|
||||||
CASE WHEN profile_image_blob IS NOT NULL AND octet_length(profile_image_blob) > 0 THEN 1 ELSE 0 END as has_profile_image,
|
CASE WHEN profile_image_blob IS NOT NULL AND octet_length(profile_image_blob) > 0 THEN 1 ELSE 0 END as has_profile_image,
|
||||||
@ -1222,19 +1455,62 @@ SELECT
|
|||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
FROM models
|
FROM models
|
||||||
WHERE id=$1;
|
WHERE id=$1;
|
||||||
`, id).Scan(
|
`
|
||||||
&input, &isURL, &host, &path, &modelKey,
|
|
||||||
&tags, &lastStream,
|
// q2: Fallback, falls cb_online_last_error nicht existiert (oder q1 aus anderen Gründen scheitert)
|
||||||
&lastSeenOnline, &lastSeenOnlineAt,
|
// Wichtig: gleiche Spaltenreihenfolge + Typen kompatibel halten.
|
||||||
&profileImageURL, &profileImageUpdatedAt, &hasProfileImage,
|
q2 := `
|
||||||
&watching, &favorite, &hot, &keep, &liked,
|
SELECT
|
||||||
&createdAt, &updatedAt,
|
COALESCE(input,'') as input,
|
||||||
)
|
is_url,
|
||||||
|
COALESCE(host,'') as host,
|
||||||
|
COALESCE(path,'') as path,
|
||||||
|
COALESCE(model_key,'') as model_key,
|
||||||
|
COALESCE(tags,'') as tags,
|
||||||
|
last_stream,
|
||||||
|
last_seen_online,
|
||||||
|
last_seen_online_at,
|
||||||
|
COALESCE(cb_online_json,''),
|
||||||
|
cb_online_fetched_at,
|
||||||
|
'' as cb_online_last_error,
|
||||||
|
COALESCE(profile_image_url,''),
|
||||||
|
profile_image_updated_at,
|
||||||
|
CASE WHEN profile_image_blob IS NOT NULL AND octet_length(profile_image_blob) > 0 THEN 1 ELSE 0 END as has_profile_image,
|
||||||
|
watching,favorite,hot,keep,liked,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM models
|
||||||
|
WHERE id=$1;
|
||||||
|
`
|
||||||
|
|
||||||
|
scan := func(q string) error {
|
||||||
|
return s.db.QueryRow(q, id).Scan(
|
||||||
|
&input, &isURL, &host, &path, &modelKey,
|
||||||
|
&tags, &lastStream,
|
||||||
|
&lastSeenOnline, &lastSeenOnlineAt,
|
||||||
|
&cbOnlineJSON, &cbOnlineFetchedAt, &cbOnlineLastError,
|
||||||
|
&profileImageURL, &profileImageUpdatedAt, &hasProfileImage,
|
||||||
|
&watching, &favorite, &hot, &keep, &liked,
|
||||||
|
&createdAt, &updatedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scan(q1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Wenn die Zeile nicht existiert, nicht noch fallbacken.
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return StoredModel{}, errors.New("model nicht gefunden")
|
return StoredModel{}, errors.New("model nicht gefunden")
|
||||||
}
|
}
|
||||||
return StoredModel{}, err
|
|
||||||
|
// Fallback versuchen (typisch: "column cb_online_last_error does not exist")
|
||||||
|
err2 := scan(q2)
|
||||||
|
if err2 != nil {
|
||||||
|
// wenn fallback auch kein Row findet, sauber melden
|
||||||
|
if errors.Is(err2, sql.ErrNoRows) {
|
||||||
|
return StoredModel{}, errors.New("model nicht gefunden")
|
||||||
|
}
|
||||||
|
// sonst ursprünglichen Fehler behalten? -> ich gebe hier err2 zurück, weil er meist aussagekräftiger ist.
|
||||||
|
return StoredModel{}, err2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m := StoredModel{
|
m := StoredModel{
|
||||||
@ -1249,6 +1525,10 @@ WHERE id=$1;
|
|||||||
LastSeenOnline: ptrBoolFromNullBool(lastSeenOnline),
|
LastSeenOnline: ptrBoolFromNullBool(lastSeenOnline),
|
||||||
LastSeenOnlineAt: fmtNullTime(lastSeenOnlineAt),
|
LastSeenOnlineAt: fmtNullTime(lastSeenOnlineAt),
|
||||||
|
|
||||||
|
CbOnlineJSON: cbOnlineJSON,
|
||||||
|
CbOnlineFetchedAt: fmtNullTime(cbOnlineFetchedAt),
|
||||||
|
CbOnlineLastError: cbOnlineLastError,
|
||||||
|
|
||||||
Watching: watching,
|
Watching: watching,
|
||||||
Favorite: favorite,
|
Favorite: favorite,
|
||||||
Hot: hot,
|
Hot: hot,
|
||||||
|
|||||||
@ -501,7 +501,7 @@ function DownloadsCardRow({
|
|||||||
<div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={name}>
|
<div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={name}>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
|
{ /* Status-Badge */}
|
||||||
<span
|
<span
|
||||||
className={[
|
className={[
|
||||||
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold',
|
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -442,8 +442,19 @@ export default function ModelsTab() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
const list = await apiJSON<StoredModel[]>('/api/models', { cache: 'no-store' })
|
const res = await fetch('/api/models', { cache: 'no-store' as any })
|
||||||
setModels(Array.isArray(list) ? list : [])
|
if (!res.ok) throw new Error(await res.text().catch(() => `HTTP ${res.status}`))
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => null)
|
||||||
|
|
||||||
|
// ✅ akzeptiere beide Formen: Array ODER { items: [...] }
|
||||||
|
const list: StoredModel[] = Array.isArray(data?.items)
|
||||||
|
? (data.items as StoredModel[])
|
||||||
|
: Array.isArray(data)
|
||||||
|
? (data as StoredModel[])
|
||||||
|
: []
|
||||||
|
|
||||||
|
setModels(list)
|
||||||
void refreshVideoCounts()
|
void refreshVideoCounts()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setErr(e?.message ?? String(e))
|
setErr(e?.message ?? String(e))
|
||||||
|
|||||||
@ -1744,8 +1744,7 @@ export default function Player({
|
|||||||
const videoChrome = (
|
const videoChrome = (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative overflow-visible',
|
'relative overflow-visible flex-1 min-h-0'
|
||||||
expanded ? 'flex-1 min-h-0' : miniDesktop ? 'flex-1 min-h-0' : 'aspect-video'
|
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (!miniDesktop || !canHover) return
|
if (!miniDesktop || !canHover) return
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user