+ baseline to the git

This commit is contained in:
Itsigo
2025-11-04 13:52:36 +01:00
commit b3193a10f9
29 changed files with 2079 additions and 0 deletions

178
internal/apis/tmdb.go Normal file
View File

@@ -0,0 +1,178 @@
package apis
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
_ "github.com/joho/godotenv/autoload"
)
var (
token = os.Getenv("TMDB_API_KEY")
apiurl = "https://api.themoviedb.org/3/"
)
func request(requrl string) (*http.Response, []byte) {
req, _ := http.NewRequest("GET", apiurl+requrl, nil)
req.Header.Add("authorization", "Bearer "+token)
req.Header.Add("accept", "application/json")
res, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return res, body
}
func buildQuery(opts map[string]string) string {
query := url.Values{}
for k, v := range opts {
query.Add(k, v)
}
return query.Encode()
}
type Response struct {
Results []Movie `json:"results"`
}
type Movie struct {
Id int `json:"id"`
Title string `json:"title"`
OriginalTitle string `json:"original_title"`
PosterPath string `json:"poster_path"`
ReleaseDate string `json:"release_date"`
ImdbID string `json:"imdb_id"`
Lenght int `json:"runtime"`
Genres []Genre `json:"genres"`
Overview string `json:"overview"`
Director string
}
type Genre struct {
Name string `json:"name"`
}
type Crew struct {
Person []Person `json:"crew"`
}
type Person struct {
Name string `json:"name"`
Department string `json:"job"`
}
type WatchProviders struct {
ID int `json:"id"`
Results map[string]Country `json:"results"`
}
type Country struct {
Link string `json:"link"`
Flatrate []Provider `json:"flatrate,omitempty"`
Free []Provider `json:"free,omitempty"`
Rent []Provider `json:"rent,omitempty"`
Buy []Provider `json:"buy,omitempty"`
}
type Provider struct {
LogoPath string `json:"logo_path"`
ProviderID int `json:"provider_id"`
ProviderName string `json:"provider_name"`
DisplayPriority int `json:"display_priority"`
}
func SearchTmdbMovie(search string) []Movie {
opts := map[string]string{"query": search, "page": "1"}
url := fmt.Sprintf("search/movie?%s", buildQuery(opts))
_, body := request(url)
var responseObjekt Response
json.Unmarshal(body, &responseObjekt)
return responseObjekt.Results
}
func getMovieDirector(id string) string {
url := fmt.Sprintf("movie/%s/credits", id)
_, body := request(url)
var crewObjekt Crew
json.Unmarshal(body, &crewObjekt)
for _, v := range crewObjekt.Person {
if v.Department == "Director" {
return v.Name
}
}
return ""
}
func GetMovieStreamingServices(id int, lang string) Country {
url := fmt.Sprintf("movie/%d/watch/providers", id)
_, body := request(url)
var responseObjekt WatchProviders
json.Unmarshal(body, &responseObjekt)
// Returning the available streaming services
return responseObjekt.Results[lang]
}
func GetTmdbMovie(id string) Movie {
url := fmt.Sprintf("movie/%s", id)
_, body := request(url)
getMovieDirector(id)
var responseObjekt Movie
json.Unmarshal(body, &responseObjekt)
responseObjekt.Director = getMovieDirector(id)
return responseObjekt
}
type WatchResults struct {
Results []Provider `json:"results,omitempty"`
}
func GetMovieProviders(region string) []Provider {
opts := map[string]string{"language": "en_US", "watch_region": region}
url := fmt.Sprintf("watch/providers/movie?%s", buildQuery(opts))
_, body := request(url)
var responseObjekt WatchResults
json.Unmarshal(body, &responseObjekt)
return responseObjekt.Results
}
type RegionResults struct {
Results []Region `json:"results"`
}
type Region struct {
Iso string `json:"iso_3166_1"`
EnglishName string `json:"english_name"`
NativeName string `json:"native_name"`
}
func GetAvailableRegions() []Region {
url := "watch/providers/regions"
_, body := request(url)
var responseObjekt RegionResults
json.Unmarshal(body, &responseObjekt)
return responseObjekt.Results
}

