commit b3193a10f9e4d70060bd187fc49189c7d98e7484 Author: Itsigo Date: Tue Nov 4 13:52:36 2025 +0100 + baseline to the git diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..d007e2f --- /dev/null +++ b/.air.toml @@ -0,0 +1,51 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "make build" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = ["internal/database/db.go", "internal/database/models.go"] + exclude_regex = ["_test.go", ".*_templ.go", ".sql.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "templ", "css", "js", "sql"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = true + +[screen] + clear_on_rebuild = false + keep_scroll = true + +[proxy] + enabled = true + proxy_port = 8383 + app_port = 3000 diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..6db3791 --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +PORT=3000 +TMDB_API_KEY=YOUR_KEY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d8fa81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +data/ +tmp/ +main +pico.min.css +datastar.js +*_templ.go +db.go +models.go +*.sql.go +jersey15* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c6dd43a --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +run: + @templ generate + @go run cmd/api/main.go + +build: + @templ generate + @sqlc generate + @go build -o tmp/main cmd/api/main.go + +setup: + @curl -o cmd/web/assets/js/datastar.js https://cdn.jsdelivr.net/gh/starfederation/datastar@main/bundles/datastar.js + @curl -o cmd/web/assets/css/fonts/jersey15.woff2 https://fonts.gstatic.com/s/jersey15/v3/_6_9EDzuROGsUuk2TWjiZYAg.woff2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0329c88 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# PocketMovie + +A small database to keep track of the movies you have seen or want to see. diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..219d52f --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + "strconv" + + "git.itsigo.dev/istigo/pocketmovie/internal/server" + "github.com/gofiber/fiber/v3" + _ "github.com/joho/godotenv/autoload" +) + +func initial() { + if _, err := os.Stat("./data"); os.IsNotExist(err) { + os.Mkdir("./data/", 0755) + fmt.Println("Data created") + os.Mkdir("./data/img/", 0755) + fmt.Println("Img created") + } +} + +func main() { + initial() + + app := server.New() + + app.RegisterFiberRoutes() + + port, _ := strconv.Atoi(os.Getenv("PORT")) + err := app.Listen(fmt.Sprintf(":%d", port), fiber.ListenConfig{ + DisableStartupMessage: true, + }) + if err != nil { + panic(fmt.Sprintf("http server error: %s", err)) + } +} diff --git a/cmd/web/apiinput.templ b/cmd/web/apiinput.templ new file mode 100644 index 0000000..33b40d8 --- /dev/null +++ b/cmd/web/apiinput.templ @@ -0,0 +1,39 @@ +package web + +templ Apikeyinput(apierror string) { + @Html("Please input your TMDB Api key - PocketMovie") { +
+
+

API Read Access Token needed

+

To use this application you need to enter your TMDB API Read Access Token. If you do not have a Token you can get one by creating a TMDB account.

+

It is needed to search movies, pull metadata and stream providers.

+
+
+ + +
+ if apierror != "" { + { apierror } + } +
+
+
+ } +} diff --git a/cmd/web/assets/css/catppuccin.css b/cmd/web/assets/css/catppuccin.css new file mode 100644 index 0000000..7fa532e --- /dev/null +++ b/cmd/web/assets/css/catppuccin.css @@ -0,0 +1,80 @@ +:root { + --ctp-macchiato-rosewater: #f4dbd6; + --ctp-macchiato-rosewater-rgb: 244 219 214; + --ctp-macchiato-rosewater-hsl: 10.000 57.692% 89.804%; + --ctp-macchiato-flamingo: #f0c6c6; + --ctp-macchiato-flamingo-rgb: 240 198 198; + --ctp-macchiato-flamingo-hsl: 0.000 58.333% 85.882%; + --ctp-macchiato-pink: #f5bde6; + --ctp-macchiato-pink-rgb: 245 189 230; + --ctp-macchiato-pink-hsl: 316.071 73.684% 85.098%; + --ctp-macchiato-mauve: #c6a0f6; + --ctp-macchiato-mauve-rgb: 198 160 246; + --ctp-macchiato-mauve-hsl: 266.512 82.692% 79.608%; + --ctp-macchiato-red: #ed8796; + --ctp-macchiato-red-rgb: 237 135 150; + --ctp-macchiato-red-hsl: 351.176 73.913% 72.941%; + --ctp-macchiato-maroon: #ee99a0; + --ctp-macchiato-maroon-rgb: 238 153 160; + --ctp-macchiato-maroon-hsl: 355.059 71.429% 76.667%; + --ctp-macchiato-peach: #f5a97f; + --ctp-macchiato-peach-rgb: 245 169 127; + --ctp-macchiato-peach-hsl: 21.356 85.507% 72.941%; + --ctp-macchiato-yellow: #eed49f; + --ctp-macchiato-yellow-rgb: 238 212 159; + --ctp-macchiato-yellow-hsl: 40.253 69.912% 77.843%; + --ctp-macchiato-green: #a6da95; + --ctp-macchiato-green-rgb: 166 218 149; + --ctp-macchiato-green-hsl: 105.217 48.252% 71.961%; + --ctp-macchiato-teal: #8bd5ca; + --ctp-macchiato-teal-rgb: 139 213 202; + --ctp-macchiato-teal-hsl: 171.081 46.835% 69.020%; + --ctp-macchiato-sky: #91d7e3; + --ctp-macchiato-sky-rgb: 145 215 227; + --ctp-macchiato-sky-hsl: 188.780 59.420% 72.941%; + --ctp-macchiato-sapphire: #7dc4e4; + --ctp-macchiato-sapphire-rgb: 125 196 228; + --ctp-macchiato-sapphire-hsl: 198.641 65.605% 69.216%; + --ctp-macchiato-blue: #8aadf4; + --ctp-macchiato-blue-rgb: 138 173 244; + --ctp-macchiato-blue-hsl: 220.189 82.813% 74.902%; + --ctp-macchiato-lavender: #b7bdf8; + --ctp-macchiato-lavender-rgb: 183 189 248; + --ctp-macchiato-lavender-hsl: 234.462 82.278% 84.510%; + --ctp-macchiato-text: #cad3f5; + --ctp-macchiato-text-rgb: 202 211 245; + --ctp-macchiato-text-hsl: 227.442 68.254% 87.647%; + --ctp-macchiato-subtext1: #b8c0e0; + --ctp-macchiato-subtext1-rgb: 184 192 224; + --ctp-macchiato-subtext1-hsl: 228.000 39.216% 80.000%; + --ctp-macchiato-subtext0: #a5adcb; + --ctp-macchiato-subtext0-rgb: 165 173 203; + --ctp-macchiato-subtext0-hsl: 227.368 26.761% 72.157%; + --ctp-macchiato-overlay2: #939ab7; + --ctp-macchiato-overlay2-rgb: 147 154 183; + --ctp-macchiato-overlay2-hsl: 228.333 20.000% 64.706%; + --ctp-macchiato-overlay1: #8087a2; + --ctp-macchiato-overlay1-rgb: 128 135 162; + --ctp-macchiato-overlay1-hsl: 227.647 15.455% 56.863%; + --ctp-macchiato-overlay0: #6e738d; + --ctp-macchiato-overlay0-rgb: 110 115 141; + --ctp-macchiato-overlay0-hsl: 230.323 12.351% 49.216%; + --ctp-macchiato-surface2: #5b6078; + --ctp-macchiato-surface2-rgb: 91 96 120; + --ctp-macchiato-surface2-hsl: 229.655 13.744% 41.373%; + --ctp-macchiato-surface1: #494d64; + --ctp-macchiato-surface1-rgb: 73 77 100; + --ctp-macchiato-surface1-hsl: 231.111 15.607% 33.922%; + --ctp-macchiato-surface0: #363a4f; + --ctp-macchiato-surface0-rgb: 54 58 79; + --ctp-macchiato-surface0-hsl: 230.400 18.797% 26.078%; + --ctp-macchiato-base: #24273a; + --ctp-macchiato-base-rgb: 36 39 58; + --ctp-macchiato-base-hsl: 231.818 23.404% 18.431%; + --ctp-macchiato-mantle: #1e2030; + --ctp-macchiato-mantle-rgb: 30 32 48; + --ctp-macchiato-mantle-hsl: 233.333 23.077% 15.294%; + --ctp-macchiato-crust: #181926; + --ctp-macchiato-crust-rgb: 24 25 38; + --ctp-macchiato-crust-hsl: 235.714 22.581% 12.157%; +} diff --git a/cmd/web/assets/css/fonts.css b/cmd/web/assets/css/fonts.css new file mode 100644 index 0000000..73d304b --- /dev/null +++ b/cmd/web/assets/css/fonts.css @@ -0,0 +1,10 @@ +/* jersey-15-regular - latin */ +@font-face { + font-display: swap; + /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Jersey 15'; + font-style: normal; + font-weight: 400; + src: url('./fonts/jersey15.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} diff --git a/cmd/web/assets/css/main.css b/cmd/web/assets/css/main.css new file mode 100644 index 0000000..e620ef8 --- /dev/null +++ b/cmd/web/assets/css/main.css @@ -0,0 +1,182 @@ +@import "fonts.css"; +@import "catppuccin.css"; + +:root { + --pico-form-element-spacing-vertical: 0.5rem; + --pico-form-element-spacing-horizontal: 0.75rem; + + --pico-color: var(--ctp-macchiato-text); + --pico-h1-color: var(--ctp-macchiato-text); + --pico-primary-inverse: var(--ctp-macchiato-base); + + --pico-primary: var(--ctp-macchiato-blue); + --pico-primary-underline: var(--ctp-macchiato-blue); + --pico-primary-background: var(--ctp-macchiato-blue); + + --pico-primary-hover: var(--ctp-macchiato-mauve); + --pico-primary-hover-background: var(--ctp-macchiato-mauve); + + --pico-background-color: var(--ctp-macchiato-crust); +} + +.bg-red{ + color: hsl(351deg, 74%, 40%); + background-color: var(--ctp-macchiato-red) !important; +} + + +.bg-green{ + color: hsl(105deg, 48%, 40%); + background-color: var(--ctp-macchiato-green) !important; +} + +.red-link{ + color: var(--ctp-macchiato-red); + text-decoration-color: var(--ctp-macchiato-red); +} + +h1 { + font-family: "Jersey 15"; +} + +#search-dialog { + & article { + max-height: 700px; + } + + & tr { + cursor: pointer; + + & :hover { + background-color: var(--pico-hover-background); + } + } +} + +.hidden { + display: none; +} + +.center { + text-align: center; +} + +.flex-center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.m-0 { + margin: 0; +} + +.grid-4 { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: 1fr; + grid-column-gap: 1rem; + grid-row-gap: 0px; +} + +.grid-3 { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 1fr; + grid-column-gap: 1rem; + grid-row-gap: 0px; +} + +.movie-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: 1fr; + grid-column-gap: 1.5rem; + grid-row-gap: 1.5rem; + + & .movie-tile { + + & div { + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.7); + padding: 0.25rem; + border-bottom-right-radius: var(--pico-border-radius); + border-top-left-radius: var(--pico-border-radius); + + & svg { + width: 2rem; + } + } + + & img { + border-radius: var(--pico-border-radius); + } + + } + +} + +.watched { + text-align: center; + cursor: pointer; + + & svg { + width: 3rem; + color: var(--pico-primary); + } +} + + +.ratingBox { + text-align: center; + + i { + background-color: var(--pico-secondary); + cursor: pointer; + } + + .rated { + background-color: var(--pico-primary); + } + + >i { + + &:hover .icon, + &:hover~.icon { + background-color: var(--pico-secondary) !important; + } + } + + &:hover>.icon { + background-color: var(--pico-primary); + } + +} + + +.listRating { + i { + width: 1.25rem; + height: 1.25rem; + background-color: var(--pico-secondary); + } + + .rated { + background-color: var(--pico-primary); + } +} + + +.icon { + display: inline-block; + font-size: var(--pico-font-size); + width: 2rem; + height: 2rem; + + &.star-full { + mask: url('data:image/svg+xml,'); + } +} diff --git a/cmd/web/assets/js/modal.js b/cmd/web/assets/js/modal.js new file mode 100644 index 0000000..f7b2151 --- /dev/null +++ b/cmd/web/assets/js/modal.js @@ -0,0 +1,60 @@ +/* + * Modal + * + * Pico.css - https://picocss.com + * Copyright 2019-2024 - Licensed under MIT + */ + +// Config +const isOpenClass = "modal-is-open"; +const openingClass = "modal-is-opening"; +const closingClass = "modal-is-closing"; +const scrollbarWidthCssVar = "--pico-scrollbar-width"; +const animationDuration = 400; // ms +let visibleModal = null; + +// Toggle modal +const toggleModal = (event) => { + event.preventDefault(); + const modal = document.getElementById(event.currentTarget.dataset.target); + if (!modal) return; + modal && (modal.open ? closeModal(modal) : openModal(modal)); +}; + +// Open modal +const openModal = (modal) => { + const { documentElement: html } = document; + const scrollbarWidth = getScrollbarWidth(); + if (scrollbarWidth) { + html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`); + } + html.classList.add(isOpenClass, openingClass); + setTimeout(() => { + visibleModal = modal; + html.classList.remove(openingClass); + }, animationDuration); + modal.showModal(); +}; + +// Close modal +const closeModal = (modal) => { + visibleModal = null; + const { documentElement: html } = document; + html.classList.add(closingClass); + setTimeout(() => { + html.classList.remove(closingClass, isOpenClass); + html.style.removeProperty(scrollbarWidthCssVar); + modal.close(); + }, animationDuration); +}; + +// Get scrollbar width +const getScrollbarWidth = () => { + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + return scrollbarWidth; +}; + +// Is scrollbar visible +const isScrollbarVisible = () => { + return document.body.scrollHeight > screen.height; +}; diff --git a/cmd/web/base.templ b/cmd/web/base.templ new file mode 100644 index 0000000..e771fb4 --- /dev/null +++ b/cmd/web/base.templ @@ -0,0 +1,156 @@ +package web + +import "strings" +import "git.itsigo.dev/istigo/pocketmovie/internal/apis" +import "fmt" + +templ Html(title string) { + + + + + + + + + + + { title } + + + { children... } + + +} + +templ Base(title string) { + @Html(title) { +
+ @header() +
+
+ { children... } +
+ // + +
+
+ +

+ 🎬 Add Movie +

+
+
+
+ + +
+ + +
+
+
+ @AddMovieConfig() +
+
+
+ } +} + +templ header() { + +} + +templ SearchMovie(movies []apis.Movie) { + + for _, v := range movies { + + + { v.Title } + if v.ReleaseDate != "" { + ({ strings.Split(v.ReleaseDate, "-")[0] }) + } + + + } + +} + +templ AddMovieConfig() { +
+
+ + General + + + +
+ + +
+
+
+ + Physical data + + + + + +
+ + +
+} + +templ addMovieButton() { + +} + +templ MediaSelectOptions() { + + + + + + +} diff --git a/cmd/web/efs.go b/cmd/web/efs.go new file mode 100644 index 0000000..a48007e --- /dev/null +++ b/cmd/web/efs.go @@ -0,0 +1,6 @@ +package web + +import "embed" + +//go:embed assets/* +var Files embed.FS diff --git a/cmd/web/home.templ b/cmd/web/home.templ new file mode 100644 index 0000000..080e817 --- /dev/null +++ b/cmd/web/home.templ @@ -0,0 +1,138 @@ +package web + +import "git.itsigo.dev/istigo/pocketmovie/internal/database" +import "strings" +import "fmt" + +func getConfVal(settings []database.Setting, name string) string { + for _, s := range settings { + if s.Name == name { + return s.Value + } + } + return "" +} + +templ Show(movies []database.Movie, settings []database.Setting) { + @Base("Movies - PocketMovie") { + //

