WY2EPG7IXF67RR4R7VVDH4CYXNSEISFCRS5UPUQNZNAVOSNQATNQC 42UVPZU3N47BJIV5KBUN4KDB2JKP5QCGMNEOTZTLWP2XNUCNOARAC 4VO5HC4R37PDGNAN6SX24X2M5G2V4RXIX2WCTFXDOH2TPRZA7DZQC NAZQZRYQTXWVE2VFY65ONSD6O3EUMNRHARCDVH2D2HKM3YH4RGUAC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC 7NS27QXZMVTZBK4VPMYL5IKGSTTAWR6NDG5SOVITNX44VNIRZPMAC X3K56A54LNNXODOH6MK22NTSEUQ54BUEZ3EL6ANKXYNL4RROL73QC 47GPFVLW7RWBBHHUZYMEEYWG3KBJBWELR7RDKMJRWMNRWYJUBR7QC 4AFSDSVWQCDWDJEH3DD2S7UUB2LHLQOZLH5SZ6LS4LCSBG4EORXAC L4STQEXDGCPZXDHTEUBCOQKBMTFDRVXRLNFQHPDHOVXDCJO33LQQC C3YEXRHPVZVGUJDZEUPDYWC5JZYBCSSC2ZHORSYSER5TICPX76WAC RMWLXG5HGB44LH3CEA7FWTAFPSZQZQGH52OHVQJUDP6ASPVDVQJAC // getLocationData retrieves location coordinates and timezonefunc getLocationData(database *sql.DB, locationID string) (*locationData, error) {var loc locationData
// GetLocationData retrieves location coordinates and timezonefunc GetLocationData(database *sql.DB, locationID string) (*LocationData, error) {var loc LocationData
Recursive *bool `json:"recursive,omitempty" jsonschema:"Scan subfolders recursively (default: true)"`
Recursive *bool `json:"recursive,omitempty" jsonschema:"Scan subfolders recursively (default: true)"` // *bool because default is true; plain bool would make "not provided" indistinguishable from "false"
// Validate ID formats first (fast fail before DB queries)if err := utils.ValidateShortID(input.DatasetID, "dataset_id"); err != nil {return err}if err := utils.ValidateShortID(input.LocationID, "location_id"); err != nil {return err}if err := utils.ValidateShortID(input.ClusterID, "cluster_id"); err != nil {return err}
}return validateHierarchyIDs(input.DatasetID, input.LocationID, input.ClusterID, dbPath)}// validateHierarchyIDs validates dataset/location/cluster ID formats and database relationshipsfunc 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
err = database.QueryRow("SELECT EXISTS(SELECT 1 FROM dataset WHERE id = ? AND active = true)", input.DatasetID).Scan(&datasetExists)
err = database.QueryRow("SELECT EXISTS(SELECT 1 FROM dataset WHERE id = ? AND active = true)", datasetID).Scan(&datasetExists)
err = database.QueryRow("SELECT dataset_id FROM location WHERE id = ? AND active = true", input.LocationID).Scan(&locationDatasetID)
err = database.QueryRow("SELECT dataset_id FROM location WHERE id = ? AND active = true", locationID).Scan(&locationDatasetID)
if locationDatasetID != input.DatasetID {return fmt.Errorf("location %s does not belong to dataset %s", input.LocationID, input.DatasetID)
if locationDatasetID != datasetID {return fmt.Errorf("location %s does not belong to dataset %s", locationID, datasetID)
err = database.QueryRow("SELECT location_id FROM cluster WHERE id = ? AND active = true", input.ClusterID).Scan(&clusterLocationID)
err = database.QueryRow("SELECT location_id FROM cluster WHERE id = ? AND active = true", clusterID).Scan(&clusterLocationID)
if clusterLocationID != input.LocationID {return fmt.Errorf("cluster %s does not belong to location %s", input.ClusterID, input.LocationID)
if clusterLocationID != locationID {return fmt.Errorf("cluster %s does not belong to location %s", clusterID, locationID)
}// locationData holds location information (local wrapper around utils types)type locationData struct {Latitude float64Longitude float64TimezoneID string}// fileData holds file metadata (local wrapper around utils types)type fileData struct {FileName stringHash stringDuration float64SampleRate intTimestampLocal time.TimeIsAudioMoth boolMothData *utils.AudioMothDataAstroData utils.AstronomicalData
if err := validateImportInput(ImportAudioFilesInput{DatasetID: input.DatasetID,LocationID: input.LocationID,ClusterID: input.ClusterID,FolderPath: filepath.Dir(input.FilePath), // For validation only}, dbPath); err != nil {
if err := validateHierarchyIDs(input.DatasetID, input.LocationID, input.ClusterID, dbPath); err != nil {
// Phase 3: Get location data for astronomical calculationslocationData, err := getLocationData(dbPath, input.LocationID)
// Phase 3: Open database connection (single connection for all DB operations)database, err := db.OpenWriteableDB(dbPath)if err != nil {return output, fmt.Errorf("database connection failed: %w", err)}defer database.Close()// Phase 4: Get location data for astronomical calculationslocData, err := utils.GetLocationData(database, input.LocationID)
output.FileName = fileData.FileNameoutput.Hash = fileData.Hashoutput.Duration = fileData.Durationoutput.SampleRate = fileData.SampleRateoutput.TimestampLocal = fileData.TimestampLocaloutput.IsAudioMoth = fileData.IsAudioMoth
output.FileName = result.FileNameoutput.Hash = result.Hashoutput.Duration = result.Durationoutput.SampleRate = result.SampleRateoutput.TimestampLocal = result.TimestampLocaloutput.IsAudioMoth = result.IsAudioMoth
// Phase 5: Ensure cluster path is setif err := ensureClusterPath(dbPath, input.ClusterID, filepath.Dir(input.FilePath)); err != nil {
// Phase 6: Ensure cluster path is setif err := utils.EnsureClusterPath(database, input.ClusterID, filepath.Dir(input.FilePath)); err != nil {
// Phase 6: Insert into databasefileID, isDuplicate, err := insertFileIntoDB(dbPath,fileData,input.DatasetID,input.ClusterID,input.LocationID,)
// Phase 7: Insert into databasefileID, isDuplicate, err := insertFileIntoDB(ctx, database, result, input.DatasetID, input.ClusterID, input.LocationID)
// processFile extracts all metadata from a single filefunc processFile(filePath string, location *locationData) (*fileData, error) {result, err := utils.ProcessSingleFile(filePath, location.Latitude, location.Longitude, location.TimezoneID, true)if err != nil {return nil, err}return &fileData{FileName: result.FileName,Hash: result.Hash,Duration: result.Duration,SampleRate: result.SampleRate,TimestampLocal: result.TimestampLocal,IsAudioMoth: result.IsAudioMoth,MothData: result.MothData,AstroData: result.AstroData,}, nil}
fileID, fileData.FileName, fileData.Hash, locationID,fileData.TimestampLocal, clusterID, fileData.Duration, fileData.SampleRate,fileData.AstroData.SolarNight, fileData.AstroData.CivilNight, fileData.AstroData.MoonPhase,
fileID, result.FileName, result.Hash, locationID,result.TimestampLocal, clusterID, result.Duration, result.SampleRate,result.AstroData.SolarNight, result.AstroData.CivilNight, result.AstroData.MoonPhase,
fileData.MothData.Timestamp,&fileData.MothData.RecorderID,&fileData.MothData.Gain,&fileData.MothData.BatteryV,&fileData.MothData.TempC,
result.MothData.Timestamp,&result.MothData.RecorderID,&result.MothData.Gain,&result.MothData.BatteryV,&result.MothData.TempC,
}// getLocationData retrieves location coordinates and timezone from databasefunc getLocationData(dbPath, locationID string) (*locationData, error) {database, err := db.OpenReadOnlyDB(dbPath)if err != nil {return nil, err}defer database.Close()var loc locationDataerr = database.QueryRow("SELECT latitude, longitude, timezone_id FROM location WHERE id = ?",locationID,).Scan(&loc.Latitude, &loc.Longitude, &loc.TimezoneID)if err != nil {return nil, fmt.Errorf("failed to query location data: %w", err)}return &loc, nil
// ensureClusterPath sets the cluster's path field if it's currently emptyfunc ensureClusterPath(dbPath, clusterID, folderPath string) error {database, err := db.OpenWriteableDB(dbPath)if err != nil {return fmt.Errorf("failed to open database: %w", err)}defer database.Close()return utils.EnsureClusterPath(database, clusterID, folderPath)}