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, COALESCE(code, ''), COALESCE(date, ''), COALESCE(studio_id, 0), COALESCE(description, ''), COALESCE(image_path, ''), COALESCE(image_url, ''), COALESCE(director, ''), COALESCE(url, ''), COALESCE(source, ''), COALESCE(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, COALESCE(code, ''), COALESCE(date, ''), COALESCE(studio_id, 0), COALESCE(description, ''), COALESCE(image_path, ''), COALESCE(image_url, ''), COALESCE(director, ''), COALESCE(url, ''), COALESCE(source, ''), COALESCE(source_id, ''), created_at, updated_at FROM scenes WHERE title LIKE ? OR COALESCE(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 { return s.AddTagWithConfidence(sceneID, tagID, 1.0, "user", false) } // AddTagWithConfidence associates a tag with a scene with ML support func (s *SceneStore) AddTagWithConfidence(sceneID, tagID int64, confidence float64, source string, verified bool) error { verifiedInt := 0 if verified { verifiedInt = 1 } _, err := s.db.conn.Exec(` INSERT OR REPLACE INTO scene_tags (scene_id, tag_id, confidence, source, verified, created_at) VALUES (?, ?, ?, ?, ?, datetime('now')) `, sceneID, tagID, confidence, source, verifiedInt) if err != nil { return fmt.Errorf("failed to add tag to scene: %w", err) } return nil } // VerifyTag marks a scene tag as human-verified func (s *SceneStore) VerifyTag(sceneID, tagID int64) error { _, err := s.db.conn.Exec(` UPDATE scene_tags SET verified = 1 WHERE scene_id = ? AND tag_id = ? `, sceneID, tagID) if err != nil { return fmt.Errorf("failed to verify tag: %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 } // GetPerformers retrieves all performers for a scene func (s *SceneStore) GetPerformers(sceneID int64) ([]model.Performer, error) { rows, err := s.db.conn.Query(` SELECT p.id, p.name, COALESCE(p.aliases, ''), COALESCE(p.gender, ''), COALESCE(p.birthday, ''), COALESCE(p.nationality, ''), COALESCE(p.source, ''), COALESCE(p.source_id, ''), p.created_at, p.updated_at FROM performers p INNER JOIN scene_performers sp ON p.id = sp.performer_id WHERE sp.scene_id = ? ORDER BY p.name `, sceneID) if err != nil { return nil, fmt.Errorf("failed to get performers: %w", err) } defer rows.Close() var performers []model.Performer for rows.Next() { var p model.Performer var createdAt, updatedAt string err := rows.Scan(&p.ID, &p.Name, &p.Aliases, &p.Gender, &p.Birthday, &p.Nationality, &p.Source, &p.SourceID, &createdAt, &updatedAt) if err != nil { return nil, fmt.Errorf("failed to scan performer: %w", err) } p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) performers = append(performers, p) } return performers, nil } // GetTags retrieves all tags for a scene func (s *SceneStore) GetTags(sceneID int64) ([]model.Tag, error) { rows, err := s.db.conn.Query(` SELECT t.id, t.name, t.category_id, COALESCE(t.description, ''), COALESCE(t.source, ''), COALESCE(t.source_id, ''), t.created_at, t.updated_at FROM tags t INNER JOIN scene_tags st ON t.id = st.tag_id WHERE st.scene_id = ? ORDER BY t.name `, sceneID) if err != nil { return nil, fmt.Errorf("failed to get tags: %w", err) } defer rows.Close() var tags []model.Tag for rows.Next() { var t model.Tag var createdAt, updatedAt string err := rows.Scan(&t.ID, &t.Name, &t.CategoryID, &t.Description, &t.Source, &t.SourceID, &createdAt, &updatedAt) if err != nil { return nil, fmt.Errorf("failed to scan tag: %w", err) } t.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) t.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) tags = append(tags, t) } return tags, nil } // Upsert inserts or updates a scene based on source_id func (s *SceneStore) Upsert(scene *model.Scene) error { // Try to find existing scene by source_id existing, err := s.GetBySourceID(scene.Source, scene.SourceID) if err == nil && existing != nil { // Update existing scene.ID = existing.ID return s.Update(scene) } // Create new return s.Create(scene) } // GetBySourceID retrieves a scene by its source and source_id func (s *SceneStore) GetBySourceID(source, sourceID string) (*model.Scene, error) { var scene model.Scene var createdAt, updatedAt string err := s.db.conn.QueryRow(` SELECT id, title, COALESCE(code, ''), COALESCE(date, ''), COALESCE(studio_id, 0), COALESCE(description, ''), COALESCE(image_path, ''), COALESCE(image_url, ''), COALESCE(director, ''), COALESCE(url, ''), COALESCE(source, ''), COALESCE(source_id, ''), created_at, updated_at FROM scenes WHERE source = ? AND source_id = ? `, source, sourceID).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, nil } 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 } // GetByPerformer retrieves all scenes featuring a specific performer func (s *SceneStore) GetByPerformer(performerID int64) ([]model.Scene, error) { rows, err := s.db.conn.Query(` SELECT DISTINCT s.id, s.title, COALESCE(s.code, ''), COALESCE(s.date, ''), COALESCE(s.studio_id, 0), COALESCE(s.description, ''), COALESCE(s.image_path, ''), COALESCE(s.image_url, ''), COALESCE(s.director, ''), COALESCE(s.url, ''), COALESCE(s.source, ''), COALESCE(s.source_id, ''), s.created_at, s.updated_at FROM scenes s INNER JOIN scene_performers sp ON s.id = sp.scene_id WHERE sp.performer_id = ? ORDER BY s.date DESC, s.title ASC `, performerID) if err != nil { return nil, fmt.Errorf("failed to get scenes for performer: %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 } // GetMovies retrieves all movies that contain this scene func (s *SceneStore) GetMovies(sceneID int64) ([]model.Movie, error) { rows, err := s.db.conn.Query(` SELECT m.id, m.title, COALESCE(m.date, ''), COALESCE(m.studio_id, 0), COALESCE(m.description, ''), COALESCE(m.director, ''), COALESCE(m.duration, 0), COALESCE(m.image_path, ''), COALESCE(m.image_url, ''), COALESCE(m.back_image_url, ''), COALESCE(m.url, ''), COALESCE(m.source, ''), COALESCE(m.source_id, ''), m.created_at, m.updated_at, COALESCE(ms.scene_number, 0) FROM movies m INNER JOIN movie_scenes ms ON m.id = ms.movie_id WHERE ms.scene_id = ? ORDER BY m.date DESC, m.title ASC `, sceneID) if err != nil { return nil, fmt.Errorf("failed to get movies for scene: %w", err) } defer rows.Close() var movies []model.Movie for rows.Next() { var m model.Movie var studioID sql.NullInt64 var createdAt, updatedAt string var sceneNumber int err := rows.Scan( &m.ID, &m.Title, &m.Date, &studioID, &m.Description, &m.Director, &m.Duration, &m.ImagePath, &m.ImageURL, &m.BackImageURL, &m.URL, &m.Source, &m.SourceID, &createdAt, &updatedAt, &sceneNumber, ) if err != nil { return nil, fmt.Errorf("failed to scan movie: %w", err) } if studioID.Valid && studioID.Int64 > 0 { m.StudioID = &studioID.Int64 } m.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) m.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) movies = append(movies, m) } return movies, nil }