mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
d6a074a859
commit
06aee646ca
@ -88,6 +88,11 @@ export default [
|
|||||||
centeredContent: true,
|
centeredContent: true,
|
||||||
extendedContent: true,
|
extendedContent: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/violation",
|
||||||
|
useLayout: "minimal",
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
// THIS MUST BE THE LAST ROUTE
|
// THIS MUST BE THE LAST ROUTE
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
@ -40,7 +40,7 @@ export default class Login extends React.Component {
|
|||||||
loginInputs: {},
|
loginInputs: {},
|
||||||
error: null,
|
error: null,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
mfa_required: null
|
mfa_required: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
formRef = React.createRef()
|
formRef = React.createRef()
|
||||||
@ -60,6 +60,18 @@ export default class Login extends React.Component {
|
|||||||
this.toggleLoading(true)
|
this.toggleLoading(true)
|
||||||
|
|
||||||
await AuthModel.login(payload, this.onDone).catch((error) => {
|
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)
|
console.error(error, error.response)
|
||||||
|
|
||||||
this.toggleLoading(false)
|
this.toggleLoading(false)
|
||||||
|
@ -23,7 +23,7 @@ export const Panel = (props) => {
|
|||||||
|
|
||||||
export class PagePanelWithNavMenu extends React.Component {
|
export class PagePanelWithNavMenu extends React.Component {
|
||||||
state = {
|
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: [],
|
renders: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,16 +115,16 @@ export default class Player extends Core {
|
|||||||
|
|
||||||
internalEvents = {
|
internalEvents = {
|
||||||
"player.state.update:loading": () => {
|
"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": () => {
|
"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": () => {
|
"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) => {
|
"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")) {
|
if (playlist.some((item) => typeof item === "string")) {
|
||||||
this.console.log("Resolving missing manifests by ids...")
|
this.console.log("Resolving missing manifests by ids...")
|
||||||
playlist = await this.getTracksByIds(playlist)
|
playlist = await ServicesHandlers.default.resolveMany(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist = playlist.slice(startIndex)
|
playlist = playlist.slice(startIndex)
|
||||||
|
@ -3,7 +3,18 @@ import MusicModel from "comty.js/models/music"
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
"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) => {
|
toggleLike: async (manifest, to) => {
|
||||||
return await MusicModel.toggleTrackLike(manifest, to)
|
return await MusicModel.toggleTrackLike(manifest, to)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
return <div>
|
|
||||||
Dashboard
|
|
||||||
</div>
|
|
||||||
}
|
|
@ -139,11 +139,7 @@ const PlaylistItem = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OwnPlaylists = (props) => {
|
const OwnPlaylists = (props) => {
|
||||||
const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists, {
|
const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists)
|
||||||
services: {
|
|
||||||
tidal: app.cores.sync.getActiveLinkedServices().tidal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (E_Playlists) {
|
if (E_Playlists) {
|
||||||
console.error(E_Playlists)
|
console.error(E_Playlists)
|
||||||
@ -183,8 +179,36 @@ const OwnPlaylists = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
const Library = (props) => {
|
||||||
return <div className="music-library">
|
return <div className="music-library">
|
||||||
|
<div className="music-library_header">
|
||||||
|
<h1>Library</h1>
|
||||||
|
|
||||||
|
<antd.Segmented
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: "tracks",
|
||||||
|
label: "Tracks",
|
||||||
|
icon: <Icons.MdMusicNote />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "playlist",
|
||||||
|
label: "Playlists",
|
||||||
|
icon: <Icons.MdPlaylistPlay />
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PlaylistItem
|
||||||
|
type="action"
|
||||||
|
data={{
|
||||||
|
icon: <Icons.MdPlaylistAdd />,
|
||||||
|
title: "Create new",
|
||||||
|
}}
|
||||||
|
onClick={OpenPlaylistCreator}
|
||||||
|
/>
|
||||||
<OwnPlaylists />
|
<OwnPlaylists />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Library
|
@ -170,4 +170,18 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.music-library_header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
15
packages/app/src/pages/music/dashboard/index.jsx
Executable file
15
packages/app/src/pages/music/dashboard/index.jsx
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
return <div className="music-dashboard">
|
||||||
|
<div className="music-dashboard_header">
|
||||||
|
<h1>Your Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="music-dashboard_content">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
8
packages/app/src/pages/music/dashboard/index.less
Normal file
8
packages/app/src/pages/music/dashboard/index.less
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.music-dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.music-dashboard_header {}
|
||||||
|
}
|
@ -5,7 +5,7 @@ import { Icons } from "components/Icons"
|
|||||||
import { ImageViewer } from "components"
|
import { ImageViewer } from "components"
|
||||||
import Searcher from "components/Searcher"
|
import Searcher from "components/Searcher"
|
||||||
|
|
||||||
import ReleaseCreator from "../../../creator"
|
import ReleaseCreator from "../../creator"
|
||||||
|
|
||||||
import MusicModel from "models/music"
|
import MusicModel from "models/music"
|
||||||
|
|
@ -1,9 +1,6 @@
|
|||||||
import LibraryTab from "./components/library"
|
import LibraryTab from "./components/library"
|
||||||
import FavoritesTab from "./components/favorites"
|
import FavoritesTab from "./components/favorites"
|
||||||
import ExploreTab from "./components/explore"
|
import ExploreTab from "./components/explore"
|
||||||
import DashboardTab from "./components/dashboard"
|
|
||||||
|
|
||||||
import ReleasesTab from "./components/dashboard/releases"
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@ -30,18 +27,4 @@ export default [
|
|||||||
icon: "Radio",
|
icon: "Radio",
|
||||||
disabled: true
|
disabled: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "artist_panel",
|
|
||||||
label: "Creator Panel",
|
|
||||||
icon: "MdSpaceDashboard",
|
|
||||||
component: DashboardTab,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "artist_panel.releases",
|
|
||||||
label: "Releases",
|
|
||||||
icon: "MdUpcoming",
|
|
||||||
component: ReleasesTab,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
]
|
40
packages/app/src/pages/music/track/[track_id]/index.jsx
Normal file
40
packages/app/src/pages/music/track/[track_id]/index.jsx
Normal file
@ -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 <antd.Result
|
||||||
|
status="warning"
|
||||||
|
title="Error"
|
||||||
|
subTitle={error.message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="track-page">
|
||||||
|
<PlaylistView
|
||||||
|
playlist={{
|
||||||
|
title: result.title,
|
||||||
|
cover: result.cover_url,
|
||||||
|
list: [result]
|
||||||
|
}}
|
||||||
|
centered={app.isMobile}
|
||||||
|
hasMore={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrackPage
|
6
packages/app/src/pages/music/track/[track_id]/index.less
Normal file
6
packages/app/src/pages/music/track/[track_id]/index.less
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.track-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
9
packages/app/src/pages/violation/index.jsx
Normal file
9
packages/app/src/pages/violation/index.jsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
const ViolationPage = (props) => {
|
||||||
|
return <div>
|
||||||
|
<h1>Violation</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViolationPage
|
@ -1,5 +1,5 @@
|
|||||||
import jwt from "jsonwebtoken"
|
import jwt from "jsonwebtoken"
|
||||||
import { Session, RegenerationToken, User } from "../../db_models"
|
import { Session, RegenerationToken, User, TosViolations } from "../../db_models"
|
||||||
|
|
||||||
export default class Token {
|
export default class Token {
|
||||||
static get strategy() {
|
static get strategy() {
|
||||||
@ -69,6 +69,21 @@ export default class Token {
|
|||||||
|
|
||||||
result.data = decoded
|
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 sessions = await Session.find({ user_id: decoded.user_id })
|
||||||
const currentSession = sessions.find((session) => session.token === token)
|
const currentSession = sessions.find((session) => session.token === token)
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ export async function uploadChunkFile(req, {
|
|||||||
}) {
|
}) {
|
||||||
return await new Promise(async (resolve, reject) => {
|
return await new Promise(async (resolve, reject) => {
|
||||||
if (!checkChunkUploadHeaders(req.headers)) {
|
if (!checkChunkUploadHeaders(req.headers)) {
|
||||||
reject(new OperationErrorError(400, "Missing header(s)"))
|
reject(new OperationError(400, "Missing header(s)"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
packages/server/db_models/tosViolations/index.js
Normal file
17
packages/server/db_models/tosViolations/index.js
Normal file
@ -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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,8 @@ export default {
|
|||||||
album: {
|
album: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
artist: {
|
artists: {
|
||||||
type: String,
|
type: Array,
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -31,10 +31,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: "https://storage.ragestudio.net/comty-static-assets/default_song.png"
|
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: {
|
videoCanvas: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
@ -3,19 +3,23 @@ import SecureEntry from "../../classes/SecureEntry"
|
|||||||
import AuthToken from "../../classes/AuthToken"
|
import AuthToken from "../../classes/AuthToken"
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
function reject(description) {
|
function reject(data) {
|
||||||
return res.status(401).json({ error: `${description ?? "Invalid session"}` })
|
return res.status(401).json(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenAuthHeader = req.headers?.authorization?.split(" ")
|
const tokenAuthHeader = req.headers?.authorization?.split(" ")
|
||||||
|
|
||||||
if (!tokenAuthHeader) {
|
if (!tokenAuthHeader) {
|
||||||
return reject("Missing token header")
|
return reject({
|
||||||
|
error: "Missing token header"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tokenAuthHeader[1]) {
|
if (!tokenAuthHeader[1]) {
|
||||||
return reject("Recived header, missing token")
|
return reject({
|
||||||
|
error: "Recived header, missing token"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (tokenAuthHeader[0]) {
|
switch (tokenAuthHeader[0]) {
|
||||||
@ -25,7 +29,7 @@ export default async (req, res) => {
|
|||||||
const validation = await AuthToken.validate(token)
|
const validation = await AuthToken.validate(token)
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return reject(validation.error)
|
return reject(validation)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.auth = {
|
req.auth = {
|
||||||
@ -41,7 +45,9 @@ export default async (req, res) => {
|
|||||||
const [client_id, token] = tokenAuthHeader[1].split(":")
|
const [client_id, token] = tokenAuthHeader[1].split(":")
|
||||||
|
|
||||||
if (client_id === "undefined" || token === "undefined") {
|
if (client_id === "undefined" || token === "undefined") {
|
||||||
return reject("Invalid server token")
|
return reject({
|
||||||
|
error: "Invalid server token"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const secureEntries = new SecureEntry(authorizedServerTokens)
|
const secureEntries = new SecureEntry(authorizedServerTokens)
|
||||||
@ -52,11 +58,15 @@ export default async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!serverTokenEntry) {
|
if (!serverTokenEntry) {
|
||||||
return reject("Invalid server token")
|
return reject({
|
||||||
|
error: "Invalid server token"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverTokenEntry !== token) {
|
if (serverTokenEntry !== token) {
|
||||||
return reject("Missmatching server token")
|
return reject({
|
||||||
|
error: "Missmatching server token"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = {
|
req.user = {
|
||||||
@ -68,11 +78,16 @@ export default async (req, res) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return reject("Invalid token type")
|
return reject({
|
||||||
|
error: "Invalid token type"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import AuthToken from "@shared-classes/AuthToken"
|
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 requiredFields from "@shared-utils/requiredFields"
|
||||||
import obscureEmail from "@shared-utils/obscureEmail"
|
import obscureEmail from "@shared-utils/obscureEmail"
|
||||||
|
|
||||||
@ -13,6 +13,15 @@ export default async (req, res) => {
|
|||||||
password: req.body.password,
|
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(() => {
|
const userConfig = await UserConfig.findOne({ user_id: user._id.toString() }).catch(() => {
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
5
packages/server/services/music/classes/track/index.js
Normal file
5
packages/server/services/music/classes/track/index.js
Normal file
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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 })
|
||||||
|
}
|
30
packages/server/services/music/classes/track/methods/get.js
Normal file
30
packages/server/services/music/classes/track/methods/get.js
Normal file
@ -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
|
||||||
|
}
|
@ -24,7 +24,8 @@
|
|||||||
"moment-timezone": "0.5.37",
|
"moment-timezone": "0.5.37",
|
||||||
"mongoose": "^6.9.0",
|
"mongoose": "^6.9.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
"music-metadata": "^7.14.0",
|
||||||
"redis": "^4.6.6",
|
"redis": "^4.6.6",
|
||||||
"socket.io": "^4.5.4"
|
"socket.io": "^4.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
packages/server/services/music/routes/music/tracks/put.js
Normal file
16
packages/server/services/music/routes/music/tracks/put.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user