2023-05-31 21:17:20 +00:00

575 lines
16 KiB
JavaScript
Executable File

import React from "react"
import * as antd from "antd"
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js"
import PlaylistModel from "models/playlists"
import BasicInformation from "./components/BasicInformation"
import TracksUploads from "./components/TracksUploads"
import "./index.less"
const allowedTrackFieldChanges = [
"title",
"artist",
"cover",
"thumbnail",
"album",
"year",
"genre",
"comment",
"explicit",
"lyricsEnabled",
"spotifyId",
"public",
]
function createDefaultTrackData({
uid,
status = "uploading",
title,
artist,
album,
source,
cover = "https://storage.ragestudio.net/comty-static-assets/default_song.png",
lyricsEnabled = false,
explicit = false,
spotifyId = null,
}) {
return {
uid: uid,
title: title,
artist: artist,
album: album,
source: source,
status: status,
cover: cover,
lyricsEnabled: lyricsEnabled,
explicit: explicit,
spotifyId: spotifyId,
}
}
export default class PlaylistCreatorSteps extends React.Component {
state = {
playlistData: {},
fileList: [],
trackList: [],
pendingTracksUpload: [],
loading: true,
submitting: false,
currentStep: 0,
}
_hacks = {
revertTrackOrders: () => {
this.setState({
trackList: this.state.trackList.reverse()
})
},
enableLyricsForAllTracks: () => {
this.setState({
trackList: this.state.trackList.map((track) => {
track.lyricsEnabled = true
return track
})
})
},
disableLyricsForAllTracks: () => {
this.setState({
trackList: this.state.trackList.map((track) => {
track.lyricsEnabled = false
return track
})
})
},
}
updatePlaylistData = (key, value) => {
this.setState({
playlistData: {
...this.state.playlistData,
[key]: value
}
})
}
updateTrackList = (trackList) => {
this.setState({
trackList
})
}
canSubmit = () => {
const { playlistData, trackList, pendingTracksUpload } = this.state
const hasValidTitle = playlistData.title && playlistData.title.length > 0
const hasTracks = trackList.length > 0
const hasPendingUploads = pendingTracksUpload.length > 0
const tracksHasValidData = trackList.every((track) => {
return track.title !== null && track.title?.length !== 0 && track.source !== null && track.source?.length !== 0
})
return hasValidTitle && hasTracks && !hasPendingUploads && tracksHasValidData
}
submit = async () => {
this.setState({
submitting: true
})
const { playlistData, trackList } = this.state
console.log(`Submitting playlist ${playlistData.title} with ${trackList.length} tracks`, playlistData, trackList)
const result = await PlaylistModel.putPlaylist({
...playlistData,
list: trackList,
})
this.setState({
submitting: false
})
if (result) {
app.message.success("Playlist published")
if (typeof this.props.onModification === "function") {
this.props.onModification()
}
if (typeof this.props.close === "function") {
this.props.close()
}
}
}
// TRACK UPLOADS METHODS
analyzeTrackMetadata = async (file) => {
return new Promise((resolve, reject) => {
jsmediatags.read(file, {
onSuccess: (data) => {
return resolve(data)
},
onError: (error) => {
return reject(error)
}
})
})
}
handleUploadTrack = async (req) => {
const response = await app.cores.remoteStorage.uploadFile(req.file, {
timeout: 2000
}).catch((error) => {
console.error(error)
antd.message.error(error)
req.onError(error)
return false
})
if (response) {
req.onSuccess(response)
}
}
handleTrackDragEnd = (result) => {
if (!result.destination) {
return
}
const trackList = this.state.trackList
const [removed] = trackList.splice(result.source.index, 1)
trackList.splice(result.destination.index, 0, removed)
this.setState({
trackList,
})
}
handleTrackRemove = (uid) => {
this.setState({
fileList: this.state.fileList.filter((file) => file.uid !== uid),
trackList: this.state.trackList.filter((file) => file.uid !== uid),
pendingTracksUpload: this.state.pendingTracksUpload.filter((file_uid) => file_uid !== uid)
})
}
handleTrackInfoChange = (uid, key, value) => {
if (!uid) {
console.error(`Cannot update track withouth uid`)
return
}
let trackList = this.state.trackList
const track = trackList.find((track) => track.uid === uid)
if (typeof key === "object") {
allowedTrackFieldChanges.forEach((_key) => {
if (typeof key[_key] !== "undefined") {
track[_key] = key[_key]
}
})
} else {
if (!allowedTrackFieldChanges.includes(key)) {
console.error(`Cannot update track with key ${key}`)
return
}
track[key] = value
}
this.setState({
trackList: trackList
})
console.log(`New data for track ${uid}: `, this.state.trackList.find((track) => track.uid === uid))
}
handleTrackCoverChange = async (uid, file) => {
if (!uid) {
console.error(`Cannot update track withouth uid`)
return
}
// upload cover file
const result = await app.cores.remoteStorage.uploadFile(file, {
timeout: 2000
})
console.log(`Uploaded cover for track ${uid}: `, result)
if (result) {
this.handleTrackInfoChange(uid, "cover", result.url)
}
}
handleDeletePlaylist = async () => {
if (!this.props.playlist_id) {
console.error(`Cannot delete playlist without id`)
return
}
antd.Modal.confirm({
title: "Are you sure you want to delete this playlist?",
content: "This action cannot be undone",
okText: "Delete",
okType: "danger",
cancelText: "Cancel",
onOk: async () => {
const result = await PlaylistModel.deletePlaylist(this.props.playlist_id)
if (result) {
app.message.success("Playlist deleted")
if (typeof this.props.onModification === "function") {
this.props.onModification()
}
if (typeof this.props.close === "function") {
this.props.close()
}
}
}
})
}
onTrackUploaderChange = async (change) => {
switch (change.file.status) {
case "uploading": {
const { pendingTracksUpload } = this.state
if (!pendingTracksUpload.includes(change.file.uid)) {
pendingTracksUpload.push(change.file.uid)
}
const trackMetadata = await this.analyzeTrackMetadata(change.file.originFileObj)
.catch((error) => {
console.error(`Failed to analyze track metadata: `, error)
// return empty metadata
return {
tags: {}
}
})
console.log(trackMetadata)
if (trackMetadata.tags.picture) {
const data = trackMetadata.tags.picture.data
const format = trackMetadata.tags.picture.format
if (data && format) {
console.log(data, format)
const filenameExt = format.split("/")[1]
const filename = `cover.${filenameExt}`
const byteArray = new Uint8Array(data)
const blob = new Blob([byteArray], { type: data.type })
// create a file object
const file = new File([blob], filename, {
type: format,
})
console.log(file)
this.handleTrackCoverChange(change.file.uid, file)
}
}
this.setState({
pendingTracksUpload: pendingTracksUpload,
fileList: [...this.state.fileList, change.file],
trackList: [...this.state.trackList, createDefaultTrackData({
uid: change.file.uid,
title: trackMetadata.tags.title ?? change.file.name,
artist: trackMetadata.tags.artist ?? null,
album: trackMetadata.tags.album ?? null,
tags: trackMetadata.tags,
})]
})
break
}
case "done": {
// remove pending file
this.setState({
pendingTracksUpload: this.state.pendingTracksUpload.filter((uid) => uid !== change.file.uid)
})
// update file url in the track info
const track = this.state.trackList.find((file) => file.uid === change.file.uid)
if (track) {
track.source = change.file.response.url
track.status = "done"
}
this.setState({
trackList: this.state.trackList
})
break
}
case "error": {
// remove pending file
this.handleTrackRemove(change.file.uid)
// open a dialog to show the error and ask user to retry
antd.Modal.error({
title: "Upload failed",
content: "An error occurred while uploading the file. You want to retry?",
cancelText: "No",
okText: "Retry",
onOk: () => {
this.handleUploadTrack(change)
},
onCancel: () => {
this.handleTrackRemove(change.file.uid)
}
})
}
case "removed": {
this.handleTrackRemove(change.file.uid)
}
default: {
break
}
}
}
steps = [
{
title: "Information",
crender: BasicInformation,
},
{
title: "Tracks",
crender: TracksUploads,
}
]
onChangeStep = (toStep) => {
// check if can change step
if (toStep > this.state.currentStep) {
if (!this.canNextStep()) {
return
}
}
this.setState({
currentStep: toStep
})
}
nextStep = () => {
if (!this.canNextStep()) {
return
}
const nextStep = this.state.currentStep + 1
if (nextStep >= this.steps.length) {
return this.submit()
}
this.setState({
currentStep: nextStep
})
}
previousStep = () => {
const previusStep = this.state.currentStep - 1
if (previusStep < 0) {
return
}
this.setState({
currentStep: previusStep
})
}
canNextStep = () => {
// check current step
switch (this.state.currentStep) {
case 0:
return typeof this.state.playlistData.title === "string" && this.state.playlistData.title.length > 0
case 1:
return this.canSubmit()
default:
return true
}
}
componentDidMount() {
window._hacks = this._hacks
if (this.props.playlist_id) {
this.loadPlaylistData(this.props.playlist_id)
} else {
this.setState({
loading: false
})
}
}
componentWillUnmount() {
delete window._hacks
}
loadPlaylistData = async (playlist_id) => {
console.log(`Loading playlist data for playlist ${playlist_id}...`)
const playlistData = await PlaylistModel.getPlaylist(playlist_id).catch((error) => {
console.error(error)
antd.message.error(error)
return false
})
if (playlistData) {
const trackList = playlistData.list.map((track) => {
return {
...track,
_id: track._id,
uid: track._id,
status: "done",
}
})
this.setState({
playlistData: playlistData,
trackList: trackList,
fileList: trackList.map((track) => {
return {
uid: track.uid,
name: track.title,
status: "done",
url: track.source
}
})
})
}
this.setState({
loading: false
})
}
render() {
if (this.state.loading) {
return <antd.Skeleton active />
}
return <div className="playlistCreator">
<antd.Steps
direction="horizontal"
current={this.state.currentStep}
onChange={this.onChangeStep}
items={this.steps}
/>
<div className="stepContent">
{
React.createElement(this.steps[this.state.currentStep].crender, {
playlist: this.state.playlistData,
trackList: this.state.trackList,
fileList: this.state.fileList,
onTitleChange: (title) => {
this.updatePlaylistData("title", title)
},
onDescriptionChange: (description) => {
this.updatePlaylistData("description", description)
},
onPlaylistCoverChange: (url) => {
this.updatePlaylistData("cover", url)
},
onVisibilityChange: (visibility) => {
this.updatePlaylistData("public", visibility)
},
onDeletePlaylist: this.handleDeletePlaylist,
handleUploadTrack: this.handleUploadTrack,
handleTrackDragEnd: this.handleTrackDragEnd,
handleTrackRemove: this.handleTrackRemove,
handleTrackInfoChange: this.handleTrackInfoChange,
onTrackUploaderChange: this.onTrackUploaderChange,
})
}
</div>
<div className="stepActions">
<antd.Button
onClick={this.previousStep}
disabled={this.state.currentStep === 0}
>
Previous
</antd.Button>
<antd.Button
type="primary"
onClick={this.nextStep}
disabled={!this.canNextStep()}
>
{
this.state.currentStep === this.steps.length - 1 ? "Finish" : "Next"
}
</antd.Button>
</div>
</div>
}
}