review job queue

This commit is contained in:
Erik Winter 2023-12-29 19:10:31 +01:00
parent a100751af9
commit 946b05a9a1
20 changed files with 632 additions and 170 deletions

View File

@ -20,6 +20,9 @@ run-md-export:
fi \ fi \
done done
run-worker:
go run ./cmd/worker/main.go
build-api: build-api:
go build -o emdb-api ./cmd/api-service/service.go go build -o emdb-api ./cmd/api-service/service.go

View File

@ -7,7 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"ewintr.nl/emdb/model" "ewintr.nl/emdb/cmd/api-service/moviestore"
) )
type EMDB struct { type EMDB struct {
@ -24,7 +24,7 @@ func NewEMDB(baseURL string, apiKey string) *EMDB {
} }
} }
func (e *EMDB) GetMovies() ([]model.Movie, error) { func (e *EMDB) GetMovies() ([]moviestore.Movie, error) {
//var movies []model.Movie //var movies []model.Movie
//for i := 0; i < 5; i++ { //for i := 0; i < 5; i++ {
// movies = append(movies, model.Movie{ // movies = append(movies, model.Movie{
@ -55,7 +55,7 @@ func (e *EMDB) GetMovies() ([]model.Movie, error) {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
defer resp.Body.Close() defer resp.Body.Close()
var movies []model.Movie var movies []moviestore.Movie
if err := json.Unmarshal(body, &movies); err != nil { if err := json.Unmarshal(body, &movies); err != nil {
return nil, err return nil, err
} }
@ -63,73 +63,73 @@ func (e *EMDB) GetMovies() ([]model.Movie, error) {
return movies, nil return movies, nil
} }
func (e *EMDB) CreateMovie(movie model.Movie) (model.Movie, error) { func (e *EMDB) CreateMovie(m moviestore.Movie) (moviestore.Movie, error) {
body, err := json.Marshal(movie) body, err := json.Marshal(m)
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
url := fmt.Sprintf("%s/movie", e.baseURL) url := fmt.Sprintf("%s/movie", e.baseURL)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
req.Header.Add("Authorization", e.apiKey) req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req) resp, err := e.c.Do(req)
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return model.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) return moviestore.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} }
newBody, err := io.ReadAll(resp.Body) newBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
defer resp.Body.Close() defer resp.Body.Close()
var newMovie model.Movie var newMovie moviestore.Movie
if err := json.Unmarshal(newBody, &newMovie); err != nil { if err := json.Unmarshal(newBody, &newMovie); err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
return newMovie, nil return newMovie, nil
} }
func (e *EMDB) UpdateMovie(movie model.Movie) (model.Movie, error) { func (e *EMDB) UpdateMovie(m moviestore.Movie) (moviestore.Movie, error) {
body, err := json.Marshal(movie) body, err := json.Marshal(m)
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
url := fmt.Sprintf("%s/movie/%s", e.baseURL, movie.ID) url := fmt.Sprintf("%s/movie/%s", e.baseURL, m.ID)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body)) req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body))
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
req.Header.Add("Authorization", e.apiKey) req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req) resp, err := e.c.Do(req)
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return model.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) return moviestore.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} }
newBody, err := io.ReadAll(resp.Body) newBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
defer resp.Body.Close() defer resp.Body.Close()
var newMovie model.Movie var newMovie moviestore.Movie
if err := json.Unmarshal(newBody, &newMovie); err != nil { if err := json.Unmarshal(newBody, &newMovie); err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
return newMovie, nil return newMovie, nil

64
client/imdb.go Normal file
View File

@ -0,0 +1,64 @@
package client
import (
"fmt"
"net/http"
"github.com/PuerkitoBio/goquery"
)
type Review struct {
Source string
Review string
}
type IMDB struct {
}
func NewIMDB() *IMDB {
return &IMDB{}
}
func (i *IMDB) GetReviews(imdbID string) (map[string]string, error) {
url := fmt.Sprintf("https://www.imdb.com/title/%s/reviews", imdbID)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return nil, err
}
defer res.Body.Close()
reviews := make(map[string]string)
doc.Find(".lister-item-content").Each(func(i int, reviewNode *goquery.Selection) {
var permaLink string
reviewNode.Find("a").Each(func(i int, s *goquery.Selection) {
if s.Text() == "Permalink" {
link, exists := s.Attr("href")
if exists {
permaLink = link
}
}
})
if permaLink == "" {
return
}
reviews[permaLink] = reviewNode.Text()
})
return reviews, nil
}

