239 lines
4.9 KiB
Go
239 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type StoredModel struct {
|
|
ID string `json:"id"` // i.d.R. modelKey (unique)
|
|
Input string `json:"input"` // Original-URL/Eingabe
|
|
IsURL bool `json:"isUrl"` // vom Parser
|
|
Host string `json:"host,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
ModelKey string `json:"modelKey"` // Display/Key
|
|
|
|
Watching bool `json:"watching"`
|
|
Favorite bool `json:"favorite"`
|
|
Hot bool `json:"hot"`
|
|
Keep bool `json:"keep"`
|
|
Liked *bool `json:"liked,omitempty"` // null => unbekannt
|
|
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type ModelStore struct {
|
|
path string
|
|
mu sync.RWMutex
|
|
items map[string]StoredModel
|
|
}
|
|
|
|
func NewModelStore(path string) *ModelStore {
|
|
return &ModelStore{
|
|
path: path,
|
|
items: map[string]StoredModel{},
|
|
}
|
|
}
|
|
|
|
func (s *ModelStore) Load() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
b, err := os.ReadFile(s.path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil // ok
|
|
}
|
|
return err
|
|
}
|
|
|
|
var list []StoredModel
|
|
if err := json.Unmarshal(b, &list); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.items = map[string]StoredModel{}
|
|
for _, m := range list {
|
|
if m.ID == "" {
|
|
m.ID = m.ModelKey
|
|
}
|
|
if m.ID != "" {
|
|
s.items[m.ID] = m
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ModelStore) saveLocked() error {
|
|
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
list := make([]StoredModel, 0, len(s.items))
|
|
for _, m := range s.items {
|
|
list = append(list, m)
|
|
}
|
|
|
|
// Neueste zuerst
|
|
sort.Slice(list, func(i, j int) bool { return list[i].UpdatedAt > list[j].UpdatedAt })
|
|
|
|
b, err := json.MarshalIndent(list, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tmp := s.path + ".tmp"
|
|
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Windows: Rename überschreibt nicht immer zuverlässig -> erst versuchen, sonst löschen & retry
|
|
if err := os.Rename(tmp, s.path); err != nil {
|
|
_ = os.Remove(s.path)
|
|
if err2 := os.Rename(tmp, s.path); err2 != nil {
|
|
_ = os.Remove(tmp)
|
|
return err2
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ModelStore) List() []StoredModel {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
out := make([]StoredModel, 0, len(s.items))
|
|
for _, m := range s.items {
|
|
out = append(out, m)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].UpdatedAt > out[j].UpdatedAt })
|
|
return out
|
|
}
|
|
|
|
type ParsedModelDTO struct {
|
|
Input string `json:"input"`
|
|
IsURL bool `json:"isUrl"`
|
|
Host string `json:"host,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
ModelKey string `json:"modelKey"`
|
|
}
|
|
|
|
func (s *ModelStore) UpsertFromParsed(p ParsedModelDTO) (StoredModel, error) {
|
|
if p.ModelKey == "" {
|
|
return StoredModel{}, errors.New("modelKey fehlt")
|
|
}
|
|
|
|
input := strings.TrimSpace(p.Input)
|
|
if input == "" {
|
|
return StoredModel{}, errors.New("URL fehlt.")
|
|
}
|
|
if !p.IsURL {
|
|
return StoredModel{}, errors.New("Nur URL erlaubt.")
|
|
}
|
|
u, err := url.Parse(input)
|
|
if err != nil || u.Scheme == "" || u.Hostname() == "" {
|
|
return StoredModel{}, errors.New("Ungültige URL.")
|
|
}
|
|
if strings.TrimSpace(p.ModelKey) == "" {
|
|
return StoredModel{}, errors.New("ModelKey fehlt.")
|
|
}
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
id := p.ModelKey
|
|
existing, ok := s.items[id]
|
|
if !ok {
|
|
existing = StoredModel{
|
|
ID: id,
|
|
CreatedAt: now,
|
|
}
|
|
}
|
|
|
|
// Felder aktualisieren
|
|
existing.Input = p.Input
|
|
existing.IsURL = p.IsURL
|
|
existing.Host = p.Host
|
|
existing.Path = p.Path
|
|
existing.ModelKey = p.ModelKey
|
|
existing.UpdatedAt = now
|
|
|
|
s.items[id] = existing
|
|
if err := s.saveLocked(); err != nil {
|
|
return StoredModel{}, err
|
|
}
|
|
return existing, nil
|
|
}
|
|
|
|
type ModelFlagsPatch struct {
|
|
ID string `json:"id"`
|
|
Watching *bool `json:"watching,omitempty"`
|
|
Favorite *bool `json:"favorite,omitempty"`
|
|
Hot *bool `json:"hot,omitempty"`
|
|
Keep *bool `json:"keep,omitempty"`
|
|
Liked *bool `json:"liked,omitempty"`
|
|
ClearLiked bool `json:"clearLiked,omitempty"`
|
|
}
|
|
|
|
func (s *ModelStore) PatchFlags(patch ModelFlagsPatch) (StoredModel, error) {
|
|
if patch.ID == "" {
|
|
return StoredModel{}, errors.New("id fehlt")
|
|
}
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
m, ok := s.items[patch.ID]
|
|
if !ok {
|
|
return StoredModel{}, errors.New("model nicht gefunden")
|
|
}
|
|
|
|
if patch.Watching != nil {
|
|
m.Watching = *patch.Watching
|
|
}
|
|
if patch.Favorite != nil {
|
|
m.Favorite = *patch.Favorite
|
|
}
|
|
if patch.Hot != nil {
|
|
m.Hot = *patch.Hot
|
|
}
|
|
if patch.Keep != nil {
|
|
m.Keep = *patch.Keep
|
|
}
|
|
if patch.ClearLiked {
|
|
m.Liked = nil
|
|
} else if patch.Liked != nil {
|
|
m.Liked = patch.Liked
|
|
}
|
|
|
|
m.UpdatedAt = now
|
|
s.items[m.ID] = m
|
|
|
|
if err := s.saveLocked(); err != nil {
|
|
return StoredModel{}, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *ModelStore) Delete(id string) error {
|
|
if id == "" {
|
|
return errors.New("id fehlt")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
delete(s.items, id)
|
|
return s.saveLocked()
|
|
}
|