made the jump from remote api to local postgres
This commit is contained in:
parent
96246469cb
commit
4d207eed8b
|
@ -3,4 +3,6 @@
|
||||||
*.db-wal
|
*.db-wal
|
||||||
emdb
|
emdb
|
||||||
emdb-api
|
emdb-api
|
||||||
public
|
public
|
||||||
|
*.sql
|
||||||
|
*.sql.bak
|
2
Makefile
2
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
|
EMDB_BASE_URL=http://localhost:8085/ EMDB_API_KEY=hoi go run ./cmd/terminal-client/main.go
|
||||||
|
|
||||||
run-tui:
|
run-tui:
|
||||||
go run ./cmd/terminal-client/main.go
|
go run ./terminal-client/main.go
|
||||||
|
|
||||||
run-md-export:
|
run-md-export:
|
||||||
go run ./cmd/markdown-export/main.go
|
go run ./cmd/markdown-export/main.go
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/job"
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||||
|
"code.ewintr.nl/emdb/job"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobAPI struct {
|
type JobAPI struct {
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/job"
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||||
|
"code.ewintr.nl/emdb/job"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -135,9 +135,9 @@ func NewSQLite(dbPath string) (*SQLite, error) {
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.migrate(sqliteMigrations); err != nil {
|
//if err := s.migrate(sqliteMigrations); err != nil {
|
||||||
return &SQLite{}, err
|
// return &SQLite{}, err
|
||||||
}
|
//}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
@ -154,71 +154,71 @@ func (s *SQLite) Query(query string, args ...any) (*sql.Rows, error) {
|
||||||
return s.db.Query(query, args...)
|
return s.db.Query(query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLite) migrate(wanted []sqliteMigration) error {
|
//func (s *SQLite) migrate(wanted []sqliteMigration) error {
|
||||||
// admin table
|
// // admin table
|
||||||
if _, err := s.db.Exec(`
|
// if _, err := s.db.Exec(`
|
||||||
CREATE TABLE IF NOT EXISTS migration
|
//CREATE TABLE IF NOT EXISTS migration
|
||||||
("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT)
|
//("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT)
|
||||||
`); err != nil {
|
//`); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// find existing
|
// // find existing
|
||||||
rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`)
|
// rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
existing := []sqliteMigration{}
|
// existing := []sqliteMigration{}
|
||||||
for rows.Next() {
|
// for rows.Next() {
|
||||||
var query string
|
// var query string
|
||||||
if err := rows.Scan(&query); err != nil {
|
// if err := rows.Scan(&query); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
// }
|
||||||
existing = append(existing, sqliteMigration(query))
|
// existing = append(existing, sqliteMigration(query))
|
||||||
}
|
// }
|
||||||
rows.Close()
|
// rows.Close()
|
||||||
|
//
|
||||||
// compare
|
// // compare
|
||||||
missing, err := compareMigrations(wanted, existing)
|
// missing, err := compareMigrations(wanted, existing)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// execute missing
|
// // execute missing
|
||||||
for _, query := range missing {
|
// for _, query := range missing {
|
||||||
if _, err := s.db.Exec(string(query)); err != nil {
|
// if _, err := s.db.Exec(string(query)); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// register
|
// // register
|
||||||
if _, err := s.db.Exec(`
|
// if _, err := s.db.Exec(`
|
||||||
INSERT INTO migration
|
//INSERT INTO migration
|
||||||
(query) VALUES (?)
|
//(query) VALUES (?)
|
||||||
`, query); err != nil {
|
//`, query); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return nil
|
// return nil
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
func compareMigrations(wanted, existing []sqliteMigration) ([]sqliteMigration, error) {
|
//func compareMigrations(wanted, existing []sqliteMigration) ([]sqliteMigration, error) {
|
||||||
needed := []sqliteMigration{}
|
// needed := []sqliteMigration{}
|
||||||
if len(wanted) < len(existing) {
|
// if len(wanted) < len(existing) {
|
||||||
return []sqliteMigration{}, ErrNotEnoughSQLMigrations
|
// return []sqliteMigration{}, ErrNotEnoughSQLMigrations
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
for i, want := range wanted {
|
// for i, want := range wanted {
|
||||||
switch {
|
// switch {
|
||||||
case i >= len(existing):
|
// case i >= len(existing):
|
||||||
needed = append(needed, want)
|
// needed = append(needed, want)
|
||||||
case want == existing[i]:
|
// case want == existing[i]:
|
||||||
// do nothing
|
// // do nothing
|
||||||
case want != existing[i]:
|
// case want != existing[i]:
|
||||||
return []sqliteMigration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
// return []sqliteMigration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return needed, nil
|
// return needed, nil
|
||||||
}
|
//}
|
||||||
|
|
|
@ -11,8 +11,8 @@ import (
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/client"
|
"code.ewintr.nl/emdb/client"
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/handler"
|
"code.ewintr.nl/emdb/cmd/api-service/handler"
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/job"
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||||
|
job2 "code.ewintr.nl/emdb/job"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -32,8 +32,8 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
jobQueue := job.NewJobQueue(db, logger)
|
jobQueue := job2.NewJobQueue(db, logger)
|
||||||
worker := job.NewWorker(jobQueue, moviestore.NewMovieRepository(db), moviestore.NewReviewRepository(db), client.NewIMDB(), logger)
|
worker := job2.NewWorker(jobQueue, moviestore.NewMovieRepository(db), moviestore.NewReviewRepository(db), client.NewIMDB(), logger)
|
||||||
go worker.Run()
|
go worker.Run()
|
||||||
|
|
||||||
apis := handler.APIIndex{
|
apis := handler.APIIndex{
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
1
go.mod
|
@ -10,6 +10,7 @@ require (
|
||||||
github.com/charmbracelet/lipgloss v0.10.0
|
github.com/charmbracelet/lipgloss v0.10.0
|
||||||
github.com/cyruzin/golang-tmdb v1.6.0
|
github.com/cyruzin/golang-tmdb v1.6.0
|
||||||
github.com/google/uuid 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/muesli/termenv v0.15.2
|
||||||
github.com/tmc/langchaingo v0.1.5
|
github.com/tmc/langchaingo v0.1.5
|
||||||
modernc.org/sqlite v1.29.3
|
modernc.org/sqlite v1.29.3
|
||||||
|
|
2
go.sum
2
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/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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
|
|
@ -9,14 +9,15 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||||
|
"code.ewintr.nl/emdb/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobQueue struct {
|
type JobQueue struct {
|
||||||
db *moviestore.SQLite
|
db *storage.Postgres
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJobQueue(db *moviestore.SQLite, logger *slog.Logger) *JobQueue {
|
func NewJobQueue(db *storage.Postgres, logger *slog.Logger) *JobQueue {
|
||||||
jq := &JobQueue{
|
jq := &JobQueue{
|
||||||
db: db,
|
db: db,
|
||||||
logger: logger.With("service", "jobqueue"),
|
logger: logger.With("service", "jobqueue"),
|
||||||
|
@ -38,7 +39,7 @@ func (jq *JobQueue) Run() {
|
||||||
UPDATE job_queue
|
UPDATE job_queue
|
||||||
SET status = 'todo'
|
SET status = 'todo'
|
||||||
WHERE status = 'doing'
|
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)
|
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")
|
return errors.New("invalid action")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := jq.db.Exec(`INSERT INTO job_queue (action_id, action, status)
|
_, err := jq.db.Exec(`
|
||||||
VALUES (?, ?, 'todo')`, movieID, action)
|
INSERT INTO job_queue (action_id, action, status)
|
||||||
|
VALUES ($1, $2, 'todo');`, movieID, action)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -68,9 +70,9 @@ func (jq *JobQueue) Next(t moviestore.JobType) (moviestore.Job, error) {
|
||||||
SELECT id, action_id, action
|
SELECT id, action_id, action
|
||||||
FROM job_queue
|
FROM job_queue
|
||||||
WHERE status='todo'
|
WHERE status='todo'
|
||||||
AND action IN %s
|
AND action = ANY($1)
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1`, actionsStr)
|
LIMIT 1;`, actionsStr)
|
||||||
row := jq.db.QueryRow(query)
|
row := jq.db.QueryRow(query)
|
||||||
var job moviestore.Job
|
var job moviestore.Job
|
||||||
err := row.Scan(&job.ID, &job.ActionID, &job.Action)
|
err := row.Scan(&job.ID, &job.ActionID, &job.Action)
|
||||||
|
@ -85,7 +87,7 @@ LIMIT 1`, actionsStr)
|
||||||
if _, err := jq.db.Exec(`
|
if _, err := jq.db.Exec(`
|
||||||
UPDATE job_queue
|
UPDATE job_queue
|
||||||
SET status='doing'
|
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")
|
logger.Error("could not set job to doing", "error")
|
||||||
return moviestore.Job{}, err
|
return moviestore.Job{}, err
|
||||||
}
|
}
|
||||||
|
@ -97,7 +99,7 @@ func (jq *JobQueue) MarkDone(id int) {
|
||||||
logger := jq.logger.With("method", "markdone")
|
logger := jq.logger.With("method", "markdone")
|
||||||
if _, err := jq.db.Exec(`
|
if _, err := jq.db.Exec(`
|
||||||
DELETE FROM job_queue
|
DELETE FROM job_queue
|
||||||
WHERE id=?`, id); err != nil {
|
WHERE id=$1;`, id); err != nil {
|
||||||
logger.Error("could not mark job done", "error", err)
|
logger.Error("could not mark job done", "error", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -108,7 +110,7 @@ func (jq *JobQueue) MarkFailed(id int) {
|
||||||
if _, err := jq.db.Exec(`
|
if _, err := jq.db.Exec(`
|
||||||
UPDATE job_queue
|
UPDATE job_queue
|
||||||
SET status='failed'
|
SET status='failed'
|
||||||
WHERE id=?`, id); err != nil {
|
WHERE id=$1;`, id); err != nil {
|
||||||
logger.Error("could not mark job failed", "error", err)
|
logger.Error("could not mark job failed", "error", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -118,7 +120,7 @@ func (jq *JobQueue) List() ([]moviestore.Job, error) {
|
||||||
rows, err := jq.db.Query(`
|
rows, err := jq.db.Query(`
|
||||||
SELECT id, action_id, action, status, created_at, updated_at
|
SELECT id, action_id, action, status, created_at, updated_at
|
||||||
FROM job_queue
|
FROM job_queue
|
||||||
ORDER BY id DESC`)
|
ORDER BY id DESC;`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -138,15 +140,14 @@ ORDER BY id DESC`)
|
||||||
func (jq *JobQueue) Delete(id string) error {
|
func (jq *JobQueue) Delete(id string) error {
|
||||||
if _, err := jq.db.Exec(`
|
if _, err := jq.db.Exec(`
|
||||||
DELETE FROM job_queue
|
DELETE FROM job_queue
|
||||||
WHERE id=?`, id); err != nil {
|
WHERE id=$1;`, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jq *JobQueue) DeleteAll() error {
|
func (jq *JobQueue) DeleteAll() error {
|
||||||
if _, err := jq.db.Exec(`
|
if _, err := jq.db.Exec(`DELETE FROM job_queue;`); err != nil {
|
||||||
DELETE FROM job_queue`); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,12 +5,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/client"
|
"code.ewintr.nl/emdb/client"
|
||||||
|
"code.ewintr.nl/emdb/job"
|
||||||
|
"code.ewintr.nl/emdb/storage"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type baseModel struct {
|
type baseModel struct {
|
||||||
emdb *client.EMDB
|
movieRepo *storage.MovieRepositoryPG
|
||||||
|
reviewRepo *storage.ReviewRepositoryPG
|
||||||
|
jobQueue *job.JobQueue
|
||||||
tmdb *client.TMDB
|
tmdb *client.TMDB
|
||||||
tabs *TabSet
|
tabs *TabSet
|
||||||
initialized bool
|
initialized bool
|
||||||
|
@ -20,12 +24,14 @@ type baseModel struct {
|
||||||
contentSize tea.WindowSizeMsg
|
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 := viewport.New(0, 0)
|
||||||
logViewport.KeyMap = viewport.KeyMap{}
|
logViewport.KeyMap = viewport.KeyMap{}
|
||||||
|
|
||||||
m := baseModel{
|
m := baseModel{
|
||||||
emdb: emdb,
|
movieRepo: movieRepo,
|
||||||
|
reviewRepo: reviewRepo,
|
||||||
|
jobQueue: jobQueue,
|
||||||
tmdb: tmdb,
|
tmdb: tmdb,
|
||||||
tabs: NewTabSet(),
|
tabs: NewTabSet(),
|
||||||
logViewport: logViewport,
|
logViewport: logViewport,
|
||||||
|
@ -53,11 +59,11 @@ func (m baseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.windowSize = msg
|
m.windowSize = msg
|
||||||
if !m.initialized {
|
if !m.initialized {
|
||||||
var emdbTab, tmdbTab tea.Model
|
var emdbTab, tmdbTab tea.Model
|
||||||
emdbTab, cmd = NewTabEMDB(m.emdb, m.logger)
|
emdbTab, cmd = NewTabEMDB(m.movieRepo, m.logger)
|
||||||
cmds = append(cmds, cmd)
|
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)
|
cmds = append(cmds, cmd)
|
||||||
reviewTab, cmd := NewTabReview(m.emdb, m.logger)
|
reviewTab, cmd := NewTabReview(m.reviewRepo, m.logger)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
m.tabs.AddTab("emdb", "Watched movies", emdbTab)
|
m.tabs.AddTab("emdb", "Watched movies", emdbTab)
|
||||||
m.tabs.AddTab("review", "Review", reviewTab)
|
m.tabs.AddTab("review", "Review", reviewTab)
|
||||||
|
@ -74,7 +80,7 @@ func (m baseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case NewMovie:
|
case NewMovie:
|
||||||
m.Log(fmt.Sprintf("imported movie %s", msg.m.Title))
|
m.Log(fmt.Sprintf("imported movie %s", msg.m.Title))
|
||||||
m.tabs.Select("emdb")
|
m.tabs.Select("emdb")
|
||||||
cmds = append(cmds, FetchMovieList(m.emdb))
|
cmds = append(cmds, FetchMovieList(m.movieRepo))
|
||||||
case error:
|
case error:
|
||||||
m.Log(fmt.Sprintf("ERROR: %s", msg.Error()))
|
m.Log(fmt.Sprintf("ERROR: %s", msg.Error()))
|
||||||
default:
|
default:
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/client"
|
"code.ewintr.nl/emdb/storage"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
@ -24,7 +24,7 @@ type StoredMovie struct{}
|
||||||
|
|
||||||
type tabEMDB struct {
|
type tabEMDB struct {
|
||||||
initialized bool
|
initialized bool
|
||||||
emdb *client.EMDB
|
movieRepo *storage.MovieRepositoryPG
|
||||||
mode string
|
mode string
|
||||||
focused string
|
focused string
|
||||||
colWidth int
|
colWidth int
|
||||||
|
@ -38,7 +38,7 @@ type tabEMDB struct {
|
||||||
logger *Logger
|
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()
|
del := list.NewDefaultDelegate()
|
||||||
list := list.New([]list.Item{}, del, 0, 0)
|
list := list.New([]list.Item{}, del, 0, 0)
|
||||||
list.Title = "Movies"
|
list.Title = "Movies"
|
||||||
|
@ -65,7 +65,7 @@ func NewTabEMDB(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
m := tabEMDB{
|
m := tabEMDB{
|
||||||
focused: "form",
|
focused: "form",
|
||||||
emdb: emdb,
|
movieRepo: movieRepo,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
mode: "view",
|
mode: "view",
|
||||||
list: list,
|
list: list,
|
||||||
|
@ -76,7 +76,7 @@ func NewTabEMDB(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Log("search emdb...")
|
logger.Log("search emdb...")
|
||||||
return m, FetchMovieList(emdb)
|
return m, FetchMovieList(movieRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m tabEMDB) Init() tea.Cmd {
|
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)
|
cmds = append(cmds, cmd)
|
||||||
case StoredMovie:
|
case StoredMovie:
|
||||||
m.logger.Log("stored movie, fetching movie list")
|
m.logger.Log("stored movie, fetching movie list")
|
||||||
cmds = append(cmds, FetchMovieList(m.emdb))
|
cmds = append(cmds, FetchMovieList(m.movieRepo))
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case "edit":
|
case "edit":
|
||||||
|
@ -265,7 +265,7 @@ func (m *tabEMDB) StoreMovie() tea.Cmd {
|
||||||
return fmt.Errorf("rating cannot be converted to an int: %w", err)
|
return fmt.Errorf("rating cannot be converted to an int: %w", err)
|
||||||
}
|
}
|
||||||
updatedMovie.m.Comment = m.inputComment.Value()
|
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 err
|
||||||
}
|
}
|
||||||
return StoredMovie{}
|
return StoredMovie{}
|
||||||
|
@ -276,9 +276,9 @@ func (m *tabEMDB) Log(s string) {
|
||||||
m.logger.Log(s)
|
m.logger.Log(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchMovieList(emdb *client.EMDB) tea.Cmd {
|
func FetchMovieList(movieRepo *storage.MovieRepositoryPG) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ems, err := emdb.GetMovies()
|
ems, err := movieRepo.FindAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/client"
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||||
|
"code.ewintr.nl/emdb/storage"
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
@ -16,7 +16,7 @@ import (
|
||||||
|
|
||||||
type tabReview struct {
|
type tabReview struct {
|
||||||
initialized bool
|
initialized bool
|
||||||
emdb *client.EMDB
|
reviewRepo *storage.ReviewRepositoryPG
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
mode string
|
mode string
|
||||||
|
@ -28,7 +28,7 @@ type tabReview struct {
|
||||||
logger *Logger
|
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 := viewport.New(0, 0)
|
||||||
//reviewViewport.KeyMap = viewport.KeyMap{}
|
//reviewViewport.KeyMap = viewport.KeyMap{}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func NewTabReview(emdb *client.EMDB, logger *Logger) (tea.Model, tea.Cmd) {
|
||||||
inputMentions.CharLimit = 500
|
inputMentions.CharLimit = 500
|
||||||
|
|
||||||
return &tabReview{
|
return &tabReview{
|
||||||
emdb: emdb,
|
reviewRepo: reviewRepo,
|
||||||
mode: "view",
|
mode: "view",
|
||||||
reviewViewport: reviewViewport,
|
reviewViewport: reviewViewport,
|
||||||
inputQuality: inputQuality,
|
inputQuality: inputQuality,
|
||||||
|
@ -97,7 +97,7 @@ func (m *tabReview) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.formFocus = 0
|
m.formFocus = 0
|
||||||
m.logger.Log("fetching next unrated review")
|
m.logger.Log("fetching next unrated review")
|
||||||
cmds = append(cmds, m.inputQuality.Focus())
|
cmds = append(cmds, m.inputQuality.Focus())
|
||||||
cmds = append(cmds, FetchNextUnratedReview(m.emdb))
|
cmds = append(cmds, FetchNextUnratedReview(m.reviewRepo))
|
||||||
default:
|
default:
|
||||||
m.logger.Log(fmt.Sprintf("key: %s", msg.String()))
|
m.logger.Log(fmt.Sprintf("key: %s", msg.String()))
|
||||||
m.reviewViewport, cmd = m.reviewViewport.Update(msg)
|
m.reviewViewport, cmd = m.reviewViewport.Update(msg)
|
||||||
|
@ -115,7 +115,7 @@ func (m *tabReview) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case ReviewStored:
|
case ReviewStored:
|
||||||
m.logger.Log(fmt.Sprintf("stored review %s", msg))
|
m.logger.Log(fmt.Sprintf("stored review %s", msg))
|
||||||
cmds = append(cmds, m.inputQuality.Focus())
|
cmds = append(cmds, m.inputQuality.Focus())
|
||||||
cmds = append(cmds, FetchNextUnratedReview(m.emdb))
|
cmds = append(cmds, FetchNextUnratedReview(m.reviewRepo))
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
|
@ -210,7 +210,7 @@ func (m *tabReview) StoreReview() tea.Cmd {
|
||||||
m.selectedReview.Quality = quality
|
m.selectedReview.Quality = quality
|
||||||
//m.selectedReview.Mentions = strings.Split(mentions, ",")
|
//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
|
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 {
|
return func() tea.Msg {
|
||||||
review, err := emdb.GetNextUnratedReview()
|
review, err := reviewRepo.FindNextUnrated()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -5,13 +5,16 @@ import (
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/client"
|
"code.ewintr.nl/emdb/client"
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
"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/list"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tabTMDB struct {
|
type tabTMDB struct {
|
||||||
emdb *client.EMDB
|
movieRepo *storage.MovieRepositoryPG
|
||||||
|
jobQueue *job.JobQueue
|
||||||
tmdb *client.TMDB
|
tmdb *client.TMDB
|
||||||
initialized bool
|
initialized bool
|
||||||
focused string
|
focused string
|
||||||
|
@ -20,11 +23,12 @@ type tabTMDB struct {
|
||||||
logger *Logger
|
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{
|
m := tabTMDB{
|
||||||
emdb: emdb,
|
movieRepo: movieRepo,
|
||||||
tmdb: tmdb,
|
jobQueue: jobQueue,
|
||||||
logger: logger,
|
tmdb: tmdb,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -127,15 +131,14 @@ func (m *tabTMDB) SearchTMDBCmd(query string) tea.Cmd {
|
||||||
|
|
||||||
func (m *tabTMDB) ImportMovieCmd(movie Movie) tea.Cmd {
|
func (m *tabTMDB) ImportMovieCmd(movie Movie) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
newMovie, err := m.emdb.CreateMovie(movie.m)
|
if err := m.movieRepo.Store(movie.m); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewMovie(Movie{m: newMovie})
|
return NewMovie(movie)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.ewintr.nl/emdb/client"
|
"code.ewintr.nl/emdb/client"
|
||||||
|
"code.ewintr.nl/emdb/job"
|
||||||
|
"code.ewintr.nl/emdb/storage"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"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 := viewport.New(0, 0)
|
||||||
logViewport.KeyMap = viewport.KeyMap{}
|
logViewport.KeyMap = viewport.KeyMap{}
|
||||||
|
|
||||||
m, _ := NewBaseModel(emdb, tmdb, logger)
|
m, _ := NewBaseModel(movieRepo, reviewRepo, jobQueue, tmdb, logger)
|
||||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
Loading…
Reference in New Issue