diff --git a/Makefile b/Makefile index bf29499..da6b3ad 100644 --- a/Makefile +++ b/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 diff --git a/client/emdb.go b/client/emdb.go index 5c5048a..e1b1e41 100644 --- a/client/emdb.go +++ b/client/emdb.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 diff --git a/client/imdb.go b/client/imdb.go new file mode 100644 index 0000000..afd7884 --- /dev/null +++ b/client/imdb.go @@ -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 +} diff --git a/client/tmdb.go b/client/tmdb.go index 707fbb7..b6fc069 100644 --- a/client/tmdb.go +++ b/client/tmdb.go @@ -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, diff --git a/cmd/api-service/server/handler.go b/cmd/api-service/handler/handler.go similarity index 86% rename from cmd/api-service/server/handler.go rename to cmd/api-service/handler/handler.go index 9393511..9784c0a 100644 --- a/cmd/api-service/server/handler.go +++ b/cmd/api-service/handler/handler.go @@ -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"}`) diff --git a/cmd/api-service/server/movie.go b/cmd/api-service/handler/movie.go similarity index 53% rename from cmd/api-service/server/movie.go rename to cmd/api-service/handler/movie.go index 0686fe7..9e47c7a 100644 --- a/cmd/api-service/server/movie.go +++ b/cmd/api-service/handler/movie.go @@ -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 diff --git a/cmd/api-service/handler/review.go b/cmd/api-service/handler/review.go new file mode 100644 index 0000000..9a40d82 --- /dev/null +++ b/cmd/api-service/handler/review.go @@ -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) + } +} diff --git a/cmd/api-service/server/server.go b/cmd/api-service/handler/server.go similarity index 99% rename from cmd/api-service/server/server.go rename to cmd/api-service/handler/server.go index 9fb0c7b..5499e18 100644 --- a/cmd/api-service/server/server.go +++ b/cmd/api-service/handler/server.go @@ -1,4 +1,4 @@ -package server +package handler import ( "fmt" diff --git a/cmd/api-service/handler/worker.go b/cmd/api-service/handler/worker.go new file mode 100644 index 0000000..3b52ef1 --- /dev/null +++ b/cmd/api-service/handler/worker.go @@ -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)) +} diff --git a/cmd/api-service/moviestore/job.go b/cmd/api-service/moviestore/job.go new file mode 100644 index 0000000..acc7dd2 --- /dev/null +++ b/cmd/api-service/moviestore/job.go @@ -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 +} diff --git a/cmd/api-service/moviestore/movie.go b/cmd/api-service/moviestore/movie.go new file mode 100644 index 0000000..e60f317 --- /dev/null +++ b/cmd/api-service/moviestore/movie.go @@ -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 +} diff --git a/cmd/api-service/moviestore/review.go b/cmd/api-service/moviestore/review.go new file mode 100644 index 0000000..3df8177 --- /dev/null +++ b/cmd/api-service/moviestore/review.go @@ -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 +} diff --git a/cmd/api-service/server/sqlite.go b/cmd/api-service/moviestore/sqlite.go similarity index 62% rename from cmd/api-service/server/sqlite.go rename to cmd/api-service/moviestore/sqlite.go index ad92534..d3df191 100644 --- a/cmd/api-service/server/sqlite.go +++ b/cmd/api-service/moviestore/sqlite.go @@ -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 { diff --git a/cmd/api-service/server/emdb.go b/cmd/api-service/server/emdb.go deleted file mode 100644 index abb4e43..0000000 --- a/cmd/api-service/server/emdb.go +++ /dev/null @@ -1 +0,0 @@ -package server diff --git a/cmd/api-service/service.go b/cmd/api-service/service.go index 9f26a6f..f91c740 100644 --- a/cmd/api-service/service.go +++ b/cmd/api-service/service.go @@ -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) diff --git a/cmd/terminal-client/tui/movie.go b/cmd/terminal-client/tui/movie.go index a9ff926..03ac838 100644 --- a/cmd/terminal-client/tui/movie.go +++ b/cmd/terminal-client/tui/movie.go @@ -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{} diff --git a/cmd/worker/main.go b/cmd/worker/main.go new file mode 100644 index 0000000..5629ea2 --- /dev/null +++ b/cmd/worker/main.go @@ -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) +} diff --git a/go.mod b/go.mod index 1873ba3..2504d7e 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0a8c82b..954b210 100644 --- a/go.sum +++ b/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= diff --git a/model/movie.go b/model/movie.go deleted file mode 100644 index 230a002..0000000 --- a/model/movie.go +++ /dev/null @@ -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 -}