From 4d207eed8bbe243fad9fa45f7bd1ca83550c04c9 Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Sat, 9 Mar 2024 12:19:55 +0100 Subject: [PATCH] made the jump from remote api to local postgres --- .gitignore | 4 +- Makefile | 2 +- cmd/api-service/handler/job.go | 2 +- cmd/api-service/handler/movie.go | 2 +- cmd/api-service/moviestore/sqlite.go | 142 +++++------ cmd/api-service/service.go | 6 +- cmd/import/main.go | 57 +++++ cmd/terminal-client/main.go | 30 --- go.mod | 1 + go.sum | 2 + {cmd/api-service/job => job}/job.go | 0 {cmd/api-service/job => job}/queue.go | 29 +-- {cmd/api-service/job => job}/worker.go | 0 storage/moviepg.go | 98 ++++++++ storage/postgres.go | 165 ++++++++++++ storage/reviewpg.go | 236 ++++++++++++++++++ terminal-client/main.go | 47 ++++ .../tui/basemodel.go | 20 +- .../tui/movie.go | 0 .../tui/review.go | 0 .../tui/tabemdb.go | 18 +- .../tui/tabreview.go | 18 +- .../tui/tabset.go | 0 .../tui/tabtmdb.go | 21 +- .../tui/tui.go | 6 +- 25 files changed, 748 insertions(+), 158 deletions(-) create mode 100644 cmd/import/main.go delete mode 100644 cmd/terminal-client/main.go rename {cmd/api-service/job => job}/job.go (100%) rename {cmd/api-service/job => job}/queue.go (82%) rename {cmd/api-service/job => job}/worker.go (100%) create mode 100644 storage/moviepg.go create mode 100644 storage/postgres.go create mode 100644 storage/reviewpg.go create mode 100644 terminal-client/main.go rename {cmd/terminal-client => terminal-client}/tui/basemodel.go (80%) rename {cmd/terminal-client => terminal-client}/tui/movie.go (100%) rename {cmd/terminal-client => terminal-client}/tui/review.go (100%) rename {cmd/terminal-client => terminal-client}/tui/tabemdb.go (93%) rename {cmd/terminal-client => terminal-client}/tui/tabreview.go (91%) rename {cmd/terminal-client => terminal-client}/tui/tabset.go (100%) rename {cmd/terminal-client => terminal-client}/tui/tabtmdb.go (84%) rename {cmd/terminal-client => terminal-client}/tui/tui.go (80%) diff --git a/.gitignore b/.gitignore index 07766d4..1a5beff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ *.db-wal emdb emdb-api -public \ No newline at end of file +public +*.sql +*.sql.bak \ No newline at end of file diff --git a/Makefile b/Makefile index da6b3ad..460507f 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ run-tui-local: EMDB_BASE_URL=http://localhost:8085/ EMDB_API_KEY=hoi go run ./cmd/terminal-client/main.go run-tui: - go run ./cmd/terminal-client/main.go + go run ./terminal-client/main.go run-md-export: go run ./cmd/markdown-export/main.go diff --git a/cmd/api-service/handler/job.go b/cmd/api-service/handler/job.go index af82672..c101d39 100644 --- a/cmd/api-service/handler/job.go +++ b/cmd/api-service/handler/job.go @@ -8,8 +8,8 @@ import ( "log/slog" "net/http" - "code.ewintr.nl/emdb/cmd/api-service/job" "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "code.ewintr.nl/emdb/job" ) type JobAPI struct { diff --git a/cmd/api-service/handler/movie.go b/cmd/api-service/handler/movie.go index 891d90b..08e4f66 100644 --- a/cmd/api-service/handler/movie.go +++ b/cmd/api-service/handler/movie.go @@ -10,8 +10,8 @@ import ( "log/slog" "net/http" - "code.ewintr.nl/emdb/cmd/api-service/job" "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "code.ewintr.nl/emdb/job" "github.com/google/uuid" ) diff --git a/cmd/api-service/moviestore/sqlite.go b/cmd/api-service/moviestore/sqlite.go index 9e069bf..900ac09 100644 --- a/cmd/api-service/moviestore/sqlite.go +++ b/cmd/api-service/moviestore/sqlite.go @@ -135,9 +135,9 @@ func NewSQLite(dbPath string) (*SQLite, error) { db: db, } - if err := s.migrate(sqliteMigrations); err != nil { - return &SQLite{}, err - } + //if err := s.migrate(sqliteMigrations); err != nil { + // return &SQLite{}, err + //} return s, nil } @@ -154,71 +154,71 @@ func (s *SQLite) Query(query string, args ...any) (*sql.Rows, error) { return s.db.Query(query, args...) } -func (s *SQLite) migrate(wanted []sqliteMigration) error { - // admin table - if _, err := s.db.Exec(` -CREATE TABLE IF NOT EXISTS migration -("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT) -`); err != nil { - return err - } - - // find existing - rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`) - if err != nil { - return fmt.Errorf("%w: %v", ErrSqliteFailure, err) - } - - existing := []sqliteMigration{} - for rows.Next() { - var query string - if err := rows.Scan(&query); err != nil { - return fmt.Errorf("%w: %v", ErrSqliteFailure, err) - } - existing = append(existing, sqliteMigration(query)) - } - rows.Close() - - // compare - missing, err := compareMigrations(wanted, existing) - if err != nil { - return fmt.Errorf("%w: %v", ErrSqliteFailure, err) - } - - // execute missing - for _, query := range missing { - if _, err := s.db.Exec(string(query)); err != nil { - return fmt.Errorf("%w: %v", ErrSqliteFailure, err) - } - - // register - if _, err := s.db.Exec(` -INSERT INTO migration -(query) VALUES (?) -`, query); err != nil { - return fmt.Errorf("%w: %v", ErrSqliteFailure, err) - } - } - - return nil -} - -func compareMigrations(wanted, existing []sqliteMigration) ([]sqliteMigration, error) { - needed := []sqliteMigration{} - if len(wanted) < len(existing) { - return []sqliteMigration{}, ErrNotEnoughSQLMigrations - } - - for i, want := range wanted { - switch { - case i >= len(existing): - needed = append(needed, want) - case want == existing[i]: - // do nothing - case want != existing[i]: - return []sqliteMigration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want) - } - } - - return needed, nil -} +//func (s *SQLite) migrate(wanted []sqliteMigration) error { +// // admin table +// if _, err := s.db.Exec(` +//CREATE TABLE IF NOT EXISTS migration +//("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT) +//`); err != nil { +// return err +// } +// +// // find existing +// rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`) +// if err != nil { +// return fmt.Errorf("%w: %v", ErrSqliteFailure, err) +// } +// +// existing := []sqliteMigration{} +// for rows.Next() { +// var query string +// if err := rows.Scan(&query); err != nil { +// return fmt.Errorf("%w: %v", ErrSqliteFailure, err) +// } +// existing = append(existing, sqliteMigration(query)) +// } +// rows.Close() +// +// // compare +// missing, err := compareMigrations(wanted, existing) +// if err != nil { +// return fmt.Errorf("%w: %v", ErrSqliteFailure, err) +// } +// +// // execute missing +// for _, query := range missing { +// if _, err := s.db.Exec(string(query)); err != nil { +// return fmt.Errorf("%w: %v", ErrSqliteFailure, err) +// } +// +// // register +// if _, err := s.db.Exec(` +//INSERT INTO migration +//(query) VALUES (?) +//`, query); err != nil { +// return fmt.Errorf("%w: %v", ErrSqliteFailure, err) +// } +// } +// +// return nil +//} +// +//func compareMigrations(wanted, existing []sqliteMigration) ([]sqliteMigration, error) { +// needed := []sqliteMigration{} +// if len(wanted) < len(existing) { +// return []sqliteMigration{}, ErrNotEnoughSQLMigrations +// } +// +// for i, want := range wanted { +// switch { +// case i >= len(existing): +// needed = append(needed, want) +// case want == existing[i]: +// // do nothing +// case want != existing[i]: +// return []sqliteMigration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want) +// } +// } +// +// return needed, nil +//} diff --git a/cmd/api-service/service.go b/cmd/api-service/service.go index 1292cb1..ff1af33 100644 --- a/cmd/api-service/service.go +++ b/cmd/api-service/service.go @@ -11,8 +11,8 @@ import ( "code.ewintr.nl/emdb/client" "code.ewintr.nl/emdb/cmd/api-service/handler" - "code.ewintr.nl/emdb/cmd/api-service/job" "code.ewintr.nl/emdb/cmd/api-service/moviestore" + job2 "code.ewintr.nl/emdb/job" ) var ( @@ -32,8 +32,8 @@ func main() { os.Exit(1) } - jobQueue := job.NewJobQueue(db, logger) - worker := job.NewWorker(jobQueue, moviestore.NewMovieRepository(db), moviestore.NewReviewRepository(db), client.NewIMDB(), logger) + jobQueue := job2.NewJobQueue(db, logger) + worker := job2.NewWorker(jobQueue, moviestore.NewMovieRepository(db), moviestore.NewReviewRepository(db), client.NewIMDB(), logger) go worker.Run() apis := handler.APIIndex{ diff --git a/cmd/import/main.go b/cmd/import/main.go new file mode 100644 index 0000000..f11aa94 --- /dev/null +++ b/cmd/import/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "os" + + "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "code.ewintr.nl/emdb/storage" +) + +func main() { + dbSQLite, err := moviestore.NewSQLite("./emdb.db") + if err != nil { + fmt.Printf("could not create new sqlite repo: %s", err.Error()) + os.Exit(1) + } + + pgConnStr := "" + dbPostgres, err := storage.NewPostgres(pgConnStr) + if err != nil { + fmt.Printf("could not create new postgres repo: %s", err.Error()) + os.Exit(1) + } + + //fmt.Println("movies") + //movieRepoSqlite := moviestore.NewMovieRepository(dbSQLite) + //movieRepoPG := moviestore.NewMovieRepositoryPG(dbPostgres) + // + //movies, err := movieRepoSqlite.FindAll() + //if err != nil { + // fmt.Println(err) + // os.Exit(1) + //} + //for _, movie := range movies { + // if err := movieRepoPG.Store(movie); err != nil { + // fmt.Println(err) + // os.Exit(1) + // } + //} + fmt.Println("reviews") + reviewRepoSqlite := moviestore.NewReviewRepository(dbSQLite) + reviewRepoPG := storage.NewReviewRepositoryPG(dbPostgres) + + reviews, err := reviewRepoSqlite.FindAll() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + for _, review := range reviews { + if err := reviewRepoPG.Store(review); err != nil { + fmt.Println(err) + os.Exit(1) + } + } + + fmt.Println("success") +} diff --git a/cmd/terminal-client/main.go b/cmd/terminal-client/main.go deleted file mode 100644 index e658ca4..0000000 --- a/cmd/terminal-client/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "code.ewintr.nl/emdb/client" - "code.ewintr.nl/emdb/cmd/terminal-client/tui" -) - -func main() { - logger := tui.NewLogger() - tmdb, err := client.NewTMDB(os.Getenv("TMDB_API_KEY")) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - emdb := client.NewEMDB(os.Getenv("EMDB_BASE_URL"), os.Getenv("EMDB_API_KEY")) - - p, err := tui.New(emdb, tmdb, logger) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - logger.SetProgram(p) - if _, err := p.Run(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} diff --git a/go.mod b/go.mod index d50bb58..d6908d5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/charmbracelet/lipgloss v0.10.0 github.com/cyruzin/golang-tmdb v1.6.0 github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 github.com/muesli/termenv v0.15.2 github.com/tmc/langchaingo v0.1.5 modernc.org/sqlite v1.29.3 diff --git a/go.sum b/go.sum index 7d85c55..39522eb 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/cmd/api-service/job/job.go b/job/job.go similarity index 100% rename from cmd/api-service/job/job.go rename to job/job.go diff --git a/cmd/api-service/job/queue.go b/job/queue.go similarity index 82% rename from cmd/api-service/job/queue.go rename to job/queue.go index 55eeed5..68918e9 100644 --- a/cmd/api-service/job/queue.go +++ b/job/queue.go @@ -9,14 +9,15 @@ import ( "time" "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "code.ewintr.nl/emdb/storage" ) type JobQueue struct { - db *moviestore.SQLite + db *storage.Postgres logger *slog.Logger } -func NewJobQueue(db *moviestore.SQLite, logger *slog.Logger) *JobQueue { +func NewJobQueue(db *storage.Postgres, logger *slog.Logger) *JobQueue { jq := &JobQueue{ db: db, logger: logger.With("service", "jobqueue"), @@ -38,7 +39,7 @@ func (jq *JobQueue) Run() { UPDATE job_queue SET status = 'todo' WHERE status = 'doing' - AND strftime('%s', 'now') - strftime('%s', updated_at) > 2*24*60*60;`); err != nil { + AND EXTRACT(EPOCH FROM now() - updated_at) > 2*24*60*60;`); err != nil { logger.Error("could not clean up job queue", "error", err) } } @@ -50,8 +51,9 @@ func (jq *JobQueue) Add(movieID, action string) error { return errors.New("invalid action") } - _, err := jq.db.Exec(`INSERT INTO job_queue (action_id, action, status) - VALUES (?, ?, 'todo')`, movieID, action) + _, err := jq.db.Exec(` +INSERT INTO job_queue (action_id, action, status) +VALUES ($1, $2, 'todo');`, movieID, action) return err } @@ -68,9 +70,9 @@ func (jq *JobQueue) Next(t moviestore.JobType) (moviestore.Job, error) { SELECT id, action_id, action FROM job_queue WHERE status='todo' - AND action IN %s + AND action = ANY($1) ORDER BY id ASC -LIMIT 1`, actionsStr) +LIMIT 1;`, actionsStr) row := jq.db.QueryRow(query) var job moviestore.Job err := row.Scan(&job.ID, &job.ActionID, &job.Action) @@ -85,7 +87,7 @@ LIMIT 1`, actionsStr) if _, err := jq.db.Exec(` UPDATE job_queue SET status='doing' -WHERE id=?`, job.ID); err != nil { +WHERE id=$1;`, job.ID); err != nil { logger.Error("could not set job to doing", "error") return moviestore.Job{}, err } @@ -97,7 +99,7 @@ func (jq *JobQueue) MarkDone(id int) { logger := jq.logger.With("method", "markdone") if _, err := jq.db.Exec(` DELETE FROM job_queue -WHERE id=?`, id); err != nil { +WHERE id=$1;`, id); err != nil { logger.Error("could not mark job done", "error", err) } return @@ -108,7 +110,7 @@ func (jq *JobQueue) MarkFailed(id int) { if _, err := jq.db.Exec(` UPDATE job_queue SET status='failed' -WHERE id=?`, id); err != nil { +WHERE id=$1;`, id); err != nil { logger.Error("could not mark job failed", "error", err) } return @@ -118,7 +120,7 @@ func (jq *JobQueue) List() ([]moviestore.Job, error) { rows, err := jq.db.Query(` SELECT id, action_id, action, status, created_at, updated_at FROM job_queue -ORDER BY id DESC`) +ORDER BY id DESC;`) if err != nil { return nil, err } @@ -138,15 +140,14 @@ ORDER BY id DESC`) func (jq *JobQueue) Delete(id string) error { if _, err := jq.db.Exec(` DELETE FROM job_queue -WHERE id=?`, id); err != nil { +WHERE id=$1;`, id); err != nil { return err } return nil } func (jq *JobQueue) DeleteAll() error { - if _, err := jq.db.Exec(` -DELETE FROM job_queue`); err != nil { + if _, err := jq.db.Exec(`DELETE FROM job_queue;`); err != nil { return err } return nil diff --git a/cmd/api-service/job/worker.go b/job/worker.go similarity index 100% rename from cmd/api-service/job/worker.go rename to job/worker.go diff --git a/storage/moviepg.go b/storage/moviepg.go new file mode 100644 index 0000000..7d4fd69 --- /dev/null +++ b/storage/moviepg.go @@ -0,0 +1,98 @@ +package storage + +import ( + "fmt" + "strings" + + "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "github.com/google/uuid" +) + +type MovieRepositoryPG struct { + db *Postgres +} + +func NewMovieRepositoryPG(db *Postgres) *MovieRepositoryPG { + return &MovieRepositoryPG{ + db: db, + } +} + +func (mr *MovieRepositoryPG) Store(m moviestore.Movie) error { + if m.ID == "" { + m.ID = uuid.New().String() + } + + directors := strings.Join(m.Directors, ",") + if _, err := mr.db.Exec(`INSERT INTO movie (id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +ON CONFLICT (id) DO UPDATE +SET + tmdb_id = EXCLUDED.tmdb_id, + imdb_id = EXCLUDED.imdb_id, + title = EXCLUDED.title, + english_title = EXCLUDED.english_title, + year = EXCLUDED.year, + directors = EXCLUDED.directors, + summary = EXCLUDED.summary, + watched_on = EXCLUDED.watched_on, + rating = EXCLUDED.rating, + comment = EXCLUDED.comment;`, + m.ID, m.TMDBID, m.IMDBID, m.Title, m.EnglishTitle, m.Year, directors, m.Summary, m.WatchedOn, m.Rating, m.Comment); err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + + return nil +} + +func (mr *MovieRepositoryPG) Delete(id string) error { + if _, err := mr.db.Exec(`DELETE FROM movie WHERE id=$1`, id); err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + + return nil +} + +func (mr *MovieRepositoryPG) FindOne(id string) (moviestore.Movie, error) { + row := mr.db.QueryRow(` +SELECT id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment +FROM movie +WHERE id=$1`, id) + if row.Err() != nil { + return moviestore.Movie{}, row.Err() + } + + m := moviestore.Movie{ + ID: id, + } + var directors string + if err := row.Scan(&m.ID, &m.TMDBID, &m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.Summary, &m.WatchedOn, &m.Rating, &m.Comment); err != nil { + return moviestore.Movie{}, fmt.Errorf("%w: %w", moviestore.ErrSqliteFailure, err) + } + m.Directors = strings.Split(directors, ",") + + return m, nil +} + +func (mr *MovieRepositoryPG) FindAll() ([]moviestore.Movie, error) { + rows, err := mr.db.Query(` +SELECT id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment +FROM movie`) + if err != nil { + return nil, fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + + movies := make([]moviestore.Movie, 0) + defer rows.Close() + for rows.Next() { + m := moviestore.Movie{} + var directors string + if err := rows.Scan(&m.ID, &m.TMDBID, &m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.Summary, &m.WatchedOn, &m.Rating, &m.Comment); err != nil { + return nil, fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + m.Directors = strings.Split(directors, ",") + movies = append(movies, m) + } + + return movies, nil +} diff --git a/storage/postgres.go b/storage/postgres.go new file mode 100644 index 0000000..4e31d5a --- /dev/null +++ b/storage/postgres.go @@ -0,0 +1,165 @@ +package storage + +import ( + "database/sql" + "fmt" + + "code.ewintr.nl/emdb/cmd/api-service/moviestore" + _ "github.com/lib/pq" +) + +type migration string + +var migrations = []migration{ + `CREATE TABLE movie ( + "id" TEXT UNIQUE NOT NULL, + "imdb_id" TEXT NOT NULL DEFAULT '', + "title" TEXT NOT NULL DEFAULT '', + "english_title" TEXT NOT NULL DEFAULT '', + "year" INTEGER NOT NULL DEFAULT 0, + "directors" TEXT NOT NULL DEFAULT '', + "watched_on" TEXT NOT NULL DEFAULT '', + "rating" INTEGER NOT NULL DEFAULT 0, + "comment" TEXT NOT NULL DEFAULT '', + "tmdb_id" INTEGER NOT NULL DEFAULT 0, + "summary" TEXT NOT NULL DEFAULT '' + );`, + `CREATE TABLE movie_new ( + "id" TEXT UNIQUE NOT NULL, + "imdb_id" TEXT UNIQUE NOT NULL DEFAULT '', + "tmdb_id" INTEGER UNIQUE NOT NULL DEFAULT 0, + "title" TEXT NOT NULL DEFAULT '', + "english_title" TEXT NOT NULL DEFAULT '', + "year" INTEGER NOT NULL DEFAULT 0, + "directors" TEXT NOT NULL DEFAULT '', + "summary" TEXT NOT NULL DEFAULT '', + "watched_on" TEXT NOT NULL DEFAULT '', + "rating" INTEGER NOT NULL DEFAULT 0, + "comment" TEXT NOT NULL DEFAULT '' + );`, + `CREATE TABLE system ("latest_sync" INTEGER);`, + `CREATE TABLE review ( + "id" TEXT UNIQUE NOT NULL, + "movie_id" TEXT NOT NULL, + "source" TEXT NOT NULL DEFAULT '', + "url" TEXT NOT NULL DEFAULT '', + "review" TEXT NOT NULL DEFAULT '', + "references" TEXT NOT NULL DEFAULT '', + "quality" INTEGER NOT NULL DEFAULT 0, + "mentions" TEXT NOT NULL DEFAULT '', + "movie_rating" INTEGER NOT NULL DEFAULT 0, + "mentioned_titles" JSONB NOT NULL DEFAULT '[]' + );`, + `CREATE TABLE job_queue ( + "id" SERIAL PRIMARY KEY, + "action_id" TEXT NOT NULL, + "action" TEXT NOT NULL DEFAULT '', + "status" TEXT NOT NULL DEFAULT '', + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + );`, +} + +type Postgres struct { + db *sql.DB +} + +func NewPostgres(connStr string) (*Postgres, error) { + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, err + } + + pg := &Postgres{ + db: db, + } + + if err := pg.migrate(migrations); err != nil { + return &Postgres{}, err + } + + return pg, nil +} + +func (pg *Postgres) migrate(wanted []migration) error { + // admin table + if _, err := pg.db.Exec(` +CREATE TABLE IF NOT EXISTS migration +( + id SERIAL PRIMARY KEY, + query TEXT +)`); err != nil { + return err + } + + // find existing + rows, err := pg.db.Query(`SELECT query FROM migration ORDER BY id`) + if err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + + existing := []migration{} + for rows.Next() { + var query string + if err := rows.Scan(&query); err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + existing = append(existing, migration(query)) + } + rows.Close() + + // compare + missing, err := compareMigrations(wanted, existing) + if err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + + // execute missing + for _, query := range missing { + if _, err := pg.db.Exec(string(query)); err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + + // register + if _, err := pg.db.Exec(` +INSERT INTO migration +(query) VALUES ($1) +`, query); err != nil { + return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err) + } + } + + return nil +} + +func (pg *Postgres) Exec(query string, args ...any) (sql.Result, error) { + return pg.db.Exec(query, args...) +} + +func (pg *Postgres) QueryRow(query string, args ...any) *sql.Row { + return pg.db.QueryRow(query, args...) +} + +func (pg *Postgres) Query(query string, args ...any) (*sql.Rows, error) { + return pg.db.Query(query, args...) +} + +func compareMigrations(wanted, existing []migration) ([]migration, error) { + needed := []migration{} + if len(wanted) < len(existing) { + return []migration{}, moviestore.ErrNotEnoughSQLMigrations + } + + for i, want := range wanted { + switch { + case i >= len(existing): + needed = append(needed, want) + case want == existing[i]: + // do nothing + case want != existing[i]: + return []migration{}, fmt.Errorf("%w: %v", moviestore.ErrIncompatibleSQLMigration, want) + } + } + + return needed, nil +} diff --git a/storage/reviewpg.go b/storage/reviewpg.go new file mode 100644 index 0000000..d67cbdf --- /dev/null +++ b/storage/reviewpg.go @@ -0,0 +1,236 @@ +package storage + +import ( + "encoding/json" + + "code.ewintr.nl/emdb/cmd/api-service/moviestore" +) + +//const ( +// ReviewSourceIMDB = "imdb" +// +// MentionsSeparator = "|" +//) +// +//type ReviewSource string +// +//type Titles struct { +// Movies []string `json:"movies"` +// TVShows []string `json:"tvShows"` +// Games []string `json:"games"` +// Books []string `json:"books"` +//} +// +//type Review struct { +// ID string +// MovieID string +// Source ReviewSource +// URL string +// Review string +// MovieRating int +// Quality int +// Titles Titles +//} + +type ReviewRepositoryPG struct { + db *Postgres +} + +func NewReviewRepositoryPG(db *Postgres) *ReviewRepositoryPG { + return &ReviewRepositoryPG{ + db: db, + } +} + +func (rr *ReviewRepositoryPG) Store(r moviestore.Review) error { + titles, err := json.Marshal(r.Titles) + if err != nil { + return err + } + if _, err := rr.db.Exec(`INSERT INTO review (id, movie_id, source, url, review, movie_rating, quality, mentioned_titles) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +ON CONFLICT (id) DO UPDATE SET movie_id = EXCLUDED.movie_id, source = EXCLUDED.source, url = EXCLUDED.url, +review = EXCLUDED.review, movie_rating = EXCLUDED.movie_rating, quality = EXCLUDED.quality, +mentioned_titles = EXCLUDED.mentioned_titles;`, + r.ID, r.MovieID, r.Source, r.URL, r.Review, r.MovieRating, r.Quality, titles); err != nil { + return err + } + + return nil +} + +func (rr *ReviewRepositoryPG) FindOne(id string) (moviestore.Review, error) { + row := rr.db.QueryRow(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review +WHERE id=$1`, id) + if row.Err() != nil { + return moviestore.Review{}, row.Err() + } + + r := moviestore.Review{} + var titles string + if err := row.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return moviestore.Review{}, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return moviestore.Review{}, err + } + + return r, nil +} + +func (rr *ReviewRepositoryPG) FindByMovieID(movieID string) ([]moviestore.Review, error) { + rows, err := rr.db.Query(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review +WHERE movie_id=$1`, movieID) + if err != nil { + return nil, err + } + + reviews := make([]moviestore.Review, 0) + var titles string + for rows.Next() { + r := moviestore.Review{} + if err := rows.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return []moviestore.Review{}, err + } + reviews = append(reviews, r) + } + rows.Close() + + return reviews, nil +} + +func (rr *ReviewRepositoryPG) FindNextUnrated() (moviestore.Review, error) { + row := rr.db.QueryRow(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review +WHERE quality=0 +LIMIT 1`) + if row.Err() != nil { + return moviestore.Review{}, row.Err() + } + + r := moviestore.Review{} + var titles string + if err := row.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return moviestore.Review{}, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return moviestore.Review{}, err + } + + return r, nil +} + +func (rr *ReviewRepositoryPG) FindUnrated() ([]moviestore.Review, error) { + rows, err := rr.db.Query(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review +WHERE quality=0`) + if err != nil { + return nil, err + } + + reviews := make([]moviestore.Review, 0) + var titles string + for rows.Next() { + r := moviestore.Review{} + if err := rows.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return []moviestore.Review{}, err + } + reviews = append(reviews, r) + } + rows.Close() + + return reviews, nil +} + +func (rr *ReviewRepositoryPG) FindNextNoTitles() (moviestore.Review, error) { + row := rr.db.QueryRow(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review +WHERE mentioned_titles='{}' +LIMIT 1`) + if row.Err() != nil { + return moviestore.Review{}, row.Err() + } + + r := moviestore.Review{} + var titles string + if err := row.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return moviestore.Review{}, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return moviestore.Review{}, err + } + + return r, nil +} + +func (rr *ReviewRepositoryPG) FindNoTitles() ([]moviestore.Review, error) { + rows, err := rr.db.Query(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review +WHERE mentioned_titles='{}'`) + if err != nil { + return nil, err + } + + reviews := make([]moviestore.Review, 0) + var titles string + for rows.Next() { + r := moviestore.Review{} + if err := rows.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return []moviestore.Review{}, err + } + reviews = append(reviews, r) + } + rows.Close() + + return reviews, nil +} + +func (rr *ReviewRepositoryPG) FindAll() ([]moviestore.Review, error) { + rows, err := rr.db.Query(` +SELECT id, movie_id, source, url, review, movie_rating, quality, mentioned_titles +FROM review`) + if err != nil { + return nil, err + } + + reviews := make([]moviestore.Review, 0) + var titles string + for rows.Next() { + r := moviestore.Review{} + if err := rows.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review, &r.MovieRating, &r.Quality, &titles); err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(titles), &r.Titles); err != nil { + return []moviestore.Review{}, err + } + reviews = append(reviews, r) + } + rows.Close() + + return reviews, nil +} + +func (rr *ReviewRepositoryPG) DeleteByMovieID(id string) error { + if _, err := rr.db.Exec(`DELETE FROM review WHERE movie_id=$1`, id); err != nil { + return err + } + + return nil +} diff --git a/terminal-client/main.go b/terminal-client/main.go new file mode 100644 index 0000000..709c46c --- /dev/null +++ b/terminal-client/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + + "code.ewintr.nl/emdb/client" + "code.ewintr.nl/emdb/job" + "code.ewintr.nl/emdb/storage" + "code.ewintr.nl/emdb/terminal-client/tui" +) + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + tuiLogger := tui.NewLogger() + tmdb, err := client.NewTMDB(os.Getenv("TMDB_API_KEY")) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + //emdb := client.NewEMDB(os.Getenv("EMDB_BASE_URL"), os.Getenv("EMDB_API_KEY")) + dbHost := os.Getenv("EMDB_DB_HOST") + dbName := os.Getenv("EMDB_DB_NAME") + dbUser := os.Getenv("EMDB_DB_USER") + dbPassword := os.Getenv("EMDB_DB_PASSWORD") + pgConnStr := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", dbHost, dbUser, dbPassword, dbName) + dbPostgres, err := storage.NewPostgres(pgConnStr) + if err != nil { + fmt.Printf("could not create new postgres repo: %s", err.Error()) + os.Exit(1) + } + movieRepo := storage.NewMovieRepositoryPG(dbPostgres) + reviewRepo := storage.NewReviewRepositoryPG(dbPostgres) + jobQueue := job.NewJobQueue(dbPostgres, logger) + + p, err := tui.New(movieRepo, reviewRepo, jobQueue, tmdb, tuiLogger) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + tuiLogger.SetProgram(p) + if _, err := p.Run(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/terminal-client/tui/basemodel.go b/terminal-client/tui/basemodel.go similarity index 80% rename from cmd/terminal-client/tui/basemodel.go rename to terminal-client/tui/basemodel.go index 3eff408..56ee6c3 100644 --- a/cmd/terminal-client/tui/basemodel.go +++ b/terminal-client/tui/basemodel.go @@ -5,12 +5,16 @@ import ( "strings" "code.ewintr.nl/emdb/client" + "code.ewintr.nl/emdb/job" + "code.ewintr.nl/emdb/storage" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" ) type baseModel struct { - emdb *client.EMDB + movieRepo *storage.MovieRepositoryPG + reviewRepo *storage.ReviewRepositoryPG + jobQueue *job.JobQueue tmdb *client.TMDB tabs *TabSet initialized bool @@ -20,12 +24,14 @@ type baseModel struct { contentSize tea.WindowSizeMsg } -func NewBaseModel(emdb *client.EMDB, tmdb *client.TMDB, logger *Logger) (tea.Model, tea.Cmd) { +func NewBaseModel(movieRepo *storage.MovieRepositoryPG, reviewRepo *storage.ReviewRepositoryPG, jobQueue *job.JobQueue, tmdb *client.TMDB, logger *Logger) (tea.Model, tea.Cmd) { logViewport := viewport.New(0, 0) logViewport.KeyMap = viewport.KeyMap{} m := baseModel{ - emdb: emdb, + movieRepo: movieRepo, + reviewRepo: reviewRepo, + jobQueue: jobQueue, tmdb: tmdb, tabs: NewTabSet(), logViewport: logViewport, @@ -53,11 +59,11 @@ func (m baseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.windowSize = msg if !m.initialized { var emdbTab, tmdbTab tea.Model - emdbTab, cmd = NewTabEMDB(m.emdb, m.logger) + emdbTab, cmd = NewTabEMDB(m.movieRepo, m.logger) cmds = append(cmds, cmd) - tmdbTab, cmd = NewTabTMDB(m.emdb, m.tmdb, m.logger) + tmdbTab, cmd = NewTabTMDB(m.movieRepo, m.jobQueue, m.tmdb, m.logger) cmds = append(cmds, cmd) - reviewTab, cmd := NewTabReview(m.emdb, m.logger) + reviewTab, cmd := NewTabReview(m.reviewRepo, m.logger) cmds = append(cmds, cmd) m.tabs.AddTab("emdb", "Watched movies", emdbTab) m.tabs.AddTab("review", "Review", reviewTab) @@ -74,7 +80,7 @@ func (m baseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case NewMovie: m.Log(fmt.Sprintf("imported movie %s", msg.m.Title)) m.tabs.Select("emdb") - cmds = append(cmds, FetchMovieList(m.emdb)) + cmds = append(cmds, FetchMovieList(m.movieRepo)) case error: m.Log(fmt.Sprintf("ERROR: %s", msg.Error())) default: diff --git a/cmd/terminal-client/tui/movie.go b/terminal-client/tui/movie.go similarity index 100% rename from cmd/terminal-client/tui/movie.go rename to terminal-client/tui/movie.go diff --git a/cmd/terminal-client/tui/review.go b/terminal-client/tui/review.go similarity index 100% rename from cmd/terminal-client/tui/review.go rename to terminal-client/tui/review.go diff --git a/cmd/terminal-client/tui/tabemdb.go b/terminal-client/tui/tabemdb.go similarity index 93% rename from cmd/terminal-client/tui/tabemdb.go rename to terminal-client/tui/tabemdb.go index 82ff5dd..a52aeb6 100644 --- a/cmd/terminal-client/tui/tabemdb.go +++ b/terminal-client/tui/tabemdb.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "code.ewintr.nl/emdb/client" + "code.ewintr.nl/emdb/storage" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/textinput" @@ -24,7 +24,7 @@ type StoredMovie struct{} type tabEMDB struct { initialized bool - emdb *client.EMDB + movieRepo *storage.MovieRepositoryPG mode string focused string colWidth int @@ -38,7 +38,7 @@ type tabEMDB struct { logger *Logger } -func NewTabEMDB(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) { +func NewTabEMDB(movieRepo *storage.MovieRepositoryPG, logger *Logger) (tea.Model, tea.Cmd) { del := list.NewDefaultDelegate() list := list.New([]list.Item{}, del, 0, 0) list.Title = "Movies" @@ -65,7 +65,7 @@ func NewTabEMDB(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) { m := tabEMDB{ focused: "form", - emdb: emdb, + movieRepo: movieRepo, logger: logger, mode: "view", list: list, @@ -76,7 +76,7 @@ func NewTabEMDB(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) { } logger.Log("search emdb...") - return m, FetchMovieList(emdb) + return m, FetchMovieList(movieRepo) } func (m tabEMDB) Init() tea.Cmd { @@ -105,7 +105,7 @@ func (m tabEMDB) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) case StoredMovie: m.logger.Log("stored movie, fetching movie list") - cmds = append(cmds, FetchMovieList(m.emdb)) + cmds = append(cmds, FetchMovieList(m.movieRepo)) case tea.KeyMsg: switch m.mode { case "edit": @@ -265,7 +265,7 @@ func (m *tabEMDB) StoreMovie() tea.Cmd { return fmt.Errorf("rating cannot be converted to an int: %w", err) } updatedMovie.m.Comment = m.inputComment.Value() - if _, err := m.emdb.CreateMovie(updatedMovie.m); err != nil { + if err := m.movieRepo.Store(updatedMovie.m); err != nil { return err } return StoredMovie{} @@ -276,9 +276,9 @@ func (m *tabEMDB) Log(s string) { m.logger.Log(s) } -func FetchMovieList(emdb *client.EMDB) tea.Cmd { +func FetchMovieList(movieRepo *storage.MovieRepositoryPG) tea.Cmd { return func() tea.Msg { - ems, err := emdb.GetMovies() + ems, err := movieRepo.FindAll() if err != nil { return err } diff --git a/cmd/terminal-client/tui/tabreview.go b/terminal-client/tui/tabreview.go similarity index 91% rename from cmd/terminal-client/tui/tabreview.go rename to terminal-client/tui/tabreview.go index 8dc9c24..288ee61 100644 --- a/cmd/terminal-client/tui/tabreview.go +++ b/terminal-client/tui/tabreview.go @@ -5,8 +5,8 @@ import ( "strconv" "strings" - "code.ewintr.nl/emdb/client" "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "code.ewintr.nl/emdb/storage" "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -16,7 +16,7 @@ import ( type tabReview struct { initialized bool - emdb *client.EMDB + reviewRepo *storage.ReviewRepositoryPG width int height int mode string @@ -28,7 +28,7 @@ type tabReview struct { logger *Logger } -func NewTabReview(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) { +func NewTabReview(reviewRepo *storage.ReviewRepositoryPG, logger *Logger) (tea.Model, tea.Cmd) { reviewViewport := viewport.New(0, 0) //reviewViewport.KeyMap = viewport.KeyMap{} @@ -42,7 +42,7 @@ func NewTabReview(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) { inputMentions.CharLimit = 500 return &tabReview{ - emdb: emdb, + reviewRepo: reviewRepo, mode: "view", reviewViewport: reviewViewport, inputQuality: inputQuality, @@ -97,7 +97,7 @@ func (m *tabReview) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.formFocus = 0 m.logger.Log("fetching next unrated review") cmds = append(cmds, m.inputQuality.Focus()) - cmds = append(cmds, FetchNextUnratedReview(m.emdb)) + cmds = append(cmds, FetchNextUnratedReview(m.reviewRepo)) default: m.logger.Log(fmt.Sprintf("key: %s", msg.String())) m.reviewViewport, cmd = m.reviewViewport.Update(msg) @@ -115,7 +115,7 @@ func (m *tabReview) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case ReviewStored: m.logger.Log(fmt.Sprintf("stored review %s", msg)) cmds = append(cmds, m.inputQuality.Focus()) - cmds = append(cmds, FetchNextUnratedReview(m.emdb)) + cmds = append(cmds, FetchNextUnratedReview(m.reviewRepo)) } return m, tea.Batch(cmds...) @@ -210,7 +210,7 @@ func (m *tabReview) StoreReview() tea.Cmd { m.selectedReview.Quality = quality //m.selectedReview.Mentions = strings.Split(mentions, ",") - if err := m.emdb.UpdateReview(m.selectedReview); err != nil { + if err := m.reviewRepo.Store(m.selectedReview); err != nil { return err } @@ -218,9 +218,9 @@ func (m *tabReview) StoreReview() tea.Cmd { } } -func FetchNextUnratedReview(emdb *client.EMDB) tea.Cmd { +func FetchNextUnratedReview(reviewRepo *storage.ReviewRepositoryPG) tea.Cmd { return func() tea.Msg { - review, err := emdb.GetNextUnratedReview() + review, err := reviewRepo.FindNextUnrated() if err != nil { return err } diff --git a/cmd/terminal-client/tui/tabset.go b/terminal-client/tui/tabset.go similarity index 100% rename from cmd/terminal-client/tui/tabset.go rename to terminal-client/tui/tabset.go diff --git a/cmd/terminal-client/tui/tabtmdb.go b/terminal-client/tui/tabtmdb.go similarity index 84% rename from cmd/terminal-client/tui/tabtmdb.go rename to terminal-client/tui/tabtmdb.go index 32f63b6..3dcde96 100644 --- a/cmd/terminal-client/tui/tabtmdb.go +++ b/terminal-client/tui/tabtmdb.go @@ -5,13 +5,16 @@ import ( "code.ewintr.nl/emdb/client" "code.ewintr.nl/emdb/cmd/api-service/moviestore" + "code.ewintr.nl/emdb/job" + "code.ewintr.nl/emdb/storage" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type tabTMDB struct { - emdb *client.EMDB + movieRepo *storage.MovieRepositoryPG + jobQueue *job.JobQueue tmdb *client.TMDB initialized bool focused string @@ -20,11 +23,12 @@ type tabTMDB struct { logger *Logger } -func NewTabTMDB(emdb *client.EMDB, tmdb *client.TMDB, logger *Logger) (tea.Model, tea.Cmd) { +func NewTabTMDB(movieRepo *storage.MovieRepositoryPG, jobQueue *job.JobQueue, tmdb *client.TMDB, logger *Logger) (tea.Model, tea.Cmd) { m := tabTMDB{ - emdb: emdb, - tmdb: tmdb, - logger: logger, + movieRepo: movieRepo, + jobQueue: jobQueue, + tmdb: tmdb, + logger: logger, } return m, nil @@ -127,15 +131,14 @@ func (m *tabTMDB) SearchTMDBCmd(query string) tea.Cmd { func (m *tabTMDB) ImportMovieCmd(movie Movie) tea.Cmd { return func() tea.Msg { - newMovie, err := m.emdb.CreateMovie(movie.m) - if err != nil { + if err := m.movieRepo.Store(movie.m); err != nil { return err } - if err := m.emdb.CreateJob(newMovie.ID, string(moviestore.ActionRefreshIMDBReviews)); err != nil { + if err := m.jobQueue.Add(movie.m.ID, string(moviestore.ActionRefreshIMDBReviews)); err != nil { return err } - return NewMovie(Movie{m: newMovie}) + return NewMovie(movie) } } diff --git a/cmd/terminal-client/tui/tui.go b/terminal-client/tui/tui.go similarity index 80% rename from cmd/terminal-client/tui/tui.go rename to terminal-client/tui/tui.go index 8689020..b72a7b6 100644 --- a/cmd/terminal-client/tui/tui.go +++ b/terminal-client/tui/tui.go @@ -2,6 +2,8 @@ package tui import ( "code.ewintr.nl/emdb/client" + "code.ewintr.nl/emdb/job" + "code.ewintr.nl/emdb/storage" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -50,11 +52,11 @@ func SelectPrevTab() tea.Cmd { } } -func New(emdb *client.EMDB, tmdb *client.TMDB, logger *Logger) (*tea.Program, error) { +func New(movieRepo *storage.MovieRepositoryPG, reviewRepo *storage.ReviewRepositoryPG, jobQueue *job.JobQueue, tmdb *client.TMDB, logger *Logger) (*tea.Program, error) { logViewport := viewport.New(0, 0) logViewport.KeyMap = viewport.KeyMap{} - m, _ := NewBaseModel(emdb, tmdb, logger) + m, _ := NewBaseModel(movieRepo, reviewRepo, jobQueue, tmdb, logger) p := tea.NewProgram(m, tea.WithAltScreen()) return p, nil