package controllers import ( "encoding/json" "fmt" "io" "log" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" "list_app/models" ) // PostersDir is where dynamically fetched poster images are cached on disk. // Unlike the static frontend assets, these are downloaded at runtime and // can't be embedded into the binary, so they live in a real directory next // to it, served separately (see routes.New). const PostersDir = "posters" func PosterDiskPath(webImg string) string { rel := strings.TrimPrefix(webImg, "/poster/") return filepath.Join(PostersDir, rel) } type omdbRes struct { Title string `json:"Title"` Released string `json:"Released"` Response string `json:"Response"` Poster string `json:"Poster"` Type string `json:"Type"` ImdbID string `json:"imdbID"` Year string `json:"Year"` } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(body) } func fromStringToTable(input string) (models.Table, bool) { switch input { case "games": return models.Games, true case "movies": return models.Movies, true case "series": return models.Series, true } return "", false } func downloadImage(media models.Media, table models.Table) { outputPath := "/poster/" + string(table) + "/" + media.Code + ".jpg" response, err := http.Get(media.Poster) if err != nil || !(response.StatusCode >= 200 && response.StatusCode < 300) { log.Println("fetch image error") log.Println(media.Title) return } defer response.Body.Close() imageBytes, err := io.ReadAll(response.Body) if err != nil { log.Println("fetch image error") log.Println(media.Title) return } destPath := PosterDiskPath(outputPath) if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { log.Println(err) return } if err := os.WriteFile(destPath, imageBytes, 0644); err != nil { log.Println(err) return } models.UpdateWebImg(table, media.Code, outputPath) } func createMed(mediaCode string) (models.Media, error) { omdbKey, ok := models.GetValue(models.OmdbKey) if !ok || omdbKey == "" { return models.Media{}, fmt.Errorf("error when creating media") } uri := "http://www.omdbapi.com/?i=" + url.QueryEscape(mediaCode) + "&apikey=" + url.QueryEscape(omdbKey) resp, err := http.Get(uri) if err != nil { return models.Media{}, fmt.Errorf("error when creating media") } defer resp.Body.Close() var mData omdbRes if err := json.NewDecoder(resp.Body).Decode(&mData); err != nil { return models.Media{}, fmt.Errorf("error when creating media") } if mData.Response == "False" { return models.Media{}, fmt.Errorf("wrong code") } media := models.Media{ ID: 0, Code: mData.ImdbID, Title: mData.Title, Released: mData.Released, WebImg: "", Poster: mData.Poster, Year: mData.Year, } tableType := models.Series if strings.EqualFold(mData.Type, "movie") { tableType = models.Movies } found := models.FindOne(tableType, mediaCode) if len(found) != 0 { downloadImage(media, tableType) return models.Media{}, fmt.Errorf("media already exists") } models.Save(tableType, mData.ImdbID, mData.Title, mData.Released, "", mData.Poster, mData.Year) downloadImage(media, tableType) return media, nil } func createGame(gameCodeStr string) (models.Media, error) { twitchClientID, ok1 := models.GetValue(models.TwitchClientID) twitchClientSecret, ok2 := models.GetValue(models.TwitchClientSecret) if !ok1 || !ok2 || twitchClientID == "" || twitchClientSecret == "" { return models.Media{}, fmt.Errorf("error when creating game") } gameFound := models.FindOne(models.Games, gameCodeStr) if len(gameFound) != 0 { return models.Media{}, fmt.Errorf("game already exists") } tokenURI := "https://id.twitch.tv/oauth2/token?client_id=" + url.QueryEscape(twitchClientID) + "&client_secret=" + url.QueryEscape(twitchClientSecret) + "&grant_type=client_credentials" tokenResp, err := http.Post(tokenURI, "application/x-www-form-urlencoded", nil) if err != nil { return models.Media{}, fmt.Errorf("error when creating game: %w", err) } defer tokenResp.Body.Close() var tokenData struct { AccessToken string `json:"access_token"` } if err := json.NewDecoder(tokenResp.Body).Decode(&tokenData); err != nil { return models.Media{}, fmt.Errorf("error when creating game: %w", err) } gameCode, err := strconv.Atoi(gameCodeStr) if err != nil { return models.Media{}, fmt.Errorf("wrong code") } doIgdbRequest := func(path, body string) ([]byte, error) { req, err := http.NewRequest(http.MethodPost, "https://api.igdb.com/v4/"+path, strings.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Client-ID", twitchClientID) req.Header.Set("Authorization", "Bearer "+tokenData.AccessToken) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } gamesBody, err := doIgdbRequest("games", fmt.Sprintf("fields name, first_release_date; where id = %d;", gameCode)) if err != nil { return models.Media{}, fmt.Errorf("error when creating game: %w", err) } var gameData []struct { Name string `json:"name"` FirstReleaseDate int64 `json:"first_release_date"` } if err := json.Unmarshal(gamesBody, &gameData); err != nil { return models.Media{}, fmt.Errorf("error when creating game: %w", err) } if len(gameData) == 0 { return models.Media{}, fmt.Errorf("wrong code") } releaseDate := time.Unix(gameData[0].FirstReleaseDate, 0).UTC() dateStr := releaseDate.Format("2 Jan 2006") coversBody, err := doIgdbRequest("covers", fmt.Sprintf("fields image_id; where game = %d;", gameCode)) if err != nil { return models.Media{}, fmt.Errorf("error when creating game: %w", err) } var coverData []struct { ImageID string `json:"image_id"` } if err := json.Unmarshal(coversBody, &coverData); err != nil { return models.Media{}, fmt.Errorf("error when creating game: %w", err) } imageID := "" if len(coverData) > 0 { imageID = coverData[0].ImageID } game := models.Media{ ID: 0, Code: gameCodeStr, Title: gameData[0].Name, Released: dateStr, WebImg: "", Poster: fmt.Sprintf("https://images.igdb.com/igdb/image/upload/t_cover_big/%s.jpg", imageID), Year: strconv.Itoa(releaseDate.Year()), } models.Save(models.Games, game.Code, game.Title, game.Released, game.WebImg, game.Poster, game.Year) downloadImage(game, models.Games) return game, nil } // Create adds a movie, series, or game by its OMDB (imdb "tt...") or IGDB // (numeric) code, dispatching to the right lookup based on the code's shape. func Create(code string) (models.Media, error) { if strings.HasPrefix(code, "tt") { return createMed(code) } return createGame(code) } func List(w http.ResponseWriter, r *http.Request) { mediaTable, ok := fromStringToTable(r.PathValue("mediaType")) if !ok { writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "Error when getting media."}) return } media := models.Find(mediaTable) writeJSON(w, http.StatusOK, media) } func CheckImages() { checkTableImages(models.Games) checkTableImages(models.Movies) checkTableImages(models.Series) } func checkTableImages(table models.Table) { list := models.Find(table) for _, element := range list { path := PosterDiskPath(element.WebImg) if _, err := os.Stat(path); err == nil { continue } log.Println(element.Title) downloadImage(element, table) time.Sleep(1 * time.Second) } }