mirror of
https://github.com/Itsig0/pocketmovie.git
synced 2026-01-22 08:24:38 +00:00
+ baseline to the git
This commit is contained in:
51
.air.toml
Normal file
51
.air.toml
Normal file
@@ -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
|
||||
2
.env.sample
Normal file
2
.env.sample
Normal file
@@ -0,0 +1,2 @@
|
||||
PORT=3000
|
||||
TMDB_API_KEY=YOUR_KEY
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.env
|
||||
data/
|
||||
tmp/
|
||||
main
|
||||
pico.min.css
|
||||
datastar.js
|
||||
*_templ.go
|
||||
db.go
|
||||
models.go
|
||||
*.sql.go
|
||||
jersey15*
|
||||
12
Makefile
Normal file
12
Makefile
Normal file
@@ -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
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# PocketMovie
|
||||
|
||||
A small database to keep track of the movies you have seen or want to see.
|
||||
36
cmd/api/main.go
Normal file
36
cmd/api/main.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
39
cmd/web/apiinput.templ
Normal file
39
cmd/web/apiinput.templ
Normal file
@@ -0,0 +1,39 @@
|
||||
package web
|
||||
|
||||
templ Apikeyinput(apierror string) {
|
||||
@Html("Please input your TMDB Api key - PocketMovie") {
|
||||
<div class="flex-center" style="height:100vh">
|
||||
<div style="width: 1000px">
|
||||
<h1>API Read Access Token needed</h1>
|
||||
<p>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.</p>
|
||||
<p>It is needed to search movies, pull metadata and stream providers.</p>
|
||||
<form method="POST">
|
||||
<fieldset
|
||||
if apierror != "" {
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-helper"
|
||||
}
|
||||
role="group"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="apikey"
|
||||
placeholder="API Read Access Token"
|
||||
required
|
||||
if apierror != "" {
|
||||
aria-invalid="true"
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value="Submit"
|
||||
/>
|
||||
</fieldset>
|
||||
if apierror != "" {
|
||||
<small id="invalid-helper">{ apierror }</small>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
80
cmd/web/assets/css/catppuccin.css
Normal file
80
cmd/web/assets/css/catppuccin.css
Normal file
@@ -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%;
|
||||
}
|
||||
10
cmd/web/assets/css/fonts.css
Normal file
10
cmd/web/assets/css/fonts.css
Normal file
@@ -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+ */
|
||||
}
|
||||
182
cmd/web/assets/css/main.css
Normal file
182
cmd/web/assets/css/main.css
Normal file
@@ -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,<svg viewBox="0 0 24 24" fill="%231C274C" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M9.15316 5.40838C10.4198 3.13613 11.0531 2 12 2C12.9469 2 13.5802 3.13612 14.8468 5.40837L15.1745 5.99623C15.5345 6.64193 15.7144 6.96479 15.9951 7.17781C16.2757 7.39083 16.6251 7.4699 17.3241 7.62805L17.9605 7.77203C20.4201 8.32856 21.65 8.60682 21.9426 9.54773C22.2352 10.4886 21.3968 11.4691 19.7199 13.4299L19.2861 13.9372C18.8096 14.4944 18.5713 14.773 18.4641 15.1177C18.357 15.4624 18.393 15.8341 18.465 16.5776L18.5306 17.2544C18.7841 19.8706 18.9109 21.1787 18.1449 21.7602C17.3788 22.3417 16.2273 21.8115 13.9243 20.7512L13.3285 20.4768C12.6741 20.1755 12.3469 20.0248 12 20.0248C11.6531 20.0248 11.3259 20.1755 10.6715 20.4768L10.0757 20.7512C7.77268 21.8115 6.62118 22.3417 5.85515 21.7602C5.08912 21.1787 5.21588 19.8706 5.4694 17.2544L5.53498 16.5776C5.60703 15.8341 5.64305 15.4624 5.53586 15.1177C5.42868 14.773 5.19043 14.4944 4.71392 13.9372L4.2801 13.4299C2.60325 11.4691 1.76482 10.4886 2.05742 9.54773C2.35002 8.60682 3.57986 8.32856 6.03954 7.77203L6.67589 7.62805C7.37485 7.4699 7.72433 7.39083 8.00494 7.17781C8.28555 6.96479 8.46553 6.64194 8.82547 5.99623L9.15316 5.40838Z" ></path></g></svg>');
|
||||
}
|
||||
}
|
||||
60
cmd/web/assets/js/modal.js
Normal file
60
cmd/web/assets/js/modal.js
Normal file
@@ -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;
|
||||
};
|
||||
156
cmd/web/base.templ
Normal file
156
cmd/web/base.templ
Normal file
@@ -0,0 +1,156 @@
|
||||
package web
|
||||
|
||||
import "strings"
|
||||
import "git.itsigo.dev/istigo/pocketmovie/internal/apis"
|
||||
import "fmt"
|
||||
|
||||
templ Html(title string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎬</text></svg>"/>
|
||||
<link rel="stylesheet" href="/assets/css/pico.min.css"/>
|
||||
<link rel="stylesheet" href="/assets/css/main.css"/>
|
||||
<script type="module" src="/assets/js/datastar.js"></script>
|
||||
<script src="/assets/js/modal.js"></script>
|
||||
<title>{ title }</title>
|
||||
</head>
|
||||
<body>
|
||||
{ children... }
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ Base(title string) {
|
||||
@Html(title) {
|
||||
<header>
|
||||
@header()
|
||||
</header>
|
||||
<main class="container">
|
||||
{ children... }
|
||||
</main>
|
||||
// <footer>
|
||||
// @footer()
|
||||
// </footer>
|
||||
<dialog
|
||||
id="search-dialog"
|
||||
data-signals="{id: ''}"
|
||||
data-on-keydown__window="
|
||||
if(evt.key !== 'Escape' || visibleModal === null) return;
|
||||
closeModal(visibleModal);
|
||||
$id = '';
|
||||
"
|
||||
>
|
||||
<article
|
||||
data-on-click__outside="
|
||||
if (visibleModal === null) return;
|
||||
closeModal(visibleModal);
|
||||
$id = '';
|
||||
"
|
||||
>
|
||||
<header>
|
||||
<button
|
||||
aria-label="Close"
|
||||
rel="prev"
|
||||
data-target="search-dialog"
|
||||
data-on-click="$id = ''; toggleModal(event); "
|
||||
></button>
|
||||
<p>
|
||||
<strong>🎬 Add Movie</strong>
|
||||
</p>
|
||||
</header>
|
||||
<div data-show="$id == ''" id="search-container">
|
||||
<form data-on-submit="@post('/apis/tmdb/searchMovie', {contentType: 'form'})" role="search">
|
||||
<input type="search" id="search" name="search" placeholder="Search"/>
|
||||
<input type="submit" value="Search"/>
|
||||
</form>
|
||||
<table class="striped">
|
||||
<tbody id="search-results"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div data-show="$id != ''" id="add-options">
|
||||
@AddMovieConfig()
|
||||
</div>
|
||||
</article>
|
||||
</dialog>
|
||||
}
|
||||
}
|
||||
|
||||
templ header() {
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><strong style="font-family: 'Jersey 15'; font-size: 2rem">Pocket Movie</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/">Movies</a></li>
|
||||
<li><a href="/watchlist">Watchlist</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ SearchMovie(movies []apis.Movie) {
|
||||
<tbody id="search-results">
|
||||
for _, v := range movies {
|
||||
<tr>
|
||||
<td
|
||||
class="add-movie"
|
||||
data-on-click={ fmt.Sprintf("$id = '%d'", v.Id) }
|
||||
>
|
||||
{ v.Title }
|
||||
if v.ReleaseDate != "" {
|
||||
({ strings.Split(v.ReleaseDate, "-")[0] })
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
|
||||
templ AddMovieConfig() {
|
||||
<form
|
||||
id="addMovieForm"
|
||||
class="add-movie"
|
||||
data-on-submit="@post('/db/addMovie/' + $id, {contentType: 'form'}); closeModal(visibleModal)"
|
||||
>
|
||||
<fieldset>
|
||||
<legend>
|
||||
General
|
||||
</legend>
|
||||
<input type="checkbox" name="watched" id="watched"/>
|
||||
<label style="padding-bottom: 0.5rem" htmlFor="watched">Watched</label>
|
||||
<div class="grid">
|
||||
<input type="number" name="watchcount" placeholder="Watchcount" aria-label="watchcount"/>
|
||||
<input type="number" name="rating" placeholder="Rating" aria-label="rating"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
Physical data
|
||||
</legend>
|
||||
<input type="checkbox" name="owned" id="owned"/>
|
||||
<label>Owned</label>
|
||||
<input type="checkbox" name="ripped" id="ripped"/>
|
||||
<label>Ripped</label>
|
||||
</fieldset>
|
||||
<select name="version" id="version" aria-label="Select physical version..">
|
||||
@MediaSelectOptions()
|
||||
</select>
|
||||
<input id="submitMovieButton" type="submit" value="Add Movie"/>
|
||||
</form>
|
||||
}
|
||||
|
||||
templ addMovieButton() {
|
||||
<button id="show-modal-dialog" data-target="search-dialog" onCLick="toggleModal(event);" type="button">Add Movie</button>
|
||||
}
|
||||
|
||||
templ MediaSelectOptions() {
|
||||
<option selected disabled>Select..</option>
|
||||
<option value="">None</option>
|
||||
<option value="DVD">DVD</option>
|
||||
<option value="BD">BD</option>
|
||||
<option value="4KBD">4KBD</option>
|
||||
<option value="DL">DL</option>
|
||||
}
|
||||
6
cmd/web/efs.go
Normal file
6
cmd/web/efs.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets/*
|
||||
var Files embed.FS
|
||||
138
cmd/web/home.templ
Normal file
138
cmd/web/home.templ
Normal file
@@ -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") {
|
||||
//<pre id="data"></pre>
|
||||
//<script>
|
||||
// var data = JSON.parse({{ data }});
|
||||
// var formated_data = JSON.stringify(data, null, 2);
|
||||
// document.getElementById("data").textContent = formated_data;
|
||||
//</script>
|
||||
@addMovieButton()
|
||||
{{ list, grid := "1", "" }}
|
||||
if getConfVal(settings, "HOME_GRID_VIEW" ) == "true" {
|
||||
{{ list, grid = "", "1" }}
|
||||
}
|
||||
<div
|
||||
data-signals={ fmt.Sprintf("{list : '%s', grid : '%s'}", list, grid) }
|
||||
style="float:right"
|
||||
>
|
||||
<button
|
||||
data-show="$list != ''"
|
||||
data-on-click="
|
||||
@post('/db/updateTableView')
|
||||
$list = '';
|
||||
$grid = '1'
|
||||
"
|
||||
>List ☰</button>
|
||||
<button
|
||||
data-show="$grid != ''"
|
||||
data-on-click="
|
||||
@post('/db/updateTableView')
|
||||
$list = '1';
|
||||
$grid = ''
|
||||
"
|
||||
>Grid 田</button>
|
||||
</div>
|
||||
if grid != "" {
|
||||
@MovieTiles(movies)
|
||||
} else {
|
||||
@MovieList(movies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ MovieList(movies []database.Movie) {
|
||||
<table id="movie-list" class="striped">
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Year</th>
|
||||
<th>Rating</th>
|
||||
<th>Watched</th>
|
||||
<th>Owned</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
for _, item := range movies {
|
||||
<tr>
|
||||
<td><a href={ fmt.Sprintf("/movie/%d", item.ID) }>{ item.Title }</a></td>
|
||||
<td>{ strings.Split(item.Year, "-")[0] }</td>
|
||||
<td>
|
||||
<div class="listRating">
|
||||
{{ class := "icon star-full" }}
|
||||
for i := range(5) {
|
||||
<i
|
||||
if int64(i) < item.Rating {
|
||||
class={ class + " rated" }
|
||||
} else {
|
||||
class={ class }
|
||||
}
|
||||
></i>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
{{ statusClasses := "bg-red" }}
|
||||
if item.Status == 1 {
|
||||
{{ statusClasses = "bg-green" }}
|
||||
}
|
||||
<td
|
||||
class={ statusClasses }
|
||||
>
|
||||
{ item.Status != 0 }
|
||||
</td>
|
||||
{{ ownedClasses := "bg-red" }}
|
||||
if item.Owned == 1 {
|
||||
{{ ownedClasses = "bg-green" }}
|
||||
}
|
||||
<td
|
||||
class={ ownedClasses }
|
||||
>
|
||||
{ item.Owned != 0 }
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class="red-link"
|
||||
href="#"
|
||||
data-on-click={ fmt.Sprintf("confirm('Are you sure?') && @delete('/db/deleteMovie/%d')", item.ID) }
|
||||
>Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
||||
templ MovieTiles(movies []database.Movie) {
|
||||
<div id="movie-list" class="movie-grid">
|
||||
for _, item := range movies {
|
||||
<a
|
||||
class="movie-tile"
|
||||
data-tooltip={ item.Title }
|
||||
data-placement="bottom"
|
||||
href={ fmt.Sprintf("/movie/%d", item.ID) }
|
||||
title={ item.Title }
|
||||
style="position:relative"
|
||||
>
|
||||
<div>
|
||||
if item.Status == 1 {
|
||||
@fullEye()
|
||||
} else {
|
||||
@closedEye()
|
||||
}
|
||||
</div>
|
||||
<img alt={ item.Title } src={ fmt.Sprintf("/movie/posters/%d.jpg", item.ID) }/>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
200
cmd/web/moviedetails.templ
Normal file
200
cmd/web/moviedetails.templ
Normal file
File diff suppressed because one or more lines are too long
95
cmd/web/settings.templ
Normal file
95
cmd/web/settings.templ
Normal file
@@ -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") {
|
||||
<h1>Settings</h1>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h3>Stream Region</h3>
|
||||
@Region(settings)
|
||||
<h3>Stream Providers</h3>
|
||||
@ProviderTable(settings)
|
||||
</div>
|
||||
<div>
|
||||
<h3>API Token</h3>
|
||||
@ApiKey(settings)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ ApiKey(settings SettingsConfig) {
|
||||
<form id="apikeyinput">
|
||||
<fieldset role="group">
|
||||
<input type="text" value={ settings.APIKey } name="apikey" placeholder="TMDB Read Access Token"/>
|
||||
<input type="submit" value="Change" data-on-click="@post('/db/updateApiKey', {contentType: 'form'})"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
|
||||
templ ProviderTable(settings SettingsConfig) {
|
||||
<div id="providers">
|
||||
<table class="striped">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
for _, p := range(settings.Providers) {
|
||||
<tr>
|
||||
<td>{ p.Title }</td>
|
||||
<td class="center">
|
||||
<a
|
||||
class="red-link"
|
||||
href="#"
|
||||
data-on-click={ fmt.Sprintf("confirm('Are you sure?') && @delete('/db/deleteStreamingService/%d')", p.ID) }
|
||||
>Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<form>
|
||||
<fieldset role="group">
|
||||
<select name="service" aria-label="Services" required>
|
||||
<option selected disabled value="">Select</option>
|
||||
for _, p := range(settings.AvailableProviders) {
|
||||
<option>{ p }</option>
|
||||
}
|
||||
</select>
|
||||
<input type="submit" value="Add" data-on-click="@post('/db/addStreamingService', {contentType: 'form'})"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Region(settings SettingsConfig) {
|
||||
<form id="regionselector">
|
||||
<fieldset role="group">
|
||||
<select
|
||||
data-on-change={ fmt.Sprintf("@post('/db/updateRegion', {contentType: 'form'})") }
|
||||
name="region"
|
||||
aria-label="Regions"
|
||||
>
|
||||
for _, r := range(settings.Regions) {
|
||||
<option
|
||||
if settings.SelectedRegion == r.Iso {
|
||||
selected
|
||||
}
|
||||
value={ r.Iso }
|
||||
>{ r.EnglishName }</option>
|
||||
}
|
||||
</select>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
52
cmd/web/watchlist.templ
Normal file
52
cmd/web/watchlist.templ
Normal file
@@ -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()
|
||||
<button
|
||||
data-on-click="@post('/db/updateStreamingServices')"
|
||||
data-tooltip="Refresh streaming services"
|
||||
type="button"
|
||||
>
|
||||
@refresh()
|
||||
</button>
|
||||
@List(movies)
|
||||
}
|
||||
}
|
||||
|
||||
templ List(movies []database.Movie) {
|
||||
<table id="movie-list" class="striped">
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Year</th>
|
||||
<th>Genre</th>
|
||||
<th>Owned</th>
|
||||
<th>Streaming service</th>
|
||||
</tr>
|
||||
for _, item := range movies {
|
||||
<tr>
|
||||
<td><a href={ fmt.Sprintf("/movie/%d", item.ID) }>{ item.Title }</a></td>
|
||||
<td>{ strings.Split(item.Year, "-")[0] }</td>
|
||||
<td>{ item.Genre }</td>
|
||||
{{ ownedClasses := "bg-red" }}
|
||||
if item.Owned == 1 {
|
||||
{{ ownedClasses = "bg-green" }}
|
||||
}
|
||||
<td
|
||||
class={ ownedClasses }
|
||||
>
|
||||
{ item.Owned != 0 }
|
||||
</td>
|
||||
<td>{ item.StreamingServices }</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
||||
templ refresh() {
|
||||
<svg width="1em" height="1em" viewBox="0 0 24 24" stroke="var(--pico-primary-inverse)" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M12 21C16.9706 21 21 16.9706 21 12C21 9.69494 20.1334 7.59227 18.7083 6L16 3M12 3C7.02944 3 3 7.02944 3 12C3 14.3051 3.86656 16.4077 5.29168 18L8 21M21 3H16M16 3V8M3 21H8M8 21V16" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
||||
}
|
||||
28
go.mod
Normal file
28
go.mod
Normal file
@@ -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
|
||||
)
|
||||
57
go.sum
Normal file
57
go.sum
Normal file
@@ -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=
|
||||
178
internal/apis/tmdb.go
Normal file
178
internal/apis/tmdb.go
Normal 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
|
||||
}
|
||||
32
internal/database/database.go
Normal file
32
internal/database/database.go
Normal 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)
|
||||
}
|
||||
73
internal/database/movies.sql
Normal file
73
internal/database/movies.sql
Normal 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 = ?;
|
||||
51
internal/database/schema.sql
Normal file
51
internal/database/schema.sql
Normal 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");
|
||||
28
internal/middleware/apikeychecker/apikeychecker.go
Normal file
28
internal/middleware/apikeychecker/apikeychecker.go
Normal 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
40
internal/server/index.go
Normal 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
357
internal/server/routes.go
Normal 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
23
internal/server/server.go
Normal 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
70
internal/server/util.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user