Convert to go
This commit is contained in:
+3
-3
@@ -1,4 +1,4 @@
|
||||
node_modules/
|
||||
output/
|
||||
posters/
|
||||
*.sqlite
|
||||
.vscode
|
||||
.vscode
|
||||
main
|
||||
@@ -1,19 +1,19 @@
|
||||
# List App
|
||||
|
||||
A web application for managing and organizing lists of movies, series, and games. The app allows users to add, view, and delete media items, with support for sorting and filtering.
|
||||
A web application for managing and organizing lists of movies, series, and games. The app allows users to add and view media items, with support for sorting and filtering.
|
||||
|
||||
## Features
|
||||
|
||||
- **Media Management**: Add, view, and delete movies, series, and games
|
||||
- **Media Management**: Add and view movies, series, and games
|
||||
- **Sorting Options**: Sort by title, year, or date added
|
||||
- **Image Caching**: Automatically downloads and caches poster images
|
||||
- **Authentication**: Password-protected actions for adding/deleting media
|
||||
- **Authentication**: Password-protected for adding media
|
||||
- **Responsive Design**: Works on mobile and desktop devices
|
||||
- **No client-side JavaScript**: Pages are fully rendered server-side; the only script on the page is Bootstrap's own bundle (for the collapsible nav/dropdown), not anything authored by this app
|
||||
|
||||
## Technologies
|
||||
|
||||
- **Backend**: TypeScript, Express, Bun runtime
|
||||
- **Frontend**: TypeScript, Bootstrap 5
|
||||
- **Backend**: Go, standard library `net/http` and `html/template`, `modernc.org/sqlite` (pure Go, cgo-free)
|
||||
- **Database**: SQLite
|
||||
- **APIs**: OMDB API (movies/series), Twitch IGDB API (games)
|
||||
|
||||
@@ -21,43 +21,39 @@ A web application for managing and organizing lists of movies, series, and games
|
||||
|
||||
```
|
||||
list_app/
|
||||
├── backend/ # Backend server code
|
||||
│ ├── controllers/ # Business logic
|
||||
│ ├── models/ # Database models
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── miscellaneous/ # Utilities and middleware
|
||||
│ └── app.ts # Main server entry
|
||||
├── controllers/ # Business logic (OMDB/IGDB lookups, poster downloads)
|
||||
├── models/ # Database models
|
||||
├── routes/ # Route registration
|
||||
├── web/ # Server-rendered page + HTML form handling
|
||||
│ ├── page.html # Go html/template for the page
|
||||
│ ├── page.go # Grouping/sorting logic, GET / and POST /add handlers
|
||||
│ └── templates.go # go:embed for page.html
|
||||
├── db/ # DB connection and schema
|
||||
├── main.go # Main server entry
|
||||
│
|
||||
├── frontend/ # Frontend code
|
||||
│ ├── list/ # Main list components
|
||||
│ ├── utils/ # Type definitions
|
||||
│ └── elementcreate.tsx # Custom element creation
|
||||
├── public/ # Static assets, embedded into the Go binary
|
||||
│ ├── embed.go # go:embed directive
|
||||
│ ├── logo.ico # Favicon
|
||||
│ └── no_poster.jpg # Fallback poster image
|
||||
│
|
||||
├── public/ # Static files
|
||||
│ ├── index.html # Main HTML file
|
||||
│ ├── logo.ico # Favicon
|
||||
│ └── no_poster.jpg # Fallback poster image
|
||||
│
|
||||
├── package.json # Project dependencies
|
||||
└── README.md # This file
|
||||
├── go.mod # Go module
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install Bun runtime: https://bun.sh/
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
3. Set up your API keys in the database:
|
||||
1. Install Go: https://go.dev/
|
||||
2. Set up your API keys in the database:
|
||||
- OMDB API key for movies/series
|
||||
- Twitch client ID and secret for games
|
||||
|
||||
## API Endpoints
|
||||
## Routes
|
||||
|
||||
- `GET /api/media/:mediaType` - List all media of a type (movies, series, games)
|
||||
- `POST /api/media/:mediaType` - Add new media (requires password)
|
||||
- `DELETE /api/media/:mediaType` - Delete media (requires password)
|
||||
- `GET /` - Renders the media list page. Query params: `listType` (`movies`, `series`, `games`; defaults to `movies`) and `sortType` (`title`, `year`, `id`; defaults to `title`)
|
||||
- `POST /add` - Plain HTML form submission to add media (`pass`, `code`, plus hidden `listType`/`sortType` to redirect back to the right view). Always redirects back to `/` with `?error=...` set on failure
|
||||
- `GET /api/media/:mediaType` - JSON listing of all media of a type (movies, series, games); not used by the page itself, kept as a small read-only API
|
||||
|
||||
There is no delete endpoint or UI — removing media isn't supported; use `sqlite3 mydb.sqlite` directly if you need to remove an entry.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -68,18 +64,18 @@ The application uses SQLite for data storage. The database file `mydb.sqlite` wi
|
||||
- `games` - Stores game information
|
||||
- `userData` - Stores configuration including API keys and password
|
||||
|
||||
The compiled binary is self-contained: `logo.ico` and `no_poster.jpg` are embedded into it via `public/embed.go`, and the page markup lives in a Go `html/template` embedded via `web/templates.go` — so only the binary (plus the SQLite file) needs to be deployed. Poster images are fetched from OMDB/IGDB at runtime and can't be embedded, so they're cached on disk under a `posters/` directory created next to wherever the binary runs, and served at `/poster/...`. Whether a poster has actually downloaded yet is checked server-side on each page render — if it's missing, the fallback image is used directly in the rendered `<img src>`, so no client-side JS is needed for that either.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to `http://localhost:4080` in your browser
|
||||
2. Use the navigation buttons to switch between movies, series, and games
|
||||
2. Use the navigation links to switch between movies, series, and games
|
||||
3. Use the "Sort" dropdown to change sorting method
|
||||
4. Enter your password and media ID in the form to add new items
|
||||
5. Click "Edit" then use delete buttons to remove items
|
||||
4. Enter your password and media ID in the form and submit to add new items
|
||||
|
||||
## Building
|
||||
|
||||
To build the frontend:
|
||||
To build the binary at `output/app`:
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import express from "express";
|
||||
|
||||
const hostname = '127.0.0.1';
|
||||
const httpPort = 4080;
|
||||
|
||||
const app = express();
|
||||
|
||||
// import morgan from 'morgan'
|
||||
// app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static('public'));
|
||||
|
||||
import apiRouter from './routes/api/apiRouter';
|
||||
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
app.listen(httpPort, () => {
|
||||
console.log(`Server running at http://${hostname}:${httpPort}/`);
|
||||
});
|
||||
|
||||
import mediaController from "./controllers/mediaController";
|
||||
|
||||
await mediaController.checkImages();
|
||||
@@ -1,252 +0,0 @@
|
||||
import { type Request, type Response } from "express";
|
||||
import UserModel, { values } from '../models/userModel';
|
||||
import MediaModel, { Table, Media } from '../models/mediaModel';
|
||||
|
||||
interface omdbRes {
|
||||
Title: string,
|
||||
Released: string,
|
||||
Response: string,
|
||||
Poster: string,
|
||||
Type: string,
|
||||
imdbID: string,
|
||||
Year: string,
|
||||
}
|
||||
|
||||
function fromStringToTable(input: string | string[]): (Table | undefined) {
|
||||
var value = "";
|
||||
if (Array.isArray(input)) {
|
||||
if (input.length > 0)
|
||||
value = input[0];
|
||||
}
|
||||
else {
|
||||
value = input;
|
||||
}
|
||||
if (value.localeCompare("games") == 0) return Table.games;
|
||||
if (value.localeCompare("movies") == 0) return Table.movies;
|
||||
if (value.localeCompare("series") == 0) return Table.series;
|
||||
return;
|
||||
}
|
||||
|
||||
async function downloadImage(mData: Media, type: Table) {
|
||||
// Specify the path where you want to save the image
|
||||
const outputPath = '/poster/' + type + '/' + mData.code + '.jpg';
|
||||
|
||||
// Use Bun's built-in fetch to download the image
|
||||
const response = await fetch(mData.poster);
|
||||
|
||||
// Check if the request was successful
|
||||
if (!response.ok) {
|
||||
console.log("fetch image error");
|
||||
console.log(mData.title);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the response to a blob
|
||||
const imageBlob = await response.blob();
|
||||
// Use Bun's write to save the image to a file
|
||||
await Bun.write('./public/' + outputPath, await imageBlob.arrayBuffer());
|
||||
MediaModel.updateWebImg(type, mData.code, outputPath);
|
||||
|
||||
}
|
||||
|
||||
async function createMed(req: Request, res: Response) {
|
||||
const mediaCode: string = req.body.code;
|
||||
|
||||
const omdb_key = UserModel.getValue(values.omdb_key);
|
||||
|
||||
if (!omdb_key) {
|
||||
return res.status(500).json({ message: 'Error when creating media' });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const uri = `http://www.omdbapi.com/?i=${mediaCode}&apikey=${omdb_key}`;
|
||||
const mJson = await fetch(uri);
|
||||
const mData: omdbRes = await mJson.json();
|
||||
|
||||
if (mData.Response == 'False') {
|
||||
return res.status(404).json({ message: 'wrong code' });
|
||||
}
|
||||
|
||||
const media: Media = {
|
||||
id: 0,
|
||||
code: mData.imdbID,
|
||||
title: mData.Title,
|
||||
released: mData.Released,
|
||||
webImg: "",
|
||||
poster: mData.Poster,
|
||||
year: mData.Year
|
||||
};
|
||||
|
||||
var tableType = Table.series;
|
||||
|
||||
if (mData.Type.localeCompare("movie") == 0) {
|
||||
tableType = Table.movies;
|
||||
}
|
||||
|
||||
const found = MediaModel.findOne(tableType, mediaCode);
|
||||
if (found.length != 0) {
|
||||
res.status(409).json({ message: 'Media already exists' });
|
||||
await downloadImage(media, tableType);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const savedMedia = MediaModel.save(tableType, mData.imdbID, mData.Title, mData.Released, "", mData.Poster, mData.Year);
|
||||
await downloadImage(media, tableType);
|
||||
|
||||
res.status(201).json(media);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ message: 'Error when creating media' });
|
||||
}
|
||||
}
|
||||
|
||||
async function createGame(req: Request, res: Response) {
|
||||
var gameCode = req.body.code;
|
||||
|
||||
const twitch_client_id = UserModel.getValue(values.twitch_client_id);
|
||||
const twitch_client_secret = UserModel.getValue(values.twitch_client_secret);
|
||||
|
||||
if (!twitch_client_id || !twitch_client_secret) {
|
||||
return res.status(500).json({ message: 'Error when creating game' });
|
||||
}
|
||||
|
||||
try {
|
||||
const gameFound = MediaModel.findOne(Table.games, gameCode);
|
||||
if (gameFound) {
|
||||
return res.status(409).json({ message: 'Game already exists' });
|
||||
}
|
||||
|
||||
const uri = "https://id.twitch.tv/oauth2/token?client_id=" + twitch_client_id + "&client_secret=" + twitch_client_secret + "&grant_type=client_credentials";
|
||||
var response = await fetch(uri, { method: 'POST' });
|
||||
const mData = await response.json();
|
||||
|
||||
const mheaders: HeadersInit = {
|
||||
'Accept': 'application/json',
|
||||
'Client-ID': twitch_client_id,
|
||||
'Authorization': 'Bearer ' + mData.access_token
|
||||
}
|
||||
|
||||
gameCode = parseInt(gameCode)
|
||||
|
||||
response = await fetch(
|
||||
"https://api.igdb.com/v4/games",
|
||||
{
|
||||
method: 'POST',
|
||||
headers: mheaders,
|
||||
body: `fields name, first_release_date; where id = ${gameCode};`
|
||||
}
|
||||
)
|
||||
const gameData = await response.json()
|
||||
if (gameData.length == 0) {
|
||||
return res.status(404).json({ message: 'wrong code' });
|
||||
}
|
||||
|
||||
const date = new Date(gameData[0].first_release_date * 1000);
|
||||
const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }
|
||||
const dateStr = date.toLocaleDateString(undefined, options);
|
||||
|
||||
|
||||
response = await fetch(
|
||||
"https://api.igdb.com/v4/covers",
|
||||
{
|
||||
method: 'POST',
|
||||
headers: mheaders,
|
||||
body: `fields image_id; where game = ${gameCode};`
|
||||
}
|
||||
)
|
||||
const coverData = await response.json()
|
||||
const game: Media = {
|
||||
id: 0,
|
||||
code: gameCode,
|
||||
title: gameData[0].name,
|
||||
released: dateStr,
|
||||
webImg: "",
|
||||
poster: `https://images.igdb.com/igdb/image/upload/t_cover_big/${coverData[0].image_id}.jpg`,
|
||||
year: date.getFullYear().toString(),
|
||||
};
|
||||
|
||||
const savedGame = MediaModel.save(Table.games, game.code, game.title, game.released, game.webImg, game.poster, game.year);
|
||||
await downloadImage(game, Table.games);
|
||||
return res.status(201).json(game);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
return res.status(500).json({ message: 'Error when creating game', error: error });
|
||||
}
|
||||
}
|
||||
|
||||
function list(req: Request, res: Response) {
|
||||
const mediaTable = fromStringToTable(req.params.mediaType);
|
||||
if (!mediaTable) {
|
||||
return res.status(500).json({
|
||||
message: 'Error when getting media.'
|
||||
});
|
||||
}
|
||||
|
||||
const media = MediaModel.find(mediaTable);
|
||||
return res.json(media);
|
||||
}
|
||||
|
||||
async function create(req: Request, res: Response) {
|
||||
const mediaCode: string = req.body.code;
|
||||
if (mediaCode.startsWith("tt")) {
|
||||
return await createMed(req, res);
|
||||
} else {
|
||||
return await createGame(req, res);
|
||||
}
|
||||
}
|
||||
function remove(req: Request, res: Response) {
|
||||
const mediaTable = fromStringToTable(req.params.mediaType);
|
||||
if (!mediaTable) {
|
||||
return res.status(500).json({
|
||||
message: 'Error when deleting the media.'
|
||||
});
|
||||
}
|
||||
|
||||
const code = req.body.code;
|
||||
|
||||
try {
|
||||
const media = MediaModel.findOneAndDelete(mediaTable, code);
|
||||
if (!media) {
|
||||
return res.status(404).json({ message: 'No such media' });
|
||||
}
|
||||
|
||||
return res.status(204).json();
|
||||
}
|
||||
catch (err) {
|
||||
return res.status(500).json({ message: 'Error when deleting the media.' });
|
||||
}
|
||||
}
|
||||
|
||||
async function checkImages() {
|
||||
await checkTableImages(Table.games);
|
||||
await checkTableImages(Table.movies);
|
||||
await checkTableImages(Table.series);
|
||||
}
|
||||
|
||||
function delay(time: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
async function checkTableImages(table: Table) {
|
||||
const list = MediaModel.find(table);
|
||||
|
||||
for (const element of list) {
|
||||
const path = "./public/" + element.webImg;
|
||||
const f = Bun.file(path);
|
||||
const exists = await f.exists();
|
||||
if (!exists) {
|
||||
console.log(element.title);
|
||||
await downloadImage(element, table);
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
list,
|
||||
create,
|
||||
remove,
|
||||
checkImages
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { type NextFunction, type Request, type Response } from "express";
|
||||
import userModel, { values } from 'backend/models/userModel';
|
||||
|
||||
function checkAuthenticated(req: Request, res: Response, next: NextFunction) {
|
||||
const pass = req.body.pass;
|
||||
const password = userModel.getValue(values.pass);
|
||||
if (pass && password) {
|
||||
if (pass == password) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
return res.status(500).json({ message: 'Error when getting transactions.' });
|
||||
}
|
||||
|
||||
export default checkAuthenticated;
|
||||
@@ -1,84 +0,0 @@
|
||||
import pool from 'backend/miscellaneous/db'
|
||||
|
||||
|
||||
export class Media {
|
||||
id!: number;
|
||||
code!: string
|
||||
title!: string;
|
||||
released!: string;
|
||||
webImg!: string;
|
||||
poster!: string;
|
||||
year!: string;
|
||||
}
|
||||
|
||||
export enum Table {
|
||||
movies = "movies",
|
||||
series = "series",
|
||||
games = "games",
|
||||
}
|
||||
|
||||
function save(table: Table, code: string, title: string, released:string, webImg:string, poster: string, year: string): number {
|
||||
try {
|
||||
const sql = "INSERT INTO " + table + " (code, title, released, webImg, poster, year) VALUES (?,?,?,?,?,?)";
|
||||
|
||||
const result = pool.query(sql).run(code, title, released, webImg, poster, year);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function updateWebImg(table: Table, code: string, webImg: string): number {
|
||||
try {
|
||||
const sql = "UPDATE " + table + " SET webImg = ? WHERE code = ?;";
|
||||
const result = pool.query(sql).run(webImg, code);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findOneAndDelete(table: Table, code: string): number {
|
||||
try {
|
||||
const result = pool.query("DELETE FROM " + table + " WHERE code = ?;").run(code);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findOne(table: Table, code: string): Media[] {
|
||||
try {
|
||||
const rows = pool.query("SELECT * FROM " + table + " WHERE code = ?;").as(Media).all(code);
|
||||
return rows;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function find(table: Table): Media[] {
|
||||
try {
|
||||
const rows = pool.query("SELECT * FROM " + table + ";").as(Media).all();
|
||||
return rows;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export default {
|
||||
save,
|
||||
updateWebImg,
|
||||
findOneAndDelete,
|
||||
findOne,
|
||||
find
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import pool from 'backend/miscellaneous/db'
|
||||
|
||||
class UserD {
|
||||
name?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export enum values {
|
||||
pass = 1,
|
||||
omdb_key,
|
||||
twitch_client_id,
|
||||
twitch_client_secret,
|
||||
}
|
||||
|
||||
|
||||
function getValue(name: values): string | undefined {
|
||||
try {
|
||||
const rows = pool.query("SELECT name, value FROM userData where id = ?;").as(UserD).all(name);
|
||||
if (rows.length > 0)
|
||||
return rows[0].value;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
getValue
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import express, { type Request, type Response } from "express";
|
||||
import mediaRouter from './mediaRouter';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/media', mediaRouter);
|
||||
|
||||
router.get('/', function (req: Request, res: Response) {
|
||||
res.status(200).json({ message: 'API is working' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,13 +0,0 @@
|
||||
import express from "express";
|
||||
import mediaController from '../../controllers/mediaController.js';
|
||||
import checkAuthenticated from '../../miscellaneous/checkAuthenticated.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:mediaType', mediaController.list);
|
||||
|
||||
router.post('/:mediaType', checkAuthenticated, mediaController.create);
|
||||
|
||||
router.delete('/:mediaType', checkAuthenticated, mediaController.remove);
|
||||
|
||||
export default router;
|
||||
@@ -1,5 +0,0 @@
|
||||
bun build ./backend/app.ts --outfile=output/app --target=bun --minify --compile
|
||||
bun build ./frontend/list/list.tsx --outfile=output/public/list.js --minify
|
||||
|
||||
cp -r public/* output/public/
|
||||
cp mydb.sqlite output/
|
||||
@@ -1,190 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "web",
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"bun-types": "^1.2.22",
|
||||
"express": "^5.1.0",
|
||||
"morgan": "~1.10.1",
|
||||
"typescript": "^5.9.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"list_app/models"
|
||||
)
|
||||
|
||||
// PostersDir is where dynamically fetched poster images are cached on disk.
|
||||
// Unlike the static frontend assets, these are downloaded at runtime and
|
||||
// can't be embedded into the binary, so they live in a real directory next
|
||||
// to it, served separately (see routes.New).
|
||||
const PostersDir = "posters"
|
||||
|
||||
func PosterDiskPath(webImg string) string {
|
||||
rel := strings.TrimPrefix(webImg, "/poster/")
|
||||
return filepath.Join(PostersDir, rel)
|
||||
}
|
||||
|
||||
type omdbRes struct {
|
||||
Title string `json:"Title"`
|
||||
Released string `json:"Released"`
|
||||
Response string `json:"Response"`
|
||||
Poster string `json:"Poster"`
|
||||
Type string `json:"Type"`
|
||||
ImdbID string `json:"imdbID"`
|
||||
Year string `json:"Year"`
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func fromStringToTable(input string) (models.Table, bool) {
|
||||
switch input {
|
||||
case "games":
|
||||
return models.Games, true
|
||||
case "movies":
|
||||
return models.Movies, true
|
||||
case "series":
|
||||
return models.Series, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func downloadImage(media models.Media, table models.Table) {
|
||||
outputPath := "/poster/" + string(table) + "/" + media.Code + ".jpg"
|
||||
|
||||
response, err := http.Get(media.Poster)
|
||||
if err != nil || !(response.StatusCode >= 200 && response.StatusCode < 300) {
|
||||
log.Println("fetch image error")
|
||||
log.Println(media.Title)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
imageBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
log.Println("fetch image error")
|
||||
log.Println(media.Title)
|
||||
return
|
||||
}
|
||||
|
||||
destPath := PosterDiskPath(outputPath)
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(destPath, imageBytes, 0644); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
models.UpdateWebImg(table, media.Code, outputPath)
|
||||
}
|
||||
|
||||
func createMed(mediaCode string) (models.Media, error) {
|
||||
omdbKey, ok := models.GetValue(models.OmdbKey)
|
||||
if !ok || omdbKey == "" {
|
||||
return models.Media{}, fmt.Errorf("error when creating media")
|
||||
}
|
||||
|
||||
uri := "http://www.omdbapi.com/?i=" + url.QueryEscape(mediaCode) + "&apikey=" + url.QueryEscape(omdbKey)
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating media")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var mData omdbRes
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mData); err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating media")
|
||||
}
|
||||
|
||||
if mData.Response == "False" {
|
||||
return models.Media{}, fmt.Errorf("wrong code")
|
||||
}
|
||||
|
||||
media := models.Media{
|
||||
ID: 0,
|
||||
Code: mData.ImdbID,
|
||||
Title: mData.Title,
|
||||
Released: mData.Released,
|
||||
WebImg: "",
|
||||
Poster: mData.Poster,
|
||||
Year: mData.Year,
|
||||
}
|
||||
|
||||
tableType := models.Series
|
||||
if strings.EqualFold(mData.Type, "movie") {
|
||||
tableType = models.Movies
|
||||
}
|
||||
|
||||
found := models.FindOne(tableType, mediaCode)
|
||||
if len(found) != 0 {
|
||||
downloadImage(media, tableType)
|
||||
return models.Media{}, fmt.Errorf("media already exists")
|
||||
}
|
||||
|
||||
models.Save(tableType, mData.ImdbID, mData.Title, mData.Released, "", mData.Poster, mData.Year)
|
||||
downloadImage(media, tableType)
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func createGame(gameCodeStr string) (models.Media, error) {
|
||||
twitchClientID, ok1 := models.GetValue(models.TwitchClientID)
|
||||
twitchClientSecret, ok2 := models.GetValue(models.TwitchClientSecret)
|
||||
if !ok1 || !ok2 || twitchClientID == "" || twitchClientSecret == "" {
|
||||
return models.Media{}, fmt.Errorf("error when creating game")
|
||||
}
|
||||
|
||||
gameFound := models.FindOne(models.Games, gameCodeStr)
|
||||
if len(gameFound) != 0 {
|
||||
return models.Media{}, fmt.Errorf("game already exists")
|
||||
}
|
||||
|
||||
tokenURI := "https://id.twitch.tv/oauth2/token?client_id=" + url.QueryEscape(twitchClientID) +
|
||||
"&client_secret=" + url.QueryEscape(twitchClientSecret) + "&grant_type=client_credentials"
|
||||
tokenResp, err := http.Post(tokenURI, "application/x-www-form-urlencoded", nil)
|
||||
if err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating game: %w", err)
|
||||
}
|
||||
defer tokenResp.Body.Close()
|
||||
|
||||
var tokenData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
if err := json.NewDecoder(tokenResp.Body).Decode(&tokenData); err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating game: %w", err)
|
||||
}
|
||||
|
||||
gameCode, err := strconv.Atoi(gameCodeStr)
|
||||
if err != nil {
|
||||
return models.Media{}, fmt.Errorf("wrong code")
|
||||
}
|
||||
|
||||
doIgdbRequest := func(path, body string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, "https://api.igdb.com/v4/"+path, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Client-ID", twitchClientID)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenData.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
gamesBody, err := doIgdbRequest("games", fmt.Sprintf("fields name, first_release_date; where id = %d;", gameCode))
|
||||
if err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating game: %w", err)
|
||||
}
|
||||
|
||||
var gameData []struct {
|
||||
Name string `json:"name"`
|
||||
FirstReleaseDate int64 `json:"first_release_date"`
|
||||
}
|
||||
if err := json.Unmarshal(gamesBody, &gameData); err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating game: %w", err)
|
||||
}
|
||||
if len(gameData) == 0 {
|
||||
return models.Media{}, fmt.Errorf("wrong code")
|
||||
}
|
||||
|
||||
releaseDate := time.Unix(gameData[0].FirstReleaseDate, 0).UTC()
|
||||
dateStr := releaseDate.Format("2 Jan 2006")
|
||||
|
||||
coversBody, err := doIgdbRequest("covers", fmt.Sprintf("fields image_id; where game = %d;", gameCode))
|
||||
if err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating game: %w", err)
|
||||
}
|
||||
|
||||
var coverData []struct {
|
||||
ImageID string `json:"image_id"`
|
||||
}
|
||||
if err := json.Unmarshal(coversBody, &coverData); err != nil {
|
||||
return models.Media{}, fmt.Errorf("error when creating game: %w", err)
|
||||
}
|
||||
|
||||
imageID := ""
|
||||
if len(coverData) > 0 {
|
||||
imageID = coverData[0].ImageID
|
||||
}
|
||||
|
||||
game := models.Media{
|
||||
ID: 0,
|
||||
Code: gameCodeStr,
|
||||
Title: gameData[0].Name,
|
||||
Released: dateStr,
|
||||
WebImg: "",
|
||||
Poster: fmt.Sprintf("https://images.igdb.com/igdb/image/upload/t_cover_big/%s.jpg", imageID),
|
||||
Year: strconv.Itoa(releaseDate.Year()),
|
||||
}
|
||||
|
||||
models.Save(models.Games, game.Code, game.Title, game.Released, game.WebImg, game.Poster, game.Year)
|
||||
downloadImage(game, models.Games)
|
||||
|
||||
return game, nil
|
||||
}
|
||||
|
||||
// Create adds a movie, series, or game by its OMDB (imdb "tt...") or IGDB
|
||||
// (numeric) code, dispatching to the right lookup based on the code's shape.
|
||||
func Create(code string) (models.Media, error) {
|
||||
if strings.HasPrefix(code, "tt") {
|
||||
return createMed(code)
|
||||
}
|
||||
return createGame(code)
|
||||
}
|
||||
|
||||
func List(w http.ResponseWriter, r *http.Request) {
|
||||
mediaTable, ok := fromStringToTable(r.PathValue("mediaType"))
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "Error when getting media."})
|
||||
return
|
||||
}
|
||||
|
||||
media := models.Find(mediaTable)
|
||||
writeJSON(w, http.StatusOK, media)
|
||||
}
|
||||
|
||||
func CheckImages() {
|
||||
checkTableImages(models.Games)
|
||||
checkTableImages(models.Movies)
|
||||
checkTableImages(models.Series)
|
||||
}
|
||||
|
||||
func checkTableImages(table models.Table) {
|
||||
list := models.Find(table)
|
||||
|
||||
for _, element := range list {
|
||||
path := PosterDiskPath(element.WebImg)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
continue
|
||||
}
|
||||
log.Println(element.Title)
|
||||
downloadImage(element, table)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,22 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
package db
|
||||
|
||||
const pool = new Database("mydb.sqlite", { strict: true });
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
pool.run(`
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var Pool *sql.DB
|
||||
|
||||
func init() {
|
||||
pool, err := sql.Open("sqlite", "mydb.sqlite")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
Pool = pool
|
||||
|
||||
mustExec(`
|
||||
CREATE TABLE IF NOT EXISTS series (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL,
|
||||
@@ -12,9 +26,9 @@ CREATE TABLE IF NOT EXISTS series (
|
||||
poster TEXT NOT NULL,
|
||||
year TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
`)
|
||||
|
||||
pool.run(`
|
||||
mustExec(`
|
||||
CREATE TABLE IF NOT EXISTS movies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL,
|
||||
@@ -24,9 +38,9 @@ CREATE TABLE IF NOT EXISTS movies (
|
||||
poster TEXT NOT NULL,
|
||||
year TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
`)
|
||||
|
||||
pool.run(`
|
||||
mustExec(`
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL,
|
||||
@@ -36,33 +50,44 @@ CREATE TABLE IF NOT EXISTS games (
|
||||
poster TEXT NOT NULL,
|
||||
year TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
`)
|
||||
|
||||
pool.run(`
|
||||
mustExec(`
|
||||
CREATE TABLE IF NOT EXISTS userData (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
`)
|
||||
|
||||
function inset_keys() {
|
||||
class co {
|
||||
count!: number;
|
||||
}
|
||||
const result = pool.query("SELECT count(*) as count FROM userData;").as(co).get();
|
||||
if(result && result.count >= 4){
|
||||
return;
|
||||
}
|
||||
|
||||
const stmt = pool.prepare("INSERT INTO userData (name, value) VALUES (?, ?)");
|
||||
stmt.run("pass", "");
|
||||
stmt.run("omdb_key", "");
|
||||
stmt.run("twitch_client_id", "");
|
||||
stmt.run("twitch_client_secret", "");
|
||||
stmt.finalize();
|
||||
insertKeys()
|
||||
}
|
||||
|
||||
inset_keys();
|
||||
func mustExec(query string) {
|
||||
if _, err := Pool.Exec(query); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
export default pool;
|
||||
func insertKeys() {
|
||||
var count int
|
||||
if err := Pool.QueryRow("SELECT count(*) FROM userData;").Scan(&count); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if count >= 4 {
|
||||
return
|
||||
}
|
||||
|
||||
stmt, err := Pool.Prepare("INSERT INTO userData (name, value) VALUES (?, ?)")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
stmt.Exec("pass", "")
|
||||
stmt.Exec("omdb_key", "")
|
||||
stmt.Exec("twitch_client_id", "")
|
||||
stmt.Exec("twitch_client_secret", "")
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/// <reference path="./utils/element-types.d.ts" />
|
||||
/// <reference path="./utils/events.d.ts" />
|
||||
/// <reference path="./utils/intrinsic-elements.d.ts" />
|
||||
|
||||
export interface Children {
|
||||
children?: AttributeValue;
|
||||
}
|
||||
|
||||
export interface CustomElementHandler {
|
||||
(attributes: Attributes, contents: (string | HTMLElement)[]): HTMLElement;
|
||||
}
|
||||
|
||||
export interface Attributes {
|
||||
[key: string]: AttributeValue;
|
||||
}
|
||||
|
||||
|
||||
export function createElement(
|
||||
tag: string | CustomElementHandler,
|
||||
attrs: Attributes & Children | undefined = {},
|
||||
...children: (string | HTMLElement)[]
|
||||
): HTMLElement {
|
||||
|
||||
if (typeof tag === "function") {
|
||||
if (attrs == null) {
|
||||
attrs = { num: 0 };
|
||||
}
|
||||
if (children == null) {
|
||||
children = [""];
|
||||
}
|
||||
return tag(attrs, children);
|
||||
}
|
||||
|
||||
const retElement = document.createElement(tag);
|
||||
|
||||
for (let name in attrs) {
|
||||
if (name && attrs.hasOwnProperty(name)) {
|
||||
|
||||
let value = attrs[name];
|
||||
if (typeof value === "number") {
|
||||
retElement.setAttribute(name, value.toString());
|
||||
} else if (typeof value === "function") {
|
||||
retElement.addEventListener(name.slice(2), value);
|
||||
}
|
||||
else {
|
||||
retElement.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 2; i < arguments.length; i++) {
|
||||
let child = arguments[i];
|
||||
|
||||
// check if child is a HTMLElement
|
||||
if (child.nodeType != undefined) {
|
||||
retElement.appendChild(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child instanceof Array) {
|
||||
for (let j = 0; j < child.length; j++) {
|
||||
if (child[j].nodeType != undefined) retElement.appendChild(child[j]);
|
||||
else retElement.appendChild(document.createTextNode(child[j].toString()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// child is a string
|
||||
retElement.appendChild(document.createTextNode(child.toString()));
|
||||
}
|
||||
return retElement;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { Attributes } from "frontend/elementcreate";
|
||||
import * as elements from "frontend/elementcreate";
|
||||
|
||||
function MediaElement(attributes: Attributes, contents: string[]) {
|
||||
const ret = <div class="col media-element" id={attributes['id']}>
|
||||
<div class='card shadow-sm'>
|
||||
<img class='card-img-top' src={attributes['webImg']} width='100%' onerror={attributes['imageError']} loading="lazy" style="min-height: 100px;"></img>
|
||||
<div class='card-body'>
|
||||
<h5 class='card-title'>{attributes['title']}</h5>
|
||||
<p class='card-text'>{attributes['released']}</p>
|
||||
<div class="d-none justify-content-between align-items-center">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick={attributes['fun']}>delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
function MyHeader(attributes: Attributes, contents: string[]) {
|
||||
return <div class="row">
|
||||
<div class='col'>
|
||||
<h2 class='text-center'>{attributes['title']} {attributes['num'] ? ": " + attributes['num'] : ""}</h2>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function MediaContainer(attributes: Attributes, contents: string[]) {
|
||||
return <div id={attributes['id']} class="row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-3">{contents[0]}</div>;
|
||||
}
|
||||
|
||||
export { MediaElement, MyHeader, MediaContainer }
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
function splitByTitle(movies: Array<Movie>): { [s: string]: Movie[]; } {
|
||||
const result = movies.reduce((r, a) => {
|
||||
var letter = a.title[0].toUpperCase();
|
||||
if (!isNaN(parseInt(letter))) letter = "#";
|
||||
r[letter] = r[letter] || [];
|
||||
r[letter].push(a);
|
||||
return r;
|
||||
}, Object.create(null));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function splitByYear(movies: Array<Movie>): { [s: string]: Movie[]; } {
|
||||
const result = movies.reduce((r, a) => {
|
||||
const year = new Date(a.released).getFullYear();
|
||||
r[year] = r[year] || [];
|
||||
r[year].push(a);
|
||||
return r;
|
||||
}, Object.create(null));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export { splitByTitle, splitByYear };
|
||||
@@ -1,287 +0,0 @@
|
||||
import { MediaElement, MyHeader, MediaContainer } from "frontend/list/elements";
|
||||
import { splitByTitle, splitByYear } from "frontend/list/functions";
|
||||
|
||||
import * as elements from "frontend/elementcreate";
|
||||
|
||||
var sortType = 0;
|
||||
var listType = 0;
|
||||
|
||||
const sortTypeTitle = 0;
|
||||
const sortTypeYear = 1;
|
||||
const sortTypeId = 2;
|
||||
|
||||
const moviesType = 0;
|
||||
const gamesType = 1;
|
||||
const seriesType = 2;
|
||||
|
||||
var listButtons: Array<HTMLElement | null> = [];
|
||||
var sortButtons: Array<HTMLElement | null> = [];
|
||||
|
||||
var root: HTMLElement | null;
|
||||
var editButton: HTMLElement | null;
|
||||
var movieElements: HTMLElement[] = [];
|
||||
|
||||
function getLink(): string {
|
||||
switch (listType) {
|
||||
case moviesType:
|
||||
return "/api/media/movies";
|
||||
case gamesType:
|
||||
return "/api/media/games";
|
||||
case seriesType:
|
||||
return "/api/media/series";
|
||||
}
|
||||
return "/api/media/movies";
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
try {
|
||||
const response = await fetch(getLink());
|
||||
const movies = await response.json();
|
||||
renderMedias(movies);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
function submitMedia(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
const pass = document.getElementById("pass") as HTMLInputElement | null;
|
||||
if (!pass) return;
|
||||
|
||||
const input_id = document.getElementById("input_id") as HTMLInputElement | null;
|
||||
if (!input_id) return;
|
||||
|
||||
|
||||
if (pass.value == "" || input_id.value == "") return;
|
||||
|
||||
|
||||
fetch(getLink(), {
|
||||
body: JSON.stringify({ pass: pass.value, code: input_id.value }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST"
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.status != 201) {
|
||||
const json = await response.json();
|
||||
console.log(json);
|
||||
alert(json.message);
|
||||
return;
|
||||
}
|
||||
|
||||
await reload();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
input_id.value = "";
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (searchParams.has("listType")) {
|
||||
switch (searchParams.get("listType")) {
|
||||
case "movies":
|
||||
listType = moviesType;
|
||||
break;
|
||||
case "series":
|
||||
listType = seriesType;
|
||||
break;
|
||||
case "games":
|
||||
listType = gamesType;
|
||||
break;
|
||||
default:
|
||||
listType = moviesType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (searchParams.has("sortType")) {
|
||||
switch (searchParams.get("sortType")) {
|
||||
case "title":
|
||||
sortType = sortTypeTitle;
|
||||
break;
|
||||
case "year":
|
||||
sortType = sortTypeYear;
|
||||
break;
|
||||
case "id":
|
||||
sortType = sortTypeId;
|
||||
break;
|
||||
default:
|
||||
sortType = sortTypeTitle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeType(type: number) {
|
||||
listType = type;
|
||||
loadPage();
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
switch (listType) {
|
||||
case moviesType:
|
||||
searchParams.set("listType", "movies");
|
||||
break;
|
||||
case gamesType:
|
||||
searchParams.set("listType", "games");
|
||||
break;
|
||||
case seriesType:
|
||||
searchParams.set("listType", "series");
|
||||
break;
|
||||
}
|
||||
history.replaceState({}, '', window.location.pathname + '?' + searchParams.toString());
|
||||
}
|
||||
|
||||
function changeSort(type: number) {
|
||||
sortType = type;
|
||||
loadPage();
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
switch (type) {
|
||||
case sortTypeTitle:
|
||||
searchParams.set("sortType", "title");
|
||||
break;
|
||||
case sortTypeYear:
|
||||
searchParams.set("sortType", "year");
|
||||
break;
|
||||
case sortTypeId:
|
||||
searchParams.set("sortType", "id");
|
||||
break;
|
||||
}
|
||||
history.replaceState({}, '', window.location.pathname + '?' + searchParams.toString());
|
||||
}
|
||||
|
||||
|
||||
function splitBySort(movies: Array<Movie>): { [s: string]: Movie[]; } {
|
||||
switch (sortType) {
|
||||
case sortTypeYear:
|
||||
const sorted = movies.sort((a, b) => {
|
||||
const ay = Date.parse(a.released);
|
||||
const by = Date.parse(b.released);
|
||||
return ay - by;
|
||||
});
|
||||
return splitByYear(sorted);
|
||||
case sortTypeId:
|
||||
movies.sort((a, b) => a.id < b.id ? 1 : -1);
|
||||
return { "added": movies };
|
||||
default:
|
||||
return splitByTitle(movies.sort((a, b) => a.title.localeCompare(b.title)));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEdit() {
|
||||
movieElements.forEach(element => {
|
||||
const div = element.querySelector(".d-none");
|
||||
if (!div) return;
|
||||
div.classList.remove("d-none");
|
||||
div.classList.add("d-flex");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
document.getElementById("myform")?.addEventListener("submit", submitMedia);
|
||||
|
||||
listButtons.push(document.getElementById("movieButton"));
|
||||
listButtons.push(document.getElementById("gameButton"));
|
||||
listButtons.push(document.getElementById("seriesButton"));
|
||||
listButtons.forEach((button, index) => button?.addEventListener("click", () => changeType(index)));
|
||||
|
||||
sortButtons.push(document.getElementById("titleButton"));
|
||||
sortButtons.push(document.getElementById("yearButton"));
|
||||
sortButtons.push(document.getElementById("idButton"));
|
||||
sortButtons.forEach((button, index) => button?.addEventListener("click", () => changeSort(index)));
|
||||
|
||||
editButton = document.getElementById("editButton");
|
||||
editButton?.addEventListener("click", () => toggleEdit());
|
||||
|
||||
loadState();
|
||||
loadPage();
|
||||
});
|
||||
|
||||
async function loadPage() {
|
||||
|
||||
listButtons.forEach(button => button?.classList.remove("active"));
|
||||
listButtons[listType]?.classList.add("active");
|
||||
|
||||
await reload();
|
||||
}
|
||||
|
||||
|
||||
function removeMedia(evt: Event) {
|
||||
const password = document.getElementById("pass") as HTMLInputElement | null;
|
||||
if (!password) return;
|
||||
if (password.value == "") return;
|
||||
|
||||
let elem = evt.target as HTMLElement | null;
|
||||
|
||||
while (elem && !elem.classList.contains('media-element')) {
|
||||
elem = elem.parentElement;
|
||||
}
|
||||
|
||||
if (!elem) return;
|
||||
const id = elem.id;
|
||||
|
||||
fetch(getLink(), {
|
||||
body: JSON.stringify({ pass: password.value, code: id }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "DELETE"
|
||||
})
|
||||
.then(async (response) => {
|
||||
|
||||
if (response.status != 204) {
|
||||
console.log("error");
|
||||
console.log(response.body);
|
||||
return;
|
||||
}
|
||||
document.getElementById(id)?.remove();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
password.value = "";
|
||||
}
|
||||
|
||||
function onImgError(evt: Event) {
|
||||
const imgT = evt.target as HTMLImageElement;
|
||||
imgT.src = "/no_poster.jpg";
|
||||
console.log(imgT.parentElement?.parentElement?.id);
|
||||
}
|
||||
|
||||
function renderMedias(unsorted_movies: Array<Movie>) {
|
||||
root = document.getElementById('root');
|
||||
if (!root) return;
|
||||
|
||||
root.innerHTML = "";
|
||||
movieElements = [];
|
||||
|
||||
const splitMovies = splitBySort(unsorted_movies);
|
||||
|
||||
let years;
|
||||
if (sortType == sortTypeTitle) {
|
||||
years = Object.keys(splitMovies).sort((a, b) => -b.localeCompare(a));
|
||||
} else {
|
||||
years = Object.keys(splitMovies).sort((a, b) => b.localeCompare(a));
|
||||
}
|
||||
|
||||
|
||||
root.appendChild(<MyHeader title={unsorted_movies.length} />);
|
||||
|
||||
for (const letter of years) {
|
||||
|
||||
const movies = splitMovies[letter];
|
||||
|
||||
const header = <MyHeader title={letter} num={movies.length} />;
|
||||
root.appendChild(header);
|
||||
|
||||
const row =
|
||||
<MediaContainer id={letter}>
|
||||
{movies.map(movie => {
|
||||
const med = <MediaElement webImg={movie.webImg} title={movie.title} released={movie.released} id={movie.code} fun={removeMedia} imageError={onImgError}></MediaElement>;
|
||||
movieElements.push(med);
|
||||
return med;
|
||||
})}
|
||||
</MediaContainer>;
|
||||
|
||||
root.appendChild(row);
|
||||
}
|
||||
}
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
interface Movie {
|
||||
title: string;
|
||||
released: string;
|
||||
code: string;
|
||||
webImg: string;
|
||||
id: string;
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
type AttributeValue = number | string | EventListener;
|
||||
Vendored
-341
@@ -1,341 +0,0 @@
|
||||
declare namespace JSX {
|
||||
interface HtmlTag {
|
||||
accesskey?: string;
|
||||
class?: string;
|
||||
contenteditable?: string;
|
||||
dir?: string;
|
||||
hidden?: string | boolean;
|
||||
id?: AttributeValue;
|
||||
role?: string;
|
||||
lang?: string;
|
||||
draggable?: string | boolean;
|
||||
spellcheck?: string | boolean;
|
||||
style?: string;
|
||||
tabindex?: string;
|
||||
title?: string;
|
||||
translate?: string | boolean;
|
||||
}
|
||||
interface HtmlAnchorTag extends HtmlTag {
|
||||
href?: string;
|
||||
target?: string;
|
||||
download?: string;
|
||||
ping?: string;
|
||||
rel?: string;
|
||||
media?: string;
|
||||
hreflang?: string;
|
||||
type?: string;
|
||||
}
|
||||
interface HtmlAreaTag extends HtmlTag {
|
||||
alt?: string;
|
||||
coords?: string;
|
||||
shape?: string;
|
||||
href?: string;
|
||||
target?: string;
|
||||
ping?: string;
|
||||
rel?: string;
|
||||
media?: string;
|
||||
hreflang?: string;
|
||||
type?: string;
|
||||
}
|
||||
interface HtmlAudioTag extends HtmlTag {
|
||||
src?: string;
|
||||
autobuffer?: string;
|
||||
autoplay?: string;
|
||||
loop?: string;
|
||||
controls?: string;
|
||||
}
|
||||
interface BaseTag extends HtmlTag {
|
||||
href?: string;
|
||||
target?: string;
|
||||
}
|
||||
interface HtmlQuoteTag extends HtmlTag {
|
||||
cite?: string;
|
||||
}
|
||||
interface HtmlBodyTag extends HtmlTag {
|
||||
}
|
||||
interface HtmlButtonTag extends HtmlTag {
|
||||
action?: string;
|
||||
autofocus?: string;
|
||||
disabled?: string;
|
||||
enctype?: string;
|
||||
form?: string;
|
||||
method?: string;
|
||||
name?: string;
|
||||
novalidate?: string | boolean;
|
||||
target?: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
onClick?: Function;
|
||||
}
|
||||
interface HtmlDataListTag extends HtmlTag {
|
||||
}
|
||||
interface HtmlCanvasTag extends HtmlTag {
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
interface HtmlTableColTag extends HtmlTag {
|
||||
span?: string;
|
||||
}
|
||||
interface HtmlTableSectionTag extends HtmlTag {
|
||||
}
|
||||
interface HtmlTableRowTag extends HtmlTag {
|
||||
}
|
||||
interface DataTag extends HtmlTag {
|
||||
value?: string;
|
||||
}
|
||||
interface HtmlEmbedTag extends HtmlTag {
|
||||
src?: string;
|
||||
type?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
interface HtmlFieldSetTag extends HtmlTag {
|
||||
disabled?: string;
|
||||
form?: string;
|
||||
name?: string;
|
||||
}
|
||||
interface HtmlFormTag extends HtmlTag {
|
||||
acceptCharset?: string;
|
||||
action?: string;
|
||||
autocomplete?: string;
|
||||
enctype?: string;
|
||||
method?: string;
|
||||
name?: string;
|
||||
novalidate?: string | boolean;
|
||||
target?: string;
|
||||
}
|
||||
interface HtmlHtmlTag extends HtmlTag {
|
||||
manifest?: string;
|
||||
}
|
||||
interface HtmlIFrameTag extends HtmlTag {
|
||||
src?: string;
|
||||
srcdoc?: string;
|
||||
name?: string;
|
||||
sandbox?: string;
|
||||
seamless?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
interface HtmlImageTag extends HtmlTag {
|
||||
alt?: string;
|
||||
src?: AttributeValue;
|
||||
crossorigin?: string;
|
||||
usemap?: string;
|
||||
ismap?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
interface HtmlInputTag extends HtmlTag {
|
||||
accept?: string;
|
||||
action?: string;
|
||||
alt?: string;
|
||||
autocomplete?: string;
|
||||
autofocus?: string;
|
||||
checked?: string | boolean;
|
||||
disabled?: string | boolean;
|
||||
enctype?: string;
|
||||
form?: string;
|
||||
height?: string;
|
||||
list?: string;
|
||||
max?: string;
|
||||
maxlength?: string;
|
||||
method?: string;
|
||||
min?: string;
|
||||
multiple?: string;
|
||||
name?: string;
|
||||
novalidate?: string | boolean;
|
||||
pattern?: string;
|
||||
placeholder?: string;
|
||||
readonly?: string;
|
||||
required?: string;
|
||||
size?: string;
|
||||
src?: string;
|
||||
step?: string;
|
||||
target?: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
width?: string;
|
||||
}
|
||||
interface HtmlModTag extends HtmlTag {
|
||||
cite?: string;
|
||||
datetime?: string | Date;
|
||||
}
|
||||
interface KeygenTag extends HtmlTag {
|
||||
autofocus?: string;
|
||||
challenge?: string;
|
||||
disabled?: string;
|
||||
form?: string;
|
||||
keytype?: string;
|
||||
name?: string;
|
||||
}
|
||||
interface HtmlLabelTag extends HtmlTag {
|
||||
form?: string;
|
||||
for?: string;
|
||||
}
|
||||
interface HtmlLITag extends HtmlTag {
|
||||
value?: string | number;
|
||||
}
|
||||
interface HtmlLinkTag extends HtmlTag {
|
||||
href?: string;
|
||||
crossorigin?: string;
|
||||
rel?: string;
|
||||
media?: string;
|
||||
hreflang?: string;
|
||||
type?: string;
|
||||
sizes?: string;
|
||||
integrity?: string;
|
||||
}
|
||||
interface HtmlMapTag extends HtmlTag {
|
||||
name?: string;
|
||||
}
|
||||
interface HtmlMetaTag extends HtmlTag {
|
||||
name?: string;
|
||||
httpEquiv?: string;
|
||||
content?: string;
|
||||
charset?: string;
|
||||
}
|
||||
interface HtmlMeterTag extends HtmlTag {
|
||||
value?: string | number;
|
||||
min?: string | number;
|
||||
max?: string | number;
|
||||
low?: string | number;
|
||||
high?: string | number;
|
||||
optimum?: string | number;
|
||||
}
|
||||
interface HtmlObjectTag extends HtmlTag {
|
||||
data?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
usemap?: string;
|
||||
form?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
interface HtmlOListTag extends HtmlTag {
|
||||
reversed?: string;
|
||||
start?: string | number;
|
||||
}
|
||||
interface HtmlOptgroupTag extends HtmlTag {
|
||||
disabled?: string;
|
||||
label?: string;
|
||||
}
|
||||
interface HtmlOptionTag extends HtmlTag {
|
||||
disabled?: string;
|
||||
label?: string;
|
||||
selected?: string;
|
||||
value?: string;
|
||||
}
|
||||
interface HtmlOutputTag extends HtmlTag {
|
||||
for?: string;
|
||||
form?: string;
|
||||
name?: string;
|
||||
}
|
||||
interface HtmlParamTag extends HtmlTag {
|
||||
name?: string;
|
||||
value?: string;
|
||||
}
|
||||
interface HtmlProgressTag extends HtmlTag {
|
||||
value?: string | number;
|
||||
max?: string | number;
|
||||
}
|
||||
interface HtmlCommandTag extends HtmlTag {
|
||||
type?: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
disabled?: string;
|
||||
checked?: string;
|
||||
radiogroup?: string;
|
||||
default?: string;
|
||||
}
|
||||
interface HtmlLegendTag extends HtmlTag {
|
||||
}
|
||||
interface HtmlBrowserButtonTag extends HtmlTag {
|
||||
type?: string;
|
||||
}
|
||||
interface HtmlMenuTag extends HtmlTag {
|
||||
type?: string;
|
||||
label?: string;
|
||||
}
|
||||
interface HtmlScriptTag extends HtmlTag {
|
||||
src?: string;
|
||||
type?: string;
|
||||
charset?: string;
|
||||
async?: string;
|
||||
defer?: string;
|
||||
crossorigin?: string;
|
||||
integrity?: string;
|
||||
text?: string;
|
||||
}
|
||||
interface HtmlDetailsTag extends HtmlTag {
|
||||
open?: string;
|
||||
}
|
||||
interface HtmlSelectTag extends HtmlTag {
|
||||
autofocus?: string;
|
||||
disabled?: string;
|
||||
form?: string;
|
||||
multiple?: string;
|
||||
name?: string;
|
||||
required?: string;
|
||||
size?: string;
|
||||
}
|
||||
interface HtmlSourceTag extends HtmlTag {
|
||||
src?: string;
|
||||
type?: string;
|
||||
media?: string;
|
||||
}
|
||||
interface HtmlStyleTag extends HtmlTag {
|
||||
media?: string;
|
||||
type?: string;
|
||||
disabled?: string;
|
||||
scoped?: string;
|
||||
}
|
||||
interface HtmlTableTag extends HtmlTag {
|
||||
}
|
||||
interface HtmlTableDataCellTag extends HtmlTag {
|
||||
colspan?: string | number;
|
||||
rowspan?: string | number;
|
||||
headers?: string;
|
||||
}
|
||||
interface HtmlTextAreaTag extends HtmlTag {
|
||||
autofocus?: string;
|
||||
cols?: string;
|
||||
dirname?: string;
|
||||
disabled?: string;
|
||||
form?: string;
|
||||
maxlength?: string;
|
||||
minlength?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
readonly?: string;
|
||||
required?: string;
|
||||
rows?: string;
|
||||
wrap?: string;
|
||||
}
|
||||
interface HtmlTableHeaderCellTag extends HtmlTag {
|
||||
colspan?: string | number;
|
||||
rowspan?: string | number;
|
||||
headers?: string;
|
||||
scope?: string;
|
||||
}
|
||||
interface HtmlTimeTag extends HtmlTag {
|
||||
datetime?: string | Date;
|
||||
}
|
||||
interface HtmlTrackTag extends HtmlTag {
|
||||
default?: string;
|
||||
kind?: string;
|
||||
label?: string;
|
||||
src?: string;
|
||||
srclang?: string;
|
||||
}
|
||||
interface HtmlVideoTag extends HtmlTag {
|
||||
src?: string;
|
||||
poster?: string;
|
||||
autobuffer?: string;
|
||||
autoplay?: string;
|
||||
loop?: string;
|
||||
controls?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=element-types.d.ts.map
|
||||
Vendored
-98
@@ -1,98 +0,0 @@
|
||||
declare namespace JSX {
|
||||
interface HtmlBodyTag {
|
||||
onafterprint?: string;
|
||||
onbeforeprint?: string;
|
||||
onbeforeonload?: string;
|
||||
onblur?: string;
|
||||
onerror?: string;
|
||||
onfocus?: string;
|
||||
onhaschange?: string;
|
||||
onload?: string;
|
||||
onmessage?: string;
|
||||
onoffline?: string;
|
||||
ononline?: string;
|
||||
onpagehide?: string;
|
||||
onpageshow?: string;
|
||||
onpopstate?: string;
|
||||
onredo?: string;
|
||||
onresize?: string;
|
||||
onstorage?: string;
|
||||
onundo?: string;
|
||||
onunload?: string;
|
||||
}
|
||||
interface HtmlTag {
|
||||
oncontextmenu?: string;
|
||||
onkeydown?: string;
|
||||
onkeypress?: string;
|
||||
onkeyup?: string;
|
||||
onclick?: AttributeValue;
|
||||
ondblclick?: string;
|
||||
ondrag?: string;
|
||||
ondragend?: string;
|
||||
ondragenter?: string;
|
||||
ondragleave?: string;
|
||||
ondragover?: string;
|
||||
ondragstart?: string;
|
||||
ondrop?: string;
|
||||
onmousedown?: string;
|
||||
onmousemove?: string;
|
||||
onmouseout?: string;
|
||||
onmouseover?: string;
|
||||
onmouseup?: string;
|
||||
onmousewheel?: string;
|
||||
onscroll?: string;
|
||||
}
|
||||
interface FormEvents {
|
||||
onblur?: string;
|
||||
onchange?: string;
|
||||
onfocus?: string;
|
||||
onformchange?: string;
|
||||
onforminput?: string;
|
||||
oninput?: string;
|
||||
oninvalid?: string;
|
||||
onselect?: string;
|
||||
onsubmit?: string;
|
||||
}
|
||||
interface HtmlInputTag extends FormEvents {
|
||||
}
|
||||
interface HtmlFieldSetTag extends FormEvents {
|
||||
}
|
||||
interface HtmlFormTag extends FormEvents {
|
||||
}
|
||||
interface MediaEvents {
|
||||
onabort?: string;
|
||||
oncanplay?: string;
|
||||
oncanplaythrough?: string;
|
||||
ondurationchange?: string;
|
||||
onemptied?: string;
|
||||
onended?: string;
|
||||
onerror?: AttributeValue;
|
||||
onloadeddata?: string;
|
||||
onloadedmetadata?: string;
|
||||
onloadstart?: string;
|
||||
onpause?: string;
|
||||
onplay?: string;
|
||||
onplaying?: string;
|
||||
onprogress?: string;
|
||||
onratechange?: string;
|
||||
onreadystatechange?: string;
|
||||
onseeked?: string;
|
||||
onseeking?: string;
|
||||
onstalled?: string;
|
||||
onsuspend?: string;
|
||||
ontimeupdate?: string;
|
||||
onvolumechange?: string;
|
||||
onwaiting?: string;
|
||||
}
|
||||
interface HtmlAudioTag extends MediaEvents {
|
||||
}
|
||||
interface HtmlEmbedTag extends MediaEvents {
|
||||
}
|
||||
interface HtmlImageTag extends MediaEvents {
|
||||
}
|
||||
interface HtmlObjectTag extends MediaEvents {
|
||||
}
|
||||
interface HtmlVideoTag extends MediaEvents {
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=events.d.ts.map
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
declare namespace JSX {
|
||||
type Element = HTMLElement;
|
||||
interface IntrinsicElements {
|
||||
a: HtmlAnchorTag;
|
||||
abbr: HtmlTag;
|
||||
address: HtmlTag;
|
||||
area: HtmlAreaTag;
|
||||
article: HtmlTag;
|
||||
aside: HtmlTag;
|
||||
audio: HtmlAudioTag;
|
||||
b: HtmlTag;
|
||||
bb: HtmlBrowserButtonTag;
|
||||
base: BaseTag;
|
||||
bdi: HtmlTag;
|
||||
bdo: HtmlTag;
|
||||
blockquote: HtmlQuoteTag;
|
||||
body: HtmlBodyTag;
|
||||
br: HtmlTag;
|
||||
button: HtmlButtonTag;
|
||||
canvas: HtmlCanvasTag;
|
||||
caption: HtmlTag;
|
||||
cite: HtmlTag;
|
||||
code: HtmlTag;
|
||||
col: HtmlTableColTag;
|
||||
colgroup: HtmlTableColTag;
|
||||
commands: HtmlCommandTag;
|
||||
data: DataTag;
|
||||
datalist: HtmlDataListTag;
|
||||
dd: HtmlTag;
|
||||
del: HtmlModTag;
|
||||
details: HtmlDetailsTag;
|
||||
dfn: HtmlTag;
|
||||
div: HtmlTag;
|
||||
dl: HtmlTag;
|
||||
dt: HtmlTag;
|
||||
em: HtmlTag;
|
||||
embed: HtmlEmbedTag;
|
||||
fieldset: HtmlFieldSetTag;
|
||||
figcaption: HtmlTag;
|
||||
figure: HtmlTag;
|
||||
footer: HtmlTag;
|
||||
form: HtmlFormTag;
|
||||
h1: HtmlTag;
|
||||
h2: HtmlTag;
|
||||
h3: HtmlTag;
|
||||
h4: HtmlTag;
|
||||
h5: HtmlTag;
|
||||
h6: HtmlTag;
|
||||
head: HtmlTag;
|
||||
header: HtmlTag;
|
||||
hr: HtmlTag;
|
||||
html: HtmlHtmlTag;
|
||||
i: HtmlTag;
|
||||
iframe: HtmlIFrameTag;
|
||||
img: HtmlImageTag;
|
||||
input: HtmlInputTag;
|
||||
ins: HtmlModTag;
|
||||
kbd: HtmlTag;
|
||||
keygen: KeygenTag;
|
||||
label: HtmlLabelTag;
|
||||
legend: HtmlLegendTag;
|
||||
li: HtmlLITag;
|
||||
link: HtmlLinkTag;
|
||||
main: HtmlTag;
|
||||
map: HtmlMapTag;
|
||||
mark: HtmlTag;
|
||||
menu: HtmlMenuTag;
|
||||
meta: HtmlMetaTag;
|
||||
meter: HtmlMeterTag;
|
||||
nav: HtmlTag;
|
||||
noscript: HtmlTag;
|
||||
object: HtmlObjectTag;
|
||||
ol: HtmlOListTag;
|
||||
optgroup: HtmlOptgroupTag;
|
||||
option: HtmlOptionTag;
|
||||
output: HtmlOutputTag;
|
||||
p: HtmlTag;
|
||||
param: HtmlParamTag;
|
||||
pre: HtmlTag;
|
||||
progress: HtmlProgressTag;
|
||||
q: HtmlQuoteTag;
|
||||
rb: HtmlTag;
|
||||
rp: HtmlTag;
|
||||
rt: HtmlTag;
|
||||
rtc: HtmlTag;
|
||||
ruby: HtmlTag;
|
||||
s: HtmlTag;
|
||||
samp: HtmlTag;
|
||||
script: HtmlScriptTag;
|
||||
section: HtmlTag;
|
||||
select: HtmlSelectTag;
|
||||
small: HtmlTag;
|
||||
source: HtmlSourceTag;
|
||||
span: HtmlTag;
|
||||
strong: HtmlTag;
|
||||
style: HtmlStyleTag;
|
||||
sub: HtmlTag;
|
||||
sup: HtmlTag;
|
||||
table: HtmlTableTag;
|
||||
tbody: HtmlTag;
|
||||
td: HtmlTableDataCellTag;
|
||||
template: HtmlTag;
|
||||
textarea: HtmlTextAreaTag;
|
||||
tfoot: HtmlTableSectionTag;
|
||||
th: HtmlTableHeaderCellTag;
|
||||
thead: HtmlTableSectionTag;
|
||||
time: HtmlTimeTag;
|
||||
title: HtmlTag;
|
||||
tr: HtmlTableRowTag;
|
||||
track: HtmlTrackTag;
|
||||
u: HtmlTag;
|
||||
ul: HtmlTag;
|
||||
var: HtmlTag;
|
||||
video: HtmlVideoTag;
|
||||
wbr: HtmlTag;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=intrinsic-elements.d.ts.map
|
||||
@@ -0,0 +1,17 @@
|
||||
module list_app
|
||||
|
||||
go 1.25.6
|
||||
|
||||
require modernc.org/sqlite v1.55.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
modernc.org/libc v1.74.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
_ "list_app/db"
|
||||
|
||||
"list_app/controllers"
|
||||
"list_app/routes"
|
||||
)
|
||||
|
||||
const hostname = "127.0.0.1"
|
||||
const httpPort = "4080"
|
||||
|
||||
func main() {
|
||||
mux := routes.New()
|
||||
|
||||
go controllers.CheckImages()
|
||||
|
||||
log.Printf("Server running at http://%s:%s/\n", hostname, httpPort)
|
||||
if err := http.ListenAndServe(hostname+":"+httpPort, mux); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
"list_app/db"
|
||||
)
|
||||
|
||||
type Table string
|
||||
|
||||
const (
|
||||
Movies Table = "movies"
|
||||
Series Table = "series"
|
||||
Games Table = "games"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
ID int `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Released string `json:"released"`
|
||||
WebImg string `json:"webImg"`
|
||||
Poster string `json:"poster"`
|
||||
Year string `json:"year"`
|
||||
}
|
||||
|
||||
func Save(table Table, code, title, released, webImg, poster, year string) int64 {
|
||||
sql := "INSERT INTO " + string(table) + " (code, title, released, webImg, poster, year) VALUES (?,?,?,?,?,?)"
|
||||
|
||||
result, err := db.Pool.Exec(sql, code, title, released, webImg, poster, year)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0
|
||||
}
|
||||
changes, _ := result.RowsAffected()
|
||||
return changes
|
||||
}
|
||||
|
||||
func UpdateWebImg(table Table, code, webImg string) int64 {
|
||||
sql := "UPDATE " + string(table) + " SET webImg = ? WHERE code = ?;"
|
||||
|
||||
result, err := db.Pool.Exec(sql, webImg, code)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0
|
||||
}
|
||||
changes, _ := result.RowsAffected()
|
||||
return changes
|
||||
}
|
||||
|
||||
func FindOneAndDelete(table Table, code string) int64 {
|
||||
result, err := db.Pool.Exec("DELETE FROM "+string(table)+" WHERE code = ?;", code)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0
|
||||
}
|
||||
changes, _ := result.RowsAffected()
|
||||
return changes
|
||||
}
|
||||
|
||||
func FindOne(table Table, code string) []Media {
|
||||
rows, err := db.Pool.Query("SELECT id, code, title, released, webImg, poster, year FROM "+string(table)+" WHERE code = ?;", code)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return []Media{}
|
||||
}
|
||||
return scanMedia(rows)
|
||||
}
|
||||
|
||||
func Find(table Table) []Media {
|
||||
rows, err := db.Pool.Query("SELECT id, code, title, released, webImg, poster, year FROM " + string(table) + ";")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return []Media{}
|
||||
}
|
||||
return scanMedia(rows)
|
||||
}
|
||||
|
||||
func scanMedia(rows *sql.Rows) []Media {
|
||||
defer rows.Close()
|
||||
result := []Media{}
|
||||
for rows.Next() {
|
||||
var m Media
|
||||
if err := rows.Scan(&m.ID, &m.Code, &m.Title, &m.Released, &m.WebImg, &m.Poster, &m.Year); err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
result = append(result, m)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"list_app/db"
|
||||
)
|
||||
|
||||
type UserValue int
|
||||
|
||||
const (
|
||||
Pass UserValue = iota + 1
|
||||
OmdbKey
|
||||
TwitchClientID
|
||||
TwitchClientSecret
|
||||
)
|
||||
|
||||
func GetValue(id UserValue) (string, bool) {
|
||||
var value string
|
||||
err := db.Pool.QueryRow("SELECT value FROM userData where id = ?;", int(id)).Scan(&value)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func CheckPassword(pass string) bool {
|
||||
password, ok := GetValue(Pass)
|
||||
return ok && password != "" && pass == password
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"express": "^5.1.0",
|
||||
"morgan": "~1.10.1",
|
||||
"bun-types": "^1.2.22",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package public
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed logo.ico no_poster.jpg
|
||||
var FS embed.FS
|
||||
@@ -1,74 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/logo.ico" type="image/x-icon">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<title>List</title>
|
||||
<script src="/list.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" id="movieButton">Movies</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="seriesButton">Series</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="gameButton">Games</button>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<button class="nav-link dropdown-toggle" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
Sort
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li><button class="dropdown-item" id="titleButton">Title</button></li>
|
||||
<li><button class="dropdown-item" id="yearButton">Year</button></li>
|
||||
<li><button class="dropdown-item" id="idButton">Added</button></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="editButton">Edit</button>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex" action="" id="myform">
|
||||
<input class="form-control me-2" type="password" name="password" id="pass" placeholder="password">
|
||||
<input class="form-control me-2" type="text" name="input_id" id="input_id" placeholder="id">
|
||||
<input class="btn btn-outline-success" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="py-5 bg-body-tertiary mt-3">
|
||||
<div class="container" id="root">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"list_app/controllers"
|
||||
"list_app/public"
|
||||
"list_app/web"
|
||||
)
|
||||
|
||||
func New() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /{$}", web.Index)
|
||||
mux.HandleFunc("POST /add", web.Add)
|
||||
|
||||
mux.HandleFunc("GET /api", apiIndex)
|
||||
mux.HandleFunc("GET /api/{$}", apiIndex)
|
||||
mux.HandleFunc("GET /api/media/{mediaType}", controllers.List)
|
||||
|
||||
// Poster images are fetched at runtime, so they're served from a real
|
||||
// directory on disk rather than the embedded static assets below.
|
||||
mux.Handle("/poster/", http.StripPrefix("/poster/", http.FileServer(http.Dir(controllers.PostersDir))))
|
||||
|
||||
mux.Handle("/", http.FileServerFS(public.FS))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func apiIndex(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "API is working"})
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "elements.createElement",
|
||||
"allowJs": true,
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"types": [
|
||||
"bun-types"
|
||||
],
|
||||
"baseUrl": "./"
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"list_app/controllers"
|
||||
"list_app/models"
|
||||
)
|
||||
|
||||
type mediaView struct {
|
||||
Code string
|
||||
Title string
|
||||
Released string
|
||||
WebImg string
|
||||
}
|
||||
|
||||
type group struct {
|
||||
Label string
|
||||
Items []mediaView
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
ListType string
|
||||
Sort string
|
||||
Total int
|
||||
Groups []group
|
||||
Error string
|
||||
}
|
||||
|
||||
func tableFor(listType string) (models.Table, bool) {
|
||||
switch listType {
|
||||
case "movies":
|
||||
return models.Movies, true
|
||||
case "series":
|
||||
return models.Series, true
|
||||
case "games":
|
||||
return models.Games, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// resolveWebImg is decided at render time rather than falling back via
|
||||
// client-side JS: if the poster hasn't been downloaded yet (or the file's
|
||||
// missing), the fallback image is used directly in the rendered `src`.
|
||||
func resolveWebImg(m models.Media) string {
|
||||
if m.WebImg == "" {
|
||||
return "/no_poster.jpg"
|
||||
}
|
||||
if _, err := os.Stat(controllers.PosterDiskPath(m.WebImg)); err != nil {
|
||||
return "/no_poster.jpg"
|
||||
}
|
||||
return m.WebImg
|
||||
}
|
||||
|
||||
func toView(m models.Media) mediaView {
|
||||
return mediaView{
|
||||
Code: m.Code,
|
||||
Title: m.Title,
|
||||
Released: m.Released,
|
||||
WebImg: resolveWebImg(m),
|
||||
}
|
||||
}
|
||||
|
||||
func toViews(media []models.Media) []mediaView {
|
||||
views := make([]mediaView, len(media))
|
||||
for i, m := range media {
|
||||
views[i] = toView(m)
|
||||
}
|
||||
return views
|
||||
}
|
||||
|
||||
func parseReleased(released string) time.Time {
|
||||
t, err := time.Parse("2 Jan 2006", released)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func groupLetter(title string) string {
|
||||
if title == "" {
|
||||
return "#"
|
||||
}
|
||||
r := []rune(strings.ToUpper(title))[0]
|
||||
if unicode.IsDigit(r) {
|
||||
return "#"
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func buildGroups(sortType string, media []models.Media) []group {
|
||||
switch sortType {
|
||||
case "year":
|
||||
sort.SliceStable(media, func(i, j int) bool {
|
||||
return parseReleased(media[i].Released).Before(parseReleased(media[j].Released))
|
||||
})
|
||||
|
||||
byYear := map[string][]models.Media{}
|
||||
for _, m := range media {
|
||||
key := strconv.Itoa(parseReleased(m.Released).Year())
|
||||
byYear[key] = append(byYear[key], m)
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(byYear))
|
||||
for k := range byYear {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(keys)))
|
||||
|
||||
groups := make([]group, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
groups = append(groups, group{Label: k, Items: toViews(byYear[k])})
|
||||
}
|
||||
return groups
|
||||
|
||||
case "id":
|
||||
sort.SliceStable(media, func(i, j int) bool { return media[i].ID > media[j].ID })
|
||||
return []group{{Label: "added", Items: toViews(media)}}
|
||||
|
||||
default: // title
|
||||
sort.SliceStable(media, func(i, j int) bool {
|
||||
return strings.ToLower(media[i].Title) < strings.ToLower(media[j].Title)
|
||||
})
|
||||
|
||||
byLetter := map[string][]models.Media{}
|
||||
for _, m := range media {
|
||||
key := groupLetter(m.Title)
|
||||
byLetter[key] = append(byLetter[key], m)
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(byLetter))
|
||||
for k := range byLetter {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
groups := make([]group, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
groups = append(groups, group{Label: k, Items: toViews(byLetter[k])})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
}
|
||||
|
||||
func Index(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
|
||||
listType := q.Get("listType")
|
||||
table, ok := tableFor(listType)
|
||||
if !ok {
|
||||
listType = "movies"
|
||||
table = models.Movies
|
||||
}
|
||||
|
||||
sortType := q.Get("sortType")
|
||||
if sortType != "title" && sortType != "year" && sortType != "id" {
|
||||
sortType = "title"
|
||||
}
|
||||
|
||||
media := models.Find(table)
|
||||
|
||||
data := pageData{
|
||||
ListType: listType,
|
||||
Sort: sortType,
|
||||
Total: len(media),
|
||||
Groups: buildGroups(sortType, media),
|
||||
Error: q.Get("error"),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := pageTemplate.Execute(w, data); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func redirectToList(w http.ResponseWriter, r *http.Request, listType, sortType, errMsg string) {
|
||||
vals := url.Values{}
|
||||
vals.Set("listType", listType)
|
||||
vals.Set("sortType", sortType)
|
||||
if errMsg != "" {
|
||||
vals.Set("error", errMsg)
|
||||
}
|
||||
http.Redirect(w, r, "/?"+vals.Encode(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// Add handles the plain HTML form submission for adding new media: no JS,
|
||||
// so it always ends in a redirect back to the list, with an `error` query
|
||||
// param set for the page to render as a flash message on failure.
|
||||
func Add(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pass := r.FormValue("pass")
|
||||
code := r.FormValue("code")
|
||||
listType := r.FormValue("listType")
|
||||
sortType := r.FormValue("sortType")
|
||||
|
||||
if pass == "" || code == "" {
|
||||
redirectToList(w, r, listType, sortType, "")
|
||||
return
|
||||
}
|
||||
|
||||
if !models.CheckPassword(pass) {
|
||||
redirectToList(w, r, listType, sortType, "Incorrect password")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := controllers.Create(code); err != nil {
|
||||
redirectToList(w, r, listType, sortType, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
redirectToList(w, r, listType, sortType, "")
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/logo.ico" type="image/x-icon">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<title>List</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{if eq .ListType "movies"}} active{{end}}" href="/?listType=movies&sortType={{.Sort}}">Movies</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{if eq .ListType "series"}} active{{end}}" href="/?listType=series&sortType={{.Sort}}">Series</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{if eq .ListType "games"}} active{{end}}" href="/?listType=games&sortType={{.Sort}}">Games</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
Sort
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/?listType={{.ListType}}&sortType=title">Title</a></li>
|
||||
<li><a class="dropdown-item" href="/?listType={{.ListType}}&sortType=year">Year</a></li>
|
||||
<li><a class="dropdown-item" href="/?listType={{.ListType}}&sortType=id">Added</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex" method="POST" action="/add">
|
||||
<input type="hidden" name="listType" value="{{.ListType}}">
|
||||
<input type="hidden" name="sortType" value="{{.Sort}}">
|
||||
<input class="form-control me-2" type="password" name="pass" placeholder="password">
|
||||
<input class="form-control me-2" type="text" name="code" placeholder="id">
|
||||
<input class="btn btn-outline-success" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="py-5 bg-body-tertiary mt-3">
|
||||
<div class="container" id="root">
|
||||
{{if .Error}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="alert alert-danger" role="alert">{{.Error}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2 class="text-center">{{.Total}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{range .Groups}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2 class="text-center">{{.Label}}: {{len .Items}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-3">
|
||||
{{range .Items}}
|
||||
<div class="col media-element" id="{{.Code}}">
|
||||
<div class="card shadow-sm">
|
||||
<img class="card-img-top" src="{{.WebImg}}" width="100%" loading="lazy" style="min-height: 100px;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{.Title}}</h5>
|
||||
<p class="card-text">{{.Released}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
//go:embed page.html
|
||||
var templateFS embed.FS
|
||||
|
||||
var pageTemplate = template.Must(template.ParseFS(templateFS, "page.html"))
|
||||
Reference in New Issue
Block a user