package utils
import (
"encoding/json"
"fmt"
"maps"
"os"
"sort"
"strings"
)
type DataFile struct {
Meta *DataMeta
Segments []*Segment
FilePath string
}
type DataMeta struct {
Operator string
Reviewer string
Duration float64
Extra map[string]any }
type Segment struct {
StartTime float64
EndTime float64
FreqLow float64
FreqHigh float64
Labels []*Label
}
const CallTypeNone = "_"
type Label struct {
Species string
Certainty int
Filter string
CallType string
Comment string Bookmark bool Extra map[string]any }
func ParseDataFile(path string) (*DataFile, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("parse JSON: %w", err)
}
if len(raw) == 0 {
return nil, fmt.Errorf("empty .data file")
}
df := &DataFile{
FilePath: path,
Segments: make([]*Segment, 0, len(raw)-1),
}
df.Meta = parseMeta(raw[0])
for i := 1; i < len(raw); i++ {
seg, err := parseSegment(raw[i])
if err != nil {
continue }
df.Segments = append(df.Segments, seg)
}
sort.Slice(df.Segments, func(i, j int) bool {
return df.Segments[i].StartTime < df.Segments[j].StartTime
})
return df, nil
}
func parseMeta(raw json.RawMessage) *DataMeta {
var obj map[string]any
if err := json.Unmarshal(raw, &obj); err != nil {
return &DataMeta{}
}
meta := &DataMeta{Extra: make(map[string]any)}
if v, ok := obj["Operator"].(string); ok {
meta.Operator = v
delete(obj, "Operator")
}
if v, ok := obj["Reviewer"].(string); ok {
meta.Reviewer = v
delete(obj, "Reviewer")
}
if v, ok := obj["Duration"].(float64); ok {
meta.Duration = v
delete(obj, "Duration")
}
maps.Copy(meta.Extra, obj)
return meta
}
func parseSegment(raw json.RawMessage) (*Segment, error) {
var arr []json.RawMessage
if err := json.Unmarshal(raw, &arr); err != nil {
return nil, err
}
if len(arr) < 5 {
return nil, fmt.Errorf("segment too short")
}
seg := &Segment{}
if v, err := parseFloat(arr[0]); err == nil {
seg.StartTime = v
}
if v, err := parseFloat(arr[1]); err == nil {
seg.EndTime = v
}
if v, err := parseFloat(arr[2]); err == nil {
seg.FreqLow = v
}
if v, err := parseFloat(arr[3]); err == nil {
seg.FreqHigh = v
}
var labelArr []json.RawMessage
if err := json.Unmarshal(arr[4], &labelArr); err == nil {
for _, labelRaw := range labelArr {
if label := parseLabel(labelRaw); label != nil {
seg.Labels = append(seg.Labels, label)
}
}
}
sort.Slice(seg.Labels, func(i, j int) bool {
return seg.Labels[i].Species < seg.Labels[j].Species
})
return seg, nil
}
func parseLabel(raw json.RawMessage) *Label {
var obj map[string]any
if err := json.Unmarshal(raw, &obj); err != nil {
return nil
}
label := &Label{Extra: make(map[string]any)}
if v, ok := obj["species"].(string); ok {
label.Species = v
delete(obj, "species")
}
if v, ok := obj["certainty"].(float64); ok {
label.Certainty = int(v)
delete(obj, "certainty")
}
if v, ok := obj["filter"].(string); ok {
label.Filter = v
delete(obj, "filter")
}
if v, ok := obj["calltype"].(string); ok {
label.CallType = v
delete(obj, "calltype")
}
if v, ok := obj["comment"].(string); ok {
label.Comment = v
delete(obj, "comment")
}
if v, ok := obj["bookmark"].(bool); ok {
label.Bookmark = v
delete(obj, "bookmark")
}
maps.Copy(label.Extra, obj)
return label
}
func parseFloat(raw json.RawMessage) (float64, error) {
var v float64
err := json.Unmarshal(raw, &v)
return v, err
}
func (df *DataFile) Write(path string) error {
var raw []any
meta := make(map[string]any)
if df.Meta.Operator != "" {
meta["Operator"] = df.Meta.Operator
}
if df.Meta.Reviewer != "" {
meta["Reviewer"] = df.Meta.Reviewer
}
if df.Meta.Duration > 0 {
meta["Duration"] = df.Meta.Duration
}
maps.Copy(meta, df.Meta.Extra)
raw = append(raw, meta)
for _, seg := range df.Segments {
labels := make([]any, 0, len(seg.Labels))
for _, label := range seg.Labels {
l := make(map[string]any)
l["species"] = label.Species
l["certainty"] = label.Certainty
if label.Filter != "" {
l["filter"] = label.Filter
}
if label.CallType != "" {
l["calltype"] = label.CallType
}
if label.Comment != "" {
l["comment"] = label.Comment
}
if label.Bookmark {
l["bookmark"] = true
}
maps.Copy(l, label.Extra)
labels = append(labels, l)
}
segArr := []any{
seg.StartTime,
seg.EndTime,
seg.FreqLow,
seg.FreqHigh,
labels,
}
raw = append(raw, segArr)
}
data, err := json.MarshalIndent(raw, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
func (s *Segment) HasFilterLabel(filter string) bool {
if filter == "" {
return true
}
for _, label := range s.Labels {
if label.Filter == filter {
return true
}
}
return false
}
func (s *Segment) GetFilterLabels(filter string) []*Label {
var result []*Label
for _, label := range s.Labels {
if filter == "" || label.Filter == filter {
result = append(result, label)
}
}
return result
}
func (s *Segment) SegmentMatchesFilters(filter, species, callType string, certainty int) bool {
if filter == "" && species == "" && callType == "" && certainty < 0 {
return true }
for _, label := range s.Labels {
if labelMatchesFilters(label, filter, species, callType, certainty) {
return true
}
}
return false
}
func labelMatchesFilters(label *Label, filter, species, callType string, certainty int) bool {
if filter != "" && label.Filter != filter {
return false
}
if species != "" && label.Species != species {
return false
}
if callType == CallTypeNone {
if label.CallType != "" {
return false
}
} else if callType != "" && label.CallType != callType {
return false
}
if certainty >= 0 && label.Certainty != certainty {
return false
}
return true
}
func ParseSpeciesCallType(label string) (species, callType string) {
if label == "" {
return "", ""
}
if before, after, ok := strings.Cut(label, "+"); ok {
return before, after
}
return label, ""
}
func FindDataFiles(folder string) ([]string, error) {
return FindFiles(folder, FindFilesOptions{
Extension: ".data",
Recursive: false,
SkipHidden: true,
})
}