Clean slate initialization of Goondex - a fast, local-first media indexer. Features: - SQLite database with WAL mode and foreign keys - Full schema for performers, studios, scenes, and tags - Many-to-many relationships via junction tables - CRUD stores for all entities with search capabilities - CLI with performer/studio/scene search commands - Pluggable scraper architecture with registry - TPDB client stub (ready for implementation) - Configuration system via YAML files - Comprehensive .gitignore and documentation Architecture: - cmd/goondex: CLI application with Cobra - cmd/goondexd: Daemon placeholder for v0.2.0 - internal/db: Database layer with stores - internal/model: Clean data models - internal/scraper: Scraper interface and TPDB client - config/: YAML configuration templates Database schema includes indexes on common query fields and uses RFC3339 timestamps for consistency. Built and tested successfully with Go 1.25.4. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.leaktechnologies.dev/stu/Goondex/internal/model"
|
|
)
|
|
|
|
// StudioStore handles CRUD operations for studios
|
|
type StudioStore struct {
|
|
db *DB
|
|
}
|
|
|
|
// NewStudioStore creates a new studio store
|
|
func NewStudioStore(db *DB) *StudioStore {
|
|
return &StudioStore{db: db}
|
|
}
|
|
|
|
// Create inserts a new studio
|
|
func (s *StudioStore) Create(studio *model.Studio) error {
|
|
now := time.Now()
|
|
studio.CreatedAt = now
|
|
studio.UpdatedAt = now
|
|
|
|
result, err := s.db.conn.Exec(`
|
|
INSERT INTO studios (name, parent_id, image_path, image_url, description, source, source_id, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, studio.Name, studio.ParentID, studio.ImagePath, studio.ImageURL, studio.Description, studio.Source, studio.SourceID, studio.CreatedAt.Format(time.RFC3339), studio.UpdatedAt.Format(time.RFC3339))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create studio: %w", err)
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get last insert id: %w", err)
|
|
}
|
|
|
|
studio.ID = id
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves a studio by ID
|
|
func (s *StudioStore) GetByID(id int64) (*model.Studio, error) {
|
|
studio := &model.Studio{}
|
|
var createdAt, updatedAt string
|
|
|
|
err := s.db.conn.QueryRow(`
|
|
SELECT id, name, parent_id, image_path, image_url, description, source, source_id, created_at, updated_at
|
|
FROM studios WHERE id = ?
|
|
`, id).Scan(&studio.ID, &studio.Name, &studio.ParentID, &studio.ImagePath, &studio.ImageURL, &studio.Description, &studio.Source, &studio.SourceID, &createdAt, &updatedAt)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("studio not found")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get studio: %w", err)
|
|
}
|
|
|
|
studio.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
|
studio.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
|
|
|
return studio, nil
|
|
}
|
|
|
|
// Search searches for studios by name
|
|
func (s *StudioStore) Search(query string) ([]model.Studio, error) {
|
|
rows, err := s.db.conn.Query(`
|
|
SELECT id, name, parent_id, image_path, image_url, description, source, source_id, created_at, updated_at
|
|
FROM studios
|
|
WHERE name LIKE ?
|
|
ORDER BY name
|
|
`, "%"+query+"%")
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to search studios: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var studios []model.Studio
|
|
for rows.Next() {
|
|
var studio model.Studio
|
|
var createdAt, updatedAt string
|
|
|
|
err := rows.Scan(&studio.ID, &studio.Name, &studio.ParentID, &studio.ImagePath, &studio.ImageURL, &studio.Description, &studio.Source, &studio.SourceID, &createdAt, &updatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan studio: %w", err)
|
|
}
|
|
|
|
studio.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
|
studio.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
|
|
|
studios = append(studios, studio)
|
|
}
|
|
|
|
return studios, nil
|
|
}
|
|
|
|
// Update updates an existing studio
|
|
func (s *StudioStore) Update(studio *model.Studio) error {
|
|
studio.UpdatedAt = time.Now()
|
|
|
|
result, err := s.db.conn.Exec(`
|
|
UPDATE studios
|
|
SET name = ?, parent_id = ?, image_path = ?, image_url = ?, description = ?, source = ?, source_id = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`, studio.Name, studio.ParentID, studio.ImagePath, studio.ImageURL, studio.Description, studio.Source, studio.SourceID, studio.UpdatedAt.Format(time.RFC3339), studio.ID)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update studio: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("studio not found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes a studio by ID
|
|
func (s *StudioStore) Delete(id int64) error {
|
|
result, err := s.db.conn.Exec("DELETE FROM studios WHERE id = ?", id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete studio: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("studio not found")
|
|
}
|
|
|
|
return nil
|
|
}
|