diff --git a/model/entities.go b/model/entities.go index 7b91fe3a..0383a08b 100644 --- a/model/entities.go +++ b/model/entities.go @@ -46,6 +46,7 @@ func RegisterEntities() { datastore.RegisterEntity(typeUser, func() datastore.Entity { return new(User) }) datastore.RegisterEntity(typeVariable, func() datastore.Entity { return new(Variable) }) datastore.RegisterEntity(typeFeed, func() datastore.Entity { return new(Feed) }) + datastore.RegisterEntity(typeSubFeed, func() datastore.Entity { return new(SubFeed) }) datastore.RegisterEntity(typeSubscriber, func() datastore.Entity { return new(Subscriber) }) datastore.RegisterEntity(typeSubscription, func() datastore.Entity { return new(Subscription) }) datastore.RegisterEntity(TypeSubscriberRegion, func() datastore.Entity { return new(SubscriberRegion) }) diff --git a/model/model_test.go b/model/model_test.go index 0e188560..04173171 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -1458,6 +1458,100 @@ func testFeed(t *testing.T, kind string) { store.Delete(ctx, store.IDKey(typeFeed, testFeedID)) } +func TestSubFeed(t *testing.T) { + const ( + testSubFeedID = 1234567890 + testSubFeedFeedID = 9876543210 + testSubFeedSource = "https://youtube.com/watch?v=1234567890" + ) + + ctx := context.Background() + store, err := datastore.NewStore(ctx, "file", "vidgrind", "") + if err != nil { + t.Fatalf("could not get store: %v", err) + } + + startTime := time.Now().UTC().Truncate(0) + + // Add an arbitrary amount of time to differentiate start and finish. + finishTime := startTime.Add(1 * time.Hour) + + subfeed := &SubFeed{ + ID: testSubFeedID, + FeedID: testSubFeedFeedID, + Source: testSubFeedSource, + Active: true, + Start: startTime, + Finish: finishTime, + } + err = CreateSubFeed(ctx, store, subfeed) + if err != nil { + t.Errorf("could not create subfeed: %v", err) + } + + subfeed2, err := GetSubFeed(ctx, store, testSubFeedID, testSubFeedFeedID) + if err != nil { + t.Errorf("could not get subfeed: %v", err) + } + + assert.Equal(t, subfeed, subfeed2, "Got different subfeed than put, got: \n%+v, wanted \n%+v", subfeed2, subfeed) + + subfeed.Source = "https://youtube.com/watch?v=0987654321" + subfeed, err = UpdateSubFeed(ctx, store, subfeed) + if err != nil { + t.Errorf("could not update subfeed: %v", err) + } + + subfeed3, err := GetSubFeed(ctx, store, testSubFeedID, testSubFeedFeedID) + if err != nil { + t.Errorf("could not get subfeed: %v", err) + } + + assert.Equal(t, subfeed, subfeed3, "Got different subfeed than put, got: \n%+v, wanted \n%+v", subfeed3, subfeed) + + newSubfeed := &SubFeed{ + ID: testSubFeedID + 1, + FeedID: testSubFeedFeedID, + Source: "https://youtube.com/watch?v=1122334455", + Active: true, + Start: startTime, + Finish: finishTime, + } + err = CreateSubFeed(ctx, store, newSubfeed) + if err != nil { + t.Errorf("could not create new subfeed: %v", err) + } + + subfeeds, err := GetSubFeedsByFeed(ctx, store, testSubFeedFeedID) + if err != nil { + t.Errorf("could not get all subfeeds: %v", err) + } + + assert.Equal(t, []SubFeed{*subfeed, *newSubfeed}, subfeeds, "Got different subfeeds than put, got: \n%+v, wanted \n%+v", subfeeds, []SubFeed{*subfeed, *newSubfeed}) + + err = DeleteSubFeed(ctx, store, testSubFeedID, testSubFeedFeedID) + if err != nil { + t.Errorf("could not delete subfeed: %v", err) + } + + subfeed4, err := GetSubFeed(ctx, store, testSubFeedID, testSubFeedFeedID) + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected ErrNoSuchEntity, got %v", err) + } + + if subfeed4 != nil { + t.Errorf("expected nil, got %v", subfeed4) + } + + // Cleanup. + t.Cleanup(func() { + err := os.RemoveAll("vidgrind") + if err != nil { + panic(err) + } + }) +} + // Benchmarks follow. // These are executed by running "go test -bench=." diff --git a/model/subfeed.go b/model/subfeed.go new file mode 100644 index 00000000..d34dd73e --- /dev/null +++ b/model/subfeed.go @@ -0,0 +1,126 @@ +/* +AUTHORS + David Sutton + +LICENSE + Copyright (C) 2025 the Australian Ocean Lab (AusOcean). + + This is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + in gpl.txt. If not, see http://www.gnu.org/licenses/. +*/ + +package model + +import ( + "context" + "fmt" + "time" + + "github.com/ausocean/openfish/datastore" +) + +const typeSubFeed = "SubFeed" // SubFeed datastore type. + +// SubFeed captures all the information about a short-lived instance of a feed. +type SubFeed struct { + ID int64 // Unique 10 digit ID. + FeedID int64 // Parent Feed ID. + Source string // Feed source URL, e.g., a YouTube URL, or a URL to an AusOcean data stream (such as weather data). + Active bool // True if active, false if historical. + Start time.Time // Start time. + Finish time.Time // Finish time. +} + +// Copy copies a SubFeed to dst, or returns a copy of the SubFeed when dst is nil. +func (f *SubFeed) Copy(dst datastore.Entity) (datastore.Entity, error) { + var f2 *SubFeed + if dst == nil { + f2 = new(SubFeed) + } else { + var ok bool + f2, ok = dst.(*SubFeed) + if !ok { + return nil, datastore.ErrWrongType + } + } + *f2 = *f + return f2, nil +} + +// GetCache returns nil, indicating no caching. +func (f *SubFeed) GetCache() datastore.Cache { + return nil +} + +// GetSubFeed retrieves a SubFeed entity from the datastore by its ID. +func GetSubFeed(ctx context.Context, store datastore.Store, ID, feedID int64) (*SubFeed, error) { + key := store.NameKey(typeSubFeed, fmt.Sprintf("%d.%d", feedID, ID)) + + subfeed := &SubFeed{} + err := store.Get(ctx, key, subfeed) + if err != nil { + return nil, fmt.Errorf("error getting subfeed by ID (%d): %w", ID, err) + } + + return subfeed, nil +} + +// GetAllSubFeeds retrieves all SubFeed entities from the datastore for a given FeedID. +func GetSubFeedsByFeed(ctx context.Context, store datastore.Store, feedID int64) ([]SubFeed, error) { + q := store.NewQuery(typeSubFeed, false, "FeedID", "ID") + q.FilterField("FeedID", "=", feedID) + subfeeds := []SubFeed{} + _, err := store.GetAll(ctx, q, &subfeeds) + if err != nil { + return nil, fmt.Errorf("error getting all subfeeds: %w", err) + } + + return subfeeds, nil +} + +// CreateSubFeed creates a subfeed, or returns an error if a subfeed with the given ID exists. +func CreateSubFeed(ctx context.Context, store datastore.Store, subfeed *SubFeed) error { + key := store.NameKey(typeSubFeed, fmt.Sprintf("%d.%d", subfeed.FeedID, subfeed.ID)) + return store.Create(ctx, key, subfeed) +} + +// UpdateSubFeed updates a subfeed, or returns an error if the subfeed does not exist. +func UpdateSubFeed(ctx context.Context, store datastore.Store, subfeed *SubFeed) (*SubFeed, error) { + key := store.NameKey(typeSubFeed, fmt.Sprintf("%d.%d", subfeed.FeedID, subfeed.ID)) + updated := &SubFeed{} + err := store.Update(ctx, key, func(e datastore.Entity) { + _subfeed := e.(*SubFeed) + _subfeed.ID = subfeed.ID + _subfeed.FeedID = subfeed.FeedID + _subfeed.Source = subfeed.Source + _subfeed.Active = subfeed.Active + _subfeed.Start = subfeed.Start + _subfeed.Finish = subfeed.Finish + }, updated) + return updated, err +} + +// MarkSubFeedInactive updates the Active field of a given subfeed to false. +func MarkSubFeedInactive(ctx context.Context, store datastore.Store, ID, feedID int64) error { + key := store.NameKey(typeSubFeed, fmt.Sprintf("%d.%d", feedID, ID)) + return store.Update(ctx, key, func(e datastore.Entity) { + subfeed := e.(*SubFeed) + subfeed.Active = false + }, &SubFeed{}) +} + +// DeleteSubFeed deletes a subfeed, or returns an error if the subfeed does not exist. +func DeleteSubFeed(ctx context.Context, store datastore.Store, ID, feedID int64) error { + key := store.NameKey(typeSubFeed, fmt.Sprintf("%d.%d", feedID, ID)) + return store.Delete(ctx, key) +}