Files
2026-07-30 00:30:27 +02:00

278 lines
7.5 KiB
Go

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)
}
}