import_files.go
package imp
import (
"context"
"database/sql"
"fmt"
"os"
"time"
"skraak/db"
"skraak/utils"
)
// ImportAudioFilesInput defines the input parameters for the import_audio_files tool
type ImportAudioFilesInput struct {
DBPath string `json:"db_path"`
FolderPath string `json:"folder_path"`
DatasetID string `json:"dataset_id"`
LocationID string `json:"location_id"`
ClusterID string `json:"cluster_id"`
Recursive *bool `json:"recursive,omitempty"` // *bool because default is true; plain bool would make "not provided" indistinguishable from "false"
}
// ImportAudioFilesOutput defines the output structure for the import_audio_files tool
type ImportAudioFilesOutput struct {
Summary ImportSummary `json:"summary"`
FileIDs []string `json:"file_ids"`
Errors []FileImportError `json:"errors,omitempty"`
}
// ImportSummary provides summary statistics for the import operation
type ImportSummary struct {
TotalFiles int `json:"total_files"`
ImportedFiles int `json:"imported_files"`
SkippedFiles int `json:"skipped_files"` // Duplicates
FailedFiles int `json:"failed_files"`
AudioMothFiles int `json:"audiomoth_files"`
TotalDuration float64 `json:"total_duration_seconds"`
ProcessingTime string `json:"processing_time"`
}
// ImportAudioFiles batch imports WAV files from a folder with hash-based duplicate detection
func ImportAudioFiles(
ctx context.Context,
input ImportAudioFilesInput,
) (ImportAudioFilesOutput, error) {
startTime := time.Now()
var output ImportAudioFilesOutput
// Default recursive to true
recursive := true
if input.Recursive != nil {
recursive = *input.Recursive
}
// Validate database hierarchy (dataset → location → cluster)
if err := validateImportInput(input, db.ResolveDBPath(input.DBPath, "")); err != nil {
return output, fmt.Errorf("validation failed: %w", err)
}
// Open database
database, err := db.OpenWriteableDB(db.ResolveDBPath(input.DBPath, ""))
if err != nil {
return output, fmt.Errorf("failed to open database: %w", err)
}
defer database.Close()
// Import the cluster (ALL THE LOGIC IS HERE)
tx, err := db.BeginLoggedTx(ctx, database, "import_audio_files")
if err != nil {
return output, fmt.Errorf("failed to begin transaction: %w", err)
}
// Set cluster path if empty (inside transaction for logging + rollback safety)
err = EnsureClusterPath(tx, input.ClusterID, input.FolderPath)
if err != nil {
tx.Rollback()
return output, fmt.Errorf("failed to set cluster path: %w", err)
}
clusterOutput, err := ImportCluster(database, tx, ClusterImportInput{
FolderPath: input.FolderPath,
DatasetID: input.DatasetID,
LocationID: input.LocationID,
ClusterID: input.ClusterID,
Recursive: recursive,
})
if err != nil {
tx.Rollback()
return output, fmt.Errorf("cluster import failed: %w", err)
}
if err := tx.Commit(); err != nil {
return output, fmt.Errorf("transaction commit failed: %w", err)
}
// Map to output format
output = ImportAudioFilesOutput{
Summary: ImportSummary{
TotalFiles: clusterOutput.TotalFiles,
ImportedFiles: clusterOutput.ImportedFiles,
SkippedFiles: clusterOutput.SkippedFiles,
FailedFiles: clusterOutput.FailedFiles,
AudioMothFiles: clusterOutput.AudioMothFiles,
TotalDuration: clusterOutput.TotalDuration,
ProcessingTime: time.Since(startTime).String(),
},
FileIDs: []string{}, // File IDs not tracked currently
Errors: clusterOutput.Errors,
}
return output, nil
}
// validateImportInput validates all input parameters and database relationships
func validateImportInput(input ImportAudioFilesInput, dbPath string) error {
// Verify folder exists
info, err := os.Stat(input.FolderPath)
if err != nil {
return fmt.Errorf("folder not accessible: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("path is not a directory: %s", input.FolderPath)
}
return validateHierarchyIDs(input.DatasetID, input.LocationID, input.ClusterID, dbPath)
}
// validateHierarchyIDs validates dataset/location/cluster ID formats and database relationships
func validateHierarchyIDs(datasetID, locationID, clusterID, dbPath string) error {
// Validate ID formats first (fast fail before DB queries)
if err := utils.ValidateShortID(datasetID, "dataset_id"); err != nil {
return err
}
if err := utils.ValidateShortID(locationID, "location_id"); err != nil {
return err
}
if err := utils.ValidateShortID(clusterID, "cluster_id"); err != nil {
return err
}
return db.WithReadDB(dbPath, func(database *sql.DB) error {
// Verify dataset exists, is active, and is 'structured' type
if err := db.ValidateDatasetTypeForImport(database, datasetID); err != nil {
return err
}
// Verify location exists and belongs to dataset
if err := db.ValidateLocationBelongsToDataset(database, locationID, datasetID); err != nil {
return err
}
// Verify cluster exists and belongs to location
if err := db.ClusterBelongsToLocation(database, clusterID, locationID); err != nil {
return err
}
return nil
})
}