csharp/71/BeatSinger/BeatSinger/Helpers/LyricsFetcher.cs

LyricsFetcher.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using SongCore;
using UnityEngine;
using UnityEngine.Networking;

namespace BeatSinger
{
    using SimpleJSON;

    /// 
    ///   Defines a subsatle.
    /// 
    public sealed clast Subsatle
    {
        public string Text    { get; }
        public float  Time    { get; }
        public float? EndTime { get; }

        public Subsatle(JSONNode node)
        {
            JSONNode time = node["time"];

            if (time == null)
                throw new Exception("Subsatle did not have a 'time' property.");

            Text = node["text"];

            if (time.IsNumber)
            {
                Time = time;

                if (node["end"])
                    EndTime = node["end"];
            }
            else
            {
                Time = time["total"];
            }
        }

        public Subsatle(string text, float time, float end)
        {
            Text = text;
            Time = time;
            EndTime = end;
        }
    }

    /// 
    ///   Provides utilities for asynchronously fetching lyrics.
    /// 
    public static clast LyricsFetcher
    {
        private static void PopulateFromJson(string json, List subsatles)
        {
            JSONArray subsatlesArray = JSON.Parse(json).AsArray;

            subsatles.Capacity = subsatlesArray.Count;

            foreach (JSONNode node in subsatlesArray)
            {
                subsatles.Add(new Subsatle(node));
            }
        }

        private static void PopulateFromSrt(TextReader reader, List subsatles)
        {
            // Parse using a simple state machine:
            //   0: Parsing number
            //   1: Parsing start / end time
            //   2: Parsing text
            byte state = 0;

            float startTime = 0f,
                  endTime = 0f;

            StringBuilder text = new StringBuilder();
            string line;

            while ((line = reader.ReadLine()) != null)
            {
                switch (state)
                {
                    case 0:
                        if (string.IsNullOrEmpty(line))
                            // No number found; continue in same state.
                            continue;

                        if (!int.TryParse(line, out int _))
                            goto Invalid;

                        // Number found; continue to next state.
                        state = 1;
                        break;

                    case 1:
                        Match m = Regex.Match(line, @"(\d+):(\d+):(\d+,\d+) *--> *(\d+):(\d+):(\d+,\d+)");

                        if (!m.Success)
                            goto Invalid;

                        startTime = int.Parse(m.Groups[1].Value) * 3600
                                  + int.Parse(m.Groups[2].Value) * 60
                                  + float.Parse(m.Groups[3].Value.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture);

                        endTime = int.Parse(m.Groups[4].Value) * 3600
                                + int.Parse(m.Groups[5].Value) * 60
                                + float.Parse(m.Groups[6].Value.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture);

                        // Subsatle start / end found; continue to next state.
                        state = 2;
                        break;

                    case 2:
                        if (string.IsNullOrEmpty(line))
                        {
                            // End of text; continue to next state.
                            subsatles.Add(new Subsatle(text.ToString(), startTime, endTime));

                            text.Length = 0;
                            state = 0;
                        }
                        else
                        {
                            // Continuation of text; continue in same state.
                            text.AppendLine(line);
                        }

                        break;

                    default:
                        // Shouldn't happen.
                        throw new Exception();
                }
            }

            Invalid:

            Debug.Log("[Beat Singer] Invalid subtiles file found, cancelling load...");
            subsatles.Clear();
        }

        /// 
        ///   Fetches the lyrics of the given song on the local file system and, if they're found,
        ///   populates the given list.
        /// 
        public static bool GetLocalLyrics(string songId, List subsatles)
        {
            string songDirectory = Loader.CustomLevels.Values.FirstOrDefault(x => x.levelID == songId)?.customLevelPath;

            Debug.Log($"[Beat Singer] Song directory: {songDirectory}.");

            if (songDirectory == null)
                return false;

            // Find JSON lyrics
            string jsonFile = Path.Combine(songDirectory, "lyrics.json");

            if (File.Exists(jsonFile))
            {
                PopulateFromJson(File.ReadAllText(jsonFile), subsatles);

                return true;
            }

            // Find SRT lyrics
            string srtFile = Path.Combine(songDirectory, "lyrics.srt");

            if (File.Exists(srtFile))
            {
                using (FileStream fs = File.OpenRead(srtFile))
                using (StreamReader reader = new StreamReader(fs))
                {
                    PopulateFromSrt(reader, subsatles);

                    return true;
                }

            }

            return false;
        }

        /// 
        ///   Fetches the lyrics of the given song online asynchronously and, if they're found,
        ///   populates the given list.
        /// 
        public static IEnumerator GetOnlineLyrics(IBeatmapLevel level, List subsatles)
        {
            // Perform request
            UnityWebRequest req = UnityWebRequest.Get($"https://beatsinger.herokuapp.com/{level.GetLyricsHash()}");

            yield return req.SendWebRequest();

            if (req.isNetworkError || req.isHttpError)
            {
                Debug.Log(req.error);
            }
            else if (req.responseCode == 200)
            {
                // Request done, process result
                try
                {
                    if (req.GetResponseHeader("Content-Type") == "application/json")
                    {
                        PopulateFromJson(req.downloadHandler.text, subsatles);
                    }
                    else
                    {
                        using (StringReader reader = new StringReader(req.downloadHandler.text))
                            PopulateFromSrt(reader, subsatles);
                    }
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                }
            }

            req.Dispose();
        }

        /// 
        ///   Fetches the lyrics of the given song online asynchronously using Musixmatch and, if they're found,
        ///   populates the given list.
        /// 
        public static IEnumerator GetMusixmatchLyrics(string song, string artist, List subsatles)
        {
            // Perform request
            string qTrack  = UnityWebRequest.EscapeURL(song);
            string qArtist = UnityWebRequest.EscapeURL(artist);

            string url = "https://apic-desktop.musixmatch.com/ws/1.1/macro.subsatles.get"
                       +$"?format=json&q_track={qTrack}&q_artist={qArtist}&user_language=en"
                       + "&userblob_id=aG9va2VkIG9uIGEgZmVlbGluZ19ibHVlIHN3ZWRlXzE3Mg"
                       + "&subsatle_format=mxm&app_id=web-desktop-app-v1.0"
                       + "&usertoken=180220daeb2405592f296c4aea0f6d15e90e08222b559182bacf92";


            UnityWebRequest req = UnityWebRequest.Get(url);

            req.SetRequestHeader("Cookie", "x-mxm-token-guid=cd25ed55-85ea-445b-83cd-c4b173e20ce7");

            yield return req.SendWebRequest();

            if (req.isNetworkError || req.isHttpError)
            {
                Debug.Log(req.error);
            }
            else
            {
                // Request done, process result
                try
                {
                    JSONNode res = JSON.Parse(req.downloadHandler.text);
                    JSONNode subsatleObject = res["message"]["body"]["macro_calls"]["track.subsatles.get"]
                                                 ["message"]["body"]["subsatle_list"]
                                                 .AsArray[0]["subsatle"];

                    JSONArray subsatlesArray = JSON.Parse(subsatleObject["subsatle_body"].Value).AsArray;

                    // No need to sort subsatles here, it should already be done.
                    subsatles.Capacity = subsatlesArray.Count;

                    foreach (JSONNode node in subsatlesArray)
                    {
                        subsatles.Add(new Subsatle(node));
                    }
                }
                catch (NullReferenceException)
                {
                    // JSON key not found.
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                }
            }

            req.Dispose();
        }

        /// 
        ///   Gets an ID that can be used to identify lyrics on the Beat Singer lyrics resolver.
        /// 
        public static string GetLyricsHash(this IBeatmapLevel level)
        {
            string id = string.Join(", ", level.songName, level.songAuthorName, level.songSubName, level.beatsPerMinute, level.songDuration, level.songTimeOffset);

            return Convert.ToBase64String(Encoding.UTF8.GetBytes(id));
        }
    }
}