diff --git a/javascripts/ajouter-un-album.js b/javascripts/ajouter-un-album.js index a9d81a8..3bce60c 100644 --- a/javascripts/ajouter-un-album.js +++ b/javascripts/ajouter-un-album.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -177,12 +178,15 @@ Vue.createApp({ this.submitting = true; return axios - .post("/api/v1/albums", { + .post(`/api/v1/${action}`, { album: this.details, share: this.share, }) .then(() => { - window.location.href = "/ma-collection"; + window.location.href = + action === "albums" + ? "/ma-collection" + : "/ma-liste-de-souhaits"; }) .catch((err) => { this.submitting = false; diff --git a/javascripts/collection.js b/javascripts/collection.js index 74250a1..2b25945 100644 --- a/javascripts/collection.js +++ b/javascripts/collection.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -74,7 +75,7 @@ Vue.createApp({ this.sortOrder = `${sortOrder.sort}-${sortOrder.order}`; - let url = `/api/v1/albums?page=${this.page}&sort=${this.sort}&order=${this.order}`; + let url = `/api/v1/${action}?page=${this.page}&sort=${this.sort}&order=${this.order}`; if (this.artist) { url += `&artist=${this.formatParams(this.artist)}`; } @@ -186,7 +187,7 @@ Vue.createApp({ return false; } return axios - .delete(`/api/v1/albums/${this.itemId}`) + .delete(`/api/v1/${action}/${this.itemId}`) .then(() => { this.fetch(); }) diff --git a/javascripts/mon-compte/index.js b/javascripts/mon-compte/index.js index 3546884..a71a456 100644 --- a/javascripts/mon-compte/index.js +++ b/javascripts/mon-compte/index.js @@ -19,6 +19,8 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { token: "", message: "Je viens d'ajouter {artist} - {album} à ma collection !", + wantlist: + "Je viens d'ajouter {artist} - {album} à ma liste de souhaits !", }, }, loading: false, @@ -58,8 +60,13 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { // eslint-disable-next-line no-unused-vars async updateProfil() { this.errors = []; - const { oldPassword, password, passwordConfirm, mastodon, pagination } = - this.formData; + const { + oldPassword, + password, + passwordConfirm, + mastodon, + pagination, + } = this.formData; if (password && !oldPassword) { this.errors.push("emptyPassword"); diff --git a/javascripts/mon-compte/ma-collection/details.js b/javascripts/mon-compte/ma-collection/details.js index c515d95..09932ea 100644 --- a/javascripts/mon-compte/ma-collection/details.js +++ b/javascripts/mon-compte/ma-collection/details.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ if (typeof item !== "undefined") { Vue.createApp({ data() { @@ -196,7 +197,7 @@ if (typeof item !== "undefined") { updateItem() { showToastr("Mise à jour en cours…", true); axios - .patch(`/api/v1/albums/${this.item._id}`) + .patch(`/api/v1/${action}/${this.item._id}`) .then((res) => { showToastr("Mise à jour réalisée avec succès", true); this.item = res.data; @@ -215,9 +216,12 @@ if (typeof item !== "undefined") { }, deleteItem() { axios - .delete(`/api/v1/albums/${this.item._id}`) + .delete(`/api/v1/${action}/${this.item._id}`) .then(() => { - window.location.href = "/ma-collection"; + window.location.href = + action === "albums" + ? "/ma-collection" + : "/ma-liste-de-souhaits"; }) .catch((err) => { showToastr( @@ -238,7 +242,7 @@ if (typeof item !== "undefined") { } this.shareSubmiting = true; axios - .post(`/api/v1/albums/${this.item._id}/share`, { + .post(`/api/v1/${action}/${this.item._id}/share`, { message: this.shareMessageTransformed, }) .then(() => { diff --git a/javascripts/mon-compte/ma-collection/exporter.js b/javascripts/mon-compte/ma-collection/exporter.js index b391436..90ef809 100644 --- a/javascripts/mon-compte/ma-collection/exporter.js +++ b/javascripts/mon-compte/ma-collection/exporter.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -10,7 +11,10 @@ Vue.createApp({ exportCollection(event) { event.preventDefault(); - window.open(`/api/v1/albums?exportFormat=${this.format}`, "_blank"); + window.open( + `/api/v1/${action}?exportFormat=${this.format}`, + "_blank" + ); }, }, }).mount("#exporter"); diff --git a/javascripts/mon-compte/ma-collection/importer.js b/javascripts/mon-compte/ma-collection/importer.js index 08f2b6f..6103bfa 100644 --- a/javascripts/mon-compte/ma-collection/importer.js +++ b/javascripts/mon-compte/ma-collection/importer.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -66,11 +67,11 @@ Vue.createApp({ try { const res = await axios.get( - `/api/v1/albums?discogsId=${release_id}` + `/api/v1/${action}?discogsId=${release_id}` ); if (res.status === 204) { - await axios.post("/api/v1/albums", { + await axios.post(`/api/v1/${action}`, { discogsId: release_id, share: false, }); diff --git a/src/app.js b/src/app.js index f8dd554..4f9e4aa 100644 --- a/src/app.js +++ b/src/app.js @@ -15,12 +15,14 @@ import { isXhr } from "./helpers"; import indexRouter from "./routes"; import maCollectionRouter from "./routes/ma-collection"; +import wantlistRouter from "./routes/wantlist"; import monCompteRouter from "./routes/mon-compte"; import collectionRouter from "./routes/collection"; import importJobsRouter from "./routes/jobs"; import importAlbumRouterApiV1 from "./routes/api/v1/albums"; +import importWantlistRouterApiV1 from "./routes/api/v1/wantlist"; import importSearchRouterApiV1 from "./routes/api/v1/search"; import importMastodonRouterApiV1 from "./routes/api/v1/mastodon"; import importMeRouterApiV1 from "./routes/api/v1/me"; @@ -81,9 +83,11 @@ app.use(express.static(path.join(__dirname, "../public"))); app.use("/", indexRouter); app.use("/mon-compte", monCompteRouter); app.use("/ma-collection", maCollectionRouter); +app.use("/ma-liste-de-souhaits", wantlistRouter); app.use("/collection", collectionRouter); app.use("/jobs", importJobsRouter); app.use("/api/v1/albums", importAlbumRouterApiV1); +app.use("/api/v1/wantlist", importWantlistRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1); app.use("/api/v1/mastodon", importMastodonRouterApiV1); app.use("/api/v1/me", importMeRouterApiV1); diff --git a/src/helpers/index.js b/src/helpers/index.js index d07fe14..59d92ca 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -53,3 +53,28 @@ export const isXhr = (req) => { return is; }; + +/** + * Méthode permettant de récupérer les éléments distincts d'une collection + * @param {Object} model + * @param {String} field + * @param {import("mongoose").ObjectId} user + * @returns + */ +export const getAllDistincts = async (model, field, user) => { + const distincts = await model + .find( + { + User: user, + }, + [], + { + sort: { + [field]: 1, + }, + } + ) + .distinct(field); + + return distincts; +}; diff --git a/src/middleware/Albums.js b/src/middleware/Albums.js index a095525..a6d8c60 100644 --- a/src/middleware/Albums.js +++ b/src/middleware/Albums.js @@ -12,7 +12,7 @@ import JobsModel from "../models/jobs"; import UsersModel from "../models/users"; import ErrorEvent from "../libs/error"; -import { getAlbumDetails } from "../helpers"; +import { getAlbumDetails, getAllDistincts } from "../helpers"; /** * Classe permettant la gestion des albums d'un utilisateur @@ -42,8 +42,11 @@ class Albums extends Pages { discogsId: albumDetails.id, User: user._id, }; + // eslint-disable-next-line no-nested-ternary data.released = data.released - ? new Date(data.released.replace("-00", "-01")) + ? typeof data.released === "string" + ? new Date(data.released.replace("-00", "-01")) + : data.released : null; delete data.id; @@ -142,28 +145,6 @@ Publié automatiquement via #musictopus`; return album; } - /** - * Méthode permettant de récupérer les éléments distincts d'une collection - * @param {String} field - * @param {ObjectId} user - * @return {Array} - */ - static async getAllDistincts(field, user) { - const distincts = await AlbumsModel.find( - { - User: user, - }, - [], - { - sort: { - [field]: 1, - }, - } - ).distinct(field); - - return distincts; - } - constructor(req, viewname) { super(req, viewname); @@ -178,6 +159,8 @@ Publié automatiquement via #musictopus`; "#a3be8c", "#b48ead", ]; + + this.setPageContent("action", "albums"); } /** @@ -457,20 +440,28 @@ Publié automatiquement via #musictopus`; * Méthode permettant de créer la page "ma-collection" */ async loadMyCollection() { - const artists = await Albums.getAllDistincts( + const artists = await getAllDistincts( + AlbumsModel, "artists.name", this.req.user._id ); - const formats = await Albums.getAllDistincts( + const formats = await getAllDistincts( + AlbumsModel, "formats.name", this.req.user._id ); - const years = await Albums.getAllDistincts("year", this.req.user._id); - const genres = await Albums.getAllDistincts( + const years = await getAllDistincts( + AlbumsModel, + "year", + this.req.user._id + ); + const genres = await getAllDistincts( + AlbumsModel, "genres", this.req.user._id ); - const styles = await Albums.getAllDistincts( + const styles = await getAllDistincts( + AlbumsModel, "styles", this.req.user._id ); @@ -669,11 +660,19 @@ Publié automatiquement via #musictopus`; ); } - const artists = await Albums.getAllDistincts("artists.name", userId); - const formats = await Albums.getAllDistincts("formats.name", userId); - const years = await Albums.getAllDistincts("year", userId); - const genres = await Albums.getAllDistincts("genres", userId); - const styles = await Albums.getAllDistincts("styles", userId); + const artists = await getAllDistincts( + AlbumsModel, + "artists.name", + userId + ); + const formats = await getAllDistincts( + AlbumsModel, + "formats.name", + userId + ); + const years = await getAllDistincts(AlbumsModel, "year", userId); + const genres = await getAllDistincts(AlbumsModel, "genres", userId); + const styles = await getAllDistincts(AlbumsModel, "styles", userId); this.setPageTitle(`Collection publique de ${user.username}`); this.setPageContent("username", user.username); diff --git a/src/middleware/Jobs.js b/src/middleware/Jobs.js index 91964f2..4273d83 100644 --- a/src/middleware/Jobs.js +++ b/src/middleware/Jobs.js @@ -5,6 +5,7 @@ import { getAlbumDetails } from "../helpers"; import JobsModel from "../models/jobs"; import AlbumsModel from "../models/albums"; +import WantListModel from "../models/wantlist"; class Jobs { /** @@ -51,6 +52,50 @@ class Jobs { return true; } + /** + * Méthode permettant de télécharger toute les images d'un album + * @param {ObjectId} itemId + */ + static async importAlbumForWantListAssets(itemId) { + const album = await WantListModel.findById(itemId); + + if (!album) { + throw new ErrorEvent( + 404, + "Item non trouvé", + `L'album avec l'id ${itemId} n'existe plus dans la collection` + ); + } + + const item = await getAlbumDetails(album.discogsId); + + if (!item) { + throw new ErrorEvent( + 404, + "Erreur de communication", + "Erreur lors de la récupération des informations sur Discogs" + ); + } + + if (item.thumb) { + album.thumb = await uploadFromUrl(item.thumb); + album.thumbType = "local"; + } + const { images } = item; + if (images && images.length > 0) { + for (let i = 0; i < images.length; i += 1) { + images[i].uri150 = await uploadFromUrl(images[i].uri150); + images[i].uri = await uploadFromUrl(images[i].uri); + } + } + + album.images = images; + + await album.save(); + + return true; + } + /** * Point d'entrée * @param {String} state @@ -78,6 +123,9 @@ class Jobs { case "Albums": await Jobs.importAlbumAssets(job.id); break; + case "WantList": + await Jobs.importAlbumForWantListAssets(job.id); + break; default: throw new ErrorEvent( 500, diff --git a/src/middleware/Me.js b/src/middleware/Me.js index 5dd6e3e..a4175de 100644 --- a/src/middleware/Me.js +++ b/src/middleware/Me.js @@ -26,6 +26,7 @@ class Me extends Pages { url: Joi.string().uri().allow(null, ""), token: Joi.string().allow(null, ""), message: Joi.string().allow(null, ""), + wantlist: Joi.string().allow(null, ""), }, }); diff --git a/src/middleware/Wantlist.js b/src/middleware/Wantlist.js new file mode 100644 index 0000000..ecbab7f --- /dev/null +++ b/src/middleware/Wantlist.js @@ -0,0 +1,687 @@ +import { format as formatDate } from "date-fns"; +import fs from "fs"; + +import Mastodon from "mastodon"; +import { v4 } from "uuid"; +import axios from "axios"; +import Pages from "./Pages"; +import Export from "./Export"; + +import WantListModel from "../models/wantlist"; +import JobsModel from "../models/jobs"; +import UsersModel from "../models/users"; +import ErrorEvent from "../libs/error"; + +import { getAlbumDetails, getAllDistincts } from "../helpers"; + +/** + * Classe permettant la gestion da la liste de souhaits d'un utilisateur + */ +class Wantlist extends Pages { + /** + * Méthode permettant d'ajouter un album dans une collection + * @param {Object} req + * @return {Object} + */ + static async postAddOne(req) { + const { body, user } = req; + const { share, discogsId } = body; + + let albumDetails = body.album; + if (discogsId) { + albumDetails = await getAlbumDetails(discogsId); + body.id = discogsId; + } + + if (!albumDetails) { + throw new ErrorEvent(406, "Aucun album à ajouter"); + } + + const data = { + ...albumDetails, + discogsId: albumDetails.id, + User: user._id, + }; + // eslint-disable-next-line no-nested-ternary + data.released = data.released + ? typeof data.released === "string" + ? new Date(data.released.replace("-00", "-01")) + : data.released + : null; + delete data.id; + + const album = new WantListModel(data); + + await album.save(); + + const jobData = { + model: "WantList", + id: album._id, + }; + const job = new JobsModel(jobData); + + job.save(); + + try { + const User = await UsersModel.findOne({ _id: user._id }); + + const { mastodon: mastodonConfig } = User; + + const { publish, token, url, wantlist } = mastodonConfig; + + if (share && publish && url && token) { + const M = new Mastodon({ + access_token: token, + api_url: url, + }); + + const video = + data.videos && data.videos.length > 0 + ? data.videos[0].uri + : ""; + + const status = `${( + wantlist || + "Je viens d'ajouter {artist} - {album} à ma liste de souhaits !" + ) + .replaceAll("{artist}", data.artists[0].name) + .replaceAll("{format}", data.formats[0].name) + .replaceAll("{genres}", data.genres.join()) + .replaceAll("{styles}", data.styles.join()) + .replaceAll("{year}", data.year) + .replaceAll("{video}", video) + .replaceAll("{album}", data.title)} + +Publié automatiquement via #musictopus`; + + const media_ids = []; + + if (data.images.length > 0) { + for (let i = 0; i < data.images.length; i += 1) { + if (media_ids.length === 4) { + break; + } + + const filename = `${v4()}.jpg`; + const file = `/tmp/${filename}`; + + // eslint-disable-next-line no-await-in-loop + const { data: buff } = await axios.get( + data.images[i].uri, + { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + }, + responseType: "arraybuffer", + } + ); + + fs.writeFileSync(file, buff); + + // eslint-disable-next-line no-await-in-loop + const { data: media } = await M.post("media", { + file: fs.createReadStream(file), + }); + + const { id } = media; + + media_ids.push(id); + + fs.unlinkSync(file); + } + } + + await M.post("statuses", { status, media_ids }); + } + } catch (err) { + throw new ErrorEvent( + 500, + "Mastodon", + "Album ajouté à votre collection mais impossible de publier sur Mastodon" + ); + } + + return album; + } + + constructor(req, viewname) { + super(req, viewname); + + this.colors = [ + "#2e3440", + "#d8dee9", + "#8fbcbb", + "#5e81ac", + "#d08770", + "#bf616a", + "#ebcb8b", + "#a3be8c", + "#b48ead", + ]; + + this.setPageContent("action", "wantlist"); + } + + /** + * Méthode permettant de récupérer la liste des albums d'une collection + * @return {Object} + */ + async getAll() { + const { + page, + exportFormat = "json", + sort = "artists_sort", + order = "asc", + artist, + format, + year, + genre, + style, + userId: collectionUserId, + discogsIds, + discogsId, + } = this.req.query; + + const limit = this.req.user?.pagination || 16; + + let userId = this.req.user?._id; + + const where = {}; + + if (artist) { + where["artists.name"] = artist; + } + if (format) { + where["formats.name"] = format; + } + if (year) { + where.year = year; + } + if (genre) { + where.genres = genre; + } + if (style) { + where.styles = style; + } + + if (!this.req.user && !collectionUserId) { + throw new ErrorEvent( + 401, + "Collection", + "Cette collection n'est pas publique" + ); + } + + if (collectionUserId) { + const userIsSharingCollection = await UsersModel.findById( + collectionUserId + ); + + if ( + !userIsSharingCollection || + !userIsSharingCollection.isPublicCollection + ) { + throw new ErrorEvent( + 401, + "Collection", + "Cette collection n'est pas publique" + ); + } + + userId = userIsSharingCollection._id; + } + + if (discogsIds) { + where.discogsId = { $in: discogsIds }; + } + if (discogsId) { + where.discogsId = Number(discogsId); + } + + const count = await WantListModel.count({ + User: userId, + ...where, + }); + + let params = { + sort: { + [sort]: order.toLowerCase() === "asc" ? 1 : -1, + }, + }; + + if (page && limit) { + const skip = (page - 1) * limit; + + params = { + ...params, + skip, + limit, + }; + } + + const rows = await WantListModel.find( + { + User: userId, + ...where, + }, + [], + params + ); + + switch (exportFormat) { + case "csv": + return Export.convertToCsv(rows); + case "xls": + return Export.convertToXls(rows); + case "xml": + return Export.convertToXml(rows); + case "musictopus": + return Export.convertToMusicTopus(rows); + case "json": + default: + return { + rows, + limit, + count, + }; + } + } + + /** + * Méthode permettant de récupérer le détails d'un album + * + * @return {Object} + */ + async getOne() { + const { itemId: _id } = this.req.params; + const { _id: User } = this.req.user; + const album = await WantListModel.findOne({ + _id, + User, + }); + + return { + ...album.toJSON(), + released: album.released + ? formatDate(album.released, "MM/dd/yyyy") + : null, + }; + } + + /** + * Méthode permettant de mettre à jour un album + * + * @return {Object} + */ + async patchOne() { + const { itemId: _id } = this.req.params; + const { _id: User } = this.req.user; + const query = { + _id, + User, + }; + const album = await WantListModel.findOne(query); + + if (!album) { + throw new ErrorEvent( + 404, + "Mise à jour", + "Impossible de trouver cet album" + ); + } + + const values = await getAlbumDetails(album.discogsId); + + await WantListModel.findOneAndUpdate(query, values, { new: true }); + + return this.getOne(); + } + + /** + * Méthode permettant de supprimer un élément d'une collection + * @return {Boolean} + */ + async deleteOne() { + const res = await WantListModel.findOneAndDelete({ + User: this.req.user._id, + _id: this.req.params.itemId, + }); + + if (res) { + return true; + } + + throw new ErrorEvent( + 404, + "Suppression", + "Impossible de trouver cet album" + ); + } + + async shareOne() { + const { message: status } = this.req.body; + const { itemId: _id } = this.req.params; + const { _id: User } = this.req.user; + const query = { + _id, + User, + }; + + const album = await WantListModel.findOne(query); + + if (!album) { + throw new ErrorEvent( + 404, + "Mise à jour", + "Impossible de trouver cet album" + ); + } + + const { mastodon: mastodonConfig } = this.req.user; + const { publish, token, url } = mastodonConfig; + + if (publish && url && token) { + const M = new Mastodon({ + access_token: token, + api_url: url, + }); + + const media_ids = []; + + if (album.images.length > 0) { + for (let i = 0; i < album.images.length; i += 1) { + if (media_ids.length === 4) { + break; + } + + const filename = `${v4()}.jpg`; + const file = `/tmp/${filename}`; + + // eslint-disable-next-line no-await-in-loop + const { data: buff } = await axios.get( + album.images[i].uri, + { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + }, + responseType: "arraybuffer", + } + ); + + fs.writeFileSync(file, buff); + + // eslint-disable-next-line no-await-in-loop + const { data: media } = await M.post("media", { + file: fs.createReadStream(file), + }); + + const { id } = media; + + media_ids.push(id); + + fs.unlinkSync(file); + } + } + + await M.post("statuses", { status, media_ids }); + } else { + throw new ErrorEvent( + 406, + `Vous n'avez pas configuré vos options de partage sur votre compte` + ); + } + + return true; + } + + /** + * Méthode permettant de créer la page "ma-collection" + */ + async loadMyCollection() { + const artists = await getAllDistincts( + WantListModel, + "artists.name", + this.req.user._id + ); + const formats = await getAllDistincts( + WantListModel, + "formats.name", + this.req.user._id + ); + const years = await getAllDistincts( + WantListModel, + "year", + this.req.user._id + ); + const genres = await getAllDistincts( + WantListModel, + "genres", + this.req.user._id + ); + const styles = await getAllDistincts( + WantListModel, + "styles", + this.req.user._id + ); + + this.setPageContent("artists", artists); + this.setPageContent("formats", formats); + this.setPageContent("years", years); + this.setPageContent("genres", genres); + this.setPageContent("styles", styles); + this.setPageTitle("Ma collection"); + } + + /** + * Méthode permettant d'afficher le détails d'un album + */ + async loadItem() { + const item = await this.getOne(); + + this.setPageContent("item", item); + this.setPageTitle( + `Détails de l'album ${item.title} de ${item.artists_sort}` + ); + } + + /** + * Méthode permettant de choisir un album de manière aléatoire dans la collection d'un utilisateur + */ + async onAir() { + const { _id: User } = this.req.user; + const count = await WantListModel.count({ + User, + }); + + const items = await WantListModel.find( + { + User, + }, + [], + { + skip: Math.floor(Math.random() * (count + 1)), + limit: 1, + } + ); + + this.req.params.itemId = items[0]._id; + + await this.loadItem(); + } + + /** + * Méthode permettant d'afficher des statistiques au sujet de ma collection + */ + async statistics() { + const { _id: User } = this.req.user; + const top = {}; + const byGenres = {}; + const byStyles = {}; + const byFormats = {}; + const top10 = []; + let byStyles10 = []; + const max = this.colors.length - 1; + + const colorsCount = this.colors.length; + + const albums = await WantListModel.find({ + User, + artists: { $exists: true, $not: { $size: 0 } }, + }); + + for (let i = 0; i < albums.length; i += 1) { + const currentFormats = []; + const { artists, genres, styles, formats } = albums[i]; + + // INFO: On regroupe les artistes par nom pour en faire le top10 + for (let j = 0; j < artists.length; j += 1) { + const { name } = artists[j]; + if (!top[name]) { + top[name] = { + name, + count: 0, + }; + } + top[name].count += 1; + } + + // INFO: On regroupe les genres + for (let j = 0; j < genres.length; j += 1) { + const name = genres[j]; + if (!byGenres[name]) { + byGenres[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byGenres).length % colorsCount + ], + }; + } + + byGenres[name].count += 1; + } + + // INFO: On regroupe les styles + for (let j = 0; j < styles.length; j += 1) { + const name = styles[j]; + if (!byStyles[name]) { + byStyles[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byStyles).length % colorsCount + ], + }; + } + + byStyles[name].count += 1; + } + + // INFO: On regroupe les formats + for (let j = 0; j < formats.length; j += 1) { + const { name } = formats[j]; + // INFO: On évite qu'un album avec 2 vinyles soit compté 2x + if (!currentFormats.includes(name)) { + if (!byFormats[name]) { + byFormats[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byFormats).length % colorsCount + ], + }; + } + + byFormats[name].count += 1; + currentFormats.push(name); + } + } + } + + // INFO: On convertit le top en tableau + Object.keys(top).forEach((index) => { + top10.push(top[index]); + }); + + // INFO: On convertit les styles en tableau + Object.keys(byStyles).forEach((index) => { + byStyles10.push(byStyles[index]); + }); + + // INFO: On ordonne les artistes par quantité d'albums + top10.sort((a, b) => (a.count > b.count ? -1 : 1)); + + // INFO: On ordonne les styles par quantité + byStyles10.sort((a, b) => (a.count > b.count ? -1 : 1)); + const tmp = []; + + // INFO: On recupère le top N des styles et on mets le reste dans le label "autre" + for (let i = 0; i < byStyles10.length; i += 1) { + if (i < max) { + tmp.push({ + ...byStyles10[i], + color: this.colors[max - i], + }); + } else if (i === max) { + tmp.push({ + name: "Autre", + count: 0, + color: this.colors[0], + }); + tmp[max].count += byStyles10[i].count; + } else { + tmp[max].count += byStyles10[i].count; + } + } + byStyles10 = tmp; + + this.setPageTitle("Mes statistiques"); + this.setPageContent("top10", top10.splice(0, 10)); + this.setPageContent("byGenres", byGenres); + this.setPageContent("byStyles", byStyles10); + this.setPageContent("byFormats", byFormats); + } + + /** + * Méthode permettant de créer la page "collection/:userId" + */ + async loadPublicCollection() { + const { userId } = this.req.params; + + const user = await UsersModel.findById(userId); + + if (!user || !user.isPublicCollection) { + throw new ErrorEvent( + 401, + "Collection non partagée", + "Cet utilisateur ne souhaite pas partager sa collection" + ); + } + + const artists = await getAllDistincts( + WantListModel, + "artists.name", + userId + ); + const formats = await getAllDistincts( + WantListModel, + "formats.name", + userId + ); + const years = await getAllDistincts(WantListModel, "year", userId); + const genres = await getAllDistincts(WantListModel, "genres", userId); + const styles = await getAllDistincts(WantListModel, "styles", userId); + + this.setPageTitle(`Collection publique de ${user.username}`); + this.setPageContent("username", user.username); + this.setPageContent("artists", artists); + this.setPageContent("formats", formats); + this.setPageContent("years", years); + this.setPageContent("genres", genres); + this.setPageContent("styles", styles); + } +} + +export default Wantlist; diff --git a/src/models/users.js b/src/models/users.js index 0b17802..e0c8a5d 100644 --- a/src/models/users.js +++ b/src/models/users.js @@ -38,6 +38,7 @@ const UserSchema = new mongoose.Schema( token: String, url: String, message: String, + wantlist: String, }, }, { diff --git a/src/models/wantlist.js b/src/models/wantlist.js new file mode 100644 index 0000000..1f8b334 --- /dev/null +++ b/src/models/wantlist.js @@ -0,0 +1,37 @@ +import mongoose from "mongoose"; + +const { Schema } = mongoose; + +const WantListSchema = new mongoose.Schema( + { + User: { + type: Schema.Types.ObjectId, + ref: "Users", + }, + discogsId: Number, + year: Number, + released: Date, + uri: String, + artists: Array, + artists_sort: String, + labels: Array, + series: Array, + companies: Array, + formats: Array, + title: String, + country: String, + notes: String, + identifiers: Array, + videos: Array, + genres: Array, + styles: Array, + tracklist: Array, + extraartists: Array, + images: Array, + thumb: String, + thumbType: String, + }, + { timestamps: true } +); + +export default mongoose.model("WantList", WantListSchema); diff --git a/src/routes/api/v1/wantlist.js b/src/routes/api/v1/wantlist.js new file mode 100644 index 0000000..82738b5 --- /dev/null +++ b/src/routes/api/v1/wantlist.js @@ -0,0 +1,84 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import { sendResponse } from "../../../libs/format"; +import Albums from "../../../middleware/Wantlist"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router + .route("/") + .get(async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.getAll(); + const { exportFormat = "json" } = req.query; + + switch (exportFormat) { + case "csv": + case "musictopus": + res.header("Content-Type", "text/csv"); + res.attachment("export-musictopus.csv"); + return res.status(200).send(data); + case "xml": + res.header("Content-type", "text/xml"); + res.attachment("export-musictopus.xml"); + return res.status(200).send(data); + case "xls": + return data.write("musictopus.xls", res); + case "json": + default: + return sendResponse(req, res, data); + } + } catch (err) { + return next(err); + } + }) + .post(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const data = await Albums.postAddOne(req); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +router + .route("/:itemId") + .patch(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.patchOne(); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }) + .delete(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.deleteOne(); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +router + .route("/:itemId/share") + .post(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.shareOne(); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +export default router; diff --git a/src/routes/index.js b/src/routes/index.js index faa4142..301f25e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -104,8 +104,23 @@ router try { const page = new Pages(req, "ajouter-un-album"); + page.setPageContent("action", "albums"); page.setPageTitle("Ajouter un album"); + render(res, page); + } catch (err) { + next(err); + } + }); +router + .route("/ajouter-a-ma-liste-de-souhaits") + .get(ensureLoggedIn("/connexion"), (req, res, next) => { + try { + const page = new Pages(req, "ajouter-un-album"); + + page.setPageContent("action", "wantlist"); + page.setPageTitle("Ajouter un album à ma liste de souhaits"); + render(res, page); } catch (err) { next(err); diff --git a/src/routes/wantlist.js b/src/routes/wantlist.js new file mode 100644 index 0000000..6c5394d --- /dev/null +++ b/src/routes/wantlist.js @@ -0,0 +1,99 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import Albums from "../middleware/Wantlist"; + +import render from "../libs/format"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "collection"); + + await page.loadMyCollection(); + + if (page.getPageContent("artists").length > 0) { + render(res, page); + } else { + res.redirect("/ajouter-a-ma-liste-de-souhaits"); + } + } catch (err) { + next(err); + } +}); + +router + .route("/on-air") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/details"); + + await page.onAir(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +router + .route("/statistiques") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums( + req, + "mon-compte/ma-collection/statistiques" + ); + + await page.statistics(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +router + .route("/exporter") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/exporter"); + + page.setPageTitle("Exporter ma collection"); + + render(res, page); + } catch (err) { + next(err); + } + }); +router + .route("/importer") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/importer"); + + page.setPageTitle("Importer une collection"); + + render(res, page); + } catch (err) { + next(err); + } + }); + +router + .route("/:itemId") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/details"); + + await page.loadItem(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +export default router; diff --git a/views/index.ejs b/views/index.ejs index cc50e24..f78f5e7 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -58,12 +58,18 @@ @@ -121,12 +127,16 @@ Ma collection + + Ma liste de souhaits + On air Statistiques +
Exporter ma collection @@ -134,6 +144,13 @@ Importer une collection
+ + Exporter ma liste de souhaits + + + Importer une liste de souhaits + +
Déconnexion @@ -217,4 +234,4 @@

- + \ No newline at end of file diff --git a/views/pages/ajouter-un-album.ejs b/views/pages/ajouter-un-album.ejs index f8f7c96..a9a1c2d 100644 --- a/views/pages/ajouter-un-album.ejs +++ b/views/pages/ajouter-un-album.ejs @@ -200,4 +200,5 @@ \ No newline at end of file diff --git a/views/pages/collection.ejs b/views/pages/collection.ejs index 8d18ab2..317ba9b 100644 --- a/views/pages/collection.ejs +++ b/views/pages/collection.ejs @@ -3,19 +3,23 @@ %>
-

- <% if ( pageType === 'private' ) { - __append('Ma collection '); - } else { - __append(`Collection de ${page.username}`); - } %> -

- <% if ( pageType === 'private' ) { %> -
- - Voir ma collection partagée - -
+ <% if (page.action === 'albums') { %> +

+ <% if ( pageType === 'private' ) { + __append('Ma collection '); + } else { + __append(`Collection de ${page.username}`); + } %> +

+ <% if ( pageType === 'private' ) { %> +
+ + Voir ma collection partagée + +
+ <% } %> + <% } else { %> +

Ma liste de souhaits

<% } %> <%- include('../components/filters/index') %> @@ -30,7 +34,7 @@
<% if ( pageType === 'private' ) { %> - {{ renderAlbumTitle(item) }} + {{ renderAlbumTitle(item) }} <% } else { %> {{ item.artists_sort}} - {{ item.title }} @@ -39,7 +43,7 @@
<% if ( pageType === 'private' ) { %> - + <% } else { %> <% } %> @@ -147,6 +151,7 @@ \ No newline at end of file diff --git a/views/pages/mon-compte/ma-collection/exporter.ejs b/views/pages/mon-compte/ma-collection/exporter.ejs index 28cc559..09a0ca1 100644 --- a/views/pages/mon-compte/ma-collection/exporter.ejs +++ b/views/pages/mon-compte/ma-collection/exporter.ejs @@ -1,5 +1,9 @@
-

Exporter ma collection

+ <% if (page.action === 'albums') { %> +

Exporter ma collection

+ <% } else { %> +

Exporter ma liste de souhaits

+ <% } %>

Les formats CSV et Excel sont facilement lisiblent par un humain. Dans ces 2 formats vous trouverez seulement les informations principales de vos albums, à savoir :

@@ -44,4 +48,8 @@ Exporter -
\ No newline at end of file +
+ + \ No newline at end of file diff --git a/views/pages/mon-compte/ma-collection/importer.ejs b/views/pages/mon-compte/ma-collection/importer.ejs index f1a8b49..1383d16 100644 --- a/views/pages/mon-compte/ma-collection/importer.ejs +++ b/views/pages/mon-compte/ma-collection/importer.ejs @@ -1,7 +1,11 @@
-

Importer une collection

+ <% if (page.action === 'albums') { %> +

Importer une collection

+ <% } else { %> +

Importer une liste de souhaits

+ <% } %>

- Il est actuellement possible d'importer une collection provenant de discogs. + Il est actuellement possible d'importer <%= page.action === 'albums' ? "une collection" : "une liste de souhaits" %> provenant de discogs.
Vous devez dans un premier temps vous rendre sur la page Exporter de discogs.
@@ -40,4 +44,8 @@ Importatation terminée -

\ No newline at end of file + + + \ No newline at end of file