View File

@ -3,7 +3,7 @@ package client
import ( import (
"time" "time"
"ewintr.nl/emdb/model" "ewintr.nl/emdb/cmd/api-service/moviestore"
tmdb "github.com/cyruzin/golang-tmdb" tmdb "github.com/cyruzin/golang-tmdb"
) )
@ -24,13 +24,13 @@ func NewTMDB(apikey string) (*TMDB, error) {
}, nil }, nil
} }
func (t TMDB) Search(query string) ([]model.Movie, error) { func (t TMDB) Search(query string) ([]moviestore.Movie, error) {
results, err := t.c.GetSearchMovies(query, nil) results, err := t.c.GetSearchMovies(query, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
movies := make([]model.Movie, len(results.Results)) movies := make([]moviestore.Movie, len(results.Results))
for i, result := range results.Results { for i, result := range results.Results {
movies[i], err = t.GetMovie(result.ID) movies[i], err = t.GetMovie(result.ID)
if err != nil { if err != nil {
@ -41,12 +41,12 @@ func (t TMDB) Search(query string) ([]model.Movie, error) {
return movies, nil return movies, nil
} }
func (t TMDB) GetMovie(id int64) (model.Movie, error) { func (t TMDB) GetMovie(id int64) (moviestore.Movie, error) {
result, err := t.c.GetMovieDetails(int(id), map[string]string{ result, err := t.c.GetMovieDetails(int(id), map[string]string{
"append_to_response": "credits", "append_to_response": "credits",
}) })
if err != nil { if err != nil {
return model.Movie{}, err return moviestore.Movie{}, err
} }
var year int var year int
@ -61,7 +61,7 @@ func (t TMDB) GetMovie(id int64) (model.Movie, error) {
} }
} }
return model.Movie{ return moviestore.Movie{
Title: result.OriginalTitle, Title: result.OriginalTitle,
EnglishTitle: result.Title, EnglishTitle: result.Title,
TMDBID: result.ID, TMDBID: result.ID,

View File

@ -1,4 +1,4 @@
package server package handler
import ( import (
"encoding/json" "encoding/json"
@ -7,6 +7,12 @@ import (
"net/http" "net/http"
) )
type ContextKey string
const (
MovieKey = ContextKey("movie")
)
func Index(w http.ResponseWriter) { func Index(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"message":"emdb index"}`) fmt.Fprint(w, `{"message":"emdb index"}`)

View File

@ -1,6 +1,7 @@
package server package handler
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@ -9,46 +10,59 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"ewintr.nl/emdb/model" "ewintr.nl/emdb/cmd/api-service/moviestore"
"github.com/google/uuid" "github.com/google/uuid"
) )
type MovieAPI struct { type MovieAPI struct {
repo model.MovieRepository apis APIIndex
repo *moviestore.MovieRepository
jq *moviestore.JobQueue
logger *slog.Logger logger *slog.Logger
} }
func NewMovieAPI(repo model.MovieRepository, logger *slog.Logger) *MovieAPI { func NewMovieAPI(apis APIIndex, repo *moviestore.MovieRepository, jq *moviestore.JobQueue, logger *slog.Logger) *MovieAPI {
return &MovieAPI{ return &MovieAPI{
apis: apis,
repo: repo, repo: repo,
jq: jq,
logger: logger.With("api", "movie"), logger: logger.With("api", "movie"),
} }
} }
func (api *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (movieAPI *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := api.logger.With("method", "serveHTTP") logger := movieAPI.logger.With("method", "serveHTTP")
subPath, subTail := ShiftPath(r.URL.Path)
for aPath, api := range movieAPI.apis {
if subPath == aPath {
r.URL.Path = subTail
r = r.Clone(context.WithValue(r.Context(), MovieKey, subPath))
api.ServeHTTP(w, r)
return
}
}
subPath, _ := ShiftPath(r.URL.Path)
switch { switch {
case r.Method == http.MethodGet && subPath != "": case r.Method == http.MethodGet && subPath != "":
api.Read(w, r, subPath) movieAPI.Read(w, r, subPath)
case r.Method == http.MethodPut && subPath != "": case r.Method == http.MethodPut && subPath != "":
api.Store(w, r, subPath) movieAPI.Store(w, r, subPath)
case r.Method == http.MethodPost && subPath == "": case r.Method == http.MethodPost && subPath == "":
api.Store(w, r, "") movieAPI.Store(w, r, "")
case r.Method == http.MethodDelete && subPath != "": case r.Method == http.MethodDelete && subPath != "":
api.Delete(w, r, subPath) movieAPI.Delete(w, r, subPath)
case r.Method == http.MethodGet && subPath == "": case r.Method == http.MethodGet && subPath == "":
api.List(w, r) movieAPI.List(w, r)
default: default:
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, subPath), logger) Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, subPath), logger)
} }
} }
func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string) { func (movieAPI *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string) {
logger := api.logger.With("method", "read") logger := movieAPI.logger.With("method", "read")
movie, err := api.repo.FindOne(movieID) m, err := movieAPI.repo.FindOne(movieID)
switch { switch {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -59,7 +73,7 @@ func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string
return return
} }
resJson, err := json.Marshal(movie) resJson, err := json.Marshal(m)
if err != nil { if err != nil {
Error(w, http.StatusInternalServerError, "could not marshal response", err, logger) Error(w, http.StatusInternalServerError, "could not marshal response", err, logger)
return return
@ -68,8 +82,8 @@ func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string
fmt.Fprint(w, string(resJson)) fmt.Fprint(w, string(resJson))
} }
func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) { func (movieAPI *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) {
logger := api.logger.With("method", "create") logger := movieAPI.logger.With("method", "create")
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
@ -78,28 +92,33 @@ func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string)
} }
defer r.Body.Close() defer r.Body.Close()
var movie *model.Movie var m moviestore.Movie
if err := json.Unmarshal(body, &movie); err != nil { if err := json.Unmarshal(body, &m); err != nil {
Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger) Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger)
return return
} }
switch { switch {
case urlID == "" && movie.ID == "": case urlID == "" && m.ID == "":
movie.ID = uuid.New().String() m.ID = uuid.New().String()
case urlID != "" && movie.ID == "": case urlID != "" && m.ID == "":
movie.ID = urlID m.ID = urlID
case urlID != "" && movie.ID != "" && urlID != movie.ID: case urlID != "" && m.ID != "" && urlID != m.ID:
Error(w, http.StatusBadRequest, "id in path does not match id in body", err, logger) Error(w, http.StatusBadRequest, "id in path does not match id in body", err, logger)
return return
} }
if err := api.repo.Store(movie); err != nil { if err := movieAPI.repo.Store(m); err != nil {
Error(w, http.StatusInternalServerError, "could not store movie", err, logger) Error(w, http.StatusInternalServerError, "could not store movie", err, logger)
return return
} }
resBody, err := json.Marshal(movie) if err := movieAPI.jq.Add(m.ID, moviestore.ActionFetchIMDBReviews); err != nil {
Error(w, http.StatusInternalServerError, "could not add job to queue", err, logger)
return
}
resBody, err := json.Marshal(m)
if err != nil { if err != nil {
Error(w, http.StatusInternalServerError, "could not marshal movie", err, logger) Error(w, http.StatusInternalServerError, "could not marshal movie", err, logger)
return return
@ -108,10 +127,10 @@ func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string)
fmt.Fprint(w, string(resBody)) fmt.Fprint(w, string(resBody))
} }
func (api *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) { func (movieAPI *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) {
logger := api.logger.With("method", "delete") logger := movieAPI.logger.With("method", "delete")
err := api.repo.Delete(urlID) err := movieAPI.repo.Delete(urlID)
switch { switch {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -125,10 +144,10 @@ func (api *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) { func (movieAPI *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
logger := api.logger.With("method", "list") logger := movieAPI.logger.With("method", "list")
movies, err := api.repo.FindAll() movies, err := movieAPI.repo.FindAll()
if err != nil { if err != nil {
Error(w, http.StatusInternalServerError, "could not get movies", err, logger) Error(w, http.StatusInternalServerError, "could not get movies", err, logger)
return return

View File

@ -0,0 +1,41 @@
package handler
import (
"fmt"
"log/slog"
"net/http"
"ewintr.nl/emdb/cmd/api-service/moviestore"
)
type ReviewAPI struct {
repo *moviestore.ReviewRepository
logger *slog.Logger
}
func NewReviewAPI(repo *moviestore.ReviewRepository, logger *slog.Logger) *ReviewAPI {
return &ReviewAPI{
repo: repo,
logger: logger.With("api", "review"),
}
}
func (reviewAPI *ReviewAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := reviewAPI.logger.With("method", "serveHTTP")
subPath, _ := ShiftPath(r.URL.Path)
switch {
//case r.Method == http.MethodGet && subPath != "":
// reviewAPI.Read(w, r, subPath)
//case r.Method == http.MethodPut && subPath != "":
// reviewAPI.Store(w, r, subPath)
//case r.Method == http.MethodPost && subPath == "":
// reviewAPI.Store(w, r, "")
//case r.Method == http.MethodDelete && subPath != "":
// reviewAPI.Delete(w, r, subPath)
//case r.Method == http.MethodGet && subPath == "":
// reviewAPI.List(w, r)
default:
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /review", r.Method, subPath), logger)
}
}

View File

@ -1,4 +1,4 @@
package server package handler
import ( import (
"fmt" "fmt"

View File

@ -0,0 +1,71 @@
package handler
import (
"log/slog"
"ewintr.nl/emdb/client"
movie2 "ewintr.nl/emdb/cmd/api-service/moviestore"
"github.com/google/uuid"
)
type Worker struct {
jq *movie2.JobQueue
movieRepo *movie2.MovieRepository
reviewRepo *movie2.ReviewRepository
imdb *client.IMDB
logger *slog.Logger
}
func NewWorker(jq *movie2.JobQueue, movieRepo *movie2.MovieRepository, reviewRepo *movie2.ReviewRepository, imdb *client.IMDB, logger *slog.Logger) *Worker {
return &Worker{
jq: jq,
movieRepo: movieRepo,
reviewRepo: reviewRepo,
imdb: imdb,
logger: logger.With("service", "worker"),
}
}
func (w *Worker) Run() {
w.logger.Info("starting worker")
for job := range w.jq.Next() {
w.logger.Info("got a new job", "jobID", job.ID, "movieID", job.MovieID, "action", job.Action)
switch job.Action {
case movie2.ActionFetchIMDBReviews:
w.fetchReviews(job)
default:
w.logger.Warn("unknown job action", "action", job.Action)
}
}
}
func (w *Worker) fetchReviews(job movie2.Job) {
logger := w.logger.With("method", "fetchReviews", "jobID", job.ID, "movieID", job.MovieID)
m, err := w.movieRepo.FindOne(job.MovieID)
if err != nil {
logger.Error("could not get movie", "error", err)
return
}
reviews, err := w.imdb.GetReviews(m.IMDBID)
if err != nil {
logger.Error("could not get reviews", "error", err)
return
}
for url, review := range reviews {
if err := w.reviewRepo.Store(movie2.Review{
ID: uuid.New().String(),
MovieID: m.ID,
Source: movie2.ReviewSourceIMDB,
URL: url,
Review: review,
}); err != nil {
logger.Error("could not store review", "error", err)
return
}
}
logger.Info("fetched reviews", "count", len(reviews))
}

View File

@ -0,0 +1,105 @@
package moviestore
import (
"database/sql"
"errors"
"log/slog"
"time"
)
type JobStatus string
const (
JobStatusToDo JobStatus = "todo"
JobStatusDoing JobStatus = "doing"
JobStatusDone JobStatus = "done"
)
type Action string
const (
interval = 10 * time.Second
ActionFetchIMDBReviews Action = "fetch-imdb-reviews"
)
type Job struct {
ID int
MovieID string
Action Action
Status JobStatus
}
type JobQueue struct {
db *SQLite
out chan Job
logger *slog.Logger
}
func NewJobQueue(db *SQLite, logger *slog.Logger) *JobQueue {
return &JobQueue{
db: db,
out: make(chan Job),
logger: logger.With("service", "jobqueue"),
}
}
func (jq *JobQueue) Add(movieID string, action Action) error {
_, err := jq.db.Exec(`INSERT INTO job_queue (movie_id, action, status)
VALUES (?, ?, 'todo')`, movieID, action)
return err
}
func (jq *JobQueue) Next() chan Job {
return jq.out
}
func (jq *JobQueue) Run() {
logger := jq.logger.With("method", "run")
logger.Info("starting job queue")
for {
row := jq.db.QueryRow(`
SELECT id, movie_id, action
FROM job_queue
WHERE status='todo'
ORDER BY id DESC
LIMIT 1`)
var job Job
err := row.Scan(&job.ID, &job.MovieID, &job.Action)
switch {
case errors.Is(err, sql.ErrNoRows):
logger.Info("nothing to do")
time.Sleep(interval)
continue
case err != nil:
logger.Error("could not fetch next job", "error", row.Err())
time.Sleep(interval)
continue
}
logger.Info("found a job", "id", job.ID)
if _, err := jq.db.Exec(`
UPDATE job_queue
SET status='doing'
WHERE id=?`, job.ID); err != nil {
logger.Error("could not set job to doing", "error")
time.Sleep(interval)
continue
}
jq.out <- job
}
}
func (jq *JobQueue) MarkDone(id string) {
logger := jq.logger.With("method", "markdone")
if _, err := jq.db.Exec(`
UPDATE job_queue SET
status='done'
WHERE id=?`, id); err != nil {
logger.Error("could not mark job done", "error", err)
}
return
}

View File

@ -0,0 +1,99 @@
package moviestore
import (
"fmt"
"strings"
"github.com/google/uuid"
)
type Movie struct {
ID string `json:"id"`
TMDBID int64 `json:"tmdbID"`
IMDBID string `json:"imdbID"`
Title string `json:"title"`
EnglishTitle string `json:"englishTitle"`
Year int `json:"year"`
Directors []string `json:"directors"`
WatchedOn string `json:"watchedOn"`
Rating int `json:"rating"`
Summary string `json:"summary"`
Comment string `json:"comment"`
}
type MovieRepository struct {
db *SQLite
}
func NewMovieRepository(db *SQLite) *MovieRepository {
return &MovieRepository{
db: db,
}
}
func (mr *MovieRepository) Store(m Movie) error {
if m.ID == "" {
m.ID = uuid.New().String()
}
directors := strings.Join(m.Directors, ",")
if _, err := mr.db.Exec(`REPLACE INTO movie (id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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", ErrSqliteFailure, err)
}
return nil
}
func (mr *MovieRepository) Delete(id string) error {
if _, err := mr.db.Exec(`DELETE FROM movie WHERE id=?`, id); err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
return nil
}
func (mr *MovieRepository) FindOne(id string) (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=?`, id)
if row.Err() != nil {
return Movie{}, row.Err()
}
m := 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 Movie{}, fmt.Errorf("%w: %w", ErrSqliteFailure, err)
}
m.Directors = strings.Split(directors, ",")
return m, nil
}
func (mr *MovieRepository) FindAll() ([]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", ErrSqliteFailure, err)
}
movies := make([]Movie, 0)
defer rows.Close()
for rows.Next() {
m := 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", ErrSqliteFailure, err)
}
m.Directors = strings.Split(directors, ",")
movies = append(movies, m)
}
return movies, nil
}

View File

@ -0,0 +1,67 @@
package moviestore
const (
ReviewSourceIMDB = "imdb"
)
type ReviewSource string
type Review struct {
ID string
MovieID string
Source ReviewSource
URL string
Review string
}
type ReviewRepository struct {
db *SQLite
}
func NewReviewRepository(db *SQLite) *ReviewRepository {
return &ReviewRepository{
db: db,
}
}
func (rr *ReviewRepository) Store(r Review) error {
if _, err := rr.db.Exec(`REPLACE INTO review (id, movie_id, source, url, review) VALUES (?, ?, ?, ?, ?)`,
r.ID, r.MovieID, r.Source, r.URL, r.Review); err != nil {
return err
}
return nil
}
func (rr *ReviewRepository) FindOne(id string) (Review, error) {
row := rr.db.QueryRow(`SELECT id, movie_id, source, url, review FROM review WHERE id=?`, id)
if row.Err() != nil {
return Review{}, row.Err()
}
review := Review{}
if err := row.Scan(&review.ID, &review.MovieID, &review.Source, &review.URL, &review.Review); err != nil {
return Review{}, err
}
return review, nil
}
func (rr *ReviewRepository) FindByMovieID(movieID string) ([]Review, error) {
rows, err := rr.db.Query(`SELECT id, movie_id, source, url, review FROM review WHERE movie_id=?`, movieID)
if err != nil {
return nil, err
}
reviews := make([]Review, 0)
for rows.Next() {
r := Review{}
if err := rows.Scan(&r.ID, &r.MovieID, &r.Source, &r.URL, &r.Review); err != nil {
return nil, err
}
reviews = append(reviews, r)
}
rows.Close()
return reviews, nil
}

View File

@ -1,13 +1,11 @@
package server package moviestore
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"strings" "time"
"ewintr.nl/emdb/model"
"github.com/google/uuid"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@ -48,6 +46,23 @@ var sqliteMigrations = []sqliteMigration{
DROP TABLE movie; DROP TABLE movie;
ALTER TABLE movie_new RENAME TO movie; ALTER TABLE movie_new RENAME TO movie;
COMMIT`, COMMIT`,
`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 ""
)`,
`CREATE TABLE job_queue (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"movie_id" TEXT NOT NULL,
"action" TEXT NOT NULL DEFAULT "",
"status" TEXT NOT NULL DEFAULT ""
)`,
`PRAGMA journal_mode=WAL`,
`INSERT INTO job_queue (movie_id, action, status)
SELECT id, 'fetch-imdb-reviews', 'todo'
FROM movie`,
} }
var ( var (
@ -67,6 +82,8 @@ func NewSQLite(dbPath string) (*SQLite, error) {
return &SQLite{}, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err) return &SQLite{}, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
} }
_, err = db.Exec(fmt.Sprintf("PRAGMA busy_timeout=%d;", 5*time.Second))
s := &SQLite{ s := &SQLite{
db: db, db: db,
} }
@ -78,81 +95,16 @@ func NewSQLite(dbPath string) (*SQLite, error) {
return s, nil return s, nil
} }
func (s *SQLite) Store(m *model.Movie) error { func (s *SQLite) Exec(query string, args ...any) (sql.Result, error) {
if m.ID == "" { return s.db.Exec(query, args...)
m.ID = uuid.New().String()
}
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
defer tx.Rollback()
directors := strings.Join(m.Directors, ",")
if _, err := s.db.Exec(`REPLACE INTO movie (id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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", ErrSqliteFailure, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
return nil
} }
func (s *SQLite) Delete(id string) error { func (s *SQLite) QueryRow(query string, args ...any) *sql.Row {
if _, err := s.db.Exec(`DELETE FROM movie WHERE id=?`, id); err != nil { return s.db.QueryRow(query, args...)
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
return nil
} }
func (s *SQLite) FindOne(id string) (*model.Movie, error) { func (s *SQLite) Query(query string, args ...any) (*sql.Rows, error) {
row := s.db.QueryRow(` return s.db.Query(query, args...)
SELECT id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment
FROM movie
WHERE id=?`, id)
if row.Err() != nil {
return nil, row.Err()
}
m := &model.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 nil, fmt.Errorf("%w: %w", ErrSqliteFailure, err)
}
m.Directors = strings.Split(directors, ",")
return m, nil
}
func (s *SQLite) FindAll() ([]*model.Movie, error) {
rows, err := s.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", ErrSqliteFailure, err)
}
movies := make([]*model.Movie, 0)
defer rows.Close()
for rows.Next() {
m := &model.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", ErrSqliteFailure, err)
}
m.Directors = strings.Split(directors, ",")
movies = append(movies, m)
}
return movies, nil
} }
func (s *SQLite) migrate(wanted []sqliteMigration) error { func (s *SQLite) migrate(wanted []sqliteMigration) error {

View File

@ -1 +0,0 @@
package server

View File

@ -9,7 +9,9 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"ewintr.nl/emdb/cmd/api-service/server" "ewintr.nl/emdb/client"
"ewintr.nl/emdb/cmd/api-service/handler"
"ewintr.nl/emdb/cmd/api-service/moviestore"
) )
var ( var (
@ -23,17 +25,24 @@ func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("starting server", "port", *port, "dbPath", *dbPath) logger.Info("starting server", "port", *port, "dbPath", *dbPath)
repo, err := server.NewSQLite(*dbPath) db, err := moviestore.NewSQLite(*dbPath)
if err != nil { if err != nil {
fmt.Printf("could not create new sqlite repo: %s", err.Error()) fmt.Printf("could not create new sqlite repo: %s", err.Error())
os.Exit(1) os.Exit(1)
} }
apis := server.APIIndex{ jobQueue := moviestore.NewJobQueue(db, logger)
"movie": server.NewMovieAPI(repo, logger), go jobQueue.Run()
worker := handler.NewWorker(jobQueue, moviestore.NewMovieRepository(db), moviestore.NewReviewRepository(db), client.NewIMDB(), logger)
go worker.Run()
apis := handler.APIIndex{
"movie": handler.NewMovieAPI(handler.APIIndex{
"review": handler.NewReviewAPI(moviestore.NewReviewRepository(db), logger),
}, moviestore.NewMovieRepository(db), jobQueue, logger),
} }
go http.ListenAndServe(fmt.Sprintf(":%d", *port), server.NewServer(*apiKey, apis, logger)) go http.ListenAndServe(fmt.Sprintf(":%d", *port), handler.NewServer(*apiKey, apis, logger))
logger.Info("server started") logger.Info("server started")
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)

View File

@ -3,12 +3,12 @@ package tui
import ( import (
"fmt" "fmt"
"ewintr.nl/emdb/model" "ewintr.nl/emdb/cmd/api-service/moviestore"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
) )
type Movie struct { type Movie struct {
m model.Movie m moviestore.Movie
} }
func (m Movie) FilterValue() string { func (m Movie) FilterValue() string {
@ -23,7 +23,7 @@ func (m Movie) Description() string {
return fmt.Sprintf("%s", m.m.Summary) return fmt.Sprintf("%s", m.m.Summary)
} }
type Movies []model.Movie type Movies []moviestore.Movie
func (ms Movies) listItems() []list.Item { func (ms Movies) listItems() []list.Item {
items := []list.Item{} items := []list.Item{}

21
cmd/worker/main.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"ewintr.nl/emdb/client"
)
func main() {
fmt.Println("worker")
imdb := client.NewIMDB()
reviews, err := imdb.GetReviews("tt5540188")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("reviews: %+v", reviews)
}

5
go.mod
View File

@ -14,6 +14,8 @@ require (
) )
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
@ -33,10 +35,11 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect

25
go.sum
View File

@ -4,6 +4,8 @@ ewintr.nl/go-kit v0.2.1 h1:8WMeAOPwiPoVjwEtYxrua35aqNbHs0DMayTgeHlC5Tk=
ewintr.nl/go-kit v0.2.1/go.mod h1:mIlMyAvKBuuQSQuX5f1+1gYZ02vz6xRFGN9wiO0HfzI= ewintr.nl/go-kit v0.2.1/go.mod h1:mIlMyAvKBuuQSQuX5f1+1gYZ02vz6xRFGN9wiO0HfzI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
@ -12,6 +14,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -287,6 +291,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@ -305,6 +310,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -331,6 +337,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -339,6 +350,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -356,17 +368,29 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -381,6 +405,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@ -1,22 +0,0 @@
package model
type Movie struct {
ID string `json:"id"`
TMDBID int64 `json:"tmdbID"`
IMDBID string `json:"imdbID"`
Title string `json:"title"`
EnglishTitle string `json:"englishTitle"`
Year int `json:"year"`
Directors []string `json:"directors"`
WatchedOn string `json:"watchedOn"`
Rating int `json:"rating"`
Summary string `json:"summary"`
Comment string `json:"comment"`
}
type MovieRepository interface {
Store(movie *Movie) error
FindOne(id string) (*Movie, error)
FindAll() ([]*Movie, error)
Delete(id string) error
}