Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions model/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) })
Expand Down
94 changes: 94 additions & 0 deletions model/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 1 hour have any significance, or just arbitrary ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an arbitrary time so that they are different (not that it really matters)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comment that it's arbitrary


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=."

Expand Down
126 changes: 126 additions & 0 deletions model/subfeed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
AUTHORS
David Sutton <davidsutton@ausocean.org>

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)
}
Loading