made the jump from remote api to local postgres

This commit is contained in:
Erik Winter 2024-03-09 12:19:55 +01:00
parent 96246469cb
commit 4d207eed8b
25 changed files with 748 additions and 158 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@
emdb
emdb-api
public
*.sql
*.sql.bak

View File

@ -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

View File

@ -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 {

View File

@ -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"
)

View File

@ -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
//}

View File

@ -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{

57
cmd/import/main.go Normal file
View File

@ -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")
}

View File

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

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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

98
storage/moviepg.go Normal file
View File

@ -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
}

165
storage/postgres.go Normal file
View File

@ -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
}

236
storage/reviewpg.go Normal file
View File

@ -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
}

47
terminal-client/main.go Normal file
View File

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

View File

@ -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:

View File

@ -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
}

View File

@ -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
}

View File

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

View File

@ -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