From 848a79a04e918fbfbf249e4b6953c03055fdcdfa Mon Sep 17 00:00:00 2001 From: mahek Date: Mon, 21 Jul 2025 15:43:20 +0200 Subject: [PATCH] Initial commit - Projet Managerr --- .gitignore | 38 ++ README.md | 136 +++++ TODO.md | 37 ++ backend/controllers/authController.js | 100 ++++ backend/controllers/radarrController.js | 83 +++ backend/controllers/settingsController.js | 112 ++++ backend/controllers/sonarrController.js | 76 +++ backend/middleware/auth.js | 20 + backend/models/Settings.js | 39 ++ backend/models/User.js | 23 + backend/package.json | 25 + backend/routes/auth.js | 15 + backend/routes/radarr.js | 24 + backend/routes/settings.js | 18 + backend/routes/sonarr.js | 24 + backend/server.js | 53 ++ config/.env.example | 18 + config/radarr.js | 5 + config/sonarr.js | 5 + frontend/package.json | 29 + frontend/public/index.html | 18 + frontend/src/App.vue | 65 +++ frontend/src/assets/css/responsive.css | 214 ++++++++ frontend/src/assets/css/theme.css | 62 +++ frontend/src/components/CalendarComponent.vue | 202 +++++++ frontend/src/components/NavBar.vue | 254 +++++++++ frontend/src/components/ThemeToggle.vue | 73 +++ frontend/src/main.js | 29 + frontend/src/router/index.js | 89 +++ frontend/src/store/index.js | 122 +++++ frontend/src/views/Dashboard.vue | 306 +++++++++++ frontend/src/views/Login.vue | 160 ++++++ frontend/src/views/Movies.vue | 377 +++++++++++++ frontend/src/views/Series.vue | 513 ++++++++++++++++++ frontend/src/views/Settings.vue | 455 ++++++++++++++++ project.md | 31 ++ 36 files changed, 3850 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TODO.md create mode 100644 backend/controllers/authController.js create mode 100644 backend/controllers/radarrController.js create mode 100644 backend/controllers/settingsController.js create mode 100644 backend/controllers/sonarrController.js create mode 100644 backend/middleware/auth.js create mode 100644 backend/models/Settings.js create mode 100644 backend/models/User.js create mode 100644 backend/package.json create mode 100644 backend/routes/auth.js create mode 100644 backend/routes/radarr.js create mode 100644 backend/routes/settings.js create mode 100644 backend/routes/sonarr.js create mode 100644 backend/server.js create mode 100644 config/.env.example create mode 100644 config/radarr.js create mode 100644 config/sonarr.js create mode 100644 frontend/package.json create mode 100644 frontend/public/index.html create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/css/responsive.css create mode 100644 frontend/src/assets/css/theme.css create mode 100644 frontend/src/components/CalendarComponent.vue create mode 100644 frontend/src/components/NavBar.vue create mode 100644 frontend/src/components/ThemeToggle.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/store/index.js create mode 100644 frontend/src/views/Dashboard.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/Movies.vue create mode 100644 frontend/src/views/Series.vue create mode 100644 frontend/src/views/Settings.vue create mode 100755 project.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..106c25a --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Node modules +node_modules/ + +# Fichiers de logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Fichiers de l'environnement +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Fichiers de build +/backend/dist/ +/frontend/dist/ + +# Fichiers et dossiers de cache +.npm +.eslintcache +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Fichiers du système d'exploitation +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1c9a7b --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Managerr + +Une application web pour gérer vos films et séries TV en utilisant les API de Sonarr et Radarr. + +## Fonctionnalités + +- **Authentification** : Système complet d'inscription et de connexion pour les utilisateurs +- **Tableau de bord** : Affichage d'un agenda mensuel avec les téléchargements prévus +- **Films** : Gestion complète de votre collection de films + - Liste de tous les films + - Films manquants + - Films à venir + - Historique des téléchargements +- **Séries** : Gestion complète de vos séries TV + - Liste de toutes les séries + - Épisodes manquants + - Épisodes à venir + - Historique des téléchargements + +## Technologies utilisées + +### Backend +- **Node.js** et **Express.js** pour l'API +- **MongoDB** pour la base de données +- **JWT** pour l'authentification + +### Frontend +- **Vue.js 3** avec **Composition API** +- **Vue Router** pour la navigation +- **Vuex** pour la gestion d'état +- **Axios** pour les requêtes HTTP + +## Prérequis + +- Node.js (v14 ou supérieur) +- MongoDB +- Instances fonctionnelles de Sonarr et Radarr avec leurs API accessibles + +## Installation + +### Configuration du backend + +1. Cloner le dépôt +```bash +git clone https://github.com/votre-username/managerr.git +cd managerr +``` + +2. Installer les dépendances du backend +```bash +cd backend +npm install +``` + +3. Créer un fichier `.env` basé sur le fichier `.env.example` +```bash +cp ../config/.env.example ../config/.env +``` + +4. Modifier le fichier `.env` avec vos propres paramètres +``` +PORT=5000 +MONGODB_URI=mongodb://localhost:27017/managerr +JWT_SECRET=votre_secret_jwt +SONARR_API_URL=http://votre-ip-sonarr:8989/api/v3 +SONARR_API_KEY=votre-cle-api-sonarr +RADARR_API_URL=http://votre-ip-radarr:7878/api/v3 +RADARR_API_KEY=votre-cle-api-radarr +``` + +### Configuration du frontend + +1. Installer les dépendances du frontend +```bash +cd ../frontend +npm install +``` + +2. Créer un fichier `.env.local` pour la configuration du frontend +```bash +echo "VUE_APP_API_URL=http://localhost:5000/api" > .env.local +``` + +## Lancement de l'application + +### Démarrer le backend +```bash +cd backend +npm run dev +``` + +### Démarrer le frontend +```bash +cd frontend +npm run serve +``` + +L'application sera accessible à l'adresse : http://localhost:8080 + +## Déploiement + +### Backend + +1. Construire le backend pour la production +```bash +cd backend +npm start +``` + +### Frontend + +1. Construire le frontend pour la production +```bash +cd frontend +npm run build +``` + +2. Déployer le dossier `/dist` sur votre serveur web + +## Contribuer + +1. Fork le projet +2. Créer une branche pour votre fonctionnalité (`git checkout -b feature/amazing-feature`) +3. Commit vos changements (`git commit -m 'Add some amazing feature'`) +4. Push vers la branche (`git push origin feature/amazing-feature`) +5. Ouvrir une Pull Request + +## Licence + +Distribué sous la licence MIT. Voir `LICENSE` pour plus d'informations. + +## Contact + +Votre Nom - [@votre_twitter](https://twitter.com/votre_twitter) - email@exemple.com + +Lien du projet: [https://github.com/votre-username/managerr](https://github.com/votre-username/managerr) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..27d362e --- /dev/null +++ b/TODO.md @@ -0,0 +1,37 @@ +# Liste des tâches pour le projet Managerr + +## Configuration et mise en place (⏳ En cours) +- [x] Définir les exigences du projet +- [x] Mettre en place la structure du projet +- [ ] Initialiser les dépôts Git + +## Backend (⏳ En cours) +- [x] Choisir un framework backend (Express.js) +- [x] Configurer la base de données (MongoDB) +- [x] Mettre en place l'architecture MVC +- [x] Implémenter l'authentification des utilisateurs +- [x] Créer les modèles de données (utilisateurs, films, séries) +- [x] Développer les APIs pour communiquer avec Sonarr et Radarr +- [x] Implémenter la logique métier pour le traitement des films et séries +- [ ] Mettre en place les tests unitaires et d'intégration + +## Frontend (⏳ En cours) +- [x] Initialiser le projet Vue.js +- [x] Configurer Vue Router pour la navigation +- [x] Implémenter Vuex pour la gestion d'état +- [x] Créer les composants UI réutilisables +- [x] Développer la page de connexion/inscription +- [x] Construire le tableau de bord avec l'agenda mensuel +- [x] Créer la page des films avec les différentes vues +- [x] Créer la page des séries avec les différentes vues +- [x] Implémenter les appels API vers le backend +- [x] Ajouter des validations de formulaire et gestion des erreurs +- [x] Appliquer un design responsive +- [ ] Optimisation des performances + +## Documentation (⏳ En cours) +- [x] Documentation technique +- [x] Guide d'utilisation +- [ ] Documentation API + +## État général du projet: 🏗️ En développement diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js new file mode 100644 index 0000000..0cd7b31 --- /dev/null +++ b/backend/controllers/authController.js @@ -0,0 +1,100 @@ +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const User = require('../models/User'); + +// Inscription d'un nouvel utilisateur +exports.register = async (req, res) => { + try { + const { username, email, password } = req.body; + + // Vérifier si l'utilisateur existe déjà + let user = await User.findOne({ email }); + if (user) { + return res.status(400).json({ message: 'Cet utilisateur existe déjà' }); + } + + // Créer un nouvel utilisateur + user = new User({ + username, + email, + password + }); + + // Hashage du mot de passe + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(password, salt); + + // Sauvegarder l'utilisateur dans la base de données + await user.save(); + + // Générer un token JWT + const payload = { + user: { + id: user.id + } + }; + + jwt.sign( + payload, + process.env.JWT_SECRET || 'secret', + { expiresIn: '24h' }, + (err, token) => { + if (err) throw err; + res.json({ token }); + } + ); + } catch (err) { + console.error(err.message); + res.status(500).send('Erreur serveur'); + } +}; + +// Connexion d'un utilisateur +exports.login = async (req, res) => { + try { + const { email, password } = req.body; + + // Vérifier si l'utilisateur existe + const user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ message: 'Identifiants invalides' }); + } + + // Vérifier le mot de passe + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(400).json({ message: 'Identifiants invalides' }); + } + + // Générer un token JWT + const payload = { + user: { + id: user.id + } + }; + + jwt.sign( + payload, + process.env.JWT_SECRET || 'secret', + { expiresIn: '24h' }, + (err, token) => { + if (err) throw err; + res.json({ token }); + } + ); + } catch (err) { + console.error(err.message); + res.status(500).send('Erreur serveur'); + } +}; + +// Récupérer les informations de l'utilisateur connecté +exports.getMe = async (req, res) => { + try { + const user = await User.findById(req.user.id).select('-password'); + res.json(user); + } catch (err) { + console.error(err.message); + res.status(500).send('Erreur serveur'); + } +}; diff --git a/backend/controllers/radarrController.js b/backend/controllers/radarrController.js new file mode 100644 index 0000000..4438af0 --- /dev/null +++ b/backend/controllers/radarrController.js @@ -0,0 +1,83 @@ +const axios = require('axios'); +const config = require('../../config/radarr'); + +// Client API pour Radarr +const radarrClient = axios.create({ + baseURL: config.baseURL, + headers: { + 'X-Api-Key': config.apiKey + } +}); + +// Récupérer tous les films +exports.getAllMovies = async (req, res) => { + try { + const response = await radarrClient.get('/movie'); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération des films:', error); + res.status(500).json({ message: 'Erreur lors de la récupération des films' }); + } +}; + +// Récupérer les films manquants +exports.getMissingMovies = async (req, res) => { + try { + const response = await radarrClient.get('/movie'); + const missingMovies = response.data.filter(movie => !movie.hasFile); + res.json(missingMovies); + } catch (error) { + console.error('Erreur lors de la récupération des films manquants:', error); + res.status(500).json({ message: 'Erreur lors de la récupération des films manquants' }); + } +}; + +// Récupérer les films à venir +exports.getUpcomingMovies = async (req, res) => { + try { + const today = new Date(); + const response = await radarrClient.get('/movie'); + + const upcomingMovies = response.data.filter(movie => { + const releaseDate = new Date(movie.digitalRelease || movie.physicalRelease || movie.inCinemas); + return releaseDate > today; + }); + + res.json(upcomingMovies); + } catch (error) { + console.error('Erreur lors de la récupération des films à venir:', error); + res.status(500).json({ message: 'Erreur lors de la récupération des films à venir' }); + } +}; + +// Récupérer l'historique des téléchargements +exports.getHistory = async (req, res) => { + try { + const response = await radarrClient.get('/history'); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération de l\'historique:', error); + res.status(500).json({ message: 'Erreur lors de la récupération de l\'historique' }); + } +}; + +// Récupérer le calendrier +exports.getCalendar = async (req, res) => { + try { + const { start, end } = req.query; + const startDate = start || new Date().toISOString(); + const endDate = end || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(); + + const response = await radarrClient.get('/calendar', { + params: { + start: startDate, + end: endDate + } + }); + + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération du calendrier:', error); + res.status(500).json({ message: 'Erreur lors de la récupération du calendrier' }); + } +}; diff --git a/backend/controllers/settingsController.js b/backend/controllers/settingsController.js new file mode 100644 index 0000000..162aad0 --- /dev/null +++ b/backend/controllers/settingsController.js @@ -0,0 +1,112 @@ +const Settings = require('../models/Settings'); +const axios = require('axios'); + +// Récupérer les paramètres de l'utilisateur connecté +exports.getSettings = async (req, res) => { + try { + // Rechercher les paramètres pour l'utilisateur connecté + const settings = await Settings.findOne({ user: req.user.id }); + + if (!settings) { + return res.status(404).json({ message: "Aucun paramètre trouvé pour cet utilisateur" }); + } + + // Ne pas envoyer l'ID de l'utilisateur ni l'ID du document dans la réponse + const { sonarr, radarr } = settings; + + res.json({ sonarr, radarr }); + } catch (error) { + console.error('Erreur lors de la récupération des paramètres:', error); + res.status(500).json({ message: 'Erreur serveur' }); + } +}; + +// Sauvegarder ou mettre à jour les paramètres +exports.saveSettings = async (req, res) => { + try { + const { sonarr, radarr } = req.body; + + // Validation des entrées + if (!sonarr || !radarr) { + return res.status(400).json({ message: 'Les configurations Sonarr et Radarr sont requises' }); + } + + if (!sonarr.url || !sonarr.apiKey || !radarr.url || !radarr.apiKey) { + return res.status(400).json({ message: 'Tous les champs sont requis' }); + } + + // Vérifier si l'utilisateur a déjà des paramètres + let settings = await Settings.findOne({ user: req.user.id }); + + if (settings) { + // Mettre à jour les paramètres existants + settings.sonarr = sonarr; + settings.radarr = radarr; + settings.updatedAt = Date.now(); + } else { + // Créer de nouveaux paramètres + settings = new Settings({ + user: req.user.id, + sonarr, + radarr + }); + } + + await settings.save(); + + res.json({ message: 'Paramètres enregistrés avec succès' }); + } catch (error) { + console.error('Erreur lors de l\'enregistrement des paramètres:', error); + res.status(500).json({ message: 'Erreur serveur' }); + } +}; + +// Tester la connexion à une API (Sonarr ou Radarr) +exports.testConnection = async (req, res) => { + try { + const { type, url, apiKey } = req.body; + + // Validation des entrées + if (!type || !url || !apiKey) { + return res.status(400).json({ message: 'Type, URL et clé API sont requis' }); + } + + if (type !== 'sonarr' && type !== 'radarr') { + return res.status(400).json({ message: 'Type invalide. Doit être "sonarr" ou "radarr"' }); + } + + // Tentative de connexion à l'API + try { + // Utiliser le endpoint /system/status qui est disponible à la fois sur Sonarr et Radarr + const response = await axios.get(`${url}/system/status`, { + headers: { + 'X-Api-Key': apiKey + }, + timeout: 5000 // Timeout de 5 secondes + }); + + if (response.status === 200) { + res.json({ + success: true, + message: 'Connexion établie avec succès', + details: { + version: response.data.version, + appName: response.data.appName + } + }); + } else { + res.status(400).json({ success: false, message: 'Échec de la connexion' }); + } + } catch (apiError) { + console.error(`Erreur lors du test de l'API ${type}:`, apiError); + res.status(400).json({ + success: false, + message: 'Échec de la connexion', + error: apiError.message + }); + } + } catch (error) { + console.error('Erreur lors du test de connexion:', error); + res.status(500).json({ message: 'Erreur serveur' }); + } +}; diff --git a/backend/controllers/sonarrController.js b/backend/controllers/sonarrController.js new file mode 100644 index 0000000..645d748 --- /dev/null +++ b/backend/controllers/sonarrController.js @@ -0,0 +1,76 @@ +const axios = require('axios'); +const config = require('../../config/sonarr'); + +// Client API pour Sonarr +const sonarrClient = axios.create({ + baseURL: config.baseURL, + headers: { + 'X-Api-Key': config.apiKey + } +}); + +// Récupérer toutes les séries +exports.getAllSeries = async (req, res) => { + try { + const response = await sonarrClient.get('/series'); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération des séries:', error); + res.status(500).json({ message: 'Erreur lors de la récupération des séries' }); + } +}; + +// Récupérer les séries manquantes +exports.getMissingSeries = async (req, res) => { + try { + const response = await sonarrClient.get('/wanted/missing'); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération des séries manquantes:', error); + res.status(500).json({ message: 'Erreur lors de la récupération des séries manquantes' }); + } +}; + +// Récupérer les séries à venir +exports.getUpcomingSeries = async (req, res) => { + try { + const response = await sonarrClient.get('/calendar', { + params: { + start: new Date().toISOString(), + end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() // 30 jours + } + }); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération des séries à venir:', error); + res.status(500).json({ message: 'Erreur lors de la récupération des séries à venir' }); + } +}; + +// Récupérer l'historique des téléchargements +exports.getHistory = async (req, res) => { + try { + const response = await sonarrClient.get('/history'); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération de l\'historique:', error); + res.status(500).json({ message: 'Erreur lors de la récupération de l\'historique' }); + } +}; + +// Récupérer le calendrier +exports.getCalendar = async (req, res) => { + try { + const { start, end } = req.query; + const response = await sonarrClient.get('/calendar', { + params: { + start: start || new Date().toISOString(), + end: end || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + } + }); + res.json(response.data); + } catch (error) { + console.error('Erreur lors de la récupération du calendrier:', error); + res.status(500).json({ message: 'Erreur lors de la récupération du calendrier' }); + } +}; diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..2611d92 --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,20 @@ +const jwt = require('jsonwebtoken'); + +module.exports = function (req, res, next) { + // Récupérer le token du header + const token = req.header('x-auth-token'); + + // Vérifier si le token existe + if (!token) { + return res.status(401).json({ message: 'Pas de token, autorisation refusée' }); + } + + // Vérifier le token + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret'); + req.user = decoded.user; + next(); + } catch (err) { + res.status(401).json({ message: 'Token invalide' }); + } +}; diff --git a/backend/models/Settings.js b/backend/models/Settings.js new file mode 100644 index 0000000..e19587f --- /dev/null +++ b/backend/models/Settings.js @@ -0,0 +1,39 @@ +const mongoose = require('mongoose'); + +const SettingsSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + sonarr: { + url: { + type: String, + required: true, + trim: true + }, + apiKey: { + type: String, + required: true, + trim: true + } + }, + radarr: { + url: { + type: String, + required: true, + trim: true + }, + apiKey: { + type: String, + required: true, + trim: true + } + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('Settings', SettingsSchema); diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000..6dadbaa --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); + +const UserSchema = new mongoose.Schema({ + username: { + type: String, + required: true + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('User', UserSchema); diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..6244082 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,25 @@ +{ + "name": "managerr-backend", + "version": "1.0.0", + "description": "Backend pour l'application Managerr - Gestion de films et séries via Sonarr et Radarr", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "test": "jest" + }, + "dependencies": { + "express": "^4.18.2", + "mongoose": "^7.5.0", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.1", + "axios": "^1.6.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "jest": "^29.6.4", + "nodemon": "^3.0.1", + "supertest": "^6.3.3" + } +} diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..ca382b6 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); +const auth = require('../middleware/auth'); + +// Inscription d'un nouvel utilisateur +router.post('/register', authController.register); + +// Connexion d'un utilisateur +router.post('/login', authController.login); + +// Récupération des informations de l'utilisateur connecté +router.get('/me', auth, authController.getMe); + +module.exports = router; diff --git a/backend/routes/radarr.js b/backend/routes/radarr.js new file mode 100644 index 0000000..99c453d --- /dev/null +++ b/backend/routes/radarr.js @@ -0,0 +1,24 @@ +const express = require('express'); +const router = express.Router(); +const radarrController = require('../controllers/radarrController'); +const auth = require('../middleware/auth'); + +// Appliquer le middleware d'authentification à toutes les routes +router.use(auth); + +// Récupérer tous les films +router.get('/movies', radarrController.getAllMovies); + +// Récupérer les films manquants +router.get('/movies/missing', radarrController.getMissingMovies); + +// Récupérer les films à venir +router.get('/movies/upcoming', radarrController.getUpcomingMovies); + +// Récupérer l'historique des téléchargements +router.get('/history', radarrController.getHistory); + +// Récupérer le calendrier +router.get('/calendar', radarrController.getCalendar); + +module.exports = router; diff --git a/backend/routes/settings.js b/backend/routes/settings.js new file mode 100644 index 0000000..959c5cf --- /dev/null +++ b/backend/routes/settings.js @@ -0,0 +1,18 @@ +const express = require('express'); +const router = express.Router(); +const settingsController = require('../controllers/settingsController'); +const auth = require('../middleware/auth'); + +// Appliquer le middleware d'authentification à toutes les routes +router.use(auth); + +// Récupérer les paramètres de l'utilisateur +router.get('/', settingsController.getSettings); + +// Enregistrer ou mettre à jour les paramètres +router.post('/', settingsController.saveSettings); + +// Tester la connexion à une API +router.post('/test-connection', settingsController.testConnection); + +module.exports = router; diff --git a/backend/routes/sonarr.js b/backend/routes/sonarr.js new file mode 100644 index 0000000..a4fdb2f --- /dev/null +++ b/backend/routes/sonarr.js @@ -0,0 +1,24 @@ +const express = require('express'); +const router = express.Router(); +const sonarrController = require('../controllers/sonarrController'); +const auth = require('../middleware/auth'); + +// Appliquer le middleware d'authentification à toutes les routes +router.use(auth); + +// Récupérer toutes les séries +router.get('/series', sonarrController.getAllSeries); + +// Récupérer les séries manquantes +router.get('/series/missing', sonarrController.getMissingSeries); + +// Récupérer les séries à venir +router.get('/series/upcoming', sonarrController.getUpcomingSeries); + +// Récupérer l'historique des téléchargements +router.get('/history', sonarrController.getHistory); + +// Récupérer le calendrier +router.get('/calendar', sonarrController.getCalendar); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..f3256db --- /dev/null +++ b/backend/server.js @@ -0,0 +1,53 @@ +const express = require('express'); +const cors = require('cors'); +const mongoose = require('mongoose'); +const dotenv = require('dotenv'); + +// Chargement des variables d'environnement +dotenv.config({ path: '../config/.env' }); + +// Initialisation de l'application Express +const app = express(); + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Routes +const authRoutes = require('./routes/auth'); +const sonarrRoutes = require('./routes/sonarr'); +const radarrRoutes = require('./routes/radarr'); +const settingsRoutes = require('./routes/settings'); + +app.use('/api/auth', authRoutes); +app.use('/api/sonarr', sonarrRoutes); +app.use('/api/radarr', radarrRoutes); +app.use('/api/settings', settingsRoutes); + +// Route de base pour vérifier que l'API fonctionne +app.get('/', (req, res) => { + res.json({ message: 'API Managerr fonctionne correctement' }); +}); + +// Gestion des erreurs +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ message: 'Une erreur est survenue', error: err.message }); +}); + +// Connexion à la base de données MongoDB +const PORT = process.env.PORT || 5000; +const DB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/managerr'; + +mongoose + .connect(DB_URI) + .then(() => { + console.log('Connecté à la base de données MongoDB'); + app.listen(PORT, () => { + console.log(`Serveur démarré sur le port ${PORT}`); + }); + }) + .catch((err) => { + console.error('Erreur de connexion à MongoDB:', err); + process.exit(1); + }); diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..523705b --- /dev/null +++ b/config/.env.example @@ -0,0 +1,18 @@ +# Variables d'environnement pour l'application Managerr + +# Port du serveur backend +PORT=5000 + +# URI MongoDB +MONGODB_URI=mongodb://localhost:27017/managerr + +# Secret JWT +JWT_SECRET=votre_secret_jwt_securise + +# Configuration API Sonarr +SONARR_API_URL=http://localhost:8989/api/v3 +SONARR_API_KEY=votre_cle_api_sonarr + +# Configuration API Radarr +RADARR_API_URL=http://localhost:7878/api/v3 +RADARR_API_KEY=votre_cle_api_radarr diff --git a/config/radarr.js b/config/radarr.js new file mode 100644 index 0000000..ae44541 --- /dev/null +++ b/config/radarr.js @@ -0,0 +1,5 @@ +// Configuration pour Radarr API +module.exports = { + baseURL: process.env.RADARR_API_URL || 'http://localhost:7878/api/v3', + apiKey: process.env.RADARR_API_KEY || 'votre-clé-api-radarr' +}; diff --git a/config/sonarr.js b/config/sonarr.js new file mode 100644 index 0000000..0d5eaea --- /dev/null +++ b/config/sonarr.js @@ -0,0 +1,5 @@ +// Configuration pour Sonarr API +module.exports = { + baseURL: process.env.SONARR_API_URL || 'http://localhost:8989/api/v3', + apiKey: process.env.SONARR_API_KEY || 'votre-clé-api-sonarr' +}; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..cf1038b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "managerr-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "axios": "^1.6.0", + "core-js": "^3.33.0", + "vue": "^3.3.0", + "vue-router": "^4.2.0", + "vuex": "^4.1.0", + "date-fns": "^2.30.0" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~5.0.0", + "@vue/cli-plugin-eslint": "~5.0.0", + "@vue/cli-plugin-router": "~5.0.0", + "@vue/cli-plugin-vuex": "~5.0.0", + "@vue/cli-service": "~5.0.0", + "eslint": "^8.50.0", + "eslint-plugin-vue": "^9.17.0", + "sass": "^1.69.0", + "sass-loader": "^13.3.0" + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..8970c6d --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,18 @@ + + + + + + + + Managerr - Gestion de films et séries + + + + +
+ + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..e200ccc --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend/src/assets/css/responsive.css b/frontend/src/assets/css/responsive.css new file mode 100644 index 0000000..0a88564 --- /dev/null +++ b/frontend/src/assets/css/responsive.css @@ -0,0 +1,214 @@ +/* Responsive.css - Styles responsives globaux pour l'application Managerr */ + +/* Variables CSS globales */ +:root { + --primary-color: #2c3e50; + --secondary-color: #3498db; + --background-color: #f5f5f5; + --card-bg: white; + --text-color: #333; + --border-color: #e0e0e0; + --success-bg: #d4edda; + --success-text: #155724; + --warning-bg: #fff3cd; + --warning-text: #856404; + --danger-bg: #f8d7da; + --danger-text: #721c24; + --info-bg: #cce5ff; + --info-text: #004085; +} + +/* Règles de base pour le responsive design */ +html { + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body { + margin: 0; + padding: 0; + font-family: 'Avenir', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: var(--text-color); + background-color: var(--background-color); +} + +/* Layout responsive */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Media queries */ + +/* Tablettes */ +@media (max-width: 768px) { + .container { + padding: 0 0.75rem; + } + + /* Ajustement des grilles pour les tablettes */ + .movie-grid, + .series-grid { + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)) !important; + } + + /* Ajustement du tableau de bord */ + .dashboard-summary { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) !important; + } + + /* Ajustement du calendrier */ + .calendar-day { + min-height: 60px !important; + } + + /* Ajustement des formulaires */ + .form-container { + width: 90% !important; + max-width: 500px !important; + } +} + +/* Mobiles */ +@media (max-width: 576px) { + h1 { + font-size: 1.5rem !important; + } + + /* Navigation */ + .navbar { + flex-direction: column !important; + padding: 0.5rem !important; + height: auto !important; + } + + .navbar-menu { + flex-direction: column !important; + width: 100% !important; + gap: 0.5rem !important; + margin: 0.5rem 0 !important; + } + + .navbar-user { + width: 100% !important; + justify-content: space-between !important; + margin-top: 0.5rem !important; + } + + .nav-item { + padding: 0.5rem 0 !important; + width: 100% !important; + text-align: center !important; + } + + /* Ajustement des grilles pour les mobiles */ + .movie-grid, + .series-grid { + grid-template-columns: 1fr !important; + } + + /* Ajustement des cartes */ + .movie-card, + .series-card { + height: auto !important; + flex-direction: row !important; + } + + .movie-card .movie-poster, + .series-card .series-poster { + width: 100px !important; + height: 150px !important; + } + + /* Ajustement des tabs */ + .tabs { + overflow-x: auto !important; + white-space: nowrap !important; + -webkit-overflow-scrolling: touch !important; + display: flex !important; + } + + .tab-button { + padding: 0.5rem 1rem !important; + font-size: 0.9rem !important; + } + + /* Calendrier pour mobile */ + .calendar-grid { + font-size: 0.8rem !important; + } + + .calendar-day { + min-height: 50px !important; + padding: 0.25rem !important; + } + + /* Tables pour mobile */ + table { + display: block !important; + overflow-x: auto !important; + white-space: nowrap !important; + } + + th, td { + padding: 0.5rem !important; + font-size: 0.9rem !important; + } +} + +/* Petits écrans mobiles */ +@media (max-width: 360px) { + .movie-card, + .series-card { + flex-direction: column !important; + } + + .movie-card .movie-poster, + .series-card .series-poster { + width: 100% !important; + height: 200px !important; + } + + .calendar-header { + font-size: 0.7rem !important; + padding: 0.25rem !important; + } + + .calendar-day { + min-height: 40px !important; + } +} + +/* Utilitaires responsive */ +.d-none { + display: none !important; +} + +.d-flex { + display: flex !important; +} + +@media (max-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-flex { + display: flex !important; + } +} + +@media (max-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-flex { + display: flex !important; + } +} diff --git a/frontend/src/assets/css/theme.css b/frontend/src/assets/css/theme.css new file mode 100644 index 0000000..95eedcd --- /dev/null +++ b/frontend/src/assets/css/theme.css @@ -0,0 +1,62 @@ +/* Theme.css - Configuration des thèmes pour l'application Managerr */ + +/* Thème clair (défaut) */ +:root { + --primary-color: #2c3e50; + --secondary-color: #3498db; + --background-color: #f5f5f5; + --card-bg: white; + --text-color: #333; + --border-color: #e0e0e0; + --header-bg: #2c3e50; + --header-text: white; + --success-bg: #d4edda; + --success-text: #155724; + --warning-bg: #fff3cd; + --warning-text: #856404; + --danger-bg: #f8d7da; + --danger-text: #721c24; + --info-bg: #cce5ff; + --info-text: #004085; + --input-bg: white; + --input-border: #ced4da; + --input-text: #495057; + --button-primary-bg: #2c3e50; + --button-primary-text: white; + --button-secondary-bg: #e9ecef; + --button-secondary-text: #495057; + --shadow-color: rgba(0, 0, 0, 0.1); +} + +/* Thème sombre */ +[data-theme="dark"] { + --primary-color: #375a7f; + --secondary-color: #3498db; + --background-color: #222; + --card-bg: #333; + --text-color: #eee; + --border-color: #555; + --header-bg: #375a7f; + --header-text: white; + --success-bg: #204d28; + --success-text: #8fd19e; + --warning-bg: #533f03; + --warning-text: #ffeeba; + --danger-bg: #721c24; + --danger-text: #f8d7da; + --info-bg: #002752; + --info-text: #cce5ff; + --input-bg: #444; + --input-border: #666; + --input-text: #eee; + --button-primary-bg: #375a7f; + --button-primary-text: white; + --button-secondary-bg: #555; + --button-secondary-text: #eee; + --shadow-color: rgba(0, 0, 0, 0.3); +} + +/* Classes d'utilitaire pour les transitions */ +.theme-transition { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; +} diff --git a/frontend/src/components/CalendarComponent.vue b/frontend/src/components/CalendarComponent.vue new file mode 100644 index 0000000..d098f9e --- /dev/null +++ b/frontend/src/components/CalendarComponent.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/frontend/src/components/NavBar.vue b/frontend/src/components/NavBar.vue new file mode 100644 index 0000000..c35f35d --- /dev/null +++ b/frontend/src/components/NavBar.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/frontend/src/components/ThemeToggle.vue b/frontend/src/components/ThemeToggle.vue new file mode 100644 index 0000000..e45655b --- /dev/null +++ b/frontend/src/components/ThemeToggle.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..1e83ccf --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,29 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import store from './store' + +// Importer les styles +import './assets/css/theme.css' +import './assets/css/responsive.css' + +// Fonction pour détecter la préférence de thème de l'utilisateur +const detectColorScheme = () => { + // Vérifie si l'utilisateur a déjà une préférence stockée + const savedTheme = localStorage.getItem('theme') + if (savedTheme) { + document.documentElement.setAttribute('data-theme', savedTheme) + return + } + + // Si l'utilisateur préfère le thème sombre + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.setAttribute('data-theme', 'dark') + } +} + +// Appliquer la préférence de thème +detectColorScheme() + +// Créer et monter l'application +createApp(App).use(store).use(router).mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..b3f78c0 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,89 @@ +import { createRouter, createWebHistory } from 'vue-router' +import store from '../store' + +// Lazy loading des composants +const Home = () => import('../views/Home.vue') +const Login = () => import('../views/Login.vue') +const Register = () => import('../views/Register.vue') +const Dashboard = () => import('../views/Dashboard.vue') +const Movies = () => import('../views/Movies.vue') +const Series = () => import('../views/Series.vue') +const Settings = () => import('../views/Settings.vue') +const NotFound = () => import('../views/NotFound.vue') + +const routes = [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/login', + name: 'Login', + component: Login, + meta: { guestOnly: true } + }, + { + path: '/register', + name: 'Register', + component: Register, + meta: { guestOnly: true } + }, + { + path: '/dashboard', + name: 'Dashboard', + component: Dashboard, + meta: { requiresAuth: true } + }, + { + path: '/movies', + name: 'Movies', + component: Movies, + meta: { requiresAuth: true } + }, + { + path: '/series', + name: 'Series', + component: Series, + meta: { requiresAuth: true } + }, + { + path: '/settings', + name: 'Settings', + component: Settings, + meta: { requiresAuth: true } + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: NotFound + } +] + +const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes +}) + +// Navigation guards +router.beforeEach((to, from, next) => { + const isAuthenticated = store.getters.isAuthenticated + + if (to.matched.some(record => record.meta.requiresAuth)) { + if (!isAuthenticated) { + next('/login') + } else { + next() + } + } else if (to.matched.some(record => record.meta.guestOnly)) { + if (isAuthenticated) { + next('/dashboard') + } else { + next() + } + } else { + next() + } +}) + +export default router diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js new file mode 100644 index 0000000..21abab1 --- /dev/null +++ b/frontend/src/store/index.js @@ -0,0 +1,122 @@ +import { createStore } from 'vuex' +import axios from 'axios' + +const API_URL = process.env.VUE_APP_API_URL || 'http://localhost:5000/api' + +export default createStore({ + state: { + token: localStorage.getItem('token') || '', + user: null, + loading: false, + error: null + }, + + getters: { + isAuthenticated: state => !!state.token, + user: state => state.user, + loading: state => state.loading, + error: state => state.error + }, + + mutations: { + AUTH_START(state) { + state.loading = true + state.error = null + }, + AUTH_SUCCESS(state, token) { + state.loading = false + state.token = token + state.error = null + }, + SET_USER(state, user) { + state.user = user + }, + AUTH_ERROR(state, error) { + state.loading = false + state.error = error + }, + CLEAR_ERROR(state) { + state.error = null + }, + LOGOUT(state) { + state.token = '' + state.user = null + } + }, + + actions: { + // Vérifier si l'utilisateur est authentifié + checkAuth({ commit, dispatch }) { + const token = localStorage.getItem('token') + if (token) { + commit('AUTH_SUCCESS', token) + dispatch('fetchUser') + } + }, + + // Connexion utilisateur + async login({ commit, dispatch }, credentials) { + commit('AUTH_START') + try { + const response = await axios.post(`${API_URL}/auth/login`, credentials) + const token = response.data.token + localStorage.setItem('token', token) + axios.defaults.headers.common['x-auth-token'] = token + commit('AUTH_SUCCESS', token) + dispatch('fetchUser') + return response + } catch (error) { + commit('AUTH_ERROR', error.response?.data?.message || 'Erreur de connexion') + localStorage.removeItem('token') + throw error + } + }, + + // Inscription utilisateur + async register({ commit, dispatch }, userData) { + commit('AUTH_START') + try { + const response = await axios.post(`${API_URL}/auth/register`, userData) + const token = response.data.token + localStorage.setItem('token', token) + axios.defaults.headers.common['x-auth-token'] = token + commit('AUTH_SUCCESS', token) + dispatch('fetchUser') + return response + } catch (error) { + commit('AUTH_ERROR', error.response?.data?.message || 'Erreur d\'inscription') + throw error + } + }, + + // Récupérer les informations de l'utilisateur + async fetchUser({ commit }) { + try { + const response = await axios.get(`${API_URL}/auth/me`, { + headers: { + 'x-auth-token': localStorage.getItem('token') + } + }) + commit('SET_USER', response.data) + } catch (error) { + console.error('Erreur lors de la récupération des informations utilisateur', error) + } + }, + + // Déconnexion + logout({ commit }) { + localStorage.removeItem('token') + delete axios.defaults.headers.common['x-auth-token'] + commit('LOGOUT') + }, + + // Effacer les erreurs + clearError({ commit }) { + commit('CLEAR_ERROR') + } + }, + + modules: { + // Modules supplémentaires pour films et séries si nécessaire + } +}) diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..f2a2398 --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..7f93fff --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/frontend/src/views/Movies.vue b/frontend/src/views/Movies.vue new file mode 100644 index 0000000..e8dd695 --- /dev/null +++ b/frontend/src/views/Movies.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/frontend/src/views/Series.vue b/frontend/src/views/Series.vue new file mode 100644 index 0000000..af9f8d5 --- /dev/null +++ b/frontend/src/views/Series.vue @@ -0,0 +1,513 @@ + + + + + diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue new file mode 100644 index 0000000..c63dbb2 --- /dev/null +++ b/frontend/src/views/Settings.vue @@ -0,0 +1,455 @@ + + + + + diff --git a/project.md b/project.md new file mode 100755 index 0000000..ebbdbff --- /dev/null +++ b/project.md @@ -0,0 +1,31 @@ +Application avec framework JS. +De préférence avec VueJS, tu peux choisr le framework pour le backoffice et la base de donnée. + +## Cahier des charges : + +### APIs +Utilisation des API de : + - https://sonarr.tv/docs/api/ + - https://radarr.video/docs/api/ + +### Authentification +Avoir un système d'identification +Un utilisateur doit être connecter pour accéder à l'application + +### Tableau de bord +Un agenda avec tous les jours pour 1 mois dans le futur. +Un point par jour avec les films et séries prévu en téléchargement par jour. + +### Films +Une page films avec : + - Tous les films + - Ceux qui sont manquants + - Ceux qui sont à venir + - Un historique des films télécharger + +### Séries +Une page séries avec : + - Tous les séries + - Ceux qui sont manquants + - Ceux qui sont à venir + - Un historique des séries télécharger \ No newline at end of file