this repo has no description

feat: add videostream handler

Eric Davis 14bd694b 80c57926

Changed files
+238 -1
cmd
videostream
pkg
+4 -1
Makefile
··· 1 - all: bin/mostliked bin/feedweb 1 + all: bin/videostream bin/mostliked bin/feedweb 2 + 3 + bin/videostream: cmd/videostream/*.go pkg/videostream/*.go 4 + go build -o $@ ./cmd/videostream 2 5 3 6 bin/mostliked: cmd/mostliked/main.go pkg/mostliked/handler.go db/mostliked/*.go pkg/feeds/*.go 4 7 go build -o $@ ./cmd/mostliked
+67
cmd/videostream/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "log" 7 + "os" 8 + "os/signal" 9 + "syscall" 10 + 11 + jetstream "github.com/bluesky-social/jetstream/pkg/models" 12 + "github.com/edavis/bsky-feeds/pkg/videostream" 13 + "github.com/gorilla/websocket" 14 + _ "github.com/mattn/go-sqlite3" 15 + ) 16 + 17 + const JetstreamUrl = `wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post` 18 + 19 + func main() { 20 + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 21 + defer stop() 22 + 23 + conn, _, err := websocket.DefaultDialer.DialContext(ctx, JetstreamUrl, nil) 24 + if err != nil { 25 + log.Fatalf("failed to open websocket: %v\n", err) 26 + } 27 + defer func() { 28 + if err := conn.Close(); err != nil { 29 + log.Printf("failed to close websocket: %v\n", err) 30 + } 31 + log.Printf("websocket closed\n") 32 + }() 33 + 34 + dbCnx, err := sql.Open("sqlite3", "data/videostream.db?_journal=WAL&_fk=on&_timeout=5000&_sync=1&_txlock=immediate") 35 + if err != nil { 36 + log.Fatalf("failed to open database: %v\n", err) 37 + } 38 + defer func() { 39 + if _, err := dbCnx.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { 40 + log.Printf("error doing final WAL checkpoint: %v\n", err) 41 + } 42 + if err := dbCnx.Close(); err != nil { 43 + log.Printf("failed to close db: %v\n", err) 44 + } 45 + log.Printf("db closed\n") 46 + }() 47 + 48 + queue := videostream.NewQueue(1000) 49 + go videostream.Handler(ctx, queue, dbCnx) 50 + 51 + log.Printf("starting up\n") 52 + go func() { 53 + for { 54 + var event jetstream.Event 55 + err := conn.ReadJSON(&event) 56 + if err != nil { 57 + log.Printf("ReadJSON error: %v\n", err) 58 + stop() 59 + break 60 + } 61 + queue.Enqueue(event) 62 + } 63 + }() 64 + 65 + <-ctx.Done() 66 + log.Printf("shutting down\n") 67 + }
+7
pkg/videostream/checkpoint.go
··· 1 + package videostream 2 + 3 + type CheckpointResults struct { 4 + Blocked int 5 + Pages int 6 + Transferred int 7 + }
+1
pkg/videostream/generator.go
··· 1 + package videostream
+107
pkg/videostream/handler.go
··· 1 + package videostream 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + _ "embed" 7 + "encoding/json" 8 + "fmt" 9 + "log" 10 + "time" 11 + 12 + appbsky "github.com/bluesky-social/indigo/api/bsky" 13 + jetstream "github.com/bluesky-social/jetstream/pkg/models" 14 + "github.com/edavis/bsky-feeds/pkg/feeds" 15 + _ "github.com/mattn/go-sqlite3" 16 + ) 17 + 18 + //go:embed schema.sql 19 + var ddl string 20 + 21 + func Handler(ctx context.Context, events *Queue, dbCnx *sql.DB) { 22 + var ( 23 + dbTx *sql.Tx 24 + err error 25 + eventCount int 26 + ) 27 + 28 + if _, err = dbCnx.ExecContext(ctx, ddl); err != nil { 29 + log.Printf("could not create tables: %v\n", err) 30 + } 31 + if _, err = dbCnx.ExecContext(ctx, `PRAGMA wal_autocheckpoint = 0`); err != nil { 32 + log.Printf("could not set PRAGMA wal_autocheckpoint: %v\n", err) 33 + } 34 + 35 + for { 36 + select { 37 + case <-ctx.Done(): 38 + return 39 + default: 40 + } 41 + 42 + event, ok := events.Dequeue() 43 + if !ok { 44 + time.Sleep(100 * time.Millisecond) 45 + continue 46 + } 47 + 48 + if dbTx == nil { 49 + dbTx, err = dbCnx.BeginTx(ctx, nil) 50 + if err != nil { 51 + log.Printf("failed to begin transaction: %v\n", err) 52 + } 53 + } 54 + 55 + if event.Kind != jetstream.EventKindCommit { 56 + continue 57 + } 58 + 59 + if event.Commit.Operation != jetstream.CommitOperationCreate { 60 + continue 61 + } 62 + 63 + commit := *event.Commit 64 + var post appbsky.FeedPost 65 + if err = json.Unmarshal(commit.Record, &post); err != nil { 66 + log.Printf("error parsing commit.Record: %v\n", err) 67 + continue 68 + } 69 + 70 + if post.Embed != nil && post.Embed.EmbedVideo != nil { 71 + uri := fmt.Sprintf("at://%s/%s/%s", event.Did, commit.Collection, commit.RKey) 72 + ts := feeds.SafeTimestamp(post.CreatedAt) 73 + dbTx.ExecContext(ctx, `insert or ignore into posts (uri, create_ts) values (?, ?)`, uri, ts) 74 + } else { 75 + continue 76 + } 77 + 78 + eventCount += 1 79 + if eventCount%25 == 0 { 80 + // TODO trim 81 + 82 + if err = dbTx.Commit(); err != nil { 83 + log.Printf("commit failed: %v\n", err) 84 + } 85 + 86 + var results CheckpointResults 87 + err = dbCnx.QueryRowContext(ctx, `PRAGMA wal_checkpoint(RESTART)`).Scan(&results.Blocked, &results.Pages, &results.Transferred) 88 + switch { 89 + case err != nil: 90 + log.Printf("failed checkpoint: %v\n", err) 91 + case results.Blocked == 1: 92 + log.Printf("checkpoint: blocked\n") 93 + case results.Pages == results.Transferred: 94 + log.Printf("checkpoint: %d pages transferred\n", results.Transferred) 95 + case results.Pages != results.Transferred: 96 + log.Printf("checkpoint: %d pages, %d transferred\n", results.Pages, results.Transferred) 97 + } 98 + 99 + dbTx, err = dbCnx.BeginTx(ctx, nil) 100 + if err != nil { 101 + log.Printf("failed to begin transaction: %v\n", err) 102 + } 103 + 104 + log.Printf("queue size: %d\n", events.Size()) 105 + } 106 + } 107 + }
+46
pkg/videostream/queue.go
··· 1 + package videostream 2 + 3 + import ( 4 + "sync" 5 + 6 + jetstream "github.com/bluesky-social/jetstream/pkg/models" 7 + ) 8 + 9 + type Queue struct { 10 + lk sync.Mutex 11 + events []jetstream.Event 12 + } 13 + 14 + func NewQueue(capacity int) *Queue { 15 + return &Queue{ 16 + events: make([]jetstream.Event, 0, capacity), 17 + } 18 + } 19 + 20 + func (q *Queue) Enqueue(event jetstream.Event) { 21 + q.lk.Lock() 22 + defer q.lk.Unlock() 23 + 24 + q.events = append(q.events, event) 25 + } 26 + 27 + func (q *Queue) Dequeue() (jetstream.Event, bool) { 28 + q.lk.Lock() 29 + defer q.lk.Unlock() 30 + 31 + if len(q.events) == 0 { 32 + var e jetstream.Event 33 + return e, false 34 + } 35 + 36 + event := q.events[0] 37 + q.events = q.events[1:] 38 + return event, true 39 + } 40 + 41 + func (q *Queue) Size() int { 42 + q.lk.Lock() 43 + defer q.lk.Unlock() 44 + 45 + return len(q.events) 46 + }
+6
pkg/videostream/schema.sql
··· 1 + create table if not exists posts ( 2 + uri text primary key, 3 + create_ts int not null 4 + ); 5 + 6 + create index if not exists ts_idx on posts(create_ts);