mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
commit
88a4e063a6
@ -1 +1 @@
|
|||||||
Subproject commit ff38d45b9686ccbd2e902477bde4cd7eb7d251e8
|
Subproject commit 57d8b4bed14b0b35d1d9753847ac39710e0d9be5
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@comty/app",
|
"name": "@comty/app",
|
||||||
"version": "1.37.1@alpha",
|
"version": "1.38.0@alpha",
|
||||||
"license": "ComtyLicense",
|
"license": "ComtyLicense",
|
||||||
"main": "electron/main",
|
"main": "electron/main",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"comty.js": "^0.63.1",
|
"comty.js": "^0.63.1",
|
||||||
"dashjs": "^4.7.4",
|
"dashjs": "^5.0.0",
|
||||||
"dompurify": "^3.0.0",
|
"dompurify": "^3.0.0",
|
||||||
"fast-average-color": "^9.2.0",
|
"fast-average-color": "^9.2.0",
|
||||||
"fuse.js": "6.5.3",
|
"fuse.js": "6.5.3",
|
||||||
@ -49,6 +49,7 @@
|
|||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
"motion": "^12.4.2",
|
"motion": "^12.4.2",
|
||||||
"mpegts.js": "^1.6.10",
|
"mpegts.js": "^1.6.10",
|
||||||
|
"music-metadata": "^11.2.1",
|
||||||
"plyr": "^3.7.8",
|
"plyr": "^3.7.8",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
|
@ -170,7 +170,7 @@ export default class ChunkedUpload {
|
|||||||
|
|
||||||
// check if is the last chunk, if so, handle sse events
|
// check if is the last chunk, if so, handle sse events
|
||||||
if (this.chunkCount === this.totalChunks) {
|
if (this.chunkCount === this.totalChunks) {
|
||||||
if (data.sseChannelId || data.eventChannelURL) {
|
if (data.sseChannelId || data.sseUrl) {
|
||||||
this.waitOnSSE(data)
|
this.waitOnSSE(data)
|
||||||
} else {
|
} else {
|
||||||
this.events.emit("finish", data)
|
this.events.emit("finish", data)
|
||||||
@ -178,9 +178,8 @@ export default class ChunkedUpload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.events.emit("progress", {
|
this.events.emit("progress", {
|
||||||
percentProgress: Math.round(
|
percent: Math.round((100 / this.totalChunks) * this.chunkCount),
|
||||||
(100 / this.totalChunks) * this.chunkCount,
|
state: "Uploading",
|
||||||
),
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.events.emit("error", error)
|
this.events.emit("error", error)
|
||||||
@ -196,12 +195,9 @@ export default class ChunkedUpload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
waitOnSSE(data) {
|
waitOnSSE(data) {
|
||||||
console.log(
|
console.log(`[UPLOADER] Connecting to SSE channel >`, data.sseUrl)
|
||||||
`[UPLOADER] Connecting to SSE channel >`,
|
|
||||||
data.eventChannelURL,
|
|
||||||
)
|
|
||||||
|
|
||||||
const eventSource = new EventSource(data.eventChannelURL)
|
const eventSource = new EventSource(data.sseUrl)
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
eventSource.onerror = (error) => {
|
||||||
this.events.emit("error", error)
|
this.events.emit("error", error)
|
||||||
@ -218,19 +214,20 @@ export default class ChunkedUpload {
|
|||||||
|
|
||||||
console.log(`[UPLOADER] SSE Event >`, messageData)
|
console.log(`[UPLOADER] SSE Event >`, messageData)
|
||||||
|
|
||||||
if (messageData.status === "done") {
|
if (messageData.event === "done") {
|
||||||
this.events.emit("finish", messageData.result)
|
this.events.emit("finish", messageData.result)
|
||||||
eventSource.close()
|
eventSource.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageData.status === "error") {
|
if (messageData.event === "error") {
|
||||||
this.events.emit("error", messageData.result)
|
this.events.emit("error", messageData.result)
|
||||||
eventSource.close()
|
eventSource.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageData.status === "progress") {
|
if (messageData.state) {
|
||||||
this.events.emit("progress", {
|
this.events.emit("progress", {
|
||||||
percentProgress: messageData.progress,
|
percent: messageData.percent,
|
||||||
|
state: messageData.state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,19 @@ const CoverEditor = (props) => {
|
|||||||
setInit(false)
|
setInit(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className="cover-editor">
|
// Handle when value prop change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
setUrl(defaultUrl)
|
||||||
|
} else {
|
||||||
|
setUrl(value)
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cover-editor">
|
||||||
<div className="cover-editor-preview">
|
<div className="cover-editor-preview">
|
||||||
<Image
|
<Image src={url} />
|
||||||
src={url}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="cover-editor-actions">
|
<div className="cover-editor-actions">
|
||||||
@ -51,11 +59,10 @@ const CoverEditor = (props) => {
|
|||||||
Reset
|
Reset
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
|
|
||||||
{
|
{props.extraActions}
|
||||||
props.extraActions
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CoverEditor
|
export default CoverEditor
|
||||||
|
@ -392,9 +392,7 @@ const PlaylistView = (props) => {
|
|||||||
key={item._id}
|
key={item._id}
|
||||||
order={item._id}
|
order={item._id}
|
||||||
track={item}
|
track={item}
|
||||||
onClickPlayBtn={() =>
|
onPlay={() => handleOnClickTrack(item)}
|
||||||
handleOnClickTrack(item)
|
|
||||||
}
|
|
||||||
changeState={(update) =>
|
changeState={(update) =>
|
||||||
handleTrackChangeState(
|
handleTrackChangeState(
|
||||||
item._id,
|
item._id,
|
||||||
@ -418,7 +416,7 @@ const PlaylistView = (props) => {
|
|||||||
<MusicTrack
|
<MusicTrack
|
||||||
order={index + 1}
|
order={index + 1}
|
||||||
track={item}
|
track={item}
|
||||||
onClickPlayBtn={() =>
|
onPlay={() =>
|
||||||
handleOnClickTrack(item)
|
handleOnClickTrack(item)
|
||||||
}
|
}
|
||||||
changeState={(update) =>
|
changeState={(update) =>
|
||||||
|
@ -52,6 +52,10 @@ const Track = (props) => {
|
|||||||
const isPlaying = isCurrent && playback_status === "playing"
|
const isPlaying = isCurrent && playback_status === "playing"
|
||||||
|
|
||||||
const handleClickPlayBtn = React.useCallback(() => {
|
const handleClickPlayBtn = React.useCallback(() => {
|
||||||
|
if (typeof props.onPlay === "function") {
|
||||||
|
return props.onPlay(props.track)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof props.onClickPlayBtn === "function") {
|
if (typeof props.onClickPlayBtn === "function") {
|
||||||
props.onClickPlayBtn(props.track)
|
props.onClickPlayBtn(props.track)
|
||||||
}
|
}
|
||||||
|
@ -20,21 +20,22 @@ const VideoEditor = (props) => {
|
|||||||
props.onChange(key, value)
|
props.onChange(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="video-editor">
|
return (
|
||||||
|
<div className="video-editor">
|
||||||
<h1>
|
<h1>
|
||||||
<Icons.MdVideocam />
|
<Icons.MdVideocam />
|
||||||
Video
|
Video
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{
|
{!props.videoSourceURL && (
|
||||||
(!props.videoSourceURL) && <antd.Empty
|
<antd.Empty
|
||||||
image={<Icons.MdVideocam />}
|
image={<Icons.MdVideocam />}
|
||||||
description="No video"
|
description="No video"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{
|
{props.videoSourceURL && (
|
||||||
props.videoSourceURL && <div className="video-editor-preview">
|
<div className="video-editor-preview">
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
controls={[
|
controls={[
|
||||||
"play",
|
"play",
|
||||||
@ -47,7 +48,7 @@ const VideoEditor = (props) => {
|
|||||||
src={props.videoSourceURL}
|
src={props.videoSourceURL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<div className="flex-column align-start gap10">
|
<div className="flex-column align-start gap10">
|
||||||
<div className="flex-row align-center gap10">
|
<div className="flex-row align-center gap10">
|
||||||
@ -64,7 +65,10 @@ const VideoEditor = (props) => {
|
|||||||
|
|
||||||
<antd.TimePicker
|
<antd.TimePicker
|
||||||
showNow={false}
|
showNow={false}
|
||||||
defaultValue={props.startSyncAt && dayjs((props.startSyncAt), "mm:ss:SSS")}
|
defaultValue={
|
||||||
|
props.startSyncAt &&
|
||||||
|
dayjs(props.startSyncAt, "mm:ss:SSS")
|
||||||
|
}
|
||||||
format={"mm:ss:SSS"}
|
format={"mm:ss:SSS"}
|
||||||
onChange={(time, str) => {
|
onChange={(time, str) => {
|
||||||
handleChange("startSyncAt", str)
|
handleChange("startSyncAt", str)
|
||||||
@ -78,19 +82,15 @@ const VideoEditor = (props) => {
|
|||||||
onSuccess={(id, response) => {
|
onSuccess={(id, response) => {
|
||||||
handleChange("videoSourceURL", response.url)
|
handleChange("videoSourceURL", response.url)
|
||||||
}}
|
}}
|
||||||
accept={[
|
accept={["video/*"]}
|
||||||
"video/*",
|
|
||||||
]}
|
|
||||||
headers={{
|
headers={{
|
||||||
"transmux": "mq-hls",
|
transformations: "mq-hls",
|
||||||
}}
|
}}
|
||||||
disabled={props.loading}
|
disabled={props.loading}
|
||||||
>
|
>
|
||||||
Upload video
|
Upload video
|
||||||
</UploadButton>
|
</UploadButton>
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
<antd.Input
|
<antd.Input
|
||||||
placeholder="Set a video HLS URL"
|
placeholder="Set a video HLS URL"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -101,6 +101,7 @@ const VideoEditor = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VideoEditor
|
export default VideoEditor
|
@ -1,15 +1,17 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
|
||||||
import { Icons, createIconRender } from "@components/Icons"
|
import { Icons, createIconRender } from "@components/Icons"
|
||||||
|
|
||||||
import MusicModel from "@models/music"
|
import MusicModel from "@models/music"
|
||||||
|
import compareObjectsByProperties from "@utils/compareObjectsByProperties"
|
||||||
import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey"
|
import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey"
|
||||||
|
|
||||||
import TrackManifest from "@cores/player/classes/TrackManifest"
|
import TrackManifest from "@cores/player/classes/TrackManifest"
|
||||||
|
|
||||||
import { DefaultReleaseEditorState, ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
|
import {
|
||||||
|
DefaultReleaseEditorState,
|
||||||
|
ReleaseEditorStateContext,
|
||||||
|
} from "@contexts/MusicReleaseEditor"
|
||||||
|
|
||||||
import Tabs from "./tabs"
|
import Tabs from "./tabs"
|
||||||
|
|
||||||
@ -25,14 +27,17 @@ const ReleaseEditor = (props) => {
|
|||||||
const [submitError, setSubmitError] = React.useState(null)
|
const [submitError, setSubmitError] = React.useState(null)
|
||||||
|
|
||||||
const [loadError, setLoadError] = React.useState(null)
|
const [loadError, setLoadError] = React.useState(null)
|
||||||
const [globalState, setGlobalState] = React.useState(DefaultReleaseEditorState)
|
const [globalState, setGlobalState] = React.useState(
|
||||||
|
DefaultReleaseEditorState,
|
||||||
|
)
|
||||||
|
const [initialValues, setInitialValues] = React.useState({})
|
||||||
|
|
||||||
const [customPage, setCustomPage] = React.useState(null)
|
const [customPage, setCustomPage] = React.useState(null)
|
||||||
const [customPageActions, setCustomPageActions] = React.useState([])
|
const [customPageActions, setCustomPageActions] = React.useState([])
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useUrlQueryActiveKey({
|
const [selectedTab, setSelectedTab] = useUrlQueryActiveKey({
|
||||||
defaultKey: "info",
|
defaultKey: "info",
|
||||||
queryKey: "tab"
|
queryKey: "tab",
|
||||||
})
|
})
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
@ -43,8 +48,8 @@ const ReleaseEditor = (props) => {
|
|||||||
try {
|
try {
|
||||||
let releaseData = await MusicModel.getReleaseData(release_id)
|
let releaseData = await MusicModel.getReleaseData(release_id)
|
||||||
|
|
||||||
if (Array.isArray(releaseData.list)) {
|
if (Array.isArray(releaseData.items)) {
|
||||||
releaseData.list = releaseData.list.map((item) => {
|
releaseData.items = releaseData.items.map((item) => {
|
||||||
return new TrackManifest(item)
|
return new TrackManifest(item)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -53,6 +58,8 @@ const ReleaseEditor = (props) => {
|
|||||||
...globalState,
|
...globalState,
|
||||||
...releaseData,
|
...releaseData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setInitialValues(releaseData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoadError(error)
|
setLoadError(error)
|
||||||
}
|
}
|
||||||
@ -61,6 +68,22 @@ const ReleaseEditor = (props) => {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasChanges() {
|
||||||
|
const stagedChanges = {
|
||||||
|
title: globalState.title,
|
||||||
|
type: globalState.type,
|
||||||
|
public: globalState.public,
|
||||||
|
cover: globalState.cover,
|
||||||
|
items: globalState.items,
|
||||||
|
}
|
||||||
|
|
||||||
|
return !compareObjectsByProperties(
|
||||||
|
stagedChanges,
|
||||||
|
initialValues,
|
||||||
|
Object.keys(stagedChanges),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function renderCustomPage(page, actions) {
|
async function renderCustomPage(page, actions) {
|
||||||
setCustomPage(page ?? null)
|
setCustomPage(page ?? null)
|
||||||
setCustomPageActions(actions ?? [])
|
setCustomPageActions(actions ?? [])
|
||||||
@ -71,11 +94,15 @@ const ReleaseEditor = (props) => {
|
|||||||
setSubmitError(null)
|
setSubmitError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("Submitting Tracks")
|
||||||
|
|
||||||
// first sumbit tracks
|
// first sumbit tracks
|
||||||
const tracks = await MusicModel.putTrack({
|
const tracks = await MusicModel.putTrack({
|
||||||
list: globalState.list,
|
items: globalState.items,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("Submitting release")
|
||||||
|
|
||||||
// then submit release
|
// then submit release
|
||||||
const result = await MusicModel.putRelease({
|
const result = await MusicModel.putRelease({
|
||||||
_id: globalState._id,
|
_id: globalState._id,
|
||||||
@ -85,7 +112,7 @@ const ReleaseEditor = (props) => {
|
|||||||
cover: globalState.cover,
|
cover: globalState.cover,
|
||||||
explicit: globalState.explicit,
|
explicit: globalState.explicit,
|
||||||
type: globalState.type,
|
type: globalState.type,
|
||||||
list: tracks.list.map((item) => item._id),
|
items: tracks.items.map((item) => item._id),
|
||||||
})
|
})
|
||||||
|
|
||||||
app.location.push(`/studio/music/${result._id}`)
|
app.location.push(`/studio/music/${result._id}`)
|
||||||
@ -109,13 +136,15 @@ const ReleaseEditor = (props) => {
|
|||||||
descriptionText: "This action cannot be undone.",
|
descriptionText: "This action cannot be undone.",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await MusicModel.deleteRelease(globalState._id)
|
await MusicModel.deleteRelease(globalState._id)
|
||||||
app.location.push(window.location.pathname.split("/").slice(0, -1).join("/"))
|
app.location.push(
|
||||||
|
window.location.pathname.split("/").slice(0, -1).join("/"),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function canFinish() {
|
function canFinish() {
|
||||||
return true
|
return hasChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -123,11 +152,13 @@ const ReleaseEditor = (props) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (loadError) {
|
if (loadError) {
|
||||||
return <antd.Result
|
return (
|
||||||
|
<antd.Result
|
||||||
status="warning"
|
status="warning"
|
||||||
title="Error"
|
title="Error"
|
||||||
subTitle={loadError.message}
|
subTitle={loadError.message}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -139,10 +170,11 @@ const ReleaseEditor = (props) => {
|
|||||||
const CustomPageProps = {
|
const CustomPageProps = {
|
||||||
close: () => {
|
close: () => {
|
||||||
renderCustomPage(null, null)
|
renderCustomPage(null, null)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ReleaseEditorStateContext.Provider
|
return (
|
||||||
|
<ReleaseEditorStateContext.Provider
|
||||||
value={{
|
value={{
|
||||||
...globalState,
|
...globalState,
|
||||||
setGlobalState,
|
setGlobalState,
|
||||||
@ -151,59 +183,67 @@ const ReleaseEditor = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="music-studio-release-editor">
|
<div className="music-studio-release-editor">
|
||||||
{
|
{customPage && (
|
||||||
customPage && <div className="music-studio-release-editor-custom-page">
|
<div className="music-studio-release-editor-custom-page">
|
||||||
{
|
{customPage.header && (
|
||||||
customPage.header && <div className="music-studio-release-editor-custom-page-header">
|
<div className="music-studio-release-editor-custom-page-header">
|
||||||
<div className="music-studio-release-editor-custom-page-header-title">
|
<div className="music-studio-release-editor-custom-page-header-title">
|
||||||
<antd.Button
|
<antd.Button
|
||||||
icon={<Icons.IoIosArrowBack />}
|
icon={<Icons.IoIosArrowBack />}
|
||||||
onClick={() => renderCustomPage(null, null)}
|
onClick={() =>
|
||||||
|
renderCustomPage(null, null)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>{customPage.header}</h2>
|
<h2>{customPage.header}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{Array.isArray(customPageActions) &&
|
||||||
Array.isArray(customPageActions) && customPageActions.map((action, index) => {
|
customPageActions.map((action, index) => {
|
||||||
return <antd.Button
|
return (
|
||||||
|
<antd.Button
|
||||||
key={index}
|
key={index}
|
||||||
type={action.type}
|
type={action.type}
|
||||||
icon={createIconRender(action.icon)}
|
icon={createIconRender(
|
||||||
|
action.icon,
|
||||||
|
)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (typeof action.onClick === "function") {
|
if (
|
||||||
|
typeof action.onClick ===
|
||||||
|
"function"
|
||||||
|
) {
|
||||||
await action.onClick()
|
await action.onClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.fireEvent) {
|
if (action.fireEvent) {
|
||||||
app.eventBus.emit(action.fireEvent)
|
app.eventBus.emit(
|
||||||
|
action.fireEvent,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customPage.content && (React.isValidElement(customPage.content) ?
|
|
||||||
React.cloneElement(customPage.content, {
|
|
||||||
...CustomPageProps,
|
|
||||||
...customPage.props
|
|
||||||
}) :
|
|
||||||
React.createElement(customPage.content, {
|
|
||||||
...CustomPageProps,
|
|
||||||
...customPage.props
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{
|
|
||||||
!customPage && <>
|
{customPage.content &&
|
||||||
|
(React.isValidElement(customPage.content)
|
||||||
|
? React.cloneElement(customPage.content, {
|
||||||
|
...CustomPageProps,
|
||||||
|
...customPage.props,
|
||||||
|
})
|
||||||
|
: React.createElement(customPage.content, {
|
||||||
|
...CustomPageProps,
|
||||||
|
...customPage.props,
|
||||||
|
}))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!customPage && (
|
||||||
|
<>
|
||||||
<div className="music-studio-release-editor-menu">
|
<div className="music-studio-release-editor-menu">
|
||||||
<antd.Menu
|
<antd.Menu
|
||||||
onClick={(e) => setSelectedTab(e.key)}
|
onClick={(e) => setSelectedTab(e.key)}
|
||||||
@ -216,65 +256,77 @@ const ReleaseEditor = (props) => {
|
|||||||
<antd.Button
|
<antd.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
icon={release_id !== "new" ? <Icons.FiSave /> : <Icons.MdSend />}
|
icon={
|
||||||
disabled={submitting || loading || !canFinish()}
|
release_id !== "new" ? (
|
||||||
|
<Icons.FiSave />
|
||||||
|
) : (
|
||||||
|
<Icons.MdSend />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
submitting || loading || !canFinish()
|
||||||
|
}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
>
|
>
|
||||||
{release_id !== "new" ? "Save" : "Release"}
|
{release_id !== "new" ? "Save" : "Release"}
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
|
|
||||||
{
|
{release_id !== "new" ? (
|
||||||
release_id !== "new" ? <antd.Button
|
<antd.Button
|
||||||
icon={<Icons.IoMdTrash />}
|
icon={<Icons.IoMdTrash />}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</antd.Button> : null
|
</antd.Button>
|
||||||
}
|
) : null}
|
||||||
|
|
||||||
{
|
{release_id !== "new" ? (
|
||||||
release_id !== "new" ? <antd.Button
|
<antd.Button
|
||||||
icon={<Icons.MdLink />}
|
icon={<Icons.MdLink />}
|
||||||
onClick={() => app.location.push(`/music/release/${globalState._id}`)}
|
onClick={() =>
|
||||||
|
app.location.push(
|
||||||
|
`/music/release/${globalState._id}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Go to release
|
Go to release
|
||||||
</antd.Button> : null
|
</antd.Button>
|
||||||
}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="music-studio-release-editor-content">
|
<div className="music-studio-release-editor-content">
|
||||||
{
|
{submitError && (
|
||||||
submitError && <antd.Alert
|
<antd.Alert
|
||||||
message={submitError.message}
|
message={submitError.message}
|
||||||
type="error"
|
type="error"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{
|
{!Tab && (
|
||||||
!Tab && <antd.Result
|
<antd.Result
|
||||||
status="error"
|
status="error"
|
||||||
title="Error"
|
title="Error"
|
||||||
subTitle="Tab not found"
|
subTitle="Tab not found"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{
|
{Tab &&
|
||||||
Tab && React.createElement(Tab.render, {
|
React.createElement(Tab.render, {
|
||||||
release: globalState,
|
release: globalState,
|
||||||
|
|
||||||
state: globalState,
|
state: globalState,
|
||||||
setState: setGlobalState,
|
setState: setGlobalState,
|
||||||
|
|
||||||
references: {
|
references: {
|
||||||
basic: basicInfoRef
|
basic: basicInfoRef,
|
||||||
}
|
},
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ReleaseEditorStateContext.Provider>
|
</ReleaseEditorStateContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReleaseEditor
|
export default ReleaseEditor
|
@ -11,13 +11,27 @@ import { ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
|
|||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
const stateToString = {
|
||||||
|
uploading: "Uploading",
|
||||||
|
transmuxing: "Processing...",
|
||||||
|
uploading_s3: "Archiving...",
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTitleString = ({ track, progress }) => {
|
||||||
|
if (progress) {
|
||||||
|
return stateToString[progress.state] || progress.state
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.title
|
||||||
|
}
|
||||||
|
|
||||||
const TrackListItem = (props) => {
|
const TrackListItem = (props) => {
|
||||||
const context = React.useContext(ReleaseEditorStateContext)
|
const context = React.useContext(ReleaseEditorStateContext)
|
||||||
|
|
||||||
const [loading, setLoading] = React.useState(false)
|
const [loading, setLoading] = React.useState(false)
|
||||||
const [error, setError] = React.useState(null)
|
const [error, setError] = React.useState(null)
|
||||||
|
|
||||||
const { track } = props
|
const { track, progress } = props
|
||||||
|
|
||||||
async function onClickEditTrack() {
|
async function onClickEditTrack() {
|
||||||
context.renderCustomPage({
|
context.renderCustomPage({
|
||||||
@ -33,8 +47,6 @@ const TrackListItem = (props) => {
|
|||||||
props.onDelete(track.uid)
|
props.onDelete(track.uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("render")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
@ -50,7 +62,7 @@ const TrackListItem = (props) => {
|
|||||||
<div
|
<div
|
||||||
className="music-studio-release-editor-tracks-list-item-progress"
|
className="music-studio-release-editor-tracks-list-item-progress"
|
||||||
style={{
|
style={{
|
||||||
"--upload-progress": `${props.uploading.progress}%`,
|
"--upload-progress": `${props.progress?.percent ?? 0}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -58,7 +70,7 @@ const TrackListItem = (props) => {
|
|||||||
<span>{props.index + 1}</span>
|
<span>{props.index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{props.uploading.working && <Icons.LoadingOutlined />}
|
{progress !== null && <Icons.LoadingOutlined />}
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
src={track.cover}
|
src={track.cover}
|
||||||
@ -69,7 +81,7 @@ const TrackListItem = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>{track.title}</span>
|
<span>{getTitleString({ track, progress })}</span>
|
||||||
|
|
||||||
<div className="music-studio-release-editor-tracks-list-item-actions">
|
<div className="music-studio-release-editor-tracks-list-item-actions">
|
||||||
<antd.Popconfirm
|
<antd.Popconfirm
|
||||||
|
@ -17,12 +17,12 @@ class TracksManager extends React.Component {
|
|||||||
swapyRef = React.createRef()
|
swapyRef = React.createRef()
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
list: Array.isArray(this.props.list) ? this.props.list : [],
|
items: Array.isArray(this.props.items) ? this.props.items : [],
|
||||||
pendingUploads: [],
|
pendingUploads: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = (prevProps, prevState) => {
|
componentDidUpdate = (prevProps, prevState) => {
|
||||||
if (prevState.list !== this.state.list) {
|
if (prevState.items !== this.state.items) {
|
||||||
if (typeof this.props.onChangeState === "function") {
|
if (typeof this.props.onChangeState === "function") {
|
||||||
this.props.onChangeState(this.state)
|
this.props.onChangeState(this.state)
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ class TracksManager extends React.Component {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.state.list.find((item) => item.uid === uid)
|
return this.state.items.find((item) => item.uid === uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
addTrackToList = (track) => {
|
addTrackToList = (track) => {
|
||||||
@ -64,7 +64,7 @@ class TracksManager extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
list: [...this.state.list, track],
|
items: [...this.state.items, track],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,18 +76,17 @@ class TracksManager extends React.Component {
|
|||||||
this.removeTrackUIDFromPendingUploads(uid)
|
this.removeTrackUIDFromPendingUploads(uid)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
list: this.state.list.filter((item) => item.uid !== uid),
|
items: this.state.items.filter((item) => item.uid !== uid),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyTrackByUid = (uid, track) => {
|
modifyTrackByUid = (uid, track) => {
|
||||||
console.log("modifyTrackByUid", uid, track)
|
|
||||||
if (!uid || !track) {
|
if (!uid || !track) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
list: this.state.list.map((item) => {
|
items: this.state.items.map((item) => {
|
||||||
if (item.uid === uid) {
|
if (item.uid === uid) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@ -140,7 +139,7 @@ class TracksManager extends React.Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (uploadProgressIndex === -1) {
|
if (uploadProgressIndex === -1) {
|
||||||
return 0
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.state.pendingUploads[uploadProgressIndex].progress
|
return this.state.pendingUploads[uploadProgressIndex].progress
|
||||||
@ -159,7 +158,7 @@ class TracksManager extends React.Component {
|
|||||||
|
|
||||||
newData[uploadProgressIndex].progress = progress
|
newData[uploadProgressIndex].progress = progress
|
||||||
|
|
||||||
console.log(`Updating progress for [${uid}] to [${progress}]`)
|
console.log(`Updating progress for [${uid}] to >`, progress)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
pendingUploads: newData,
|
pendingUploads: newData,
|
||||||
@ -177,8 +176,7 @@ class TracksManager extends React.Component {
|
|||||||
|
|
||||||
const trackManifest = new TrackManifest({
|
const trackManifest = new TrackManifest({
|
||||||
uid: uid,
|
uid: uid,
|
||||||
file: change.file,
|
file: change.file.originFileObj,
|
||||||
onChange: this.modifyTrackByUid,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.addTrackToList(trackManifest)
|
this.addTrackToList(trackManifest)
|
||||||
@ -189,7 +187,7 @@ class TracksManager extends React.Component {
|
|||||||
// remove pending file
|
// remove pending file
|
||||||
this.removeTrackUIDFromPendingUploads(uid)
|
this.removeTrackUIDFromPendingUploads(uid)
|
||||||
|
|
||||||
let trackManifest = this.state.list.find(
|
let trackManifest = this.state.items.find(
|
||||||
(item) => item.uid === uid,
|
(item) => item.uid === uid,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -206,6 +204,23 @@ class TracksManager extends React.Component {
|
|||||||
trackManifest.source = change.file.response.url
|
trackManifest.source = change.file.response.url
|
||||||
trackManifest = await trackManifest.initialize()
|
trackManifest = await trackManifest.initialize()
|
||||||
|
|
||||||
|
// if has a cover, Upload
|
||||||
|
if (trackManifest._coverBlob) {
|
||||||
|
console.log(
|
||||||
|
`[${trackManifest.uid}] Founded cover, uploading...`,
|
||||||
|
)
|
||||||
|
const coverFile = new File(
|
||||||
|
[trackManifest._coverBlob],
|
||||||
|
"cover.jpg",
|
||||||
|
{ type: trackManifest._coverBlob.type },
|
||||||
|
)
|
||||||
|
|
||||||
|
const coverUpload =
|
||||||
|
await app.cores.remoteStorage.uploadFile(coverFile)
|
||||||
|
|
||||||
|
trackManifest.cover = coverUpload.url
|
||||||
|
}
|
||||||
|
|
||||||
await this.modifyTrackByUid(uid, trackManifest)
|
await this.modifyTrackByUid(uid, trackManifest)
|
||||||
|
|
||||||
break
|
break
|
||||||
@ -231,9 +246,8 @@ class TracksManager extends React.Component {
|
|||||||
const response = await app.cores.remoteStorage
|
const response = await app.cores.remoteStorage
|
||||||
.uploadFile(req.file, {
|
.uploadFile(req.file, {
|
||||||
onProgress: this.handleTrackFileUploadProgress,
|
onProgress: this.handleTrackFileUploadProgress,
|
||||||
service: "b2",
|
|
||||||
headers: {
|
headers: {
|
||||||
transmux: "a-dash",
|
transformations: "a-dash",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -258,17 +272,17 @@ class TracksManager extends React.Component {
|
|||||||
this.setState((prev) => {
|
this.setState((prev) => {
|
||||||
// move all list items by id
|
// move all list items by id
|
||||||
const orderedIds = orderedIdsArray.map((id) =>
|
const orderedIds = orderedIdsArray.map((id) =>
|
||||||
this.state.list.find((item) => item._id === id),
|
this.state.items.find((item) => item._id === id),
|
||||||
)
|
)
|
||||||
console.log("orderedIds", orderedIds)
|
console.log("orderedIds", orderedIds)
|
||||||
return {
|
return {
|
||||||
list: orderedIds,
|
items: orderedIds,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.log(`Tracks List >`, this.state.list)
|
console.log(`Tracks List >`, this.state.items)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="music-studio-release-editor-tracks">
|
<div className="music-studio-release-editor-tracks">
|
||||||
@ -280,7 +294,7 @@ class TracksManager extends React.Component {
|
|||||||
accept="audio/*"
|
accept="audio/*"
|
||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
{this.state.list.length === 0 ? (
|
{this.state.items.length === 0 ? (
|
||||||
<UploadHint />
|
<UploadHint />
|
||||||
) : (
|
) : (
|
||||||
<antd.Button
|
<antd.Button
|
||||||
@ -296,11 +310,11 @@ class TracksManager extends React.Component {
|
|||||||
id="editor-tracks-list"
|
id="editor-tracks-list"
|
||||||
className="music-studio-release-editor-tracks-list"
|
className="music-studio-release-editor-tracks-list"
|
||||||
>
|
>
|
||||||
{this.state.list.length === 0 && (
|
{this.state.items.length === 0 && (
|
||||||
<antd.Result status="info" title="No tracks" />
|
<antd.Result status="info" title="No tracks" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.state.list.map((track, index) => {
|
{this.state.items.map((track, index) => {
|
||||||
const progress = this.getUploadProgress(track.uid)
|
const progress = this.getUploadProgress(track.uid)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -310,12 +324,7 @@ class TracksManager extends React.Component {
|
|||||||
track={track}
|
track={track}
|
||||||
onEdit={this.modifyTrackByUid}
|
onEdit={this.modifyTrackByUid}
|
||||||
onDelete={this.removeTrackByUid}
|
onDelete={this.removeTrackByUid}
|
||||||
uploading={{
|
progress={progress}
|
||||||
progress: progress,
|
|
||||||
working: this.state.pendingUploads.find(
|
|
||||||
(item) => item.uid === track.uid,
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
disabled={progress > 0}
|
disabled={progress > 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -336,7 +345,7 @@ const ReleaseTracks = (props) => {
|
|||||||
|
|
||||||
<TracksManager
|
<TracksManager
|
||||||
_id={state._id}
|
_id={state._id}
|
||||||
list={state.list}
|
items={state.items}
|
||||||
onChangeState={(managerState) => {
|
onChangeState={(managerState) => {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
@ -17,7 +17,7 @@ const TrackEditor = (props) => {
|
|||||||
setTrack((prev) => {
|
setTrack((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[key]: value
|
[key]: value,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -28,15 +28,17 @@ const TrackEditor = (props) => {
|
|||||||
content: EnhancedLyricsEditor,
|
content: EnhancedLyricsEditor,
|
||||||
props: {
|
props: {
|
||||||
track: track,
|
track: track,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOnSave() {
|
async function handleOnSave() {
|
||||||
setTrack((prev) => {
|
setTrack((prev) => {
|
||||||
const listData = [...context.list]
|
const listData = [...context.items]
|
||||||
|
|
||||||
const trackIndex = listData.findIndex((item) => item.uid === prev.uid)
|
const trackIndex = listData.findIndex(
|
||||||
|
(item) => item.uid === prev.uid,
|
||||||
|
)
|
||||||
|
|
||||||
if (trackIndex === -1) {
|
if (trackIndex === -1) {
|
||||||
return prev
|
return prev
|
||||||
@ -46,13 +48,19 @@ const TrackEditor = (props) => {
|
|||||||
|
|
||||||
context.setGlobalState({
|
context.setGlobalState({
|
||||||
...context,
|
...context,
|
||||||
list: listData
|
items: listData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
props.close()
|
||||||
|
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setParentCover() {
|
||||||
|
handleChange("cover", context.cover)
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
context.setCustomPageActions([
|
context.setCustomPageActions([
|
||||||
{
|
{
|
||||||
@ -65,7 +73,8 @@ const TrackEditor = (props) => {
|
|||||||
])
|
])
|
||||||
}, [track])
|
}, [track])
|
||||||
|
|
||||||
return <div className="track-editor">
|
return (
|
||||||
|
<div className="track-editor">
|
||||||
<div className="track-editor-field">
|
<div className="track-editor-field">
|
||||||
<div className="track-editor-field-header">
|
<div className="track-editor-field-header">
|
||||||
<Icons.MdImage />
|
<Icons.MdImage />
|
||||||
@ -76,9 +85,9 @@ const TrackEditor = (props) => {
|
|||||||
value={track.cover}
|
value={track.cover}
|
||||||
onChange={(url) => handleChange("cover", url)}
|
onChange={(url) => handleChange("cover", url)}
|
||||||
extraActions={[
|
extraActions={[
|
||||||
<antd.Button>
|
<antd.Button onClick={setParentCover}>
|
||||||
Use Parent
|
Use Parent
|
||||||
</antd.Button>
|
</antd.Button>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -103,7 +112,7 @@ const TrackEditor = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<antd.Input
|
<antd.Input
|
||||||
value={track.artists?.join(", ")}
|
value={track.artist}
|
||||||
placeholder="Artist"
|
placeholder="Artist"
|
||||||
onChange={(e) => handleChange("artist", e.target.value)}
|
onChange={(e) => handleChange("artist", e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -138,12 +147,6 @@ const TrackEditor = (props) => {
|
|||||||
<div className="track-editor-field-header">
|
<div className="track-editor-field-header">
|
||||||
<Icons.MdLyrics />
|
<Icons.MdLyrics />
|
||||||
<span>Enhanced Lyrics</span>
|
<span>Enhanced Lyrics</span>
|
||||||
|
|
||||||
<antd.Switch
|
|
||||||
checked={track.lyrics_enabled}
|
|
||||||
onChange={(value) => handleChange("lyrics_enabled", value)}
|
|
||||||
disabled={!track.params._id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="track-editor-field-actions">
|
<div className="track-editor-field-actions">
|
||||||
@ -154,14 +157,16 @@ const TrackEditor = (props) => {
|
|||||||
Edit
|
Edit
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
|
|
||||||
{
|
{!track.params._id && (
|
||||||
!track.params._id && <span>
|
<span>
|
||||||
You cannot edit Video and Lyrics without release first
|
You cannot edit Video and Lyrics without release
|
||||||
|
first
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TrackEditor
|
export default TrackEditor
|
@ -6,41 +6,51 @@ import LikeButton from "@components/LikeButton"
|
|||||||
|
|
||||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
const ExtraActions = (props) => {
|
const ExtraActions = (props) => {
|
||||||
const [playerState] = usePlayerStateContext()
|
const [trackInstance, setTrackInstance] = React.useState({})
|
||||||
|
|
||||||
|
const onPlayerStateChange = React.useCallback((state) => {
|
||||||
|
const instance = app.cores.player.track()
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
setTrackInstance(instance)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [playerState] = usePlayerStateContext(onPlayerStateChange)
|
||||||
|
|
||||||
const handleClickLike = async () => {
|
const handleClickLike = async () => {
|
||||||
if (!playerState.track_manifest) {
|
if (!trackInstance) {
|
||||||
console.error("Cannot like a track if nothing is playing")
|
console.error("Cannot like a track if nothing is playing")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const track = app.cores.player.track()
|
await trackInstance.manifest.serviceOperations.toggleItemFavourite(
|
||||||
|
|
||||||
await track.manifest.serviceOperations.toggleItemFavourite(
|
|
||||||
"track",
|
"track",
|
||||||
playerState.track_manifest._id,
|
trackInstance.manifest._id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="extra_actions">
|
<div className="player-actions">
|
||||||
{app.isMobile && (
|
{app.isMobile && (
|
||||||
<Button
|
<Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
icon={<Icons.MdAbc />}
|
icon={<Icons.MdAbc />}
|
||||||
disabled={!playerState.track_manifest?.lyrics_enabled}
|
disabled={!trackInstance?.manifest?.lyrics_enabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!app.isMobile && (
|
{!app.isMobile && (
|
||||||
<LikeButton
|
<LikeButton
|
||||||
liked={
|
liked={
|
||||||
playerState.track_manifest?.serviceOperations
|
trackInstance?.manifest?.serviceOperations
|
||||||
.fetchLikeStatus
|
?.fetchLikeStatus
|
||||||
}
|
}
|
||||||
onClick={handleClickLike}
|
onClick={handleClickLike}
|
||||||
disabled={!playerState.track_manifest?._id}
|
disabled={!trackInstance?.manifest?._id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
19
packages/app/src/components/Player/Actions/index.less
Normal file
19
packages/app/src/components/Player/Actions/index.less
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.player-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
width: 70%;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
padding: 2px 25px;
|
||||||
|
|
||||||
|
background-color: rgba(var(--layoutBackgroundColor), 0.7);
|
||||||
|
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
@ -47,7 +47,17 @@ const EventsHandlers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Controls = (props) => {
|
const Controls = (props) => {
|
||||||
const [playerState] = usePlayerStateContext()
|
const [trackInstance, setTrackInstance] = React.useState({})
|
||||||
|
|
||||||
|
const onPlayerStateChange = React.useCallback((state) => {
|
||||||
|
const instance = app.cores.player.track()
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
setTrackInstance(instance)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [playerState] = usePlayerStateContext(onPlayerStateChange)
|
||||||
|
|
||||||
const handleAction = (event, ...args) => {
|
const handleAction = (event, ...args) => {
|
||||||
if (typeof EventsHandlers[event] !== "function") {
|
if (typeof EventsHandlers[event] !== "function") {
|
||||||
@ -122,10 +132,11 @@ const Controls = (props) => {
|
|||||||
{app.isMobile && (
|
{app.isMobile && (
|
||||||
<LikeButton
|
<LikeButton
|
||||||
liked={
|
liked={
|
||||||
playerState.track_manifest?.serviceOperations
|
trackInstance?.manifest?.serviceOperations
|
||||||
.fetchLikeStatus
|
?.fetchLikeStatus
|
||||||
}
|
}
|
||||||
onClick={() => handleAction("like")}
|
onClick={() => handleAction("like")}
|
||||||
|
disabled={!trackInstance?.manifest?._id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,11 +8,10 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
|||||||
import LiveInfo from "@components/Player/LiveInfo"
|
import LiveInfo from "@components/Player/LiveInfo"
|
||||||
import SeekBar from "@components/Player/SeekBar"
|
import SeekBar from "@components/Player/SeekBar"
|
||||||
import Controls from "@components/Player/Controls"
|
import Controls from "@components/Player/Controls"
|
||||||
|
import Actions from "@components/Player/Actions"
|
||||||
|
|
||||||
import RGBStringToValues from "@utils/rgbToValues"
|
import RGBStringToValues from "@utils/rgbToValues"
|
||||||
|
|
||||||
import ExtraActions from "../ExtraActions"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
function isOverflown(parent, element) {
|
function isOverflown(parent, element) {
|
||||||
@ -93,7 +92,7 @@ const Player = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, artistStr, service, cover_analysis, cover } =
|
const { title, artist, service, cover_analysis, cover } =
|
||||||
playerState.track_manifest ?? {}
|
playerState.track_manifest ?? {}
|
||||||
|
|
||||||
const playing = playerState.playback_status === "playing"
|
const playing = playerState.playback_status === "playing"
|
||||||
@ -201,7 +200,7 @@ const Player = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="toolbar_player_info_subtitle">
|
<p className="toolbar_player_info_subtitle">
|
||||||
{artistStr ?? ""}
|
{artist ?? ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -218,7 +217,7 @@ const Player = (props) => {
|
|||||||
streamMode={playerState.live}
|
streamMode={playerState.live}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExtraActions streamMode={playerState.live} />
|
<Actions streamMode={playerState.live} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Indicators
|
<Indicators
|
||||||
|
@ -21,8 +21,12 @@
|
|||||||
|
|
||||||
.toolbar_player_top_actions {
|
.toolbar_player_top_actions {
|
||||||
height: @toolbar_player_top_actions_height;
|
height: @toolbar_player_top_actions_height;
|
||||||
padding: @toolbar_player_top_actions_padding_vertical @toolbar_player_top_actions_padding_horizontal;
|
padding: @toolbar_player_top_actions_padding_vertical
|
||||||
padding-bottom: calc(calc(@toolbar_player_borderRadius / 2) + @toolbar_player_top_actions_padding_vertical);
|
@toolbar_player_top_actions_padding_horizontal;
|
||||||
|
padding-bottom: calc(
|
||||||
|
calc(@toolbar_player_borderRadius / 2) +
|
||||||
|
@toolbar_player_top_actions_padding_vertical
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,43 +235,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.extra_actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
width: 70%;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
padding: 7px 25px;
|
|
||||||
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
background-color: rgba(var(--layoutBackgroundColor), 0.7);
|
|
||||||
|
|
||||||
-webkit-backdrop-filter: blur(5px);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
button,
|
|
||||||
.likeButton {
|
|
||||||
width: 32px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
.ant-btn-icon {
|
|
||||||
svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-btn {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar_player_indicators_wrapper {
|
.toolbar_player_indicators_wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
@ -276,7 +243,6 @@
|
|||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
|
||||||
.toolbar_player_indicators {
|
.toolbar_player_indicators {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -299,7 +265,7 @@
|
|||||||
svg {
|
svg {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
|
||||||
color: white
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -110,6 +110,7 @@ export class PostsListsComponent extends React.Component {
|
|||||||
|
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
list: this.props.list ?? [],
|
list: this.props.list ?? [],
|
||||||
|
pageCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
parentRef = this.props.innerRef
|
parentRef = this.props.innerRef
|
||||||
@ -148,12 +149,17 @@ export class PostsListsComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLoad = async (fn, params = {}) => {
|
handleLoad = async (fn, params = {}) => {
|
||||||
|
if (this.state.loading === true) {
|
||||||
|
console.warn(`Please wait to load the post before load more`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
trim: this.state.list.length,
|
page: this.state.pageCount,
|
||||||
limit: app.cores.settings.get("feed_max_fetch"),
|
limit: app.cores.settings.get("feed_max_fetch"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,10 +170,6 @@ export class PostsListsComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.replace) {
|
|
||||||
payload.trim = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await fn(payload).catch((err) => {
|
const result = await fn(payload).catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
||||||
@ -186,10 +188,12 @@ export class PostsListsComponent extends React.Component {
|
|||||||
if (params.replace) {
|
if (params.replace) {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: result,
|
list: result,
|
||||||
|
pageCount: 0,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: [...this.state.list, ...result],
|
list: [...this.state.list, ...result],
|
||||||
|
pageCount: this.state.pageCount + 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import "./index.less"
|
|||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [uploading, setUploading] = React.useState(false)
|
const [uploading, setUploading] = React.useState(false)
|
||||||
const [progess, setProgess] = React.useState(null)
|
const [progress, setProgress] = React.useState(null)
|
||||||
|
|
||||||
const handleOnStart = (file_uid, file) => {
|
const handleOnStart = (file_uid, file) => {
|
||||||
if (typeof props.onStart === "function") {
|
if (typeof props.onStart === "function") {
|
||||||
@ -36,18 +36,18 @@ export default (props) => {
|
|||||||
|
|
||||||
const handleUpload = async (req) => {
|
const handleUpload = async (req) => {
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
setProgess(1)
|
setProgress(1)
|
||||||
|
|
||||||
handleOnStart(req.file.uid, req.file)
|
handleOnStart(req.file.uid, req.file)
|
||||||
|
|
||||||
await app.cores.remoteStorage.uploadFile(req.file, {
|
await app.cores.remoteStorage.uploadFile(req.file, {
|
||||||
headers: props.headers,
|
headers: props.headers,
|
||||||
onProgress: (file, progress) => {
|
onProgress: (file, progress) => {
|
||||||
setProgess(progress)
|
setProgress(progress)
|
||||||
handleOnProgress(file.uid, progress)
|
handleOnProgress(file.uid, progress)
|
||||||
},
|
},
|
||||||
onError: (file, error) => {
|
onError: (file, error) => {
|
||||||
setProgess(null)
|
setProgress(null)
|
||||||
handleOnError(file.uid, error)
|
handleOnError(file.uid, error)
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
},
|
},
|
||||||
@ -64,55 +64,45 @@ export default (props) => {
|
|||||||
handleOnSuccess(req.file.uid, response)
|
handleOnSuccess(req.file.uid, response)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setProgess(null)
|
setProgress(null)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Upload
|
return (
|
||||||
|
<Upload
|
||||||
customRequest={handleUpload}
|
customRequest={handleUpload}
|
||||||
multiple={
|
multiple={props.multiple ?? false}
|
||||||
props.multiple ?? false
|
accept={props.accept ?? ["image/*", "video/*", "audio/*"]}
|
||||||
}
|
|
||||||
accept={
|
|
||||||
props.accept ?? [
|
|
||||||
"image/*",
|
|
||||||
"video/*",
|
|
||||||
"audio/*",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
progress={false}
|
progress={false}
|
||||||
fileList={[]}
|
fileList={[]}
|
||||||
className={classnames(
|
className={classnames("uploadButton", {
|
||||||
"uploadButton",
|
["uploading"]: !!progress || uploading,
|
||||||
{
|
})}
|
||||||
["uploading"]: !!progess || uploading
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
>
|
>
|
||||||
<div className="uploadButton-content">
|
<div className="uploadButton-content">
|
||||||
{
|
{!progress &&
|
||||||
!progess && (props.icon ?? <Icons.FiUpload
|
(props.icon ?? (
|
||||||
|
<Icons.FiUpload
|
||||||
style={{
|
style={{
|
||||||
margin: 0
|
margin: 0,
|
||||||
}}
|
}}
|
||||||
/>)
|
/>
|
||||||
}
|
))}
|
||||||
|
|
||||||
{
|
{progress && (
|
||||||
progess && <Progress
|
<Progress
|
||||||
type="circle"
|
type="circle"
|
||||||
percent={progess}
|
percent={progress?.percent ?? 0}
|
||||||
strokeWidth={20}
|
strokeWidth={20}
|
||||||
format={() => null}
|
format={() => null}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{
|
{props.children ?? "Upload"}
|
||||||
props.children ?? "Upload"
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
)
|
||||||
}
|
}
|
@ -6,12 +6,14 @@ export const DefaultReleaseEditorState = {
|
|||||||
type: "single",
|
type: "single",
|
||||||
public: false,
|
public: false,
|
||||||
|
|
||||||
list: [],
|
items: [],
|
||||||
pendingUploads: [],
|
pendingUploads: [],
|
||||||
|
|
||||||
setCustomPage: () => {},
|
setCustomPage: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReleaseEditorStateContext = React.createContext(DefaultReleaseEditorState)
|
export const ReleaseEditorStateContext = React.createContext(
|
||||||
|
DefaultReleaseEditorState,
|
||||||
|
)
|
||||||
|
|
||||||
export default ReleaseEditorStateContext
|
export default ReleaseEditorStateContext
|
123
packages/app/src/cores/player/classes/AudioBase.js
Normal file
123
packages/app/src/cores/player/classes/AudioBase.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { MediaPlayer } from "dashjs"
|
||||||
|
import PlayerProcessors from "./PlayerProcessors"
|
||||||
|
import AudioPlayerStorage from "../player.storage"
|
||||||
|
|
||||||
|
export default class AudioBase {
|
||||||
|
constructor(player) {
|
||||||
|
this.player = player
|
||||||
|
}
|
||||||
|
|
||||||
|
audio = new Audio()
|
||||||
|
context = null
|
||||||
|
demuxer = null
|
||||||
|
elementSource = null
|
||||||
|
|
||||||
|
processorsManager = new PlayerProcessors(this)
|
||||||
|
processors = {}
|
||||||
|
|
||||||
|
waitUpdateTimeout = null
|
||||||
|
|
||||||
|
initialize = async () => {
|
||||||
|
// create a audio context
|
||||||
|
this.context = new AudioContext({
|
||||||
|
sampleRate:
|
||||||
|
AudioPlayerStorage.get("sample_rate") ??
|
||||||
|
this.player.constructor.defaultSampleRate,
|
||||||
|
latencyHint: "playback",
|
||||||
|
})
|
||||||
|
|
||||||
|
// configure some settings for audio
|
||||||
|
this.audio.crossOrigin = "anonymous"
|
||||||
|
this.audio.preload = "metadata"
|
||||||
|
|
||||||
|
// listen all events
|
||||||
|
for (const [key, value] of Object.entries(this.audioEvents)) {
|
||||||
|
this.audio.addEventListener(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup demuxer for mpd
|
||||||
|
this.createDemuxer()
|
||||||
|
|
||||||
|
// create element source
|
||||||
|
this.elementSource = this.context.createMediaElementSource(this.audio)
|
||||||
|
|
||||||
|
// initialize audio processors
|
||||||
|
await this.processorsManager.initialize()
|
||||||
|
await this.processorsManager.attachAllNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
createDemuxer() {
|
||||||
|
this.demuxer = MediaPlayer().create()
|
||||||
|
|
||||||
|
this.demuxer.updateSettings({
|
||||||
|
streaming: {
|
||||||
|
buffer: {
|
||||||
|
resetSourceBuffersForTrackSwitch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.demuxer.initialize(this.audio, null, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
this.audio.pause()
|
||||||
|
this.audio.src = null
|
||||||
|
this.audio.currentTime = 0
|
||||||
|
|
||||||
|
this.demuxer.destroy()
|
||||||
|
this.createDemuxer()
|
||||||
|
}
|
||||||
|
|
||||||
|
audioEvents = {
|
||||||
|
ended: () => {
|
||||||
|
this.player.next()
|
||||||
|
},
|
||||||
|
loadeddata: () => {
|
||||||
|
this.player.state.loading = false
|
||||||
|
},
|
||||||
|
loadedmetadata: () => {
|
||||||
|
if (this.audio.duration === Infinity) {
|
||||||
|
this.player.state.live = true
|
||||||
|
} else {
|
||||||
|
this.player.state.live = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
play: () => {
|
||||||
|
this.player.state.playback_status = "playing"
|
||||||
|
},
|
||||||
|
playing: () => {
|
||||||
|
this.player.state.loading = false
|
||||||
|
|
||||||
|
this.player.state.playback_status = "playing"
|
||||||
|
|
||||||
|
if (typeof this.waitUpdateTimeout !== "undefined") {
|
||||||
|
clearTimeout(this.waitUpdateTimeout)
|
||||||
|
this.waitUpdateTimeout = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
this.player.state.playback_status = "paused"
|
||||||
|
},
|
||||||
|
durationchange: () => {
|
||||||
|
this.player.eventBus.emit(
|
||||||
|
`player.durationchange`,
|
||||||
|
this.audio.duration,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
waiting: () => {
|
||||||
|
if (this.waitUpdateTimeout) {
|
||||||
|
clearTimeout(this.waitUpdateTimeout)
|
||||||
|
this.waitUpdateTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// if takes more than 150ms to load, update loading state
|
||||||
|
this.waitUpdateTimeout = setTimeout(() => {
|
||||||
|
this.player.state.loading = true
|
||||||
|
}, 150)
|
||||||
|
},
|
||||||
|
seeked: () => {
|
||||||
|
this.player.eventBus.emit(`player.seeked`, this.audio.currentTime)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
56
packages/app/src/cores/player/classes/MediaSession.js
Normal file
56
packages/app/src/cores/player/classes/MediaSession.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
export default class MediaSession {
|
||||||
|
constructor(player) {
|
||||||
|
this.player = player
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
for (const [action, handler] of this.handlers) {
|
||||||
|
navigator.mediaSession.setActionHandler(action, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = [
|
||||||
|
[
|
||||||
|
"play",
|
||||||
|
() => {
|
||||||
|
console.log("media session play event", "play")
|
||||||
|
this.player.resumePlayback()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"pause",
|
||||||
|
() => {
|
||||||
|
console.log("media session pause event", "pause")
|
||||||
|
this.player.pausePlayback()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"seekto",
|
||||||
|
(seek) => {
|
||||||
|
console.log("media session seek event", seek)
|
||||||
|
this.player.seek(seek.seekTime)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
update = (manifest) => {
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: manifest.title,
|
||||||
|
artist: manifest.artist,
|
||||||
|
album: manifest.album,
|
||||||
|
artwork: [
|
||||||
|
{
|
||||||
|
src: manifest.cover,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
flush = () => {
|
||||||
|
navigator.mediaSession.metadata = null
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIsPlaying = (isPlaying) => {
|
||||||
|
navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused"
|
||||||
|
}
|
||||||
|
}
|
@ -1,83 +1,96 @@
|
|||||||
import defaultAudioProccessors from "../processors"
|
import defaultAudioProccessors from "../processors"
|
||||||
|
|
||||||
export default class PlayerProcessors {
|
export default class PlayerProcessors {
|
||||||
constructor(player) {
|
constructor(base) {
|
||||||
this.player = player
|
this.base = base
|
||||||
}
|
}
|
||||||
|
|
||||||
processors = []
|
nodes = []
|
||||||
|
attached = []
|
||||||
|
|
||||||
public = {}
|
public = {}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// if already exists audio processors, destroy all before create new
|
// if already exists audio processors, destroy all before create new
|
||||||
if (this.processors.length > 0) {
|
if (this.nodes.length > 0) {
|
||||||
this.player.console.log("Destroying audio processors")
|
this.base.player.console.log("Destroying audio processors")
|
||||||
|
|
||||||
this.processors.forEach((processor) => {
|
this.nodes.forEach((node) => {
|
||||||
this.player.console.log(`Destroying audio processor ${processor.constructor.name}`, processor)
|
this.base.player.console.log(
|
||||||
processor._destroy()
|
`Destroying audio processor node ${node.constructor.name}`,
|
||||||
|
node,
|
||||||
|
)
|
||||||
|
node._destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.processors = []
|
this.nodes = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// instanciate default audio processors
|
// instanciate default audio processors
|
||||||
for await (const defaultProccessor of defaultAudioProccessors) {
|
for await (const defaultProccessor of defaultAudioProccessors) {
|
||||||
this.processors.push(new defaultProccessor(this.player))
|
this.nodes.push(new defaultProccessor(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize audio processors
|
// initialize audio processors
|
||||||
for await (const processor of this.processors) {
|
for await (const node of this.nodes) {
|
||||||
if (typeof processor._init === "function") {
|
if (typeof node._init === "function") {
|
||||||
try {
|
try {
|
||||||
await processor._init(this.player.audioContext)
|
await node._init()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.player.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
|
this.base.player.console.error(
|
||||||
|
`Failed to initialize audio processor node ${node.constructor.name} >`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if processor has exposed public methods
|
// check if processor has exposed public methods
|
||||||
if (processor.exposeToPublic) {
|
if (node.exposeToPublic) {
|
||||||
Object.entries(processor.exposeToPublic).forEach(([key, value]) => {
|
Object.entries(node.exposeToPublic).forEach(([key, value]) => {
|
||||||
const refName = processor.constructor.refName
|
const refName = node.constructor.refName
|
||||||
|
|
||||||
if (typeof this.player.public[refName] === "undefined") {
|
if (typeof this.base.processors[refName] === "undefined") {
|
||||||
// by default create a empty object
|
// by default create a empty object
|
||||||
this.player.public[refName] = {}
|
this.base.processors[refName] = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.player.public[refName][key] = value
|
this.base.processors[refName][key] = value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async attachProcessorsToInstance(instance) {
|
attachAllNodes = async () => {
|
||||||
for await (const [index, processor] of this.processors.entries()) {
|
for await (const [index, node] of this.nodes.entries()) {
|
||||||
if (processor.constructor.node_bypass === true) {
|
if (node.constructor.node_bypass === true) {
|
||||||
instance.contextElement.connect(processor.processor)
|
this.base.context.elementSource.connect(node.processor)
|
||||||
|
|
||||||
processor.processor.connect(this.player.audioContext.destination)
|
node.processor.connect(this.base.context.destination)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof processor._attach !== "function") {
|
if (typeof node._attach !== "function") {
|
||||||
this.player.console.error(`Processor ${processor.constructor.refName} not support attach`)
|
this.base.console.error(
|
||||||
|
`Processor ${node.constructor.refName} not support attach`,
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
instance = await processor._attach(instance, index)
|
await node._attach(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor
|
const lastProcessor = this.attached[this.attached.length - 1].processor
|
||||||
|
|
||||||
// now attach to destination
|
// now attach to destination
|
||||||
lastProcessor.connect(this.player.audioContext.destination)
|
lastProcessor.connect(this.base.context.destination)
|
||||||
|
}
|
||||||
|
|
||||||
return instance
|
detachAllNodes = async () => {
|
||||||
|
for (const [index, node] of this.attached.entries()) {
|
||||||
|
await node._detach()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,206 +1,131 @@
|
|||||||
import TrackManifest from "./TrackManifest"
|
import TrackManifest from "./TrackManifest"
|
||||||
import { MediaPlayer } from "dashjs"
|
|
||||||
|
|
||||||
export default class TrackInstance {
|
export default class TrackInstance {
|
||||||
constructor(player, manifest) {
|
constructor(manifest, player) {
|
||||||
|
if (typeof manifest === "undefined") {
|
||||||
|
throw new Error("Manifest is required")
|
||||||
|
}
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
throw new Error("Player core is required")
|
throw new Error("Player core is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof manifest === "undefined") {
|
if (!(manifest instanceof TrackManifest)) {
|
||||||
throw new Error("Manifest is required")
|
manifest = new TrackManifest(manifest, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest.source) {
|
||||||
|
throw new Error("Manifest must have a source")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.player = player
|
this.player = player
|
||||||
this.manifest = manifest
|
this.manifest = manifest
|
||||||
|
|
||||||
this.id = this.manifest.id ?? this.manifest._id
|
this.id = this.manifest.id ?? this.manifest._id
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialized = false
|
play = async (params = {}) => {
|
||||||
|
const startTime = performance.now()
|
||||||
|
|
||||||
audio = null
|
if (!this.manifest.source.endsWith(".mpd")) {
|
||||||
|
this.player.base.demuxer.destroy()
|
||||||
contextElement = null
|
this.player.base.audio.src = this.manifest.source
|
||||||
|
|
||||||
abortController = new AbortController()
|
|
||||||
|
|
||||||
attachedProcessors = []
|
|
||||||
|
|
||||||
waitUpdateTimeout = null
|
|
||||||
|
|
||||||
mediaEvents = {
|
|
||||||
ended: () => {
|
|
||||||
this.player.next()
|
|
||||||
},
|
|
||||||
loadeddata: () => {
|
|
||||||
this.player.state.loading = false
|
|
||||||
},
|
|
||||||
loadedmetadata: () => {
|
|
||||||
if (this.audio.duration === Infinity) {
|
|
||||||
this.player.state.live = true
|
|
||||||
} else {
|
} else {
|
||||||
this.player.state.live = false
|
if (!this.player.base.demuxer) {
|
||||||
}
|
this.player.base.createDemuxer()
|
||||||
},
|
|
||||||
play: () => {
|
|
||||||
this.player.state.playback_status = "playing"
|
|
||||||
},
|
|
||||||
playing: () => {
|
|
||||||
this.player.state.loading = false
|
|
||||||
|
|
||||||
this.player.state.playback_status = "playing"
|
|
||||||
|
|
||||||
if (typeof this.waitUpdateTimeout !== "undefined") {
|
|
||||||
clearTimeout(this.waitUpdateTimeout)
|
|
||||||
this.waitUpdateTimeout = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pause: () => {
|
|
||||||
this.player.state.playback_status = "paused"
|
|
||||||
},
|
|
||||||
durationchange: () => {
|
|
||||||
this.player.eventBus.emit(
|
|
||||||
`player.durationchange`,
|
|
||||||
this.audio.duration,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
waiting: () => {
|
|
||||||
if (this.waitUpdateTimeout) {
|
|
||||||
clearTimeout(this.waitUpdateTimeout)
|
|
||||||
this.waitUpdateTimeout = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if takes more than 150ms to load, update loading state
|
await this.player.base.demuxer.attachSource(
|
||||||
this.waitUpdateTimeout = setTimeout(() => {
|
`${this.manifest.source}?t=${Date.now()}`,
|
||||||
this.player.state.loading = true
|
|
||||||
}, 150)
|
|
||||||
},
|
|
||||||
seeked: () => {
|
|
||||||
this.player.eventBus.emit(`player.seeked`, this.audio.currentTime)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize = async () => {
|
|
||||||
this.manifest = await this.resolveManifest()
|
|
||||||
|
|
||||||
this.audio = new Audio()
|
|
||||||
|
|
||||||
this.audio.signal = this.abortController.signal
|
|
||||||
this.audio.crossOrigin = "anonymous"
|
|
||||||
this.audio.preload = "metadata"
|
|
||||||
|
|
||||||
// support for dash audio streaming
|
|
||||||
if (this.manifest.source.endsWith(".mpd")) {
|
|
||||||
this.muxerPlayer = MediaPlayer().create()
|
|
||||||
this.muxerPlayer.updateSettings({
|
|
||||||
streaming: {
|
|
||||||
buffer: {
|
|
||||||
resetSourceBuffersForTrackSwitch: true,
|
|
||||||
useChangeTypeForTrackSwitch: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.muxerPlayer.initialize(this.audio, null, false)
|
|
||||||
|
|
||||||
this.muxerPlayer.attachSource(this.manifest.source)
|
|
||||||
} else {
|
|
||||||
this.audio.src = this.manifest.source
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(this.mediaEvents)) {
|
|
||||||
this.audio.addEventListener(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.contextElement = this.player.audioContext.createMediaElementSource(
|
|
||||||
this.audio,
|
|
||||||
)
|
|
||||||
|
|
||||||
this._initialized = true
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
stop = () => {
|
|
||||||
if (this.audio) {
|
|
||||||
this.audio.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.muxerPlayer) {
|
|
||||||
this.muxerPlayer.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastProcessor =
|
|
||||||
this.attachedProcessors[this.attachedProcessors.length - 1]
|
|
||||||
|
|
||||||
if (lastProcessor) {
|
|
||||||
this.attachedProcessors[
|
|
||||||
this.attachedProcessors.length - 1
|
|
||||||
]._destroy(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attachedProcessors = []
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveManifest = async () => {
|
|
||||||
if (typeof this.manifest === "string") {
|
|
||||||
this.manifest = {
|
|
||||||
src: this.manifest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.manifest = new TrackManifest(this.manifest, {
|
|
||||||
serviceProviders: this.player.serviceProviders,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.manifest.service) {
|
|
||||||
if (!this.player.serviceProviders.has(this.manifest.service)) {
|
|
||||||
throw new Error(
|
|
||||||
`Service ${this.manifest.service} is not supported`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to resolve source file
|
this.player.base.audio.currentTime = params.time ?? 0
|
||||||
if (!this.manifest.source) {
|
|
||||||
console.log("Resolving manifest cause no source defined")
|
|
||||||
|
|
||||||
this.manifest = await this.player.serviceProviders.resolve(
|
if (this.player.base.audio.paused) {
|
||||||
this.manifest.service,
|
await this.player.base.audio.play()
|
||||||
this.manifest,
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log("Manifest resolved", this.manifest)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.manifest.source) {
|
// reset audio volume and gain
|
||||||
throw new Error("Manifest `source` is required")
|
this.player.base.audio.volume = 1
|
||||||
|
this.player.base.processors.gain.set(this.player.state.volume)
|
||||||
|
|
||||||
|
const endTime = performance.now()
|
||||||
|
|
||||||
|
this._loadMs = endTime - startTime
|
||||||
|
|
||||||
|
console.log(`[INSTANCE] Playing >`, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set empty metadata if not provided
|
pause = async () => {
|
||||||
if (!this.manifest.metadata) {
|
console.log("[INSTANCE] Pausing >", this)
|
||||||
this.manifest.metadata = {}
|
|
||||||
|
this.player.base.audio.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto name if a title is not provided
|
resume = async () => {
|
||||||
if (!this.manifest.metadata.title) {
|
console.log("[INSTANCE] Resuming >", this)
|
||||||
this.manifest.metadata.title = this.manifest.source.split("/").pop()
|
|
||||||
|
this.player.base.audio.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
// process overrides
|
// resolveManifest = async () => {
|
||||||
const override = await this.manifest.serviceOperations.fetchOverride()
|
// if (typeof this.manifest === "string") {
|
||||||
|
// this.manifest = {
|
||||||
|
// src: this.manifest,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (override) {
|
// this.manifest = new TrackManifest(this.manifest, {
|
||||||
console.log(
|
// serviceProviders: this.player.serviceProviders,
|
||||||
`Override found for track ${this.manifest._id}`,
|
// })
|
||||||
override,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.manifest.overrides = override
|
// if (this.manifest.service) {
|
||||||
}
|
// if (!this.player.serviceProviders.has(this.manifest.service)) {
|
||||||
|
// throw new Error(
|
||||||
|
// `Service ${this.manifest.service} is not supported`,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
return this.manifest
|
// // try to resolve source file
|
||||||
}
|
// if (!this.manifest.source) {
|
||||||
|
// console.log("Resolving manifest cause no source defined")
|
||||||
|
|
||||||
|
// this.manifest = await this.player.serviceProviders.resolve(
|
||||||
|
// this.manifest.service,
|
||||||
|
// this.manifest,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// console.log("Manifest resolved", this.manifest)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!this.manifest.source) {
|
||||||
|
// throw new Error("Manifest `source` is required")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // set empty metadata if not provided
|
||||||
|
// if (!this.manifest.metadata) {
|
||||||
|
// this.manifest.metadata = {}
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // auto name if a title is not provided
|
||||||
|
// if (!this.manifest.metadata.title) {
|
||||||
|
// this.manifest.metadata.title = this.manifest.source.split("/").pop()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // process overrides
|
||||||
|
// const override = await this.manifest.serviceOperations.fetchOverride()
|
||||||
|
|
||||||
|
// if (override) {
|
||||||
|
// console.log(
|
||||||
|
// `Override found for track ${this.manifest._id}`,
|
||||||
|
// override,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// this.manifest.overrides = override
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return this.manifest
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js"
|
import { parseBlob } from "music-metadata"
|
||||||
import { FastAverageColor } from "fast-average-color"
|
import { FastAverageColor } from "fast-average-color"
|
||||||
|
|
||||||
export default class TrackManifest {
|
export default class TrackManifest {
|
||||||
@ -33,13 +33,6 @@ export default class TrackManifest {
|
|||||||
this.artist = params.artist
|
this.artist = params.artist
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
typeof params.artists !== "undefined" ||
|
|
||||||
Array.isArray(params.artists)
|
|
||||||
) {
|
|
||||||
this.artistStr = params.artists.join(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof params.source !== "undefined") {
|
if (typeof params.source !== "undefined") {
|
||||||
this.source = params.source
|
this.source = params.source
|
||||||
}
|
}
|
||||||
@ -48,8 +41,8 @@ export default class TrackManifest {
|
|||||||
this.metadata = params.metadata
|
this.metadata = params.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof params.lyrics_enabled !== "undefined") {
|
if (typeof params.liked !== "undefined") {
|
||||||
this.lyrics_enabled = params.lyrics_enabled
|
this.liked = params.liked
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
@ -58,85 +51,43 @@ export default class TrackManifest {
|
|||||||
_id = null // used for api requests
|
_id = null // used for api requests
|
||||||
uid = null // used for internal
|
uid = null // used for internal
|
||||||
|
|
||||||
cover =
|
|
||||||
"https://storage.ragestudio.net/comty-static-assets/default_song.png"
|
|
||||||
title = "Untitled"
|
title = "Untitled"
|
||||||
album = "Unknown"
|
album = "Unknown"
|
||||||
artist = "Unknown"
|
artist = "Unknown"
|
||||||
|
cover = null // set default cover url
|
||||||
source = null
|
source = null
|
||||||
metadata = null
|
metadata = {}
|
||||||
|
|
||||||
// set default service to default
|
// set default service to default
|
||||||
service = "default"
|
service = "default"
|
||||||
|
|
||||||
// Extended from db
|
|
||||||
lyrics_enabled = false
|
|
||||||
liked = null
|
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
if (this.params.file) {
|
if (!this.params.file) {
|
||||||
this.metadata = await this.analyzeMetadata(
|
|
||||||
this.params.file.originFileObj,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.metadata.format = this.metadata.type.toUpperCase()
|
|
||||||
|
|
||||||
if (this.metadata.tags) {
|
|
||||||
if (this.metadata.tags.title) {
|
|
||||||
this.title = this.metadata.tags.title
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.metadata.tags.artist) {
|
|
||||||
this.artist = this.metadata.tags.artist
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.metadata.tags.album) {
|
|
||||||
this.album = this.metadata.tags.album
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.metadata.tags.picture) {
|
|
||||||
this.cover = app.cores.remoteStorage.binaryArrayToFile(
|
|
||||||
this.metadata.tags.picture,
|
|
||||||
"cover",
|
|
||||||
)
|
|
||||||
|
|
||||||
const coverUpload =
|
|
||||||
await app.cores.remoteStorage.uploadFile(this.cover)
|
|
||||||
|
|
||||||
this.cover = coverUpload.url
|
|
||||||
|
|
||||||
delete this.metadata.tags.picture
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleChanges({
|
|
||||||
cover: this.cover,
|
|
||||||
title: this.title,
|
|
||||||
artist: this.artist,
|
|
||||||
album: this.album,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChanges = (changes) => {
|
const analyzedMetadata = await parseBlob(this.params.file, {
|
||||||
if (typeof this.params.onChange === "function") {
|
skipPostHeaders: true,
|
||||||
this.params.onChange(this.uid, changes)
|
}).catch(() => ({}))
|
||||||
}
|
|
||||||
|
if (analyzedMetadata.format) {
|
||||||
|
this.metadata.format = analyzedMetadata.format.codec
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzeMetadata = async (file) => {
|
if (analyzedMetadata.common) {
|
||||||
return new Promise((resolve, reject) => {
|
this.title = analyzedMetadata.common.title ?? this.title
|
||||||
jsmediatags.read(file, {
|
this.artist = analyzedMetadata.common.artist ?? this.artist
|
||||||
onSuccess: (data) => {
|
this.album = analyzedMetadata.common.album ?? this.album
|
||||||
return resolve(data)
|
}
|
||||||
},
|
|
||||||
onError: (error) => {
|
if (analyzedMetadata.common.picture) {
|
||||||
return reject(error)
|
const cover = analyzedMetadata.common.picture[0]
|
||||||
},
|
|
||||||
})
|
this._coverBlob = new Blob([cover.data], { type: cover.format })
|
||||||
})
|
this.cover = URL.createObjectURL(this._coverBlob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzeCoverColor = async () => {
|
analyzeCoverColor = async () => {
|
||||||
@ -169,8 +120,6 @@ export default class TrackManifest {
|
|||||||
this,
|
this,
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(this.overrides)
|
|
||||||
|
|
||||||
if (this.overrides) {
|
if (this.overrides) {
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
@ -210,6 +159,7 @@ export default class TrackManifest {
|
|||||||
return {
|
return {
|
||||||
_id: this._id,
|
_id: this._id,
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
|
cover: this.cover,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
album: this.album,
|
album: this.album,
|
||||||
artist: this.artist,
|
artist: this.artist,
|
||||||
|
@ -3,11 +3,11 @@ import { Core } from "@ragestudio/vessel"
|
|||||||
import ActivityEvent from "@classes/ActivityEvent"
|
import ActivityEvent from "@classes/ActivityEvent"
|
||||||
import QueueManager from "@classes/QueueManager"
|
import QueueManager from "@classes/QueueManager"
|
||||||
import TrackInstance from "./classes/TrackInstance"
|
import TrackInstance from "./classes/TrackInstance"
|
||||||
//import MediaSession from "./classes/MediaSession"
|
import MediaSession from "./classes/MediaSession"
|
||||||
import ServiceProviders from "./classes/Services"
|
import ServiceProviders from "./classes/Services"
|
||||||
import PlayerState from "./classes/PlayerState"
|
import PlayerState from "./classes/PlayerState"
|
||||||
import PlayerUI from "./classes/PlayerUI"
|
import PlayerUI from "./classes/PlayerUI"
|
||||||
import PlayerProcessors from "./classes/PlayerProcessors"
|
import AudioBase from "./classes/AudioBase"
|
||||||
|
|
||||||
import setSampleRate from "./helpers/setSampleRate"
|
import setSampleRate from "./helpers/setSampleRate"
|
||||||
|
|
||||||
@ -22,27 +22,18 @@ export default class Player extends Core {
|
|||||||
|
|
||||||
// player config
|
// player config
|
||||||
static defaultSampleRate = 48000
|
static defaultSampleRate = 48000
|
||||||
static gradualFadeMs = 150
|
|
||||||
static maxManifestPrecompute = 3
|
|
||||||
|
|
||||||
state = new PlayerState(this)
|
state = new PlayerState(this)
|
||||||
ui = new PlayerUI(this)
|
ui = new PlayerUI(this)
|
||||||
serviceProviders = new ServiceProviders()
|
serviceProviders = new ServiceProviders()
|
||||||
//nativeControls = new MediaSession()
|
nativeControls = new MediaSession(this)
|
||||||
audioContext = new AudioContext({
|
|
||||||
sampleRate:
|
|
||||||
AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
|
|
||||||
latencyHint: "playback",
|
|
||||||
})
|
|
||||||
|
|
||||||
audioProcessors = new PlayerProcessors(this)
|
base = new AudioBase(this)
|
||||||
|
|
||||||
queue = new QueueManager({
|
queue = new QueueManager({
|
||||||
loadFunction: this.createInstance,
|
loadFunction: this.createInstance,
|
||||||
})
|
})
|
||||||
|
|
||||||
currentTrackInstance = null
|
|
||||||
|
|
||||||
public = {
|
public = {
|
||||||
start: this.start,
|
start: this.start,
|
||||||
close: this.close,
|
close: this.close,
|
||||||
@ -74,10 +65,11 @@ export default class Player extends Core {
|
|||||||
eventBus: () => {
|
eventBus: () => {
|
||||||
return this.eventBus
|
return this.eventBus
|
||||||
},
|
},
|
||||||
|
base: () => {
|
||||||
|
return this.base
|
||||||
|
},
|
||||||
state: this.state,
|
state: this.state,
|
||||||
ui: this.ui.public,
|
ui: this.ui.public,
|
||||||
audioContext: this.audioContext,
|
|
||||||
gradualFadeMs: Player.gradualFadeMs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async afterInitialize() {
|
async afterInitialize() {
|
||||||
@ -85,8 +77,8 @@ export default class Player extends Core {
|
|||||||
this.state.volume = 1
|
this.state.volume = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
//await this.nativeControls.initialize()
|
await this.nativeControls.initialize()
|
||||||
await this.audioProcessors.initialize()
|
await this.base.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -100,10 +92,6 @@ export default class Player extends Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createInstance(manifest) {
|
|
||||||
return new TrackInstance(this, manifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Playback methods
|
// Playback methods
|
||||||
//
|
//
|
||||||
@ -112,46 +100,21 @@ export default class Player extends Core {
|
|||||||
throw new Error("Audio instance is required")
|
throw new Error("Audio instance is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.console.log("Initializing instance", instance)
|
|
||||||
|
|
||||||
// resume audio context if needed
|
// resume audio context if needed
|
||||||
if (this.audioContext.state === "suspended") {
|
if (this.base.context.state === "suspended") {
|
||||||
this.audioContext.resume()
|
this.base.context.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize instance if is not
|
|
||||||
if (this.queue.currentItem._initialized === false) {
|
|
||||||
this.queue.currentItem = await instance.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.console.log("Instance", this.queue.currentItem)
|
|
||||||
|
|
||||||
// update manifest
|
// update manifest
|
||||||
this.state.track_manifest = this.queue.currentItem.manifest
|
this.state.track_manifest =
|
||||||
|
this.queue.currentItem.manifest.toSeriableObject()
|
||||||
// attach processors
|
|
||||||
this.queue.currentItem =
|
|
||||||
await this.audioProcessors.attachProcessorsToInstance(
|
|
||||||
this.queue.currentItem,
|
|
||||||
)
|
|
||||||
|
|
||||||
// set audio properties
|
|
||||||
this.queue.currentItem.audio.currentTime = params.time ?? 0
|
|
||||||
this.queue.currentItem.audio.muted = this.state.muted
|
|
||||||
this.queue.currentItem.audio.loop =
|
|
||||||
this.state.playback_mode === "repeat"
|
|
||||||
this.queue.currentItem.gainNode.gain.value = Math.pow(
|
|
||||||
this.state.volume,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
|
|
||||||
// play
|
// play
|
||||||
await this.queue.currentItem.audio.play()
|
//await this.queue.currentItem.audio.play()
|
||||||
|
await this.queue.currentItem.play(params)
|
||||||
this.console.log(`Playing track >`, this.queue.currentItem)
|
|
||||||
|
|
||||||
// update native controls
|
// update native controls
|
||||||
//this.nativeControls.update(this.queue.currentItem.manifest)
|
this.nativeControls.update(this.queue.currentItem.manifest)
|
||||||
|
|
||||||
return this.queue.currentItem
|
return this.queue.currentItem
|
||||||
}
|
}
|
||||||
@ -160,10 +123,10 @@ export default class Player extends Core {
|
|||||||
this.ui.attachPlayerComponent()
|
this.ui.attachPlayerComponent()
|
||||||
|
|
||||||
if (this.queue.currentItem) {
|
if (this.queue.currentItem) {
|
||||||
await this.queue.currentItem.stop()
|
await this.queue.currentItem.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.abortPreloads()
|
//await this.abortPreloads()
|
||||||
await this.queue.flush()
|
await this.queue.flush()
|
||||||
|
|
||||||
this.state.loading = true
|
this.state.loading = true
|
||||||
@ -187,8 +150,8 @@ export default class Player extends Core {
|
|||||||
playlist = await this.serviceProviders.resolveMany(playlist)
|
playlist = await this.serviceProviders.resolveMany(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const [index, _manifest] of playlist.entries()) {
|
for await (let [index, _manifest] of playlist.entries()) {
|
||||||
let instance = await this.createInstance(_manifest)
|
let instance = new TrackInstance(_manifest, this)
|
||||||
|
|
||||||
this.queue.add(instance)
|
this.queue.add(instance)
|
||||||
}
|
}
|
||||||
@ -229,10 +192,6 @@ export default class Player extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next() {
|
||||||
if (this.queue.currentItem) {
|
|
||||||
this.queue.currentItem.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
//const isRandom = this.state.playback_mode === "shuffle"
|
//const isRandom = this.state.playback_mode === "shuffle"
|
||||||
const item = this.queue.next()
|
const item = this.queue.next()
|
||||||
|
|
||||||
@ -244,10 +203,6 @@ export default class Player extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
previous() {
|
previous() {
|
||||||
if (this.queue.currentItem) {
|
|
||||||
this.queue.currentItem.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = this.queue.previous()
|
const item = this.queue.previous()
|
||||||
|
|
||||||
return this.play(item)
|
return this.play(item)
|
||||||
@ -275,18 +230,14 @@ export default class Player extends Core {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// set gain exponentially
|
this.base.processors.gain.fade(0)
|
||||||
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
|
|
||||||
0.0001,
|
|
||||||
this.audioContext.currentTime + Player.gradualFadeMs / 1000,
|
|
||||||
)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.queue.currentItem.audio.pause()
|
this.queue.currentItem.pause()
|
||||||
resolve()
|
resolve()
|
||||||
}, Player.gradualFadeMs)
|
}, Player.gradualFadeMs)
|
||||||
|
|
||||||
//this.nativeControls.updateIsPlaying(false)
|
this.nativeControls.updateIsPlaying(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,19 +253,12 @@ export default class Player extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure audio elemeto starts from 0 volume
|
// ensure audio elemeto starts from 0 volume
|
||||||
this.queue.currentItem.gainNode.gain.value = 0.0001
|
this.queue.currentItem.resume().then(() => {
|
||||||
|
|
||||||
this.queue.currentItem.audio.play().then(() => {
|
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
this.base.processors.gain.fade(this.state.volume)
|
||||||
|
|
||||||
// set gain exponentially
|
this.nativeControls.updateIsPlaying(true)
|
||||||
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
|
|
||||||
Math.pow(this.state.volume, 2),
|
|
||||||
this.audioContext.currentTime + Player.gradualFadeMs / 1000,
|
|
||||||
)
|
|
||||||
|
|
||||||
//this.nativeControls.updateIsPlaying(true)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,10 +269,7 @@ export default class Player extends Core {
|
|||||||
|
|
||||||
this.state.playback_mode = mode
|
this.state.playback_mode = mode
|
||||||
|
|
||||||
if (this.queue.currentItem) {
|
this.base.audio.loop = this.state.playback_mode === "repeat"
|
||||||
this.queue.currentItem.audio.loop =
|
|
||||||
this.state.playback_mode === "repeat"
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioPlayerStorage.set("mode", mode)
|
AudioPlayerStorage.set("mode", mode)
|
||||||
|
|
||||||
@ -336,22 +277,15 @@ export default class Player extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopPlayback() {
|
stopPlayback() {
|
||||||
if (this.queue.currentItem) {
|
this.base.flush()
|
||||||
this.queue.currentItem.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queue.flush()
|
this.queue.flush()
|
||||||
|
|
||||||
this.abortPreloads()
|
|
||||||
|
|
||||||
this.state.playback_status = "stopped"
|
this.state.playback_status = "stopped"
|
||||||
this.state.track_manifest = null
|
this.state.track_manifest = null
|
||||||
|
|
||||||
this.queue.currentItem = null
|
this.queue.currentItem = null
|
||||||
this.track_next_instances = []
|
|
||||||
this.track_prev_instances = []
|
|
||||||
|
|
||||||
//this.nativeControls.destroy()
|
//this.abortPreloads()
|
||||||
|
this.nativeControls.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -369,7 +303,7 @@ export default class Player extends Core {
|
|||||||
|
|
||||||
if (typeof to === "boolean") {
|
if (typeof to === "boolean") {
|
||||||
this.state.muted = to
|
this.state.muted = to
|
||||||
this.queue.currentItem.audio.muted = to
|
this.base.audio.muted = to
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.state.muted
|
return this.state.muted
|
||||||
@ -395,65 +329,42 @@ export default class Player extends Core {
|
|||||||
volume = 0
|
volume = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.volume = volume
|
|
||||||
|
|
||||||
AudioPlayerStorage.set("volume", volume)
|
AudioPlayerStorage.set("volume", volume)
|
||||||
|
|
||||||
if (this.queue.currentItem) {
|
this.state.volume = volume
|
||||||
if (this.queue.currentItem.gainNode) {
|
this.base.processors.gain.set(volume)
|
||||||
this.queue.currentItem.gainNode.gain.value = Math.pow(
|
|
||||||
this.state.volume,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.volume
|
return this.state.volume
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(time) {
|
seek(time) {
|
||||||
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
|
if (!this.base.audio) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// if time not provided, return current time
|
// if time not provided, return current time
|
||||||
if (typeof time === "undefined") {
|
if (typeof time === "undefined") {
|
||||||
return this.queue.currentItem.audio.currentTime
|
return this.base.audio.currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// if time is provided, seek to that time
|
// if time is provided, seek to that time
|
||||||
if (typeof time === "number") {
|
if (typeof time === "number") {
|
||||||
this.console.log(
|
this.console.log(
|
||||||
`Seeking to ${time} | Duration: ${this.queue.currentItem.audio.duration}`,
|
`Seeking to ${time} | Duration: ${this.base.audio.duration}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
this.queue.currentItem.audio.currentTime = time
|
this.base.audio.currentTime = time
|
||||||
|
|
||||||
return time
|
return time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
duration() {
|
duration() {
|
||||||
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
|
if (!this.base.audio) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.queue.currentItem.audio.duration
|
return this.base.audio.duration
|
||||||
}
|
|
||||||
|
|
||||||
loop(to) {
|
|
||||||
if (typeof to !== "boolean") {
|
|
||||||
this.console.warn("Loop must be a boolean")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.loop = to ?? !this.state.loop
|
|
||||||
|
|
||||||
if (this.queue.currentItem.audio) {
|
|
||||||
this.queue.currentItem.audio.loop = this.state.loop
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.loop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
@ -27,12 +27,8 @@ export default class CompressorProcessorNode extends ProcessorNode {
|
|||||||
static refName = "compressor"
|
static refName = "compressor"
|
||||||
static dependsOnSettings = ["player.compressor"]
|
static dependsOnSettings = ["player.compressor"]
|
||||||
|
|
||||||
async init(AudioContext) {
|
async init() {
|
||||||
if (!AudioContext) {
|
this.processor = this.audioContext.createDynamicsCompressor()
|
||||||
throw new Error("AudioContext is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processor = AudioContext.createDynamicsCompressor()
|
|
||||||
|
|
||||||
this.applyValues()
|
this.applyValues()
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,17 @@ export default class EqProcessorNode extends ProcessorNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static refName = "eq"
|
static refName = "eq"
|
||||||
static lock = true
|
|
||||||
|
|
||||||
applyValues() {
|
applyValues() {
|
||||||
// apply to current instance
|
// apply to current instance
|
||||||
this.processor.eqNodes.forEach((processor) => {
|
this.processor.eqNodes.forEach((processor) => {
|
||||||
const gainValue = this.presets.currentPresetValues[processor.frequency.value]
|
const gainValue =
|
||||||
|
this.presets.currentPresetValues[processor.frequency.value]
|
||||||
|
|
||||||
if (processor.gain.value !== gainValue) {
|
if (processor.gain.value !== gainValue) {
|
||||||
console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`)
|
console.debug(
|
||||||
|
`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`,
|
||||||
|
)
|
||||||
processor.gain.value = gainValue
|
processor.gain.value = gainValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -53,12 +55,14 @@ export default class EqProcessorNode extends ProcessorNode {
|
|||||||
|
|
||||||
this.processor.eqNodes = []
|
this.processor.eqNodes = []
|
||||||
|
|
||||||
const values = Object.entries(this.presets.currentPresetValues).map((entry) => {
|
const values = Object.entries(this.presets.currentPresetValues).map(
|
||||||
|
(entry) => {
|
||||||
return {
|
return {
|
||||||
freq: parseFloat(entry[0]),
|
freq: parseFloat(entry[0]),
|
||||||
gain: parseFloat(entry[1]),
|
gain: parseFloat(entry[1]),
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
values.forEach((eqValue, index) => {
|
values.forEach((eqValue, index) => {
|
||||||
// chekc if freq and gain is valid
|
// chekc if freq and gain is valid
|
||||||
@ -69,7 +73,8 @@ export default class EqProcessorNode extends ProcessorNode {
|
|||||||
eqValue.gain = 0
|
eqValue.gain = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processor.eqNodes[index] = this.audioContext.createBiquadFilter()
|
this.processor.eqNodes[index] =
|
||||||
|
this.audioContext.createBiquadFilter()
|
||||||
this.processor.eqNodes[index].type = "peaking"
|
this.processor.eqNodes[index].type = "peaking"
|
||||||
this.processor.eqNodes[index].frequency.value = eqValue.freq
|
this.processor.eqNodes[index].frequency.value = eqValue.freq
|
||||||
this.processor.eqNodes[index].gain.value = eqValue.gain
|
this.processor.eqNodes[index].gain.value = eqValue.gain
|
||||||
|
@ -1,41 +1,41 @@
|
|||||||
import AudioPlayerStorage from "../../player.storage"
|
|
||||||
import ProcessorNode from "../node"
|
import ProcessorNode from "../node"
|
||||||
|
|
||||||
export default class GainProcessorNode extends ProcessorNode {
|
export default class GainProcessorNode extends ProcessorNode {
|
||||||
static refName = "gain"
|
static refName = "gain"
|
||||||
|
static gradualFadeMs = 150
|
||||||
static lock = true
|
|
||||||
|
|
||||||
static defaultValues = {
|
|
||||||
gain: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
gain: AudioPlayerStorage.get("gain") ?? GainProcessorNode.defaultValues.gain,
|
|
||||||
}
|
|
||||||
|
|
||||||
exposeToPublic = {
|
exposeToPublic = {
|
||||||
modifyValues: function (values) {
|
set: this.setGain.bind(this),
|
||||||
this.state = {
|
linearRampToValueAtTime: this.linearRampToValueAtTime.bind(this),
|
||||||
...this.state,
|
fade: this.fade.bind(this),
|
||||||
...values,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioPlayerStorage.set("gain", this.state.gain)
|
setGain(gain) {
|
||||||
|
gain = this.processGainValue(gain)
|
||||||
|
|
||||||
this.applyValues()
|
return (this.processor.gain.value = gain)
|
||||||
}.bind(this),
|
|
||||||
resetDefaultValues: function () {
|
|
||||||
this.exposeToPublic.modifyValues(GainProcessorNode.defaultValues)
|
|
||||||
|
|
||||||
return this.state
|
|
||||||
}.bind(this),
|
|
||||||
values: () => this.state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyValues() {
|
linearRampToValueAtTime(gain, time) {
|
||||||
// apply to current instance
|
gain = this.processGainValue(gain)
|
||||||
this.processor.gain.value = app.cores.player.state.volume * this.state.gain
|
return this.processor.gain.linearRampToValueAtTime(gain, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
fade(gain) {
|
||||||
|
if (gain <= 0) {
|
||||||
|
gain = 0.0001
|
||||||
|
} else {
|
||||||
|
gain = this.processGainValue(gain)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = this.audioContext.currentTime
|
||||||
|
const fadeTime = currentTime + this.constructor.gradualFadeMs / 1000
|
||||||
|
|
||||||
|
this.processor.gain.linearRampToValueAtTime(gain, fadeTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
processGainValue(gain) {
|
||||||
|
return Math.pow(gain, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -44,17 +44,6 @@ export default class GainProcessorNode extends ProcessorNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.processor = this.audioContext.createGain()
|
this.processor = this.audioContext.createGain()
|
||||||
|
this.processor.gain.value = this.player.state.volume
|
||||||
this.applyValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateInstance(instance) {
|
|
||||||
if (!instance) {
|
|
||||||
throw new Error("instance is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.gainNode = this.processor
|
|
||||||
|
|
||||||
return instance
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,13 +2,12 @@ import EqProcessorNode from "./eqNode"
|
|||||||
import GainProcessorNode from "./gainNode"
|
import GainProcessorNode from "./gainNode"
|
||||||
import CompressorProcessorNode from "./compressorNode"
|
import CompressorProcessorNode from "./compressorNode"
|
||||||
//import BPMProcessorNode from "./bpmNode"
|
//import BPMProcessorNode from "./bpmNode"
|
||||||
|
//import SpatialNode from "./spatialNode"
|
||||||
import SpatialNode from "./spatialNode"
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
//BPMProcessorNode,
|
//BPMProcessorNode,
|
||||||
EqProcessorNode,
|
EqProcessorNode,
|
||||||
GainProcessorNode,
|
GainProcessorNode,
|
||||||
CompressorProcessorNode,
|
CompressorProcessorNode,
|
||||||
SpatialNode,
|
//SpatialNode,
|
||||||
]
|
]
|
@ -1,17 +1,19 @@
|
|||||||
export default class ProcessorNode {
|
export default class ProcessorNode {
|
||||||
constructor(PlayerCore) {
|
constructor(manager) {
|
||||||
if (!PlayerCore) {
|
if (!manager) {
|
||||||
throw new Error("PlayerCore is required")
|
throw new Error("processorManager is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.PlayerCore = PlayerCore
|
this.manager = manager
|
||||||
this.audioContext = PlayerCore.audioContext
|
this.audioContext = manager.base.context
|
||||||
|
this.elementSource = manager.base.elementSource
|
||||||
|
this.player = manager.base.player
|
||||||
}
|
}
|
||||||
|
|
||||||
async _init() {
|
async _init() {
|
||||||
// check if has init method
|
// check if has init method
|
||||||
if (typeof this.init === "function") {
|
if (typeof this.init === "function") {
|
||||||
await this.init(this.audioContext)
|
await this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if has declared bus events
|
// check if has declared bus events
|
||||||
@ -28,36 +30,40 @@ export default class ProcessorNode {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
_attach(instance, index) {
|
_attach(index) {
|
||||||
if (typeof instance !== "object") {
|
|
||||||
instance = this.PlayerCore.currentAudioInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if has dependsOnSettings
|
// check if has dependsOnSettings
|
||||||
if (Array.isArray(this.constructor.dependsOnSettings)) {
|
if (Array.isArray(this.constructor.dependsOnSettings)) {
|
||||||
// check if the instance has the settings
|
// check if the instance has the settings
|
||||||
if (!this.constructor.dependsOnSettings.every((setting) => app.cores.settings.get(setting))) {
|
if (
|
||||||
console.warn(`Skipping attachment for [${this.constructor.refName ?? this.constructor.name}] node, cause is not passing the settings dependecy > ${this.constructor.dependsOnSettings.join(", ")}`)
|
!this.constructor.dependsOnSettings.every((setting) =>
|
||||||
|
app.cores.settings.get(setting),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping attachment for [${this.constructor.refName ?? this.constructor.name}] node, cause is not passing the settings dependecy > ${this.constructor.dependsOnSettings.join(", ")}`,
|
||||||
|
)
|
||||||
|
|
||||||
return instance
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if index is not defined, attach to the last node
|
// if index is not defined, attach to the last node
|
||||||
if (!index) {
|
if (!index) {
|
||||||
index = instance.attachedProcessors.length
|
index = this.manager.attached.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevNode = instance.attachedProcessors[index - 1]
|
const prevNode = this.manager.attached[index - 1]
|
||||||
const nextNode = instance.attachedProcessors[index + 1]
|
const nextNode = this.manager.attached[index + 1]
|
||||||
|
|
||||||
const currentIndex = this._findIndex(instance)
|
const currentIndex = this._findIndex()
|
||||||
|
|
||||||
// check if is already attached
|
// check if is already attached
|
||||||
if (currentIndex !== false) {
|
if (currentIndex !== false) {
|
||||||
console.warn(`[${this.constructor.refName ?? this.constructor.name}] node is already attached`)
|
console.warn(
|
||||||
|
`[${this.constructor.refName ?? this.constructor.name}] node is already attached`,
|
||||||
|
)
|
||||||
|
|
||||||
return instance
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// first check if has prevNode and if is connected to something
|
// first check if has prevNode and if is connected to something
|
||||||
@ -72,7 +78,7 @@ export default class ProcessorNode {
|
|||||||
prevNode.processor._last.connect(this.processor)
|
prevNode.processor._last.connect(this.processor)
|
||||||
} else {
|
} else {
|
||||||
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
|
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
|
||||||
instance.contextElement.connect(this.processor)
|
this.elementSource.connect(this.processor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// now, check if it has a next node
|
// now, check if it has a next node
|
||||||
@ -83,31 +89,27 @@ export default class ProcessorNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add to the attachedProcessors
|
// add to the attachedProcessors
|
||||||
instance.attachedProcessors.splice(index, 0, this)
|
this.manager.attached.splice(index, 0, this)
|
||||||
|
|
||||||
// handle instance mutation
|
// // handle instance mutation
|
||||||
if (typeof this.mutateInstance === "function") {
|
// if (typeof this.mutateInstance === "function") {
|
||||||
instance = this.mutateInstance(instance)
|
// instance = this.mutateInstance(instance)
|
||||||
}
|
// }
|
||||||
|
|
||||||
return instance
|
return this
|
||||||
}
|
|
||||||
|
|
||||||
_detach(instance) {
|
|
||||||
if (typeof instance !== "object") {
|
|
||||||
instance = this.PlayerCore.currentAudioInstance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_detach() {
|
||||||
// find index of the node within the attachedProcessors serching for matching refName
|
// find index of the node within the attachedProcessors serching for matching refName
|
||||||
const index = this._findIndex(instance)
|
const index = this._findIndex()
|
||||||
|
|
||||||
if (!index) {
|
if (!index) {
|
||||||
return instance
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the previous and next nodes
|
// retrieve the previous and next nodes
|
||||||
const prevNode = instance.attachedProcessors[index - 1]
|
const prevNode = this.manager.attached[index - 1]
|
||||||
const nextNode = instance.attachedProcessors[index + 1]
|
const nextNode = this.manager.attached[index + 1]
|
||||||
|
|
||||||
// check if has previous node and if has outputs
|
// check if has previous node and if has outputs
|
||||||
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
|
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
|
||||||
@ -116,7 +118,8 @@ export default class ProcessorNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// disconnect
|
// disconnect
|
||||||
instance = this._destroy(instance)
|
this.processor.disconnect()
|
||||||
|
this.manager.attached.splice(index, 1)
|
||||||
|
|
||||||
// now, connect the previous node to the next node
|
// now, connect the previous node to the next node
|
||||||
if (prevNode && nextNode) {
|
if (prevNode && nextNode) {
|
||||||
@ -126,40 +129,12 @@ export default class ProcessorNode {
|
|||||||
prevNode.processor._last.connect(this.audioContext.destination)
|
prevNode.processor._last.connect(this.audioContext.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance
|
return this
|
||||||
}
|
|
||||||
|
|
||||||
_destroy(instance) {
|
|
||||||
if (typeof instance !== "object") {
|
|
||||||
instance = this.PlayerCore.currentAudioInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this._findIndex(instance)
|
|
||||||
|
|
||||||
if (!index) {
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processor.disconnect()
|
|
||||||
|
|
||||||
instance.attachedProcessors.splice(index, 1)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
_findIndex(instance) {
|
|
||||||
if (!instance) {
|
|
||||||
instance = this.PlayerCore.currentAudioInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!instance) {
|
|
||||||
console.warn(`Instance is not defined`)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_findIndex() {
|
||||||
// find index of the node within the attachedProcessors serching for matching refName
|
// find index of the node within the attachedProcessors serching for matching refName
|
||||||
const index = instance.attachedProcessors.findIndex((node) => {
|
const index = this.manager.attached.findIndex((node) => {
|
||||||
return node.constructor.refName === this.constructor.refName
|
return node.constructor.refName === this.constructor.refName
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ export default class RemoteStorage extends Core {
|
|||||||
_reject(message)
|
_reject(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
uploader.events.on("progress", ({ percentProgress }) => {
|
uploader.events.on("progress", (data) => {
|
||||||
if (typeof onProgress === "function") {
|
if (typeof onProgress === "function") {
|
||||||
onProgress(file, percentProgress)
|
onProgress(file, data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export default class SFXCore extends Core {
|
|||||||
this.play("test", {
|
this.play("test", {
|
||||||
volume: volume / 100,
|
volume: volume / 100,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSoundpack(soundpack) {
|
async loadSoundpack(soundpack) {
|
||||||
@ -34,7 +34,7 @@ export default class SFXCore extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if is valid url with regex
|
// check if is valid url with regex
|
||||||
const urlRegex = /^(http|https):\/\/[^ "]+$/;
|
const urlRegex = /^(http|https):\/\/[^ "]+$/
|
||||||
|
|
||||||
if (urlRegex.test(soundpack)) {
|
if (urlRegex.test(soundpack)) {
|
||||||
const { data } = await axios.get(soundpack)
|
const { data } = await axios.get(soundpack)
|
||||||
@ -43,11 +43,15 @@ export default class SFXCore extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof soundpack.sounds !== "object") {
|
if (typeof soundpack.sounds !== "object") {
|
||||||
this.console.error(`Soundpack [${soundpack.id}] is not a valid soundpack.`)
|
this.console.error(
|
||||||
|
`Soundpack [${soundpack.id}] is not a valid soundpack.`,
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.console.log(`Loading soundpack [${soundpack.id} | ${soundpack.name}] by ${soundpack.author} (${soundpack.version})`)
|
this.console.log(
|
||||||
|
`Loading soundpack [${soundpack.id} | ${soundpack.name}] by ${soundpack.author} (${soundpack.version})`,
|
||||||
|
)
|
||||||
|
|
||||||
for (const [name, path] of Object.entries(soundpack.sounds)) {
|
for (const [name, path] of Object.entries(soundpack.sounds)) {
|
||||||
this.soundsPool[name] = new Howl({
|
this.soundsPool[name] = new Howl({
|
||||||
@ -71,7 +75,9 @@ export default class SFXCore extends Core {
|
|||||||
if (typeof options.volume !== "undefined") {
|
if (typeof options.volume !== "undefined") {
|
||||||
audioInstance.volume(options.volume)
|
audioInstance.volume(options.volume)
|
||||||
} else {
|
} else {
|
||||||
audioInstance.volume((window.app.cores.settings.get("ui.general_volume") ?? 0) / 100)
|
audioInstance.volume(
|
||||||
|
(window.app.cores.settings.get("ui.general_volume") ?? 0) / 100,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
audioInstance.play()
|
audioInstance.play()
|
||||||
@ -79,7 +85,8 @@ export default class SFXCore extends Core {
|
|||||||
|
|
||||||
async handleClick(event) {
|
async handleClick(event) {
|
||||||
// search for closest button
|
// search for closest button
|
||||||
const button = event.target.closest("button") || event.target.closest(".ant-btn")
|
const button =
|
||||||
|
event.target.closest("button") || event.target.closest(".ant-btn")
|
||||||
|
|
||||||
// search for a slider
|
// search for a slider
|
||||||
const slider = event.target.closest("input[type=range]")
|
const slider = event.target.closest("input[type=range]")
|
||||||
@ -87,7 +94,11 @@ export default class SFXCore extends Core {
|
|||||||
// if button exist and has aria-checked attribute then play switch_on or switch_off
|
// if button exist and has aria-checked attribute then play switch_on or switch_off
|
||||||
if (button) {
|
if (button) {
|
||||||
if (button.hasAttribute("aria-checked")) {
|
if (button.hasAttribute("aria-checked")) {
|
||||||
return this.play(button.getAttribute("aria-checked") === "true" ? "component.switch_off" : "component.switch_on")
|
return this.play(
|
||||||
|
button.getAttribute("aria-checked") === "true"
|
||||||
|
? "component.switch_off"
|
||||||
|
: "component.switch_on",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.play("generic_click")
|
return this.play("generic_click")
|
||||||
@ -102,6 +113,12 @@ export default class SFXCore extends Core {
|
|||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
await this.loadSoundpack()
|
await this.loadSoundpack()
|
||||||
|
|
||||||
document.addEventListener("click", (...args) => { this.handleClick(...args) }, true)
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
(...args) => {
|
||||||
|
this.handleClick(...args)
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ import classnames from "classnames"
|
|||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import SeekBar from "@components/Player/SeekBar"
|
import SeekBar from "@components/Player/SeekBar"
|
||||||
import Controls from "@components/Player/Controls"
|
import Controls from "@components/Player/Controls"
|
||||||
import ExtraActions from "@components/Player/ExtraActions"
|
import Actions from "@components/Player/Actions"
|
||||||
|
|
||||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||||
import RGBStringToValues from "@utils/rgbToValues"
|
import RGBStringToValues from "@utils/rgbToValues"
|
||||||
@ -18,9 +18,11 @@ const ServiceIndicator = (props) => {
|
|||||||
|
|
||||||
switch (props.service) {
|
switch (props.service) {
|
||||||
case "tidal": {
|
case "tidal": {
|
||||||
return <div className="service_indicator">
|
return (
|
||||||
|
<div className="service_indicator">
|
||||||
<Icons.SiTidal /> Playing from Tidal
|
<Icons.SiTidal /> Playing from Tidal
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return null
|
return null
|
||||||
@ -33,9 +35,12 @@ const AudioPlayer = (props) => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (app.currentDragger) {
|
if (app.currentDragger) {
|
||||||
app.currentDragger.setBackgroundColorValues(RGBStringToValues(playerState.track_manifest?.cover_analysis?.rgb))
|
app.currentDragger.setBackgroundColorValues(
|
||||||
|
RGBStringToValues(
|
||||||
|
playerState.track_manifest?.cover_analysis?.rgb,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [playerState.track_manifest?.cover_analysis])
|
}, [playerState.track_manifest?.cover_analysis])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -51,47 +56,35 @@ const AudioPlayer = (props) => {
|
|||||||
const playing = playerState.playback_status === "playing"
|
const playing = playerState.playback_status === "playing"
|
||||||
const stopped = playerState.playback_status === "stopped"
|
const stopped = playerState.playback_status === "stopped"
|
||||||
|
|
||||||
const titleText = (!playing && stopped) ? "Stopped" : (title ?? "Untitled")
|
const titleText = !playing && stopped ? "Stopped" : (title ?? "Untitled")
|
||||||
const subtitleText = `${artist} | ${album?.title ?? album}`
|
const subtitleText = `${artist} | ${album?.title ?? album}`
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
className={classnames(
|
<div
|
||||||
"mobile_media_player_wrapper",
|
className={classnames("mobile-player_wrapper", {
|
||||||
{
|
cover_light: cover_analysis?.isLight,
|
||||||
"cover_light": cover_analysis?.isLight,
|
})}
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
"--cover_isLight": cover_analysis?.isLight,
|
"--cover_isLight": cover_analysis?.isLight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mobile_media_player">
|
<div className="mobile-player">
|
||||||
<ServiceIndicator
|
<ServiceIndicator service={service} />
|
||||||
service={service}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="cover"
|
className="mobile-player-cover"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${cover ?? "/assets/no_song.png"})`,
|
backgroundImage: `url(${cover ?? "/assets/no_song.png"})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="header">
|
<div className="mobile-player-header">
|
||||||
<div className="info">
|
<div className="mobile-player-info">
|
||||||
<div className="title">
|
<div className="mobile-player-info-title">
|
||||||
<h2>
|
<h1>{titleText}</h1>
|
||||||
{
|
|
||||||
titleText
|
|
||||||
}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="subTitle">
|
|
||||||
<div className="artist">
|
|
||||||
<h3>
|
|
||||||
{subtitleText}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mobile-player-info-subTitle">
|
||||||
|
<span>{subtitleText}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,9 +98,10 @@ const AudioPlayer = (props) => {
|
|||||||
disabled={playerState.control_locked}
|
disabled={playerState.control_locked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExtraActions />
|
<Actions />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AudioPlayer
|
export default AudioPlayer
|
@ -1,6 +1,6 @@
|
|||||||
@top_controls_height: 55px;
|
@top_controls_height: 55px;
|
||||||
|
|
||||||
.mobile_media_player_wrapper {
|
.mobile-player_wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
z-index: 320;
|
z-index: 320;
|
||||||
@ -12,7 +12,9 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.mobile_media_player_background {
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
.mobile-player_background {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
z-index: 320;
|
z-index: 320;
|
||||||
@ -27,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile_media_player {
|
.mobile-player {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -55,7 +57,7 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover {
|
.mobile-player-cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
z-index: 320;
|
z-index: 320;
|
||||||
@ -85,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.mobile-player-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -93,7 +95,7 @@
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.info {
|
.mobile-player-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -111,33 +113,29 @@
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.title {
|
.mobile-player-info-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-color);
|
|
||||||
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|
||||||
font-family: "Space Grotesk", sans-serif;
|
font-family: "Space Grotesk", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subTitle {
|
.mobile-player-info-subTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
width: 100%;
|
align-items: center;
|
||||||
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.likeButton {
|
font-size: 0.7rem;
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.artist {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,17 +181,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.extra_actions {
|
|
||||||
padding: 0 30px;
|
|
||||||
|
|
||||||
.ant-btn {
|
|
||||||
padding: 5px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: 23px;
|
|
||||||
min-width: 23px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
47
packages/app/src/pages/_debug/audiometadata/index.jsx
Normal file
47
packages/app/src/pages/_debug/audiometadata/index.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import TrackManifest from "@cores/player/classes/TrackManifest"
|
||||||
|
|
||||||
|
const D_Manifest = () => {
|
||||||
|
const [manifest, setManifest] = React.useState(null)
|
||||||
|
|
||||||
|
function selectLocalFile() {
|
||||||
|
const input = document.createElement("input")
|
||||||
|
input.type = "file"
|
||||||
|
input.accept = "audio/*"
|
||||||
|
input.onchange = (e) => {
|
||||||
|
loadManifest(e.target.files[0])
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadManifest(file) {
|
||||||
|
let track = new TrackManifest({ file: file })
|
||||||
|
|
||||||
|
await track.initialize()
|
||||||
|
|
||||||
|
console.log(track)
|
||||||
|
|
||||||
|
setManifest(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-column gap-10">
|
||||||
|
<p>Select a local file to view & create a track manifest</p>
|
||||||
|
|
||||||
|
<button onClick={selectLocalFile}>Select</button>
|
||||||
|
|
||||||
|
{manifest?.cover && (
|
||||||
|
<img
|
||||||
|
src={manifest.cover}
|
||||||
|
alt="Cover"
|
||||||
|
style={{ width: "100px", height: "100px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<code style={{ whiteSpace: "break-spaces", width: "300px" }}>
|
||||||
|
{JSON.stringify(manifest)}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default D_Manifest
|
@ -177,7 +177,7 @@ const PlayerController = React.forwardRef((props, ref) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="lyrics-player-controller-tags">
|
<div className="lyrics-player-controller-tags">
|
||||||
{playerState.track_manifest?.metadata.lossless && (
|
{playerState.track_manifest?.metadata?.lossless && (
|
||||||
<Tag
|
<Tag
|
||||||
icon={
|
icon={
|
||||||
<Icons.Lossless
|
<Icons.Lossless
|
||||||
|
@ -22,7 +22,10 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined") {
|
if (
|
||||||
|
!lyrics.video_source ||
|
||||||
|
typeof lyrics.sync_audio_at_ms === "undefined"
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +33,8 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
|
|
||||||
setSyncingVideo(true)
|
setSyncingVideo(true)
|
||||||
|
|
||||||
let newTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000
|
let newTime =
|
||||||
|
currentTrackTime + lyrics.sync_audio_at_ms / 1000 + 150 / 1000
|
||||||
|
|
||||||
// dec some ms to ensure the video seeks correctly
|
// dec some ms to ensure the video seeks correctly
|
||||||
newTime -= 5 / 1000
|
newTime -= 5 / 1000
|
||||||
@ -40,17 +44,26 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
|
|
||||||
async function syncPlayback() {
|
async function syncPlayback() {
|
||||||
// if something is wrong, stop syncing
|
// if something is wrong, stop syncing
|
||||||
if (videoRef.current === null || !lyrics || !lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined" || playerState.playback_status !== "playing") {
|
if (
|
||||||
|
videoRef.current === null ||
|
||||||
|
!lyrics ||
|
||||||
|
!lyrics.video_source ||
|
||||||
|
typeof lyrics.sync_audio_at_ms === "undefined" ||
|
||||||
|
playerState.playback_status !== "playing"
|
||||||
|
) {
|
||||||
return stopSyncInterval()
|
return stopSyncInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTrackTime = app.cores.player.controls.seek()
|
const currentTrackTime = app.cores.player.controls.seek()
|
||||||
const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000)
|
const currentVideoTime =
|
||||||
|
videoRef.current.currentTime - lyrics.sync_audio_at_ms / 1000
|
||||||
|
|
||||||
//console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`)
|
//console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`)
|
||||||
|
|
||||||
const maxOffset = maxLatencyInMs / 1000
|
const maxOffset = maxLatencyInMs / 1000
|
||||||
const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime)
|
const currentVideoTimeDiff = Math.abs(
|
||||||
|
currentVideoTime - currentTrackTime,
|
||||||
|
)
|
||||||
|
|
||||||
setCurrentVideoLatency(currentVideoTimeDiff)
|
setCurrentVideoLatency(currentVideoTimeDiff)
|
||||||
|
|
||||||
@ -75,11 +88,19 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
|
|
||||||
//* handle when player is loading
|
//* handle when player is loading
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (lyrics?.video_source && playerState.loading === true && playerState.playback_status === "playing") {
|
if (
|
||||||
|
lyrics?.video_source &&
|
||||||
|
playerState.loading === true &&
|
||||||
|
playerState.playback_status === "playing"
|
||||||
|
) {
|
||||||
videoRef.current.pause()
|
videoRef.current.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyrics?.video_source && playerState.loading === false && playerState.playback_status === "playing") {
|
if (
|
||||||
|
lyrics?.video_source &&
|
||||||
|
playerState.loading === false &&
|
||||||
|
playerState.playback_status === "playing"
|
||||||
|
) {
|
||||||
videoRef.current.play()
|
videoRef.current.play()
|
||||||
}
|
}
|
||||||
}, [playerState.loading])
|
}, [playerState.loading])
|
||||||
@ -87,7 +108,9 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
//* Handle when playback status change
|
//* Handle when playback status change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initialLoading === false) {
|
if (initialLoading === false) {
|
||||||
console.log(`VIDEO:: Playback status changed to ${playerState.playback_status}`)
|
console.log(
|
||||||
|
`VIDEO:: Playback status changed to ${playerState.playback_status}`,
|
||||||
|
)
|
||||||
|
|
||||||
if (lyrics && lyrics.video_source) {
|
if (lyrics && lyrics.video_source) {
|
||||||
if (playerState.playback_status === "playing") {
|
if (playerState.playback_status === "playing") {
|
||||||
@ -118,7 +141,8 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
|
|
||||||
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
|
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
|
||||||
videoRef.current.loop = false
|
videoRef.current.loop = false
|
||||||
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
|
videoRef.current.currentTime =
|
||||||
|
lyrics.sync_audio_at_ms / 1000
|
||||||
|
|
||||||
startSyncInterval()
|
startSyncInterval()
|
||||||
} else {
|
} else {
|
||||||
@ -147,13 +171,10 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{
|
<>
|
||||||
props.lyrics?.sync_audio_at && <div
|
{props.lyrics?.sync_audio_at && (
|
||||||
className={classnames(
|
<div className={classnames("videoDebugOverlay")}>
|
||||||
"videoDebugOverlay",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<p>Maximun latency</p>
|
<p>Maximun latency</p>
|
||||||
<p>{maxLatencyInMs}ms</p>
|
<p>{maxLatencyInMs}ms</p>
|
||||||
@ -164,21 +185,19 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
</div>
|
</div>
|
||||||
{syncingVideo ? <p>Syncing video...</p> : null}
|
{syncingVideo ? <p>Syncing video...</p> : null}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<video
|
<video
|
||||||
className={classnames(
|
className={classnames("lyrics-video", {
|
||||||
"lyrics-video",
|
["hidden"]: !lyrics || !lyrics?.video_source,
|
||||||
{
|
})}
|
||||||
["hidden"]: !lyrics || !lyrics?.video_source
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
controls={false}
|
controls={false}
|
||||||
muted
|
muted
|
||||||
preload="auto"
|
preload="auto"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default LyricsVideo
|
export default LyricsVideo
|
@ -89,7 +89,8 @@ const EnhancedLyricsPage = () => {
|
|||||||
|
|
||||||
// Track manifest comparison
|
// Track manifest comparison
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newManifest = playerState.track_manifest?.toSeriableObject()
|
const newManifest = playerState.track_manifest
|
||||||
|
|
||||||
if (JSON.stringify(newManifest) !== JSON.stringify(trackManifest)) {
|
if (JSON.stringify(newManifest) !== JSON.stringify(trackManifest)) {
|
||||||
setTrackManifest(newManifest)
|
setTrackManifest(newManifest)
|
||||||
}
|
}
|
||||||
|
@ -11,33 +11,33 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
settings: [
|
settings: [
|
||||||
{
|
// {
|
||||||
id: "player.gain",
|
// id: "player.gain",
|
||||||
title: "Gain",
|
// title: "Gain",
|
||||||
icon: "MdGraphicEq",
|
// icon: "MdGraphicEq",
|
||||||
group: "general",
|
// group: "general",
|
||||||
description: "Adjust gain for audio output",
|
// description: "Adjust gain for audio output",
|
||||||
component: "Slider",
|
// component: "Slider",
|
||||||
props: {
|
// props: {
|
||||||
min: 1,
|
// min: 1,
|
||||||
max: 2,
|
// max: 2,
|
||||||
step: 0.1,
|
// step: 0.1,
|
||||||
marks: {
|
// marks: {
|
||||||
1: "Normal",
|
// 1: "Normal",
|
||||||
1.5: "+50%",
|
// 1.5: "+50%",
|
||||||
2: "+100%",
|
// 2: "+100%",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
defaultValue: () => {
|
// defaultValue: () => {
|
||||||
return app.cores.player.gain.values().gain
|
// return app.cores.player.gain.values().gain
|
||||||
},
|
// },
|
||||||
onUpdate: (value) => {
|
// onUpdate: (value) => {
|
||||||
app.cores.player.gain.modifyValues({
|
// app.cores.player.gain.modifyValues({
|
||||||
gain: value,
|
// gain: value,
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
storaged: false,
|
// storaged: false,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: "player.sample_rate",
|
id: "player.sample_rate",
|
||||||
title: "Sample Rate",
|
title: "Sample Rate",
|
||||||
@ -66,7 +66,7 @@ export default {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
defaultValue: (ctx) => {
|
defaultValue: (ctx) => {
|
||||||
return app.cores.player.audioContext.sampleRate
|
return app.cores.player.base().context.sampleRate
|
||||||
},
|
},
|
||||||
onUpdate: async (value) => {
|
onUpdate: async (value) => {
|
||||||
const sampleRate =
|
const sampleRate =
|
||||||
@ -94,10 +94,10 @@ export default {
|
|||||||
onEnabledChange: (enabled) => {
|
onEnabledChange: (enabled) => {
|
||||||
if (enabled === true) {
|
if (enabled === true) {
|
||||||
app.cores.settings.set("player.compressor", true)
|
app.cores.settings.set("player.compressor", true)
|
||||||
app.cores.player.compressor.attach()
|
//app.cores.player.compressor.attach()
|
||||||
} else {
|
} else {
|
||||||
app.cores.settings.set("player.compressor", false)
|
app.cores.settings.set("player.compressor", false)
|
||||||
app.cores.player.compressor.detach()
|
//app.cores.player.compressor.detach()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraActions: [
|
extraActions: [
|
||||||
@ -106,8 +106,9 @@ export default {
|
|||||||
title: "Default",
|
title: "Default",
|
||||||
icon: "MdRefresh",
|
icon: "MdRefresh",
|
||||||
onClick: async (ctx) => {
|
onClick: async (ctx) => {
|
||||||
const values =
|
const values = await app.cores.player
|
||||||
await app.cores.player.compressor.presets.setCurrentPresetToDefault()
|
.base()
|
||||||
|
.processors.compressor.presets.setCurrentPresetToDefault()
|
||||||
|
|
||||||
ctx.updateCurrentValue(values)
|
ctx.updateCurrentValue(values)
|
||||||
},
|
},
|
||||||
@ -152,13 +153,14 @@ export default {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
onUpdate: (value) => {
|
onUpdate: (value) => {
|
||||||
app.cores.player.compressor.presets.setToCurrent(value)
|
app.cores.player
|
||||||
|
.base()
|
||||||
|
.processors.compressor.presets.setToCurrent(value)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
storaged: false,
|
storaged: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "player.eq",
|
id: "player.eq",
|
||||||
title: "Equalizer",
|
title: "Equalizer",
|
||||||
@ -172,8 +174,9 @@ export default {
|
|||||||
title: "Reset",
|
title: "Reset",
|
||||||
icon: "MdRefresh",
|
icon: "MdRefresh",
|
||||||
onClick: (ctx) => {
|
onClick: (ctx) => {
|
||||||
const values =
|
const values = app.cores.player
|
||||||
app.cores.player.eq.presets.setCurrentPresetToDefault()
|
.base()
|
||||||
|
.processors.eq.presets.setCurrentPresetToDefault()
|
||||||
|
|
||||||
ctx.updateCurrentValue(values)
|
ctx.updateCurrentValue(values)
|
||||||
},
|
},
|
||||||
@ -260,7 +263,9 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
app.cores.player.eq.presets.setToCurrent(values)
|
app.cores.player
|
||||||
|
.base()
|
||||||
|
.processors.eq.presets.setToCurrent(values)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
|
@ -2,13 +2,11 @@ import { Switch } from "antd"
|
|||||||
import SlidersWithPresets from "../../../components/slidersWithPresets"
|
import SlidersWithPresets from "../../../components/slidersWithPresets"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
return <SlidersWithPresets
|
return (
|
||||||
|
<SlidersWithPresets
|
||||||
{...props}
|
{...props}
|
||||||
controller={app.cores.player.compressor.presets}
|
controller={app.cores.player.base().processors.compressor.presets}
|
||||||
extraHeaderItems={[
|
extraHeaderItems={[<Switch onChange={props.onEnabledChange} />]}
|
||||||
<Switch
|
|
||||||
onChange={props.onEnabledChange}
|
|
||||||
/>
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import SlidersWithPresets from "../../../components/slidersWithPresets"
|
import SlidersWithPresets from "../../../components/slidersWithPresets"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
return <SlidersWithPresets
|
return (
|
||||||
|
<SlidersWithPresets
|
||||||
{...props}
|
{...props}
|
||||||
controller={app.cores.player.eq.presets}
|
controller={app.cores.player.base().processors.eq.presets}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
99
packages/app/src/utils/compareObjectsByProperties/index.js
Normal file
99
packages/app/src/utils/compareObjectsByProperties/index.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
export default function compareObjectsByProperties(obj1, obj2, props) {
|
||||||
|
// validate that obj1 and obj2 are objects
|
||||||
|
if (
|
||||||
|
!obj1 ||
|
||||||
|
!obj2 ||
|
||||||
|
typeof obj1 !== "object" ||
|
||||||
|
typeof obj2 !== "object"
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate that props is an array
|
||||||
|
if (!Array.isArray(props)) {
|
||||||
|
throw new Error("The props parameter must be an array")
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through each property and compare
|
||||||
|
for (const prop of props) {
|
||||||
|
// check if the property exists in both objects
|
||||||
|
const prop1Exists = prop in obj1
|
||||||
|
const prop2Exists = prop in obj2
|
||||||
|
|
||||||
|
// if the property doesnt exist in one of the objects
|
||||||
|
if (prop1Exists !== prop2Exists) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the property exists in both, compare values
|
||||||
|
if (prop1Exists && prop2Exists) {
|
||||||
|
// for nested objects, perform deep comparison
|
||||||
|
if (
|
||||||
|
typeof obj1[prop] === "object" &&
|
||||||
|
obj1[prop] !== null &&
|
||||||
|
typeof obj2[prop] === "object" &&
|
||||||
|
obj2[prop] !== null
|
||||||
|
) {
|
||||||
|
// compare arrays
|
||||||
|
if (Array.isArray(obj1[prop]) && Array.isArray(obj2[prop])) {
|
||||||
|
if (obj1[prop].length !== obj2[prop].length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < obj1[prop].length; i++) {
|
||||||
|
// if elements are objects, call recursively
|
||||||
|
if (
|
||||||
|
typeof obj1[prop][i] === "object" &&
|
||||||
|
typeof obj2[prop][i] === "object"
|
||||||
|
) {
|
||||||
|
// get all properties of the object
|
||||||
|
const nestedProps = [
|
||||||
|
...new Set([
|
||||||
|
...Object.keys(obj1[prop][i]),
|
||||||
|
...Object.keys(obj2[prop][i]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (
|
||||||
|
!compareObjectsByProperties(
|
||||||
|
obj1[prop][i],
|
||||||
|
obj2[prop][i],
|
||||||
|
nestedProps,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (obj1[prop][i] !== obj2[prop][i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// compare objects
|
||||||
|
else {
|
||||||
|
const nestedProps = [
|
||||||
|
...new Set([
|
||||||
|
...Object.keys(obj1[prop]),
|
||||||
|
...Object.keys(obj2[prop]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (
|
||||||
|
!compareObjectsByProperties(
|
||||||
|
obj1[prop],
|
||||||
|
obj2[prop],
|
||||||
|
nestedProps,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for primitive values, compare directly
|
||||||
|
else if (obj1[prop] !== obj2[prop]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/swcrc",
|
|
||||||
"exclude":[
|
|
||||||
"node_modules/minio/**",
|
|
||||||
"node_modules/@octokit/**"
|
|
||||||
],
|
|
||||||
"module": {
|
|
||||||
"type": "commonjs",
|
|
||||||
// These are defaults.
|
|
||||||
"strict": false,
|
|
||||||
"strictMode": true,
|
|
||||||
"lazy": false,
|
|
||||||
"noInterop": false
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ RUN apt install -y --no-install-recommends build-essential
|
|||||||
RUN apt install -y --no-install-recommends git
|
RUN apt install -y --no-install-recommends git
|
||||||
RUN apt install -y --no-install-recommends ssh
|
RUN apt install -y --no-install-recommends ssh
|
||||||
RUN apt install -y --no-install-recommends curl
|
RUN apt install -y --no-install-recommends curl
|
||||||
|
RUN apt install -y --no-install-recommends nscd
|
||||||
RUN apt install -y --no-install-recommends ca-certificates
|
RUN apt install -y --no-install-recommends ca-certificates
|
||||||
RUN apt install -y --no-install-recommends ffmpeg
|
RUN apt install -y --no-install-recommends ffmpeg
|
||||||
|
|
||||||
|
@ -8,17 +8,6 @@ const { Buffer } = require("node:buffer")
|
|||||||
const { webcrypto: crypto } = require("node:crypto")
|
const { webcrypto: crypto } = require("node:crypto")
|
||||||
const { InfisicalClient } = require("@infisical/sdk")
|
const { InfisicalClient } = require("@infisical/sdk")
|
||||||
const moduleAlias = require("module-alias")
|
const moduleAlias = require("module-alias")
|
||||||
const { onExit } = require("signal-exit")
|
|
||||||
const opentelemetry = require("@opentelemetry/sdk-node")
|
|
||||||
const {
|
|
||||||
getNodeAutoInstrumentations,
|
|
||||||
} = require("@opentelemetry/auto-instrumentations-node")
|
|
||||||
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-http")
|
|
||||||
const { OTLPLogExporter } = require("@opentelemetry/exporter-logs-otlp-http")
|
|
||||||
const { Resource } = require("@opentelemetry/resources")
|
|
||||||
const {
|
|
||||||
SemanticResourceAttributes,
|
|
||||||
} = require("@opentelemetry/semantic-conventions")
|
|
||||||
|
|
||||||
// Override file execution arg
|
// Override file execution arg
|
||||||
process.argv.splice(1, 1)
|
process.argv.splice(1, 1)
|
||||||
@ -164,34 +153,12 @@ async function Boot(main) {
|
|||||||
throw new Error("main class is not defined")
|
throw new Error("main class is not defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
const service_id = process.env.lb_service.id
|
const { lb_service_id } = process.env
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[BOOT] Booting (${service_id}) in [${global.isProduction ? "production" : "development"}] mode...`,
|
`[BOOT] Booting in [${global.isProduction ? "production" : "development"}] mode...`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const traceExporter = new OTLPTraceExporter({
|
|
||||||
url:
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
|
|
||||||
"http://fr02.ragestudio.net:4318/v1/traces",
|
|
||||||
})
|
|
||||||
const logExporter = new OTLPLogExporter({
|
|
||||||
url:
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ??
|
|
||||||
"http://fr02.ragestudio.net:4318/v1/logs",
|
|
||||||
})
|
|
||||||
|
|
||||||
const sdk = new opentelemetry.NodeSDK({
|
|
||||||
traceExporter,
|
|
||||||
logExporter,
|
|
||||||
instrumentations: [getNodeAutoInstrumentations()],
|
|
||||||
resource: new Resource({
|
|
||||||
[SemanticResourceAttributes.SERVICE_NAME]: service_id ?? "node_app",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
sdk.start()
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.INFISICAL_CLIENT_ID &&
|
process.env.INFISICAL_CLIENT_ID &&
|
||||||
process.env.INFISICAL_CLIENT_SECRET
|
process.env.INFISICAL_CLIENT_SECRET
|
||||||
@ -204,30 +171,35 @@ async function Boot(main) {
|
|||||||
|
|
||||||
const instance = new main()
|
const instance = new main()
|
||||||
|
|
||||||
onExit(
|
process.on("exit", (code) => {
|
||||||
(code, signal) => {
|
console.log(`[BOOT] Closing...`)
|
||||||
console.log(`[BOOT] Cleaning up...`)
|
|
||||||
|
|
||||||
sdk.shutdown()
|
|
||||||
.then(() => console.log("Tracing terminated"))
|
|
||||||
.catch((error) =>
|
|
||||||
console.log("Error terminating tracing", error),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if (instance._fireClose) {
|
||||||
|
instance._fireClose()
|
||||||
|
} else {
|
||||||
if (typeof instance.onClose === "function") {
|
if (typeof instance.onClose === "function") {
|
||||||
instance.onClose()
|
instance.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.engine.close()
|
instance.engine.close()
|
||||||
},
|
}
|
||||||
{
|
})
|
||||||
alwaysLast: true,
|
|
||||||
},
|
process.on("SIGTERM", () => {
|
||||||
)
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
await instance.initialize()
|
await instance.initialize()
|
||||||
|
|
||||||
if (process.env.lb_service && process.send) {
|
if (lb_service_id && process.send) {
|
||||||
process.send({
|
process.send({
|
||||||
status: "ready",
|
status: "ready",
|
||||||
})
|
})
|
||||||
|
@ -107,8 +107,7 @@ export async function handleChunkFile(
|
|||||||
{ tmpDir, headers, maxFileSize, maxChunkSize },
|
{ tmpDir, headers, maxFileSize, maxChunkSize },
|
||||||
) {
|
) {
|
||||||
return await new Promise(async (resolve, reject) => {
|
return await new Promise(async (resolve, reject) => {
|
||||||
const workPath = path.join(tmpDir, headers["uploader-file-id"])
|
const chunksPath = path.join(tmpDir, "chunks")
|
||||||
const chunksPath = path.join(workPath, "chunks")
|
|
||||||
const chunkPath = path.join(
|
const chunkPath = path.join(
|
||||||
chunksPath,
|
chunksPath,
|
||||||
headers["uploader-chunk-number"],
|
headers["uploader-chunk-number"],
|
||||||
@ -188,7 +187,7 @@ export async function handleChunkFile(
|
|||||||
// build data
|
// build data
|
||||||
chunksPath: chunksPath,
|
chunksPath: chunksPath,
|
||||||
filePath: path.resolve(
|
filePath: path.resolve(
|
||||||
workPath,
|
tmpDir,
|
||||||
`${filename}.${extension}`,
|
`${filename}.${extension}`,
|
||||||
),
|
),
|
||||||
maxFileSize: maxFileSize,
|
maxFileSize: maxFileSize,
|
||||||
@ -207,38 +206,4 @@ export async function handleChunkFile(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadChunkFile(
|
|
||||||
req,
|
|
||||||
{ tmpDir, maxFileSize, maxChunkSize },
|
|
||||||
) {
|
|
||||||
// create a readable stream from req.body data blob
|
|
||||||
//
|
|
||||||
const chunkData = new Blob([req.body], { type: "application/octet-stream" })
|
|
||||||
|
|
||||||
console.log(chunkData)
|
|
||||||
|
|
||||||
if (!checkChunkUploadHeaders(req.headers)) {
|
|
||||||
reject(new OperationError(400, "Missing header(s)"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// return await new Promise(async (resolve, reject) => {
|
|
||||||
// // create a readable node stream from "req.body" (octet-stream)
|
|
||||||
// await req.multipart(async (field) => {
|
|
||||||
// try {
|
|
||||||
// const result = await handleChunkFile(field.file.stream, {
|
|
||||||
// tmpDir: tmpDir,
|
|
||||||
// headers: req.headers,
|
|
||||||
// maxFileSize: maxFileSize,
|
|
||||||
// maxChunkSize: maxChunkSize,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// return resolve(result)
|
|
||||||
// } catch (error) {
|
|
||||||
// return reject(error)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default uploadChunkFile
|
export default uploadChunkFile
|
||||||
|
110
packages/server/classes/FFMPEGLib/index.js
Normal file
110
packages/server/classes/FFMPEGLib/index.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { EventEmitter } from "node:events"
|
||||||
|
import child_process from "node:child_process"
|
||||||
|
|
||||||
|
function getBinaryPath(name) {
|
||||||
|
try {
|
||||||
|
return child_process
|
||||||
|
.execSync(`which ${name}`, { encoding: "utf8" })
|
||||||
|
.trim()
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FFMPEGLib extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.ffmpegBin = getBinaryPath("ffmpeg")
|
||||||
|
this.ffprobeBin = getBinaryPath("ffprobe")
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProgress(stdout, endTime, onProgress = () => {}) {
|
||||||
|
let currentTime = 0
|
||||||
|
|
||||||
|
stdout.on("data", (data) => {
|
||||||
|
for (const line of data.toString().split("\n")) {
|
||||||
|
if (line.startsWith("out_time_ms=")) {
|
||||||
|
currentTime = parseInt(line.split("=")[1]) / 1000000
|
||||||
|
} else if (line.startsWith("progress=")) {
|
||||||
|
const status = line.split("=")[1]
|
||||||
|
|
||||||
|
if (status === "end") {
|
||||||
|
onProgress(100)
|
||||||
|
} else if (endTime > 0 && currentTime > 0) {
|
||||||
|
onProgress(
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round((currentTime / endTime) * 100),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpeg(payload) {
|
||||||
|
return this.exec(this.ffmpegBin, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
ffprobe(payload) {
|
||||||
|
return this.exec(this.ffprobeBin, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(bin, { args, onProcess, cwd }) {
|
||||||
|
if (Array.isArray(args)) {
|
||||||
|
args = args.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const process = child_process.exec(
|
||||||
|
`${bin} ${args}`,
|
||||||
|
{
|
||||||
|
cwd: cwd,
|
||||||
|
},
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(stderr)
|
||||||
|
} else {
|
||||||
|
resolve(stdout.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (typeof onProcess === "function") {
|
||||||
|
onProcess(process)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Utils {
|
||||||
|
static async probe(input) {
|
||||||
|
const lib = new FFMPEGLib()
|
||||||
|
|
||||||
|
const result = await lib
|
||||||
|
.ffprobe({
|
||||||
|
args: [
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-print_format",
|
||||||
|
"json",
|
||||||
|
"-show_format",
|
||||||
|
"-show_streams",
|
||||||
|
input,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FFMPEGLib
|
@ -1,45 +1,41 @@
|
|||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { exec } from "node:child_process"
|
|
||||||
import { EventEmitter } from "node:events"
|
|
||||||
|
|
||||||
export default class MultiqualityHLSJob {
|
import { FFMPEGLib, Utils } from "../FFMPEGLib"
|
||||||
constructor({
|
|
||||||
input,
|
|
||||||
outputDir,
|
|
||||||
outputMasterName = "master.m3u8",
|
|
||||||
levels,
|
|
||||||
}) {
|
|
||||||
this.input = input
|
|
||||||
this.outputDir = outputDir
|
|
||||||
this.levels = levels
|
|
||||||
this.outputMasterName = outputMasterName
|
|
||||||
|
|
||||||
this.bin = require("ffmpeg-static")
|
export default class MultiqualityHLSJob extends FFMPEGLib {
|
||||||
|
constructor(params = {}) {
|
||||||
|
super()
|
||||||
|
|
||||||
return this
|
this.params = {
|
||||||
|
outputMasterName: "master.m3u8",
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
original: true,
|
||||||
|
codec: "libx264",
|
||||||
|
bitrate: "10M",
|
||||||
|
preset: "ultrafast",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...params,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events = new EventEmitter()
|
buildArgs = () => {
|
||||||
|
|
||||||
buildCommand = () => {
|
|
||||||
const cmdStr = [
|
const cmdStr = [
|
||||||
this.bin,
|
`-v error -hide_banner -progress pipe:1`,
|
||||||
`-v quiet -stats`,
|
`-i ${this.params.input}`,
|
||||||
`-i ${this.input}`,
|
|
||||||
`-filter_complex`,
|
`-filter_complex`,
|
||||||
]
|
]
|
||||||
|
|
||||||
// set split args
|
// set split args
|
||||||
let splitLevels = [
|
let splitLevels = [`[0:v]split=${this.params.levels.length}`]
|
||||||
`[0:v]split=${this.levels.length}`
|
|
||||||
]
|
|
||||||
|
|
||||||
this.levels.forEach((level, i) => {
|
this.params.levels.forEach((level, i) => {
|
||||||
splitLevels[0] += (`[v${i + 1}]`)
|
splitLevels[0] += `[v${i + 1}]`
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const [index, level] of this.levels.entries()) {
|
for (const [index, level] of this.params.levels.entries()) {
|
||||||
if (level.original) {
|
if (level.original) {
|
||||||
splitLevels.push(`[v1]copy[v1out]`)
|
splitLevels.push(`[v1]copy[v1out]`)
|
||||||
continue
|
continue
|
||||||
@ -53,7 +49,7 @@ export default class MultiqualityHLSJob {
|
|||||||
cmdStr.push(`"${splitLevels.join(";")}"`)
|
cmdStr.push(`"${splitLevels.join(";")}"`)
|
||||||
|
|
||||||
// set levels map
|
// set levels map
|
||||||
for (const [index, level] of this.levels.entries()) {
|
for (const [index, level] of this.params.levels.entries()) {
|
||||||
let mapArgs = [
|
let mapArgs = [
|
||||||
`-map "[v${index + 1}out]"`,
|
`-map "[v${index + 1}out]"`,
|
||||||
`-x264-params "nal-hrd=cbr:force-cfr=1"`,
|
`-x264-params "nal-hrd=cbr:force-cfr=1"`,
|
||||||
@ -78,13 +74,13 @@ export default class MultiqualityHLSJob {
|
|||||||
cmdStr.push(`-hls_flags independent_segments`)
|
cmdStr.push(`-hls_flags independent_segments`)
|
||||||
cmdStr.push(`-hls_segment_type mpegts`)
|
cmdStr.push(`-hls_segment_type mpegts`)
|
||||||
cmdStr.push(`-hls_segment_filename stream_%v/data%02d.ts`)
|
cmdStr.push(`-hls_segment_filename stream_%v/data%02d.ts`)
|
||||||
cmdStr.push(`-master_pl_name ${this.outputMasterName}`)
|
cmdStr.push(`-master_pl_name ${this.params.outputMasterName}`)
|
||||||
|
|
||||||
cmdStr.push(`-var_stream_map`)
|
cmdStr.push(`-var_stream_map`)
|
||||||
|
|
||||||
let streamMapVar = []
|
let streamMapVar = []
|
||||||
|
|
||||||
for (const [index, level] of this.levels.entries()) {
|
for (const [index, level] of this.params.levels.entries()) {
|
||||||
streamMapVar.push(`v:${index}`)
|
streamMapVar.push(`v:${index}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,54 +90,49 @@ export default class MultiqualityHLSJob {
|
|||||||
return cmdStr.join(" ")
|
return cmdStr.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
run = () => {
|
run = async () => {
|
||||||
const cmdStr = this.buildCommand()
|
const cmdStr = this.buildArgs()
|
||||||
|
|
||||||
console.log(cmdStr)
|
const outputPath =
|
||||||
|
this.params.outputDir ??
|
||||||
|
path.join(path.dirname(this.params.input), "hls")
|
||||||
|
const outputFile = path.join(outputPath, this.params.outputMasterName)
|
||||||
|
|
||||||
const cwd = `${path.dirname(this.input)}/hls`
|
this.emit("start", {
|
||||||
|
input: this.params.input,
|
||||||
if (!fs.existsSync(cwd)) {
|
output: outputPath,
|
||||||
fs.mkdirSync(cwd, { recursive: true })
|
params: this.params,
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[HLS] Started multiquality transcode`, {
|
|
||||||
input: this.input,
|
|
||||||
cwd: cwd,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const process = exec(
|
if (!fs.existsSync(outputPath)) {
|
||||||
cmdStr,
|
fs.mkdirSync(outputPath, { recursive: true })
|
||||||
{
|
}
|
||||||
cwd: cwd,
|
|
||||||
|
const inputProbe = await Utils.probe(this.params.input)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ffmpeg({
|
||||||
|
args: cmdStr,
|
||||||
|
cwd: outputPath,
|
||||||
|
onProcess: (process) => {
|
||||||
|
this.handleProgress(
|
||||||
|
process.stdout,
|
||||||
|
parseFloat(inputProbe.format.duration),
|
||||||
|
(progress) => {
|
||||||
|
this.emit("progress", progress)
|
||||||
},
|
},
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.log(`[HLS] Failed to transcode >`, error)
|
|
||||||
|
|
||||||
return this.events.emit("error", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
//return this.events.emit("error", stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[HLS] Finished transcode >`, cwd)
|
|
||||||
|
|
||||||
return this.events.emit("end", {
|
|
||||||
filepath: path.join(cwd, this.outputMasterName),
|
|
||||||
isDirectory: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
process.stdout.on("data", (data) => {
|
|
||||||
console.log(data.toString())
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
on = (key, cb) => {
|
this.emit("end", {
|
||||||
this.events.on(key, cb)
|
outputPath: outputPath,
|
||||||
return this
|
outputFile: outputFile,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
return this.emit("error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,6 @@
|
|||||||
import Redis from "ioredis"
|
import Redis from "ioredis"
|
||||||
|
|
||||||
export function composeURL({
|
export function composeURL({ host, port, username, password } = {}) {
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
} = {}) {
|
|
||||||
let url = "redis://"
|
let url = "redis://"
|
||||||
|
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
@ -21,36 +16,34 @@ export function composeURL({
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default (params = {}) => {
|
||||||
let { REDIS_HOST, REDIS_PORT, REDIS_NO_AUTH, REDIS_AUTH, REDIS_DB } = process.env
|
let { REDIS_HOST, REDIS_PORT, REDIS_NO_AUTH, REDIS_AUTH, REDIS_DB } =
|
||||||
|
process.env
|
||||||
REDIS_NO_AUTH = ToBoolean(REDIS_NO_AUTH)
|
|
||||||
|
|
||||||
let clientOptions = {
|
let clientOptions = {
|
||||||
host: REDIS_HOST,
|
host: REDIS_HOST ?? "localhost",
|
||||||
port: REDIS_PORT,
|
port: REDIS_PORT ?? 6379,
|
||||||
lazyConnect: true,
|
lazyConnect: true,
|
||||||
autoConnect: false
|
autoConnect: false,
|
||||||
|
...params,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!REDIS_NO_AUTH) {
|
// if redis auth is provided, set username and password
|
||||||
if (REDIS_AUTH) {
|
if (!ToBoolean(REDIS_NO_AUTH) && REDIS_AUTH) {
|
||||||
const [user, password] = REDIS_AUTH.split(":")
|
const [user, password] = REDIS_AUTH.split(":")
|
||||||
|
|
||||||
clientOptions.username = user
|
clientOptions.username = user
|
||||||
clientOptions.password = password
|
clientOptions.password = password
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log("⚠️ Redis auth is disabled")
|
console.log("⚠️ Redis auth is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if redis db is provided, set db
|
||||||
if (REDIS_DB) {
|
if (REDIS_DB) {
|
||||||
clientOptions.db = REDIS_DB
|
clientOptions.db = REDIS_DB
|
||||||
}
|
}
|
||||||
|
|
||||||
clientOptions = composeURL(clientOptions)
|
let client = new Redis(clientOptions)
|
||||||
|
|
||||||
let client = new Redis(clientOptions.host, clientOptions.port, clientOptions)
|
|
||||||
|
|
||||||
client.on("error", (error) => {
|
client.on("error", (error) => {
|
||||||
console.error("❌ Redis client error:", error)
|
console.error("❌ Redis client error:", error)
|
||||||
@ -74,6 +67,6 @@ export default () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
client,
|
client,
|
||||||
initialize
|
initialize,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,112 +1,108 @@
|
|||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { exec } from "node:child_process"
|
|
||||||
import { EventEmitter } from "node:events"
|
|
||||||
|
|
||||||
export default class SegmentedAudioMPDJob {
|
import { FFMPEGLib, Utils } from "../FFMPEGLib"
|
||||||
constructor({
|
|
||||||
input,
|
|
||||||
outputDir,
|
|
||||||
outputMasterName = "master.mpd",
|
|
||||||
|
|
||||||
audioCodec = "aac",
|
export default class SegmentedAudioMPDJob extends FFMPEGLib {
|
||||||
audioBitrate = undefined,
|
constructor(params = {}) {
|
||||||
audioSampleRate = undefined,
|
super()
|
||||||
segmentTime = 10,
|
|
||||||
}) {
|
|
||||||
this.input = input
|
|
||||||
this.outputDir = outputDir
|
|
||||||
this.outputMasterName = outputMasterName
|
|
||||||
|
|
||||||
this.audioCodec = audioCodec
|
this.params = {
|
||||||
this.audioBitrate = audioBitrate
|
outputMasterName: "master.mpd",
|
||||||
this.segmentTime = segmentTime
|
audioCodec: "libopus",
|
||||||
this.audioSampleRate = audioSampleRate
|
audioBitrate: "320k",
|
||||||
|
audioSampleRate: "48000",
|
||||||
this.bin = require("ffmpeg-static")
|
segmentTime: 10,
|
||||||
|
includeMetadata: true,
|
||||||
return this
|
...params,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events = new EventEmitter()
|
buildSegmentationArgs = () => {
|
||||||
|
const args = [
|
||||||
buildCommand = () => {
|
//`-threads 1`, // limits to one thread
|
||||||
const cmdStr = [
|
`-v error -hide_banner -progress pipe:1`,
|
||||||
this.bin,
|
`-i ${this.params.input}`,
|
||||||
`-v quiet -stats`,
|
`-c:a ${this.params.audioCodec}`,
|
||||||
`-i ${this.input}`,
|
|
||||||
`-c:a ${this.audioCodec}`,
|
|
||||||
`-map 0:a`,
|
`-map 0:a`,
|
||||||
`-map_metadata -1`,
|
|
||||||
`-f dash`,
|
`-f dash`,
|
||||||
`-dash_segment_type mp4`,
|
`-dash_segment_type mp4`,
|
||||||
`-segment_time ${this.segmentTime}`,
|
`-segment_time ${this.params.segmentTime}`,
|
||||||
`-use_template 1`,
|
`-use_template 1`,
|
||||||
`-use_timeline 1`,
|
`-use_timeline 1`,
|
||||||
`-init_seg_name "init.m4s"`,
|
`-init_seg_name "init.m4s"`,
|
||||||
]
|
]
|
||||||
|
|
||||||
if (typeof this.audioBitrate !== "undefined") {
|
if (this.params.includeMetadata === false) {
|
||||||
cmdStr.push(`-b:a ${this.audioBitrate}`)
|
args.push(`-map_metadata -1`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof this.audioSampleRate !== "undefined") {
|
if (
|
||||||
cmdStr.push(`-ar ${this.audioSampleRate}`)
|
typeof this.params.audioBitrate === "string" &&
|
||||||
|
this.params.audioBitrate !== "default"
|
||||||
|
) {
|
||||||
|
args.push(`-b:a ${this.params.audioBitrate}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdStr.push(this.outputMasterName)
|
if (
|
||||||
|
typeof this.params.audioSampleRate !== "undefined" &&
|
||||||
return cmdStr.join(" ")
|
this.params.audioSampleRate !== "default"
|
||||||
|
) {
|
||||||
|
args.push(`-ar ${this.params.audioSampleRate}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
run = () => {
|
args.push(this.params.outputMasterName)
|
||||||
const cmdStr = this.buildCommand()
|
|
||||||
|
|
||||||
console.log(cmdStr)
|
return args
|
||||||
|
|
||||||
const cwd = `${path.dirname(this.input)}/dash`
|
|
||||||
|
|
||||||
if (!fs.existsSync(cwd)) {
|
|
||||||
fs.mkdirSync(cwd, { recursive: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DASH] Started audio segmentation`, {
|
run = async () => {
|
||||||
input: this.input,
|
const segmentationCmd = this.buildSegmentationArgs()
|
||||||
cwd: cwd,
|
const outputPath =
|
||||||
|
this.params.outputDir ?? `${path.dirname(this.params.input)}/dash`
|
||||||
|
const outputFile = path.join(outputPath, this.params.outputMasterName)
|
||||||
|
|
||||||
|
this.emit("start", {
|
||||||
|
input: this.params.input,
|
||||||
|
output: outputPath,
|
||||||
|
params: this.params,
|
||||||
})
|
})
|
||||||
|
|
||||||
const process = exec(
|
if (!fs.existsSync(outputPath)) {
|
||||||
cmdStr,
|
fs.mkdirSync(outputPath, { recursive: true })
|
||||||
{
|
}
|
||||||
cwd: cwd,
|
|
||||||
|
const inputProbe = await Utils.probe(this.params.input)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ffmpeg({
|
||||||
|
args: segmentationCmd,
|
||||||
|
onProcess: (process) => {
|
||||||
|
this.handleProgress(
|
||||||
|
process.stdout,
|
||||||
|
parseFloat(inputProbe.format.duration),
|
||||||
|
(progress) => {
|
||||||
|
this.emit("progress", progress)
|
||||||
},
|
},
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.log(`[DASH] Failed to segment audio >`, error)
|
|
||||||
|
|
||||||
return this.events.emit("error", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
//return this.events.emit("error", stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DASH] Finished segmenting audio >`, cwd)
|
|
||||||
|
|
||||||
return this.events.emit("end", {
|
|
||||||
filepath: path.join(cwd, this.outputMasterName),
|
|
||||||
isDirectory: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
process.stdout.on("data", (data) => {
|
cwd: outputPath,
|
||||||
console.log(data.toString())
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
on = (key, cb) => {
|
let outputProbe = await Utils.probe(outputFile)
|
||||||
this.events.on(key, cb)
|
|
||||||
return this
|
this.emit("end", {
|
||||||
|
probe: {
|
||||||
|
input: inputProbe,
|
||||||
|
output: outputProbe,
|
||||||
|
},
|
||||||
|
outputPath: outputPath,
|
||||||
|
outputFile: outputFile,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
return this.emit("error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
const Minio = require("minio")
|
import path from "node:path"
|
||||||
import path from "path"
|
import { Client } from "minio"
|
||||||
|
|
||||||
export const generateDefaultBucketPolicy = (payload) => {
|
export const generateDefaultBucketPolicy = (payload) => {
|
||||||
const { bucketName } = payload
|
const { bucketName } = payload
|
||||||
@ -12,30 +12,26 @@ export const generateDefaultBucketPolicy = (payload) => {
|
|||||||
Version: "2012-10-17",
|
Version: "2012-10-17",
|
||||||
Statement: [
|
Statement: [
|
||||||
{
|
{
|
||||||
Action: [
|
Action: ["s3:GetObject"],
|
||||||
"s3:GetObject"
|
|
||||||
],
|
|
||||||
Effect: "Allow",
|
Effect: "Allow",
|
||||||
Principal: {
|
Principal: {
|
||||||
AWS: [
|
AWS: ["*"],
|
||||||
"*"
|
},
|
||||||
]
|
Resource: [`arn:aws:s3:::${bucketName}/*`],
|
||||||
|
Sid: "",
|
||||||
},
|
},
|
||||||
Resource: [
|
|
||||||
`arn:aws:s3:::${bucketName}/*`
|
|
||||||
],
|
],
|
||||||
Sid: ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorageClient extends Minio.Client {
|
export class StorageClient extends Client {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options)
|
super(options)
|
||||||
|
|
||||||
this.defaultBucket = String(options.defaultBucket)
|
this.defaultBucket = String(options.defaultBucket)
|
||||||
this.defaultRegion = String(options.defaultRegion)
|
this.defaultRegion = String(options.defaultRegion)
|
||||||
|
this.setupBucket = Boolean(options.setupBucket)
|
||||||
|
this.cdnUrl = options.cdnUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
composeRemoteURL = (key, extraKey) => {
|
composeRemoteURL = (key, extraKey) => {
|
||||||
@ -45,6 +41,10 @@ export class StorageClient extends Minio.Client {
|
|||||||
_path = path.join(_path, extraKey)
|
_path = path.join(_path, extraKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.cdnUrl) {
|
||||||
|
return `${this.cdnUrl}/${_path}`
|
||||||
|
}
|
||||||
|
|
||||||
return `${this.protocol}//${this.host}:${this.port}/${_path}`
|
return `${this.protocol}//${this.host}:${this.port}/${_path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,11 +57,14 @@ export class StorageClient extends Minio.Client {
|
|||||||
initialize = async () => {
|
initialize = async () => {
|
||||||
console.log("🔌 Checking if storage client have default bucket...")
|
console.log("🔌 Checking if storage client have default bucket...")
|
||||||
|
|
||||||
|
if (this.setupBucket !== false) {
|
||||||
try {
|
try {
|
||||||
const bucketExists = await this.bucketExists(this.defaultBucket)
|
const bucketExists = await this.bucketExists(this.defaultBucket)
|
||||||
|
|
||||||
if (!bucketExists) {
|
if (!bucketExists) {
|
||||||
console.warn("🪣 Default bucket not exists! Creating new bucket...")
|
console.warn(
|
||||||
|
"🪣 Default bucket not exists! Creating new bucket...",
|
||||||
|
)
|
||||||
|
|
||||||
await this.makeBucket(this.defaultBucket, "s3")
|
await this.makeBucket(this.defaultBucket, "s3")
|
||||||
|
|
||||||
@ -69,12 +72,17 @@ export class StorageClient extends Minio.Client {
|
|||||||
await this.setDefaultBucketPolicy(this.defaultBucket)
|
await this.setDefaultBucketPolicy(this.defaultBucket)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to check if default bucket exists or create default bucket >`, error)
|
console.error(
|
||||||
|
`Failed to check if default bucket exists or create default bucket >`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// check if default bucket policy exists
|
// check if default bucket policy exists
|
||||||
const bucketPolicy = await this.getBucketPolicy(this.defaultBucket).catch(() => {
|
const bucketPolicy = await this.getBucketPolicy(
|
||||||
|
this.defaultBucket,
|
||||||
|
).catch(() => {
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -83,7 +91,11 @@ export class StorageClient extends Minio.Client {
|
|||||||
await this.setDefaultBucketPolicy(this.defaultBucket)
|
await this.setDefaultBucketPolicy(this.defaultBucket)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get or set default bucket policy >`, error)
|
console.error(
|
||||||
|
`Failed to get or set default bucket policy >`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ Storage client is ready.")
|
console.log("✅ Storage client is ready.")
|
||||||
|
@ -56,14 +56,8 @@ export default class TaskQueueManager {
|
|||||||
registerQueueEvents = (worker) => {
|
registerQueueEvents = (worker) => {
|
||||||
worker.on("progress", (job, progress) => {
|
worker.on("progress", (job, progress) => {
|
||||||
try {
|
try {
|
||||||
console.log(`Job ${job.id} reported progress: ${progress}%`)
|
|
||||||
|
|
||||||
if (job.data.sseChannelId) {
|
if (job.data.sseChannelId) {
|
||||||
global.sse.sendToChannel(job.data.sseChannelId, {
|
global.sse.sendToChannel(job.data.sseChannelId, progress)
|
||||||
status: "progress",
|
|
||||||
events: "job_progress",
|
|
||||||
progress,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// manejar error
|
// manejar error
|
||||||
@ -76,8 +70,9 @@ export default class TaskQueueManager {
|
|||||||
|
|
||||||
if (job.data.sseChannelId) {
|
if (job.data.sseChannelId) {
|
||||||
global.sse.sendToChannel(job.data.sseChannelId, {
|
global.sse.sendToChannel(job.data.sseChannelId, {
|
||||||
status: "done",
|
event: "done",
|
||||||
result,
|
state: "done",
|
||||||
|
result: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
@ -89,7 +84,8 @@ export default class TaskQueueManager {
|
|||||||
|
|
||||||
if (job.data.sseChannelId) {
|
if (job.data.sseChannelId) {
|
||||||
global.sse.sendToChannel(job.data.sseChannelId, {
|
global.sse.sendToChannel(job.data.sseChannelId, {
|
||||||
status: "error",
|
event: "error",
|
||||||
|
state: "error",
|
||||||
result: error.message,
|
result: error.message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -122,9 +118,9 @@ export default class TaskQueueManager {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await global.sse.sendToChannel(sseChannelId, {
|
await global.sse.sendToChannel(sseChannelId, {
|
||||||
status: "progress",
|
event: "job_queued",
|
||||||
events: "job_queued",
|
state: "progress",
|
||||||
progress: 5,
|
percent: 5,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
33
packages/server/classes/Transformation/handlers/a-dash.js
Normal file
33
packages/server/classes/Transformation/handlers/a-dash.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import SegmentedAudioMPDJob from "@shared-classes/SegmentedAudioMPDJob"
|
||||||
|
|
||||||
|
export default async ({ filePath, workPath, onProgress }) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const outputDir = path.resolve(workPath, "a-dash")
|
||||||
|
|
||||||
|
const job = new SegmentedAudioMPDJob({
|
||||||
|
input: filePath,
|
||||||
|
outputDir: outputDir,
|
||||||
|
|
||||||
|
// set to default as raw flac
|
||||||
|
audioCodec: "flac",
|
||||||
|
audioBitrate: "default",
|
||||||
|
audioSampleRate: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
job.on("end", (data) => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
job.on("progress", (progress) => {
|
||||||
|
if (typeof onProgress === "function") {
|
||||||
|
onProgress({
|
||||||
|
percent: progress,
|
||||||
|
state: "transmuxing",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
job.run()
|
||||||
|
})
|
||||||
|
}
|
45
packages/server/classes/Transformation/handlers/mq-hls.js
Normal file
45
packages/server/classes/Transformation/handlers/mq-hls.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import MultiqualityHLSJob from "@shared-classes/MultiqualityHLSJob"
|
||||||
|
|
||||||
|
export default async ({ filePath, workPath, onProgress }) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const outputDir = path.resolve(workPath, "mqhls")
|
||||||
|
|
||||||
|
const job = new MultiqualityHLSJob({
|
||||||
|
input: filePath,
|
||||||
|
outputDir: outputDir,
|
||||||
|
|
||||||
|
// set default
|
||||||
|
outputMasterName: "master.m3u8",
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
original: true,
|
||||||
|
codec: "libx264",
|
||||||
|
bitrate: "10M",
|
||||||
|
preset: "ultrafast",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codec: "libx264",
|
||||||
|
width: 1280,
|
||||||
|
bitrate: "3M",
|
||||||
|
preset: "ultrafast",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
job.on("end", (data) => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
job.on("progress", (progress) => {
|
||||||
|
if (typeof onProgress === "function") {
|
||||||
|
onProgress({
|
||||||
|
percent: progress,
|
||||||
|
state: "transmuxing",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
job.run()
|
||||||
|
})
|
||||||
|
}
|
63
packages/server/classes/Transformation/handlers/optimize.js
Normal file
63
packages/server/classes/Transformation/handlers/optimize.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import { fileTypeFromBuffer } from "file-type"
|
||||||
|
|
||||||
|
import readChunk from "@shared-utils/readChunk"
|
||||||
|
|
||||||
|
import Sharp from "sharp"
|
||||||
|
|
||||||
|
const thresholds = {
|
||||||
|
size: 10 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharpConfigs = {
|
||||||
|
png: {
|
||||||
|
compressionLevel: 6,
|
||||||
|
//quality: 80,
|
||||||
|
},
|
||||||
|
jpeg: {
|
||||||
|
quality: 80,
|
||||||
|
mozjpeg: true,
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
quality: 80,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async ({ filePath, workPath, onProgress }) => {
|
||||||
|
const stat = await fs.promises.stat(filePath)
|
||||||
|
|
||||||
|
const firstBuffer = await readChunk(filePath, {
|
||||||
|
length: 4100,
|
||||||
|
})
|
||||||
|
const fileType = await fileTypeFromBuffer(firstBuffer)
|
||||||
|
|
||||||
|
// first check if size over threshold
|
||||||
|
if (stat.size < thresholds.size) {
|
||||||
|
return {
|
||||||
|
outputFile: filePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the type of the file mime
|
||||||
|
const type = fileType.mime.split("/")[0]
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "image": {
|
||||||
|
let image = Sharp(filePath)
|
||||||
|
|
||||||
|
const metadata = await image.metadata()
|
||||||
|
const config = sharpConfigs[metadata.format] ?? sharpConfigs.default
|
||||||
|
|
||||||
|
image = await image[metadata.format](config).withMetadata()
|
||||||
|
|
||||||
|
filePath = path.resolve(workPath, `${path.basename(filePath)}_ff`)
|
||||||
|
|
||||||
|
await image.toFile(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
outputFile: filePath,
|
||||||
|
}
|
||||||
|
}
|
26
packages/server/classes/Transformation/index.ts
Normal file
26
packages/server/classes/Transformation/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const Handlers = {
|
||||||
|
"a-dash": require("./handlers/a-dash").default,
|
||||||
|
"mq-hls": require("./handlers/mq-hls").default,
|
||||||
|
optimize: require("./handlers/optimize").default,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransformationPayloadType = {
|
||||||
|
filePath: string
|
||||||
|
workPath: string
|
||||||
|
handler: string
|
||||||
|
onProgress?: function
|
||||||
|
}
|
||||||
|
|
||||||
|
class Transformation {
|
||||||
|
static async transform(payload: TransformationPayloadType) {
|
||||||
|
const handler = Handlers[payload.handler]
|
||||||
|
|
||||||
|
if (typeof handler !== "function") {
|
||||||
|
throw new Error(`Invalid handler: ${payload.handler}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await handler(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Transformation
|
154
packages/server/classes/Upload/index.ts
Normal file
154
packages/server/classes/Upload/index.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import { fileTypeFromBuffer } from "file-type"
|
||||||
|
|
||||||
|
import readChunk from "@shared-utils/readChunk"
|
||||||
|
import getFileHash from "@shared-utils/readFileHash"
|
||||||
|
|
||||||
|
import putObject from "./putObject"
|
||||||
|
import Transformation from "../Transformation"
|
||||||
|
|
||||||
|
export type FileHandlePayload = {
|
||||||
|
user_id: string
|
||||||
|
filePath: string
|
||||||
|
workPath: string
|
||||||
|
targetPath?: string // mostly provided by processed results
|
||||||
|
//uploadId?: string
|
||||||
|
transformations?: Array<string>
|
||||||
|
useCompression?: boolean
|
||||||
|
s3Provider?: string
|
||||||
|
onProgress?: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export type S3UploadPayload = {
|
||||||
|
filePath: string
|
||||||
|
basePath: string
|
||||||
|
targetPath?: string
|
||||||
|
s3Provider?: string
|
||||||
|
onProgress?: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Upload {
|
||||||
|
static fileHandle = async (payload: FileHandlePayload) => {
|
||||||
|
if (!payload.transformations) {
|
||||||
|
payload.transformations = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// if compression is enabled and no transformations are provided, add basic transformations for images or videos
|
||||||
|
if (
|
||||||
|
payload.useCompression === true &&
|
||||||
|
payload.transformations.length === 0
|
||||||
|
) {
|
||||||
|
payload.transformations.push("optimize")
|
||||||
|
}
|
||||||
|
|
||||||
|
// process file upload if transformations are provided
|
||||||
|
if (payload.transformations.length > 0) {
|
||||||
|
// process
|
||||||
|
const processed = await Upload.transform(payload)
|
||||||
|
|
||||||
|
// overwrite filePath
|
||||||
|
payload.filePath = processed.filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload
|
||||||
|
const result = await Upload.toS3({
|
||||||
|
filePath: payload.filePath,
|
||||||
|
targetPath: payload.targetPath,
|
||||||
|
basePath: payload.user_id,
|
||||||
|
onProgress: payload.onProgress,
|
||||||
|
s3Provider: payload.s3Provider,
|
||||||
|
})
|
||||||
|
|
||||||
|
// delete workpath
|
||||||
|
await fs.promises.rm(payload.workPath, { recursive: true, force: true })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static transform = async (payload: FileHandlePayload) => {
|
||||||
|
if (Array.isArray(payload.transformations)) {
|
||||||
|
for await (const transformation of payload.transformations) {
|
||||||
|
const transformationResult = await Transformation.transform({
|
||||||
|
filePath: payload.filePath,
|
||||||
|
workPath: payload.workPath,
|
||||||
|
onProgress: payload.onProgress,
|
||||||
|
handler: transformation,
|
||||||
|
})
|
||||||
|
|
||||||
|
// if is a file, overwrite filePath
|
||||||
|
if (transformationResult.outputFile) {
|
||||||
|
payload.filePath = transformationResult.outputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// if is a directory, overwrite filePath to upload entire directory
|
||||||
|
if (transformationResult.outputPath) {
|
||||||
|
payload.filePath = transformationResult.outputPath
|
||||||
|
payload.targetPath = transformationResult.outputFile
|
||||||
|
//payload.isDirectory = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
static toS3 = async (payload: S3UploadPayload) => {
|
||||||
|
const { filePath, basePath, targetPath, s3Provider, onProgress } =
|
||||||
|
payload
|
||||||
|
|
||||||
|
// if targetPath is provided, means its a directory
|
||||||
|
const isDirectory = !!targetPath
|
||||||
|
|
||||||
|
const metadata = await this.buildFileMetadata(
|
||||||
|
isDirectory ? targetPath : filePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
let uploadPath = path.join(basePath, metadata["File-Hash"])
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
uploadPath = path.join(basePath, global.nanoid())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onProgress === "function") {
|
||||||
|
onProgress({
|
||||||
|
percent: 0,
|
||||||
|
state: "uploading_s3",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("Uploading to S3:", {
|
||||||
|
// filePath: filePath,
|
||||||
|
// basePath: basePath,
|
||||||
|
// uploadPath: uploadPath,
|
||||||
|
// targetPath: targetPath,
|
||||||
|
// metadata: metadata,
|
||||||
|
// s3Provider: s3Provider,
|
||||||
|
// })
|
||||||
|
|
||||||
|
const result = await putObject({
|
||||||
|
filePath: filePath,
|
||||||
|
uploadPath: uploadPath,
|
||||||
|
metadata: metadata,
|
||||||
|
targetFilename: isDirectory ? path.basename(targetPath) : null,
|
||||||
|
provider: s3Provider,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static async buildFileMetadata(filePath: string) {
|
||||||
|
const firstBuffer = await readChunk(filePath, {
|
||||||
|
length: 4100,
|
||||||
|
})
|
||||||
|
const fileHash = await getFileHash(fs.createReadStream(filePath))
|
||||||
|
const fileType = await fileTypeFromBuffer(firstBuffer)
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
"File-Hash": fileHash,
|
||||||
|
"Content-Type": fileType?.mime ?? "application/octet-stream",
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
}
|
65
packages/server/classes/Upload/putObject.js
Normal file
65
packages/server/classes/Upload/putObject.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import pMap from "p-map"
|
||||||
|
|
||||||
|
export default async function putObject({
|
||||||
|
filePath,
|
||||||
|
uploadPath,
|
||||||
|
metadata = {},
|
||||||
|
targetFilename,
|
||||||
|
onFinish,
|
||||||
|
provider = "standard",
|
||||||
|
}) {
|
||||||
|
const providerClass = global.storages[provider]
|
||||||
|
|
||||||
|
if (!providerClass) {
|
||||||
|
throw new Error(`Provider [${provider}] not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirectory = await fs.promises
|
||||||
|
.lstat(filePath)
|
||||||
|
.then((stats) => stats.isDirectory())
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
let files = await fs.promises.readdir(filePath)
|
||||||
|
|
||||||
|
files = files.map((file) => {
|
||||||
|
const newPath = path.join(filePath, file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath: newPath,
|
||||||
|
uploadPath: path.join(uploadPath, file),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await pMap(files, putObject, {
|
||||||
|
concurrency: 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uploadPath,
|
||||||
|
url: providerClass.composeRemoteURL(uploadPath, targetFilename),
|
||||||
|
metadata: metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload to storage
|
||||||
|
await providerClass.fPutObject(
|
||||||
|
process.env.S3_BUCKET,
|
||||||
|
uploadPath,
|
||||||
|
filePath,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
id: uploadPath,
|
||||||
|
url: providerClass.composeRemoteURL(uploadPath),
|
||||||
|
metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onFinish === "function") {
|
||||||
|
await onFinish(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
@ -4,32 +4,33 @@ export default {
|
|||||||
schema: {
|
schema: {
|
||||||
user_id: {
|
user_id: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
list: {
|
items: {
|
||||||
type: Object,
|
type: Array,
|
||||||
default: [],
|
default: [],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
cover: {
|
cover: {
|
||||||
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",
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
public: {
|
public: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
@ -13,8 +13,8 @@ export default {
|
|||||||
album: {
|
album: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
artists: {
|
artist: {
|
||||||
type: Array,
|
type: String,
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -32,15 +32,12 @@ export default {
|
|||||||
},
|
},
|
||||||
cover: {
|
cover: {
|
||||||
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",
|
||||||
},
|
},
|
||||||
publisher: {
|
publisher: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
lyrics_enabled: {
|
},
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -4,66 +4,65 @@ export default {
|
|||||||
schema: {
|
schema: {
|
||||||
username: {
|
username: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
public_name: {
|
public_name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
cover: {
|
cover: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
type:
|
type: String,
|
||||||
String,
|
default: null,
|
||||||
default: null
|
|
||||||
},
|
},
|
||||||
roles: {
|
roles: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: []
|
default: [],
|
||||||
},
|
},
|
||||||
verified: {
|
verified: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
badges: {
|
badges: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: []
|
default: [],
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: []
|
default: [],
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
birthday: {
|
birthday: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: null,
|
default: null,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accept_tos: {
|
accept_tos: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
activated: {
|
activated: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -71,7 +70,7 @@ export default {
|
|||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
}
|
2
packages/server/dev.sh
Executable file
2
packages/server/dev.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
node --run dev
|
@ -181,19 +181,11 @@ export default class Gateway {
|
|||||||
serviceId: id,
|
serviceId: id,
|
||||||
path: path,
|
path: path,
|
||||||
target: `${http.proto}://${listen.ip}:${listen.port}${path}`,
|
target: `${http.proto}://${listen.ip}:${listen.port}${path}`,
|
||||||
|
websocket: !!websocket,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (websocket && websocket.enabled === true) {
|
|
||||||
await this.gateway.register({
|
|
||||||
serviceId: id,
|
|
||||||
websocket: true,
|
|
||||||
path: websocket.path,
|
|
||||||
target: `${http.proto}://${listen.ip}:${listen.port}${websocket.path}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.allReady) {
|
if (this.state.allReady) {
|
||||||
if (typeof this.gateway.applyConfiguration === "function") {
|
if (typeof this.gateway.applyConfiguration === "function") {
|
||||||
await this.gateway.applyConfiguration()
|
await this.gateway.applyConfiguration()
|
||||||
|
@ -6,6 +6,7 @@ import defaults from "linebridge/dist/defaults"
|
|||||||
|
|
||||||
const localNginxBinary = path.resolve(process.cwd(), "nginx-bin")
|
const localNginxBinary = path.resolve(process.cwd(), "nginx-bin")
|
||||||
const serverPkg = require("../../../package.json")
|
const serverPkg = require("../../../package.json")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NginxManager - Optimized version that batches configurations
|
* NginxManager - Optimized version that batches configurations
|
||||||
* Waits for all services to register before applying configuration
|
* Waits for all services to register before applying configuration
|
||||||
@ -253,7 +254,7 @@ http {
|
|||||||
|
|
||||||
if (debugFlag) {
|
if (debugFlag) {
|
||||||
console.log(
|
console.log(
|
||||||
`🔍 Registering route for [${serviceId}]: ${normalizedPath} -> ${target} (${websocket ? "WebSocket" : "HTTP"})`,
|
`🔍 Registering route for [${serviceId}]: ${normalizedPath} -> ${target}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,8 +262,8 @@ http {
|
|||||||
const effectivePathRewrite = pathRewrite || {}
|
const effectivePathRewrite = pathRewrite || {}
|
||||||
|
|
||||||
this.routes.set(normalizedPath, {
|
this.routes.set(normalizedPath, {
|
||||||
serviceId,
|
serviceId: serviceId,
|
||||||
target,
|
target: target,
|
||||||
pathRewrite: effectivePathRewrite,
|
pathRewrite: effectivePathRewrite,
|
||||||
websocket: !!websocket,
|
websocket: !!websocket,
|
||||||
})
|
})
|
||||||
|
@ -55,7 +55,8 @@ export default class Service {
|
|||||||
|
|
||||||
this.instance = await spawnService({
|
this.instance = await spawnService({
|
||||||
id: this.id,
|
id: this.id,
|
||||||
service: this.path,
|
service: this,
|
||||||
|
path: this.path,
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
onClose: this.handleClose.bind(this),
|
onClose: this.handleClose.bind(this),
|
||||||
onError: this.handleError.bind(this),
|
onError: this.handleError.bind(this),
|
||||||
@ -140,8 +141,7 @@ export default class Service {
|
|||||||
|
|
||||||
// Kill the current process if is running
|
// Kill the current process if is running
|
||||||
if (this.instance.exitCode === null) {
|
if (this.instance.exitCode === null) {
|
||||||
console.log(`[${this.id}] Killing current process...`)
|
await this.instance.kill()
|
||||||
await this.instance.kill("SIGKILL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new process
|
// Start a new process
|
||||||
@ -153,17 +153,13 @@ export default class Service {
|
|||||||
/**
|
/**
|
||||||
* Stop the service
|
* Stop the service
|
||||||
*/
|
*/
|
||||||
async stop() {
|
stop() {
|
||||||
console.log(`[${this.id}] Stopping service...`)
|
console.log(`[${this.id}] Stopping service...`)
|
||||||
|
|
||||||
if (this.fileWatcher) {
|
this.instance.kill()
|
||||||
await this.fileWatcher.close()
|
|
||||||
this.fileWatcher = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.instance) {
|
if (this.fileWatcher) {
|
||||||
await this.instance.kill("SIGKILL")
|
this.fileWatcher.close()
|
||||||
this.instance = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,21 +3,29 @@ import createServiceLogTransformer from "./createServiceLogTransformer"
|
|||||||
|
|
||||||
import Vars from "../vars"
|
import Vars from "../vars"
|
||||||
|
|
||||||
export default async ({ id, service, cwd, onClose, onError, onIPCData }) => {
|
export default async ({
|
||||||
|
id,
|
||||||
|
service,
|
||||||
|
path,
|
||||||
|
cwd,
|
||||||
|
onClose,
|
||||||
|
onError,
|
||||||
|
onIPCData,
|
||||||
|
}) => {
|
||||||
const instanceEnv = {
|
const instanceEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
lb_service: {
|
lb_service_id: service.id,
|
||||||
id: service.id,
|
lb_service_path: service.path,
|
||||||
index: service.index,
|
lb_service_version: service.version,
|
||||||
},
|
lb_service_cwd: service.cwd,
|
||||||
|
lb_service: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
let instance = ChildProcess.fork(Vars.bootloaderBin, [service], {
|
let instance = ChildProcess.fork(Vars.bootloaderBin, [path], {
|
||||||
detached: false,
|
detached: false,
|
||||||
silent: true,
|
silent: true,
|
||||||
cwd: cwd,
|
cwd: cwd,
|
||||||
env: instanceEnv,
|
env: instanceEnv,
|
||||||
killSignal: "SIGTERM",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.logs = {
|
instance.logs = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@comty/server",
|
"name": "@comty/server",
|
||||||
"version": "1.31.0@alpha",
|
"version": "1.38.0@alpha",
|
||||||
"license": "ComtyLicense",
|
"license": "ComtyLicense",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
165
packages/server/scripts/installLatestFfmpeg.sh
Executable file
165
packages/server/scripts/installLatestFfmpeg.sh
Executable file
@ -0,0 +1,165 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
detect_os() {
|
||||||
|
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||||
|
echo "linux"
|
||||||
|
else
|
||||||
|
echo "unsupported"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_arch() {
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
if [[ "$ARCH" == "x86_64" ]]; then
|
||||||
|
echo "amd64"
|
||||||
|
elif [[ "$ARCH" == "aarch64" ]] || [[ "$ARCH" == "arm64" ]]; then
|
||||||
|
echo "arm64"
|
||||||
|
elif [[ "$ARCH" == "armv7l" ]]; then
|
||||||
|
echo "armhf"
|
||||||
|
else
|
||||||
|
echo "unsupported"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
OS=$(detect_os)
|
||||||
|
ARCH=$(detect_arch)
|
||||||
|
|
||||||
|
if [[ "$OS" == "unsupported" ]] || [[ "$ARCH" == "unsupported" ]]; then
|
||||||
|
echo -e "${RED}Operating system or architecture not supported. This script only supports Linux on amd64, arm64, or armhf architectures.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTALL_DIR="$HOME/.local/bin"
|
||||||
|
TEMP_DIR="/tmp/ffmpegdl"
|
||||||
|
|
||||||
|
if [[ -d "$TEMP_DIR" ]]; then
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
|
||||||
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
||||||
|
echo -e "${RED}$INSTALL_DIR is not a directory.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_ffmpeg() {
|
||||||
|
echo -e "${YELLOW}Downloading the latest stable version of FFmpeg...${NC}"
|
||||||
|
|
||||||
|
# Base URL for downloads from John van Sickle's FFmpeg builds
|
||||||
|
BASE_URL="https://johnvansickle.com/ffmpeg/releases"
|
||||||
|
|
||||||
|
# Map architecture to the expected format in the URL
|
||||||
|
if [[ "$ARCH" == "amd64" ]]; then
|
||||||
|
URL_ARCH="amd64"
|
||||||
|
elif [[ "$ARCH" == "arm64" ]]; then
|
||||||
|
URL_ARCH="arm64"
|
||||||
|
elif [[ "$ARCH" == "armhf" ]]; then
|
||||||
|
URL_ARCH="armhf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the download URL for the latest release
|
||||||
|
FFMPEG_URL="$BASE_URL/ffmpeg-release-$URL_ARCH-static.tar.xz"
|
||||||
|
|
||||||
|
if [[ -z "$FFMPEG_URL" ]]; then
|
||||||
|
echo -e "${RED}Could not determine the download URL for your system.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download the file
|
||||||
|
ARCHIVE_FILE="$TEMP_DIR/ffmpeg.tar.xz"
|
||||||
|
echo -e "${YELLOW}Downloading from: $FFMPEG_URL${NC}"
|
||||||
|
|
||||||
|
if command -v wget > /dev/null; then
|
||||||
|
wget -q --show-progress -O "$ARCHIVE_FILE" "$FFMPEG_URL"
|
||||||
|
elif command -v curl > /dev/null; then
|
||||||
|
curl -L -o "$ARCHIVE_FILE" "$FFMPEG_URL"
|
||||||
|
else
|
||||||
|
echo -e "${RED}wget or curl is required to download FFmpeg.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo -e "${RED}Error downloading FFmpeg.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Download completed.${NC}"
|
||||||
|
|
||||||
|
# Extract the file
|
||||||
|
echo -e "${YELLOW}Extracting files...${NC}"
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
tar -xf "$ARCHIVE_FILE"
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo -e "${RED}Error extracting the file.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Extraction completed.${NC}"
|
||||||
|
|
||||||
|
# Clean up downloaded file
|
||||||
|
rm "$ARCHIVE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_binaries() {
|
||||||
|
echo -e "${YELLOW}Installing binaries...${NC}"
|
||||||
|
|
||||||
|
# Find the extracted directory
|
||||||
|
EXTRACTED_DIR=$(find "$TEMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*" | head -n 1)
|
||||||
|
|
||||||
|
if [[ -z "$EXTRACTED_DIR" ]]; then
|
||||||
|
echo -e "${RED}FFmpeg extracted directory not found.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the binaries
|
||||||
|
FFMPEG_BIN="$EXTRACTED_DIR/ffmpeg"
|
||||||
|
FFPROBE_BIN="$EXTRACTED_DIR/ffprobe"
|
||||||
|
|
||||||
|
# Verify binaries exist
|
||||||
|
if [[ ! -f "$FFMPEG_BIN" ]] || [[ ! -f "$FFPROBE_BIN" ]]; then
|
||||||
|
echo -e "${RED}FFmpeg and FFprobe binaries not found.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy binaries to the bin folder
|
||||||
|
mv "$FFMPEG_BIN" "$INSTALL_DIR/ffmpeg"
|
||||||
|
mv "$FFPROBE_BIN" "$INSTALL_DIR/ffprobe"
|
||||||
|
|
||||||
|
# Make binaries executable
|
||||||
|
chmod +x "$INSTALL_DIR/ffmpeg"
|
||||||
|
chmod +x "$INSTALL_DIR/ffprobe"
|
||||||
|
|
||||||
|
echo -e "${GREEN}Binaries installed in $INSTALL_DIR${NC}"
|
||||||
|
|
||||||
|
# Clean up extracted directory
|
||||||
|
rm -rf "$EXTRACTED_DIR"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_versions() {
|
||||||
|
echo -e "${YELLOW}Verifying the installation...${NC}"
|
||||||
|
|
||||||
|
FFMPEG_PATH="$INSTALL_DIR/ffmpeg"
|
||||||
|
FFPROBE_PATH="$INSTALL_DIR/ffprobe"
|
||||||
|
|
||||||
|
echo -e "${GREEN}FFmpeg installed at: $FFMPEG_PATH${NC}"
|
||||||
|
if [[ -x "$FFMPEG_PATH" ]]; then
|
||||||
|
"$FFMPEG_PATH" -version | head -n 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}FFprobe installed at: $FFPROBE_PATH${NC}"
|
||||||
|
if [[ -x "$FFPROBE_PATH" ]]; then
|
||||||
|
"$FFPROBE_PATH" -version | head -n 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download_ffmpeg
|
||||||
|
install_binaries
|
||||||
|
show_versions
|
@ -18,15 +18,9 @@ export default async ({ username, password, hash }, user) => {
|
|||||||
throw new OperationError(401, "User is disabled")
|
throw new OperationError(401, "User is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof hash !== "undefined") {
|
|
||||||
if (user.password !== hash) {
|
|
||||||
throw new OperationError(401, "Invalid credentials")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!bcrypt.compareSync(password, user.password)) {
|
if (!bcrypt.compareSync(password, user.password)) {
|
||||||
throw new OperationError(401, "Invalid credentials")
|
throw new OperationError(401, "Invalid credentials")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"name": "auth",
|
"name": "auth"
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"name": "chats",
|
"name": "chats"
|
||||||
"version": "0.60.2"
|
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ import nodemailer from "nodemailer"
|
|||||||
import DbManager from "@shared-classes/DbManager"
|
import DbManager from "@shared-classes/DbManager"
|
||||||
|
|
||||||
import SharedMiddlewares from "@shared-middlewares"
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
|
||||||
export default class API extends Server {
|
export default class API extends Server {
|
||||||
static refName = "ems"
|
static refName = "ems"
|
||||||
static useEngine = "hyper-express"
|
static useEngine = "hyper-express"
|
||||||
@ -10,7 +11,7 @@ export default class API extends Server {
|
|||||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3007
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3007
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
...SharedMiddlewares
|
...SharedMiddlewares,
|
||||||
}
|
}
|
||||||
|
|
||||||
contexts = {
|
contexts = {
|
||||||
@ -27,7 +28,8 @@ export default class API extends Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcEvents = {
|
ipcEvents = {
|
||||||
"account:activation:send": require("./ipcEvents/accountActivation").default,
|
"account:activation:send": require("./ipcEvents/accountActivation")
|
||||||
|
.default,
|
||||||
"new:login": require("./ipcEvents/newLogin").default,
|
"new:login": require("./ipcEvents/newLogin").default,
|
||||||
"mfa:send": require("./ipcEvents/mfaSend").default,
|
"mfa:send": require("./ipcEvents/mfaSend").default,
|
||||||
"apr:send": require("./ipcEvents/aprSend").default,
|
"apr:send": require("./ipcEvents/aprSend").default,
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ems",
|
"name": "ems",
|
||||||
"description": "External Messaging Service (SMS, EMAIL, PUSH)",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"nodemailer": "^6.9.11",
|
"nodemailer": "^6.9.11"
|
||||||
"web-push": "^3.6.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,11 +104,9 @@ export function createAssembleChunksPromise({
|
|||||||
|
|
||||||
export async function handleChunkFile(
|
export async function handleChunkFile(
|
||||||
fileStream,
|
fileStream,
|
||||||
{ tmpDir, headers, maxFileSize, maxChunkSize },
|
{ chunksPath, outputDir, headers, maxFileSize, maxChunkSize },
|
||||||
) {
|
) {
|
||||||
return await new Promise(async (resolve, reject) => {
|
return await new Promise(async (resolve, reject) => {
|
||||||
const workPath = path.join(tmpDir, headers["uploader-file-id"])
|
|
||||||
const chunksPath = path.join(workPath, "chunks")
|
|
||||||
const chunkPath = path.join(
|
const chunkPath = path.join(
|
||||||
chunksPath,
|
chunksPath,
|
||||||
headers["uploader-chunk-number"],
|
headers["uploader-chunk-number"],
|
||||||
@ -125,17 +123,6 @@ export async function handleChunkFile(
|
|||||||
return reject(new OperationError(500, "Chunk is out of range"))
|
return reject(new OperationError(500, "Chunk is out of range"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if is the first chunk check if dir exists before write things
|
|
||||||
if (chunkCount === 0) {
|
|
||||||
try {
|
|
||||||
if (!(await fs.promises.stat(chunksPath).catch(() => false))) {
|
|
||||||
await fs.promises.mkdir(chunksPath, { recursive: true })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return reject(new OperationError(500, error.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataWritten = 0
|
let dataWritten = 0
|
||||||
|
|
||||||
let writeStream = fs.createWriteStream(chunkPath)
|
let writeStream = fs.createWriteStream(chunkPath)
|
||||||
@ -172,25 +159,18 @@ export async function handleChunkFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
const mimetype = mimetypes.lookup(
|
// const mimetype = mimetypes.lookup(
|
||||||
headers["uploader-original-name"],
|
// headers["uploader-original-name"],
|
||||||
)
|
// )
|
||||||
const extension = mimetypes.extension(mimetype)
|
// const extension = mimetypes.extension(mimetype)
|
||||||
|
|
||||||
let filename = headers["uploader-file-id"]
|
let filename = nanoid()
|
||||||
|
|
||||||
if (headers["uploader-use-date"] === "true") {
|
|
||||||
filename = `${filename}_${Date.now()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(
|
return resolve(
|
||||||
createAssembleChunksPromise({
|
createAssembleChunksPromise({
|
||||||
// build data
|
// build data
|
||||||
chunksPath: chunksPath,
|
chunksPath: chunksPath,
|
||||||
filePath: path.resolve(
|
filePath: path.resolve(outputDir, filename),
|
||||||
workPath,
|
|
||||||
`${filename}.${extension}`,
|
|
||||||
),
|
|
||||||
maxFileSize: maxFileSize,
|
maxFileSize: maxFileSize,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
@ -1,21 +1,21 @@
|
|||||||
import { Server } from "linebridge"
|
import { Server } from "linebridge"
|
||||||
|
|
||||||
import B2 from "backblaze-b2"
|
|
||||||
|
|
||||||
import DbManager from "@shared-classes/DbManager"
|
import DbManager from "@shared-classes/DbManager"
|
||||||
|
import RedisClient from "@shared-classes/RedisClient"
|
||||||
import StorageClient from "@shared-classes/StorageClient"
|
import StorageClient from "@shared-classes/StorageClient"
|
||||||
import CacheService from "@shared-classes/CacheService"
|
import CacheService from "@shared-classes/CacheService"
|
||||||
import SSEManager from "@shared-classes/SSEManager"
|
import SSEManager from "@shared-classes/SSEManager"
|
||||||
import SharedMiddlewares from "@shared-middlewares"
|
|
||||||
import LimitsClass from "@shared-classes/Limits"
|
import LimitsClass from "@shared-classes/Limits"
|
||||||
import TaskQueueManager from "@shared-classes/TaskQueueManager"
|
import TaskQueueManager from "@shared-classes/TaskQueueManager"
|
||||||
|
|
||||||
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
|
||||||
class API extends Server {
|
class API extends Server {
|
||||||
static refName = "files"
|
static refName = "files"
|
||||||
static useEngine = "hyper-express"
|
static useEngine = "hyper-express-ng"
|
||||||
static routesPath = `${__dirname}/routes`
|
static routesPath = `${__dirname}/routes`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
|
||||||
static enableWebsockets = true
|
//static enableWebsockets = true
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
...SharedMiddlewares,
|
...SharedMiddlewares,
|
||||||
@ -24,10 +24,13 @@ class API extends Server {
|
|||||||
contexts = {
|
contexts = {
|
||||||
db: new DbManager(),
|
db: new DbManager(),
|
||||||
cache: new CacheService(),
|
cache: new CacheService(),
|
||||||
|
SSEManager: new SSEManager(),
|
||||||
|
redis: RedisClient({
|
||||||
|
maxRetriesPerRequest: null,
|
||||||
|
}),
|
||||||
|
limits: {},
|
||||||
storage: StorageClient(),
|
storage: StorageClient(),
|
||||||
b2Storage: null,
|
b2Storage: null,
|
||||||
SSEManager: new SSEManager(),
|
|
||||||
limits: {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queuesManager = new TaskQueueManager(
|
queuesManager = new TaskQueueManager(
|
||||||
@ -41,27 +44,35 @@ class API extends Server {
|
|||||||
global.sse = this.contexts.SSEManager
|
global.sse = this.contexts.SSEManager
|
||||||
|
|
||||||
if (process.env.B2_KEY_ID && process.env.B2_APP_KEY) {
|
if (process.env.B2_KEY_ID && process.env.B2_APP_KEY) {
|
||||||
this.contexts.b2Storage = new B2({
|
this.contexts.b2Storage = StorageClient({
|
||||||
applicationKeyId: process.env.B2_KEY_ID,
|
endPoint: process.env.B2_ENDPOINT,
|
||||||
applicationKey: process.env.B2_APP_KEY,
|
cdnUrl: process.env.B2_CDN_ENDPOINT,
|
||||||
|
defaultBucket: process.env.B2_BUCKET,
|
||||||
|
accessKey: process.env.B2_KEY_ID,
|
||||||
|
secretKey: process.env.B2_APP_KEY,
|
||||||
|
port: 443,
|
||||||
|
useSSL: true,
|
||||||
|
setupBucket: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
global.b2Storage = this.contexts.b2Storage
|
await this.contexts.b2Storage.initialize()
|
||||||
|
|
||||||
await this.contexts.b2Storage.authorize()
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"B2 storage not configured on environment, skipping...",
|
"B2 storage not configured on environment, skipping...",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.contexts.redis.initialize()
|
||||||
await this.queuesManager.initialize({
|
await this.queuesManager.initialize({
|
||||||
redisOptions: this.engine.ws.redis.options,
|
redisOptions: this.contexts.redis.client,
|
||||||
})
|
})
|
||||||
await this.contexts.db.initialize()
|
await this.contexts.db.initialize()
|
||||||
await this.contexts.storage.initialize()
|
await this.contexts.storage.initialize()
|
||||||
|
|
||||||
global.storage = this.contexts.storage
|
global.storages = {
|
||||||
|
standard: this.contexts.storage,
|
||||||
|
b2: this.contexts.b2Storage,
|
||||||
|
}
|
||||||
global.queues = this.queuesManager
|
global.queues = this.queuesManager
|
||||||
|
|
||||||
this.contexts.limits = await LimitsClass.get()
|
this.contexts.limits = await LimitsClass.get()
|
||||||
|
@ -1,20 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "files",
|
"name": "files",
|
||||||
"version": "0.60.2",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"backblaze-b2": "^1.7.0",
|
"file-type": "^20.4.1",
|
||||||
"busboy": "^1.6.0",
|
|
||||||
"content-range": "^2.0.2",
|
|
||||||
"ffmpeg-static": "^5.2.0",
|
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"merge-files": "^0.1.2",
|
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"minio": "^7.0.32",
|
|
||||||
"normalize-url": "^8.0.0",
|
|
||||||
"p-map": "4",
|
"p-map": "4",
|
||||||
"p-queue": "^7.3.4",
|
"sharp": "0.32.6"
|
||||||
"redis": "^4.6.6",
|
|
||||||
"sharp": "0.32.6",
|
|
||||||
"split-chunk-merge": "^1.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import path from "node:path"
|
|
||||||
|
|
||||||
import fs from "node:fs"
|
|
||||||
import RemoteUpload from "@services/remoteUpload"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: "remote_upload",
|
|
||||||
maxJobs: 10,
|
|
||||||
process: async (job) => {
|
|
||||||
const {
|
|
||||||
filePath,
|
|
||||||
parentDir,
|
|
||||||
service,
|
|
||||||
useCompression,
|
|
||||||
cachePath,
|
|
||||||
transmux,
|
|
||||||
transmuxOptions,
|
|
||||||
} = job.data
|
|
||||||
|
|
||||||
console.log("[JOB][remote_upload] Processing job >", job.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await RemoteUpload({
|
|
||||||
parentDir: parentDir,
|
|
||||||
source: filePath,
|
|
||||||
service: service,
|
|
||||||
useCompression: useCompression,
|
|
||||||
transmux: transmux,
|
|
||||||
transmuxOptions: transmuxOptions,
|
|
||||||
cachePath: cachePath,
|
|
||||||
onProgress: (progress) => {
|
|
||||||
job.updateProgress(progress)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await fs.promises
|
|
||||||
.rm(filePath, { recursive: true, force: true })
|
|
||||||
.catch(() => null)
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
await fs.promises
|
|
||||||
.rm(filePath, { recursive: true, force: true })
|
|
||||||
.catch(() => null)
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import RemoteUpload from "@services/remoteUpload"
|
|
||||||
import fs from "node:fs"
|
|
||||||
|
|
||||||
module.exports = async (job) => {
|
|
||||||
const {
|
|
||||||
filePath,
|
|
||||||
parentDir,
|
|
||||||
service,
|
|
||||||
useCompression,
|
|
||||||
cachePath,
|
|
||||||
transmux,
|
|
||||||
transmuxOptions,
|
|
||||||
} = job.data
|
|
||||||
|
|
||||||
console.log("[JOB][remote_upload] Processing job >", job.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await RemoteUpload({
|
|
||||||
parentDir: parentDir,
|
|
||||||
source: filePath,
|
|
||||||
service: service,
|
|
||||||
useCompression: useCompression,
|
|
||||||
transmux: transmux,
|
|
||||||
transmuxOptions: transmuxOptions,
|
|
||||||
cachePath: cachePath,
|
|
||||||
onProgress: (progress) => {
|
|
||||||
job.progress(progress)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await fs.promises
|
|
||||||
.rm(filePath, { recursive: true, force: true })
|
|
||||||
.catch(() => null)
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
await fs.promises
|
|
||||||
.rm(filePath, { recursive: true, force: true })
|
|
||||||
.catch(() => null)
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
29
packages/server/services/files/queues/fileProcess/index.js
Normal file
29
packages/server/services/files/queues/fileProcess/index.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import fs from "node:fs"
|
||||||
|
|
||||||
|
import Upload from "@shared-classes/Upload"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "file-process",
|
||||||
|
maxJobs: 2,
|
||||||
|
process: async (job) => {
|
||||||
|
console.log("[JOB][file-process] starting... >", job.data)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await Upload.fileHandle({
|
||||||
|
...job.data,
|
||||||
|
onProgress: (progress) => {
|
||||||
|
job.updateProgress(progress)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
await fs.promises
|
||||||
|
.rm(job.workPath, { recursive: true, force: true })
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
import mimetypes from "mime-types"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
useContext: ["storage"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const streamPath = req.path.replace(req.route.pattern.replace("*", ""), "/")
|
|
||||||
|
|
||||||
this.default.contexts.storage.getObject(process.env.S3_BUCKET, streamPath, (err, dataStream) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(404).end()
|
|
||||||
}
|
|
||||||
|
|
||||||
const extname = mimetypes.lookup(streamPath)
|
|
||||||
|
|
||||||
// send chunked response
|
|
||||||
res.status(200)
|
|
||||||
|
|
||||||
// set headers
|
|
||||||
res.setHeader("Content-Type", extname)
|
|
||||||
res.setHeader("Accept-Ranges", "bytes")
|
|
||||||
|
|
||||||
return dataStream.pipe(res)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
import path from "node:path"
|
|
||||||
import fs from "node:fs"
|
|
||||||
import axios from "axios"
|
|
||||||
|
|
||||||
import MultiqualityHLSJob from "@shared-classes/MultiqualityHLSJob"
|
|
||||||
import { standardUpload } from "@services/remoteUpload"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
useContext: ["cache", "limits"],
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const { url } = req.query
|
|
||||||
|
|
||||||
const userPath = path.join(this.default.contexts.cache.constructor.cachePath, req.auth.session.user_id)
|
|
||||||
|
|
||||||
const jobId = String(new Date().getTime())
|
|
||||||
const jobPath = path.resolve(userPath, "jobs", jobId)
|
|
||||||
|
|
||||||
const sourcePath = path.resolve(jobPath, `${jobId}.source`)
|
|
||||||
|
|
||||||
if (!fs.existsSync(jobPath)) {
|
|
||||||
fs.mkdirSync(jobPath, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceStream = fs.createWriteStream(sourcePath)
|
|
||||||
|
|
||||||
const response = await axios({
|
|
||||||
method: "get",
|
|
||||||
url,
|
|
||||||
responseType: "stream",
|
|
||||||
})
|
|
||||||
|
|
||||||
response.data.pipe(sourceStream)
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
sourceStream.on("finish", () => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
sourceStream.on("error", (err) => {
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const job = new MultiqualityHLSJob({
|
|
||||||
input: sourcePath,
|
|
||||||
outputDir: jobPath,
|
|
||||||
levels: [
|
|
||||||
{
|
|
||||||
original: true,
|
|
||||||
codec: "libx264",
|
|
||||||
bitrate: "10M",
|
|
||||||
preset: "ultrafast",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
codec: "libx264",
|
|
||||||
width: 1280,
|
|
||||||
bitrate: "3M",
|
|
||||||
preset: "ultrafast",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
job
|
|
||||||
.on("error", (err) => {
|
|
||||||
console.error(`[TRANSMUX] Transmuxing failed`, err)
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
.on("end", () => {
|
|
||||||
console.debug(`[TRANSMUX] Finished transmuxing > ${sourcePath}`)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await standardUpload({
|
|
||||||
isDirectory: true,
|
|
||||||
source: path.join(jobPath, "hls"),
|
|
||||||
remotePath: `${req.auth.session.user_id}/jobs/${jobId}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
fs.rmSync(jobPath, { recursive: true, force: true })
|
|
||||||
|
|
||||||
return {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +1,12 @@
|
|||||||
import { Duplex } from "node:stream"
|
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
import RemoteUpload from "@services/remoteUpload"
|
|
||||||
import {
|
import { checkChunkUploadHeaders, handleChunkFile } from "@classes/ChunkFile"
|
||||||
checkChunkUploadHeaders,
|
import Upload from "@shared-classes/Upload"
|
||||||
handleChunkFile,
|
import bufferToStream from "@shared-utils/bufferToStream"
|
||||||
} from "@classes/ChunkFileUpload"
|
|
||||||
|
|
||||||
const availableProviders = ["b2", "standard"]
|
const availableProviders = ["b2", "standard"]
|
||||||
|
|
||||||
function bufferToStream(bf) {
|
|
||||||
let tmp = new Duplex()
|
|
||||||
tmp.push(bf)
|
|
||||||
tmp.push(null)
|
|
||||||
return tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
useContext: ["cache", "limits"],
|
useContext: ["cache", "limits"],
|
||||||
middlewares: ["withAuthentication"],
|
middlewares: ["withAuthentication"],
|
||||||
@ -25,14 +16,16 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadId = `${req.headers["uploader-file-id"]}_${Date.now()}`
|
const uploadId = `${req.headers["uploader-file-id"]}`
|
||||||
|
|
||||||
const tmpPath = path.resolve(
|
const workPath = path.resolve(
|
||||||
this.default.contexts.cache.constructor.cachePath,
|
this.default.contexts.cache.constructor.cachePath,
|
||||||
req.auth.session.user_id,
|
`${req.auth.session.user_id}-${uploadId}`,
|
||||||
)
|
)
|
||||||
|
const chunksPath = path.join(workPath, "chunks")
|
||||||
|
const assembledPath = path.join(workPath, "assembled")
|
||||||
|
|
||||||
const limits = {
|
const config = {
|
||||||
maxFileSize:
|
maxFileSize:
|
||||||
parseInt(this.default.contexts.limits.maxFileSizeInMB) *
|
parseInt(this.default.contexts.limits.maxFileSizeInMB) *
|
||||||
1024 *
|
1024 *
|
||||||
@ -42,93 +35,91 @@ export default {
|
|||||||
1024 *
|
1024 *
|
||||||
1024,
|
1024,
|
||||||
useCompression: true,
|
useCompression: true,
|
||||||
useProvider: "standard",
|
useProvider: req.headers["use-provider"] ?? "standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
// const user = await req.auth.user()
|
// const user = await req.auth.user()
|
||||||
|
|
||||||
// if (user.roles.includes("admin")) {
|
// if (user.roles.includes("admin")) {
|
||||||
// // maxFileSize for admins 100GB
|
// // maxFileSize for admins 100GB
|
||||||
// limits.maxFileSize = 100 * 1024 * 1024 * 1024
|
// limits.maxFileSize = 100 * 1024 * 1024 * 1024
|
||||||
|
|
||||||
// // optional compression for admins
|
// // optional compression for admins
|
||||||
// limits.useCompression = req.headers["use-compression"] ?? false
|
// limits.useCompression = req.headers["use-compression"] ?? false
|
||||||
|
|
||||||
// limits.useProvider = req.headers["provider-type"] ?? "b2"
|
// limits.useProvider = req.headers["provider-type"] ?? "b2"
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// check if provider is valid
|
// check if provider is valid
|
||||||
if (!availableProviders.includes(limits.useProvider)) {
|
if (!availableProviders.includes(config.useProvider)) {
|
||||||
throw new OperationError(400, "Invalid provider")
|
throw new OperationError(400, "Invalid provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a readable stream from req.body(buffer)
|
await fs.promises.mkdir(workPath, { recursive: true })
|
||||||
|
await fs.promises.mkdir(chunksPath, { recursive: true })
|
||||||
|
await fs.promises.mkdir(assembledPath, { recursive: true })
|
||||||
|
|
||||||
|
// create a readable stream
|
||||||
const dataStream = bufferToStream(await req.buffer())
|
const dataStream = bufferToStream(await req.buffer())
|
||||||
|
|
||||||
let result = await handleChunkFile(dataStream, {
|
let assemble = await handleChunkFile(dataStream, {
|
||||||
tmpDir: tmpPath,
|
chunksPath: chunksPath,
|
||||||
|
outputDir: assembledPath,
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
maxFileSize: limits.maxFileSize,
|
maxFileSize: config.maxFileSize,
|
||||||
maxChunkSize: limits.maxChunkSize,
|
maxChunkSize: config.maxChunkSize,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (typeof result === "function") {
|
if (typeof assemble === "function") {
|
||||||
try {
|
try {
|
||||||
result = await result()
|
assemble = await assemble()
|
||||||
|
|
||||||
if (req.headers["transmux"] || limits.useCompression === true) {
|
let transformations = req.headers["transformations"]
|
||||||
// add a background task
|
|
||||||
|
if (transformations) {
|
||||||
|
transformations = transformations
|
||||||
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user_id: req.auth.session.user_id,
|
||||||
|
uploadId: uploadId,
|
||||||
|
filePath: assemble.filePath,
|
||||||
|
workPath: workPath,
|
||||||
|
transformations: transformations,
|
||||||
|
s3Provider: config.useProvider,
|
||||||
|
useCompression: config.useCompression,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if has transformations, use background job
|
||||||
|
if (
|
||||||
|
(transformations && transformations.length > 0) ||
|
||||||
|
config.useCompression
|
||||||
|
) {
|
||||||
const job = await global.queues.createJob(
|
const job = await global.queues.createJob(
|
||||||
"remote_upload",
|
"file-process",
|
||||||
{
|
payload,
|
||||||
filePath: result.filePath,
|
|
||||||
parentDir: req.auth.session.user_id,
|
|
||||||
service: limits.useProvider,
|
|
||||||
useCompression: limits.useCompression,
|
|
||||||
transmux: req.headers["transmux"] ?? false,
|
|
||||||
transmuxOptions: req.headers["transmux-options"],
|
|
||||||
cachePath: tmpPath,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
useSSE: true,
|
useSSE: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const sseChannelId = job.sseChannelId
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uploadId: uploadId,
|
uploadId: payload.uploadId,
|
||||||
sseChannelId: sseChannelId,
|
sseChannelId: job.sseChannelId,
|
||||||
eventChannelURL: `${req.headers["x-forwarded-proto"] || req.protocol}://${req.get("host")}/upload/sse_events/${sseChannelId}`,
|
sseUrl: `${req.headers["x-forwarded-proto"] || req.protocol}://${req.get("host")}/upload/sse_events/${job.sseChannelId}`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const result = await RemoteUpload({
|
|
||||||
source: result.filePath,
|
|
||||||
parentDir: req.auth.session.user_id,
|
|
||||||
service: limits.useProvider,
|
|
||||||
useCompression: limits.useCompression,
|
|
||||||
cachePath: tmpPath,
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
return await Upload.fileHandle(payload)
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await fs.promises
|
await fs.promises.rm(workPath, { recursive: true })
|
||||||
.rm(tmpPath, { recursive: true, force: true })
|
throw error
|
||||||
.catch(() => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
throw new OperationError(
|
|
||||||
error.code ?? 500,
|
|
||||||
error.message ?? "Failed to upload file",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: 1,
|
next: true,
|
||||||
chunkNumber: req.headers["uploader-chunk-number"],
|
chunkNumber: req.headers["uploader-chunk-number"],
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,48 +1,50 @@
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
|
|
||||||
import RemoteUpload from "@services/remoteUpload"
|
import Upload from "@shared-classes/Upload"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
useContext: ["cache"],
|
useContext: ["cache"],
|
||||||
middlewares: [
|
middlewares: ["withAuthentication"],
|
||||||
"withAuthentication",
|
|
||||||
],
|
|
||||||
fn: async (req, res) => {
|
fn: async (req, res) => {
|
||||||
const { cache } = this.default.contexts
|
const workPath = path.resolve(
|
||||||
|
this.default.contexts.cache.constructor.cachePath,
|
||||||
|
`${req.auth.session.user_id}-${nanoid()}`,
|
||||||
|
)
|
||||||
|
|
||||||
const providerType = req.headers["provider-type"] ?? "standard"
|
await fs.promises.mkdir(workPath, { recursive: true })
|
||||||
|
|
||||||
const userPath = path.join(cache.constructor.cachePath, req.auth.session.user_id)
|
|
||||||
|
|
||||||
let localFilepath = null
|
let localFilepath = null
|
||||||
let tmpPath = path.resolve(userPath, `${Date.now()}`)
|
|
||||||
|
|
||||||
await req.multipart(async (field) => {
|
await req.multipart(async (field) => {
|
||||||
if (!field.file) {
|
if (!field.file) {
|
||||||
throw new OperationError(400, "Missing file")
|
throw new OperationError(400, "Missing file")
|
||||||
}
|
}
|
||||||
|
|
||||||
localFilepath = path.join(tmpPath, field.file.name)
|
localFilepath = path.join(workPath, "file")
|
||||||
|
|
||||||
const existTmpDir = await fs.promises.stat(tmpPath).then(() => true).catch(() => false)
|
|
||||||
|
|
||||||
if (!existTmpDir) {
|
|
||||||
await fs.promises.mkdir(tmpPath, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
await field.write(localFilepath)
|
await field.write(localFilepath)
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await RemoteUpload({
|
let transformations = req.headers["transformations"]
|
||||||
parentDir: req.auth.session.user_id,
|
|
||||||
source: localFilepath,
|
if (transformations) {
|
||||||
service: providerType,
|
transformations = transformations.split(",").map((t) => t.trim())
|
||||||
useCompression: ToBoolean(req.headers["use-compression"]) ?? true,
|
}
|
||||||
|
|
||||||
|
const result = await Upload.fileHandle({
|
||||||
|
user_id: req.auth.session.user_id,
|
||||||
|
filePath: localFilepath,
|
||||||
|
workPath: workPath,
|
||||||
|
transformations: transformations,
|
||||||
})
|
})
|
||||||
|
|
||||||
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
res.header("deprecated", "true")
|
||||||
|
res.header(
|
||||||
|
"deprecation-replacement",
|
||||||
|
"Use the new chunked upload API endpoint",
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
const ffmpeg = require("fluent-ffmpeg")
|
|
||||||
|
|
||||||
export default async (file) => {
|
|
||||||
// analize metadata
|
|
||||||
let metadata = await new Promise((resolve, reject) => {
|
|
||||||
ffmpeg.ffprobe(file.filepath, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(data)
|
|
||||||
})
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (metadata.format) {
|
|
||||||
metadata = metadata.format
|
|
||||||
}
|
|
||||||
|
|
||||||
file.metadata = {
|
|
||||||
duration: metadata.duration,
|
|
||||||
bitrate: metadata.bit_rate,
|
|
||||||
size: metadata.size,
|
|
||||||
}
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import path from "node:path"
|
|
||||||
import Sharp from "sharp"
|
|
||||||
|
|
||||||
const imageProcessingConf = {
|
|
||||||
// TODO: Get image sizeThreshold from DB
|
|
||||||
sizeThreshold: 10 * 1024 * 1024,
|
|
||||||
// TODO: Get image quality from DB
|
|
||||||
imageQuality: 80,
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageTypeToConfig = {
|
|
||||||
png: {
|
|
||||||
compressionLevel: Math.floor(imageProcessingConf.imageQuality / 100),
|
|
||||||
},
|
|
||||||
default: {
|
|
||||||
quality: imageProcessingConf.imageQuality
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes an image file and transforms it if it's above a certain size threshold.
|
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @function
|
|
||||||
* @param {Object} file - The file to be processed.
|
|
||||||
* @param {string} file.filepath - The path of the file to be processed.
|
|
||||||
* @param {string} file.hash - The hash of the file to be processed.
|
|
||||||
* @param {string} file.cachePath - The cache path of the file to be processed.
|
|
||||||
* @throws {Error} If the file parameter is not provided.
|
|
||||||
* @return {Object} The processed file object.
|
|
||||||
*/
|
|
||||||
async function processImage(file) {
|
|
||||||
if (!file) {
|
|
||||||
throw new Error("file is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
const stat = await fs.promises.stat(file.filepath)
|
|
||||||
|
|
||||||
if (stat.size < imageProcessingConf.sizeThreshold) {
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = await Sharp(file.filepath)
|
|
||||||
|
|
||||||
const { format } = await image.metadata()
|
|
||||||
|
|
||||||
image = await image[format](imageTypeToConfig[format] ?? imageTypeToConfig.default).withMetadata()
|
|
||||||
|
|
||||||
const outputFilepath = path.resolve(file.cachePath, `${file.hash}_transformed.${format}`)
|
|
||||||
|
|
||||||
await image.toFile(outputFilepath)
|
|
||||||
|
|
||||||
file.filepath = outputFilepath
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
export default processImage
|
|
@ -1,53 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import mimetypes from "mime-types"
|
|
||||||
|
|
||||||
import processVideo from "./video"
|
|
||||||
import processImage from "./image"
|
|
||||||
import processAudio from "./audio"
|
|
||||||
|
|
||||||
const fileTransformer = {
|
|
||||||
// video
|
|
||||||
"video/avi": processVideo,
|
|
||||||
"video/quicktime": processVideo,
|
|
||||||
"video/mp4": processVideo,
|
|
||||||
"video/webm": processVideo,
|
|
||||||
//image
|
|
||||||
"image/jpeg": processImage,
|
|
||||||
"image/png": processImage,
|
|
||||||
"image/gif": processImage,
|
|
||||||
"image/bmp": processImage,
|
|
||||||
"image/tiff": processImage,
|
|
||||||
"image/webp": processImage,
|
|
||||||
"image/jfif": processImage,
|
|
||||||
// audio
|
|
||||||
"audio/flac": processAudio,
|
|
||||||
"audio/x-flac": processAudio,
|
|
||||||
"audio/mp3": processAudio,
|
|
||||||
"audio/x-mp3": processAudio,
|
|
||||||
"audio/mpeg": processAudio,
|
|
||||||
"audio/x-mpeg": processAudio,
|
|
||||||
"audio/ogg": processAudio,
|
|
||||||
"audio/x-ogg": processAudio,
|
|
||||||
"audio/wav": processAudio,
|
|
||||||
"audio/x-wav": processAudio,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (file) => {
|
|
||||||
if (!file) {
|
|
||||||
throw new Error("file is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(file.filepath)) {
|
|
||||||
throw new Error(`File ${file.filepath} not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileMimetype = mimetypes.lookup(file.filepath)
|
|
||||||
|
|
||||||
if (typeof fileTransformer[fileMimetype] !== "function") {
|
|
||||||
console.debug(`File (${file.filepath}) has mimetype ${fileMimetype} and will not be processed`)
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
return await fileTransformer[fileMimetype](file)
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import videoTranscode from "@services/videoTranscode"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a video file based on the specified options.
|
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {Object} file - The video file to process.
|
|
||||||
* @param {Object} [options={}] - The options object to use for processing.
|
|
||||||
* @param {string} [options.videoCodec="libx264"] - The video codec to use.
|
|
||||||
* @param {string} [options.format="mp4"] - The format to use.
|
|
||||||
* @param {number} [options.audioBitrate=128] - The audio bitrate to use.
|
|
||||||
* @param {number} [options.videoBitrate=2024] - The video bitrate to use.
|
|
||||||
* @throws {Error} Throws an error if file parameter is not provided.
|
|
||||||
* @return {Object} The processed video file object.
|
|
||||||
*/
|
|
||||||
async function processVideo(file, options = {}) {
|
|
||||||
if (!file) {
|
|
||||||
throw new Error("file is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Get values from db
|
|
||||||
const {
|
|
||||||
videoCodec = "libx264",
|
|
||||||
format = "mp4",
|
|
||||||
audioBitrate = 128,
|
|
||||||
videoBitrate = 3000,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const result = await videoTranscode(file.filepath, {
|
|
||||||
videoCodec,
|
|
||||||
format,
|
|
||||||
audioBitrate,
|
|
||||||
videoBitrate: [videoBitrate, true],
|
|
||||||
extraOptions: ["-threads 2"],
|
|
||||||
})
|
|
||||||
|
|
||||||
file.filepath = result.filepath
|
|
||||||
file.filename = result.filename
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
export default processVideo
|
|
@ -1,162 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import path from "node:path"
|
|
||||||
import mimeTypes from "mime-types"
|
|
||||||
import getFileHash from "@shared-utils/readFileHash"
|
|
||||||
|
|
||||||
import PostProcess from "../post-process"
|
|
||||||
import Transmux from "../transmux"
|
|
||||||
|
|
||||||
import StandardUpload from "./providers/standard"
|
|
||||||
import B2Upload from "./providers/b2"
|
|
||||||
|
|
||||||
export default async ({
|
|
||||||
source,
|
|
||||||
parentDir,
|
|
||||||
service,
|
|
||||||
useCompression,
|
|
||||||
cachePath,
|
|
||||||
transmux,
|
|
||||||
transmuxOptions,
|
|
||||||
isDirectory,
|
|
||||||
onProgress,
|
|
||||||
}) => {
|
|
||||||
if (!source) {
|
|
||||||
throw new OperationError(500, "source is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!service) {
|
|
||||||
service = "standard"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parentDir) {
|
|
||||||
parentDir = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transmuxOptions) {
|
|
||||||
transmuxOptions = JSON.parse(transmuxOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useCompression) {
|
|
||||||
if (typeof onProgress === "function") {
|
|
||||||
onProgress(10, {
|
|
||||||
event: "post_processing",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const processOutput = await PostProcess({
|
|
||||||
filepath: source,
|
|
||||||
cachePath: cachePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (processOutput) {
|
|
||||||
if (processOutput.filepath) {
|
|
||||||
source = processOutput.filepath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
throw new OperationError(500, `Failed to process file`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transmux) {
|
|
||||||
if (typeof onProgress === "function") {
|
|
||||||
onProgress(30, {
|
|
||||||
event: "transmuxing",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const processOutput = await Transmux({
|
|
||||||
transmuxer: transmux,
|
|
||||||
transmuxOptions: transmuxOptions,
|
|
||||||
filepath: source,
|
|
||||||
cachePath: cachePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (processOutput) {
|
|
||||||
if (processOutput.filepath) {
|
|
||||||
source = processOutput.filepath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processOutput.isDirectory) {
|
|
||||||
isDirectory = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
throw new OperationError(500, `Failed to transmux file`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = mimeTypes.lookup(path.basename(source))
|
|
||||||
const hash = await getFileHash(fs.createReadStream(source))
|
|
||||||
|
|
||||||
let fileId = `${hash}`
|
|
||||||
|
|
||||||
// FIXME: This is a walkaround to avoid to hashing the entire directories
|
|
||||||
if (isDirectory) {
|
|
||||||
fileId = global.nanoid()
|
|
||||||
}
|
|
||||||
|
|
||||||
let remotePath = path.join(parentDir, fileId)
|
|
||||||
|
|
||||||
let result = {}
|
|
||||||
|
|
||||||
const metadata = {
|
|
||||||
"Content-Type": type,
|
|
||||||
"File-Hash": hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof onProgress === "function") {
|
|
||||||
onProgress(80, {
|
|
||||||
event: "uploading_s3",
|
|
||||||
service: service,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (service) {
|
|
||||||
case "b2":
|
|
||||||
if (!global.b2Storage) {
|
|
||||||
throw new OperationError(
|
|
||||||
500,
|
|
||||||
"B2 storage not configured on environment, unsupported service. Please use `standard` service.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await B2Upload({
|
|
||||||
source: isDirectory ? path.dirname(source) : source,
|
|
||||||
remotePath: remotePath,
|
|
||||||
metadata: metadata,
|
|
||||||
isDirectory: isDirectory,
|
|
||||||
targetFilename: isDirectory ? path.basename(source) : null,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case "standard":
|
|
||||||
result = await StandardUpload({
|
|
||||||
source: isDirectory ? path.dirname(source) : source,
|
|
||||||
remotePath: remotePath,
|
|
||||||
metadata: metadata,
|
|
||||||
isDirectory: isDirectory,
|
|
||||||
targetFilename: isDirectory ? path.basename(source) : null,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new OperationError(500, "Unsupported service")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
throw new OperationError(500, "Failed to upload to storage")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof onProgress === "function") {
|
|
||||||
onProgress(100, {
|
|
||||||
event: "done",
|
|
||||||
result: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import path from "node:path"
|
|
||||||
import pMap from "p-map"
|
|
||||||
|
|
||||||
export default async function b2Upload({
|
|
||||||
source,
|
|
||||||
remotePath,
|
|
||||||
metadata = {},
|
|
||||||
targetFilename,
|
|
||||||
isDirectory,
|
|
||||||
retryNumber = 0
|
|
||||||
}) {
|
|
||||||
if (isDirectory) {
|
|
||||||
let files = await fs.promises.readdir(source)
|
|
||||||
|
|
||||||
files = files.map((file) => {
|
|
||||||
const filePath = path.join(source, file)
|
|
||||||
|
|
||||||
const isTargetDirectory = fs.lstatSync(filePath).isDirectory()
|
|
||||||
|
|
||||||
return {
|
|
||||||
source: filePath,
|
|
||||||
remotePath: path.join(remotePath, file),
|
|
||||||
isDirectory: isTargetDirectory,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await pMap(
|
|
||||||
files,
|
|
||||||
b2Upload,
|
|
||||||
{
|
|
||||||
concurrency: 5
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: remotePath,
|
|
||||||
url: `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}/${targetFilename}`,
|
|
||||||
metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await global.b2Storage.authorize()
|
|
||||||
|
|
||||||
if (!fs.existsSync(source)) {
|
|
||||||
throw new OperationError(500, "File not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadUrl = await global.b2Storage.getUploadUrl({
|
|
||||||
bucketId: process.env.B2_BUCKET_ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.debug(`Uploading object to B2 Storage >`, {
|
|
||||||
source: source,
|
|
||||||
remote: remotePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await fs.promises.readFile(source)
|
|
||||||
|
|
||||||
await global.b2Storage.uploadFile({
|
|
||||||
uploadUrl: uploadUrl.data.uploadUrl,
|
|
||||||
uploadAuthToken: uploadUrl.data.authorizationToken,
|
|
||||||
fileName: remotePath,
|
|
||||||
data: data,
|
|
||||||
info: metadata
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
if (retryNumber < 5) {
|
|
||||||
return await b2Upload({
|
|
||||||
source,
|
|
||||||
remotePath,
|
|
||||||
metadata,
|
|
||||||
targetFilename,
|
|
||||||
isDirectory,
|
|
||||||
retryNumber: retryNumber + 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new OperationError(500, "B2 upload failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: remotePath,
|
|
||||||
url: `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`,
|
|
||||||
metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import path from "node:path"
|
|
||||||
import pMap from "p-map"
|
|
||||||
|
|
||||||
export default async function standardUpload({
|
|
||||||
source,
|
|
||||||
remotePath,
|
|
||||||
metadata = {},
|
|
||||||
targetFilename,
|
|
||||||
isDirectory,
|
|
||||||
}) {
|
|
||||||
if (isDirectory) {
|
|
||||||
let files = await fs.promises.readdir(source)
|
|
||||||
|
|
||||||
files = files.map((file) => {
|
|
||||||
const filePath = path.join(source, file)
|
|
||||||
|
|
||||||
const isTargetDirectory = fs.lstatSync(filePath).isDirectory()
|
|
||||||
|
|
||||||
return {
|
|
||||||
source: filePath,
|
|
||||||
remotePath: path.join(remotePath, file),
|
|
||||||
isDirectory: isTargetDirectory,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await pMap(
|
|
||||||
files,
|
|
||||||
standardUpload,
|
|
||||||
{
|
|
||||||
concurrency: 3
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: remotePath,
|
|
||||||
url: global.storage.composeRemoteURL(remotePath, targetFilename),
|
|
||||||
metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(`Uploading object to S3 Minio >`, {
|
|
||||||
source: source,
|
|
||||||
remote: remotePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
// upload to storage
|
|
||||||
await global.storage.fPutObject(process.env.S3_BUCKET, remotePath, source, metadata)
|
|
||||||
|
|
||||||
// compose url
|
|
||||||
const url = global.storage.composeRemoteURL(remotePath)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: remotePath,
|
|
||||||
url: url,
|
|
||||||
metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import path from "node:path"
|
|
||||||
|
|
||||||
import MultiqualityHLSJob from "@shared-classes/MultiqualityHLSJob"
|
|
||||||
import SegmentedAudioMPDJob from "@shared-classes/SegmentedAudioMPDJob"
|
|
||||||
|
|
||||||
const transmuxers = [
|
|
||||||
{
|
|
||||||
id: "mq-hls",
|
|
||||||
container: "hls",
|
|
||||||
extension: "m3u8",
|
|
||||||
multipleOutput: true,
|
|
||||||
buildCommand: (input, outputDir) => {
|
|
||||||
return new MultiqualityHLSJob({
|
|
||||||
input: input,
|
|
||||||
outputDir: outputDir,
|
|
||||||
outputMasterName: "master.m3u8",
|
|
||||||
levels: [
|
|
||||||
{
|
|
||||||
original: true,
|
|
||||||
codec: "libx264",
|
|
||||||
bitrate: "10M",
|
|
||||||
preset: "ultrafast",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
codec: "libx264",
|
|
||||||
width: 1280,
|
|
||||||
bitrate: "3M",
|
|
||||||
preset: "ultrafast",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "a-dash",
|
|
||||||
container: "dash",
|
|
||||||
extension: "mpd",
|
|
||||||
multipleOutput: true,
|
|
||||||
buildCommand: (input, outputDir) => {
|
|
||||||
return new SegmentedAudioMPDJob({
|
|
||||||
input: input,
|
|
||||||
outputDir: outputDir,
|
|
||||||
outputMasterName: "master.mpd",
|
|
||||||
|
|
||||||
audioCodec: "flac",
|
|
||||||
//audioBitrate: "1600k",
|
|
||||||
//audioSampleRate: 96000,
|
|
||||||
segmentTime: 10,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default async (params) => {
|
|
||||||
if (!params) {
|
|
||||||
throw new Error("params is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.filepath) {
|
|
||||||
throw new Error("filepath is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.cachePath) {
|
|
||||||
throw new Error("cachePath is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.transmuxer) {
|
|
||||||
throw new Error("transmuxer is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(params.filepath)) {
|
|
||||||
throw new Error(`File ${params.filepath} not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const transmuxer = transmuxers.find((item) => item.id === params.transmuxer)
|
|
||||||
|
|
||||||
if (!transmuxer) {
|
|
||||||
throw new Error(`Transmuxer ${params.transmuxer} not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobPath = path.dirname(params.filepath)
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.dirname(jobPath))) {
|
|
||||||
fs.mkdirSync(path.dirname(jobPath), { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const command = transmuxer.buildCommand(params.filepath, jobPath)
|
|
||||||
|
|
||||||
command
|
|
||||||
.on("progress", function (progress) {
|
|
||||||
console.log("Processing: " + progress.percent + "% done")
|
|
||||||
})
|
|
||||||
.on("error", (err) => {
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
.on("end", (data) => {
|
|
||||||
resolve(data)
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[TRANSMUX] Transmuxing failed`, error)
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
import path from "path"
|
|
||||||
|
|
||||||
const ffmpeg = require("fluent-ffmpeg")
|
|
||||||
|
|
||||||
const defaultParams = {
|
|
||||||
audioBitrate: 128,
|
|
||||||
videoBitrate: 1024,
|
|
||||||
videoCodec: "libvpx",
|
|
||||||
audioCodec: "libvorbis",
|
|
||||||
format: "mp4",
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxTasks = 5
|
|
||||||
|
|
||||||
export default (input, params = defaultParams) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!global.ffmpegTasks) {
|
|
||||||
global.ffmpegTasks = []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.ffmpegTasks.length >= maxTasks) {
|
|
||||||
return reject(new Error("Too many transcoding tasks"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputFilename = `${path.basename(input).split(".")[0]}_ff.${params.format ?? "webm"}`
|
|
||||||
const outputFilepath = `${path.dirname(input)}/${outputFilename}`
|
|
||||||
|
|
||||||
console.debug(`[TRANSCODING] Transcoding ${input} to ${outputFilepath}`)
|
|
||||||
|
|
||||||
const onEnd = async () => {
|
|
||||||
console.debug(
|
|
||||||
`[TRANSCODING] Finished transcode ${input} to ${outputFilepath}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
return resolve({
|
|
||||||
filename: outputFilename,
|
|
||||||
filepath: outputFilepath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onError = (err) => {
|
|
||||||
console.error(
|
|
||||||
`[TRANSCODING] Transcoding ${input} to ${outputFilepath} failed`,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
let exec = null
|
|
||||||
|
|
||||||
const commands = {
|
|
||||||
input: input,
|
|
||||||
...params,
|
|
||||||
output: outputFilepath,
|
|
||||||
outputOptions: ["-preset veryfast"],
|
|
||||||
}
|
|
||||||
|
|
||||||
// chain methods
|
|
||||||
for (let key in commands) {
|
|
||||||
if (exec === null) {
|
|
||||||
exec = ffmpeg(commands[key])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "extraOptions" && Array.isArray(commands[key])) {
|
|
||||||
for (const option of commands[key]) {
|
|
||||||
exec = exec.inputOptions(option)
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "outputOptions" && Array.isArray(commands[key])) {
|
|
||||||
for (const option of commands[key]) {
|
|
||||||
exec = exec.outputOptions(option)
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof exec[key] !== "function") {
|
|
||||||
console.warn(`[TRANSCODING] Method ${key} is not a function`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(commands[key])) {
|
|
||||||
exec = exec[key](...commands[key])
|
|
||||||
} else {
|
|
||||||
exec = exec[key](commands[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.on("error", onError).on("end", onEnd).run()
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import fs from "node:fs"
|
|
||||||
import os from "node:os"
|
|
||||||
import axios from "axios"
|
|
||||||
|
|
||||||
export default async (outputDir) => {
|
|
||||||
const arch = os.arch()
|
|
||||||
|
|
||||||
console.log(`Downloading ffmpeg for ${arch}...`)
|
|
||||||
const baseURL = `https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${arch}-static.tar.xz`
|
|
||||||
|
|
||||||
|
|
||||||
const response = await axios.get(baseURL, {
|
|
||||||
responseType: "stream"
|
|
||||||
})
|
|
||||||
|
|
||||||
const ffmpegPath = path.join(outputDir, `ffmpeg-${arch}.tar.xz`)
|
|
||||||
const ffmpegFile = fs.createWriteStream(ffmpegPath)
|
|
||||||
|
|
||||||
response.data.pipe(ffmpegFile)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
export default (from, to) => {
|
|
||||||
const resolvedUrl = new URL(to, new URL(from, "resolve://"))
|
|
||||||
|
|
||||||
if (resolvedUrl.protocol === "resolve:") {
|
|
||||||
let { pathname, search, hash } = resolvedUrl
|
|
||||||
|
|
||||||
if (to.includes("@")) {
|
|
||||||
const fromUrl = new URL(from)
|
|
||||||
const toUrl = new URL(to, fromUrl.origin)
|
|
||||||
|
|
||||||
pathname = toUrl.pathname
|
|
||||||
search = toUrl.search
|
|
||||||
hash = toUrl.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathname + search + hash
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedUrl.toString()
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import fs from "fs"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
async function syncFolder(dir, destPath) {
|
|
||||||
const files = await fs.promises.readdir(dir)
|
|
||||||
|
|
||||||
for await (const file of files) {
|
|
||||||
const filePath = path.resolve(dir, file)
|
|
||||||
const desitinationFilePath = `${destPath}/${file}`
|
|
||||||
|
|
||||||
const stat = fs.statSync(filePath)
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
await syncFolder(filePath, desitinationFilePath)
|
|
||||||
} else {
|
|
||||||
const fileContent = await fs.promises.readFile(filePath)
|
|
||||||
|
|
||||||
await global.storage.putObject(process.env.S3_BUCKET, desitinationFilePath, fileContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default syncFolder
|
|
@ -7,9 +7,10 @@ import SharedMiddlewares from "@shared-middlewares"
|
|||||||
|
|
||||||
export default class API extends Server {
|
export default class API extends Server {
|
||||||
static refName = "main"
|
static refName = "main"
|
||||||
static enableWebsockets = true
|
static useEngine = "hyper-express-ng"
|
||||||
static routesPath = `${__dirname}/routes`
|
static routesPath = `${__dirname}/routes`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT || 3000
|
static listen_port = process.env.HTTP_LISTEN_PORT || 3000
|
||||||
|
static enableWebsockets = false
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
...require("@middlewares").default,
|
...require("@middlewares").default,
|
||||||
@ -26,8 +27,6 @@ export default class API extends Server {
|
|||||||
await this.contexts.db.initialize()
|
await this.contexts.db.initialize()
|
||||||
await StartupDB()
|
await StartupDB()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWsAuth = require("@shared-lib/handleWsAuth").default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Boot(API)
|
Boot(API)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "main",
|
"name": "main",
|
||||||
"version": "0.60.2",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/rest": "^20.0.2"
|
"@octokit/rest": "^20.0.2"
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Extension } from "@db_models"
|
|||||||
export default async function resolve(payload) {
|
export default async function resolve(payload) {
|
||||||
let { user_id, pkg } = payload
|
let { user_id, pkg } = payload
|
||||||
|
|
||||||
const [pkgName, pkgVersion] = pkg.split("@")
|
let [pkgName, pkgVersion] = pkg.split("@")
|
||||||
|
|
||||||
if (!pkgVersion) {
|
if (!pkgVersion) {
|
||||||
pkgVersion = "latest"
|
pkgVersion = "latest"
|
||||||
@ -13,7 +13,10 @@ export default async function resolve(payload) {
|
|||||||
return await Extension.findOne({
|
return await Extension.findOne({
|
||||||
user_id,
|
user_id,
|
||||||
name: pkgName,
|
name: pkgName,
|
||||||
}).sort({ version: -1 }).limit(1).exec()
|
})
|
||||||
|
.sort({ version: -1 })
|
||||||
|
.limit(1)
|
||||||
|
.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Extension.findOne({
|
return await Extension.findOne({
|
||||||
|
@ -1,41 +1,47 @@
|
|||||||
import { Server } from "linebridge"
|
import { Server } from "linebridge"
|
||||||
import B2 from "backblaze-b2"
|
|
||||||
|
|
||||||
import DbManager from "@shared-classes/DbManager"
|
import DbManager from "@shared-classes/DbManager"
|
||||||
import CacheService from "@shared-classes/CacheService"
|
import CacheService from "@shared-classes/CacheService"
|
||||||
|
import StorageClient from "@shared-classes/StorageClient"
|
||||||
|
|
||||||
import SharedMiddlewares from "@shared-middlewares"
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
|
||||||
class API extends Server {
|
class API extends Server {
|
||||||
static refName = "marketplace"
|
static refName = "marketplace"
|
||||||
static wsRoutesPath = `${__dirname}/ws_routes`
|
static useEngine = "hyper-express-ng"
|
||||||
static routesPath = `${__dirname}/routes`
|
static routesPath = `${__dirname}/routes`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3005
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3005
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
...SharedMiddlewares
|
...SharedMiddlewares,
|
||||||
}
|
}
|
||||||
|
|
||||||
contexts = {
|
contexts = {
|
||||||
db: new DbManager(),
|
db: new DbManager(),
|
||||||
b2: new B2({
|
|
||||||
applicationKeyId: process.env.B2_KEY_ID,
|
|
||||||
applicationKey: process.env.B2_APP_KEY,
|
|
||||||
}),
|
|
||||||
cache: new CacheService({
|
cache: new CacheService({
|
||||||
fsram: false
|
fsram: false,
|
||||||
|
}),
|
||||||
|
storage: StorageClient({
|
||||||
|
endPoint: process.env.B2_ENDPOINT,
|
||||||
|
cdnUrl: process.env.B2_CDN_ENDPOINT,
|
||||||
|
defaultBucket: process.env.B2_BUCKET,
|
||||||
|
accessKey: process.env.B2_KEY_ID,
|
||||||
|
secretKey: process.env.B2_APP_KEY,
|
||||||
|
port: 443,
|
||||||
|
useSSL: true,
|
||||||
|
setupBucket: false,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
await this.contexts.db.initialize()
|
await this.contexts.db.initialize()
|
||||||
await this.contexts.b2.authorize()
|
await this.contexts.storage.initialize()
|
||||||
|
|
||||||
global.cache = this.contexts.cache
|
global.cache = this.contexts.cache
|
||||||
global.b2 = this.contexts.b2
|
global.storages = {
|
||||||
|
standard: this.contexts.storage,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWsAuth = require("@shared-lib/handleWsAuth").default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Boot(API)
|
Boot(API)
|
@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marketplace",
|
"name": "marketplace",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-min": "^1.4.4",
|
"7zip-min": "^1.4.4"
|
||||||
"backblaze-b2": "^1.7.0",
|
|
||||||
"sucrase": "^3.32.0",
|
|
||||||
"uglify-js": "^3.17.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user