Skip to content

Commit 9cbae5b

Browse files
committed
Admin film improvements
Featuring very sloppy database queries. Don't look :(
1 parent 72534e3 commit 9cbae5b

8 files changed

Lines changed: 234 additions & 41 deletions

File tree

cmd/serve/admin/handlers/films.go

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,73 @@ func generateFilmPath(title string, year int) string {
3535
return "/films/" + cleaned + "/"
3636
}
3737

38+
func updateOrCreateFilmPoster(filmID int, filmTitle, posterPath string) error {
39+
if posterPath == "" {
40+
return fmt.Errorf("poster path is empty")
41+
}
42+
43+
mediaRelations, err := db.GetMediaRelationsForEntity("film", filmID)
44+
if err != nil {
45+
return fmt.Errorf("failed to get media relations: %w", err)
46+
}
47+
48+
for _, rel := range mediaRelations {
49+
if rel.Role != nil && *rel.Role == "poster" {
50+
if err := db.DeleteMediaRelation("film", filmID, rel.Path); err != nil {
51+
return fmt.Errorf("failed to delete existing media relation: %w", err)
52+
}
53+
if err := db.DeleteMedia(rel.MediaID); err != nil {
54+
return fmt.Errorf("failed to delete existing media: %w", err)
55+
}
56+
break
57+
}
58+
}
59+
60+
posterData, err := tmdb.DownloadPoster(*tmdbAPIKey, posterPath, 500)
61+
if err != nil {
62+
return fmt.Errorf("failed to download poster: %w", err)
63+
}
64+
65+
ext := ".jpg"
66+
if posterData.ContentType == "image/png" {
67+
ext = ".png"
68+
}
69+
filename := fmt.Sprintf("%d%s", filmID, ext)
70+
mediaRelationsPath := fmt.Sprintf("/films/%d/poster%s", filmID, ext)
71+
72+
mediaID, err := db.CreateMedia(posterData.ContentType, filename, posterData.Data, &posterData.Width, &posterData.Height, nil)
73+
if err != nil {
74+
return fmt.Errorf("failed to create media: %w", err)
75+
}
76+
77+
description := fmt.Sprintf("Poster of %s", filmTitle)
78+
caption := filmTitle
79+
role := "poster"
80+
if err := db.CreateMediaRelation("film", filmID, mediaID, mediaRelationsPath, &caption, &description, &role); err != nil {
81+
return fmt.Errorf("failed to create media relation: %w", err)
82+
}
83+
84+
return nil
85+
}
86+
3887
var (
3988
tmdbAPIKey = flag.String("tmdb-api-key", "", "TMDB API key")
4089
)
4190

