feat: combine the frontend and backend to work together
This commit is contained in:
parent
aed9c43d2e
commit
da59f31488
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
@ -1,23 +1,70 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import DiscRow from './components/DiscRow';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { selectDiscs, useAppSelector } from "./store"
|
||||||
import { solid } from '@fortawesome/fontawesome-svg-core/import.macro'
|
import { solid } from '@fortawesome/fontawesome-svg-core/import.macro'
|
||||||
import { selectDiscs, useAppSelector } from './store';
|
import ResourcePackCustomizer from './components/ResourcePackCustomizer';
|
||||||
|
import { CreatePackPayload, createPack, Disc } from './api';
|
||||||
|
|
||||||
|
// TODO: Add waiting icon while creation is taking place
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const discs = useAppSelector(selectDiscs)
|
const [description, setDescription] = useState("")
|
||||||
|
|
||||||
|
const store = useAppSelector(selectDiscs)
|
||||||
|
async function onClick() {
|
||||||
|
const discs: Record<string, Disc> = {}
|
||||||
|
for (var disc of store.discs) {
|
||||||
|
if (disc.title || disc.description || disc.youtubeUrl) {
|
||||||
|
const payloadDisc: Disc = {}
|
||||||
|
if (disc.title) {
|
||||||
|
payloadDisc.title = disc.title
|
||||||
|
}
|
||||||
|
if (disc.description) {
|
||||||
|
payloadDisc.description = disc.description
|
||||||
|
}
|
||||||
|
if (disc.youtubeUrl) {
|
||||||
|
payloadDisc.youtubeUrl = disc.youtubeUrl
|
||||||
|
}
|
||||||
|
discs[disc.resourceName] = payloadDisc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const info: CreatePackPayload = { discs }
|
||||||
|
if (description.length > 0) {
|
||||||
|
info.description = description
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await createPack(info)
|
||||||
|
if (blob) {
|
||||||
|
downloadBlob(blob, "music-pack.zip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBlob(blob: Blob, filename: string) {
|
||||||
|
const url = window.URL.createObjectURL(new Blob([blob]))
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = url
|
||||||
|
link.setAttribute("download", filename)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header>
|
<header>
|
||||||
<button>
|
<button onClick={onClick}>
|
||||||
<FontAwesomeIcon className="margin-right-1" icon={solid("gears")} />
|
<FontAwesomeIcon className="margin-right-1" icon={solid("gears")} />
|
||||||
Generate resource pack
|
Generate resource pack
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{ discs.discs.map((disc, idx) => { return <DiscRow key={idx} discId={idx} /> }) }
|
<hr />
|
||||||
|
<ResourcePackCustomizer
|
||||||
|
description={description}
|
||||||
|
setDescription={setDescription}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
Made By Rokas Puzonas
|
Made By Rokas Puzonas
|
||||||
|
41
client/src/api.ts
Normal file
41
client/src/api.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
const URL = "http://localhost:3001"
|
||||||
|
|
||||||
|
export interface Disc {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
youtubeUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePackPayload {
|
||||||
|
description?: string
|
||||||
|
discs?: Record<string, Disc>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTitle(url: string): Promise<string | undefined> {
|
||||||
|
const res = await fetch(`${URL}/get-title`, {
|
||||||
|
method: "POST",
|
||||||
|
body: url,
|
||||||
|
})
|
||||||
|
if (res.status == 200) {
|
||||||
|
return await res.text()
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPack(info: CreatePackPayload) {
|
||||||
|
const res = await fetch(`${URL}/create`, {
|
||||||
|
method: "POST",
|
||||||
|
mode: "cors",
|
||||||
|
headers: { "Content-Type" : "application/json" },
|
||||||
|
body: JSON.stringify(info),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status == 200) {
|
||||||
|
return await res.blob()
|
||||||
|
} else {
|
||||||
|
console.log(res)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|||||||
import { solid } from "@fortawesome/fontawesome-svg-core/import.macro"
|
import { solid } from "@fortawesome/fontawesome-svg-core/import.macro"
|
||||||
import ReactTooltip from "react-tooltip"
|
import ReactTooltip from "react-tooltip"
|
||||||
import EditNameModal from "./EditNameModal"
|
import EditNameModal from "./EditNameModal"
|
||||||
import { setDescription, setTitle } from "../store/discs"
|
import { setDescription, setTitle, setYoutubeUrl } from "../store/discs"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
import "./DiscRow.css"
|
import "./DiscRow.css"
|
||||||
@ -23,8 +23,9 @@ function DiscRow({ discId }: DiscRowProps) {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
function onReset() {
|
function onReset() {
|
||||||
dispatch(setTitle({ id: discId, title: "" }))
|
dispatch(setTitle({ id: discId, title: undefined }))
|
||||||
dispatch(setDescription({ id: discId, description: "" }))
|
dispatch(setDescription({ id: discId, description: undefined }))
|
||||||
|
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: undefined }))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,17 +16,19 @@ function EditNameModal({ discId, show, onClose }: EditNameModalProps) {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
function setInputTitle(e: React.ChangeEvent<HTMLInputElement>) {
|
function setInputTitle(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
dispatch(setTitle({
|
if (e.target.value.length == 0) {
|
||||||
id: discId,
|
dispatch(setTitle({ id: discId, title: undefined }))
|
||||||
title: e.target.value
|
} else {
|
||||||
}))
|
dispatch(setTitle({ id: discId, title: e.target.value }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setInputDescription(e: React.ChangeEvent<HTMLInputElement>) {
|
function setInputDescription(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
dispatch(setDescription({
|
if (e.target.value.length == 0) {
|
||||||
id: discId,
|
dispatch(setDescription({ id: discId, description: undefined }))
|
||||||
description: e.target.value
|
} else {
|
||||||
}))
|
dispatch(setDescription({ id: discId, description: e.target.value }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Modal show={show} onClose={onClose}>
|
return <Modal show={show} onClose={onClose}>
|
||||||
|
23
client/src/components/ResourcePackCustomizer.tsx
Normal file
23
client/src/components/ResourcePackCustomizer.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { selectDiscs, useAppSelector } from '../store';
|
||||||
|
import DiscRow from './DiscRow';
|
||||||
|
|
||||||
|
interface ResourcePackCustomizerProps {
|
||||||
|
description: string
|
||||||
|
setDescription: { (description: string): void }
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResourcePackCustomizer({ description, setDescription }: ResourcePackCustomizerProps) {
|
||||||
|
const discs = useAppSelector(selectDiscs)
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<label>Resource pack description:</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
/>
|
||||||
|
{ discs.discs.map((disc, idx) => { return <DiscRow key={idx} discId={idx} /> }) }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResourcePackCustomizer
|
@ -1,8 +1,9 @@
|
|||||||
import { selectDiscs, useAppDispatch, useAppSelector } from "../store"
|
import { selectDiscs, useAppDispatch, useAppSelector } from "../store"
|
||||||
import { setAudioUrl } from "../store/discs"
|
import { setDescription, setYoutubeUrl } from "../store/discs"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import Modal from "./Modal"
|
import Modal from "./Modal"
|
||||||
import "./UploadAudioModal.css"
|
import "./UploadAudioModal.css"
|
||||||
|
import { getTitle } from "../api"
|
||||||
|
|
||||||
interface UploadAudioModalProps {
|
interface UploadAudioModalProps {
|
||||||
discId: number
|
discId: number
|
||||||
@ -14,24 +15,36 @@ function UploadAudioModal({ discId, show, onClose }: UploadAudioModalProps) {
|
|||||||
const discs = useAppSelector(selectDiscs)
|
const discs = useAppSelector(selectDiscs)
|
||||||
const disc = discs.discs[discId]
|
const disc = discs.discs[discId]
|
||||||
|
|
||||||
const [url, setUrl] = useState("");
|
|
||||||
const [updateDescription, setUpdateDescription] = useState(true);
|
const [updateDescription, setUpdateDescription] = useState(true);
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
async function setInputYoutubeUrl(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const url = e.target.value
|
||||||
|
if (!url || url.length == 0) {
|
||||||
|
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: undefined }))
|
||||||
|
} else {
|
||||||
|
dispatch(setYoutubeUrl({ id: discId, youtubeUrl: url }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onModalClose() {
|
async function onModalClose() {
|
||||||
onClose()
|
onClose()
|
||||||
dispatch(setAudioUrl({
|
if (updateDescription && disc.youtubeUrl) {
|
||||||
id: discId,
|
const title = await getTitle(disc.youtubeUrl)
|
||||||
audioUrl: url
|
if (title) {
|
||||||
}))
|
dispatch(setDescription({ id: discId, description: title }))
|
||||||
|
} else {
|
||||||
|
dispatch(setDescription({ id: discId, description: "ERROR! Video not found" }))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Modal show={show} onClose={onModalClose}>
|
return <Modal show={show} onClose={onModalClose}>
|
||||||
<label>Youtube URL:</label>
|
<label>Youtube URL:</label>
|
||||||
<input
|
<input
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={setInputYoutubeUrl}
|
||||||
type="text"
|
type="text"
|
||||||
value={url}
|
value={disc.youtubeUrl || ""}
|
||||||
/>
|
/>
|
||||||
<div className="checkbox-group">
|
<div className="checkbox-group">
|
||||||
<label>Update disc description:</label>
|
<label>Update disc description:</label>
|
||||||
|
@ -3,7 +3,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
|||||||
export interface Disc {
|
export interface Disc {
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
audioUrl?: string
|
youtubeUrl?: string
|
||||||
|
resourceName: string
|
||||||
defaultTitle: string
|
defaultTitle: string
|
||||||
defaultDescription: string
|
defaultDescription: string
|
||||||
icon: string
|
icon: string
|
||||||
@ -18,58 +19,72 @@ const defaultTitle = "Music Disc"
|
|||||||
const initialState: DiscsState = {
|
const initialState: DiscsState = {
|
||||||
discs: [
|
discs: [
|
||||||
{
|
{
|
||||||
|
resourceName: "13",
|
||||||
defaultTitle, defaultDescription: "C418 - 13",
|
defaultTitle, defaultDescription: "C418 - 13",
|
||||||
icon: "/sprites/music_disc_13.png"
|
icon: "/sprites/music_disc_13.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "cat",
|
||||||
defaultTitle, defaultDescription: "C418 - cat",
|
defaultTitle, defaultDescription: "C418 - cat",
|
||||||
icon: "/sprites/music_disc_cat.png"
|
icon: "/sprites/music_disc_cat.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "blocks",
|
||||||
defaultTitle, defaultDescription: "C418 - blocks",
|
defaultTitle, defaultDescription: "C418 - blocks",
|
||||||
icon: "/sprites/music_disc_blocks.png"
|
icon: "/sprites/music_disc_blocks.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "chirp",
|
||||||
defaultTitle, defaultDescription: "C418 - chirp",
|
defaultTitle, defaultDescription: "C418 - chirp",
|
||||||
icon: "/sprites/music_disc_chirp.png"
|
icon: "/sprites/music_disc_chirp.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "far",
|
||||||
defaultTitle, defaultDescription: "C418 - far",
|
defaultTitle, defaultDescription: "C418 - far",
|
||||||
icon: "/sprites/music_disc_far.png"
|
icon: "/sprites/music_disc_far.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "mall",
|
||||||
defaultTitle, defaultDescription: "C418 - mall",
|
defaultTitle, defaultDescription: "C418 - mall",
|
||||||
icon: "/sprites/music_disc_mall.png"
|
icon: "/sprites/music_disc_mall.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "mellohi",
|
||||||
defaultTitle, defaultDescription: "C418 - mellohi",
|
defaultTitle, defaultDescription: "C418 - mellohi",
|
||||||
icon: "/sprites/music_disc_mellohi.png"
|
icon: "/sprites/music_disc_mellohi.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "stal",
|
||||||
defaultTitle, defaultDescription: "C418 - stal",
|
defaultTitle, defaultDescription: "C418 - stal",
|
||||||
icon: "/sprites/music_disc_stal.png"
|
icon: "/sprites/music_disc_stal.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "strad",
|
||||||
defaultTitle, defaultDescription: "C418 - strad",
|
defaultTitle, defaultDescription: "C418 - strad",
|
||||||
icon: "/sprites/music_disc_strad.png"
|
icon: "/sprites/music_disc_strad.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "ward",
|
||||||
defaultTitle, defaultDescription: "C418 - ward",
|
defaultTitle, defaultDescription: "C418 - ward",
|
||||||
icon: "/sprites/music_disc_ward.png"
|
icon: "/sprites/music_disc_ward.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "11",
|
||||||
defaultTitle, defaultDescription: "C418 - 11",
|
defaultTitle, defaultDescription: "C418 - 11",
|
||||||
icon: "/sprites/music_disc_11.png"
|
icon: "/sprites/music_disc_11.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "wait",
|
||||||
defaultTitle, defaultDescription: "C418 - wait",
|
defaultTitle, defaultDescription: "C418 - wait",
|
||||||
icon: "/sprites/music_disc_wait.png"
|
icon: "/sprites/music_disc_wait.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "otherside",
|
||||||
defaultTitle, defaultDescription: "Lena Raine - otherside",
|
defaultTitle, defaultDescription: "Lena Raine - otherside",
|
||||||
icon: "/sprites/music_disc_otherside.png"
|
icon: "/sprites/music_disc_otherside.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resourceName: "pigstep",
|
||||||
defaultTitle, defaultDescription: "Lena Raine - Pigstep",
|
defaultTitle, defaultDescription: "Lena Raine - Pigstep",
|
||||||
icon: "/sprites/music_disc_pigstep.png"
|
icon: "/sprites/music_disc_pigstep.png"
|
||||||
},
|
},
|
||||||
@ -80,21 +95,21 @@ export const discsSlice = createSlice({
|
|||||||
name: "discs",
|
name: "discs",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setTitle: (state, action: PayloadAction<{ id: number, title: string }>) => {
|
setTitle: (state, action: PayloadAction<{ id: number, title: string|undefined }>) => {
|
||||||
const { id, title } = action.payload;
|
const { id, title } = action.payload;
|
||||||
state.discs[id].title = title
|
state.discs[id].title = title
|
||||||
},
|
},
|
||||||
setDescription: (state, action: PayloadAction<{ id: number, description: string }>) => {
|
setDescription: (state, action: PayloadAction<{ id: number, description: string|undefined }>) => {
|
||||||
const { id, description } = action.payload;
|
const { id, description } = action.payload;
|
||||||
state.discs[id].description = description
|
state.discs[id].description = description
|
||||||
},
|
},
|
||||||
setAudioUrl: (state, action: PayloadAction<{ id: number, audioUrl: string }>) => {
|
setYoutubeUrl: (state, action: PayloadAction<{ id: number, youtubeUrl: string|undefined }>) => {
|
||||||
const { id, audioUrl } = action.payload;
|
const { id, youtubeUrl } = action.payload;
|
||||||
state.discs[id].audioUrl = audioUrl
|
state.discs[id].youtubeUrl = youtubeUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { setTitle, setDescription, setAudioUrl } = discsSlice.actions
|
export const { setTitle, setDescription, setYoutubeUrl } = discsSlice.actions
|
||||||
|
|
||||||
export default discsSlice.reducer
|
export default discsSlice.reducer
|
||||||
|
14
src/index.ts
14
src/index.ts
@ -3,7 +3,7 @@ import ytdl from "ytdl-core"
|
|||||||
import Joi from "joi"
|
import Joi from "joi"
|
||||||
import JSZip from "jszip"
|
import JSZip from "jszip"
|
||||||
import ffmpeg from "fluent-ffmpeg"
|
import ffmpeg from "fluent-ffmpeg"
|
||||||
import { createWriteStream, readFileSync } from "fs"
|
import { readFileSync } from "fs"
|
||||||
import temp from "temp"
|
import temp from "temp"
|
||||||
|
|
||||||
const availableDiscs = [
|
const availableDiscs = [
|
||||||
@ -13,7 +13,7 @@ const availableDiscs = [
|
|||||||
interface Disc {
|
interface Disc {
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
youtubeUrl: string
|
youtubeUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreatePackPayload {
|
interface CreatePackPayload {
|
||||||
@ -73,7 +73,7 @@ function addCreateRoute(server: hapi.Server) {
|
|||||||
const discValidator = Joi.object<Disc>({
|
const discValidator = Joi.object<Disc>({
|
||||||
title: Joi.string().optional(),
|
title: Joi.string().optional(),
|
||||||
description: Joi.string().optional(),
|
description: Joi.string().optional(),
|
||||||
youtubeUrl: Joi.string().required(),
|
youtubeUrl: Joi.string().optional(),
|
||||||
}).optional()
|
}).optional()
|
||||||
|
|
||||||
const discsValidators: Record<string, Joi.ObjectSchema<Disc>> = {}
|
const discsValidators: Record<string, Joi.ObjectSchema<Disc>> = {}
|
||||||
@ -93,6 +93,7 @@ function addCreateRoute(server: hapi.Server) {
|
|||||||
validate: { payload: payloadValidator }
|
validate: { payload: payloadValidator }
|
||||||
},
|
},
|
||||||
handler: async (req, h) => {
|
handler: async (req, h) => {
|
||||||
|
console.log("CREATE")
|
||||||
const payload = req.payload as CreatePackPayload
|
const payload = req.payload as CreatePackPayload
|
||||||
|
|
||||||
const zip = JSZip();
|
const zip = JSZip();
|
||||||
@ -117,7 +118,7 @@ function addCreateRoute(server: hapi.Server) {
|
|||||||
audioStreams.push(new Promise(async (resolve, reject) => {
|
audioStreams.push(new Promise(async (resolve, reject) => {
|
||||||
const tempFile = temp.path()
|
const tempFile = temp.path()
|
||||||
try {
|
try {
|
||||||
await dowloadYoutubeAudio(url, tempFile)
|
await dowloadYoutubeAudio(url as string, tempFile)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
reject(e)
|
reject(e)
|
||||||
@ -163,8 +164,11 @@ function addCreateRoute(server: hapi.Server) {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const server = hapi.server({
|
const server = hapi.server({
|
||||||
port: 3000,
|
port: process.env.PORT || 3000,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
routes: {
|
||||||
|
cors: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addGetTitleRoute(server)
|
addGetTitleRoute(server)
|
||||||
|
Loading…
Reference in New Issue
Block a user