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