review job queue
This commit is contained in:
parent
a100751af9
commit
946b05a9a1
3
Makefile
3
Makefile
|
@ -20,6 +20,9 @@ run-md-export:
|
|||
fi \
|
||||
done
|
||||
|
||||
run-worker:
|
||||
go run ./cmd/worker/main.go
|
||||
|
||||
|
||||
build-api:
|
||||
go build -o emdb-api ./cmd/api-service/service.go
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
"ewintr.nl/emdb/model"
|
||||
"ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||
)
|
||||
|
||||
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
|
||||
//for i := 0; i < 5; i++ {
|
||||
// movies = append(movies, model.Movie{
|
||||
|
@ -55,7 +55,7 @@ func (e *EMDB) GetMovies() ([]model.Movie, error) {
|
|||
body, err := io.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var movies []model.Movie
|
||||
var movies []moviestore.Movie
|
||||
if err := json.Unmarshal(body, &movies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -63,73 +63,73 @@ func (e *EMDB) GetMovies() ([]model.Movie, error) {
|
|||
return movies, nil
|
||||
}
|
||||
|
||||
func (e *EMDB) CreateMovie(movie model.Movie) (model.Movie, error) {
|
||||
body, err := json.Marshal(movie)
|
||||
func (e *EMDB) CreateMovie(m moviestore.Movie) (moviestore.Movie, error) {
|
||||
body, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/movie", e.baseURL)
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
req.Header.Add("Authorization", e.apiKey)
|
||||
|
||||
resp, err := e.c.Do(req)
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var newMovie model.Movie
|
||||
var newMovie moviestore.Movie
|
||||
if err := json.Unmarshal(newBody, &newMovie); err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
|
||||
return newMovie, nil
|
||||
}
|
||||
|
||||
func (e *EMDB) UpdateMovie(movie model.Movie) (model.Movie, error) {
|
||||
body, err := json.Marshal(movie)
|
||||
func (e *EMDB) UpdateMovie(m moviestore.Movie) (moviestore.Movie, error) {
|
||||
body, err := json.Marshal(m)
|
||||
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))
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
req.Header.Add("Authorization", e.apiKey)
|
||||
|
||||
resp, err := e.c.Do(req)
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var newMovie model.Movie
|
||||
var newMovie moviestore.Movie
|
||||
if err := json.Unmarshal(newBody, &newMovie); err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
|
||||
return newMovie, nil
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -3,7 +3,7 @@ package client
|
|||
import (
|
||||
"time"
|
||||
|
||||
"ewintr.nl/emdb/model"
|
||||
"ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||
tmdb "github.com/cyruzin/golang-tmdb"
|
||||
)
|
||||
|
||||
|
@ -24,13 +24,13 @@ func NewTMDB(apikey string) (*TMDB, error) {
|
|||
}, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
movies := make([]model.Movie, len(results.Results))
|
||||
movies := make([]moviestore.Movie, len(results.Results))
|
||||
for i, result := range results.Results {
|
||||
movies[i], err = t.GetMovie(result.ID)
|
||||
if err != nil {
|
||||
|
@ -41,12 +41,12 @@ func (t TMDB) Search(query string) ([]model.Movie, error) {
|
|||
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{
|
||||
"append_to_response": "credits",
|
||||
})
|
||||
if err != nil {
|
||||
return model.Movie{}, err
|
||||
return moviestore.Movie{}, err
|
||||
}
|
||||
|
||||
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,
|
||||
EnglishTitle: result.Title,
|
||||
TMDBID: result.ID,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package server
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -7,6 +7,12 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
MovieKey = ContextKey("movie")
|
||||
)
|
||||
|
||||
func Index(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"message":"emdb index"}`)
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
@ -9,46 +10,59 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"ewintr.nl/emdb/model"
|
||||
"ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MovieAPI struct {
|
||||
repo model.MovieRepository
|
||||
apis APIIndex
|
||||
repo *moviestore.MovieRepository
|
||||
jq *moviestore.JobQueue
|
||||
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{
|
||||
apis: apis,
|
||||
repo: repo,
|
||||
jq: jq,
|
||||
logger: logger.With("api", "movie"),
|
||||
}
|
||||
}
|
||||
|
||||
func (api *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
logger := api.logger.With("method", "serveHTTP")
|
||||
func (movieAPI *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
case r.Method == http.MethodGet && subPath != "":
|
||||
api.Read(w, r, subPath)
|
||||
movieAPI.Read(w, r, subPath)
|
||||
case r.Method == http.MethodPut && subPath != "":
|
||||
api.Store(w, r, subPath)
|
||||
movieAPI.Store(w, r, subPath)
|
||||
case r.Method == http.MethodPost && subPath == "":
|
||||
api.Store(w, r, "")
|
||||
movieAPI.Store(w, r, "")
|
||||
case r.Method == http.MethodDelete && subPath != "":
|
||||
api.Delete(w, r, subPath)
|
||||
movieAPI.Delete(w, r, subPath)
|
||||
case r.Method == http.MethodGet && subPath == "":
|
||||
api.List(w, r)
|
||||
movieAPI.List(w, r)
|
||||
default:
|
||||
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) {
|
||||
logger := api.logger.With("method", "read")
|
||||
func (movieAPI *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string) {
|
||||
logger := movieAPI.logger.With("method", "read")
|
||||
|
||||
movie, err := api.repo.FindOne(movieID)
|
||||
m, err := movieAPI.repo.FindOne(movieID)
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
@ -59,7 +73,7 @@ func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string
|
|||
return
|
||||
}
|
||||
|
||||
resJson, err := json.Marshal(movie)
|
||||
resJson, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "could not marshal response", err, logger)
|
||||
return
|
||||
|
@ -68,8 +82,8 @@ func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string
|
|||
fmt.Fprint(w, string(resJson))
|
||||
}
|
||||
|
||||
func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) {
|
||||
logger := api.logger.With("method", "create")
|
||||
func (movieAPI *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) {
|
||||
logger := movieAPI.logger.With("method", "create")
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
|
@ -78,28 +92,33 @@ func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string)
|
|||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var movie *model.Movie
|
||||
if err := json.Unmarshal(body, &movie); err != nil {
|
||||
var m moviestore.Movie
|
||||
if err := json.Unmarshal(body, &m); err != nil {
|
||||
Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case urlID == "" && movie.ID == "":
|
||||
movie.ID = uuid.New().String()
|
||||
case urlID != "" && movie.ID == "":
|
||||
movie.ID = urlID
|
||||
case urlID != "" && movie.ID != "" && urlID != movie.ID:
|
||||
case urlID == "" && m.ID == "":
|
||||
m.ID = uuid.New().String()
|
||||
case urlID != "" && m.ID == "":
|
||||
m.ID = urlID
|
||||
case urlID != "" && m.ID != "" && urlID != m.ID:
|
||||
Error(w, http.StatusBadRequest, "id in path does not match id in body", err, logger)
|
||||
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)
|
||||
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 {
|
||||
Error(w, http.StatusInternalServerError, "could not marshal movie", err, logger)
|
||||
return
|
||||
|
@ -108,10 +127,10 @@ func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string)
|
|||
fmt.Fprint(w, string(resBody))
|
||||
}
|
||||
|
||||
func (api *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) {
|
||||
logger := api.logger.With("method", "delete")
|
||||
func (movieAPI *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) {
|
||||
logger := movieAPI.logger.With("method", "delete")
|
||||
|
||||
err := api.repo.Delete(urlID)
|
||||
err := movieAPI.repo.Delete(urlID)
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
@ -125,10 +144,10 @@ func (api *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string
|
|||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||
logger := api.logger.With("method", "list")
|
||||
func (movieAPI *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||
logger := movieAPI.logger.With("method", "list")
|
||||
|
||||
movies, err := api.repo.FindAll()
|
||||
movies, err := movieAPI.repo.FindAll()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "could not get movies", err, logger)
|
||||
return
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package server
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
package server
|
||||
package moviestore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ewintr.nl/emdb/model"
|
||||
"github.com/google/uuid"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
|
@ -48,6 +46,23 @@ var sqliteMigrations = []sqliteMigration{
|
|||
DROP TABLE movie;
|
||||
ALTER TABLE movie_new RENAME TO movie;
|
||||
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 (
|
||||
|
@ -67,6 +82,8 @@ func NewSQLite(dbPath string) (*SQLite, error) {
|
|||
return &SQLite{}, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(fmt.Sprintf("PRAGMA busy_timeout=%d;", 5*time.Second))
|
||||
|
||||
s := &SQLite{
|
||||
db: db,
|
||||
}
|
||||
|
@ -78,81 +95,16 @@ func NewSQLite(dbPath string) (*SQLite, error) {
|
|||
return s, nil
|
||||
}
|
||||
|
||||
func (s *SQLite) Store(m *model.Movie) error {
|
||||
if m.ID == "" {
|
||||
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) Exec(query string, args ...any) (sql.Result, error) {
|
||||
return s.db.Exec(query, args...)
|
||||
}
|
||||
|
||||
func (s *SQLite) Delete(id string) error {
|
||||
if _, err := s.db.Exec(`DELETE FROM movie WHERE id=?`, id); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func (s *SQLite) QueryRow(query string, args ...any) *sql.Row {
|
||||
return s.db.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
func (s *SQLite) FindOne(id string) (*model.Movie, error) {
|
||||
row := s.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 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) Query(query string, args ...any) (*sql.Rows, error) {
|
||||
return s.db.Query(query, args...)
|
||||
}
|
||||
|
||||
func (s *SQLite) migrate(wanted []sqliteMigration) error {
|
|
@ -1 +0,0 @@
|
|||
package server
|
|
@ -9,7 +9,9 @@ import (
|
|||
"os/signal"
|
||||
"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 (
|
||||
|
@ -23,17 +25,24 @@ func main() {
|
|||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
logger.Info("starting server", "port", *port, "dbPath", *dbPath)
|
||||
|
||||
repo, err := server.NewSQLite(*dbPath)
|
||||
db, err := moviestore.NewSQLite(*dbPath)
|
||||
if err != nil {
|
||||
fmt.Printf("could not create new sqlite repo: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
apis := server.APIIndex{
|
||||
"movie": server.NewMovieAPI(repo, logger),
|
||||
jobQueue := moviestore.NewJobQueue(db, 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")
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
|
|
|
@ -3,12 +3,12 @@ package tui
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"ewintr.nl/emdb/model"
|
||||
"ewintr.nl/emdb/cmd/api-service/moviestore"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
)
|
||||
|
||||
type Movie struct {
|
||||
m model.Movie
|
||||
m moviestore.Movie
|
||||
}
|
||||
|
||||
func (m Movie) FilterValue() string {
|
||||
|
@ -23,7 +23,7 @@ func (m Movie) Description() string {
|
|||
return fmt.Sprintf("%s", m.m.Summary)
|
||||
}
|
||||
|
||||
type Movies []model.Movie
|
||||
type Movies []moviestore.Movie
|
||||
|
||||
func (ms Movies) listItems() []list.Item {
|
||||
items := []list.Item{}
|
||||
|
|
|
@ -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
5
go.mod
|
@ -14,6 +14,8 @@ 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/aymanbagabas/go-osc52/v2 v2.0.1 // 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/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // 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/sys v0.12.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
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
|
|
25
go.sum
25
go.sum
|
@ -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=
|
||||
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/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/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=
|
||||
|
@ -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/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/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.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
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.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
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/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
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-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-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/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=
|
||||
|
@ -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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
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-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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/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-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-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.5.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/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/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
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.3/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/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-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-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-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.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue