nodecdtheque => MyMusicLibrary & new UI for adding album

This commit is contained in:
Damien Broqua 2022-02-17 09:37:25 +01:00
parent a35c8899ac
commit a502fe9088
14 changed files with 231 additions and 37 deletions

View file

@ -1,6 +1,6 @@
# nodecdtheque # My Music Library
NodeCDThèque (non temporaire faute de mieux haha) est une application Web permettant de lister votre collection de CD ou vinyles. My Music Library est une application Web permettant de lister votre collection de CD ou vinyles.
## Prérequis ## Prérequis

View file

@ -1,8 +1,8 @@
version: "2.4" version: "2.4"
services: services:
nodecdtheque-www: mymusiclibrary-www:
container_name: nodecdtheque-www container_name: mymusiclibrary-www
image: "node:16" image: "node:16"
restart: always restart: always
user: "node" user: "node"
@ -17,21 +17,21 @@ services:
ports: ports:
- 3001:3001 - 3001:3001
depends_on: depends_on:
- nodecdtheque-db - mymusiclibrary-db
environment: environment:
NODE_ENV: ${NODE_ENV} NODE_ENV: ${NODE_ENV}
DISCOGS_TOKEN: ${DISCOGS_TOKEN} DISCOGS_TOKEN: ${DISCOGS_TOKEN}
networks: networks:
- nodecdtheque - mymusiclibrary
nodecdtheque-db: mymusiclibrary-db:
container_name: nodecdtheque-db container_name: mymusiclibrary-db
image: mongo:latest image: mongo:latest
restart: always restart: always
ports: ports:
- 27617:27017 - 27617:27017
networks: networks:
- nodecdtheque - mymusiclibrary
networks: networks:
nodecdtheque: mymusiclibrary:
driver: bridge driver: bridge

View file

