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 AlbumsModel from "../models/albums"; import JobsModel from "../models/jobs"; import UsersModel from "../models/users"; import ErrorEvent from "../libs/error"; import { getAlbumDetails } from "../helpers"; /** * Classe permettant la gestion des albums d'un utilisateur */ class Albums 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, }; data.released = data.released ? new Date(data.released.replace("-00", "-01")) : null; delete data.id; const album = new AlbumsModel(data); await album.save(); const jobData = { model: "Albums", 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, message } = 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 = `${( message || "Je viens d'ajouter {artist} - {album} à ma collection !" ) .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; } /** * 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); this.colors = [ "#2e3440", "#d8dee9", "#8fbcbb", "#5e81ac", "#d08770", "#bf616a", "#ebcb8b", "#a3be8c", "#b48ead", ]; } /** * 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 AlbumsModel.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 AlbumsModel.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 AlbumsModel.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 AlbumsModel.findOne(query); if (!album) { throw new ErrorEvent( 404, "Mise à jour", "Impossible de trouver cet album" ); } const values = await getAlbumDetails(album.discogsId); await AlbumsModel.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 AlbumsModel.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 AlbumsModel.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 Albums.getAllDistincts( "artists.name", this.req.user._id ); const formats = await Albums.getAllDistincts( "formats.name", this.req.user._id ); const years = await Albums.getAllDistincts("year", this.req.user._id); const genres = await Albums.getAllDistincts( "genres", this.req.user._id ); const styles = await Albums.getAllDistincts( "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 AlbumsModel.count({ User, }); const items = await AlbumsModel.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 AlbumsModel.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 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); 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 Albums;