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>
201 lines
5.6 KiB
Go
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
|
|
}
|