diff --git a/config/config.go b/config/config.go index 0b9bc90..f3e1012 100644 --- a/config/config.go +++ b/config/config.go @@ -1,14 +1,15 @@ package config import ( - "errors" "fmt" "os" "time" "projectreshoot/logging" + "projectreshoot/tmdb" "github.com/joho/godotenv" + "github.com/pkg/errors" "github.com/rs/zerolog" ) @@ -30,6 +31,8 @@ type Config struct { LogLevel zerolog.Level // Log level for global logging. Defaults to info LogOutput string // "file", "console", or "both". Defaults to console LogDir string // Path to create log files + TMDBToken string // Read access token for TMDB API + TMDBConfig *tmdb.Config // Config data for interfacing with TMDB } // Load the application configuration and get a pointer to the Config object @@ -77,6 +80,10 @@ func GetConfig(args map[string]string) (*Config, error) { if logOutput != "both" && logOutput != "console" && logOutput != "file" { logOutput = "console" } + tmdbcfg, err := tmdb.GetConfig(os.Getenv("TMDB_API_TOKEN")) + if err != nil { + return nil, errors.Wrap(err, "tmdb.GetConfig") + } config := &Config{ Host: host, @@ -96,11 +103,16 @@ func GetConfig(args map[string]string) (*Config, error) { LogLevel: logLevel, LogOutput: logOutput, LogDir: GetEnvDefault("LOG_DIR", ""), + TMDBToken: os.Getenv("TMDB_API_TOKEN"), + TMDBConfig: tmdbcfg, } if config.SecretKey == "" && args["dbver"] != "true" { return nil, errors.New("Envar not set: SECRET_KEY") } + if config.TMDBToken == "" && args["dbver"] != "true" { + return nil, errors.New("Envar not set: TMDB_API_TOKEN") + } return config, nil } diff --git a/db/connection.go b/db/connection.go index a156590..67cdf22 100644 --- a/db/connection.go +++ b/db/connection.go @@ -39,7 +39,7 @@ func checkDBVersion(db *sql.DB, expectVer int) error { ORDER BY version_id DESC LIMIT 1` rows, err := db.Query(query) if err != nil { - return errors.Wrap(err, "checkDBVersion") + return errors.Wrap(err, "db.Query") } defer rows.Close() if rows.Next() { diff --git a/handler/errorpage.go b/handler/errorpage.go index 19aa760..67fe951 100644 --- a/handler/errorpage.go +++ b/handler/errorpage.go @@ -18,7 +18,7 @@ func ErrorPage( continues to happen contact an administrator.`, 503: "The server is currently down for maintenance and should be back soon. =)", } - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(errorCode) page.Error(errorCode, http.StatusText(errorCode), message[errorCode]). Render(r.Context(), w) } diff --git a/handler/movie.go b/handler/movie.go new file mode 100644 index 0000000..b312d39 --- /dev/null +++ b/handler/movie.go @@ -0,0 +1,44 @@ +package handler + +import ( + "net/http" + "projectreshoot/config" + "projectreshoot/tmdb" + "projectreshoot/view/page" + "strconv" + + "github.com/rs/zerolog" +) + +func Movie( + logger *zerolog.Logger, + config *config.Config, +) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("movie_id") + movie_id, err := strconv.ParseInt(id, 10, 32) + if err != nil { + ErrorPage(http.StatusNotFound, w, r) + logger.Error().Err(err).Str("movie_id", id). + Msg("Error occured getting the movie") + return + } + movie, err := tmdb.GetMovie(int32(movie_id), config.TMDBToken) + if err != nil { + ErrorPage(http.StatusInternalServerError, w, r) + logger.Error().Err(err).Int32("movie_id", int32(movie_id)). + Msg("Error occured getting the movie") + return + } + credits, err := tmdb.GetCredits(int32(movie_id), config.TMDBToken) + if err != nil { + ErrorPage(http.StatusInternalServerError, w, r) + logger.Error().Err(err).Int32("movie_id", int32(movie_id)). + Msg("Error occured getting the movie credits") + return + } + page.Movie(movie, credits, &config.TMDBConfig.Image).Render(r.Context(), w) + }, + ) +} diff --git a/handler/movie_search.go b/handler/movie_search.go new file mode 100644 index 0000000..bb19b11 --- /dev/null +++ b/handler/movie_search.go @@ -0,0 +1,41 @@ +package handler + +import ( + "net/http" + "projectreshoot/config" + "projectreshoot/tmdb" + "projectreshoot/view/component/search" + "projectreshoot/view/page" + + "github.com/rs/zerolog" +) + +func SearchMovies( + logger *zerolog.Logger, + config *config.Config, +) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + query := r.FormValue("search") + if query == "" { + w.WriteHeader(http.StatusOK) + return + } + movies, err := tmdb.SearchMovies(config.TMDBToken, query, false, 1) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + search.MovieResults(movies, &config.TMDBConfig.Image).Render(r.Context(), w) + }, + ) +} + +func MoviesPage() http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + page.Movies().Render(r.Context(), w) + }, + ) +} diff --git a/handler/withtransaction.go b/handler/withtransaction.go deleted file mode 100644 index 37b709e..0000000 --- a/handler/withtransaction.go +++ /dev/null @@ -1,37 +0,0 @@ -package handler - -import ( - "context" - "net/http" - "time" - - "projectreshoot/db" - - "github.com/rs/zerolog" -) - -func removeme( - w http.ResponseWriter, - r *http.Request, - logger *zerolog.Logger, - conn *db.SafeConn, - handler func( - ctx context.Context, - tx *db.SafeTX, - w http.ResponseWriter, - r *http.Request, - ), - onfail func(err error), -) { - ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) - defer cancel() - - // Start the transaction - tx, err := conn.Begin(ctx) - if err != nil { - onfail(err) - return - } - - handler(ctx, tx, w, r) -} diff --git a/server/routes.go b/server/routes.go index 8462e72..e99fd46 100644 --- a/server/routes.go +++ b/server/routes.go @@ -60,4 +60,11 @@ func addRoutes( route("POST /change-username", loggedIn(fresh(handler.ChangeUsername(logger, conn)))) route("POST /change-bio", loggedIn(handler.ChangeBio(logger, conn))) route("POST /change-password", loggedIn(fresh(handler.ChangePassword(logger, conn)))) + + // Movies Search + route("GET /movies", handler.MoviesPage()) + route("POST /search-movies", handler.SearchMovies(logger, config)) + + // Movie page + route("GET /movie/{movie_id}", handler.Movie(logger, config)) } diff --git a/tests/database.go b/tests/database.go index 0010636..db417f9 100644 --- a/tests/database.go +++ b/tests/database.go @@ -71,7 +71,6 @@ func SetupTestDB(version int64) (*sql.DB, error) { return nil, errors.Wrap(err, "provider.UpTo") } - // NOTE: ================================================== // Load the test data dataPath, err := findTestData() if err != nil { diff --git a/tmdb/config.go b/tmdb/config.go new file mode 100644 index 0000000..b58df22 --- /dev/null +++ b/tmdb/config.go @@ -0,0 +1,32 @@ +package tmdb + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +type Config struct { + Image Image `json:"images"` +} + +type Image struct { + BaseURL string `json:"base_url"` + SecureBaseURL string `json:"secure_base_url"` + BackdropSizes []string `json:"backdrop_sizes"` + LogoSizes []string `json:"logo_sizes"` + PosterSizes []string `json:"poster_sizes"` + ProfileSizes []string `json:"profile_sizes"` + StillSizes []string `json:"still_sizes"` +} + +func GetConfig(token string) (*Config, error) { + url := "https://api.themoviedb.org/3/configuration" + data, err := tmdbGet(url, token) + if err != nil { + return nil, errors.Wrap(err, "tmdbGet") + } + config := Config{} + json.Unmarshal(data, &config) + return &config, nil +} diff --git a/tmdb/credits.go b/tmdb/credits.go new file mode 100644 index 0000000..86d9a23 --- /dev/null +++ b/tmdb/credits.go @@ -0,0 +1,54 @@ +package tmdb + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +type Credits struct { + ID int32 `json:"id"` + Cast []Cast `json:"cast"` + Crew []Crew `json:"crew"` +} + +type Cast struct { + Adult bool `json:"adult"` + Gender int `json:"gender"` + ID int32 `json:"id"` + KnownFor string `json:"known_for_department"` + Name string `json:"name"` + OriginalName string `json:"original_name"` + Popularity int `json:"popularity"` + Profile string `json:"profile_path"` + CastID int32 `json:"cast_id"` + Character string `json:"character"` + CreditID string `json:"credit_id"` + Order int `json:"order"` +} + +type Crew struct { + Adult bool `json:"adult"` + Gender int `json:"gender"` + ID int32 `json:"id"` + KnownFor string `json:"known_for_department"` + Name string `json:"name"` + OriginalName string `json:"original_name"` + Popularity int `json:"popularity"` + Profile string `json:"profile_path"` + CreditID string `json:"credit_id"` + Department string `json:"department"` + Job string `json:"job"` +} + +func GetCredits(movieid int32, token string) (*Credits, error) { + url := fmt.Sprintf("https://api.themoviedb.org/3/movie/%v/credits?language=en-US", movieid) + data, err := tmdbGet(url, token) + if err != nil { + return nil, errors.Wrap(err, "tmdbGet") + } + credits := Credits{} + json.Unmarshal(data, &credits) + return &credits, nil +} diff --git a/tmdb/crew_functions.go b/tmdb/crew_functions.go new file mode 100644 index 0000000..03e549c --- /dev/null +++ b/tmdb/crew_functions.go @@ -0,0 +1,41 @@ +package tmdb + +import "sort" + +type BilledCrew struct { + Name string + Roles []string +} + +func (credits *Credits) BilledCrew() []BilledCrew { + crewmap := make(map[string][]string) + billedcrew := []BilledCrew{} + for _, crew := range credits.Crew { + if crew.Job == "Director" || + crew.Job == "Screenplay" || + crew.Job == "Writer" || + crew.Job == "Novel" || + crew.Job == "Story" { + crewmap[crew.Name] = append(crewmap[crew.Name], crew.Job) + } + } + + for name, jobs := range crewmap { + billedcrew = append(billedcrew, BilledCrew{Name: name, Roles: jobs}) + } + for i := range billedcrew { + sort.Strings(billedcrew[i].Roles) + } + sort.Slice(billedcrew, func(i, j int) bool { + return billedcrew[i].Roles[0] < billedcrew[j].Roles[0] + }) + return billedcrew +} + +func (billedcrew *BilledCrew) FRoles() string { + jobs := "" + for _, job := range billedcrew.Roles { + jobs += job + ", " + } + return jobs[:len(jobs)-2] +} diff --git a/tmdb/movie.go b/tmdb/movie.go new file mode 100644 index 0000000..a3b77f7 --- /dev/null +++ b/tmdb/movie.go @@ -0,0 +1,45 @@ +package tmdb + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +type Movie struct { + Adult bool `json:"adult"` + Backdrop string `json:"backdrop_path"` + Collection string `json:"belongs_to_collection"` + Budget int `json:"budget"` + Genres []Genre `json:"genres"` + Homepage string `json:"homepage"` + ID int32 `json:"id"` + IMDbID string `json:"imdb_id"` + OriginalLanguage string `json:"original_language"` + OriginalTitle string `json:"original_title"` + Overview string `json:"overview"` + Popularity float32 `json:"popularity"` + Poster string `json:"poster_path"` + ProductionCompanies []ProductionCompany `json:"production_companies"` + ProductionCountries []ProductionCountry `json:"production_countries"` + ReleaseDate string `json:"release_date"` + Revenue int `json:"revenue"` + Runtime int `json:"runtime"` + SpokenLanguages []SpokenLanguage `json:"spoken_languages"` + Status string `json:"status"` + Tagline string `json:"tagline"` + Title string `json:"title"` + Video bool `json:"video"` +} + +func GetMovie(id int32, token string) (*Movie, error) { + url := fmt.Sprintf("https://api.themoviedb.org/3/movie/%v?language=en-US", id) + data, err := tmdbGet(url, token) + if err != nil { + return nil, errors.Wrap(err, "tmdbGet") + } + movie := Movie{} + json.Unmarshal(data, &movie) + return &movie, nil +} diff --git a/tmdb/movie_functions.go b/tmdb/movie_functions.go new file mode 100644 index 0000000..ebfa679 --- /dev/null +++ b/tmdb/movie_functions.go @@ -0,0 +1,42 @@ +package tmdb + +import ( + "fmt" + "net/url" + "path" +) + +func (movie *Movie) FRuntime() string { + hours := movie.Runtime / 60 + mins := movie.Runtime % 60 + return fmt.Sprintf("%dh %02dm", hours, mins) +} + +func (movie *Movie) GetPoster(image *Image, size string) string { + base, err := url.Parse(image.SecureBaseURL) + if err != nil { + return "" + } + fullPath := path.Join(base.Path, size, movie.Poster) + base.Path = fullPath + return base.String() +} + +func (movie *Movie) ReleaseYear() string { + if movie.ReleaseDate == "" { + return "" + } else { + return "(" + movie.ReleaseDate[:4] + ")" + } +} + +func (movie *Movie) FGenres() string { + genres := "" + for _, genre := range movie.Genres { + genres += genre.Name + ", " + } + if len(genres) > 2 { + return genres[:len(genres)-2] + } + return genres +} diff --git a/tmdb/request.go b/tmdb/request.go new file mode 100644 index 0000000..6c454b4 --- /dev/null +++ b/tmdb/request.go @@ -0,0 +1,28 @@ +package tmdb + +import ( + "fmt" + "io" + "net/http" + + "github.com/pkg/errors" +) + +func tmdbGet(url string, token string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, errors.Wrap(err, "http.NewRequest") + } + req.Header.Add("accept", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "http.DefaultClient.Do") + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, errors.Wrap(err, "io.ReadAll") + } + return body, nil +} diff --git a/tmdb/search.go b/tmdb/search.go new file mode 100644 index 0000000..701ff04 --- /dev/null +++ b/tmdb/search.go @@ -0,0 +1,79 @@ +package tmdb + +import ( + "encoding/json" + "fmt" + "net/url" + "path" + + "github.com/pkg/errors" +) + +type Result struct { + Page int `json:"page"` + TotalPages int `json:"total_pages"` + TotalResults int `json:"total_results"` +} + +type ResultMovies struct { + Result + Results []ResultMovie `json:"results"` +} +type ResultMovie struct { + Adult bool `json:"adult"` + BackdropPath string `json:"backdrop_path"` + GenreIDs []int `json:"genre_ids"` + ID int32 `json:"id"` + OriginalLanguage string `json:"original_language"` + OriginalTitle string `json:"original_title"` + Overview string `json:"overview"` + Popularity int `json:"popularity"` + PosterPath string `json:"poster_path"` + ReleaseDate string `json:"release_date"` + Title string `json:"title"` + Video bool `json:"video"` + VoteAverage int `json:"vote_average"` + VoteCount int `json:"vote_count"` +} + +func (movie *ResultMovie) GetPoster(image *Image, size string) string { + base, err := url.Parse(image.SecureBaseURL) + if err != nil { + return "" + } + fullPath := path.Join(base.Path, size, movie.PosterPath) + base.Path = fullPath + return base.String() +} + +func (movie *ResultMovie) ReleaseYear() string { + if movie.ReleaseDate == "" { + return "" + } else { + return "(" + movie.ReleaseDate[:4] + ")" + } +} + +// TODO: genres list https://developer.themoviedb.org/reference/genre-movie-list +// func (movie *ResultMovie) FGenres() string { +// genres := "" +// for _, genre := range movie.Genres { +// genres += genre.Name + ", " +// } +// return genres[:len(genres)-2] +// } + +func SearchMovies(token string, query string, adult bool, page int) (*ResultMovies, error) { + url := "https://api.themoviedb.org/3/search/movie" + + fmt.Sprintf("?query=%s", url.QueryEscape(query)) + + fmt.Sprintf("&include_adult=%t", adult) + + fmt.Sprintf("&page=%v", page) + + "&language=en-US" + response, err := tmdbGet(url, token) + if err != nil { + return nil, errors.Wrap(err, "tmdbGet") + } + var results ResultMovies + json.Unmarshal(response, &results) + return &results, nil +} diff --git a/tmdb/structs.go b/tmdb/structs.go new file mode 100644 index 0000000..0929dbd --- /dev/null +++ b/tmdb/structs.go @@ -0,0 +1,28 @@ +package tmdb + +import ( +// "encoding/json" +) + +type Genre struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type ProductionCompany struct { + ID int `json:"id"` + Logo string `json:"logo_path"` + Name string `json:"name"` + OriginCountry string `json:"origin_country"` +} + +type ProductionCountry struct { + ISO_3166_1 string `json:"iso_3166_1"` + Name string `json:"name"` +} + +type SpokenLanguage struct { + EnglishName string `json:"english_name"` + ISO_639_1 string `json:"iso_639_1"` + Name string `json:"name"` +} diff --git a/view/component/account/container.templ b/view/component/account/container.templ index f065949..199396f 100644 --- a/view/component/account/container.templ +++ b/view/component/account/container.templ @@ -3,7 +3,7 @@ package account templ AccountContainer(subpage string) {
- by Haelnorr +
+ Released: + { movie.ReleaseDate } +
++ Original Title: + { movie.OriginalTitle } +
+{ movie.Overview }
+