diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d9060c7 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index db75230..8325a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,20 @@ dist ayylmao.js # Python video server - generated TLS material (certbot state) -python_video_server/certs/ \ No newline at end of file +python_video_server/certs/ + +# Backup files +backups/ + +# Video files in public/video/ +public/video/* + +# Uploads directory (temporary files) +uploads/ + +# Example files +exemple/ + +# OCR training data +*.traineddata +eng.traineddata \ No newline at end of file diff --git a/README.md b/README.md index be90b77..194cc52 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,34 @@ ## Oscars Pool 2026 Little nodejs + mongodb app that I hack together with ChatGPT and Cursor to keep track of watched movies for an incoming Oscars Pool with some friends. +**Note:** The application interface is in French. + ![Oscars Pool 2026](https://raw.githubusercontent.com/quoije/oscars-pool/refs/heads/prod/img/preview.png) ## Features -- User accounts: register, login, JWT auth -- Optional registration "verification" via `DOG_NAMES` (if unset, verification is skipped) -- Mark nominees as watched (per user), with watched date -- Browse movies by Oscar year + API for available years -- User stats: watched count/ratio and watched titles -- Movie details pulled from OMDb (title/plot/rating/poster) via IMDb ID (optional) -- Admin movie management: add, edit (optionally refresh from OMDb), delete -- Built-in player page with multiple source options (VOD link, direct video URL, embed, or server file) -- Resume playback: per-user playback progress saving (time/duration) -- Global settings: active Oscar year, “completion” modal content for checklist -- Password flows: admin temp password reset + forced change on next login -- App version display (footer) managed from Admin panel (no GitHub dependency) + +### User Features +- **User accounts**: register, login, JWT authentication +- **Optional registration verification**: via `DOG_NAMES` environment variable (if unset, verification is skipped) +- **Movie tracking**: mark nominees/movies as watched (per user), with watched date +- **Movie browsing**: browse movies by Oscar year + API for available years +- **Movie details**: pulled from OMDb (title/plot/rating/poster) via IMDb ID (optional) +- **User statistics**: watched count, points from movies watched and Oscar picks, leaderboard +- **Video player**: built-in player page with multiple source options (VOD link, direct video URL, embed, or server file) +- **Resume playback**: per-user playback progress saving (time/duration) +- **Oscar picks**: make picks for each category, auto-save on selection +- **Scores**: view leaderboard with correct/incorrect pick counts, detailed breakdown per user +- **View all picks**: see everyone's picks in a comparison view + +### Admin Features +- **Movie management**: add, edit (optionally refresh from OMDb), delete movies +- **User management**: view users, reset passwords (temp password + forced change on next login) +- **Oscar category management**: create, edit, delete categories with nominees, bulk delete +- **Winner management**: mark winners for each category (can clear winners) +- **Global settings**: active Oscar year, "completion" modal content for checklist +- **Database backup/restore**: create backups and restore from backup files +- **App version control**: manage app version display (footer) from admin panel (no GitHub dependency) +- **Player UI settings**: control player page admin status display ## Setup Create a `.env` file at the project root (same folder as `package.json`). @@ -52,7 +65,13 @@ This app uses **MongoDB** via Mongoose. Provide a connection string in `MONGO_UR - `MONGO_URI` examples: - Atlas: `mongodb+srv://USER:PASS@cluster0.XXXX.mongodb.net/?retryWrites=true&w=majority` - Local: `mongodb://127.0.0.1:27017/` -- Collections are created/managed automatically from the models in `models/` (e.g. `User`, `Movie`, `Setting`, `PlaybackProgress`). +- Collections are created/managed automatically from the models in `models/`: + - `User` - user accounts + - `Movie` - Oscar-nominated movies + - `OscarCategory` - Oscar categories with nominees + - `OscarPick` - user picks for each category + - `Setting` - global settings (active year, etc.) + - `PlaybackProgress` - video playback progress per user ## More `.env` options The app also reads these environment variables (all optional): @@ -70,9 +89,5 @@ Notes: - If you use `video_file` in a movie record, it must be a **relative path** under `VIDEO_FILES_DIR` (the backend blocks traversal/absolute paths). ## Python video server (optional) -There is also a standalone Python server in `python_video_server/` that mirrors `/api/video/:id` (Range streaming + JWT + Mongo). -See `python_video_server/README.md` for full instructions. In addition to `MONGO_URI` and `JWT_SECRET`, you may need: +There is also a standalone Python server in `python_video_server/` that mirrors `/api/video/:id` (Range streaming + JWT + Mongo). See `python_video_server/README.md` for setup and usage instructions. -```bash -export MONGO_DB_NAME="oscars-pool" # if your MONGO_URI doesn't include a / path -``` diff --git a/index.js b/index.js index 877665a..049b6c3 100644 --- a/index.js +++ b/index.js @@ -25,6 +25,8 @@ const versionRoutes = require("./routes/version"); const settingsRoutes = require("./routes/settings"); const adminDbRoutes = require("./routes/adminDb"); const videoRoutes = require("./routes/video"); +const picksRoutes = require("./routes/picks"); +const categoriesRoutes = require("./routes/categories"); app.use(express.static('public')); app.use("/api/users", userRoutes); @@ -33,7 +35,9 @@ app.use("/api/version", versionRoutes); app.use("/api/settings", settingsRoutes); app.use("/api/admin", adminDbRoutes); app.use("/api/video", videoRoutes); +app.use("/api/picks", picksRoutes); +app.use("/api/categories", categoriesRoutes); // Start Server -const PORT = process.env.PORT || 5000; +const PORT = process.env.PORT || 5050; app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/models/OscarCategory.js b/models/OscarCategory.js new file mode 100644 index 0000000..126d619 --- /dev/null +++ b/models/OscarCategory.js @@ -0,0 +1,19 @@ +const mongoose = require("mongoose"); + +const oscarCategorySchema = new mongoose.Schema({ + year: { type: Number, required: true, index: true }, + categoryNumber: { type: Number, required: true }, // 1-23 from the sheet + categoryName: { type: String, required: true }, // e.g., "Best Picture" + nominees: [{ + name: { type: String, required: true }, // e.g., "Anora" or "Sean Baker, Anora" + isWinner: { type: Boolean, default: false } // Set when results are known + }], + // Store the raw text from PDF for reference + rawText: { type: String, required: false } +}, { timestamps: true }); + +// Ensure unique category per year +oscarCategorySchema.index({ year: 1, categoryNumber: 1 }, { unique: true }); + +module.exports = mongoose.model("OscarCategory", oscarCategorySchema); + diff --git a/models/OscarPick.js b/models/OscarPick.js new file mode 100644 index 0000000..4237d5f --- /dev/null +++ b/models/OscarPick.js @@ -0,0 +1,19 @@ +const mongoose = require("mongoose"); + +const oscarPickSchema = new mongoose.Schema({ + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + year: { type: Number, required: true, index: true }, + picks: [{ + categoryNumber: { type: Number, required: true }, + categoryName: { type: String, required: true }, + selectedNominee: { type: String, required: true } // The nominee name they selected + }], + submittedAt: { type: Date, default: Date.now }, + score: { type: Number, default: null } // Calculated after winners are set +}, { timestamps: true }); + +// Ensure one pick submission per user per year +oscarPickSchema.index({ userId: 1, year: 1 }, { unique: true }); + +module.exports = mongoose.model("OscarPick", oscarPickSchema); + diff --git a/package-lock.json b/package-lock.json index b5897d7..c25a51c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "jsonwebtoken": "^9.0.2", "moment-timezone": "^0.5.47", "mongoose": "^8.9.5", - "multer": "^2.0.2" + "multer": "^2.0.2", + "pdf-parse": "^2.4.5" }, "devDependencies": { "nodemon": "^3.1.9" @@ -34,6 +35,190 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1432,6 +1617,38 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdf-parse/node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1573,9 +1790,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 216c776..6f1ad48 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "jsonwebtoken": "^9.0.2", "moment-timezone": "^0.5.47", "mongoose": "^8.9.5", - "multer": "^2.0.2" + "multer": "^2.0.2", + "pdf-parse": "^2.4.5" }, "devDependencies": { "nodemon": "^3.1.9" diff --git a/public/checklist.html b/public/checklist.html index 2233d28..9d02018 100644 --- a/public/checklist.html +++ b/public/checklist.html @@ -61,7 +61,7 @@