+		//
+		@addMovieButton()
+		{{ list, grid := "1", "" }}
+		if getConfVal(settings, "HOME_GRID_VIEW" ) == "true" {
+			{{ list, grid = "", "1" }}
+		}
+		
+ + +
+ if grid != "" { + @MovieTiles(movies) + } else { + @MovieList(movies) + } + } +} + +templ MovieList(movies []database.Movie) { + + + + + + + + + + for _, item := range movies { + + + + + {{ statusClasses := "bg-red" }} + if item.Status == 1 { + {{ statusClasses = "bg-green" }} + } + + {{ ownedClasses := "bg-red" }} + if item.Owned == 1 { + {{ ownedClasses = "bg-green" }} + } + + + + } +
TitelYearRatingWatchedOwned
{ item.Title }{ strings.Split(item.Year, "-")[0] } +
+ {{ class := "icon star-full" }} + for i := range(5) { + + } +
+
+ { item.Status != 0 } + + { item.Owned != 0 } + + Delete +
+} + +templ MovieTiles(movies []database.Movie) { +
+ for _, item := range movies { + +
+ if item.Status == 1 { + @fullEye() + } else { + @closedEye() + } +
+ { +
+ } +
+} diff --git a/cmd/web/moviedetails.templ b/cmd/web/moviedetails.templ new file mode 100644 index 0000000..9f9e49e --- /dev/null +++ b/cmd/web/moviedetails.templ @@ -0,0 +1,200 @@ +package web + +import "git.itsigo.dev/istigo/pocketmovie/internal/database" +import "fmt" +import "strings" + +templ MovieDetails(movie database.Movie) { + @Base(movie.Title + " - PocketMovie") { +
+
+ { +
+
+
+

{ movie.Title }

+
{ strings.Split(movie.Year, "-")[0] } Directed by { movie.Director }
+
+
+
+

{ movie.Overview }

+

Genres: { movie.Genre }

+

Runtime: { movie.Length } Min

+ if movie.StreamingServices != "" { +

Stream provider: { movie.StreamingServices }

+ } +
+
+
+ @MovieDetailsWatched(movie.ID, movie.Status) +
+ @MovieDetailsRating(movie.ID, movie.Rating) +
+
+ @MovieDetailsOwned(movie.ID, movie.OwnedType) + @MovieDetailsRipped(movie.ID, movie.Ripped) +
+
+
+
+
+
+ } +} + +// Gives you the opposite of 1 or 0 +func revIntBool(status int64) int64 { + if status == 0 { + return 1 + } + return 0 +} + +templ MovieDetailsWatched(id int64, status int64) { +
+ + if status == 1 { + @fullEye() + } else { + @closedEye() + } + + if status == 1 { +

Watched

+ } else { +

Not watched

+ } +
+} + +templ MovieDetailsRating(id int64, rating int64) { +
+

+ if rating != 0 { + Rated + } else { + Not rated + } +

+
+ {{ class := "icon star-full" }} + for i := range(5) { + + } +
+
+} + +templ MovieDetailsOwned(id int64, option string) { +
+

+ if option != "" { + Owned + } else { + Not owned + } +

+
+ switch option { + case "4KBD": + @ultraHD() + case "BD": + @bluray() + case "DVD": + @dvd() + case "DL": + @fullDownload() + default: + @forbiddenLineCircle() + } +
+
+} + +templ MovieDetailsRipped(id int64, status int64) { +
+
+ if status != 0 { +

Ripped

+ @checkedFullCircle() + } else { +

Not ripped

+ @forbiddenLineCircle() + } +
+
+} + +templ MovieDetailsOwnedSelect(id int8) { +
+

Select option

+
+ +
+
+} + +templ fullStar() { + +} + +templ emptyStar() { + +} + +templ fullEye() { + +} + +templ closedEye() { + +} + +templ ultraHD() { + + + +} + +templ bluray() { + +} + +templ dvd() { + +} + +templ fullDownload() { + +} + +templ forbiddenLineCircle() { + +} + +templ checkedFullCircle() { + +} diff --git a/cmd/web/settings.templ b/cmd/web/settings.templ new file mode 100644 index 0000000..a31a892 --- /dev/null +++ b/cmd/web/settings.templ @@ -0,0 +1,95 @@ +package web + +import "fmt" +import "git.itsigo.dev/istigo/pocketmovie/internal/database" +import "git.itsigo.dev/istigo/pocketmovie/internal/apis" + +type SettingsConfig struct { + Providers []database.StreamingService + AvailableProviders []string + Regions []apis.Region + SelectedRegion string + APIKey string +} + +templ Settings(settings SettingsConfig) { + @Base("Settings - PocketMovie") { +

Settings

+
+
+

Stream Region

+ @Region(settings) +

Stream Providers

+ @ProviderTable(settings) +
+
+

API Token

+ @ApiKey(settings) +
+
+ } +} + +templ ApiKey(settings SettingsConfig) { +
+
+ + +
+
+} + +templ ProviderTable(settings SettingsConfig) { +
+ + + + + + for _, p := range(settings.Providers) { + + + + + } +
Name
{ p.Title } + Delete +
+
+
+ + +
+
+
+} + +templ Region(settings SettingsConfig) { +
+
+ +
+
+} diff --git a/cmd/web/watchlist.templ b/cmd/web/watchlist.templ new file mode 100644 index 0000000..92e6022 --- /dev/null +++ b/cmd/web/watchlist.templ @@ -0,0 +1,52 @@ +package web + +import "git.itsigo.dev/istigo/pocketmovie/internal/database" +import "fmt" +import "strings" + +templ WatchList(movies []database.Movie) { + @Base("Watchlist - PocketMovie") { + @addMovieButton() + + @List(movies) + } +} + +templ List(movies []database.Movie) { + + + + + + + + + for _, item := range movies { + + + + + {{ ownedClasses := "bg-red" }} + if item.Owned == 1 { + {{ ownedClasses = "bg-green" }} + } + + + + } +
TitelYearGenreOwnedStreaming service
{ item.Title }{ strings.Split(item.Year, "-")[0] }{ item.Genre } + { item.Owned != 0 } + { item.StreamingServices }
+} + +templ refresh() { + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fda0a0a --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module git.itsigo.dev/istigo/pocketmovie + +go 1.25.0 + +require ( + github.com/a-h/templ v0.3.943 + github.com/gofiber/fiber/v3 v3.0.0-rc.1 + github.com/joho/godotenv v1.5.1 + github.com/mattn/go-sqlite3 v1.14.32 +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/gofiber/schema v1.6.0 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/tinylib/msgp v1.4.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.65.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8253a09 --- /dev/null +++ b/go.sum @@ -0,0 +1,57 @@ +github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= +github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gofiber/fiber/v3 v3.0.0-rc.1 h1:034MxesK6bqGkidP+QR+Ysc1ukOacBWOHCarCKC1xfg= +github.com/gofiber/fiber/v3 v3.0.0-rc.1/go.mod h1:hFdT00oT0XVuQH1/z2i5n1pl/msExHDUie1SsLOkCuM= +github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY= +github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s= +github.com/gofiber/utils/v2 v2.0.0-rc.1 h1:b77K5Rk9+Pjdxz4HlwEBnS7u5nikhx7armQB8xPds4s= +github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg93912R/tbKWhn6V3E= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shamaton/msgpack/v2 v2.3.0 h1:eawIa7lQmwRv0V6rdmL/5Ev9KdJHk07eQH3ceJi3BUw= +github.com/shamaton/msgpack/v2 v2.3.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= +github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= +github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/apis/tmdb.go b/internal/apis/tmdb.go new file mode 100644 index 0000000..bb985ce --- /dev/null +++ b/internal/apis/tmdb.go @@ -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 +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..1913d02 --- /dev/null +++ b/internal/database/database.go @@ -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) +} diff --git a/internal/database/movies.sql b/internal/database/movies.sql new file mode 100644 index 0000000..d9921f6 --- /dev/null +++ b/internal/database/movies.sql @@ -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 = ?; diff --git a/internal/database/schema.sql b/internal/database/schema.sql new file mode 100644 index 0000000..0788edd --- /dev/null +++ b/internal/database/schema.sql @@ -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"); diff --git a/internal/middleware/apikeychecker/apikeychecker.go b/internal/middleware/apikeychecker/apikeychecker.go new file mode 100644 index 0000000..0bec017 --- /dev/null +++ b/internal/middleware/apikeychecker/apikeychecker.go @@ -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() + } +} diff --git a/internal/server/index.go b/internal/server/index.go new file mode 100644 index 0000000..56f0317 --- /dev/null +++ b/internal/server/index.go @@ -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)) +} diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..c3624d6 --- /dev/null +++ b/internal/server/routes.go @@ -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) +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..cba90d1 --- /dev/null +++ b/internal/server/server.go @@ -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 +} diff --git a/internal/server/util.go b/internal/server/util.go new file mode 100644 index 0000000..68f667d --- /dev/null +++ b/internal/server/util.go @@ -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 +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..4f04ffd --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "internal/database/movies.sql" + schema: "internal/database/schema.sql" + gen: + go: + package: "database" + out: "internal/database"