bemusic/bemuse

View on GitHub
website/src/pages/music.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * Copyright (c) 2017-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import React, { useEffect, useState } from 'react'

import Layout from '@theme/Layout'
import { MainWrapper } from '../components/main-wrapper'
import { MusicServerData } from '../lib/music'
import { Post } from '../components/post'
import styles from './music.module.css'

/* eslint-disable camelcase */
interface SongInfo {
  title: string
  song_url?: string
  long_url?: string
  bms_url?: string
}

interface ArtistInfo {
  name: string
  sortKey: string
  url: string
  songs: SongInfo[]
}

const songUrl = (song: SongInfo): string | undefined =>
  song.song_url || song.long_url || song.bms_url

const Song = ({ song }: { song: SongInfo }) => {
  const url = songUrl(song)
  if (url) {
    return <a href={url}>{song.title}</a>
  }
  return <span>{song.title}</span>
}

const Artist = ({ artist: { name, songs, url } }: { artist: ArtistInfo }) => (
  <>
    <a href={url}>
      <strong className={styles.artistName}>{name}</strong>
    </a>
    <ul>
      {songs.map((song) => (
        <li key={song.title}>
          <Song song={song} />
        </li>
      ))}
    </ul>
  </>
)

const Artists = ({ artists }: { artists: ArtistInfo[] }) => (
  <ul className={styles.artistsList}>
    {artists.map((artist) => (
      <li key={artist.name}>
        <Artist artist={artist} />
      </li>
    ))}
  </ul>
)

type LoadState =
  | {
      type: 'loading'
    }
  | { type: 'error'; error: string }
  | { type: 'loaded'; songs: MusicServerData['songs'] }

const artistAliases: Record<string, string | undefined> = {
  'BEMUSE SOUND TEAM': 'flicknote',
  'flicknote vs Dekdekbaloo feat. MindaRyn': 'flicknote',
  Larimar: 'Dachs',
  'Nego_tiator/映像:Fiz': 'Nego_tiator',
  'Ym1024 feat. lamie*': 'Ym1024',
}
const artistSortKeys: Record<string, string | undefined> = {
  ああああ: 'aaaa',
  すてらべえ: 'stellabee',
  葵: 'aoi',
}

const computeArtistInfos = (songs?: MusicServerData['songs']): ArtistInfo[] => {
  if (!songs) return []

  const result: ArtistInfo[] = []
  const map = new Map<string, ArtistInfo>()
  for (const song of songs) {
    const artistName =
      song.alias_of || artistAliases[song.artist] || song.artist
    if (!map.has(artistName)) {
      const info: ArtistInfo = {
        name: artistName,
        sortKey: (artistSortKeys[artistName] || artistName).toLowerCase(),
        url: song.artist_url,
        songs: [],
      }
      map.set(artistName, info)
      result.push(info)
    }
    const { songs } = map.get(artistName)!
    songs.push(song)
  }
  return result.sort((a, b) => {
    return a.sortKey < b.sortKey ? -1 : 1
  })
}

const Content = () => {
  const [state, setState] = useState<LoadState>({ type: 'loading' })
  useEffect(
    () =>
      void (async () => {
        const res = await fetch('https://music4.bemuse.ninja/server/index.json')
        if (!res.ok) {
          setState({
            type: 'error',
            error: `Fetching music list failed with HTTP status ${res.status}`,
          })
          return
        }
        const data: MusicServerData = await res.json()
        setState({ type: 'loaded', songs: data.songs })
      })(),
    []
  )

  return (
    <div className={styles.content}>
      {state.type === 'loading' ? (
        <div className={styles.message}>'(Loading song list...)'</div>
      ) : state.type === 'error' ? (
        <div className={styles.message}>`(Error: ${state.error})`</div>
      ) : (
        <Artists artists={computeArtistInfos(state.songs)} />
      )}
    </div>
  )
}

const Music = () => (
  <MainWrapper>
    <Post title='Artists Showcase'>
      <p>
        We'd like to thank the following artists for letting us use their songs
        in the game.
      </p>
      <Content />
    </Post>
  </MainWrapper>
)

export default () => (
  <Layout>
    <Music />
  </Layout>
)