4291
func ListFilmsHandler() func(http.ResponseWriter, *http.Request) {
4392
return func(w http.ResponseWriter, r *http.Request) {
44-
films, err := db.GetAllFilmsWithReviews()
93+
films, err := db.GetAllFilmsWithReviewsAndPosters()
4594
if err != nil {
95+
slog.Error("Failed to retrieve films", "error", err)
4696
http.Error(w, "Failed to retrieve films", http.StatusInternalServerError)
4797
return
4898
}
4999

50100
filmSummaries := make([]templates.FilmSummary, len(films))
51101
for i, film := range films {
52102
year := ""
53-
if film.Year != nil {
54-
year = strconv.Itoa(*film.Year)
103+
if film.Film.Year != nil {
104+
year = strconv.Itoa(*film.Film.Year)
55105
}
56106

57107
rating := ""
@@ -60,11 +110,14 @@ func ListFilmsHandler() func(http.ResponseWriter, *http.Request) {
60110
}
61111

62112
filmSummaries[i] = templates.FilmSummary{
63-
ID: film.ID,
64-
Title: film.Title,
65-
Year: year,
66-
Rating: rating,
67-
Published: film.Published,
113+
ID: film.Film.ID,
114+
Title: film.Film.Title,
115+
Year: year,
116+
Rating: rating,
117+
Published: film.Film.Published,
118+
PosterMediaID: film.PosterMediaID,
119+
ReviewCount: film.ReviewCount,
120+
LastWatched: film.LastWatched,
68121
}
69122
}
70123

@@ -173,31 +226,8 @@ func CreateFilmHandler() func(http.ResponseWriter, *http.Request) {
173226
return
174227
}
175228

176-
if posterPath != "" {
177-
posterData, err := tmdb.DownloadPoster(*tmdbAPIKey, posterPath, 500)
178-
if err != nil {
179-
slog.Error("Failed to download poster", "error", err)
180-
} else {
181-
ext := ".jpg"
182-
if posterData.ContentType == "image/png" {
183-
ext = ".png"
184-
}
185-
filename := fmt.Sprintf("%d%s", filmID, ext)
186-
mediaRelationsPath := fmt.Sprintf("/films/%d/poster%s", filmID, ext)
187-
188-
mediaID, err := db.CreateMedia(posterData.ContentType, filename, posterData.Data, &posterData.Width, &posterData.Height, nil)
189-
if err != nil {
190-
slog.Error("Failed to create media", "error", err)
191-
} else {
192-
description := fmt.Sprintf("Poster of %s", movie.Title)
193-
caption := movie.Title
194-
role := "poster"
195-
err := db.CreateMediaRelation("film", filmID, mediaID, mediaRelationsPath, &caption, &description, &role)
196-
if err != nil {
197-
slog.Error("Failed to create media relation", "error", err)
198-
}
199-
}
200-
}
229+
if err := updateOrCreateFilmPoster(filmID, movie.Title, posterPath); err != nil {
230+
slog.Error("Failed to update film poster", "error", err)
201231
}
202232

203233
reviewID, err := db.CreateFilmReview(filmID, 0, time.Now(), false, false, false, "")
@@ -368,6 +398,49 @@ func DeleteFilmHandler() func(http.ResponseWriter, *http.Request) {
368398
}
369399
}
370400

401+
func FetchFilmPosterHandler() func(http.ResponseWriter, *http.Request) {
402+
return func(w http.ResponseWriter, r *http.Request) {
403+
if *tmdbAPIKey == "" {
404+
http.Error(w, "TMDB API key not configured", http.StatusInternalServerError)
405+
return
406+
}
407+
408+
idStr := r.PathValue("id")
409+
id, err := strconv.Atoi(idStr)
410+
if err != nil {
411+
http.Error(w, "Invalid film ID", http.StatusBadRequest)
412+
return
413+
}
414+
415+
film, err := db.GetFilmByID(id)
416+
if err != nil {
417+
slog.Error("Failed to get film", "error", err)
418+
http.Error(w, "Film not found", http.StatusNotFound)
419+
return
420+
}
421+
422+
if film.TMDBID == nil {
423+
http.Error(w, "Film has no TMDB ID", http.StatusBadRequest)
424+
return
425+
}
426+
427+
movie, err := tmdb.GetMovie(*tmdbAPIKey, *film.TMDBID)
428+
if err != nil {
429+
slog.Error("Failed to get movie from TMDB", "error", err)
430+
http.Error(w, "Failed to fetch movie from TMDB", http.StatusInternalServerError)
431+
return
432+
}
433+
434+
if err := updateOrCreateFilmPoster(id, film.Title, movie.PosterPath); err != nil {
435+
slog.Error("Failed to update film poster", "error", err)
436+
http.Error(w, "Failed to update film poster", http.StatusInternalServerError)
437+
return
438+
}
439+
440+
http.Redirect(w, r, fmt.Sprintf("/films/edit/%d", id), http.StatusSeeOther)
441+
}
442+
}
443+
371444
func GetFilmsWithReviewsHandler() func(http.ResponseWriter, *http.Request) {
372445
return func(w http.ResponseWriter, r *http.Request) {
373446
films, err := db.GetAllFilmsWithReviews()

cmd/serve/admin/tailscale.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func Start() error {
9494
httpsMux.HandleFunc("GET /films/edit/{id}", handlers.EditFilmHandler())
9595
httpsMux.HandleFunc("POST /films/edit/{id}", handlers.UpdateFilmHandler())
9696
httpsMux.HandleFunc("POST /films/delete/{id}", handlers.DeleteFilmHandler())
97+
httpsMux.HandleFunc("POST /films/fetch-poster/{id}", handlers.FetchFilmPosterHandler())
9798
httpsMux.HandleFunc("GET /film-reviews/edit/{id}", handlers.EditFilmReviewHandler())
9899
httpsMux.HandleFunc("POST /film-reviews/create/{id}", handlers.CreateFilmReviewHandler())
99100
httpsMux.HandleFunc("POST /film-reviews/edit/{id}", handlers.UpdateFilmReviewHandler())

cmd/serve/admin/templates/edit-film.html.gotpl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,22 @@
1919
<input type="submit" />
2020
</form>
2121

22+
<form method="POST" action="/films/delete/{{.ID}}" style="margin-top: 20px;">
23+
<button type="submit">Delete Film</button>
24+
</form>
25+
2226
{{if .Poster}}
2327
<h2>Poster</h2>
2428
<img src="/media/view/{{.Poster.ID}}" alt="{{.Title}} poster" style="max-width: 300px;" />
2529
{{end}}
2630

31+
{{if .TMDBID}}
32+
<h2>Fetch Poster</h2>
33+
<form method="POST" action="/films/fetch-poster/{{.ID}}">
34+
<button type="submit">Fetch Poster from TMDB</button>
35+
</form>
36+
{{end}}
37+
2738
<h2>Reviews</h2>
2839
<form method="POST" action="/film-reviews/create/{{.ID}}">
2940
<button type="submit">Add New Review</button>

cmd/serve/admin/templates/list-films.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ type ListFilmsData struct {
2121
}
2222

2323
type FilmSummary struct {
24-
ID int
25-
Title string
26-
Year string
27-
Rating string
28-
Published bool
24+
ID int
25+
Title string
26+
Year string
27+
Rating string
28+
Published bool
29+
PosterMediaID *int
30+
ReviewCount int
31+
LastWatched *string
2932
}
3033

3134
func RenderListFilms(w http.ResponseWriter, data ListFilmsData) error {

cmd/serve/admin/templates/list-films.html.gotpl

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{{/* gotype: chameth.com/chameth.com/cmd/serve/admin/templates.ListFilmsData */}}
22
{{define "content"}}
3+
<h2>Add Film by TMDB ID</h2>
4+
<form method="POST" action="/films">
5+
<input type="text" name="tmdb_id" placeholder="TMDB ID" />
6+
<button type="submit">Add Film</button>
7+
</form>
8+
39
<h2>Search TMDB</h2>
410
<form id="tmdb-search-form">
511
<input type="text" id="tmdb-search-input" name="q" placeholder="Search for a movie..." />
@@ -12,8 +18,11 @@
1218
<thead>
1319
<tr>
1420
<th>ID</th>
21+
<th>Poster</th>
1522
<th>Title</th>
1623
<th>Year</th>
24+
<th>Reviews</th>
25+
<th>Last Watched</th>
1726
<th>Rating</th>
1827
<th>Published</th>
1928
<th>Actions</th>
@@ -23,15 +32,19 @@
2332
{{range .Films}}
2433
<tr>
2534
<td>{{.ID}}</td>
35+
<td>
36+
{{if .PosterMediaID}}
37+
<img src="/media/view/{{.PosterMediaID}}" alt="{{.Title}} poster" style="max-width: 50px; max-height: 75px;" loading="lazy" />
38+
{{end}}
39+
</td>
2640
<td>{{.Title}}</td>
2741
<td>{{.Year}}</td>
42+
<td>{{.ReviewCount}}</td>
43+
<td>{{if .LastWatched}}{{.LastWatched}}{{end}}</td>
2844
<td>{{.Rating}}</td>
2945
<td>{{.Published}}</td>
3046
<td>
3147
<a href="/films/edit/{{.ID}}">Edit</a>
32-
<form method="POST" action="/films/delete/{{.ID}}" style="display:inline">
33-
<button type="submit">Delete</button>
34-
</form>
3548
</td>
3649
</tr>
3750
{{end}}

cmd/serve/db/films.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,85 @@ func GetAllFilmsWithReviews() ([]FilmWithReview, error) {
7979
return films, nil
8080
}
8181

82+
func GetAllFilmsWithReviewsAndPosters() ([]FilmWithReviewAndPoster, error) {
83+
query := `
84+
SELECT
85+
f.id, f.tmdb_id, f.title, f.year, f.overview, f.runtime, f.published, f.path,
86+
fr.id as review_id, fr.film_id as review_film_id, fr.watched_date, fr.rating, fr.is_rewatch, fr.has_spoilers, fr.review_text, fr.published as review_published,
87+
mr.path as poster_path, mr.media_id as poster_media_id,
88+
(SELECT COUNT(*) FROM film_reviews WHERE film_id = f.id) as review_count,
89+
(SELECT to_char(MAX(watched_date), 'YYYY-MM-DD') FROM film_reviews WHERE film_id = f.id) as last_watched
90+
FROM films f
91+
LEFT JOIN LATERAL (
92+
SELECT * FROM film_reviews
93+
WHERE film_id = f.id
94+
ORDER BY watched_date DESC
95+
LIMIT 1
96+
) fr ON true
97+
LEFT JOIN media_relations mr ON mr.entity_type = 'film' AND mr.entity_id = f.id AND mr.role = 'poster'
98+
ORDER BY f.title
99+
`
100+
101+
rows, err := db.Query(query)
102+
if err != nil {
103+
return nil, err
104+
}
105+
defer rows.Close()
106+
107+
var films []FilmWithReviewAndPoster
108+
for rows.Next() {
109+
var f Film
110+
var review FilmReview
111+
var reviewID, reviewFilmID sql.NullInt64
112+
var watchedDate sql.NullTime
113+
var rating sql.NullInt64
114+
var reviewText sql.NullString
115+
var reviewPublished sql.NullBool
116+
var posterPath sql.NullString
117+
var posterMediaID sql.NullInt64
118+
var reviewCount int
119+
var lastWatched sql.NullString
120+
121+
err := rows.Scan(
122+
&f.ID, &f.TMDBID, &f.Title, &f.Year, &f.Overview, &f.Runtime, &f.Published, &f.Path,
123+
&reviewID, &reviewFilmID, &watchedDate, &rating, &review.IsRewatch, &review.HasSpoilers, &reviewText, &reviewPublished,
124+
&posterPath, &posterMediaID,
125+
&reviewCount, &lastWatched,
126+
)
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
fwr := FilmWithReview{Film: f}
132+
if reviewID.Valid {
133+
review.ID = int(reviewID.Int64)
134+
review.FilmID = int(reviewFilmID.Int64)
135+
review.WatchedDate = watchedDate.Time
136+
review.Rating = int(rating.Int64)
137+
review.ReviewText = reviewText.String
138+
review.Published = reviewPublished.Bool
139+
fwr.Review = &review
140+
}
141+
142+
var pp *string
143+
var pmi *int
144+
if posterPath.Valid {
145+
pp = &posterPath.String
146+
mid := int(posterMediaID.Int64)
147+
pmi = &mid
148+
}
149+
150+
var lw *string
151+
if lastWatched.Valid {
152+
lw = &lastWatched.String
153+
}
154+
155+
films = append(films, FilmWithReviewAndPoster{FilmWithReview: fwr, PosterPath: pp, PosterMediaID: pmi, ReviewCount: reviewCount, LastWatched: lw})
156+
}
157+
158+
return films, nil
159+
}
160+
82161
func CreateFilm(tmdbID int, title, year, path string, overview string, runtime int) (int, error) {
83162
var yearPtr *int
84163
if year != "" {

cmd/serve/db/media.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,8 @@ func CreateMediaRelation(entityType string, entityID, mediaID int, path string,
178178
`, path, mediaID, caption, description, role, entityType, entityID)
179179
return err
180180
}
181+
182+
func DeleteMedia(id int) error {
183+
_, err := db.Exec("DELETE FROM media WHERE id = $1", id)
184+
return err
185+
}

cmd/serve/db/models.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ type FilmWithReview struct {
187187
Review *FilmReview
188188
}
189189

190+
type FilmWithReviewAndPoster struct {
191+
FilmWithReview
192+
PosterPath *string
193+
PosterMediaID *int
194+
ReviewCount int
195+
LastWatched *string
196+
}
197+
190198
type FilmReviewWithFilmAndPoster struct {
191199
FilmReview `db:"filmreview"`
192200
Film `db:"film"`

0 commit comments

Comments
 (0)