View File

@@ -0,0 +1,32 @@
package database
import (
"context"
"database/sql"
_ "embed"
"log"
_ "github.com/mattn/go-sqlite3"
)
var (
dburl = "./data/data.db"
)
//go:embed schema.sql
var ddl string
func Init() *Queries {
ctx := context.Background()
db, err := sql.Open("sqlite3", dburl)
if err != nil {
log.Fatal(err)
}
if _, err := db.ExecContext(ctx, ddl); err != nil {
log.Fatal(err)
}
return New(db)
}

View File

@@ -0,0 +1,73 @@
-- name: ListMovies :many
SELECT * FROM movie
ORDER BY title ASC;
-- name: ListMovie :one
SELECT * FROM movie
WHERE id = ?;
-- name: ListWatchlist :many
SELECT * FROM movie
WHERE status = 0
ORDER BY
CASE WHEN streaming_services = '' OR streaming_services IS NULL THEN 1 ELSE 0 END,
title;
-- name: ListSettings :many
SELECT * FROM settings;
-- name: ListSetting :one
SELECT * FROM settings
WHERE id = ?;
-- name: ListSreamingServices :many
SELECT * FROM streaming_services;
-- name: CreateMovie :one
INSERT INTO movie (
title, original_title, imdbid, tmdbid, length, genre, streaming_services, director, year, watchcount, rating, status, owned, owned_type, ripped, review, overview
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
-- name: AddSreamingService :exec
INSERT INTO streaming_services (title) VALUES ( ? );
-- name: DeleteSreamingService :exec
DELETE FROM streaming_services
WHERE id = ?;
-- name: DeleteMovie :exec
DELETE FROM movie
WHERE id = ?;
-- name: ChangeMovieStatus :exec
UPDATE movie
SET status = ?
WHERE id = ?;
-- name: ChangeMovieRating :exec
UPDATE movie
SET rating = ?
WHERE id = ?;
-- name: ChangeMovieOwned :exec
UPDATE movie
SET owned = ?, owned_type = ?
WHERE id = ?;
-- name: ChangeMovieStreamingServices :exec
UPDATE movie
SET streaming_services = ?
WHERE id = ?;
-- name: ChangeMovieRipped :exec
UPDATE movie
SET ripped = ?
WHERE id = ?;
-- name: ChangeSettingValue :exec
UPDATE settings
SET value = ?
WHERE id = ?;

View File

@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS movie (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255) NOT NULL,
original_title VARCHAR(255) NOT NULL,
imdbid VARCHAR(25) NOT NULL,
tmdbid INTEGER NOT NULL,
length INTEGER NOT NULL,
genre VARCHAR(255) NOT NULL,
streaming_services VARCHAR(255) NOT NULL,
director VARCHAR(255) NOT NULL,
year VARCHAR(10) NOT NULL,
watchcount INTEGER NOT NULL,
rating INTEGER NOT NULL,
status INTEGER NOT NULL,
owned INTEGER NOT NULL,
owned_type VARCHAR(255) NOT NULL,
ripped INTEGER NOT NULL,
review TEXT NOT NULL,
overview TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS genres (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(30) NOT NULL
);
CREATE TABLE IF NOT EXISTS streaming_services (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(30) NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
value VARCHAR(100) NOT NULL,
description VARCHAR(255) NOT NULL
);
INSERT OR IGNORE INTO streaming_services
VALUES
("1", "Netflix"),
("2", "Disney Plus"),
("3", "Amazon Prime Video"),
("4", "Apple TV+");
INSERT OR IGNORE INTO settings
VALUES
("1", "HOME_GRID_VIEW", "false", "Grid or no grid on the Homepage"),
("2", "TMDB_API_KEY", "", "Your TMDB api key"),
("3", "REGION", "DE", "Your Region");

View File

@@ -0,0 +1,28 @@
package apikeychecker
import (
"strings"
"git.itsigo.dev/istigo/pocketmovie/internal/database"
"github.com/gofiber/fiber/v3"
)
type Config struct {
DB database.Queries
}
func configDefault(config ...Config) Config {
cfg := config[0]
return cfg
}
func New(config Config) fiber.Handler {
return func(c fiber.Ctx) error {
setting, _ := config.DB.ListSetting(c, 2)
referer := string(c.Request().Header.Referer())
if setting.Value == "" && !strings.Contains(referer, "apikey") {
return c.Redirect().Status(fiber.StatusMovedPermanently).To("/apikey")
}
return c.Next()
}
}

40
internal/server/index.go Normal file
View File

@@ -0,0 +1,40 @@
package server
import (
"slices"
"git.itsigo.dev/istigo/pocketmovie/cmd/web"
"git.itsigo.dev/istigo/pocketmovie/internal/apis"
"github.com/gofiber/fiber/v3"
)
func (s *FiberServer) Index(c fiber.Ctx) error {
movies, _ := s.db.ListMovies(c)
settings, _ := s.db.ListSettings(c)
return render(c, web.Show(movies, settings))
}
func (s *FiberServer) Watchlist(c fiber.Ctx) error {
movies, _ := s.db.ListWatchlist(c)
return render(c, web.WatchList(movies))
}
func (s *FiberServer) Settings(c fiber.Ctx) error {
ls, _ := s.db.ListSreamingServices(c)
_, ts := s.getAllStreamingServices(c)
slices.Sort(ts)
reg := apis.GetAvailableRegions()
selectedreg, _ := s.db.ListSetting(c, 3)
key, _ := s.db.ListSetting(c, 2)
config := web.SettingsConfig{
Providers: ls,
AvailableProviders: ts,
Regions: reg,
SelectedRegion: selectedreg.Value,
APIKey: key.Value,
}
return render(c, web.Settings(config))
}

357
internal/server/routes.go Normal file
View File

@@ -0,0 +1,357 @@
package server
import (
"fmt"
"image/jpeg"
"net/http"
"os"
"slices"
"strconv"
"strings"
"git.itsigo.dev/istigo/pocketmovie/cmd/web"
"git.itsigo.dev/istigo/pocketmovie/internal/apis"
"git.itsigo.dev/istigo/pocketmovie/internal/database"
"git.itsigo.dev/istigo/pocketmovie/internal/middleware/apikeychecker"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/fiber/v3/middleware/static"
)
func (s *FiberServer) RegisterFiberRoutes() {
s.App.Use("/apikey", s.apikeyinput)
s.App.Use("/assets", static.New("./assets", static.Config{
FS: web.Files,
Browse: false,
}))
s.App.Use("/movie/posters", static.New("./data/img"))
s.App.Use(
//basicauth.New(basicauth.Config{
// Users: map[string]string{
// // "doe" hashed using SHA-256
// "john": "{SHA256}eZ75KhGvkY4/t0HfQpNPO1aO0tk6wd908bjUGieTKm8=",
// },
//}),
//compress.New(compress.Config{
// Level: compress.LevelBestSpeed, // 1
//}),
apikeychecker.New(apikeychecker.Config{DB: *s.db}),
)
s.App.Get("/", s.Index)
s.App.Get("/watchlist", s.Watchlist)
s.App.Get("/settings", s.Settings)
s.App.Get("/movie/:id", s.movieDetails)
s.App.Get("/components/mediaTypeSelect/:id", s.mediaTypeSelect)
s.App.Post("/apis/tmdb/searchMovie", s.searchMovie)
s.App.Post("/db/addMovie/:id", s.addMovieToDb)
s.App.Post("/db/changeMovieStatus/:id.:status", s.changeMovieStatus)
s.App.Post("/db/changeMovieRating/:id.:rating", s.changeMovieRating)
s.App.Post("/db/changeMovieOwned/:id", s.changeMovieOwned)
s.App.Post("/db/changeMovieRipped/:id.:ripped", s.changeMovieRipped)
s.App.Post("/db/updateStreamingServices", s.updateMovieStreamingServices)
s.App.Post("/db/updateTableView", s.updateTableView)
s.App.Post("/db/updateRegion", s.updateRegion)
s.App.Post("/db/updateApiKey", s.updateApiKey)
s.App.Delete("/db/deleteMovie/:id", s.deleteMovie)
s.App.Post("/db/addStreamingService", s.addStreamingService)
s.App.Delete("/db/deleteStreamingService/:id", s.deleteStreamingService)
}
func (s *FiberServer) apikeyinput(c fiber.Ctx) error {
key := c.FormValue("apikey")
apierror := ""
if key != "" {
url := "https://api.themoviedb.org/3/authentication"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("accept", "application/json")
req.Header.Add("Authorization", "Bearer "+key)
res, _ := http.DefaultClient.Do(req)
if res.StatusCode == 200 {
s.db.ChangeSettingValue(c, database.ChangeSettingValueParams{
ID: 2,
Value: key,
})
c.Redirect().Status(fiber.StatusMovedPermanently).To("/")
}
apierror = "Could not certify API Read Access Token"
}
return render(c, web.Apikeyinput(apierror))
}
func (s *FiberServer) updateApiKey(c fiber.Ctx) error {
key := c.FormValue("apikey")
s.db.ChangeSettingValue(c, database.ChangeSettingValueParams{
ID: 2,
Value: key,
})
config := web.SettingsConfig{
APIKey: key,
}
return render(c, web.ApiKey(config))
}
func (s *FiberServer) movieDetails(c fiber.Ctx) error {
id, _ := strconv.ParseInt(c.Params("id"), 10, 64)
movie, _ := s.db.ListMovie(c, id)
return render(c, web.MovieDetails(movie))
}
func (s *FiberServer) mediaTypeSelect(c fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
return render(c, web.MovieDetailsOwnedSelect(int8(id)))
}
func (s *FiberServer) searchMovie(c fiber.Ctx) error {
res := apis.SearchTmdbMovie(c.FormValue("search"))
return render(c, web.SearchMovie(res))
}
func (s *FiberServer) changeMovieStatus(c fiber.Ctx) error {
id, _ := strconv.ParseInt(c.Params("id"), 10, 64)
status, _ := strconv.ParseInt(c.Params("status"), 10, 64)
s.db.ChangeMovieStatus(c, database.ChangeMovieStatusParams{
ID: id,
Status: status,
})
return render(c, web.MovieDetailsWatched(id, status))
}
func (s *FiberServer) changeMovieRating(c fiber.Ctx) error {
id, _ := strconv.ParseInt(c.Params("id"), 10, 64)
rating, _ := strconv.ParseInt(c.Params("rating"), 10, 64)
s.db.ChangeMovieRating(c, database.ChangeMovieRatingParams{
ID: id,
Rating: rating,
})
return render(c, web.MovieDetailsRating(id, rating))
}
func (s *FiberServer) changeMovieOwned(c fiber.Ctx) error {
id, _ := strconv.ParseInt(c.Params("id"), 10, 64)
option := c.FormValue("option")
var owned int64 = 0
if option != "" {
owned = 1
}
s.db.ChangeMovieOwned(c, database.ChangeMovieOwnedParams{
ID: id,
Owned: owned,
OwnedType: option,
})
return render(c, web.MovieDetailsOwned(id, option))
}
func (s *FiberServer) changeMovieRipped(c fiber.Ctx) error {
id, _ := strconv.ParseInt(c.Params("id"), 10, 64)
ripped, _ := strconv.ParseInt(c.Params("ripped"), 10, 64)
s.db.ChangeMovieRipped(c, database.ChangeMovieRippedParams{
ID: id,
Ripped: ripped,
})
return render(c, web.MovieDetailsRipped(id, ripped))
}
func (s *FiberServer) updateMovieStreamingServices(c fiber.Ctx) error {
movies, _ := s.db.ListMovies(c)
for _, v := range movies {
s.db.ChangeMovieStreamingServices(c, database.ChangeMovieStreamingServicesParams{
ID: v.ID,
StreamingServices: s.getStreamingServicesForMovie(c, v.Tmdbid),
})
}
movies, _ = s.db.ListWatchlist(c)
return render(c, web.List(movies))
}
func (s *FiberServer) updateTableView(c fiber.Ctx) error {
setting, _ := s.db.ListSetting(c, 1)
v := "false"
if setting.Value == "false" {
v = "true"
}
s.db.ChangeSettingValue(c, database.ChangeSettingValueParams{
Value: v,
ID: 1,
})
movies, _ := s.db.ListMovies(c)
if v == "true" {
return render(c, web.MovieTiles(movies))
}
return render(c, web.MovieList(movies))
}
func (s *FiberServer) updateRegion(c fiber.Ctx) error {
region := c.FormValue("region")
s.db.ChangeSettingValue(c, database.ChangeSettingValueParams{
Value: region,
ID: 3,
})
regions := apis.GetAvailableRegions()
config := web.SettingsConfig{
Regions: regions,
SelectedRegion: region,
}
return render(c, web.Region(config))
}
func (s *FiberServer) addStreamingService(c fiber.Ctx) error {
title := c.FormValue("service")
s.db.AddSreamingService(c, title)
ls, _ := s.db.ListSreamingServices(c)
_, ts := s.getAllStreamingServices(c)
slices.Sort(ts)
config := web.SettingsConfig{
Providers: ls,
AvailableProviders: ts,
}
return render(c, web.ProviderTable(config))
}
func (s *FiberServer) deleteStreamingService(c fiber.Ctx) error {
id := fiber.Params[int64](c, "id")
s.db.DeleteSreamingService(c, id)
ls, _ := s.db.ListSreamingServices(c)
_, ts := s.getAllStreamingServices(c)
slices.Sort(ts)
config := web.SettingsConfig{
Providers: ls,
AvailableProviders: ts,
}
return render(c, web.ProviderTable(config))
}
func (s *FiberServer) deleteMovie(c fiber.Ctx) error {
id := fiber.Params[int64](c, "id")
s.db.DeleteMovie(c, id)
movies, _ := s.db.ListMovies(c)
return render(c, web.MovieList(movies))
}
func (s *FiberServer) addMovieToDb(c fiber.Ctx) error {
res := apis.GetTmdbMovie(c.Params("id"))
referer := string(c.Request().Header.Referer())
// Combine Genres
genres := []string{}
for _, v := range res.Genres {
genres = append(genres, v.Name)
}
// Get the streaming providers
streamingservices := s.getStreamingServicesForMovie(c, int64(res.Id))
rating := 0
if c.FormValue("rating") != "" {
rating, _ = strconv.Atoi(c.FormValue("rating"))
}
watched := 0
if c.FormValue("watched") == "on" {
watched = 1
}
watchcount := 0
if c.FormValue("watchcount") != "" {
watchcount, _ = strconv.Atoi(c.FormValue("watchcount"))
}
owned := 0
if c.FormValue("owned") == "on" {
owned = 1
}
ownedType := ""
if c.FormValue("version") != "" {
ownedType = c.FormValue("version")
}
ripped := 0
if c.FormValue("ripped") == "on" {
ripped = 1
}
movie, err := s.db.CreateMovie(c, database.CreateMovieParams{
Title: res.Title,
OriginalTitle: res.OriginalTitle,
Imdbid: res.ImdbID,
Tmdbid: int64(res.Id),
Length: int64(res.Lenght),
Genre: strings.Join(genres, ", "),
StreamingServices: streamingservices,
Director: res.Director,
Year: res.ReleaseDate,
Watchcount: int64(watchcount),
Rating: int64(rating),
Status: int64(watched),
Owned: int64(owned),
OwnedType: ownedType,
Ripped: int64(ripped),
Review: "",
Overview: res.Overview,
})
if err != nil {
log.Error(err)
}
// Get the movie poster
resp, err := http.Get("https://image.tmdb.org/t/p/original" + res.PosterPath)
if err != nil {
log.Error(err)
}
defer resp.Body.Close()
tmp, err := jpeg.Decode(resp.Body)
if err == nil {
path := fmt.Sprintf("./data/img/%d.jpg", movie.ID)
log.Info(path)
outputFile, err := os.Create(path)
if err != nil {
log.Error(err)
}
jpeg.Encode(outputFile, tmp, nil)
outputFile.Close()
}
if strings.Contains(referer, "watchlist") {
movies, _ := s.db.ListWatchlist(c)
return render(c, web.List(movies))
}
gridSetting, _ := s.db.ListSetting(c, 1)
movies, _ := s.db.ListMovies(c)
componet := web.MovieList(movies)
if gridSetting.Value == "true" {
componet = web.MovieTiles(movies)
}
return render(c, componet)
}

23
internal/server/server.go Normal file
View File

@@ -0,0 +1,23 @@
package server
import (
"git.itsigo.dev/istigo/pocketmovie/internal/database"
"github.com/gofiber/fiber/v3"
)
type FiberServer struct {
*fiber.App
db *database.Queries
}
func New() *FiberServer {
server := &FiberServer{
App: fiber.New(fiber.Config{
ServerHeader: "PocketMovie",
AppName: "PocketMovie",
}),
db: database.Init(),
}
return server
}

70
internal/server/util.go Normal file
View File

@@ -0,0 +1,70 @@
package server
import (
"slices"
"strings"
"git.itsigo.dev/istigo/pocketmovie/internal/apis"
"github.com/a-h/templ"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/session"
)
var store = session.New()
func render(c fiber.Ctx, component templ.Component) error {
// or templ wil bork...
c.Set("Content-type", "text/html")
return component.Render(c, c.Response().BodyWriter())
}
// Gets the Streaming services for a movie as a string (service 1, service 2, ...)
func (s *FiberServer) getStreamingServicesForMovie(c fiber.Ctx, id int64) string {
// Get the streaming providers
dbServicesSlice := s.getLocalStreamingServices(c)
region, _ := s.db.ListSetting(c, 3)
flatrate := apis.GetMovieStreamingServices(int(id), region.Value).Flatrate
free := apis.GetMovieStreamingServices(int(id), region.Value).Free
flatrate = append(flatrate, free...)
streaningservices := []string{}
for _, v := range flatrate {
if slices.Contains(dbServicesSlice, v.ProviderName) {
streaningservices = append(streaningservices, v.ProviderName)
}
}
return strings.Join(streaningservices, ", ")
}
func (s *FiberServer) getAllStreamingServices(c fiber.Ctx) ([]string, []string) {
ls := s.getLocalStreamingServices(c)
ts := s.getTmdbStreamingServices(c, ls)
return ls, ts
}
func (s *FiberServer) getLocalStreamingServices(c fiber.Ctx) []string {
services, _ := s.db.ListSreamingServices(c)
sl := []string{}
for _, p := range services {
sl = append(sl, p.Title)
}
return sl
}
func (s *FiberServer) getTmdbStreamingServices(c fiber.Ctx, localservices []string) []string {
region, _ := s.db.ListSetting(c, 3)
services := apis.GetMovieProviders(region.Value)
sl := []string{}
for _, p := range services {
if !slices.Contains(localservices, p.ProviderName) {
sl = append(sl, p.ProviderName)
}
}
return sl
}