@93 - Wantlist

This commit is contained in:
Damien Broqua 2024-06-15 10:13:22 +02:00
parent bed5139a27
commit a4a3933c6d
24 changed files with 1137 additions and 70 deletions

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
Vue.createApp({ Vue.createApp({
data() { data() {
return { return {
@ -177,12 +178,15 @@ Vue.createApp({
this.submitting = true; this.submitting = true;
return axios return axios
.post("/api/v1/albums", { .post(`/api/v1/${action}`, {
album: this.details, album: this.details,
share: this.share, share: this.share,
}) })
.then(() => { .then(() => {
window.location.href = "/ma-collection"; window.location.href =
action === "albums"
? "/ma-collection"
: "/ma-liste-de-souhaits";
}) })
.catch((err) => { .catch((err) => {
this.submitting = false; this.submitting = false;

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
Vue.createApp({ Vue.createApp({
data() { data() {
return { return {
@ -74,7 +75,7 @@ Vue.createApp({
this.sortOrder = `${sortOrder.sort}-${sortOrder.order}`; 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) { if (this.artist) {
url += `&artist=${this.formatParams(this.artist)}`; url += `&artist=${this.formatParams(this.artist)}`;
} }
@ -186,7 +187,7 @@ Vue.createApp({
return false; return false;
} }
return axios return axios
.delete(`/api/v1/albums/${this.itemId}`) .delete(`/api/v1/${action}/${this.itemId}`)
.then(() => { .then(() => {
this.fetch(); this.fetch();
}) })

View file

@ -19,6 +19,8 @@ if (typeof email !== "undefined" && typeof username !== "undefined") {
token: "", token: "",
message: message:
"Je viens d'ajouter {artist} - {album} à ma collection !", "Je viens d'ajouter {artist} - {album} à ma collection !",
wantlist:
"Je viens d'ajouter {artist} - {album} à ma liste de souhaits !",
}, },
}, },
loading: false, loading: false,
@ -58,8 +60,13 @@ if (typeof email !== "undefined" && typeof username !== "undefined") {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
async updateProfil() { async updateProfil() {
this.errors = []; this.errors = [];
const { oldPassword, password, passwordConfirm, mastodon, pagination } = const {
this.formData; oldPassword,
password,
passwordConfirm,
mastodon,
pagination,
} = this.formData;
if (password && !oldPassword) { if (password && !oldPassword) {
this.errors.push("emptyPassword"); this.errors.push("emptyPassword");

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
if (typeof item !== "undefined") { if (typeof item !== "undefined") {
Vue.createApp({ Vue.createApp({
data() { data() {
@ -196,7 +197,7 @@ if (typeof item !== "undefined") {
updateItem() { updateItem() {
showToastr("Mise à jour en cours…", true); showToastr("Mise à jour en cours…", true);
axios axios
.patch(`/api/v1/albums/${this.item._id}`) .patch(`/api/v1/${action}/${this.item._id}`)
.then((res) => { .then((res) => {
showToastr("Mise à jour réalisée avec succès", true); showToastr("Mise à jour réalisée avec succès", true);
this.item = res.data; this.item = res.data;
@ -215,9 +216,12 @@ if (typeof item !== "undefined") {
}, },
deleteItem() { deleteItem() {
axios axios
.delete(`/api/v1/albums/${this.item._id}`) .delete(`/api/v1/${action}/${this.item._id}`)
.then(() => { .then(() => {
window.location.href = "/ma-collection"; window.location.href =
action === "albums"
? "/ma-collection"
: "/ma-liste-de-souhaits";
}) })
.catch((err) => { .catch((err) => {
showToastr( showToastr(
@ -238,7 +242,7 @@ if (typeof item !== "undefined") {
} }
this.shareSubmiting = true; this.shareSubmiting = true;
axios axios
.post(`/api/v1/albums/${this.item._id}/share`, { .post(`/api/v1/${action}/${this.item._id}/share`, {
message: this.shareMessageTransformed, message: this.shareMessageTransformed,
}) })
.then(() => { .then(() => {

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
Vue.createApp({ Vue.createApp({
data() { data() {
return { return {
@ -10,7 +11,10 @@ Vue.createApp({
exportCollection(event) { exportCollection(event) {
event.preventDefault(); event.preventDefault();
window.open(`/api/v1/albums?exportFormat=${this.format}`, "_blank"); window.open(
`/api/v1/${action}?exportFormat=${this.format}`,
"_blank"
);
}, },
}, },
}).mount("#exporter"); }).mount("#exporter");

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
Vue.createApp({ Vue.createApp({
data() { data() {
return { return {
@ -66,11 +67,11 @@ Vue.createApp({
try { try {
const res = await axios.get( const res = await axios.get(
`/api/v1/albums?discogsId=${release_id}` `/api/v1/${action}?discogsId=${release_id}`
); );
if (res.status === 204) { if (res.status === 204) {
await axios.post("/api/v1/albums", { await axios.post(`/api/v1/${action}`, {
discogsId: release_id, discogsId: release_id,
share: false, share: false,
}); });

View file

@ -15,12 +15,14 @@ import { isXhr } from "./helpers";
import indexRouter from "./routes"; import indexRouter from "./routes";
import maCollectionRouter from "./routes/ma-collection"; import maCollectionRouter from "./routes/ma-collection";
import wantlistRouter from "./routes/wantlist";
import monCompteRouter from "./routes/mon-compte"; import monCompteRouter from "./routes/mon-compte";
import collectionRouter from "./routes/collection"; import collectionRouter from "./routes/collection";
import importJobsRouter from "./routes/jobs"; import importJobsRouter from "./routes/jobs";
import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importAlbumRouterApiV1 from "./routes/api/v1/albums";
import importWantlistRouterApiV1 from "./routes/api/v1/wantlist";
import importSearchRouterApiV1 from "./routes/api/v1/search"; import importSearchRouterApiV1 from "./routes/api/v1/search";
import importMastodonRouterApiV1 from "./routes/api/v1/mastodon"; import importMastodonRouterApiV1 from "./routes/api/v1/mastodon";
import importMeRouterApiV1 from "./routes/api/v1/me"; import importMeRouterApiV1 from "./routes/api/v1/me";
@ -81,9 +83,11 @@ app.use(express.static(path.join(__dirname, "../public")));
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/mon-compte", monCompteRouter); app.use("/mon-compte", monCompteRouter);
app.use("/ma-collection", maCollectionRouter); app.use("/ma-collection", maCollectionRouter);
app.use("/ma-liste-de-souhaits", wantlistRouter);
app.use("/collection", collectionRouter); app.use("/collection", collectionRouter);
app.use("/jobs", importJobsRouter); app.use("/jobs", importJobsRouter);
app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/albums", importAlbumRouterApiV1);
app.use("/api/v1/wantlist", importWantlistRouterApiV1);
app.use("/api/v1/search", importSearchRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1);
app.use("/api/v1/mastodon", importMastodonRouterApiV1); app.use("/api/v1/mastodon", importMastodonRouterApiV1);
app.use("/api/v1/me", importMeRouterApiV1); app.use("/api/v1/me", importMeRouterApiV1);

View file

@ -53,3 +53,28 @@ export const isXhr = (req) => {
return is; 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;
};

View file

@ -12,7 +12,7 @@ import JobsModel from "../models/jobs";
import UsersModel from "../models/users"; import UsersModel from "../models/users";
import ErrorEvent from "../libs/error"; import ErrorEvent from "../libs/error";
import { getAlbumDetails } from "../helpers"; import { getAlbumDetails, getAllDistincts } from "../helpers";
/** /**
* Classe permettant la gestion des albums d'un utilisateur * Classe permettant la gestion des albums d'un utilisateur
@ -42,8 +42,11 @@ class Albums extends Pages {
discogsId: albumDetails.id, discogsId: albumDetails.id,
User: user._id, User: user._id,
}; };
// eslint-disable-next-line no-nested-ternary
data.released = data.released data.released = data.released
? typeof data.released === "string"
? new Date(data.released.replace("-00", "-01")) ? new Date(data.released.replace("-00", "-01"))
: data.released
: null; : null;
delete data.id; delete data.id;
@ -142,28 +145,6 @@ Publié automatiquement via #musictopus`;
return album; 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) { constructor(req, viewname) {
super(req, viewname); super(req, viewname);
@ -178,6 +159,8 @@ Publié automatiquement via #musictopus`;
"#a3be8c", "#a3be8c",
"#b48ead", "#b48ead",
]; ];
this.setPageContent("action", "albums");
} }
/** /**
@ -457,20 +440,28 @@ Publié automatiquement via #musictopus`;
* Méthode permettant de créer la page "ma-collection" * Méthode permettant de créer la page "ma-collection"
*/ */
async loadMyCollection() { async loadMyCollection() {
const artists = await Albums.getAllDistincts( const artists = await getAllDistincts(
AlbumsModel,
"artists.name", "artists.name",
this.req.user._id this.req.user._id
); );
const formats = await Albums.getAllDistincts( const formats = await getAllDistincts(
AlbumsModel,
"formats.name", "formats.name",
this.req.user._id this.req.user._id
); );
const years = await Albums.getAllDistincts("year", this.req.user._id); const years = await getAllDistincts(
const genres = await Albums.getAllDistincts( AlbumsModel,
"year",
this.req.user._id
);
const genres = await getAllDistincts(
AlbumsModel,
"genres", "genres",
this.req.user._id this.req.user._id
); );
const styles = await Albums.getAllDistincts( const styles = await getAllDistincts(
AlbumsModel,
"styles", "styles",
this.req.user._id this.req.user._id
); );
@ -669,11 +660,19 @@ Publié automatiquement via #musictopus`;
); );
} }
const artists = await Albums.getAllDistincts("artists.name", userId); const artists = await getAllDistincts(
const formats = await Albums.getAllDistincts("formats.name", userId); AlbumsModel,
const years = await Albums.getAllDistincts("year", userId); "artists.name",
const genres = await Albums.getAllDistincts("genres", userId); userId
const styles = await Albums.getAllDistincts("styles", 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.setPageTitle(`Collection publique de ${user.username}`);
this.setPageContent("username", user.username); this.setPageContent("username", user.username);

View file

@ -5,6 +5,7 @@ import { getAlbumDetails } from "../helpers";
import JobsModel from "../models/jobs"; import JobsModel from "../models/jobs";
import AlbumsModel from "../models/albums"; import AlbumsModel from "../models/albums";
import WantListModel from "../models/wantlist";
class Jobs { class Jobs {
/** /**
@ -51,6 +52,50 @@ class Jobs {
return true; 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 * Point d'entrée
* @param {String} state * @param {String} state
@ -78,6 +123,9 @@ class Jobs {
case "Albums": case "Albums":
await Jobs.importAlbumAssets(job.id); await Jobs.importAlbumAssets(job.id);
break; break;
case "WantList":
await Jobs.importAlbumForWantListAssets(job.id);
break;
default: default:
throw new ErrorEvent( throw new ErrorEvent(
500, 500,

View file

@ -26,6 +26,7 @@ class Me extends Pages {
url: Joi.string().uri().allow(null, ""), url: Joi.string().uri().allow(null, ""),
token: Joi.string().allow(null, ""), token: Joi.string().allow(null, ""),
message: Joi.string().allow(null, ""), message: Joi.string().allow(null, ""),
wantlist: Joi.string().allow(null, ""),
}, },
}); });

687
src/middleware/Wantlist.js Normal file
View file

@ -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;

View file

@ -38,6 +38,7 @@ const UserSchema = new mongoose.Schema(
token: String, token: String,
url: String, url: String,
message: String, message: String,
wantlist: String,
}, },
}, },
{ {

37
src/models/wantlist.js Normal file
View file

@ -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);

View file

@ -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;

View file

@ -104,8 +104,23 @@ router
try { try {
const page = new Pages(req, "ajouter-un-album"); const page = new Pages(req, "ajouter-un-album");
page.setPageContent("action", "albums");
page.setPageTitle("Ajouter un album"); 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); render(res, page);
} catch (err) { } catch (err) {
next(err); next(err);

99
src/routes/wantlist.js Normal file
View file

@ -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;

View file

@ -58,12 +58,18 @@
<div class="navbar-start"> <div class="navbar-start">
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<a class="button is-primary" href="/ajouter-un-album"> <a class="button is-primary" href="``">
<i class="icon-plus"></i> <i class="icon-plus"></i>
<span> <span>
Ajouter un album Ajouter un album
</span> </span>
</a> </a>
<a class="button is-secondary" href="/ajouter-a-ma-liste-de-souhaits" aria-label="Ajouter un album à ma liste de souhaits">
<i class="icon-plus"></i>
<span>
Liste de souhaits
</span>
</a>
</div> </div>
</div> </div>
</div> </div>
@ -121,12 +127,16 @@
<a class="navbar-item" href="/ma-collection"> <a class="navbar-item" href="/ma-collection">
Ma collection Ma collection
</a> </a>
<a class="navbar-item" href="/ma-liste-de-souhaits">
Ma liste de souhaits
</a>
<a class="navbar-item" href="/ma-collection/on-air"> <a class="navbar-item" href="/ma-collection/on-air">
On air On air
</a> </a>
<a class="navbar-item" href="/ma-collection/statistiques"> <a class="navbar-item" href="/ma-collection/statistiques">
Statistiques Statistiques
</a> </a>
<hr />
<a class="navbar-item" href="/ma-collection/exporter"> <a class="navbar-item" href="/ma-collection/exporter">
Exporter ma collection Exporter ma collection
</a> </a>
@ -134,6 +144,13 @@
Importer une collection Importer une collection
</a> </a>
<hr /> <hr />
<a class="navbar-item" href="/ma-liste-de-souhaits/exporter">
Exporter ma liste de souhaits
</a>
<a class="navbar-item" href="/ma-liste-de-souhaits/importer">
Importer une liste de souhaits
</a>
<hr />
<a class="navbar-item is-danger" href="/se-deconnecter"> <a class="navbar-item is-danger" href="/se-deconnecter">
Déconnexion Déconnexion
</a> </a>

View file

@ -200,4 +200,5 @@
<script> <script>
const canPublish = <%- (user.mastodon && user.mastodon.publish) || false %>; const canPublish = <%- (user.mastodon && user.mastodon.publish) || false %>;
const action = "<%- page.action %>";
</script> </script>

View file

@ -3,6 +3,7 @@
%> %>
<main class="layout-maxed collection" id="collection"> <main class="layout-maxed collection" id="collection">
<% if (page.action === 'albums') { %>
<h1> <h1>
<% if ( pageType === 'private' ) { <% if ( pageType === 'private' ) {
__append('Ma collection <i class="icon-share" @click="toggleModalShare" aria-label="Partager ma collection" title="Votre collection sera visible en lecture aux personnes ayant le lien de partage"></i>'); __append('Ma collection <i class="icon-share" @click="toggleModalShare" aria-label="Partager ma collection" title="Votre collection sera visible en lecture aux personnes ayant le lien de partage"></i>');
@ -17,6 +18,9 @@
</a> </a>
</div> </div>
<% } %> <% } %>
<% } else { %>
<h1>Ma liste de souhaits</h1>
<% } %>
<%- include('../components/filters/index') %> <%- include('../components/filters/index') %>
@ -30,7 +34,7 @@
<div class="item" v-if="!loading" v-for="item in items"> <div class="item" v-if="!loading" v-for="item in items">
<span class="title"> <span class="title">
<% if ( pageType === 'private' ) { %> <% if ( pageType === 'private' ) { %>
<a :href="'/ma-collection/' + item._id">{{ renderAlbumTitle(item) }}</a> <a :href="'/<%= page.action === 'albums' ? 'ma-collection' : 'ma-liste-de-souhaits' %>/' + item._id">{{ renderAlbumTitle(item) }}</a>
<i class="icon-trash" @click="showConfirmDelete(item._id)"></i> <i class="icon-trash" @click="showConfirmDelete(item._id)"></i>
<% } else { %> <% } else { %>
{{ item.artists_sort}} - {{ item.title }} {{ item.artists_sort}} - {{ item.title }}
@ -39,7 +43,7 @@
<div class="grid grid-cols-2 md:grid-cols-4"> <div class="grid grid-cols-2 md:grid-cols-4">
<div> <div>
<% if ( pageType === 'private' ) { %> <% if ( pageType === 'private' ) { %>
<a :href="'/ma-collection/' + item._id"><img :src="item.thumb" :alt="item.title" /></a> <a :href="'/<%= page.action === 'albums' ? 'ma-collection' : 'ma-liste-de-souhaits' %>/' + item._id"><img :src="item.thumb" :alt="item.title" /></a>
<% } else { %> <% } else { %>
<img :src="item.thumb" :alt="item.title" /> <img :src="item.thumb" :alt="item.title" />
<% } %> <% } %>
@ -147,6 +151,7 @@
<script> <script>
const vueType = "<%= pageType %>"; const vueType = "<%= pageType %>";
const query = <%- JSON.stringify(query) %>; const query = <%- JSON.stringify(query) %>;
const action = "<%- page.action %>";
<% if ( pageType === 'private' ) { %> <% if ( pageType === 'private' ) { %>
const isPublicCollection = <%= user.isPublicCollection ? 'true' : 'false' %>; const isPublicCollection = <%= user.isPublicCollection ? 'true' : 'false' %>;
const userId = "<%= user._id %>"; const userId = "<%= user._id %>";

View file

@ -110,6 +110,12 @@
id="mastodon.message" id="mastodon.message"
v-model="formData.mastodon.message" v-model="formData.mastodon.message"
></textarea> ></textarea>
<label for="mastodon.wantlist">Message pour la liste de souhaits</label>
<textarea
name="mastodon.wantlist"
id="mastodon.wantlist"
v-model="formData.mastodon.wantlist"
></textarea>
<small> <small>
Variables possibles : Variables possibles :
<ul> <ul>

View file

@ -2,7 +2,7 @@
<h1> <h1>
<template v-for="artist in item.artists"> <template v-for="artist in item.artists">
<a :href="`/ma-collection?page=1&limit=16&sort=year&order=asc&artist=${artist.name}`">{{artist.name}}</a> <a :href="`/<%= page.action === 'album' ? 'ma-collection' : 'ma-liste-de-souhaits' %>?page=1&limit=16&sort=year&order=asc&artist=${artist.name}`">{{artist.name}}</a>
<template v-if="artist.join">&nbsp;{{artist.join}}&nbsp;</template> <template v-if="artist.join">&nbsp;{{artist.join}}&nbsp;</template>
</template> </template>
- {{item.title}} - {{item.title}}
@ -96,5 +96,6 @@
<script> <script>
const item = <%- JSON.stringify(page.item) %>; const item = <%- JSON.stringify(page.item) %>;
const action = "<%- page.action %>";
const canShareItem = <%= user.mastodon?.publish || false %>; const canShareItem = <%= user.mastodon?.publish || false %>;
</script> </script>

View file

@ -1,5 +1,9 @@
<main class="layout-maxed" id="exporter"> <main class="layout-maxed" id="exporter">
<% if (page.action === 'albums') { %>
<h1>Exporter ma collection</h1> <h1>Exporter ma collection</h1>
<% } else { %>
<h1>Exporter ma liste de souhaits</h1>
<% } %>
<p> <p>
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 : 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 :
</p> </p>
@ -45,3 +49,7 @@
</button> </button>
</form> </form>
</main> </main>
<script>
const action = "<%- page.action %>";
</script>

View file

@ -1,7 +1,11 @@
<main class="layout-maxed" id="importer"> <main class="layout-maxed" id="importer">
<% if (page.action === 'albums') { %>
<h1>Importer une collection</h1> <h1>Importer une collection</h1>
<% } else { %>
<h1>Importer une liste de souhaits</h1>
<% } %>
<p> <p>
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.
<br /> <br />
Vous devez dans un premier temps vous rendre sur la page <a href="https://www.discogs.com/fr/users/export" target="_blank" rel="noopener noreferrer">Exporter</a> de discogs. Vous devez dans un premier temps vous rendre sur la page <a href="https://www.discogs.com/fr/users/export" target="_blank" rel="noopener noreferrer">Exporter</a> de discogs.
<br /> <br />
@ -41,3 +45,7 @@
</button> </button>
</form> </form>
</main> </main>
<script>
const action = "<%- page.action %>";
</script>