@ -1,5 +1,5 @@
{ {
"name": "nodecdtheque", "name": "mymusiclibrary",
"version": "1.0.0", "version": "1.0.0",
"description": "Simple application to manage your CD/Vinyl collection", "description": "Simple application to manage your CD/Vinyl collection",
"scripts": { "scripts": {
@ -18,7 +18,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@git.darkou.fr:dbroqua/nodecdtheque.git" "url": "git@git.darkou.fr:dbroqua/MyMusicLibrary.git"
}, },
"author": { "author": {
"name": "Damien Broqua", "name": "Damien Broqua",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

4
sass/bulma.scss vendored
View file

@ -1 +1,5 @@
@use '../node_modules/bulma/bulma.sass'; @use '../node_modules/bulma/bulma.sass';
.modal-content {
width: 80%;
}

View file

@ -13,8 +13,9 @@ import { isXhr } from "./helpers";
import indexRouter from "./routes"; import indexRouter from "./routes";
import addAlbumRouter from "./routes/addAlbum"; import addAlbumRouter from "./routes/addAlbum";
import importRouterApiV1 from "./routes/api/v1";
import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importAlbumRouterApiV1 from "./routes/api/v1/albums";
import importSearchRouterApiV1 from "./routes/api/v1/search";
// Mongoose schema init // Mongoose schema init
require("./models/users"); require("./models/users");
@ -81,8 +82,8 @@ app.use(
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/ajouter-un-album", addAlbumRouter); app.use("/ajouter-un-album", addAlbumRouter);
app.use("/api/v1", importRouterApiV1);
app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/albums", importAlbumRouterApiV1);
app.use("/api/v1/search", importSearchRouterApiV1);
// Handle 404 // Handle 404
app.use((req, res) => { app.use((req, res) => {

View file

@ -9,7 +9,7 @@ import http from "http";
import app from "../app"; import app from "../app";
import { port } from "../config"; import { port } from "../config";
const debug = debugLib("nodecdtheque:server"); const debug = debugLib("mymusiclibrary:server");
const server = http.createServer(app); const server = http.createServer(app);
/** /**

View file

@ -1,7 +1,8 @@
module.exports = { module.exports = {
nodeEnv: process.env.NODE_ENV || "development", nodeEnv: process.env.NODE_ENV || "development",
port: parseInt(process.env.PORT || "3001", 10), port: parseInt(process.env.PORT || "3001", 10),
mongoDbUri: process.env.MONGODB_URI || "mongodb://nodecdtheque-db/cdtheque", mongoDbUri:
process.env.MONGODB_URI || "mongodb://mymusiclibrary-db/mymusiclibrary",
secret: process.env.SECRET || "waemaeMe5ahc6ce1chaeKohKa6Io8Eik", secret: process.env.SECRET || "waemaeMe5ahc6ce1chaeKohKa6Io8Eik",
discogsToken: process.env.DISCOGS_TOKEN, discogsToken: process.env.DISCOGS_TOKEN,
}; };

View file

@ -38,6 +38,10 @@ class Pages {
this.pageContent.page[field] = value; this.pageContent.page[field] = value;
} }
getPageContent(field) {
return this.pageContent.page[field];
}
/** /**
* Rendu de la page * Rendu de la page
* @return {Object} * @return {Object}

View file

@ -2,17 +2,27 @@ import express from "express";
import { ensureLoggedIn } from "connect-ensure-login"; import { ensureLoggedIn } from "connect-ensure-login";
import { sendResponse } from "../../../libs/format"; import { sendResponse } from "../../../libs/format";
import { searchSong } from "../../../helpers"; import { searchSong, getAlbumDetails } from "../../../helpers";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const router = express.Router(); const router = express.Router();
router router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
.route("/search")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try { try {
const data = await searchSong(req.query.q); const data = await searchSong(req.query.q);
sendResponse(req, res, data);
} catch (err) {
next(err);
}
});
router
.route("/:discogsId")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const data = await getAlbumDetails(req.params.discogsId);
sendResponse(req, res, data); sendResponse(req, res, data);
} catch (err) { } catch (err) {
next(err); next(err);

View file

@ -12,15 +12,12 @@ import render from "../libs/format";
const router = express.Router(); const router = express.Router();
router.route("/").get((req, res, next) => { router.route("/").get((req, res, next) => {
if (req.user) {
return res.redirect("/ma-collection");
}
try { try {
const page = new Pages(req, "home"); const page = new Pages(req, "home");
return render(res, page); render(res, page);
} catch (err) { } catch (err) {
return next(err); next(err);
} }
}); });
@ -88,7 +85,11 @@ router
await page.loadMyCollection(); await page.loadMyCollection();
if (page.getPageContent("artists").length > 0) {
render(res, page); render(res, page);
} else {
res.redirect("/ajouter-un-album");
}
} catch (err) { } catch (err) {
next(err); next(err);
} }

View file

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title><% if (page.title) { %><%= page.title %> <% } else { %> DarKou - Ma CDThèque <% } %></title> <title><% if (page.title) { %><%= page.title %> <% } else { %> DarKou - My Music Library <% } %></title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
@ -55,7 +55,8 @@
<nav class="navbar is-fixed-top is-light" role="navigation" aria-label="main navigation"> <nav class="navbar is-fixed-top is-light" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img src="/logo.png" alt="Ma CDThèque"> <img src="/logo.png" alt="Logo">
<h1 class="title ml-2">My Music Library</h1>
</a> </a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
@ -87,7 +88,12 @@
<% if ( user ) { %> <% if ( user ) { %>
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
Mon compte <span class="icon">
<i class="fa-solid fa-user"></i>
</span>
<span>
<%= user.username %>
</span>
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
@ -165,7 +171,7 @@
<footer class="footer"> <footer class="footer">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p> <p>
<strong>Ma CDThèque</strong> par <a href="https://www.darkou.fr">Damien Broqua</a>. <strong title="Merci Brunus ! 😜">My Music Library</strong> par <a href="https://www.darkou.fr">Damien Broqua</a>.
Fait avec ❤ à Bordeaux. Fait avec ❤ à Bordeaux.
Le code source est sous licence <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU GPL-3.0-or-later</a>. Le code source est sous licence <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU GPL-3.0-or-later</a>.
</p> </p>

View file

@ -36,7 +36,8 @@
<img :src="item.thumb" :alt="item.title" style="max-width: 120px;"/> <img :src="item.thumb" :alt="item.title" style="max-width: 120px;"/>
</td> </td>
<td> <td>
<a :href="'/ajouter-un-album/' + item.id">{{ item.title }}</a> <a @click="loadDetails(item.id)">{{ item.title }}</a>
<!-- <a :href="'/ajouter-un-album/' + item.id">{{ item.title }}</a> -->
</td> </td>
<td>{{ item.year }}</td> <td>{{ item.year }}</td>
<td>{{ item.country }}</td> <td>{{ item.country }}</td>
@ -58,6 +59,120 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="modal" :class="{'is-active': modalIsVisible}" id="addAlbum">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{details.artists_sort}} - {{details.title}}</p>
<button class="delete" aria-label="close" @click="toggleModal"></button>
</header>
<section class="modal-card-body">
<div class="columns is-mobile">
<div class="column is-12-mobile is-4-desktop">
<div class="has-text-centered">
<img :src="details.thumb %>" alt="Miniature" />
<hr />
<img v-for="image in details.images" :src="image.uri150" alt="Miniature" style="max-width: 60px;" />
<hr />
</div>
<ol class="ml-4">
<li v-for="track in details.tracklist">{{ track.title }} ({{track.duration}})</li>
</ol>
</div>
<div class="column is-12-mobile is-8-desktop">
<div class="column is-12">
<div class="columns is-mobile is-multiline">
<div class="column is-12-mobile is-6-desktop" v-for="genre in details.genres">
<div class="field">
<label class="label" for="company">Genre</label>
<div class="control">
<input type="text" id="genres" name="genres" class="input" v-model="genre" disabled />
</div>
</div>
</div>
<div class="column is-12-mobile is-6-desktop" v-for="style in details.styles">
<div class="field">
<label class="label" for="company">Style</label>
<div class="control">
<input type="text" id="style" name="style" class="input" v-model="style" disabled />
</div>
</div>
</div>
</div>
<hr />
<div class="columns is-mobile">
<div class="column is-12-mobile is-6-desktop">
<div class="field">
<label class="label" for="year">Année</label>
<div class="control">
<input type="text" id="year" name="year" class="input" v-model="details.year" disabled />
</div>
</div>
</div>
<div class="column is-12-mobile is-6-desktop">
<div class="field">
<label class="label" for="released">Date de sortie</label>
<div class="control">
<input type="text" id="released" name="released" class="input" v-model="details.released" disabled />
</div>
</div>
</div>
</div>
<hr />
<div class="columns is-mobile">
<div class="column is-12-mobile is-6-desktop">
<div class="field">
<label class="label" for="country">Pays</label>
<div class="control">
<input type="text" id="country" name="country" class="input" v-model="details.country" disabled />
</div>
</div>
</div>
<div class="column is-12-mobile is-6-desktop" v-for="format in details.formats">
<div class="field">
<label class="label" for="format">Format</label>
<div class="control">
<input type="text" id="format" name="format" class="input" v-model="format.name" disabled />
</div>
</div>
</div>
</div>
<hr />
<div class="columns is-mobile">
<div class="column is-12-mobile is-6-desktop">
<span class="label">Codes barres</span>
<ol>
<li v-for="identifier in details.identifiers">
{{identifier.value}} ({{identifier.type}})
</li>
</ol>
</div>
<div class="column is-12-mobile is-6-desktop">
<span class="label">Label</span>
<div class="field" v-for="label in details.labels">
<div class="control">
<input type="text" name="label" class="input" v-model="label.name" disabled />
</div>
</div>
<span class="label">Société</span>
<div class="field" v-for="company in details.companies">
<div class="control">
<input type="text" name="company" class="input" v-model="company.name" disabled />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" @click="add">Ajouter</button>
<button class="button" @click="toggleModal">Annuler</button>
</footer>
</div>
<button class="modal-close is-large" aria-label="close" @click="toggleModal"></button>
</div>
</div> </div>
<script> <script>
Vue.createApp({ Vue.createApp({
@ -66,6 +181,8 @@
q: '', q: '',
loading: false, loading: false,
items: [], items: [],
details: {},
modalIsVisible: false,
} }
}, },
methods: { methods: {
@ -116,7 +233,38 @@
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
}); });
} },
toggleModal() {
this.modalIsVisible = !this.modalIsVisible;
},
loadDetails(discogsId) {
console.log('discogsId:', discogsId);
axios.get(`/api/v1/search/${discogsId}`)
.then( response => {
const {
data,
} = response;
this.details = data;
this.toggleModal();
})
.catch((err) => {
showToastr(err.response?.data?.message || "Impossible de charger les détails de cet album");
})
.finally(() => {
this.loading = false;
});
},
add() {
axios.post('/api/v1/albums', this.details)
.then(() => {
window.location.href = '/ma-collection';
})
.catch((err) => {
showToastr(err.response?.data?.message || "Impossible d'ajouter ce album pour le moment…");
});
},
} }
}).mount('#app') }).mount('#app')
</script> </script>

View file

@ -1,16 +1,35 @@
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<div class="header"></div> <div class="header"></div>
<h1 class="title"> <h1 class="title is-1">
Ma CDThèque My Music Library
</h1> </h1>
<p class="subtitle"> <p class="subtitle">
Retrouvez votre CDThèque partout depuis votre PC ou votre smartphone. Retrouvez votre cdthèque partout depuis votre PC ou votre smartphone.
</p> </p>
<p> <p>
Ma CDThèque est un site web permettant de sauvegarder votre liste des CDs ou Vinyles et de la retrouver facilement et n'importe ou ! <strong>My Music Library</strong> est un site web permettant de sauvegarder votre liste des CDs ou Vinyles et de la retrouver facilement et n'importe ou !
<br /> <br />
Le code source est publiée sous licence <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU GPL-3.0-or-later</a>. Le code source est librement accessible sur <a href="https://git.darkou.fr/dbroqua/nodecdtheque">git.darkou.fr</a>. Le code source est publiée sous licence <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html" target="_blank" rel="noopener noreferrer">GNU GPL-3.0-or-later</a>. Le code source est librement accessible sur <a href="https://git.darkou.fr/dbroqua/MyMusicLibrary" target="_blank">git.darkou.fr</a>.
</p> </p>
<h2 class="title is-2">
Pourquoi ?
</h2>
<p>
<strong>My Music Library</strong> est né d'un besoin personnel lorsque ma collecion de CD est devenu un peu trop grosse pour m'en souvenir et après avoir acheté par accident plusieurs fois le même CD…
<br />
Il était donc temps de trouver une solution ! Il existe plusieurs logiciel de gestion de librairies musicales mais, à ma connaissance, aucun facilement accessible via internet (genre calé sous la couette ou dans un magasin de musique 😜).
</p>
<h2 class="title is-2">
Comment ?
</h2>
<p>
Alors là clairement j'ai pas trop réfléchi… <a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">NodeJS</a> forcément, déformation pro 😅.
<br />
Pour la base de données je suis parti sur <a href="https://www.mongodb.com/" target="_blank" rel="noopener noreferrer">MongoDB</a> qui me semblait être le plus logique pour stocker une telle collection sans avoir plein de relations dans tous les sens (artiste, pays, genre, style etc…).
<br />
Pour le front j'ai voulu innover en n'utilisant pas <a href="https://jquery.com/" target="_blank" rel="noopener noreferrer">jQuery</a> et <a href="https://getbootstrap.com/" target="_blank" rel="noopener noreferrer">Boostrap</a> mais <a href="https://vuejs.org/" target="_blank" rel="noopener noreferrer">VueJS</a> et <a href="https://bulma.io/" target="_blank" rel="noopener noreferrer">Bulma</a> !
</p>
</div> </div>
</section> </section>