Files
ss/internal/model/subscription.go

153 lines
4.5 KiB
Go

// Package model defines the persisted data structures for scientific-surfing.
package model
import (
"fmt"
"path/filepath"
"sort"
)
// 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)}
}
// GetActiveSubscriptions returns every subscription currently marked active,
// sorted by name for deterministic ordering. Multiple subscriptions may be
// active simultaneously — config apply merges all of them (see
// internal/corecfg), prefixing each proxy's name with its source
// subscription's name to avoid collisions.
func (d *SubscriptionsData) GetActiveSubscriptions() []*Subscription {
var actives []*Subscription
for _, sub := range d.Subscriptions {
if sub.Status == StatusActive {
actives = append(actives, sub)
}
}
sort.Slice(actives, func(i, j int) bool { return actives[i].Name < actives[j].Name })
return actives
}
// SetActive marks name as active, in addition to any other subscriptions
// already active. Returns false if name does not exist.
func (d *SubscriptionsData) SetActive(name string) bool {
sub, ok := d.Subscriptions[name]
if !ok {
return false
}
sub.Status = StatusActive
return true
}
// SetInactive marks name as inactive. Returns false if name does not exist.
func (d *SubscriptionsData) SetInactive(name string) bool {
sub, ok := d.Subscriptions[name]
if !ok {
return false
}
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)
}