Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 875aa523b0 | |||
| 6849e65334 |
@@ -1,15 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"projectreshoot/logging"
|
"projectreshoot/logging"
|
||||||
"projectreshoot/tmdb"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ type Config struct {
|
|||||||
LogLevel zerolog.Level // Log level for global logging. Defaults to info
|
LogLevel zerolog.Level // Log level for global logging. Defaults to info
|
||||||
LogOutput string // "file", "console", or "both". Defaults to console
|
LogOutput string // "file", "console", or "both". Defaults to console
|
||||||
LogDir string // Path to create log files
|
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
|
// Load the application configuration and get a pointer to the Config object
|
||||||
@@ -80,10 +77,6 @@ func GetConfig(args map[string]string) (*Config, error) {
|
|||||||
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
|
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
|
||||||
logOutput = "console"
|
logOutput = "console"
|
||||||
}
|
}
|
||||||
tmdbcfg, err := tmdb.GetConfig(os.Getenv("TMDB_API_TOKEN"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "tmdb.GetConfig")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
Host: host,
|
Host: host,
|
||||||
@@ -103,16 +96,11 @@ func GetConfig(args map[string]string) (*Config, error) {
|
|||||||
LogLevel: logLevel,
|
LogLevel: logLevel,
|
||||||
LogOutput: logOutput,
|
LogOutput: logOutput,
|
||||||
LogDir: GetEnvDefault("LOG_DIR", ""),
|
LogDir: GetEnvDefault("LOG_DIR", ""),
|
||||||
TMDBToken: os.Getenv("TMDB_API_TOKEN"),
|
|
||||||
TMDBConfig: tmdbcfg,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SecretKey == "" && args["dbver"] != "true" {
|
if config.SecretKey == "" && args["dbver"] != "true" {
|
||||||
return nil, errors.New("Envar not set: SECRET_KEY")
|
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
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func checkDBVersion(db *sql.DB, expectVer int) error {
|
|||||||
ORDER BY version_id DESC LIMIT 1`
|
ORDER BY version_id DESC LIMIT 1`
|
||||||
rows, err := db.Query(query)
|
rows, err := db.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "db.Query")
|
return errors.Wrap(err, "checkDBVersion")
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
if rows.Next() {
|
if rows.Next() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func ErrorPage(
|
|||||||
continues to happen contact an administrator.`,
|
continues to happen contact an administrator.`,
|
||||||
503: "The server is currently down for maintenance and should be back soon. =)",
|
503: "The server is currently down for maintenance and should be back soon. =)",
|
||||||
}
|
}
|
||||||
w.WriteHeader(errorCode)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
|
page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
|
||||||
Render(r.Context(), w)
|
Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
37
handler/withtransaction.go
Normal file
37
handler/withtransaction.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ func Test_main(t *testing.T) {
|
|||||||
args := map[string]string{"test": "true"}
|
args := map[string]string{"test": "true"}
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
os.Setenv("SECRET_KEY", ".")
|
os.Setenv("SECRET_KEY", ".")
|
||||||
os.Setenv("TMDB_API_TOKEN", ".")
|
|
||||||
os.Setenv("HOST", "127.0.0.1")
|
os.Setenv("HOST", "127.0.0.1")
|
||||||
os.Setenv("PORT", "3232")
|
os.Setenv("PORT", "3232")
|
||||||
runSrvErr := make(chan error)
|
runSrvErr := make(chan error)
|
||||||
|
|||||||
@@ -60,11 +60,4 @@ func addRoutes(
|
|||||||
route("POST /change-username", loggedIn(fresh(handler.ChangeUsername(logger, conn))))
|
route("POST /change-username", loggedIn(fresh(handler.ChangeUsername(logger, conn))))
|
||||||
route("POST /change-bio", loggedIn(handler.ChangeBio(logger, conn)))
|
route("POST /change-bio", loggedIn(handler.ChangeBio(logger, conn)))
|
||||||
route("POST /change-password", loggedIn(fresh(handler.ChangePassword(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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
func TestConfig() (*config.Config, error) {
|
func TestConfig() (*config.Config, error) {
|
||||||
os.Setenv("SECRET_KEY", ".")
|
os.Setenv("SECRET_KEY", ".")
|
||||||
os.Setenv("TMDB_API_TOKEN", ".")
|
|
||||||
cfg, err := config.GetConfig(map[string]string{})
|
cfg, err := config.GetConfig(map[string]string{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "config.GetConfig")
|
return nil, errors.Wrap(err, "config.GetConfig")
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func SetupTestDB(version int64) (*sql.DB, error) {
|
|||||||
return nil, errors.Wrap(err, "provider.UpTo")
|
return nil, errors.Wrap(err, "provider.UpTo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: ==================================================
|
||||||
// Load the test data
|
// Load the test data
|
||||||
dataPath, err := findTestData()
|
dataPath, err := findTestData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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]
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package account
|
|||||||
templ AccountContainer(subpage string) {
|
templ AccountContainer(subpage string) {
|
||||||
<div
|
<div
|
||||||
id="account-container"
|
id="account-container"
|
||||||
class="flex max-w-200 min-h-100 mx-5 md:mx-auto bg-mantle mt-5 rounded-xl"
|
class="flex max-w-200 min-h-100 mx-auto bg-mantle mt-10 rounded-xl"
|
||||||
x-data="{big:window.innerWidth >=768, open:false}"
|
x-data="{big:window.innerWidth >=768, open:false}"
|
||||||
@resize.window="big = window.innerWidth >= 768"
|
@resize.window="big = window.innerWidth >= 768"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -75,13 +75,8 @@ templ Footer() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="lg:flex lg:items-end lg:justify-between">
|
<div class="lg:flex lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="mt-4 text-center text-sm text-overlay0">
|
<p class="mt-4 text-center text-sm text-subtext0">
|
||||||
by Haelnorr |
|
by Haelnorr
|
||||||
<a href="#">Film data</a> from
|
|
||||||
<a
|
|
||||||
href="https://www.themoviedb.org/"
|
|
||||||
class="underline hover:text-subtext0 transition"
|
|
||||||
>TMDB</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ templ RegisterForm(registerError string) {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
idnutanix="username"
|
||||||
name="username"
|
name="username"
|
||||||
class="py-3 px-4 block w-full rounded-lg text-sm
|
class="py-3 px-4 block w-full rounded-lg text-sm
|
||||||
focus:border-blue focus:ring-blue bg-base
|
focus:border-blue focus:ring-blue bg-base
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package search
|
|
||||||
|
|
||||||
import "projectreshoot/tmdb"
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
templ MovieResults(movies *tmdb.ResultMovies, image *tmdb.Image) {
|
|
||||||
for _, movie := range movies.Results {
|
|
||||||
<div
|
|
||||||
class="bg-surface0 p-4 rounded-lg shadow-lg flex
|
|
||||||
items-start space-x-4"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={ movie.GetPoster(image, "w92") }
|
|
||||||
alt="Movie Poster"
|
|
||||||
class="rounded-lg object-cover"
|
|
||||||
width="96"
|
|
||||||
height="144"
|
|
||||||
onerror="this.onerror=null; setFallbackColor(this);"
|
|
||||||
/>
|
|
||||||
<script>
|
|
||||||
function setFallbackColor(img) {
|
|
||||||
const baseColor = getComputedStyle(document.documentElement).
|
|
||||||
getPropertyValue('--base').trim();
|
|
||||||
img.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='144'%3E%3Crect width='100%' height='100%' fill='${baseColor}'/%3E%3C/svg%3E`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href={ templ.SafeURL(fmt.Sprintf("/movie/%v", movie.ID)) }
|
|
||||||
class="text-xl font-semibold transition hover:text-green"
|
|
||||||
>{ movie.Title } { movie.ReleaseYear() }</a>
|
|
||||||
<p class="text-subtext0">
|
|
||||||
Released:
|
|
||||||
<span class="font-medium">{ movie.ReleaseDate }</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-subtext0">
|
|
||||||
Original Title:
|
|
||||||
<span class="font-medium">{ movie.OriginalTitle }</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-subtext0">{ movie.Overview }</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import "projectreshoot/view/component/popup"
|
|||||||
|
|
||||||
// Global page layout. Includes HTML document settings, header tags
|
// Global page layout. Includes HTML document settings, header tags
|
||||||
// navbar and footer
|
// navbar and footer
|
||||||
templ Global(title string) {
|
templ Global() {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
@@ -33,7 +33,7 @@ templ Global(title string) {
|
|||||||
</script>
|
</script>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>{ title }</title>
|
<title>Project Reshoot</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"/>
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"/>
|
||||||
<link href="/static/css/output.css" rel="stylesheet"/>
|
<link href="/static/css/output.css" rel="stylesheet"/>
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
||||||
@@ -41,7 +41,7 @@ templ Global(title string) {
|
|||||||
<script src="https://unpkg.com/alpinejs" defer></script>
|
<script src="https://unpkg.com/alpinejs" defer></script>
|
||||||
<script>
|
<script>
|
||||||
// uncomment this line to enable logging of htmx events
|
// uncomment this line to enable logging of htmx events
|
||||||
// htmx.logAll();
|
htmx.logAll();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
const bodyData = {
|
const bodyData = {
|
||||||
@@ -97,7 +97,7 @@ templ Global(title string) {
|
|||||||
class="flex flex-col h-screen justify-between"
|
class="flex flex-col h-screen justify-between"
|
||||||
>
|
>
|
||||||
@nav.Navbar()
|
@nav.Navbar()
|
||||||
<div id="page-content" class="mb-auto md:px-5 md:pt-5">
|
<div id="page-content" class="mb-auto px-5">
|
||||||
{ children... }
|
{ children... }
|
||||||
</div>
|
</div>
|
||||||
@footer.Footer()
|
@footer.Footer()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "projectreshoot/view/layout"
|
|||||||
|
|
||||||
// Returns the about page content
|
// Returns the about page content
|
||||||
templ About() {
|
templ About() {
|
||||||
@layout.Global("About") {
|
@layout.Global() {
|
||||||
<div class="text-center max-w-150 m-auto">
|
<div class="text-center max-w-150 m-auto">
|
||||||
<div class="text-4xl mt-8">About</div>
|
<div class="text-4xl mt-8">About</div>
|
||||||
<div class="text-xl font-bold mt-4">What is Project Reshoot?</div>
|
<div class="text-xl font-bold mt-4">What is Project Reshoot?</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "projectreshoot/view/layout"
|
|||||||
import "projectreshoot/view/component/account"
|
import "projectreshoot/view/component/account"
|
||||||
|
|
||||||
templ Account(subpage string) {
|
templ Account(subpage string) {
|
||||||
@layout.Global("Account - " + subpage) {
|
@layout.Global() {
|
||||||
@account.AccountContainer(subpage)
|
@account.AccountContainer(subpage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "strconv"
|
|||||||
// a string, and err should be the corresponding response title.
|
// a string, and err should be the corresponding response title.
|
||||||
// Message is a custom error message displayed below the code and error.
|
// Message is a custom error message displayed below the code and error.
|
||||||
templ Error(code int, err string, message string) {
|
templ Error(code int, err string, message string) {
|
||||||
@layout.Global(err) {
|
@layout.Global() {
|
||||||
<div
|
<div
|
||||||
class="grid mt-24 left-0 right-0 top-0 bottom-0
|
class="grid mt-24 left-0 right-0 top-0 bottom-0
|
||||||
place-content-center bg-base px-4"
|
place-content-center bg-base px-4"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "projectreshoot/view/layout"
|
|||||||
|
|
||||||
// Page content for the index page
|
// Page content for the index page
|
||||||
templ Index() {
|
templ Index() {
|
||||||
@layout.Global("Project Reshoot") {
|
@layout.Global() {
|
||||||
<div class="text-center mt-24">
|
<div class="text-center mt-24">
|
||||||
<div class="text-4xl lg:text-6xl">Project Reshoot</div>
|
<div class="text-4xl lg:text-6xl">Project Reshoot</div>
|
||||||
<div>A better way to discover and rate films</div>
|
<div>A better way to discover and rate films</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "projectreshoot/view/component/form"
|
|||||||
|
|
||||||
// Returns the login page
|
// Returns the login page
|
||||||
templ Login() {
|
templ Login() {
|
||||||
@layout.Global("Login") {
|
@layout.Global() {
|
||||||
<div class="max-w-100 mx-auto px-2">
|
<div class="max-w-100 mx-auto px-2">
|
||||||
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
|
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
|
||||||
<div class="p-4 sm:p-7">
|
<div class="p-4 sm:p-7">
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package page
|
|
||||||
|
|
||||||
import "projectreshoot/tmdb"
|
|
||||||
import "projectreshoot/view/layout"
|
|
||||||
|
|
||||||
templ Movie(movie *tmdb.Movie, credits *tmdb.Credits, image *tmdb.Image) {
|
|
||||||
@layout.Global(movie.Title) {
|
|
||||||
<div class="md:bg-surface0 md:p-2 md:rounded-lg transition-all">
|
|
||||||
<div
|
|
||||||
id="billedcrew"
|
|
||||||
class="hidden"
|
|
||||||
>
|
|
||||||
for _, billedcrew := range credits.BilledCrew() {
|
|
||||||
<span class="flex flex-col text-left w-[130px] md:w-[180px]">
|
|
||||||
<span class="font-bold">{ billedcrew.Name }</span>
|
|
||||||
<span class="text-subtext1">{ billedcrew.FRoles() }</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="w-[154px] md:w-[300px] flex-col">
|
|
||||||
<img
|
|
||||||
class="object-cover aspect-[2/3] w-[154px] md:w-[300px]
|
|
||||||
transition-all md:rounded-md shadow-black shadow-2xl"
|
|
||||||
src={ movie.GetPoster(image, "w300") }
|
|
||||||
alt="Poster"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
id="billedcrew-sm"
|
|
||||||
class="text-sm md:text-lg text-subtext1 flex gap-6
|
|
||||||
mt-5 flex-wrap justify-around flex-col px-5 md:hidden"
|
|
||||||
></div>
|
|
||||||
<script>
|
|
||||||
function moveBilledCrew() {
|
|
||||||
const billedCrewMd = document.getElementById('billedcrew-md');
|
|
||||||
const billedCrewSm = document.getElementById('billedcrew-sm');
|
|
||||||
const billedCrew = document.getElementById('billedcrew');
|
|
||||||
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
billedCrewSm.innerHTML = billedCrew.innerHTML;
|
|
||||||
billedCrewMd.innerHTML = "";
|
|
||||||
} else {
|
|
||||||
billedCrewMd.innerHTML = billedCrew.innerHTML;
|
|
||||||
billedCrewSm.innerHTML = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', moveBilledCrew);
|
|
||||||
|
|
||||||
const resizeObs = new ResizeObserver(() => {
|
|
||||||
moveBilledCrew();
|
|
||||||
});
|
|
||||||
resizeObs.observe(document.body);
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col flex-1 text-center px-4">
|
|
||||||
<span class="text-xl md:text-3xl font-semibold">
|
|
||||||
{ movie.Title }
|
|
||||||
</span>
|
|
||||||
<span class="text-sm md:text-lg text-subtext1">
|
|
||||||
{ movie.FGenres() }
|
|
||||||
• { movie.FRuntime() }
|
|
||||||
• { movie.ReleaseYear() }
|
|
||||||
</span>
|
|
||||||
<div class="flex justify-center gap-2 mt-2">
|
|
||||||
<div
|
|
||||||
class="w-20 h-20 md:w-30 md:h-30 bg-overlay2
|
|
||||||
transition-all rounded-sm"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="w-20 h-20 md:w-30 md:h-30 bg-overlay2
|
|
||||||
transition-all rounded-sm"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col mt-4">
|
|
||||||
<span class="text-sm md:text-lg text-overlay2 italic">
|
|
||||||
{ movie.Tagline }
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
id="billedcrew-md"
|
|
||||||
class="hidden text-sm md:text-lg text-subtext1 md:flex gap-6
|
|
||||||
mt-5 flex-wrap justify-around"
|
|
||||||
></div>
|
|
||||||
<span class="text-lg mt-5 font-semibold">Overview</span>
|
|
||||||
<span class="text-sm md:text-lg text-subtext1">
|
|
||||||
{ movie.Overview }
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package page
|
|
||||||
|
|
||||||
import "projectreshoot/view/layout"
|
|
||||||
|
|
||||||
templ Movies() {
|
|
||||||
@layout.Global("Search movies") {
|
|
||||||
<div class="max-w-4xl mx-auto md:mt-0 mt-2 px-2 md:px-0">
|
|
||||||
<form hx-post="/search-movies" hx-target="#search-movies-results">
|
|
||||||
<div
|
|
||||||
class="max-w-100 flex items-center space-x-2 mb-2"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="search"
|
|
||||||
name="search"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search movies..."
|
|
||||||
class="flex-grow p-2 border rounded-lg
|
|
||||||
bg-mantle border-surface2 shadow-sm
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="py-2 px-4 bg-green text-mantle rounded-lg transition
|
|
||||||
hover:cursor-pointer hover:bg-green/75"
|
|
||||||
>Search</button>
|
|
||||||
</div>
|
|
||||||
<div id="search-movies-results" class="space-y-4"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import "projectreshoot/contexts"
|
|||||||
|
|
||||||
templ Profile() {
|
templ Profile() {
|
||||||
{{ user := contexts.GetUser(ctx) }}
|
{{ user := contexts.GetUser(ctx) }}
|
||||||
@layout.Global("Profile - " + user.Username) {
|
@layout.Global() {
|
||||||
<div class="">
|
<div class="">
|
||||||
Hello, { user.Username }
|
Hello, { user.Username }
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "projectreshoot/view/component/form"
|
|||||||
|
|
||||||
// Returns the login page
|
// Returns the login page
|
||||||
templ Register() {
|
templ Register() {
|
||||||
@layout.Global("Register") {
|
@layout.Global() {
|
||||||
<div class="max-w-100 mx-auto px-2">
|
<div class="max-w-100 mx-auto px-2">
|
||||||
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
|
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
|
||||||
<div class="p-4 sm:p-7">
|
<div class="p-4 sm:p-7">
|
||||||
|
|||||||
Reference in New Issue
Block a user