tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit ae1a1984a16c5943ed8c1452d2701aba6c80b364
parent 4e0b131b481e8d668d2ab90542ca719f8ce7acbb
Author: breadcat <breadcat@users.noreply.github.com>
Date:   Sat, 14 Mar 2026 01:46:35 +0000

Replace config.json with arguments and store settings in sqlite DB

Diffstat:
Minclude-admin.go | 144++++++++++++++++++++++++++++++++-----------------------------------------------
Minclude-db.go | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude-types.go | 31++++++++++++++++---------------
Mmain.go | 35++++++++++++++++++++++++++++-------
Mreadme.md | 2+-
Mstatic/sed-rules.js | 22++++++++++++++++++----
Mstatic/tag-alias.js | 24+++++++++++++++++++++---
7 files changed, 277 insertions(+), 115 deletions(-)

diff --git a/include-admin.go b/include-admin.go @@ -2,12 +2,12 @@ package main import ( "database/sql" - "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" + "strconv" "strings" "time" ) @@ -38,52 +38,13 @@ func currentAdminState(r *http.Request, orphanData OrphanData, missingThumbnails } } -func loadConfig() error { - config = Config{ - DatabasePath: "./database.db", - UploadDir: "uploads", - ServerPort: ":8080", - InstanceName: "Tagliatelle", - GallerySize: "400px", - ItemsPerPage: "100", - TagAliases: []TagAliasGroup{}, - SedRules: []SedRule{}, - } - - if data, err := os.ReadFile("config.json"); err == nil { - if err := json.Unmarshal(data, &config); err != nil { - return err - } - } - - return os.MkdirAll(config.UploadDir, 0755) -} - -func saveConfig() error { - data, err := json.MarshalIndent(config, "", " ") - if err != nil { - return err - } - return os.WriteFile("config.json", data, 0644) -} - func validateConfig(newConfig Config) error { - if newConfig.DatabasePath == "" { - return fmt.Errorf("database path cannot be empty") - } - - if newConfig.UploadDir == "" { - return fmt.Errorf("upload directory cannot be empty") - } - - if newConfig.ServerPort == "" || !strings.HasPrefix(newConfig.ServerPort, ":") { - return fmt.Errorf("server port must be in format ':8080'") + if newConfig.GallerySize == "" { + return fmt.Errorf("gallery size cannot be empty") } - - if err := os.MkdirAll(newConfig.UploadDir, 0755); err != nil { - return fmt.Errorf("cannot create upload directory: %v", err) + if newConfig.ItemsPerPage == "" { + return fmt.Errorf("items per page cannot be empty") } - return nil } @@ -129,22 +90,52 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { } } -func handleSaveAliases(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { - aliasesJSON := r.FormValue("aliases_json") +func parseAliasesFromForm(r *http.Request) []TagAliasGroup { + var groups []TagAliasGroup + for i := 0; ; i++ { + category := strings.TrimSpace(r.FormValue(fmt.Sprintf("aliases[%d][category]", i))) + if category == "" { + break + } + var aliases []string + for j := 0; ; j++ { + v := strings.TrimSpace(r.FormValue(fmt.Sprintf("aliases[%d][aliases][%d]", i, j))) + if v == "" { + break + } + aliases = append(aliases, v) + } + if len(aliases) >= 2 { + groups = append(groups, TagAliasGroup{Category: category, Aliases: aliases}) + } + } + return groups +} - var aliases []TagAliasGroup - if aliasesJSON != "" { - if err := json.Unmarshal([]byte(aliasesJSON), &aliases); err != nil { - data := currentAdminState(r, orphanData, missingThumbnails) - data.Error = "Invalid aliases JSON: " + err.Error() - renderAdminPage(w, r, data) - return +func parseSedRulesFromForm(r *http.Request) ([]SedRule, error) { + var rules []SedRule + for i := 0; ; i++ { + name := strings.TrimSpace(r.FormValue(fmt.Sprintf("sed_rules[%d][name]", i))) + if name == "" { + break } + command := strings.TrimSpace(r.FormValue(fmt.Sprintf("sed_rules[%d][command]", i))) + if command == "" { + return nil, fmt.Errorf("rule %s is missing a command", strconv.Itoa(i+1)) + } + rules = append(rules, SedRule{ + Name: name, + Description: strings.TrimSpace(r.FormValue(fmt.Sprintf("sed_rules[%d][description]", i))), + Command: command, + }) } + return rules, nil +} - config.TagAliases = aliases +func handleSaveAliases(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { + config.TagAliases = parseAliasesFromForm(r) - if err := saveConfig(); err != nil { + if err := SaveConfig(db, config); err != nil { data := currentAdminState(r, orphanData, missingThumbnails) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) @@ -157,15 +148,9 @@ func handleSaveAliases(w http.ResponseWriter, r *http.Request, orphanData Orphan } func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { - newConfig := Config{ - DatabasePath: strings.TrimSpace(r.FormValue("database_path")), - UploadDir: strings.TrimSpace(r.FormValue("upload_dir")), - ServerPort: strings.TrimSpace(r.FormValue("server_port")), - InstanceName: strings.TrimSpace(r.FormValue("instance_name")), - GallerySize: strings.TrimSpace(r.FormValue("gallery_size")), - ItemsPerPage: strings.TrimSpace(r.FormValue("items_per_page")), - TagAliases: config.TagAliases, // Preserve existing aliases - } + newConfig := config // preserve runtime fields + newConfig.GallerySize = strings.TrimSpace(r.FormValue("gallery_size")) + newConfig.ItemsPerPage = strings.TrimSpace(r.FormValue("items_per_page")) if err := validateConfig(newConfig); err != nil { data := currentAdminState(r, orphanData, missingThumbnails) @@ -174,12 +159,9 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphanData Orpha return } - needsRestart := newConfig.DatabasePath != config.DatabasePath || - newConfig.ServerPort != config.ServerPort - config = newConfig - if err := saveConfig(); err != nil { + if err := SaveConfig(db, config); err != nil { data := currentAdminState(r, orphanData, missingThumbnails) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) @@ -187,30 +169,22 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphanData Orpha } data := currentAdminState(r, orphanData, missingThumbnails) - if needsRestart { - data.Success = "Settings saved successfully! Please restart the server for database/port changes to take effect." - } else { - data.Success = "Settings saved successfully!" - } + data.Success = "Settings saved successfully!" renderAdminPage(w, r, data) } func handleSaveSedRules(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) { - sedRulesJSON := r.FormValue("sed_rules_json") - - var sedRules []SedRule - if sedRulesJSON != "" { - if err := json.Unmarshal([]byte(sedRulesJSON), &sedRules); err != nil { - data := currentAdminState(r, orphanData, missingThumbnails) - data.Error = "Invalid sed rules JSON: " + err.Error() - renderAdminPage(w, r, data) - return - } + rules, err := parseSedRulesFromForm(r) + if err != nil { + data := currentAdminState(r, orphanData, missingThumbnails) + data.Error = err.Error() + renderAdminPage(w, r, data) + return } - config.SedRules = sedRules + config.SedRules = rules - if err := saveConfig(); err != nil { + if err := SaveConfig(db, config); err != nil { data := currentAdminState(r, orphanData, missingThumbnails) data.Error = "Failed to save configuration: " + err.Error() renderAdminPage(w, r, data) diff --git a/include-db.go b/include-db.go @@ -2,6 +2,7 @@ package main import ( "database/sql" + "strings" _ "github.com/mattn/go-sqlite3" ) @@ -56,8 +57,141 @@ func createTables(db *sql.DB) error { content TEXT DEFAULT '', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' + ); + CREATE TABLE IF NOT EXISTS tag_aliases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category TEXT NOT NULL, + aliases TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS sed_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + command TEXT NOT NULL + ); ` _, err := db.Exec(schema) return err } + +func LoadConfig(db *sql.DB) (Config, error) { + cfg := Config{ + GallerySize: "400px", + ItemsPerPage: "100", + TagAliases: []TagAliasGroup{}, + SedRules: []SedRule{}, + } + + rows, err := db.Query(`SELECT key, value FROM settings`) + if err != nil { + return cfg, err + } + defer rows.Close() + for rows.Next() { + var key, value string + if err := rows.Scan(&key, &value); err != nil { + return cfg, err + } + switch key { + case "gallery_size": + if value != "" { + cfg.GallerySize = value + } + case "items_per_page": + if value != "" { + cfg.ItemsPerPage = value + } + } + } + if err := rows.Err(); err != nil { + return cfg, err + } + + aliasRows, err := db.Query(`SELECT category, aliases FROM tag_aliases ORDER BY id`) + if err != nil { + return cfg, err + } + defer aliasRows.Close() + for aliasRows.Next() { + var category, aliasesStr string + if err := aliasRows.Scan(&category, &aliasesStr); err != nil { + return cfg, err + } + cfg.TagAliases = append(cfg.TagAliases, TagAliasGroup{ + Category: category, + Aliases: strings.Split(aliasesStr, "|"), + }) + } + if err := aliasRows.Err(); err != nil { + return cfg, err + } + + sedRows, err := db.Query(`SELECT name, description, command FROM sed_rules ORDER BY id`) + if err != nil { + return cfg, err + } + defer sedRows.Close() + for sedRows.Next() { + var rule SedRule + if err := sedRows.Scan(&rule.Name, &rule.Description, &rule.Command); err != nil { + return cfg, err + } + cfg.SedRules = append(cfg.SedRules, rule) + } + if err := sedRows.Err(); err != nil { + return cfg, err + } + + return cfg, nil +} + +func SaveConfig(db *sql.DB, cfg Config) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + for _, kv := range [][2]string{ + {"gallery_size", cfg.GallerySize}, + {"items_per_page", cfg.ItemsPerPage}, + } { + if _, err := tx.Exec(` + INSERT INTO settings (key, value) VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + `, kv[0], kv[1]); err != nil { + return err + } + } + + if _, err := tx.Exec(`DELETE FROM tag_aliases`); err != nil { + return err + } + for _, group := range cfg.TagAliases { + aliasesStr := strings.Join(group.Aliases, "|") + if _, err := tx.Exec( + `INSERT INTO tag_aliases (category, aliases) VALUES (?, ?)`, + group.Category, aliasesStr, + ); err != nil { + return err + } + } + + if _, err := tx.Exec(`DELETE FROM sed_rules`); err != nil { + return err + } + for _, rule := range cfg.SedRules { + if _, err := tx.Exec( + `INSERT INTO sed_rules (name, description, command) VALUES (?, ?, ?)`, + rule.Name, rule.Description, rule.Command, + ); err != nil { + return err + } + } + + return tx.Commit() +} diff --git a/include-types.go b/include-types.go @@ -10,14 +10,15 @@ type File struct { } type Config struct { - DatabasePath string `json:"database_path"` - UploadDir string `json:"upload_dir"` - ServerPort string `json:"server_port"` - InstanceName string `json:"instance_name"` - GallerySize string `json:"gallery_size"` - ItemsPerPage string `json:"items_per_page"` - TagAliases []TagAliasGroup `json:"tag_aliases"` - SedRules []SedRule `json:"sed_rules"` + // Values from CLI arguments + DatabasePath string + UploadDir string + ServerPort string + // Values from database + GallerySize string + ItemsPerPage string + TagAliases []TagAliasGroup + SedRules []SedRule } type Breadcrumb struct { @@ -41,9 +42,9 @@ type PropertyDisplay struct { } type ListData struct { - Tagged []File - Untagged []File - Breadcrumbs []Breadcrumb + Tagged []File + Untagged []File + Breadcrumbs []Breadcrumb } type PageData struct { @@ -93,10 +94,10 @@ type BulkTagFormData struct { Error string Success string FormData struct { - FileRange string - Category string - Value string - Operation string + FileRange string + Category string + Value string + Operation string TagQuery string SelectionMode string } diff --git a/main.go b/main.go @@ -2,10 +2,13 @@ package main import ( "database/sql" + "flag" + "fmt" "html/template" "log" "net/http" "os" + "path/filepath" ) var ( @@ -15,22 +18,40 @@ var ( ) func main() { - // Load configuration - if err := loadConfig(); err != nil { - log.Fatalf("Failed to load config: %v", err) + // CLI flags + dataDir := flag.String("d", ".", "Data directory (stores tagliatelle.db and uploads/ subfolder)") + port := flag.String("p", "8080", "Port to listen on") + flag.Parse() + + // Derive paths from -d + dbPath := filepath.Join(*dataDir, "tagliatelle.db") + uploadDir := filepath.Join(*dataDir, "uploads") + serverPort := fmt.Sprintf(":%s", *port) + + // Create necessary directories + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Fatalf("Failed to create upload directory: %v", err) } + os.MkdirAll("static", 0755) // Initialize database var err error - db, err = InitDatabase(config.DatabasePath) + db, err = InitDatabase(dbPath) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } defer db.Close() - // Create necessary directories - os.MkdirAll(config.UploadDir, 0755) - os.MkdirAll("static", 0755) + // Load config from database (gallery size, items per page, aliases, sed rules) + config, err = LoadConfig(db) + if err != nil { + log.Fatalf("Failed to load config from database: %v", err) + } + + // Inject runtime values (not stored in DB) + config.DatabasePath = dbPath + config.UploadDir = uploadDir + config.ServerPort = serverPort // Initialize templates tmpl, err = InitTemplates() diff --git a/readme.md b/readme.md @@ -11,7 +11,7 @@ Very rough around the edges, but functional. Primarily intended for personal use ``` cd tagliatelle go get github.com/mattn/go-sqlite3 -go run . +go run . -d your_directory -p 8080 ``` Then access the server via a web browser, the default port is 8080. diff --git a/static/sed-rules.js b/static/sed-rules.js @@ -94,13 +94,27 @@ function setupSedRulesForm() { } } - // Update hidden field with JSON - document.getElementById('sed_rules_json').value = JSON.stringify(sedRules); - - // Let the form submit normally (don't prevent default) + // Remove any previously-injected hidden fields from a prior submit + this.querySelectorAll('input[data-generated]').forEach(el => el.remove()); + + // Append one hidden field per value — no JSON + sedRules.forEach((rule, i) => { + appendHidden(this, `sed_rules[${i}][name]`, rule.name); + appendHidden(this, `sed_rules[${i}][description]`, rule.description); + appendHidden(this, `sed_rules[${i}][command]`, rule.command); + }); }); } +function appendHidden(form, name, value) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + input.dataset.generated = '1'; + form.appendChild(input); +} + function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; diff --git a/static/tag-alias.js b/static/tag-alias.js @@ -93,18 +93,36 @@ function updateAlias(groupIndex, aliasIndex, value) { } document.getElementById('aliases-form').addEventListener('submit', function(e) { - // Filter out empty groups and aliases + // Filter out incomplete groups (need a category and at least 2 aliases) const cleanedGroups = aliasGroups .filter(group => group.category && group.aliases && group.aliases.length > 0) .map(group => ({ category: group.category.trim(), aliases: group.aliases.filter(a => a && a.trim()).map(a => a.trim()) })) - .filter(group => group.aliases.length >= 2); // Need at least 2 values to be an alias + .filter(group => group.aliases.length >= 2); - document.getElementById('aliases_json').value = JSON.stringify(cleanedGroups); + // Remove any previously-injected hidden fields from a prior submit + this.querySelectorAll('input[data-generated]').forEach(el => el.remove()); + + // Append one hidden field per value — no JSON + cleanedGroups.forEach((group, gi) => { + appendHidden(this, `aliases[${gi}][category]`, group.category); + group.aliases.forEach((alias, ai) => { + appendHidden(this, `aliases[${gi}][aliases][${ai}]`, alias); + }); + }); }); +function appendHidden(form, name, value) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + input.dataset.generated = '1'; + form.appendChild(input); +} + // Initial render document.addEventListener('DOMContentLoaded', function() { renderAliasGroups();