/// <summary> /// Initializes a new instance of the <see cref="LrcFile"/> class. /// </summary> /// <param name="metadata">The metadata.</param> /// <param name="lyrics">The lyrics.</param> /// <param name="applyOffset">if set to <c>true</c> apply the offset in metadata, otherwise ignore the offset.</param> public LrcFile(ILrcMetadata metadata, IEnumerable <IOneLineLyric> lyrics, bool applyOffset) { if (metadata == null) { throw new ArgumentNullException("metadata"); } if (lyrics == null) { throw new ArgumentNullException("lyrics"); } Metadata = metadata; var array = lyrics.OrderBy(l => l.Timestamp).ToArray(); for (var i = 0; i < array.Length; ++i) { if (applyOffset && metadata.Offset.HasValue) { var oneLineLyric = array[i] as OneLineLyric; if (oneLineLyric != null) { oneLineLyric.Timestamp -= metadata.Offset.Value; } else { array[i] = new OneLineLyric(array[i].Timestamp - metadata.Offset.Value, array[i].Content); } } if (i > 0 && array[i].Timestamp == array[i - 1].Timestamp) { throw new FormatException(string.Format("Found duplicate timestamp '{0}' with lyric '{1}' and '{2}'.", array[i].Timestamp, array[i - 1].Content, array[i].Content)); } } _lyrics = array; }
/// <summary> /// Create a new new instance of the <see cref="ILrcFile"/> interface with the specified LRC text. /// </summary> /// <param name="lrcText">The LRC text.</param> /// <param name="allowBracketInContent">If true, brackets are allowed in content and parsed as content (reserved tags are still parsed properly)</param> /// <returns></returns> public static ILrcFile FromText(string lrcText, bool allowBracketInContent = false) { if (lrcText == null) { throw new ArgumentNullException("lrcText"); } var pairs = new List <KeyValuePair <string, string> >(); var titles = new List <string>(); var sb = new StringBuilder(); // 0: Line start. Expect line ending or [. // 1: Reading title. Expect ] or all characters except line ending. // 2: Reading content. Expect line ending or [ or other charactors. var state = 0; for (var i = 0; i <= lrcText.Length; ++i) { var ended = i >= lrcText.Length; var ch = ended ? (char)0 : lrcText[i]; // ReSharper disable once IdentifierTypo var unescaped = false; if (ch == '\\') { ++i; ended = i >= lrcText.Length; if (ended) { throw new FormatException("Expect one charactor after '\\' but reaches the end."); } ch = lrcText[i]; unescaped = true; } switch (state) { case 0: if (!unescaped && ch == '[') { state = 1; } else if (!unescaped && (ch == '\r' || ch == '\n') || ended) { state = 0; } else { throw new FormatException(string.Format("Expect '[' at position {0}", i)); } break; case 1: if (!unescaped && ch == ']') { state = 2; titles.Add(sb.ToString()); sb.Clear(); } else if (!unescaped && (ch == '\r' || ch == '\n') || ended) { throw new FormatException(string.Format("Expect ']' at position {0}", i)); } else { sb.Append(ch); // append to title } break; case 2: if (!unescaped && (ch == '\r' || ch == '\n') || ended) { state = 0; var content = sb.ToString(); pairs.AddRange(titles.Select(t => new KeyValuePair <string, string>(t, content))); sb.Clear(); titles.Clear(); } else if (!unescaped && ch == '[') { if (sb.Length > 0) { state = 1; var content = sb.ToString(); pairs.AddRange(titles.Select(t => new KeyValuePair <string, string>(t, content))); sb.Clear(); titles.Clear(); } else { state = 1; } } else { sb.Append(ch); } break; } } var lyrics = new List <IOneLineLyric>(); var metadata = new LrcMetadata(); string offsetString = null; foreach (var pair in pairs) { // Parse timestamp var match = TimestampRegex.Match(pair.Key); if (match.Success) { var minutes = int.Parse(match.Groups["minutes"].Value); var seconds = double.Parse(match.Groups["seconds"].Value); var timestamp = TimeSpan.FromSeconds(minutes * 60 + seconds); lyrics.Add(new OneLineLyric(timestamp, pair.Value)); continue; } // Parse metadata match = MetadataRegex.Match(pair.Key); if (match.Success) { var title = match.Groups["title"].Value.ToLower(); var content = match.Groups["content"].Value; if (title == "ti") { if (metadata.Title != null && content != metadata.Title) { throw new FormatException(string.Format("Duplicate LRC metadata found. Metadata name: '{0}', Values: '{1}', '{2}'", "ti", metadata.Title, content)); } metadata.Title = content; } else if (title == "ar") { if (metadata.Artist != null && content != metadata.Artist) { throw new FormatException(string.Format("Duplicate LRC metadata found. Metadata name: '{0}', Values: '{1}', '{2}'", "ar", metadata.Artist, content)); } metadata.Artist = content; } else if (title == "al") { if (metadata.Album != null && content != metadata.Album) { throw new FormatException(string.Format("Duplicate LRC metadata found. Metadata name: '{0}', Values: '{1}', '{2}'", "al", metadata.Album, content)); } metadata.Album = content; } else if (title == "by") { if (metadata.Maker != null && content != metadata.Maker) { throw new FormatException(string.Format("Duplicate LRC metadata found. Metadata name: '{0}', Values: '{1}', '{2}'", "by", metadata.Maker, content)); } metadata.Maker = content; } else if (title == "offset") { if (offsetString != null && content != offsetString) { throw new FormatException(string.Format("Duplicate LRC metadata found. Metadata name: '{0}', Values: '{1}', '{2}'", "offset", offsetString, content)); } offsetString = content; metadata.Offset = TimeSpan.FromMilliseconds(double.Parse(content)); } // ReSharper disable once RedundantIfElseBlock else { // Ingore unsupported tag } } else { if (allowBracketInContent) { var lastLyric = lyrics.LastOrDefault(); if (lastLyric != null) { lyrics.Remove(lastLyric); var newLyric = new OneLineLyric(lastLyric.Timestamp, string.Format("{0}[{1}]{2}", lastLyric.Content, pair.Key, pair.Value)); lyrics.Add(newLyric); } } else { throw new FormatException(string.Format("Unknown tag [{0}]", pair.Key)); } } } if (lyrics.Count == 0) { throw new FormatException("Invalid or empty LRC text. Can't find any lyrics."); } return(new LrcFile(metadata, lyrics, true)); }