├── .prettierrc ├── src ├── utils │ ├── logging.js │ └── config.js ├── costumer.js ├── Listener.js ├── PlaylistService.js ├── MailSender.js └── templates │ └── email-template.html ├── .editorconfig ├── .env.example ├── .gitignore ├── .eslintrc.json ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/logging.js: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import pretty from 'pino-pretty'; 3 | 4 | const logger = pino( 5 | { 6 | base: { 7 | pid: false, 8 | }, 9 | }, 10 | pretty(), 11 | ); 12 | 13 | export default logger; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Aturan-aturan editor 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | # Aturan-aturan untuk file-file dengan ekstensi tertentu 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # DATABASE CONFIGURATION 2 | PGUSER=null 3 | PGPASSWORD=null 4 | PGDATABASE=openmusicapi 5 | PGHOST=localhost 6 | PGPORT=5432 7 | 8 | # Message broker 9 | RABBITMQ_SERVER=amqp://localhost 10 | 11 | # nodemailer SMTP authentication 12 | MAIL_HOST=null 13 | MAIL_PORT=null 14 | MAIL_ADDRESS=null 15 | MAIL_PASSWORD=null 16 | MAIL_FROM_ADDRESS=openmusic@gmail.com 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | 5 | # IDEs and editors 6 | .vscode/ 7 | 8 | # PostgreSQL 9 | *.backup 10 | *.sql 11 | 12 | # Logs 13 | logs/ 14 | *.log 15 | 16 | # Build files 17 | dist/ 18 | build/ 19 | 20 | # Environment variables 21 | .env 22 | 23 | # Dependencies installed via npm 24 | package-lock.json 25 | 26 | # Testing 27 | coverage/ 28 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | const config = { 6 | rabbitMq: { 7 | server: process.env.RABBITMQ_SERVER, 8 | }, 9 | mail: { 10 | host: process.env.MAIL_HOST, 11 | port: process.env.MAIL_PORT, 12 | user: process.env.MAIL_ADDRESS, 13 | password: process.env.MAIL_PASSWORD, 14 | fromAddress: process.env.MAIL_FROM_ADDRESS, 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "airbnb-base", 8 | "overrides": [], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "no-console": "warn", 15 | "no-underscore-dangle": "off", 16 | "import/named": "off", 17 | "object-curly-newline": "off", 18 | "camelcase": "off", 19 | "import/no-unresolved": "off", 20 | "import/extensions": "off", 21 | "no-else-return": "off", 22 | "newline-per-chained-call": "off", 23 | "no-useless-catch": "off", 24 | "no-shadow": "off", 25 | "import/no-extraneous-dependencies": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/costumer.js: -------------------------------------------------------------------------------- 1 | import amqp from 'amqplib'; 2 | import PlaylistsService from './PlaylistService.js'; 3 | import MailSender from './MailSender.js'; 4 | import Listener from './Listener.js'; 5 | import config from './utils/config.js'; 6 | import logger from './utils/logging.js'; 7 | 8 | const init = async () => { 9 | const playlistsService = new PlaylistsService(); 10 | const mailSender = new MailSender(); 11 | const listener = new Listener(playlistsService, mailSender); 12 | const connection = await amqp.connect(config.rabbitMq.server); 13 | 14 | const channel = await connection.createChannel(); 15 | await channel.assertQueue('export:playlists', { 16 | durable: true, 17 | }); 18 | 19 | channel.consume('export:playlists', listener.listen, { noAck: true }); 20 | 21 | logger.info('Consumer berjalan'); 22 | }; 23 | 24 | init(); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-music-api-costumer", 3 | "version": "1.0.0", 4 | "description": "Open Music API Costumer", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node ./src/costumer.js", 9 | "dev": "nodemon ./src/costumer.js", 10 | "lint": "eslint ./src" 11 | }, 12 | "author": "Hizkia Reppi", 13 | "license": "ISC", 14 | "dependencies": { 15 | "amqplib": "^0.10.3", 16 | "dotenv": "^16.3.1", 17 | "nodemailer": "^6.9.4", 18 | "pg": "^8.11.1", 19 | "pino": "^8.14.2", 20 | "pino-pretty": "^10.2.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^8.40.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-config-prettier": "^8.8.0", 26 | "eslint-plugin-import": "^2.27.5", 27 | "eslint-plugin-prettier": "^4.2.1", 28 | "nodemon": "^2.0.22", 29 | "prettier": "^3.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Listener.js: -------------------------------------------------------------------------------- 1 | import logger from './utils/logging.js'; 2 | 3 | class Listener { 4 | constructor(playlistsService, mailSender) { 5 | this._playlistsService = playlistsService; 6 | this._mailSender = mailSender; 7 | this.listen = this.listen.bind(this); 8 | } 9 | 10 | async listen(message) { 11 | try { 12 | const { playlistId, targetEmail } = JSON.parse( 13 | message.content.toString(), 14 | ); 15 | const playlistsData = await this._playlistsService.getPlaylists( 16 | playlistId, 17 | ); 18 | const songs = await this._playlistsService.getSongs(playlistId); 19 | const owner = await this._playlistsService.getPlaylistOwner(playlistId); 20 | await this._mailSender.sendEmail( 21 | targetEmail, 22 | owner, 23 | JSON.stringify({ 24 | playlist: { 25 | id: playlistsData.id, 26 | name: playlistsData.name, 27 | songs, 28 | }, 29 | }), 30 | ); 31 | 32 | logger.info(`Email dikirim ke ${targetEmail}`); 33 | } catch (error) { 34 | logger.error(error); 35 | } 36 | } 37 | } 38 | 39 | export default Listener; 40 | -------------------------------------------------------------------------------- /src/PlaylistService.js: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | 3 | const { Pool } = pg; 4 | 5 | class PlaylistsService { 6 | constructor() { 7 | this._pool = new Pool(); 8 | } 9 | 10 | async getPlaylists(playlistId) { 11 | const query = { 12 | text: 'SELECT id, name FROM playlists WHERE id = $1', 13 | values: [playlistId], 14 | }; 15 | const { rows } = await this._pool.query(query); 16 | 17 | return rows[0]; 18 | } 19 | 20 | async getSongs(playlistId) { 21 | const query = { 22 | text: `SELECT s.id, s.title, s.performer 23 | FROM songs s 24 | INNER JOIN songs_playlist p 25 | ON p.song_id = s.id 26 | WHERE p.playlist_id = $1`, 27 | values: [playlistId], 28 | }; 29 | const { rows } = await this._pool.query(query); 30 | 31 | return rows; 32 | } 33 | 34 | async getPlaylistOwner(playlistId) { 35 | const query = { 36 | text: `SELECT users.username, users.fullname 37 | FROM users 38 | INNER JOIN playlists 39 | ON users.id = playlists.owner 40 | WHERE playlists.id = $1`, 41 | values: [playlistId], 42 | }; 43 | const { rows } = await this._pool.query(query); 44 | 45 | return rows[0]; 46 | } 47 | } 48 | 49 | export default PlaylistsService; 50 | -------------------------------------------------------------------------------- /src/MailSender.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import config from './utils/config.js'; 3 | import fs from 'fs'; 4 | import util from 'util'; 5 | 6 | const readFile = util.promisify(fs.readFile); 7 | 8 | class MailSender { 9 | constructor() { 10 | this._transporter = nodemailer.createTransport({ 11 | host: config.mail.host, 12 | port: config.mail.port, 13 | auth: { 14 | user: config.mail.user, 15 | pass: config.mail.password, 16 | }, 17 | }); 18 | } 19 | 20 | async sendEmail(targetEmail, owner, content) { 21 | const emailTemplate = await readFile( 22 | 'src/templates/email-template.html', 23 | 'utf8', 24 | ); 25 | 26 | const emailContent = emailTemplate.replace( 27 | /{{name}}|{{attachmentURL}}/gi, 28 | (matched) => { 29 | switch (matched) { 30 | case '{{name}}': 31 | return owner.fullname; 32 | case '{{attachmentURL}}': 33 | return ( 34 | 'data:application/json;base64,' + 35 | Buffer.from(JSON.stringify(content, null, 2)).toString('base64') 36 | ); 37 | default: 38 | return matched; 39 | } 40 | }, 41 | ); 42 | 43 | const message = { 44 | from: config.mail.fromAddress, 45 | to: targetEmail, 46 | subject: 'Export Playlist', 47 | html: emailContent, 48 | attachments: [ 49 | { 50 | filename: 'playlist.json', 51 | content, 52 | }, 53 | ], 54 | }; 55 | 56 | return this._transporter.sendMail(message); 57 | } 58 | } 59 | 60 | export default MailSender; 61 | -------------------------------------------------------------------------------- /src/templates/email-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |Dear {{name}},
73 |Anda telah berhasil mengekspor playlist musik Anda!
74 |Berikut adalah playlist musik yang telah Anda eksport:
75 |76 | Jangan ragu untuk menikmati musik-musik favorit Anda dan berbagi dengan 77 | teman-teman Anda! 78 |
79 | 84 |Terima kasih telah menggunakan layanan kami!
85 |
86 | Salam hangat,
87 | Tim Open Music
88 |
Open Music - Aplikasi Musik Terbaik untuk Semua
90 |