// Package model defines the persisted data structures for scientific-surfing. package model import ( "fmt" "path/filepath" ) // SubscriptionStatus mirrors Python's SubscriptionStatus(str, Enum). type SubscriptionStatus string const ( StatusActive SubscriptionStatus = "active" StatusInactive SubscriptionStatus = "inactive" ) // Subscription is a single clash RSS subscription entry. type Subscription struct { Name string `yaml:"name"` URL string `yaml:"url"` Status SubscriptionStatus `yaml:"status"` LastRefresh *Time `yaml:"last_refresh"` FileSize *int64 `yaml:"file_size"` StatusCode *int `yaml:"status_code"` // int64, not uint64: this Go implementation's own fnv64a hash (see // stableHash in the subscription package) fits either way, but the // original Python implementation's builtin hash() is a signed 64-bit // value and is frequently negative — existing subscriptions.yaml files // written by it must still parse. ContentHash *int64 `yaml:"content_hash"` LastError *string `yaml:"last_error"` } // NewSubscription creates a subscription defaulting to inactive, matching the // Pydantic model's field defaults. func NewSubscription(name, url string) *Subscription { return &Subscription{ Name: name, URL: url, Status: StatusInactive, } } // GetFilePath returns the on-disk path for the downloaded subscription content. func (s *Subscription) GetFilePath(configDir string) string { return filepath.Join(configDir, "subscriptions", s.Name+".yml") } // SubscriptionsData is the full persisted collection of subscriptions. type SubscriptionsData struct { Subscriptions map[string]*Subscription `yaml:"subscriptions"` } // NewSubscriptionsData returns an empty, ready-to-use collection. func NewSubscriptionsData() *SubscriptionsData { return &SubscriptionsData{Subscriptions: make(map[string]*Subscription)} } // GetActiveSubscription returns the currently active subscription, if any. func (d *SubscriptionsData) GetActiveSubscription() *Subscription { for _, sub := range d.Subscriptions { if sub.Status == StatusActive { return sub } } return nil } // SetActive marks name as active and deactivates all others. Returns false if // name does not exist. func (d *SubscriptionsData) SetActive(name string) bool { if _, ok := d.Subscriptions[name]; !ok { return false } for subName, sub := range d.Subscriptions { if subName == name { sub.Status = StatusActive } else { sub.Status = StatusInactive } } return true } // AddSubscription adds a new subscription, activating it if it's the first one. func (d *SubscriptionsData) AddSubscription(name, url string) *Subscription { sub := NewSubscription(name, url) if len(d.Subscriptions) == 0 { sub.Status = StatusActive } d.Subscriptions[name] = sub return sub } // RemoveSubscription deletes a subscription by name. Returns false if it // doesn't exist. func (d *SubscriptionsData) RemoveSubscription(name string) bool { if _, ok := d.Subscriptions[name]; !ok { return false } delete(d.Subscriptions, name) return true } // RenameSubscription renames oldName to newName. Returns false if oldName is // missing or newName is already taken. func (d *SubscriptionsData) RenameSubscription(oldName, newName string) bool { sub, ok := d.Subscriptions[oldName] if !ok { return false } if _, taken := d.Subscriptions[newName]; taken { return false } delete(d.Subscriptions, oldName) sub.Name = newName d.Subscriptions[newName] = sub return true } // SetSubscriptionURL updates the URL for an existing subscription. func (d *SubscriptionsData) SetSubscriptionURL(name, url string) bool { sub, ok := d.Subscriptions[name] if !ok { return false } sub.URL = url return true } // ValidationError is returned by Validate methods, mirroring pydantic's // ValueError-raising validators. type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) }