diff --git a/packages/app/constants/routes.js b/packages/app/constants/routes.js index 6e223bfa..2980f84f 100755 --- a/packages/app/constants/routes.js +++ b/packages/app/constants/routes.js @@ -88,6 +88,11 @@ export default [ centeredContent: true, extendedContent: true, }, + { + path: "/violation", + useLayout: "minimal", + public: true, + }, // THIS MUST BE THE LAST ROUTE { path: "/", diff --git a/packages/app/src/components/Login/index.jsx b/packages/app/src/components/Login/index.jsx index 6e59ad78..ba7ffceb 100755 --- a/packages/app/src/components/Login/index.jsx +++ b/packages/app/src/components/Login/index.jsx @@ -40,7 +40,7 @@ export default class Login extends React.Component { loginInputs: {}, error: null, phase: 0, - mfa_required: null + mfa_required: null, } formRef = React.createRef() @@ -60,6 +60,18 @@ export default class Login extends React.Component { this.toggleLoading(true) await AuthModel.login(payload, this.onDone).catch((error) => { + if (error.response.data){ + if (error.response.data.violation) { + this.props.close({ + unlock: true + }) + + return app.history.push("/violation", { + violation: error.response.data.violation + }) + } + } + console.error(error, error.response) this.toggleLoading(false) diff --git a/packages/app/src/components/PagePanels/index.jsx b/packages/app/src/components/PagePanels/index.jsx index d31e8c5e..8c256bb1 100755 --- a/packages/app/src/components/PagePanels/index.jsx +++ b/packages/app/src/components/PagePanels/index.jsx @@ -23,7 +23,7 @@ export const Panel = (props) => { export class PagePanelWithNavMenu extends React.Component { state = { - activeTab: this.props.defaultTab ?? new URLSearchParams(window.location.search).get("type") ?? this.props.tabs[0].key, + activeTab: new URLSearchParams(window.location.search).get("type") ?? this.props.defaultTab ?? this.props.tabs[0].key, renders: [], } diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js index 9f0cf305..adde1603 100755 --- a/packages/app/src/cores/player/player.core.js +++ b/packages/app/src/cores/player/player.core.js @@ -115,16 +115,16 @@ export default class Player extends Core { internalEvents = { "player.state.update:loading": () => { - app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) + //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) }, "player.state.update:track_manifest": () => { - app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) + //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) }, "player.state.update:playback_status": () => { - app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) + //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state) }, "player.seeked": (to) => { - app.cores.sync.music.dispatchEvent("music.player.seek", to) + //app.cores.sync.music.dispatchEvent("music.player.seek", to) }, } @@ -587,7 +587,7 @@ export default class Player extends Core { if (playlist.some((item) => typeof item === "string")) { this.console.log("Resolving missing manifests by ids...") - playlist = await this.getTracksByIds(playlist) + playlist = await ServicesHandlers.default.resolveMany(playlist) } playlist = playlist.slice(startIndex) diff --git a/packages/app/src/cores/player/services.js b/packages/app/src/cores/player/services.js index e275cb88..5d7e5d8e 100755 --- a/packages/app/src/cores/player/services.js +++ b/packages/app/src/cores/player/services.js @@ -3,7 +3,18 @@ import MusicModel from "comty.js/models/music" export default { "default": { - resolve: () => { }, + resolve: async (track_id) => { + return await MusicModel.getTrackData(track_id) + }, + resolveMany: async (track_ids, options) => { + const response = await MusicModel.getTrackData(track_ids, options) + + if (response.list) { + return response + } + + return [response] + }, toggleLike: async (manifest, to) => { return await MusicModel.toggleTrackLike(manifest, to) } diff --git a/packages/app/src/pages/music/components/dashboard/index.jsx b/packages/app/src/pages/music/components/dashboard/index.jsx deleted file mode 100755 index 537bec02..00000000 --- a/packages/app/src/pages/music/components/dashboard/index.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react" - -export default (props) => { - return
- Dashboard -
-} \ No newline at end of file diff --git a/packages/app/src/pages/music/components/library/index.jsx b/packages/app/src/pages/music/components/library/index.jsx index f4679531..b4d598d7 100755 --- a/packages/app/src/pages/music/components/library/index.jsx +++ b/packages/app/src/pages/music/components/library/index.jsx @@ -139,11 +139,7 @@ const PlaylistItem = (props) => { } const OwnPlaylists = (props) => { - const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists, { - services: { - tidal: app.cores.sync.getActiveLinkedServices().tidal - } - }) + const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists) if (E_Playlists) { console.error(E_Playlists) @@ -183,8 +179,36 @@ const OwnPlaylists = (props) => { } -export default () => { +const Library = (props) => { return
+
+

Library

+ + + }, + { + value: "playlist", + label: "Playlists", + icon: + }, + ]} + /> +
+ , + title: "Create new", + }} + onClick={OpenPlaylistCreator} + />
-} \ No newline at end of file +} + +export default Library \ No newline at end of file diff --git a/packages/app/src/pages/music/components/library/index.less b/packages/app/src/pages/music/components/library/index.less index ef57c627..f35126f1 100755 --- a/packages/app/src/pages/music/components/library/index.less +++ b/packages/app/src/pages/music/components/library/index.less @@ -170,4 +170,18 @@ flex-direction: column; width: 100%; + + gap: 15px; + + .music-library_header { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: space-between; + + h1 { + margin: 0; + } + } } \ No newline at end of file diff --git a/packages/app/src/pages/music/dashboard/index.jsx b/packages/app/src/pages/music/dashboard/index.jsx new file mode 100755 index 00000000..d623623e --- /dev/null +++ b/packages/app/src/pages/music/dashboard/index.jsx @@ -0,0 +1,15 @@ +import React from "react" + +import "./index.less" + +export default (props) => { + return
+
+

Your Dashboard

+
+ +
+ +
+
+} \ No newline at end of file diff --git a/packages/app/src/pages/music/dashboard/index.less b/packages/app/src/pages/music/dashboard/index.less new file mode 100644 index 00000000..04a7f541 --- /dev/null +++ b/packages/app/src/pages/music/dashboard/index.less @@ -0,0 +1,8 @@ +.music-dashboard { + display: flex; + flex-direction: column; + + width: 100%; + + .music-dashboard_header {} +} \ No newline at end of file diff --git a/packages/app/src/pages/music/components/dashboard/releases/index.jsx b/packages/app/src/pages/music/dashboard/releases/index.jsx similarity index 99% rename from packages/app/src/pages/music/components/dashboard/releases/index.jsx rename to packages/app/src/pages/music/dashboard/releases/index.jsx index 3e8965c8..299244e3 100755 --- a/packages/app/src/pages/music/components/dashboard/releases/index.jsx +++ b/packages/app/src/pages/music/dashboard/releases/index.jsx @@ -5,7 +5,7 @@ import { Icons } from "components/Icons" import { ImageViewer } from "components" import Searcher from "components/Searcher" -import ReleaseCreator from "../../../creator" +import ReleaseCreator from "../../creator" import MusicModel from "models/music" diff --git a/packages/app/src/pages/music/components/dashboard/releases/index.less b/packages/app/src/pages/music/dashboard/releases/index.less similarity index 100% rename from packages/app/src/pages/music/components/dashboard/releases/index.less rename to packages/app/src/pages/music/dashboard/releases/index.less diff --git a/packages/app/src/pages/music/tabs.jsx b/packages/app/src/pages/music/tabs.jsx index 9939960d..fc1d3e38 100755 --- a/packages/app/src/pages/music/tabs.jsx +++ b/packages/app/src/pages/music/tabs.jsx @@ -1,9 +1,6 @@ import LibraryTab from "./components/library" import FavoritesTab from "./components/favorites" import ExploreTab from "./components/explore" -import DashboardTab from "./components/dashboard" - -import ReleasesTab from "./components/dashboard/releases" export default [ { @@ -30,18 +27,4 @@ export default [ icon: "Radio", disabled: true }, - { - key: "artist_panel", - label: "Creator Panel", - icon: "MdSpaceDashboard", - component: DashboardTab, - children: [ - { - key: "artist_panel.releases", - label: "Releases", - icon: "MdUpcoming", - component: ReleasesTab, - } - ] - }, ] \ No newline at end of file diff --git a/packages/app/src/pages/music/track/[track_id]/index.jsx b/packages/app/src/pages/music/track/[track_id]/index.jsx new file mode 100644 index 00000000..6fdfcc41 --- /dev/null +++ b/packages/app/src/pages/music/track/[track_id]/index.jsx @@ -0,0 +1,40 @@ +import React from "react" +import * as antd from "antd" + +import PlaylistView from "components/Music/PlaylistView" + +import MusicService from "models/music" + +import "./index.less" + +const TrackPage = (props) => { + const { track_id } = props.params + + const [loading, result, error, makeRequest] = app.cores.api.useRequest(MusicService.getTrackData, track_id) + + if (error) { + return + } + + if (loading) { + return + } + + return
+ +
+} + +export default TrackPage diff --git a/packages/app/src/pages/music/track/[track_id]/index.less b/packages/app/src/pages/music/track/[track_id]/index.less new file mode 100644 index 00000000..d4b5bf40 --- /dev/null +++ b/packages/app/src/pages/music/track/[track_id]/index.less @@ -0,0 +1,6 @@ +.track-page { + display: flex; + flex-direction: column; + + width: 100%; +} \ No newline at end of file diff --git a/packages/app/src/pages/violation/index.jsx b/packages/app/src/pages/violation/index.jsx new file mode 100644 index 00000000..aa1e90da --- /dev/null +++ b/packages/app/src/pages/violation/index.jsx @@ -0,0 +1,9 @@ +import React from "react" + +const ViolationPage = (props) => { + return
+

Violation

+
+} + +export default ViolationPage \ No newline at end of file diff --git a/packages/server/classes/AuthToken/index.js b/packages/server/classes/AuthToken/index.js index 801093b0..61dd939d 100644 --- a/packages/server/classes/AuthToken/index.js +++ b/packages/server/classes/AuthToken/index.js @@ -1,5 +1,5 @@ import jwt from "jsonwebtoken" -import { Session, RegenerationToken, User } from "../../db_models" +import { Session, RegenerationToken, User, TosViolations } from "../../db_models" export default class Token { static get strategy() { @@ -69,6 +69,21 @@ export default class Token { result.data = decoded + // check account tos violation + const violation = await TosViolations.findOne({ user_id: decoded.user_id }) + + if (violation) { + console.log("violation", violation) + + result.valid = false + result.banned = { + reason: violation.reason, + expire_at: violation.expire_at, + } + + return result + } + const sessions = await Session.find({ user_id: decoded.user_id }) const currentSession = sessions.find((session) => session.token === token) diff --git a/packages/server/classes/ChunkFileUpload/index.js b/packages/server/classes/ChunkFileUpload/index.js index cc5280b9..ace09795 100755 --- a/packages/server/classes/ChunkFileUpload/index.js +++ b/packages/server/classes/ChunkFileUpload/index.js @@ -169,7 +169,7 @@ export async function uploadChunkFile(req, { }) { return await new Promise(async (resolve, reject) => { if (!checkChunkUploadHeaders(req.headers)) { - reject(new OperationErrorError(400, "Missing header(s)")) + reject(new OperationError(400, "Missing header(s)")) return } diff --git a/packages/server/db_models/tosViolations/index.js b/packages/server/db_models/tosViolations/index.js new file mode 100644 index 00000000..b18f9d83 --- /dev/null +++ b/packages/server/db_models/tosViolations/index.js @@ -0,0 +1,17 @@ +export default { + name: "TosViolations", + collection: "tos_violations", + schema: { + user_id: { + type: "string", + required: true, + }, + reason: { + type: "string", + required: true, + }, + expire_at: { + type: "date", + } + } +} \ No newline at end of file diff --git a/packages/server/db_models/track/index.js b/packages/server/db_models/track/index.js index 576ddc3c..74c08bd3 100755 --- a/packages/server/db_models/track/index.js +++ b/packages/server/db_models/track/index.js @@ -9,8 +9,8 @@ export default { album: { type: String, }, - artist: { - type: String, + artists: { + type: Array, }, source: { type: String, @@ -31,10 +31,6 @@ export default { type: String, default: "https://storage.ragestudio.net/comty-static-assets/default_song.png" }, - thumbnail: { - type: String, - default: "https://storage.ragestudio.net/comty-static-assets/default_song.png" - }, videoCanvas: { type: String, }, diff --git a/packages/server/middlewares/withAuthentication/index.js b/packages/server/middlewares/withAuthentication/index.js index 80e1fb30..57c1ddc8 100755 --- a/packages/server/middlewares/withAuthentication/index.js +++ b/packages/server/middlewares/withAuthentication/index.js @@ -3,19 +3,23 @@ import SecureEntry from "../../classes/SecureEntry" import AuthToken from "../../classes/AuthToken" export default async (req, res) => { - function reject(description) { - return res.status(401).json({ error: `${description ?? "Invalid session"}` }) + function reject(data) { + return res.status(401).json(data) } try { const tokenAuthHeader = req.headers?.authorization?.split(" ") if (!tokenAuthHeader) { - return reject("Missing token header") + return reject({ + error: "Missing token header" + }) } if (!tokenAuthHeader[1]) { - return reject("Recived header, missing token") + return reject({ + error: "Recived header, missing token" + }) } switch (tokenAuthHeader[0]) { @@ -25,7 +29,7 @@ export default async (req, res) => { const validation = await AuthToken.validate(token) if (!validation.valid) { - return reject(validation.error) + return reject(validation) } req.auth = { @@ -41,7 +45,9 @@ export default async (req, res) => { const [client_id, token] = tokenAuthHeader[1].split(":") if (client_id === "undefined" || token === "undefined") { - return reject("Invalid server token") + return reject({ + error: "Invalid server token" + }) } const secureEntries = new SecureEntry(authorizedServerTokens) @@ -52,11 +58,15 @@ export default async (req, res) => { }) if (!serverTokenEntry) { - return reject("Invalid server token") + return reject({ + error: "Invalid server token" + }) } if (serverTokenEntry !== token) { - return reject("Missmatching server token") + return reject({ + error: "Missmatching server token" + }) } req.user = { @@ -68,11 +78,16 @@ export default async (req, res) => { return } default: { - return reject("Invalid token type") + return reject({ + error: "Invalid token type" + }) } } } catch (error) { console.error(error) - return res.status(500).json({ error: "An error occurred meanwhile authenticating your token" }) + + return res.status(500).json({ + error: "An error occurred meanwhile authenticating your token" + }) } } diff --git a/packages/server/services/auth/routes/auth/post.js b/packages/server/services/auth/routes/auth/post.js index d10f4ed2..e5db3f69 100644 --- a/packages/server/services/auth/routes/auth/post.js +++ b/packages/server/services/auth/routes/auth/post.js @@ -1,5 +1,5 @@ import AuthToken from "@shared-classes/AuthToken" -import { UserConfig, MFASession } from "@db_models" +import { UserConfig, MFASession, TosViolations } from "@db_models" import requiredFields from "@shared-utils/requiredFields" import obscureEmail from "@shared-utils/obscureEmail" @@ -13,6 +13,15 @@ export default async (req, res) => { password: req.body.password, }) + const violation = await TosViolations.findOne({ user_id: user._id.toString() }) + + if (violation) { + return res.status(403).json({ + error: "Terms of service violated", + violation: violation.toObject() + }) + } + const userConfig = await UserConfig.findOne({ user_id: user._id.toString() }).catch(() => { return {} }) diff --git a/packages/server/services/music/classes/Room/index.js b/packages/server/services/music/classes/Room/index.js deleted file mode 100755 index f6b18865..00000000 --- a/packages/server/services/music/classes/Room/index.js +++ /dev/null @@ -1,345 +0,0 @@ -import generateFnHandler from "@utils/generateFnHandler" -import composePayloadData from "@utils/composePayloadData" - -export default class Room { - constructor(io, roomId, roomOptions = { title: "Untitled Room" }) { - if (!io) { - throw new Error("io is required") - } - - this.io = io - this.roomId = roomId - this.roomOptions = roomOptions - } - - // declare the maximum audio offset from owner - static maxOffsetFromOwner = 1 - - ownerUserId = null - - connections = [] - - limitations = { - maxConnections: 10, - } - - currentState = null - - events = { - "music:player:start": (socket, data) => { - // dispached when someone start playing a new track - // if not owner, do nothing - if (socket.userData._id !== this.ownerUserId) { - return false - } - - if (data.state) { - this.currentState = data.state - } - - this.io.to(this.roomId).emit("music:player:start", composePayloadData(socket, data)) - }, - "music:player:seek": (socket, data) => { - // dispached when someone seek the track - // if not owner, do nothing - if (socket.userData._id !== this.ownerUserId) { - return false - } - - if (data.state) { - this.currentState = data.state - } - - this.io.to(this.roomId).emit("music:player:seek", composePayloadData(socket, data)) - }, - "music:player:loading": (socket, data) => { - // TODO: Softmode and Hardmode - // Ignore if is the owner - if (socket.userData._id === this.ownerUserId) { - return false - } - - // if not loading, check if need to sync - if (!data.loading) { - // try to sync with current state - if (data.state.time > this.currentState.time + Room.maxOffsetFromOwner) { - socket.emit("music:player:seek", composePayloadData(socket, { - position: this.currentState.time, - command_issuer: this.ownerUserId, - })) - } - } - }, - "music:player:status": (socket, data) => { - if (socket.userData._id !== this.ownerUserId) { - return false - } - - if (data.state) { - this.currentState = data.state - } - - this.io.to(this.roomId).emit("music:player:status", composePayloadData(socket, data)) - }, - // UPDATE TICK - "music:state:update": (socket, data) => { - if (socket.userData._id === this.ownerUserId) { - // update current state - this.currentState = data - - return true - } - - if (!this.currentState) { - return false - } - - if (data.loading) { - return false - } - - // check if match with current manifest - if (!data.manifest || data.manifest._id !== this.currentState.manifest._id) { - socket.emit("music:player:start", composePayloadData(socket, { - manifest: this.currentState.manifest, - time: this.currentState.time, - command_issuer: this.ownerUserId, - })) - } - - if (data.firstSync) { - // if not owner, try to sync with current state - if (data.time > this.currentState.time + Room.maxOffsetFromOwner) { - socket.emit("music:player:seek", composePayloadData(socket, { - position: this.currentState.time, - command_issuer: this.ownerUserId, - })) - } - - // check if match with current playing status - if (data.playbackStatus !== this.currentState.playbackStatus && data.firstSync) { - socket.emit("music:player:status", composePayloadData(socket, { - status: this.currentState.playbackStatus, - command_issuer: this.ownerUserId, - })) - } - } - }, - // ROOM MODERATION CONTROL - "room:moderation:kick": (socket, data) => { - if (socket.userData._id !== this.ownerUserId) { - return socket.emit("error", { - message: "You are not the owner of this room, cannot kick this user", - }) - } - - const { room_id, user_id } = data - - if (this.roomId !== room_id) { - console.warn(`[${socket.id}][@${socket.userData.username}] not connected to room ${room_id}, cannot kick`) - - return socket.emit("error", { - message: "You are not connected to requested room, cannot kick this user", - }) - } - - const socket_conn = this.connections.find((socket_conn) => { - return socket_conn.userData._id === user_id - }) - - if (!socket_conn) { - console.warn(`[${socket.id}][@${socket.userData.username}] not found user ${user_id} in room ${room_id}, cannot kick`) - - return socket.emit("error", { - message: "User not found in room, cannot kick", - }) - } - - socket_conn.emit("room:moderation:kicked", { - room_id, - }) - - this.leave(socket_conn) - }, - "room:moderation:transfer_ownership": (socket, data) => { - if (socket.userData._id !== this.ownerUserId) { - return socket.emit("error", { - message: "You are not the owner of this room, cannot transfer ownership", - }) - } - - const { room_id, user_id } = data - - if (this.roomId !== room_id) { - console.warn(`[${socket.id}][@${socket.userData.username}] not connected to room ${room_id}, cannot transfer ownership`) - - return socket.emit("error", { - message: "You are not connected to requested room, cannot transfer ownership", - }) - } - - const socket_conn = this.connections.find((socket_conn) => { - return socket_conn.userData._id === user_id - }) - - if (!socket_conn) { - console.warn(`[${socket.id}][@${socket.userData.username}] not found user ${user_id} in room ${room_id}, cannot transfer ownership`) - - return socket.emit("error", { - message: "User not found in room, cannot transfer ownership", - }) - } - - this.transferOwner(socket_conn) - } - } - - join = (socket) => { - // set connected room name - socket.connectedRoomId = this.roomId - - // join room - socket.join(this.roomId) - - // add to connections - this.connections.push(socket) - - // emit to self - socket.emit("room:joined", this.composeRoomData()) - - // emit to others - this.io.to(this.roomId).emit("room:user:joined", { - user: { - user_id: socket.userData._id, - username: socket.userData.username, - fullName: socket.userData.fullName, - avatar: socket.userData.avatar, - } - }) - - // register events - for (const [event, fn] of Object.entries(this.events)) { - const handler = generateFnHandler(fn, socket) - - if (!Array.isArray(socket.handlers)) { - socket.handlers = [] - } - - socket.handlers.push([event, handler]) - - socket.on(event, handler) - } - - // send current state - this.sendRoomData() - - console.log(`[${socket.id}][@${socket.userData.username}] joined room ${this.roomId}`) - } - - leave = (socket) => { - // if not connected to any room, do nothing - if (!socket.connectedRoomId) { - console.warn(`[${socket.id}][@${socket.userData.username}] not connected to any room`) - return - } - - // if not connected to this room, do nothing - if (socket.connectedRoomId !== this.roomId) { - console.warn(`[${socket.id}][@${socket.userData.username}] not connected to room ${this.roomId}, cannot leave`) - return false - } - - // leave room - socket.leave(this.roomId) - - // remove from connections - const connIndex = this.connections.findIndex((socket_conn) => socket_conn.id === socket.id) - - if (connIndex !== -1) { - this.connections.splice(connIndex, 1) - } - - // remove connected room name - socket.connectedRoomId = null - - // emit to self - socket.emit("room:left", this.composeRoomData()) - - // emit to others - this.io.to(this.roomId).emit("room:user:left", { - user: { - user_id: socket.userData._id, - username: socket.userData.username, - fullName: socket.userData.fullName, - avatar: socket.userData.avatar, - }, - }) - - // unregister events - for (const [event, handler] of socket.handlers) { - socket.off(event, handler) - } - - // send current state - this.sendRoomData() - - console.log(`[${socket.id}][@${socket.userData.username}] left room ${this.roomId}`) - } - - composeRoomData = () => { - return { - roomId: this.roomId, - limitations: this.limitations, - ownerUserId: this.ownerUserId, - options: this.roomOptions, - connectedUsers: this.connections.map((socket_conn) => { - return { - user_id: socket_conn.userData._id, - username: socket_conn.userData.username, - fullName: socket_conn.userData.fullName, - avatar: socket_conn.userData.avatar, - } - }), - currentState: this.currentState, - } - } - - sendRoomData = () => { - this.io.to(this.roomId).emit("room:current-data", this.composeRoomData()) - } - - transferOwner = (socket) => { - if (!socket || !socket.userData) { - console.warn(`[${socket.id}] cannot transfer owner for room [${this.roomId}], no user data`) - return false - } - - this.ownerUserId = socket.userData._id - - console.log(`[${socket.id}][@${socket.userData.username}] is now the owner of the room [${this.roomId}]`) - - this.io.to(this.roomId).emit("room:owner:changed", { - ownerUserId: this.ownerUserId, - }) - - this.sendRoomData() - } - - destroy = () => { - for (const socket of this.connections) { - this.leave(socket) - } - - this.connections = [] - - this.io.to(this.roomId).emit("room:destroyed", { - room: this.roomId, - }) - - console.log(`Room ${this.roomId} destroyed`) - } - - makeOwner = (socket) => { - this.ownerUserId = socket.userData._id - } -} \ No newline at end of file diff --git a/packages/server/services/music/classes/RoomsController/index.js b/packages/server/services/music/classes/RoomsController/index.js deleted file mode 100755 index 42885da6..00000000 --- a/packages/server/services/music/classes/RoomsController/index.js +++ /dev/null @@ -1,99 +0,0 @@ -import Room from "@classes/Room" - -export default class RoomsController { - constructor(io) { - if (!io) { - throw new Error("io is required") - } - - this.io = io - } - - rooms = [] - - checkRoomExists = (roomId) => { - return this.rooms.some((room) => room.roomId === roomId) - } - - createRoom = async (roomId, roomOptions) => { - if (this.checkRoomExists(roomId)) { - throw new Error(`Room ${roomId} already exists`) - } - - const room = new Room(this.io, roomId, roomOptions) - - this.rooms.push(room) - - return room - } - - connectSocketToRoom = async (socket, roomId, roomOptions) => { - let room = null - - if (!this.checkRoomExists(roomId)) { - room = await this.createRoom(roomId, roomOptions) - - // make owner - room.makeOwner(socket) - } - - // check if user is already connected to a room - if (socket.connectedRoomId) { - console.warn(`[${socket.id}][@${socket.userData.username}] already connected to room ${socket.connectedRoomId}`) - - this.disconnectSocketFromRoom(socket) - } - - if (!room) { - room = this.rooms.find((room) => room.roomId === roomId) - } - - return room.join(socket) - } - - disconnectSocketFromRoom = async (socket, roomId) => { - if (!roomId) { - roomId = socket.connectedRoomId - } - - if (!this.checkRoomExists(roomId)) { - console.warn(`Cannot disconnect socket [${socket.id}][@${socket.userData.username}] from room ${roomId}, room does not exists`) - return false - } - - const room = this.rooms.find((room) => room.roomId === roomId) - - // if owners leaves, rotate owner to the next user - if (socket.userData._id === room.ownerUserId) { - if (room.connections.length > 0 && room.connections[1]) { - room.transferOwner(room.connections[1]) - } - } - - // leave - room.leave(socket) - - // if room is empty, destroy it - if (room.connections.length === 0) { - await this.destroyRoom(roomId) - - return true - } - - return true - } - - destroyRoom = async (roomId) => { - if (!this.checkRoomExists(roomId)) { - throw new Error(`Room ${roomId} does not exists`) - } - - const room = this.rooms.find((room) => room.roomId === roomId) - - room.destroy() - - this.rooms.splice(this.rooms.indexOf(room), 1) - - return true - } -} diff --git a/packages/server/services/music/classes/track/index.js b/packages/server/services/music/classes/track/index.js new file mode 100644 index 00000000..280d10ff --- /dev/null +++ b/packages/server/services/music/classes/track/index.js @@ -0,0 +1,5 @@ +export default class Track { + static create = require("./methods/create").default + static delete = require("./methods/delete").default + static get = require("./methods/get").default +} \ No newline at end of file diff --git a/packages/server/services/music/classes/track/methods/create.js b/packages/server/services/music/classes/track/methods/create.js new file mode 100644 index 00000000..021c17c9 --- /dev/null +++ b/packages/server/services/music/classes/track/methods/create.js @@ -0,0 +1,84 @@ +import { Track } from "@db_models" +import requiredFields from "@shared-utils/requiredFields" +import MusicMetadata from "music-metadata" +import axios from "axios" + +export default async (payload = {}) => { + requiredFields(["title", "source", "user_id"], payload) + + const { data: stream, headers } = await axios({ + url: payload.source, + method: "GET", + responseType: "stream", + }) + + const fileMetadata = await MusicMetadata.parseStream(stream, { + mimeType: headers["content-type"], + }) + + const metadata = { + format: fileMetadata.format.codec, + channels: fileMetadata.format.numberOfChannels, + sampleRate: fileMetadata.format.sampleRate, + bits: fileMetadata.format.bitsPerSample, + lossless: fileMetadata.format.lossless, + duration: fileMetadata.format.duration, + + title: fileMetadata.common.title, + artists: fileMetadata.common.artists, + album: fileMetadata.common.album, + } + + if (typeof payload.metadata === "object") { + metadata = { + ...metadata, + ...payload.metadata, + } + } + + const obj = { + title: payload.title, + album: payload.album, + cover: payload.cover, + artists: [], + source: payload.source, + metadata: metadata, + } + + if (Array.isArray(payload.artists)) { + obj.artists = payload.artists + } + + if (typeof payload.artists === "string") { + obj.artists.push(payload.artists) + } + + if (obj.artists.length === 0 || !obj.artists) { + obj.artists = metadata.artists + } + + let track = null + + if (payload._id) { + track = await Track.findById(payload._id) + + if (!track) { + throw new OperationError(404, "Track not found, cannot update") + } + + throw new OperationError(501, "Not implemented") + } else { + track = new Track({ + ...obj, + publisher: { + user_id: payload.user_id, + } + }) + + await track.save() + } + + track = track.toObject() + + return track +} \ No newline at end of file diff --git a/packages/server/services/music/classes/track/methods/delete.js b/packages/server/services/music/classes/track/methods/delete.js new file mode 100644 index 00000000..24478cda --- /dev/null +++ b/packages/server/services/music/classes/track/methods/delete.js @@ -0,0 +1,9 @@ +import { Track } from "@db_models" + +export default async (track_id) => { + if (!track_id) { + throw new OperationError(400, "Missing track_id") + } + + return await Track.findOneAndDelete({ _id: track_id }) +} \ No newline at end of file diff --git a/packages/server/services/music/classes/track/methods/get.js b/packages/server/services/music/classes/track/methods/get.js new file mode 100644 index 00000000..b9fd1a82 --- /dev/null +++ b/packages/server/services/music/classes/track/methods/get.js @@ -0,0 +1,30 @@ +import { Track } from "@db_models" + +export default async (track_id, { limit = 50, offset = 0 } = {}) => { + if (!track_id) { + throw new OperationError(400, "Missing track_id") + } + + const isMultiple = track_id.includes(",") + + if (isMultiple) { + const track_ids = track_id.split(",") + + const tracks = await Track.find({ _id: { $in: track_ids } }) + .limit(limit) + .skip(offset) + + return { + total_count: await Track.countDocuments({ _id: { $in: track_ids } }), + list: tracks.map(track => track.toObject()), + } + } + + const track = await Track.findById(track_id).catch(() => null) + + if (!track) { + throw new OperationError(404, "Track not found") + } + + return track +} \ No newline at end of file diff --git a/packages/server/services/music/package.json b/packages/server/services/music/package.json index 7adb9b27..b414fb8b 100755 --- a/packages/server/services/music/package.json +++ b/packages/server/services/music/package.json @@ -24,7 +24,8 @@ "moment-timezone": "0.5.37", "mongoose": "^6.9.0", "morgan": "^1.10.0", + "music-metadata": "^7.14.0", "redis": "^4.6.6", "socket.io": "^4.5.4" } -} \ No newline at end of file +} diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js b/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js new file mode 100644 index 00000000..e410e18c --- /dev/null +++ b/packages/server/services/music/routes/music/tracks/[track_id]/data/get.js @@ -0,0 +1,11 @@ +import TrackClass from "@classes/track" + +export default { + fn: async (req) => { + const { track_id } = req.params + + const track = await TrackClass.get(track_id) + + return track + } +} \ No newline at end of file diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/delete.js b/packages/server/services/music/routes/music/tracks/[track_id]/delete.js new file mode 100644 index 00000000..5c14506f --- /dev/null +++ b/packages/server/services/music/routes/music/tracks/[track_id]/delete.js @@ -0,0 +1,21 @@ +import TrackClass from "@classes/track" + +export default { + middlewares: ["withAuthentication"], + fn: async (req) => { + const { track_id } = req.params + + const track = await TrackClass.get(track_id) + + if (track.publisher.user_id !== req.auth.session.user_id) { + throw new Error("Forbidden, you don't own this track") + } + + await TrackClass.delete(track_id) + + return { + success: true, + track: track, + } + } +} \ No newline at end of file diff --git a/packages/server/services/music/routes/music/tracks/put.js b/packages/server/services/music/routes/music/tracks/put.js new file mode 100644 index 00000000..c83a489d --- /dev/null +++ b/packages/server/services/music/routes/music/tracks/put.js @@ -0,0 +1,16 @@ +import requiredFields from "@shared-utils/requiredFields" +import TrackClass from "@classes/track" + +export default { + middlewares: ["withAuthentication"], + fn: async (req) => { + requiredFields(["title", "source"], req.body) + + const track = await TrackClass.create({ + ...req.body, + user_id: req.auth.session.user_id, + }) + + return track + } +} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/deleteStreamingProfile.js b/packages/server/services/tv/StreamingController/endpoints/deleteStreamingProfile.js deleted file mode 100755 index 19deaa4c..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/deleteStreamingProfile.js +++ /dev/null @@ -1,42 +0,0 @@ -import { StreamingProfile } from "@db_models" - -export default { - method: "DELETE", - route: "/streaming/profile", - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user_id = req.user._id.toString() - const { profile_id } = req.body - - if (!profile_id) { - return res.status(400).json({ - error: "Invalid request, missing profile_id" - }) - } - - // search for existing profile - let currentProfile = await StreamingProfile.findOne({ - _id: profile_id, - }) - - if (!currentProfile) { - return res.status(400).json({ - error: "Invalid request, profile not found" - }) - } - - // check if the profile belongs to the user - if (currentProfile.user_id !== user_id) { - return res.status(400).json({ - error: "Invalid request, profile does not belong to the user" - }) - } - - // delete the profile - await currentProfile.delete() - - return res.json({ - success: true - }) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/getProfileFromStreamKey.js b/packages/server/services/tv/StreamingController/endpoints/getProfileFromStreamKey.js deleted file mode 100755 index 8de0f6f2..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/getProfileFromStreamKey.js +++ /dev/null @@ -1,19 +0,0 @@ -import { StreamingProfile } from "@db_models" - -export default { - method: "GET", - route: "/profile/streamkey/:streamkey", - fn: async (req, res) => { - const profile = await StreamingProfile.findOne({ - stream_key: req.params.streamkey - }) - - if (!profile) { - return res.status(404).json({ - error: "Profile not found" - }) - } - - return res.json(profile) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/getProfilesVisibility.js b/packages/server/services/tv/StreamingController/endpoints/getProfilesVisibility.js deleted file mode 100755 index d83ad2b7..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/getProfilesVisibility.js +++ /dev/null @@ -1,24 +0,0 @@ -import { StreamingProfile } from "@db_models" - -export default { - method: "GET", - route: "/profile/visibility", - middlewares: ["withAuthentication"], - fn: async (req, res) => { - let { ids } = req.query - - if (typeof ids === "string") { - ids = [ids] - } - - let visibilities = await StreamingProfile.find({ - _id: { $in: ids } - }) - - visibilities = visibilities.map((visibility) => { - return [visibility._id.toString(), visibility.options.private] - }) - - return res.json(visibilities) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/getStreamingCategories.js b/packages/server/services/tv/StreamingController/endpoints/getStreamingCategories.js deleted file mode 100755 index 53db7e0e..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/getStreamingCategories.js +++ /dev/null @@ -1,17 +0,0 @@ -import { StreamingCategory } from "@db_models" - -export default { - method: "GET", - route: "/streaming/categories", - fn: async (req, res) => { - const categories = await StreamingCategory.find() - - if (req.query.key) { - const category = categories.find((category) => category.key === req.query.key) - - return res.json(category) - } - - return res.json(categories) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/getStreamingProfiles.js b/packages/server/services/tv/StreamingController/endpoints/getStreamingProfiles.js deleted file mode 100755 index 1507dd2d..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/getStreamingProfiles.js +++ /dev/null @@ -1,50 +0,0 @@ -import { StreamingProfile } from "@db_models" -import NewStreamingProfile from "@services/newStreamingProfile" -import composeStreamingSources from "@utils/compose-streaming-sources" - -export default { - method: "GET", - route: "/streaming/profiles", - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user_id = req.user._id.toString() - - if (!user_id) { - return res.status(400).json({ - error: "Invalid request, missing user_id" - }) - } - - let profiles = await StreamingProfile.find({ - user_id, - }).select("+stream_key") - - if (profiles.length === 0) { - // create a new profile - const profile = await NewStreamingProfile({ - user_id, - profile_name: "default", - }) - - profiles = [profile] - } - - profiles = profiles.map((profile) => { - profile = profile.toObject() - - profile._id = profile._id.toString() - - profile.stream_key = `${req.user.username}__${profile._id}?secret=${profile.stream_key}` - - return profile - }) - - profiles = profiles.map((profile) => { - profile.addresses = composeStreamingSources(req.user.username, profile._id) - - return profile - }) - - return res.json(profiles) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/getStreams.js b/packages/server/services/tv/StreamingController/endpoints/getStreams.js deleted file mode 100755 index 20546e2f..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/getStreams.js +++ /dev/null @@ -1,23 +0,0 @@ -import fetchRemoteStreams from "@services/fetchRemoteStreams" - -export default { - method: "GET", - route: "/streams", - fn: async (req, res) => { - if (req.query.username) { - const stream = await fetchRemoteStreams(`${req.query.username}${req.query.profile_id ? `__${req.query.profile_id}` : ""}`) - - if (!stream) { - return res.status(404).json({ - error: "Stream not found" - }) - } - - return res.json(stream) - } else { - const streams = await fetchRemoteStreams() - - return res.json(streams) - } - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/handleStreamPublish.js b/packages/server/services/tv/StreamingController/endpoints/handleStreamPublish.js deleted file mode 100755 index 39c0086c..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/handleStreamPublish.js +++ /dev/null @@ -1,58 +0,0 @@ -import { StreamingProfile, User } from "@db_models" - -export default { - method: "POST", - route: "/stream/publish", - fn: async (req, res) => { - const { stream, app } = req.body - - if (process.env.STREAMING__OUTPUT_PUBLISH_REQUESTS === "true") { - console.log("Publish request:", req.body) - } - - const streamingProfile = await StreamingProfile.findOne({ - stream_key: stream - }) - - if (!streamingProfile) { - return res.status(404).json({ - code: 1, - error: "Streaming profile not found", - }) - } - - const user = await User.findById(streamingProfile.user_id) - - if (!user) { - return res.status(404).json({ - code: 1, - error: "User not found", - }) - } - - const [username, profile_id] = app.split("/")[1].split("__") - - if (user.username !== username) { - return res.status(403).json({ - code: 1, - error: "Invalid mount point, username does not match with the stream key", - }) - } - - if (streamingProfile._id.toString() !== profile_id) { - return res.status(403).json({ - code: 1, - error: "Invalid mount point, profile id does not match with the stream key", - }) - } - - global.engine.ws.io.of("/").emit(`streaming.new`, streamingProfile) - - global.engine.ws.io.of("/").emit(`streaming.new.${streamingProfile.user_id}`, streamingProfile) - - return res.json({ - code: 0, - status: "ok" - }) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/handleStreamUnpublish.js b/packages/server/services/tv/StreamingController/endpoints/handleStreamUnpublish.js deleted file mode 100755 index a5545532..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/handleStreamUnpublish.js +++ /dev/null @@ -1,29 +0,0 @@ -import { StreamingProfile } from "@db_models" - -export default { - method: "POST", - route: "/stream/unpublish", - fn: async (req, res) => { - const { stream } = req.body - - const streamingProfile = await StreamingProfile.findOne({ - stream_key: stream - }) - - if (streamingProfile) { - global.engine.ws.io.of("/").emit(`streaming.end`, streamingProfile) - - global.engine.ws.io.of("/").emit(`streaming.end.${streamingProfile.user_id}`, streamingProfile) - - return res.json({ - code: 0, - status: "ok" - }) - } - - return res.json({ - code: 0, - status: "ok, but no streaming profile found" - }) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/postStreamingProfile.js b/packages/server/services/tv/StreamingController/endpoints/postStreamingProfile.js deleted file mode 100755 index d76948f8..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/postStreamingProfile.js +++ /dev/null @@ -1,64 +0,0 @@ -import { StreamingProfile } from "@db_models" -import NewStreamingProfile from "@services/newStreamingProfile" - -const AllowedChangesFields = ["profile_name", "info", "options"] - -export default { - method: "POST", - route: "/streaming/profile", - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user_id = req.user._id.toString() - - if (!user_id) { - return res.status(400).json({ - error: "Invalid request, missing user_id" - }) - } - - const { - profile_id, - profile_name, - info, - options, - } = req.body - - if (!profile_id && !profile_name) { - return res.status(400).json({ - error: "Invalid request, missing profile_id and profile_name" - }) - } - - // search for existing profile - let currentProfile = await StreamingProfile.findOne({ - _id: profile_id, - }) - - if (currentProfile && profile_id) { - // update the profile - AllowedChangesFields.forEach((field) => { - if (req.body[field]) { - currentProfile[field] = req.body[field] - } - }) - - await currentProfile.save() - } else { - if (!profile_name) { - return res.status(400).json({ - error: "Invalid request, missing profile_name" - }) - } - - // create a new profile - currentProfile = await NewStreamingProfile({ - user_id, - profile_name, - info, - options, - }) - } - - return res.json(currentProfile) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/endpoints/regenerateStreamingKey.js b/packages/server/services/tv/StreamingController/endpoints/regenerateStreamingKey.js deleted file mode 100755 index 33de6b5c..00000000 --- a/packages/server/services/tv/StreamingController/endpoints/regenerateStreamingKey.js +++ /dev/null @@ -1,37 +0,0 @@ -import { StreamingProfile } from "@db_models" - -export default { - method: "POST", - route: "/streaming/regenerate_key", - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const { profile_id } = req.body - - if (!profile_id) { - return res.status(400).json({ - message: "Missing profile_id" - }) - } - - const profile = await StreamingProfile.findById(profile_id) - - if (!profile) { - return res.status(404).json({ - message: "Profile not found" - }) - } - - // check if profile user is the same as the user in the request - if (profile.user_id !== req.user._id.toString()) { - return res.status(403).json({ - message: "You are not allowed to regenerate this key" - }) - } - - profile.stream_key = global.nanoid() - - await profile.save() - - return res.json(profile.toObject()) - } -} \ No newline at end of file diff --git a/packages/server/services/tv/StreamingController/index.js b/packages/server/services/tv/StreamingController/index.js deleted file mode 100755 index 68ccc0fe..00000000 --- a/packages/server/services/tv/StreamingController/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Controller } from "linebridge/dist/server" -import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" - -export default class StreamingController extends Controller { - static refName = "StreamingController" - static useRoute = "/tv" - - httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") - - // put = { - // "/streaming/category": { - // middlewares: ["withAuthentication", "onlyAdmin"], - // fn: Schematized({ - // required: ["key", "label"] - // }, async (req, res) => { - // const { key, label } = req.selection - - // const existingCategory = await StreamingCategory.findOne({ - // key - // }) - - // if (existingCategory) { - // return res.status(400).json({ - // error: "Category already exists" - // }) - // } - - // const category = new StreamingCategory({ - // key, - // label, - // }) - - // await category.save() - - // return res.json(category) - // }) - // } - // } -} \ No newline at end of file