package dbmodels import ( "slices" "strings" "github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "golang.org/x/text/cases" "golang.org/x/text/language" ) type SeriesEntries map[string][]*REntriesSeries func MusenalmIDSearchSeries(app core.App, query string) ([]*Series, error) { series := []*Series{} err := app.RecordQuery(SERIES_TABLE). Where(dbx.Like(MUSENALMID_FIELD, query).Match(true, false)). All(&series) if err != nil { return nil, err } return series, nil } func BasicSearchSeries(app core.App, query string) ([]*Series, []*Series, error) { query = strings.TrimSpace(query) query = datatypes.DeleteTags(query) query = datatypes.NormalizeString(query) query = datatypes.RemovePunctuation(query) query = cases.Lower(language.German).String(query) // TODO: how to normalize, which unicode normalization to use? if query == "" { return []*Series{}, []*Series{}, nil } series, err := TitleSearchSeries(app, query) if err != nil { return nil, nil, err } // INFO: Needing to differentiate matches querysplit := NormalizeQuery(query) if len(querysplit) == 0 { return series, []*Series{}, nil } altids, err := FTS5Search(app, SERIES_TABLE, FTS5QueryRequest{ Fields: []string{SERIES_TITLE_FIELD, ANNOTATION_FIELD, REFERENCES_FIELD}, Query: querysplit, }) if err != nil { return nil, nil, err } /// INFO: this is inefficient, but it only happens when there are matches longer than 3 characters, so we should be fine ids := []any{} outer_loop: for _, id := range altids { for _, i := range series { sid := i.Id if sid == id.ID { continue outer_loop } } ids = append(ids, id.ID) } altseries, err := SeriessesForIds(app, ids) if err != nil { return nil, nil, err } return series, altseries, nil } // INFO: expects a normalized query string func TitleSearchSeries(app core.App, query string) ([]*Series, error) { series := []*Series{} queries := strings.Split(query, " ") q := app.RecordQuery(SERIES_TABLE). Where(dbx.Like(SERIES_TITLE_FIELD, queries[0]).Match(true, true)) if len(queries) > 1 { for _, que := range queries[1:] { q.AndWhere(dbx.Like(SERIES_TITLE_FIELD, que).Match(true, true)) } } err := q. OrderBy(SERIES_TITLE_FIELD). All(&series) if err != nil { return nil, err } return series, nil } // INFO: expects a normalized query string // Returns all ids that match the query func FTS5SearchSeries(app core.App, query string) ([]*FTS5IDQueryResult, error) { seriesids := []*FTS5IDQueryResult{} q := NewFTS5Query(). From(SERIES_TABLE). SelectID() queries := strings.Split(query, " ") for _, que := range queries { que := datatypes.NormalizeString(que) if len(que) >= 3 { q.AndMatch([]string{SERIES_TITLE_FIELD, ANNOTATION_FIELD, REFERENCES_FIELD}, que) } } querystring := q.Query() if querystring == "" { return seriesids, nil } err := app.DB().NewQuery(querystring).All(&seriesids) if err != nil { return nil, err } return seriesids, nil } func IDsForSeriesses(series []*Series) []any { ids := []any{} for _, s := range series { ids = append(ids, s.Id) } return ids } func makeMapForEnrySeries(relations []*REntriesSeries, entries map[string]*Entry) SeriesEntries { m := map[string][]*REntriesSeries{} for _, r := range relations { m[r.Series()] = append(m[r.Series()], r) } for _, rel := range m { slices.SortFunc(rel, func(i, j *REntriesSeries) int { ientry := entries[i.Entry()] jentry := entries[j.Entry()] return ientry.Year() - jentry.Year() }) } return m } func EntriesForSeriesses(app core.App, series []*Series) ( SeriesEntries, map[string]*Entry, error) { ids := IDsForSeriesses(series) relations := []*core.Record{} err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, SERIES_TABLE)). Where(dbx.HashExp{ SERIES_TABLE: ids, }). All(&relations) if err != nil { return nil, nil, err } app.ExpandRecords(relations, []string{ENTRIES_TABLE}, nil) bmap := map[string]*Entry{} for _, r := range relations { record := r.ExpandedOne(ENTRIES_TABLE) if record == nil { continue } entry := NewEntry(record) bmap[entry.Id] = entry } smap := map[string][]*REntriesSeries{} for _, r := range relations { rel := NewREntriesSeries(r) smap[rel.Series()] = append(smap[rel.Series()], rel) } for _, rel := range smap { slices.SortFunc(rel, func(i, j *REntriesSeries) int { ientry := bmap[i.Entry()] jentry := bmap[j.Entry()] return ientry.Year() - jentry.Year() }) } return smap, bmap, nil } func LettersForSeries(app core.App) ([]string, error) { letters := []core.Record{} ids := []string{} err := app.RecordQuery(SERIES_TABLE). Select("upper(substr(" + SERIES_TITLE_FIELD + ", 1, 1)) AS id"). Distinct(true). All(&letters) if err != nil { return nil, err } for _, l := range letters { ids = append(ids, l.GetString("id")) } return ids, nil } func AllAgentsForSeries(app core.App) ([]*Agent, error) { rels := []*core.Record{} // INFO: we could just fetch all relations here err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, AGENTS_TABLE)). GroupBy(AGENTS_TABLE). All(&rels) if err != nil { return nil, err } app.ExpandRecords(rels, []string{AGENTS_TABLE}, nil) agents := []*Agent{} for _, r := range rels { record := r.ExpandedOne(AGENTS_TABLE) if record == nil { continue } agent := NewAgent(record) agents = append(agents, agent) } SortAgentsByName(agents) return agents, err } func SeriesForLetter(app core.App, letter string) ([]*Series, error) { series := []*Series{} err := app.RecordQuery(SERIES_TABLE). Where(dbx.Like(SERIES_TITLE_FIELD, letter).Match(false, true)). OrderBy(SERIES_TITLE_FIELD). All(&series) if err != nil { return nil, err } return series, nil } func SeriesForAgent(app core.App, id string) ([]*Series, SeriesEntries, map[string]*Entry, error) { entries, _, err := EntriesForAgent(app, id) if err != nil { return nil, nil, nil, err } return SeriesForEntries(app, entries) } func SeriesForPlace(app core.App, id string) ([]*Series, SeriesEntries, map[string]*Entry, error) { entries, err := EntriesForPlace(app, id) if err != nil { return nil, nil, nil, err } return SeriesForEntries(app, entries) } func SeriesForEntries(app core.App, entries []*Entry) ([]*Series, SeriesEntries, map[string]*Entry, error) { bids := make([]any, 0, len(entries)) for _, e := range entries { bids = append(bids, e.Id) } srels := []*REntriesSeries{} err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, SERIES_TABLE)). Where(dbx.HashExp{ENTRIES_TABLE: bids}). All(&srels) if err != nil { return nil, nil, nil, err } sids := []any{} for _, s := range srels { sids = append(sids, s.Series()) } series := []*Series{} err = app.RecordQuery(SERIES_TABLE). Where(dbx.HashExp{ID_FIELD: sids}). All(&series) if err != nil { return nil, nil, nil, err } bmap := make(map[string]*Entry, len(entries)) for _, e := range entries { bmap[e.Id] = e } smap := makeMapForEnrySeries(srels, bmap) return series, smap, bmap, nil } func SeriesForYear(app core.App, year int) ([]*Series, SeriesEntries, map[string]*Entry, error) { series, err := EntriesForYear(app, year) if err != nil { return nil, nil, nil, err } return SeriesForEntries(app, series) } func SeriesForId(app core.App, id string) (*Series, error) { s := &Series{} err := app.RecordQuery(SERIES_TABLE). Where(dbx.HashExp{MUSENALMID_FIELD: id}). One(s) if err != nil { return nil, err } return s, nil } func SeriessesForIds(app core.App, ids []any) ([]*Series, error) { series := []*Series{} err := app.RecordQuery(SERIES_TABLE). Where(dbx.HashExp{ID_FIELD: ids}). All(&series) if err != nil { return nil, err } return series, nil }