mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-07-07 10:35:09 +00:00
- Update API
- Add Chart Preview - Add Drum Type dropdown when the "drums" instrument is selected - Add Min/Max Year to advanced search - Add Track Hash to advanced search - Add "Download Video Backgrounds" setting - Updated and improved detected chart issues
This commit is contained in:
@@ -35,6 +35,7 @@ export interface Settings {
|
||||
zoomFactor: number // How much the display should be zoomed
|
||||
instrument: Instrument | null // The instrument selected by default, or `null` for "Any Instrument"
|
||||
difficulty: Difficulty | null // The difficulty selected by default, or `null` for "Any Difficulty"
|
||||
volume: number // The volume of the chart preview (0-100)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,4 +54,5 @@ export const defaultSettings: Settings = {
|
||||
zoomFactor: 1,
|
||||
instrument: 'guitar',
|
||||
difficulty: null,
|
||||
volume: 50,
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ export const instruments = [
|
||||
'guitar', 'guitarcoop', 'rhythm', 'bass', 'drums', 'keys', 'guitarghl', 'guitarcoopghl', 'rhythmghl', 'bassghl',
|
||||
] as const satisfies Readonly<Instrument[]>
|
||||
export const difficulties = ['expert', 'hard', 'medium', 'easy'] as const satisfies Readonly<Difficulty[]>
|
||||
export const drumTypeNames = ['fourLane', 'fourLanePro', 'fiveLane'] as const
|
||||
export type DrumTypeName = typeof drumTypeNames[number]
|
||||
|
||||
export function instrumentDisplay(instrument: Instrument | null) {
|
||||
switch (instrument) {
|
||||
@@ -101,6 +103,14 @@ export function difficultyDisplay(difficulty: Difficulty | null) {
|
||||
case null: return 'Any Difficulty'
|
||||
}
|
||||
}
|
||||
export function drumTypeDisplay(drumType: DrumTypeName | null) {
|
||||
switch (drumType) {
|
||||
case 'fourLane': return 'Four Lane'
|
||||
case 'fourLanePro': return 'Four Lane Pro'
|
||||
case 'fiveLane': return 'Five Lane'
|
||||
case null: return 'Any Drum Type'
|
||||
}
|
||||
}
|
||||
export function instrumentToDiff(instrument: Instrument | 'vocals') {
|
||||
switch (instrument) {
|
||||
case 'guitar': return 'diff_guitar'
|
||||
@@ -149,27 +159,93 @@ export function removeStyleTags(text: string) {
|
||||
}
|
||||
|
||||
export function hasIssues(chart: Pick<ChartData, 'metadataIssues' | 'folderIssues' | 'notesData'>) {
|
||||
if (chart.metadataIssues.length > 0) { return true }
|
||||
for (const folderIssue of chart.folderIssues) {
|
||||
if (!['albumArtSize', 'invalidIni', 'multipleVideo', 'badIniLine'].includes(folderIssue.folderIssue)) { return true }
|
||||
}
|
||||
for (const chartIssue of chart.notesData?.chartIssues ?? []) {
|
||||
if (chartIssue !== 'isDefaultBPM') { return true }
|
||||
}
|
||||
for (const trackIssue of chart.notesData?.trackIssues ?? []) {
|
||||
for (const ti of trackIssue.trackIssues) {
|
||||
if (ti !== 'noNotesOnNonemptyTrack') { return true }
|
||||
for (const metadataIssue of chart.metadataIssues) {
|
||||
if (!['extraValue'].includes(metadataIssue.metadataIssue)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for (const noteIssue of chart.notesData?.noteIssues ?? []) {
|
||||
for (const ni of noteIssue.noteIssues) {
|
||||
if (ni.issueType !== 'babySustain') { return true }
|
||||
for (const folderIssue of chart.folderIssues) {
|
||||
if (!['albumArtSize', 'invalidIni', 'multipleVideo', 'badIniLine'].includes(folderIssue.folderIssue)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for (const chartIssue of chart.notesData?.chartIssues ?? []) {
|
||||
if (!['isDefaultBPM', 'badEndEvent', 'emptyStarPower', 'emptySoloSection', 'emptyFlexLane', 'babySustain']
|
||||
.includes(chartIssue.noteIssue)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns extension of a file, excluding the dot. (e.g. "song.ogg" -> "ogg")
|
||||
*/
|
||||
export function getExtension(fileName: string) {
|
||||
return _.last(fileName.split('.')) ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns basename of a file, excluding the dot. (e.g. "song.ogg" -> "song")
|
||||
*/
|
||||
export function getBasename(fileName: string) {
|
||||
const parts = fileName.split('.')
|
||||
return parts.length > 1 ? parts.slice(0, -1).join('.') : fileName
|
||||
}
|
||||
/**
|
||||
* @returns `true` if `fileName` is a valid video fileName.
|
||||
*/
|
||||
export function hasVideoName(fileName: string) {
|
||||
return getBasename(fileName) === 'video' && ['mp4', 'avi', 'webm', 'vp8', 'ogv', 'mpeg'].includes(getExtension(fileName))
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `fileName` has a valid chart file extension.
|
||||
*/
|
||||
export function hasChartExtension(fileName: string) {
|
||||
return ['chart', 'mid'].includes(getExtension(fileName).toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `fileName` is a valid chart fileName.
|
||||
*/
|
||||
export function hasChartName(fileName: string) {
|
||||
return ['notes.chart', 'notes.mid'].includes(fileName)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `fileName` has a valid chart audio file extension.
|
||||
*/
|
||||
export function hasAudioExtension(fileName: string) {
|
||||
return ['ogg', 'mp3', 'wav', 'opus'].includes(getExtension(fileName).toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `fileName` has a valid chart audio fileName.
|
||||
*/
|
||||
export function hasAudioName(fileName: string) {
|
||||
return (
|
||||
[
|
||||
'song',
|
||||
'guitar',
|
||||
'bass',
|
||||
'rhythm',
|
||||
'keys',
|
||||
'vocals',
|
||||
'vocals_1',
|
||||
'vocals_2',
|
||||
'drums',
|
||||
'drums_1',
|
||||
'drums_2',
|
||||
'drums_3',
|
||||
'drums_4',
|
||||
'crowd',
|
||||
'preview',
|
||||
].includes(getBasename(fileName)) && ['ogg', 'mp3', 'wav', 'opus'].includes(getExtension(fileName))
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveChartFolderName(
|
||||
chartFolderName: string,
|
||||
chart: { name: string; artist: string; album: string; genre: string; year: string; charter: string },
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Download {
|
||||
action: 'add' | 'remove' | 'retry'
|
||||
md5: string
|
||||
// Should be defined if action === 'add'
|
||||
hasVideoBackground?: boolean
|
||||
chart?: { name: string; artist: string; album: string; genre: string; year: string; charter: string }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EventType, FolderIssueType, Instrument, MetadataIssueType, NotesData } from 'scan-chart'
|
||||
import { FolderIssueType, Instrument, NotesData, ScannedChart } from 'scan-chart'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { difficulties, instruments, Overwrite } from '../UtilFunctions'
|
||||
import { difficulties, drumTypeNames, instruments } from '../UtilFunctions.js'
|
||||
|
||||
export const sources = ['website', 'bridge'] as const
|
||||
|
||||
@@ -10,6 +10,7 @@ export const GeneralSearchSchema = z.object({
|
||||
page: z.number().positive(),
|
||||
instrument: z.enum(instruments).nullable(),
|
||||
difficulty: z.enum(difficulties).nullable(),
|
||||
drumType: z.enum(drumTypeNames).nullable(),
|
||||
source: z.enum(sources).optional(),
|
||||
})
|
||||
export type GeneralSearch = z.infer<typeof GeneralSearchSchema>
|
||||
@@ -25,6 +26,7 @@ export const AdvancedSearchSchema = z.object({
|
||||
return true
|
||||
}, { message: 'Invalid instrument list' }).nullable(),
|
||||
difficulty: z.enum(difficulties).nullable(),
|
||||
drumType: z.enum(drumTypeNames).nullable(),
|
||||
source: z.enum(sources).optional(),
|
||||
name: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
artist: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
@@ -99,14 +101,6 @@ export const ReportSchema = z.object({
|
||||
})
|
||||
export type Report = z.infer<typeof ReportSchema>
|
||||
|
||||
export type NoteStringNotesData = Overwrite<NotesData, {
|
||||
maxNps: {
|
||||
notes: {
|
||||
type: keyof typeof EventType
|
||||
}[]
|
||||
}[]
|
||||
}>
|
||||
|
||||
export interface FolderIssue {
|
||||
folderIssue: FolderIssueType
|
||||
description: string
|
||||
@@ -147,8 +141,11 @@ export interface SearchResult {
|
||||
albumArtMd5: string | null
|
||||
/** The MD5 hash of the chart folder or .sng file. */
|
||||
md5: string
|
||||
/** The MD5 hash of just the .chart or .mid file. */
|
||||
chartMd5: string
|
||||
/**
|
||||
* A blake3 hash of just the chart file and the .ini modifiers that impact chart parsing.
|
||||
* If this changes, the in-game score is reset.
|
||||
*/
|
||||
chartHash: string
|
||||
/**
|
||||
* Different versions of the same chart have the same `versionGroupId`.
|
||||
* All charts in a version group have this set to the smallest `id` in the group.
|
||||
@@ -206,6 +203,17 @@ export interface SearchResult {
|
||||
eighthnote_hopo: boolean | null
|
||||
/** Overrides the .mid note number for Star Power on 5-Fret Guitar. Valid values are 103 and 116. Only applies to .mid charts. */
|
||||
multiplier_note: number | null
|
||||
/**
|
||||
* For .mid charts, setting this causes any sustains not larger than the threshold (in number of ticks) to be reduced to length 0.
|
||||
* By default, this happens to .mid sustains shorter than 1/12 step.
|
||||
*/
|
||||
sustain_cutoff_threshold?: number
|
||||
/**
|
||||
* Notes at or closer than this threshold (in number of ticks) will be merged into a chord.
|
||||
* All note and modifier ticks are set to the tick of the earliest merged note.
|
||||
* All note sustains are set to the length of the shortest merged note.
|
||||
*/
|
||||
chord_snap_threshold?: number
|
||||
/**
|
||||
* The amount of time that should be skipped from the beginning of the video background in milliseconds.
|
||||
* A negative value will delay the start of the video by that many milliseconds.
|
||||
@@ -218,11 +226,11 @@ export interface SearchResult {
|
||||
/** `true` if the chart's end events should be used to end the chart early. Only applies to .mid charts. */
|
||||
end_events: boolean | null
|
||||
/** Data describing properties of the .chart or .mid file. `undefined` if the .chart or .mid file couldn't be parsed. */
|
||||
notesData: NoteStringNotesData
|
||||
notesData: NotesData
|
||||
/** Issues with the chart files. */
|
||||
folderIssues: FolderIssue[]
|
||||
folderIssues: ScannedChart['folderIssues']
|
||||
/** Issues with the chart's metadata. */
|
||||
metadataIssues: MetadataIssueType[]
|
||||
metadataIssues: ScannedChart['metadataIssues']
|
||||
/** `true` if the chart has a video background. */
|
||||
hasVideoBackground: boolean
|
||||
/** The date of the last time this chart was modified in Google Drive. */
|
||||
@@ -249,16 +257,14 @@ export interface SearchResult {
|
||||
/** If there is more than one chart contained inside this `DriveChart`. */
|
||||
driveChartIsPack: boolean
|
||||
/**
|
||||
* A string containing the relative path from the `DriveChart`'s archive to the chart inside the archive.
|
||||
* A string containing the relative path from the driveChart's root to the chart inside it.
|
||||
*
|
||||
* Doesn't contain the archive name, the chart file name (for file charts), or leading/trailing slashes.
|
||||
* This starts with the archive name if the driveChart is an archive.
|
||||
*
|
||||
* An empty string if the `DriveChart` is not an archive.
|
||||
* This ends with the name of the .sng file if this is an .sng file.
|
||||
*
|
||||
* This is an empty string if the driveChart is not an archive or an .sng file.
|
||||
*/
|
||||
archivePath: string
|
||||
/**
|
||||
* The name of the .sng file. `null` if the chart is not a .sng file.
|
||||
*/
|
||||
chartFileName: string | null
|
||||
internalPath: string
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { difficulties, instruments } from './UtilFunctions'
|
||||
import { difficulties, drumTypeNames, instruments } from './UtilFunctions.js'
|
||||
|
||||
export const GeneralSearchSchema = z.object({
|
||||
search: z.string(),
|
||||
@@ -8,14 +8,17 @@ export const GeneralSearchSchema = z.object({
|
||||
page: z.number().positive(),
|
||||
instrument: z.enum(instruments).nullable(),
|
||||
difficulty: z.enum(difficulties).nullable(),
|
||||
drumType: z.enum(drumTypeNames).nullable(),
|
||||
})
|
||||
export type GeneralSearch = z.infer<typeof GeneralSearchSchema>
|
||||
|
||||
const md5Validator = z.string().regex(/^[a-f0-9]{32}$/, 'Invalid MD5 hash')
|
||||
const blakeValidator = z.string().regex(/^[A-Za-z0-9-_]+={0,2}$/, 'Invalid hash')
|
||||
|
||||
export const AdvancedSearchSchema = z.object({
|
||||
instrument: z.enum(instruments).nullable(),
|
||||
difficulty: z.enum(difficulties).nullable(),
|
||||
drumType: z.enum(drumTypeNames).nullable(),
|
||||
name: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
artist: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
album: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
@@ -30,6 +33,8 @@ export const AdvancedSearchSchema = z.object({
|
||||
maxAverageNPS: z.number().nullable(),
|
||||
minMaxNPS: z.number().nullable(),
|
||||
maxMaxNPS: z.number().nullable(),
|
||||
minYear: z.number().nullable(),
|
||||
maxYear: z.number().nullable(),
|
||||
modifiedAfter: z.string().regex(/^\d+-\d{2}-\d{2}$/, 'Invalid date').or(z.coerce.date()).or(z.literal('')).nullable(),
|
||||
hash: z.string().transform(data =>
|
||||
data === '' || data.split(',').every(hash => md5Validator.safeParse(hash).success) ? data : 'invalid'
|
||||
@@ -37,6 +42,9 @@ export const AdvancedSearchSchema = z.object({
|
||||
chartHash: z.string().transform(data =>
|
||||
data === '' || data.split(',').every(hash => md5Validator.safeParse(hash).success) ? data : 'invalid'
|
||||
).nullable().optional(),
|
||||
trackHash: z.string().transform(data =>
|
||||
data === '' || data.split(',').every(hash => blakeValidator.safeParse(hash).success) ? data : 'invalid'
|
||||
).nullable().optional(),
|
||||
hasSoloSections: z.boolean().nullable(),
|
||||
hasForcedNotes: z.boolean().nullable(),
|
||||
hasOpenNotes: z.boolean().nullable(),
|
||||
@@ -70,6 +78,8 @@ export const advancedSearchNumberProperties = [
|
||||
'maxAverageNPS',
|
||||
'minMaxNPS',
|
||||
'maxMaxNPS',
|
||||
'minYear',
|
||||
'maxYear',
|
||||
] as const
|
||||
|
||||
export const advancedSearchBooleanProperties = [
|
||||
|
||||
Reference in New Issue
Block a user