Skip to content
This repository was archived by the owner on Aug 3, 2021. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 26 additions & 24 deletions client/src/ts/components/Beatmap/BeatmapResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,32 @@ const BeatmapResult: FunctionComponent<IProps> = ({
</div>

<div className='is-button-group'>
<a
href='/'
className={clsx(
preview.loading && preview.key === map.key && 'loading',
preview.loading && 'disabled'
)}
onClick={e => {
e.preventDefault()

if (preview.state === 'playing' && preview.key === map.key) {
stopPreview()
} else {
previewBeatmap(map)
}
}}
>
{preview.key !== map.key
? 'Preview'
: preview.loading
? 'Preview'
: preview.error !== null
? 'Playback error!'
: 'Stop Preview'}
</a>
{!map.metadata.requiresExternalAudioFile && (
<a
href='/'
className={clsx(
preview.loading && preview.key === map.key && 'loading',
preview.loading && 'disabled'
)}
onClick={e => {
e.preventDefault()

if (preview.state === 'playing' && preview.key === map.key) {
stopPreview()
} else {
previewBeatmap(map)
}
}}
>
{preview.key !== map.key
? 'Preview'
: preview.loading
? 'Preview'
: preview.error !== null
? 'Playback error!'
: 'Stop Preview'}
</a>
)}
<a href={`beatsaver://${map.key}`}>OneClick&trade;</a>
<a
href='/'
Expand Down
50 changes: 26 additions & 24 deletions client/src/ts/components/Beatmap/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,30 +357,32 @@ const BeatmapDetail: FunctionComponent<IProps> = ({
Download
</a>
<a href={`beatsaver://${map.key}`}>OneClick&trade; Install</a>
<a
href='/'
className={clsx(
preview.loading && preview.key === map.key && 'loading',
preview.loading && 'disabled'
)}
onClick={e => {
e.preventDefault()

if (preview.state === 'playing' && preview.key === map.key) {
stopPreview()
} else {
previewBeatmap(map)
}
}}
>
{preview.loading
? '.'
: preview.key !== map.key
? 'Preview'
: preview.error !== null
? 'Playback error!'
: 'Stop Preview'}
</a>
{!map.metadata.requiresExternalAudioFile && (
<a
href='/'
className={clsx(
preview.loading && preview.key === map.key && 'loading',
preview.loading && 'disabled'
)}
onClick={e => {
e.preventDefault()

if (preview.state === 'playing' && preview.key === map.key) {
stopPreview()
} else {
previewBeatmap(map)
}
}}
>
{preview.loading
? '.'
: preview.key !== map.key
? 'Preview'
: preview.error !== null
? 'Playback error!'
: 'Stop Preview'}
</a>
)}
{/* <a href='/'>View on BeastSaber</a> */}
<a href='/' onClick={e => copyBSR(e)}>
{copied ? (
Expand Down
9 changes: 9 additions & 0 deletions client/src/ts/components/Beatmap/Statistics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ export const BeatmapStats: FunctionComponent<IProps> = ({
hover='Beatmap Duration'
/>
) : null}

{isFullMap(map) && map.metadata.requiresExternalAudioFile ? (
<Statistic
type='text'
emoji='💿'
text='BYOS'
hover='Bring your own song file'
/>
) : null}
</ul>
)
}
Expand Down
1 change: 1 addition & 0 deletions client/src/ts/remote/beatmap.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare interface IBeatmap {
songSubName: string
songAuthorName: string
levelAuthorName: string
requiresExternalAudioFile: boolean

bpm: number
duration?: number
Expand Down
4 changes: 4 additions & 0 deletions client/src/ts/store/audio/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const previewBeatmap: (
) => TypedThunk = beatmap => async (dispatch, getState) => {
stopPreview()(dispatch, getState)

if (beatmap.metadata.requiresExternalAudioFile) {
return
}

dispatch({
payload: true,
type: AudioActionTypes.SET_LOADING,
Expand Down
2 changes: 2 additions & 0 deletions server/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const FILE_EXT_WHITELIST = [
'.jpg',
'.jpeg',
'.srt',
'.bin',
]

export const FILE_TYPE_BLACKLIST = [
Expand Down Expand Up @@ -46,3 +47,4 @@ const SCHEMA_BASE_URI =

export const SCHEMA_INFO = `${SCHEMA_BASE_URI}/info.schema.json`
export const SCHEMA_DIFFICULTY = `${SCHEMA_BASE_URI}/difficulty.schema.json`
export const SCHEMA_AUDIO_CONFIG = `${SCHEMA_BASE_URI}/audio.schema.json`
2 changes: 2 additions & 0 deletions server/src/mongo/models/Beatmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface IBeatmapLean {
songSubName: string
songAuthorName: string
levelAuthorName: string
requiresExternalAudioFile: boolean

duration: number
bpm: number
Expand Down Expand Up @@ -112,6 +113,7 @@ const schema: Schema = new Schema({
type: String,
},
songSubName: { type: String, maxlength: 255, es_indexed: false },
requiresExternalAudioFile: { type: Boolean, default: false },

bpm: { type: Number, required: true },

Expand Down
14 changes: 14 additions & 0 deletions server/src/routes/upload/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,17 @@ export const ERR_BEATMAP_PARSE_TIMEOUT = new CodedError(
'ERR_BEATMAP_PARSE_TIMEOUT',
408
)

export const ERR_BEATMAP_INVALID_AUDIO_CONFIG = new CodedError(
'invalid audio.config file',
0x30012,
'ERR_BEATMAP_INVALID_AUDIO_CONFIG',
400
)

export const ERR_BEATMAP_MISSING_FINGERPRINT = new CodedError(
'beatmap missing fingerprint.bin file',
0x30013,
'ERR_BEATMAP_MISSING_FINGERPRINT',
400
)
72 changes: 53 additions & 19 deletions server/src/routes/upload/parseBeatmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { withFile } from 'tmp-promise'
import {
FILE_EXT_WHITELIST,
FILE_TYPE_BLACKLIST,
SCHEMA_AUDIO_CONFIG,
SCHEMA_DIFFICULTY,
SCHEMA_INFO,
} from '~constants'
Expand All @@ -31,6 +32,8 @@ import {
ERR_BEATMAP_DIFF_NOT_FOUND,
ERR_BEATMAP_INFO_INVALID,
ERR_BEATMAP_INFO_NOT_FOUND,
ERR_BEATMAP_INVALID_AUDIO_CONFIG,
ERR_BEATMAP_MISSING_FINGERPRINT,
} from './errors'

export const parseBeatmap: (
Expand Down Expand Up @@ -78,32 +81,58 @@ export const parseBeatmap: (
if (size.width !== size.height) throw ERR_BEATMAP_COVER_NOT_SQUARE
if (size.width < 256 || size.height < 256) throw ERR_BEATMAP_COVER_TOO_SMOL

let audio: Buffer | undefined
let requiresExternalAudioFile = false
let audioConfigJSON: IAudioConfig | undefined

const audioEntry = zip.file(infoJSON._songFilename)
if (audioEntry === null) {
// Check if this song requires an external audio file
const audioConfigFile = zip.file('audio.json')
if (audioConfigFile !== null) {
const audioConfig = await audioConfigFile.async('text')
if (!validJSON(audioConfig)) throw ERR_BEATMAP_INVALID_AUDIO_CONFIG
const audioConfigJSON = JSON.parse(audioConfig)
const validateAudioConfig = await schemas.compile(SCHEMA_AUDIO_CONFIG)
const audioConfigValid = validateAudioConfig(audioConfigJSON)
if (audioConfigValid === false) {
parseValidationError('audio.json', validateAudioConfig.errors)
}

const fingerprint = zip.file('fingerprint.bin')
if (fingerprint !== null) {
requiresExternalAudioFile = true
} else {
throw ERR_BEATMAP_MISSING_FINGERPRINT
}
}

throw ERR_BEATMAP_AUDIO_NOT_FOUND(infoJSON._songFilename)
}

const audio = await audioEntry.async('nodebuffer')
const audioType = fileType(audio)
if (
audioType === undefined ||
(audioType.mime !== 'audio/ogg' && audioType.mime !== 'audio/wav')
) {
throw ERR_BEATMAP_AUDIO_INVALID
}
if (!requiresExternalAudioFile) {
audio = await audioEntry.async('nodebuffer')
const audioType = fileType(audio)
if (
audioType === undefined ||
(audioType.mime !== 'audio/ogg' && audioType.mime !== 'audio/wav')
) {
throw ERR_BEATMAP_AUDIO_INVALID
}

const { name, ext } = parse(infoJSON._songFilename)
if (ext === '.ogg') {
const eggName = `${name}.egg`
const { name, ext } = parse(infoJSON._songFilename)
if (ext === '.ogg') {
const eggName = `${name}.egg`

zip.remove(infoJSON._songFilename)
zip.file(eggName, audio)
zip.remove(infoJSON._songFilename)
zip.file(eggName, audio)

infoJSON._songFilename = `${name}.egg`
infoDAT = `${JSON.stringify(infoJSON, null, 2)}\n`
infoJSON._songFilename = `${name}.egg`
infoDAT = `${JSON.stringify(infoJSON, null, 2)}\n`

zip.remove(infoDATName)
zip.file(infoDATName, infoDAT)
zip.remove(infoDATName)
zip.file(infoDATName, infoDAT)
}
}

const difficulties = ([] as IDifficultyBeatmap[]).concat(
Expand Down Expand Up @@ -201,7 +230,7 @@ export const parseBeatmap: (
)

const sha1 = hash.digest('hex')
const duration = await getAudioDuration(sha1, audio)
const duration = await getAudioDuration(sha1, audio, audioConfigJSON)
const parsed: IParsedBeatmap = {
hash: sha1,

Expand All @@ -210,6 +239,7 @@ export const parseBeatmap: (
songAuthorName: infoJSON._songAuthorName,
songName: infoJSON._songName,
songSubName: infoJSON._songSubName,
requiresExternalAudioFile,

bpm: infoJSON._beatsPerMinute,
duration,
Expand Down Expand Up @@ -271,8 +301,12 @@ const inspectFile = async (file: JSZip.JSZipObject) => {

const getAudioDuration = async (
hash: string,
audio: Buffer
audio?: Buffer,
audioConfig?: IAudioConfig
): Promise<number> => {
if (audioConfig && !audio) {
return (audioConfig.lengthMs || 0) / 1000.0
}
const args = [
'-v',
'error',
Expand Down
19 changes: 19 additions & 0 deletions server/src/routes/upload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ declare interface IParsedBeatmap {
songSubName: string
songAuthorName: string
levelAuthorName: string
requiresExternalAudioFile: boolean

duration: number
bpm: number
Expand Down Expand Up @@ -111,3 +112,21 @@ declare interface IParsedDifficulty {
njs: number
njsOffset: number
}

declare interface IAudioConfig {
schemaVersion: number
lengthMs: number
notes?: string
downloadUrls?: string[]
knownGoodHashes?: Array<{ type: string, hash: string }>
patches?: {
delayStartMs?: number
padEndMs?: number
trim?: {
startMs?: number
endMs: number
}
fadeIn?: { startMs: number, durationMs: number }
fadeOut?: { startMs: number, durationMs: number }
}
}