Goondex/internal/db/scene_store.go
Team Goon 2e747c6660 Initial release: v0.1.0-dev1
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>
2025-11-14 21:37:26 -05:00

201 lines
5.6 KiB
Go

package db
import (
"database/sql"
"fmt"
"time"
"git.leaktechnologies.dev/stu/Goondex/internal/model"
)
// SceneStore handles CRUD operations for scenes
type SceneStore struct {
db *DB
}
// NewSceneStore creates a new scene store
func NewSceneStore(db *DB) *SceneStore {
return &SceneStore{db: db}
}
// Create inserts a new scene
func (s *SceneStore) Create(scene *model.Scene) error {
now := time.Now()
scene.CreatedAt = now
scene.UpdatedAt = now
result, err := s.db.conn.Exec(`
INSERT INTO scenes (title, code, date, studio_id, description, image_path, image_url, director, url, source, source_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, scene.Title, scene.Code, scene.Date, scene.StudioID, scene.Description, scene.ImagePath, scene.ImageURL, scene.Director, scene.URL, scene.Source, scene.SourceID, scene.CreatedAt.Format(time.RFC3339), scene.UpdatedAt.Format(time.RFC3339))
if err != nil {
return fmt.Errorf("failed to create scene: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("failed to get last insert id: %w", err)
}
scene.ID = id
return nil
}
// GetByID retrieves a scene by ID
func (s *SceneStore) GetByID(id int64) (*model.Scene, error) {
scene := &model.Scene{}
var createdAt, updatedAt string
err := s.db.conn.QueryRow(`
SELECT id, title, code, date, studio_id, description, image_path, image_url, director, url, source, source_id, created_at, updated_at
FROM scenes WHERE id = ?
`, id).Scan(&scene.ID, &scene.Title, &scene.Code, &scene.Date, &scene.StudioID, &scene.Description, &scene.ImagePath, &scene.ImageURL, &scene.Director, &scene.URL, &scene.Source, &scene.SourceID, &createdAt, &updatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("scene not found")
}
if err != nil {
return nil, fmt.Errorf("failed to get scene: %w", err)
}
scene.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
scene.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
return scene, nil
}
// Search searches for scenes by title or code
func (s *SceneStore) Search(query string) ([]model.Scene, error) {
rows, err := s.db.conn.Query(`
SELECT id, title, code, date, studio_id, description, image_path, image_url, director, url, source, source_id, created_at, updated_at
FROM scenes
WHERE title LIKE ? OR code LIKE ?
ORDER BY date DESC, title
`, "%"+query+"%", "%"+query+"%")
if err != nil {
return nil, fmt.Errorf("failed to search scenes: %w", err)
}
defer rows.Close()
var scenes []model.Scene
for rows.Next() {
var scene model.Scene
var createdAt, updatedAt string
err := rows.Scan(&scene.ID, &scene.Title, &scene.Code, &scene.Date, &scene.StudioID, &scene.Description, &scene.ImagePath, &scene.ImageURL, &scene.Director, &scene.URL, &scene.Source, &scene.SourceID, &createdAt, &updatedAt)
if err != nil {
return nil, fmt.Errorf("failed to scan scene: %w", err)
}
scene.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
scene.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
scenes = append(scenes, scene)
}
return scenes, nil
}
// Update updates an existing scene
func (s *SceneStore) Update(scene *model.Scene) error {
scene.UpdatedAt = time.Now()
result, err := s.db.conn.Exec(`
UPDATE scenes
SET title = ?, code = ?, date = ?, studio_id = ?, description = ?, image_path = ?, image_url = ?, director = ?, url = ?, source = ?, source_id = ?, updated_at = ?
WHERE id = ?
`, scene.Title, scene.Code, scene.Date, scene.StudioID, scene.Description, scene.ImagePath, scene.ImageURL, scene.Director, scene.URL, scene.Source, scene.SourceID, scene.UpdatedAt.Format(time.RFC3339), scene.ID)
if err != nil {
return fmt.Errorf("failed to update scene: %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("scene not found")
}
return nil
}
// Delete deletes a scene by ID
func (s *SceneStore) Delete(id int64) error {
result, err := s.db.conn.Exec("DELETE FROM scenes WHERE id = ?", id)
if err != nil {
return fmt.Errorf("failed to delete scene: %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("scene not found")
}
return nil
}
// AddPerformer associates a performer with a scene
func (s *SceneStore) AddPerformer(sceneID, performerID int64) error {
_, err := s.db.conn.Exec(`
INSERT OR IGNORE INTO scene_performers (scene_id, performer_id)
VALUES (?, ?)
`, sceneID, performerID)
if err != nil {
return fmt.Errorf("failed to add performer to scene: %w", err)
}
return nil
}
// RemovePerformer removes a performer association from a scene
func (s *SceneStore) RemovePerformer(sceneID, performerID int64) error {
_, err := s.db.conn.Exec(`
DELETE FROM scene_performers
WHERE scene_id = ? AND performer_id = ?
`, sceneID, performerID)
if err != nil {
return fmt.Errorf("failed to remove performer from scene: %w", err)
}
return nil
}
// AddTag associates a tag with a scene
func (s *SceneStore) AddTag(sceneID, tagID int64) error {
_, err := s.db.conn.Exec(`
INSERT OR IGNORE INTO scene_tags (scene_id, tag_id)
VALUES (?, ?)
`, sceneID, tagID)
if err != nil {
return fmt.Errorf("failed to add tag to scene: %w", err)
}
return nil
}
// RemoveTag removes a tag association from a scene
func (s *SceneStore) RemoveTag(sceneID, tagID int64) error {
_, err := s.db.conn.Exec(`
DELETE FROM scene_tags
WHERE scene_id = ? AND tag_id = ?
`, sceneID, tagID)
if err != nil {
return fmt.Errorf("failed to remove tag from scene: %w", err)
}
return nil
}