Files
sub-store/internal/database/collection_repo.go
T
rogee de9ac92ede feat: GeoIP-based geo detection + configurable rename template
- Add MaxMind GeoLite2-City mmdb auto-download and IP-based geo lookup
  (covers all 249 countries globally, replacing limited 8-country regex)
- Add RenameOptions with 5 toggleable fields (flag/alias/country/city/index)
  stored as rename_options_json on collections
- Collection rename pipeline uses DetectGeoWithServer(name, server) which
  tries GeoIP lookup first, falls back to name-based regex matching
- Chinese country/city names + emoji flag prefix
- Country-city dedup: skip city when it equals country (e.g. 香港/香港)
- URI fragment encoding: url.QueryEscape -> url.PathEscape (%20 not +)
- Egress cache key excludes name field so renamed nodes still match cache
- Frontend: 5 toggle buttons in collection edit form, default all on
- Migrations: 0004 (source alias + collection rename_enabled),
  0005 (collection rename_options_json)
2026-07-28 14:44:54 +08:00

174 lines
4.9 KiB
Go

package database
import (
"database/sql"
"encoding/json"
"time"
"github.com/jmoiron/sqlx"
"github.com/peterqiu0516/sub-store/internal/model"
"github.com/peterqiu0516/sub-store/internal/util"
)
type CollectionRepo struct {
db *sqlx.DB
}
func NewCollectionRepo(db *sqlx.DB) *CollectionRepo {
return &CollectionRepo{db: db}
}
type collectionRow struct {
ID string `db:"id"`
Name string `db:"name"`
SourceIdsJSON string `db:"source_ids_json"`
RenameEnabled int `db:"rename_enabled"`
RenameOptionsJSON string `db:"rename_options_json"`
TemplateID string `db:"template_id"`
IgnoreFailed int `db:"ignore_failed"`
Enabled int `db:"enabled"`
MetaJSON string `db:"meta_json"`
CreatedAt int64 `db:"created_at"`
UpdatedAt int64 `db:"updated_at"`
}
const collectionColumns = `id, name, source_ids_json, rename_enabled, rename_options_json, template_id, ignore_failed, enabled, meta_json, created_at, updated_at`
func (r *CollectionRepo) List() ([]model.CollectionRecord, error) {
var rows []collectionRow
if err := r.db.Select(&rows, `SELECT `+collectionColumns+` FROM collections ORDER BY created_at ASC`); err != nil {
return nil, err
}
result := make([]model.CollectionRecord, 0, len(rows))
for _, row := range rows {
result = append(result, collectionFromRow(row))
}
return result, nil
}
func (r *CollectionRepo) Get(id string) (*model.CollectionRecord, error) {
var row collectionRow
if err := r.db.Get(&row, `SELECT `+collectionColumns+` FROM collections WHERE id = ?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
rec := collectionFromRow(row)
return &rec, nil
}
func (r *CollectionRepo) Upsert(input model.CollectionRecord) (model.CollectionRecord, error) {
now := time.Now().UnixMilli()
id := input.ID
if id == "" {
id = util.ToId(input.Name)
}
existing, _ := r.Get(id)
createdAt := now
if existing != nil {
createdAt = existing.CreatedAt
}
sourceIds := input.SourceIds
if sourceIds == nil {
sourceIds = []string{}
}
meta := input.Meta
if meta == nil {
meta = map[string]any{}
}
templateId := input.TemplateId
if templateId == "" {
templateId = "acl4ssr-mihomo"
}
rec := model.CollectionRecord{
ID: id,
Name: input.Name,
SourceIds: sourceIds,
RenameEnabled: input.RenameEnabled,
RenameOptions: input.RenameOptions,
TemplateId: templateId,
IgnoreFailed: input.IgnoreFailed,
Enabled: input.Enabled,
Meta: meta,
CreatedAt: createdAt,
UpdatedAt: now,
}
sourceIdsJSON := marshalJSON(rec.SourceIds)
metaJSON := marshalJSON(rec.Meta)
renameOptionsJSON := ""
if rec.RenameOptions != nil {
renameOptionsJSON = marshalJSON(rec.RenameOptions)
}
_, err := r.db.Exec(
`INSERT INTO collections (id, name, source_ids_json, rename_enabled, rename_options_json, template_id, ignore_failed, enabled, meta_json, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name, source_ids_json = excluded.source_ids_json,
rename_enabled = excluded.rename_enabled, rename_options_json = excluded.rename_options_json,
template_id = excluded.template_id, ignore_failed = excluded.ignore_failed,
enabled = excluded.enabled, meta_json = excluded.meta_json, updated_at = excluded.updated_at`,
rec.ID, rec.Name, sourceIdsJSON, boolToInt(rec.RenameEnabled), renameOptionsJSON, rec.TemplateId, boolToInt(rec.IgnoreFailed), boolToInt(rec.Enabled), metaJSON, rec.CreatedAt, rec.UpdatedAt,
)
if err != nil {
return rec, err
}
return rec, nil
}
func (r *CollectionRepo) Delete(id string) error {
_, err := r.db.Exec("DELETE FROM collections WHERE id = ?", id)
return err
}
func (r *CollectionRepo) Sort(ids []string) error {
now := time.Now().UnixMilli()
tx, err := r.db.Beginx()
if err != nil {
return err
}
defer tx.Rollback()
for i, id := range ids {
_, err := tx.Exec("UPDATE collections SET created_at = ?, updated_at = ? WHERE id = ?", now+int64(i), now, id)
if err != nil {
return err
}
}
return tx.Commit()
}
func collectionFromRow(row collectionRow) model.CollectionRecord {
var sourceIds []string
json.Unmarshal([]byte(row.SourceIdsJSON), &sourceIds)
if sourceIds == nil {
sourceIds = []string{}
}
var renameOptions *model.RenameOptions
if row.RenameOptionsJSON != "" {
var opts model.RenameOptions
if err := json.Unmarshal([]byte(row.RenameOptionsJSON), &opts); err == nil {
renameOptions = &opts
}
}
return model.CollectionRecord{
ID: row.ID,
Name: row.Name,
SourceIds: sourceIds,
RenameEnabled: row.RenameEnabled != 0,
RenameOptions: renameOptions,
TemplateId: row.TemplateID,
IgnoreFailed: row.IgnoreFailed != 0,
Enabled: row.Enabled != 0,
Meta: model.UnmarshalMeta(row.MetaJSON),
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}