From 42147f2e2dd12485c8f207d3f37511374e10f0ca Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Tue, 13 Aug 2024 23:35:33 +0300 Subject: [PATCH 01/32] core: remove `^`..`*$` pattern from `Lrc.isValid()` this fixes the heavy matching that could be caused, preventing vm from freezing --- lib/src/lrc_main.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index 2d864a3..71fa8db 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -245,7 +245,7 @@ class Lrc { /// Checks if the string [input] is a valid LRC using Regex. static bool isValid(String input) => RegExp( - r'^([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)*([\r\n]*\[\d\d:\d\d\.\d\d\].*){2,}[\r\n]*$') + r'([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)*([\r\n]*\[\d\d:\d\d\.\d\d\].*){2,}[\r\n]') .hasMatch(input.trim()); @override diff --git a/pubspec.yaml b/pubspec.yaml index c44b72f..40cc002 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.0.2 +version: 1.0.3 repository: https://github.com/Yivan000/lrc environment: From 2131d6246d2a1833736c89d07bfee5a2baeeb4a2 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Thu, 15 Aug 2024 22:42:37 +0300 Subject: [PATCH 02/32] perf: use `StringBuffer` for formatting lrc --- lib/src/lrc_main.dart | 33 +++++++++++++++++---------------- pubspec.yaml | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index 71fa8db..e4f01c5 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -84,24 +84,25 @@ class Lrc { /// Format the lrc to a readable string that can then be /// outputted to an LRC file. String format() { - var output = ''; - - output += (artist != null) ? '[ar:$artist]\n' : ''; - output += (album != null) ? '[al:$album]\n' : ''; - output += (title != null) ? '[ti:$title]\n' : ''; - output += (length != null) ? '[length:$length]\n' : ''; - output += (creator != null) ? '[by:$creator]\n' : ''; - output += (author != null) ? '[au:$author]\n' : ''; - output += (offset != null) ? '[offset:${offset.toString()}]\n' : ''; - output += (program != null) ? '[re:$program]\n' : ''; - output += (version != null) ? '[ve:$version]\n' : ''; - output += (language != null) ? '[la:$language]\n' : ''; - - for (var lyric in lyrics) { - output += lyric.formattedLine + '\n'; + var buffer = StringBuffer(); + + if (artist != null) buffer.writeln('[ar:$artist]'); + if (album != null) buffer.writeln('[al:$album]'); + if (title != null) buffer.writeln('[ti:$title]'); + if (length != null) buffer.writeln('[length:$length]'); + if (creator != null) buffer.writeln('[by:$creator]'); + if (author != null) buffer.writeln('[au:$author]'); + if (offset != null) buffer.writeln('[offset:${offset.toString()}]'); + if (program != null) buffer.writeln('[re:$program]'); + if (version != null) buffer.writeln('[ve:$version]'); + if (language != null) buffer.writeln('[la:$language]'); + + final lrcLength = lyrics.length; + for (var i = 0; i < lrcLength; i++) { + buffer.writeln(lyrics[i].formattedLine); } - return output; + return buffer.toString(); } /// Parses an LRC from a string. Throws a `FormatExeption` diff --git a/pubspec.yaml b/pubspec.yaml index 40cc002..b4f3f75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.0.3 +version: 1.0.4 repository: https://github.com/Yivan000/lrc environment: From 57389fa9dc98be5cd3e937121a8c77e226c62152 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Thu, 15 Aug 2024 22:48:26 +0300 Subject: [PATCH 03/32] feat: parse multi-timestamp lyrics --- lib/src/lrc_main.dart | 115 ++++++++++++++++------------ lib/src/multi_timestamp_parser.dart | 71 +++++++++++++++++ pubspec.yaml | 2 +- 3 files changed, 136 insertions(+), 52 deletions(-) create mode 100644 lib/src/multi_timestamp_parser.dart diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index e4f01c5..c39784f 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -1,3 +1,5 @@ +part 'multi_timestamp_parser.dart'; + /// The parsed LRC class. /// /// You can instantiate this class directly @@ -105,6 +107,28 @@ class Lrc { return buffer.toString(); } + static List _splitLines(String data) { + var lines = []; + var end = data.length; + var sliceStart = 0; + var char = 0; + for (var i = 0; i < end; i++) { + var previousChar = char; + char = data.codeUnitAt(i); + if (char != 13) { + if (char != 10) continue; + if (previousChar == 13) { + sliceStart = i + 1; + continue; + } + } + lines.add(data.substring(sliceStart, i)); + sliceStart = i + 1; + } + if (sliceStart < end) lines.add(data.substring(sliceStart, end)); + return lines; + } + /// Parses an LRC from a string. Throws a `FormatExeption` /// if the inputted string is not valid. static Lrc parse(String parsed) { @@ -115,27 +139,7 @@ class Lrc { } // split string into lines, code from Linesplitter().convert(data) - var lines = ((data) { - var lines = []; - var end = data.length; - var sliceStart = 0; - var char = 0; - for (var i = 0; i < end; i++) { - var previousChar = char; - char = data.codeUnitAt(i); - if (char != 13) { - if (char != 10) continue; - if (previousChar == 13) { - sliceStart = i + 1; - continue; - } - } - lines.add(data.substring(sliceStart, i)); - sliceStart = i + 1; - } - if (sliceStart < end) lines.add(data.substring(sliceStart, end)); - return lines; - })(parsed); + var lines = _splitLines(parsed); // temporary storer variables String? artist, @@ -150,6 +154,7 @@ class Lrc { language; LrcTypes? type; var lyrics = []; + var maxMultiTimestampCountInALine = 0; String? setIfMatchTag(String toMatch, String tag) => (RegExp(r'^\[' + tag + r':.*\]$').hasMatch(toMatch)) @@ -157,23 +162,30 @@ class Lrc { : null; // loop thru each lines - for (var i in lines) { - artist = artist ?? setIfMatchTag(i, 'ar'); - album = album ?? setIfMatchTag(i, 'al'); - title = title ?? setIfMatchTag(i, 'ti'); - author = author ?? setIfMatchTag(i, 'au'); - length = length ?? setIfMatchTag(i, 'length'); - creator = creator ?? setIfMatchTag(i, 'by'); - offset = offset ?? setIfMatchTag(i, 'offset'); - program = program ?? setIfMatchTag(i, 're'); - version = version ?? setIfMatchTag(i, 've'); - language = language ?? setIfMatchTag(i, 'la'); - - if (RegExp(r'^\[\d\d:\d\d\.\d\d\].*$').hasMatch(i)) { - var lyric = i.substring(10).trim(); + for (var l in lines) { + artist ??= setIfMatchTag(l, 'ar'); + album ??= setIfMatchTag(l, 'al'); + title ??= setIfMatchTag(l, 'ti'); + author ??= setIfMatchTag(l, 'au'); + length ??= setIfMatchTag(l, 'length'); + creator ??= setIfMatchTag(l, 'by'); + offset ??= setIfMatchTag(l, 'offset'); + program ??= setIfMatchTag(l, 're'); + version ??= setIfMatchTag(l, 've'); + language ??= setIfMatchTag(l, 'la'); + + if (_LRCMultiTimestampParser._durRegex.hasMatch(l)) { var lineType = LrcTypes.simple; Map? args; + final lrclineDetails = _LRCMultiTimestampParser.parseLine(l); + final lyric = lrclineDetails.lineText; + final timestamps = lrclineDetails.timestamps; + + if (timestamps.length > maxMultiTimestampCountInALine) { + maxMultiTimestampCountInALine = timestamps.length; + } + // checkers for different types of LRCs if (lyric.contains(RegExp(r'^\w:'))) { //if extended @@ -211,23 +223,24 @@ class Lrc { ); } } - final minutes = int.parse(i.substring(1, 3)); - final seconds = int.parse(i.substring(4, 6)); - final hundreds = int.parse(i.substring(7, 9)); - - lyrics.add(LrcLine( - timestamp: Duration( - minutes: minutes, - seconds: seconds, - milliseconds: hundreds * 10, - ), - lyrics: lyric, - type: lineType, - args: args, - )); + + for (final linetimestamp in timestamps) { + lyrics.add(LrcLine( + timestamp: linetimestamp, + lyrics: lrclineDetails.lineText, + type: lineType, + args: args, + )); + } } } + final hadMultiTimestamps = maxMultiTimestampCountInALine > 1; + if (hadMultiTimestamps) { + lyrics.sort((a, b) => + a.timestamp.inMicroseconds.compareTo(b.timestamp.inMicroseconds)); + } + return Lrc( type: type ?? LrcTypes.simple, artist: artist, @@ -247,7 +260,7 @@ class Lrc { /// Checks if the string [input] is a valid LRC using Regex. static bool isValid(String input) => RegExp( r'([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)*([\r\n]*\[\d\d:\d\d\.\d\d\].*){2,}[\r\n]') - .hasMatch(input.trim()); + .hasMatch(input); @override String toString() { @@ -264,7 +277,7 @@ class Lrc { Length: '$length' Language: '$language' Offset: '$offset' - Lyrics: '$lyrics' + Lyrics: $lyrics '''; } } diff --git a/lib/src/multi_timestamp_parser.dart b/lib/src/multi_timestamp_parser.dart new file mode 100644 index 0000000..031dbb9 --- /dev/null +++ b/lib/src/multi_timestamp_parser.dart @@ -0,0 +1,71 @@ +part of 'lrc_main.dart'; + +/// Parses Multi-timestamped lyrics lines effectively. +class _LRCMultiTimestampParser { + const _LRCMultiTimestampParser(); + + /// supports 2-digit minutes, 2-digit seconds & optional 1-2-3-digit hundreds. + static final _durRegex = + RegExp(r'\[([0-9]{2}):([0-9]{2})(\.[0-9]{1,3})?\](.*)'); + + /// Converts multi-timestamped lyrics lines into a pair of `lineText` & `timestamps` list + static _MultiTimeStampDetails parseLine(String line) { + var lineText = line; + final timestamps = []; + + while (true) { + final m = _durRegex.firstMatch(lineText); + if (m != null && m.group(1) != null) { + final timestamp = _durationFromStrings( + minutes: m.group(1), + seconds: m.group(2), + hundreds: m.group(3)?.substring(1), + ); + timestamps.add(timestamp); + lineText = m.group(4) ?? ''; + if (lineText.isEmpty) break; + } else { + break; + } + } + + return _MultiTimeStampDetails( + lineText: lineText, + timestamps: timestamps, + ); + } + + static Duration _durationFromStrings({ + required String? minutes, + required String? seconds, + required String? hundreds, + }) { + if (hundreds != null) { + var zerosToAdd = 6 - hundreds.length; + while (zerosToAdd > 0) { + hundreds = hundreds! + '0'; + zerosToAdd--; + } + } + + final m = minutes == null ? null : int.tryParse(minutes); + final s = seconds == null ? null : int.tryParse(seconds); + final micros = hundreds == null ? null : int.tryParse(hundreds); + + return Duration( + minutes: m ?? 0, + seconds: s ?? 0, + microseconds: micros ?? 0, + ); + } +} + +class _MultiTimeStampDetails { + final String lineText; + final List timestamps; + + const _MultiTimeStampDetails({ + required this.lineText, + required this.timestamps, + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index b4f3f75..35cff0c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.0.4 +version: 1.1.4 repository: https://github.com/Yivan000/lrc environment: From 61f2b6ddd71820a96250eae2676a3a4fecb9bd05 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Thu, 15 Aug 2024 22:48:52 +0300 Subject: [PATCH 04/32] core: add tests --- pubspec.yaml | 2 +- test/lrc.dart | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 test/lrc.dart diff --git a/pubspec.yaml b/pubspec.yaml index 35cff0c..90a0146 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.1.4 +version: 1.1.5 repository: https://github.com/Yivan000/lrc environment: diff --git a/test/lrc.dart b/test/lrc.dart new file mode 100644 index 0000000..6de7a8d --- /dev/null +++ b/test/lrc.dart @@ -0,0 +1,15 @@ +import 'package:lrc/lrc.dart'; +import 'package:test/test.dart'; + +void main() { + test('multi timestamp lrc parsing', () { + const multiTimestampedSyncedLrc = '''[00:11.86]Line 1 lyrics +[00:24]Line 2 lyrics +[00:29.02][00:44.02]Line 3 lyrics +[00:29][00:31.1] Line 4 lyrics'''; + + final parsed = Lrc.parse(multiTimestampedSyncedLrc); + + expect(parsed.lyrics.length, 6); + }); +} From d95622bed067877cbe135a3e7e6ab784b502033a Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Thu, 29 Aug 2024 01:32:33 +0300 Subject: [PATCH 05/32] feat: parse multi language lines separated by `;` ` ` `|` --- lib/src/lrc_main.dart | 22 +++++++++++++++------- pubspec.yaml | 2 +- test/lrc.dart | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index c39784f..5abc3c7 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -129,6 +129,10 @@ class Lrc { return lines; } + static List splitMultiLanguageLine(String lyric) { + return lyric.split(RegExp(r'(\s{2,}|\||;)')); + } + /// Parses an LRC from a string. Throws a `FormatExeption` /// if the inputted string is not valid. static Lrc parse(String parsed) { @@ -224,13 +228,17 @@ class Lrc { } } - for (final linetimestamp in timestamps) { - lyrics.add(LrcLine( - timestamp: linetimestamp, - lyrics: lrclineDetails.lineText, - type: lineType, - args: args, - )); + final lyricSplit = splitMultiLanguageLine(lyric); + for (var i = 0; i < lyricSplit.length; i++) { + final part = lyricSplit[i]; + for (final linetimestamp in timestamps) { + lyrics.add(LrcLine( + timestamp: linetimestamp, + lyrics: part, + type: lineType, + args: args, + )); + } } } } diff --git a/pubspec.yaml b/pubspec.yaml index 90a0146..7bc6e78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.1.5 +version: 1.2.5 repository: https://github.com/Yivan000/lrc environment: diff --git a/test/lrc.dart b/test/lrc.dart index 6de7a8d..c3535c4 100644 --- a/test/lrc.dart +++ b/test/lrc.dart @@ -12,4 +12,23 @@ void main() { expect(parsed.lyrics.length, 6); }); + + test('multi language lrc parsing', () { + const multiLanguageSyncedLrc = '''[by:Trap_Girl] +[00:01.89] +[00:06.87]Yes we started a fire, now the bedroom is burning 我们点燃了一场火,现在卧室正燃着熊熊大火 +[00:12.16]Can we put it out? 我们可以把它扑灭吗? +[00:13.99]'Cause we're both saying things that we're gonna regret when;我们总会说一些令人后悔的事 +[00:19.16]Every word's too loud|每个字都是那么刺耳 +[00:21.12]We gotta slow, slow, slow down +[00:21.12]我们只需要冷静下来 +[00:24.30]Gotta lay low, low, low now/现在一个人出去静静 +[00:28.01]Yeah, we should go, go, go now +[00:28.01]没错我们现在都该离开 +[00:31.57]'Cause things are always better +[00:31.57]事情总会好起来的'''; + + final parsed = Lrc.parse(multiLanguageSyncedLrc); + expect(parsed.lyrics.length, 16); + }); } From 3a9fa8112d2a3ac5ecd55d73f94f2ef2b2ad9619 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Fri, 30 Aug 2024 21:01:08 +0300 Subject: [PATCH 06/32] chore: remove `;` as multi-lang splitter and improve by ensuring more characters left after the separator --- lib/src/lrc_main.dart | 5 +++-- pubspec.yaml | 2 +- test/lrc.dart | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index 5abc3c7..2382be5 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -130,7 +130,7 @@ class Lrc { } static List splitMultiLanguageLine(String lyric) { - return lyric.split(RegExp(r'(\s{2,}|\||;)')); + return lyric.split(RegExp(r'(\s{2,}|\|)(?=\S)')); } /// Parses an LRC from a string. Throws a `FormatExeption` @@ -166,7 +166,8 @@ class Lrc { : null; // loop thru each lines - for (var l in lines) { + for (var i = 0; i < lines.length; i++) { + var l = lines[i]; artist ??= setIfMatchTag(l, 'ar'); album ??= setIfMatchTag(l, 'al'); title ??= setIfMatchTag(l, 'ti'); diff --git a/pubspec.yaml b/pubspec.yaml index 7bc6e78..06cd64a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.2.5 +version: 1.2.7 repository: https://github.com/Yivan000/lrc environment: diff --git a/test/lrc.dart b/test/lrc.dart index c3535c4..3975356 100644 --- a/test/lrc.dart +++ b/test/lrc.dart @@ -18,9 +18,9 @@ void main() { [00:01.89] [00:06.87]Yes we started a fire, now the bedroom is burning 我们点燃了一场火,现在卧室正燃着熊熊大火 [00:12.16]Can we put it out? 我们可以把它扑灭吗? -[00:13.99]'Cause we're both saying things that we're gonna regret when;我们总会说一些令人后悔的事 -[00:19.16]Every word's too loud|每个字都是那么刺耳 -[00:21.12]We gotta slow, slow, slow down +[00:13.99]'Cause we're both saying things that we're gonna regret when|我们总会说一些令人后悔的事 +[00:19.16]Every word's too loud|每个字都是那么刺耳 +[00:21.12]We gotta slow, slow, slow down; [00:21.12]我们只需要冷静下来 [00:24.30]Gotta lay low, low, low now/现在一个人出去静静 [00:28.01]Yeah, we should go, go, go now From 94f9538339378398f3af3a20c3f3dce7e79cc2be Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 5 Mar 2025 17:04:56 +0200 Subject: [PATCH 07/32] core: improve regex matching --- lib/src/lrc_main.dart | 8 +++++--- lib/src/multi_timestamp_parser.dart | 2 +- pubspec.yaml | 2 +- test/lrc.dart | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index 2382be5..82df499 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -202,7 +202,8 @@ class Lrc { 'lyrics': lyric.substring(2) // get the rest of the lyrics }; lineType = LrcTypes.extended; - } else if (lyric.contains(RegExp(r'<\d\d:\d\d\.\d\d>'))) { + } else if (lyric + .contains(RegExp(r'<[0-9]{1,}:[0-9]{1,}(\.[0-9]{1,})?>'))) { // if enhanced type = (type == LrcTypes.extended) ? LrcTypes.extended_enhanced @@ -211,8 +212,9 @@ class Lrc { lineType = LrcTypes.enhanced; // for each timestamp in the line, regex has capturing // groups to make this easier - for (var j in RegExp(r'<((\d\d):(\d\d)\.(\d\d))>([^<]+)') - .allMatches(lyric)) { + for (var j + in RegExp(r'<(([0-9]{1,}):([0-9]{1,})(\.([0-9]{1,}))?)>([^<]+)') + .allMatches(lyric)) { // puts each timestamp+lyrics in the args, no duplicates args.putIfAbsent( j.group(1)!, //the key is the diff --git a/lib/src/multi_timestamp_parser.dart b/lib/src/multi_timestamp_parser.dart index 031dbb9..03ddcdc 100644 --- a/lib/src/multi_timestamp_parser.dart +++ b/lib/src/multi_timestamp_parser.dart @@ -6,7 +6,7 @@ class _LRCMultiTimestampParser { /// supports 2-digit minutes, 2-digit seconds & optional 1-2-3-digit hundreds. static final _durRegex = - RegExp(r'\[([0-9]{2}):([0-9]{2})(\.[0-9]{1,3})?\](.*)'); + RegExp(r'\[([0-9]{1,}):(\d{1,})(\.[0-9]{1,})?\](.*)'); /// Converts multi-timestamped lyrics lines into a pair of `lineText` & `timestamps` list static _MultiTimeStampDetails parseLine(String line) { diff --git a/pubspec.yaml b/pubspec.yaml index 06cd64a..522ff3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.2.7 +version: 1.2.8 repository: https://github.com/Yivan000/lrc environment: diff --git a/test/lrc.dart b/test/lrc.dart index 3975356..d0fc5fb 100644 --- a/test/lrc.dart +++ b/test/lrc.dart @@ -6,11 +6,14 @@ void main() { const multiTimestampedSyncedLrc = '''[00:11.86]Line 1 lyrics [00:24]Line 2 lyrics [00:29.02][00:44.02]Line 3 lyrics -[00:29][00:31.1] Line 4 lyrics'''; +[00:29][00:31.1] Line 4 lyrics +[102:29][104:31.1] Line 5 lyrics +[202:30] Line 6 lyrics +'''; final parsed = Lrc.parse(multiTimestampedSyncedLrc); - expect(parsed.lyrics.length, 6); + expect(parsed.lyrics.length, 9); }); test('multi language lrc parsing', () { From 596b63378c7260c380346bdc8842e8a726287a64 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 5 Mar 2025 17:05:22 +0200 Subject: [PATCH 08/32] chore: losen 'isValid' matching --- lib/src/lrc_main.dart | 5 ++--- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index 82df499..ae6f8d3 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -269,9 +269,8 @@ class Lrc { } /// Checks if the string [input] is a valid LRC using Regex. - static bool isValid(String input) => RegExp( - r'([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)*([\r\n]*\[\d\d:\d\d\.\d\d\].*){2,}[\r\n]') - .hasMatch(input); + static bool isValid(String input) => + RegExp(r'[\d{1,}:\d{1,}(\.\d{1,})?\].*)[\r\n]').hasMatch(input); @override String toString() { diff --git a/pubspec.yaml b/pubspec.yaml index 522ff3c..a99b51b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.2.8 +version: 1.2.9 repository: https://github.com/Yivan000/lrc environment: From dc4a21ffc7a60673756f32f29904ff5f41f284e8 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 5 Mar 2025 17:12:21 +0200 Subject: [PATCH 09/32] feat: add `Lrc.cleanPlainLyrics()` --- lib/src/lrc_main.dart | 6 ++++++ pubspec.yaml | 2 +- test/lrc.dart | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index ae6f8d3..aed5867 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -272,6 +272,12 @@ class Lrc { static bool isValid(String input) => RegExp(r'[\d{1,}:\d{1,}(\.\d{1,})?\].*)[\r\n]').hasMatch(input); + static String cleanPlainLyrics(String input) { + final regex = RegExp( + r'([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)'); + return input.replaceAll(regex, ''); + } + @override String toString() { var lyrics = this.lyrics.join('\n'); diff --git a/pubspec.yaml b/pubspec.yaml index a99b51b..8b7155c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.2.9 +version: 1.3.0 repository: https://github.com/Yivan000/lrc environment: diff --git a/test/lrc.dart b/test/lrc.dart index d0fc5fb..1dce345 100644 --- a/test/lrc.dart +++ b/test/lrc.dart @@ -34,4 +34,19 @@ void main() { final parsed = Lrc.parse(multiLanguageSyncedLrc); expect(parsed.lyrics.length, 16); }); + + test('clean plain lyrics', () { + const plainLyrics = '''[ti:Space Song] +[ar:Beach House] +[by:Generated using SongSync] +It was late at night, you held on tight +From an empty seat, a flash of light +It will take a while to make you smile +Somewhere in these eyes, I'm on your side +'''; + + final cleaned = Lrc.cleanPlainLyrics(plainLyrics); + + expect(cleaned.startsWith('It was late'), true); + }); } From e0e6779edd38f6e80e866457a0383565c3ece595 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Fri, 29 Aug 2025 18:02:51 +0200 Subject: [PATCH 10/32] fix: properly sort if lrc has another language in the end of the file --- lib/src/lrc_main.dart | 20 +++++++++++++++----- pubspec.yaml | 2 +- test/lrc.dart | 25 ++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index aed5867..ec4bd1d 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -158,7 +158,9 @@ class Lrc { language; LrcTypes? type; var lyrics = []; - var maxMultiTimestampCountInALine = 0; + + var registeredTimestamps = {}; + var shouldSortLyrics = false; String? setIfMatchTag(String toMatch, String tag) => (RegExp(r'^\[' + tag + r':.*\]$').hasMatch(toMatch)) @@ -187,8 +189,16 @@ class Lrc { final lyric = lrclineDetails.lineText; final timestamps = lrclineDetails.timestamps; - if (timestamps.length > maxMultiTimestampCountInALine) { - maxMultiTimestampCountInALine = timestamps.length; + if (shouldSortLyrics == false) { + if (timestamps.length > 1) { + // -- means it has multi timestamps + shouldSortLyrics = true; + } + if (timestamps + .any((element) => registeredTimestamps.contains(element))) { + // -- means it has multi language in the end of the file + shouldSortLyrics = true; + } } // checkers for different types of LRCs @@ -235,6 +245,7 @@ class Lrc { for (var i = 0; i < lyricSplit.length; i++) { final part = lyricSplit[i]; for (final linetimestamp in timestamps) { + registeredTimestamps.add(linetimestamp); lyrics.add(LrcLine( timestamp: linetimestamp, lyrics: part, @@ -246,8 +257,7 @@ class Lrc { } } - final hadMultiTimestamps = maxMultiTimestampCountInALine > 1; - if (hadMultiTimestamps) { + if (shouldSortLyrics) { lyrics.sort((a, b) => a.timestamp.inMicroseconds.compareTo(b.timestamp.inMicroseconds)); } diff --git a/pubspec.yaml b/pubspec.yaml index 8b7155c..f1e505e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.3.0 +version: 1.3.1 repository: https://github.com/Yivan000/lrc environment: diff --git a/test/lrc.dart b/test/lrc.dart index 1dce345..067f304 100644 --- a/test/lrc.dart +++ b/test/lrc.dart @@ -1,6 +1,7 @@ -import 'package:lrc/lrc.dart'; import 'package:test/test.dart'; +import 'package:lrc/lrc.dart'; + void main() { test('multi timestamp lrc parsing', () { const multiTimestampedSyncedLrc = '''[00:11.86]Line 1 lyrics @@ -33,6 +34,28 @@ void main() { final parsed = Lrc.parse(multiLanguageSyncedLrc); expect(parsed.lyrics.length, 16); + + const multiLanguageSyncedLrc2 = '''[by:Trap_Girl] +[00:01.89] +[00:06.87]Yes we started a fire, now the bedroom is burning +[00:12.16]Can we put it out? +[00:13.99]'Cause we're both saying things that we're gonna regret when +[00:19.16]Every word's too loud +[00:21.12]We gotta slow, slow, slow down; +[00:24.30]Gotta lay low, low, low now +[00:28.01]Yeah, we should go, go, go now +[00:31.57]'Cause things are always better +[00:06.87]我们点燃了一场火,现在卧室正燃着熊熊大火 +[00:12.16]我们可以把它扑灭吗? +[00:13.99]我们总会说一些令人后悔的事 +[00:19.16]每个字都是那么刺耳 +[00:24.30]现在一个人出去静静 +[00:21.12]我们只需要冷静下来 +[00:28.01]没错我们现在都该离开 +[00:31.57]事情总会好起来的'''; + + final parsed2 = Lrc.parse(multiLanguageSyncedLrc2); + expect(parsed2.lyrics.length, 17); }); test('clean plain lyrics', () { From e31ae5c8c7c44ba9c70263a8819be402ddb4defe Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 3 Dec 2025 17:23:57 +0200 Subject: [PATCH 11/32] core: improve enhanced lyrics extraction & optimize code --- lib/src/lrc_main.dart | 174 ++++++++++++++++++++++++++++++------------ pubspec.yaml | 4 +- 2 files changed, 129 insertions(+), 49 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index ec4bd1d..ca6e51d 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -6,61 +6,63 @@ part 'multi_timestamp_parser.dart'; /// or parse a string using `Lrc.parse()`. class Lrc { /// The overall type of LRC for this object - LrcTypes type; + final LrcTypes type; /// The name of the artist of the song (optional) /// /// This corresponds to the ID tag `[ar:]`. - String? artist; + final String? artist; /// The name of the album of the song (optional) /// /// This corresponds to the ID tag `[al:]`. - String? album; + final String? album; /// The title of the song (optional) /// /// This corresponds to the ID tag `[ti:]`. - String? title; + final String? title; /// The name of the author of the lyrics (optional) /// /// This corresponds to the ID tag `[au:]`. - String? author; + final String? author; /// The name of the creator of the LRC file (optional) /// /// This corresponds to the ID tag `[by:]`. - String? creator; + final String? creator; /// The name of the program that created the LRC file (optional) /// /// This corresponds to the ID tag `[re:]`. - String? program; + final String? program; /// The version of the program that created the LRC file (optional) /// /// This corresponds to the ID tag `[ve:]`. - String? version; + final String? version; /// The length of the song (optional) /// /// This corresponds to the ID tag `[length:]`. - String? length; + final String? length; /// The language of the song, using an IETF BCP 47 language tag (optional) /// /// This corresponds to the ID tag `[la:]`. - String? language; + final String? language; /// Offset of time in milliseconds, can be positive [shifts time up] /// or negative [shifts time down] (optional) /// /// This corresponds to the ID tag `[offset:]`. - int? offset; + final int? offset; /// The list of lyric lines - List lyrics; + final List lyrics; + + final int personCount; /// Handy parameter to get a stream of the lyrics. /// See `List.toStream()`. @@ -68,7 +70,7 @@ class Lrc { /// Use this constructor if you want to manually create an LRC from scratch. /// Otherwise, parse an LRC string using [Lrc.parse]. - Lrc({ + const Lrc({ this.type = LrcTypes.simple, required this.lyrics, this.artist, @@ -81,6 +83,7 @@ class Lrc { this.length, this.offset, this.language, + this.personCount = 1, }); /// Format the lrc to a readable string that can then be @@ -133,6 +136,21 @@ class Lrc { return lyric.split(RegExp(r'(\s{2,}|\|)(?=\S)')); } + static Iterable extractTimeStampPartFromLine(String line) sync* { + for (var j in RegExp(r'<(([0-9]{1,}):([0-9]{1,})\.([0-9]{1,})?)>([^<]+)') + .allMatches(line)) { + final timestamp = Duration( + minutes: int.tryParse(j.group(2)!) ?? 0, + seconds: int.tryParse(j.group(3)!) ?? 0, + milliseconds: (int.tryParse(j.group(4)!) ?? 0), + ); + yield LrcLinePart( + timestamp: timestamp, + lyrics: j.group(5)?.trim() ?? '', + ); + } + } + /// Parses an LRC from a string. Throws a `FormatExeption` /// if the inputted string is not valid. static Lrc parse(String parsed) { @@ -159,6 +177,7 @@ class Lrc { LrcTypes? type; var lyrics = []; + final personToIndex = {}; var registeredTimestamps = {}; var shouldSortLyrics = false; @@ -183,7 +202,7 @@ class Lrc { if (_LRCMultiTimestampParser._durRegex.hasMatch(l)) { var lineType = LrcTypes.simple; - Map? args; + List? parts; final lrclineDetails = _LRCMultiTimestampParser.parseLine(l); final lyric = lrclineDetails.lineText; @@ -201,16 +220,24 @@ class Lrc { } } + int? person; + // checkers for different types of LRCs if (lyric.contains(RegExp(r'^\w:'))) { //if extended type = (type == LrcTypes.enhanced) ? LrcTypes.extended_enhanced : LrcTypes.extended; - args = { - 'letter': lyric[0], // get the letter of the type of person - 'lyrics': lyric.substring(2) // get the rest of the lyrics - }; + + final personText = lyric[0]; + person = personToIndex[personText] ??= personToIndex.length + 1; + + parts = []; + parts.add(LrcLinePart( + timestamp: timestamps.first, + lyrics: lyric.substring(2), + )); + lineType = LrcTypes.extended; } else if (lyric .contains(RegExp(r'<[0-9]{1,}:[0-9]{1,}(\.[0-9]{1,})?>'))) { @@ -218,50 +245,65 @@ class Lrc { type = (type == LrcTypes.extended) ? LrcTypes.extended_enhanced : LrcTypes.enhanced; - args = {}; + parts = []; lineType = LrcTypes.enhanced; - // for each timestamp in the line, regex has capturing - // groups to make this easier - for (var j - in RegExp(r'<(([0-9]{1,}):([0-9]{1,})(\.([0-9]{1,}))?)>([^<]+)') - .allMatches(lyric)) { - // puts each timestamp+lyrics in the args, no duplicates - args.putIfAbsent( - j.group(1)!, //the key is the - () => { - // the value is another map with the duration and lyrics - 'duration': Duration( - minutes: int.parse(j.group(2)!), - seconds: int.parse(j.group(3)!), - milliseconds: int.parse(j.group(4)!) * 10, - ), - 'lyrics': j.group(5)!.trim() - }, - ); - } + + final personText = lyric.substring(0, lyric.indexOf('<')); + person = personToIndex[personText] ??= personToIndex.length + 1; + + parts.addAll(extractTimeStampPartFromLine(lyric)); } final lyricSplit = splitMultiLanguageLine(lyric); for (var i = 0; i < lyricSplit.length; i++) { final part = lyricSplit[i]; + final readableText = parts != null && parts.isNotEmpty + ? parts.map((e) => e.lyrics).join(' ') + : part; for (final linetimestamp in timestamps) { registeredTimestamps.add(linetimestamp); lyrics.add(LrcLine( timestamp: linetimestamp, lyrics: part, + readableText: readableText.isNotEmpty ? readableText : part, type: lineType, - args: args, + parts: parts, + person: person, )); } } } } + if (type == LrcTypes.enhanced || type == LrcTypes.extended_enhanced) { + // extract bg tags + for (final m in RegExp(r'\[bg:(.*)\]').allMatches(parsed)) { + final text = m.group(1); + if (text == null) continue; + final parts = extractTimeStampPartFromLine(text).toList(); + if (parts.isEmpty) continue; + final newLine = LrcLine( + timestamp: parts[0].timestamp, + lyrics: text, + readableText: parts.map((e) => e.lyrics).join(' '), + type: type ?? LrcTypes.enhanced, + parts: parts, + person: 0, + ); + lyrics.add(newLine); + shouldSortLyrics = true; + } + } + if (shouldSortLyrics) { lyrics.sort((a, b) => a.timestamp.inMicroseconds.compareTo(b.timestamp.inMicroseconds)); } + var personCount = + personToIndex.values.where((element) => element > 0).length; + if (personCount <= 0) personCount = 1; + return Lrc( type: type ?? LrcTypes.simple, artist: artist, @@ -275,6 +317,7 @@ class Lrc { version: version, lyrics: lyrics, language: language, + personCount: personCount, ); } @@ -326,24 +369,42 @@ enum LrcTypes { ///A line of lyrics, with its defined duration and raw lyrics class LrcLine { ///timestamp for the lyrics wherein it'll be displayed - Duration timestamp; + final Duration timestamp; ///the raw lyrics for the line - String lyrics; + final String lyrics; + + final String readableText; - ///the additional arguments for other lrc types - Map? args; + final List? parts; ///the type of lrc for this line - LrcTypes type; + final LrcTypes type; + + final int? person; + + bool get isBGLyrics => person == 0; - LrcLine({ + const LrcLine({ required this.timestamp, required this.lyrics, + required this.readableText, required this.type, - this.args, + required this.parts, + required this.person, }); + LrcLine withTimeStamp({required Duration newTimestamp}) { + return LrcLine( + timestamp: newTimestamp, + lyrics: lyrics, + readableText: readableText, + type: type, + person: person, + parts: parts, + ); + } + ///get the string for a formatted line String get formattedLine { ///function to add leading zeros @@ -362,7 +423,26 @@ class LrcLine { return ''' Timestamp: '$timestamp' Lyrics: '$lyrics' - Args: '$args' + Parts: '$parts' + Person: '$person' + '''; + } +} + +class LrcLinePart { + final Duration timestamp; + final String lyrics; + + const LrcLinePart({ + required this.timestamp, + required this.lyrics, + }); + + @override + String toString() { + return ''' + Timestamp: '$timestamp' + Lyrics: '$lyrics' '''; } } diff --git a/pubspec.yaml b/pubspec.yaml index f1e505e..116f28d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.3.1 +version: 1.3.5 repository: https://github.com/Yivan000/lrc environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=3.6.0 <4.0.0" dev_dependencies: From 3b34a63883d26dca4abb01fc51724fc6281cadc9 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 3 Dec 2025 17:45:44 +0200 Subject: [PATCH 12/32] chore: improve `isValid` regex --- lib/src/lrc_main.dart | 4 +--- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart index ca6e51d..779d8c4 100644 --- a/lib/src/lrc_main.dart +++ b/lib/src/lrc_main.dart @@ -154,8 +154,6 @@ class Lrc { /// Parses an LRC from a string. Throws a `FormatExeption` /// if the inputted string is not valid. static Lrc parse(String parsed) { - parsed = parsed.trim(); - if (!isValid(parsed)) { throw FormatException('The inputted string is not a valid LRC file'); } @@ -323,7 +321,7 @@ class Lrc { /// Checks if the string [input] is a valid LRC using Regex. static bool isValid(String input) => - RegExp(r'[\d{1,}:\d{1,}(\.\d{1,})?\].*)[\r\n]').hasMatch(input); + RegExp(r'\[(\d{1,}).+\]').hasMatch(input); static String cleanPlainLyrics(String input) { final regex = RegExp( diff --git a/pubspec.yaml b/pubspec.yaml index 116f28d..35b0448 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.3.5 +version: 1.3.6 repository: https://github.com/Yivan000/lrc environment: From d7fec079a9b80aa01c68818e7e80a300105d460a Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 3 Dec 2025 17:57:17 +0200 Subject: [PATCH 13/32] code: project restructure --- lib/lrc.dart | 9 +- lib/src/core/enums.dart | 16 + lib/src/core/extensions.dart | 36 ++ lib/src/lrc_main.dart | 519 ------------------ lib/src/models/lrc.dart | 133 +++++ lib/src/models/lrc_line.dart | 64 +++ lib/src/models/lrc_line_part.dart | 19 + lib/src/models/lrc_stream.dart | 39 ++ lib/src/parsers/lrc_parser.dart | 222 ++++++++ .../{ => parsers}/multi_timestamp_parser.dart | 2 +- pubspec.yaml | 2 +- 11 files changed, 539 insertions(+), 522 deletions(-) create mode 100644 lib/src/core/enums.dart create mode 100644 lib/src/core/extensions.dart delete mode 100644 lib/src/lrc_main.dart create mode 100644 lib/src/models/lrc.dart create mode 100644 lib/src/models/lrc_line.dart create mode 100644 lib/src/models/lrc_line_part.dart create mode 100644 lib/src/models/lrc_stream.dart create mode 100644 lib/src/parsers/lrc_parser.dart rename lib/src/{ => parsers}/multi_timestamp_parser.dart (98%) diff --git a/lib/lrc.dart b/lib/lrc.dart index 0ff3b73..229259f 100644 --- a/lib/lrc.dart +++ b/lib/lrc.dart @@ -1,4 +1,11 @@ /// The main library for support for LRCs. library lrc; -export 'src/lrc_main.dart'; +part 'src/core/enums.dart'; +part 'src/core/extensions.dart'; +part 'src/models/lrc.dart'; +part 'src/models/lrc_line.dart'; +part 'src/models/lrc_line_part.dart'; +part 'src/models/lrc_stream.dart'; +part 'src/parsers/lrc_parser.dart'; +part 'src/parsers/multi_timestamp_parser.dart'; diff --git a/lib/src/core/enums.dart b/lib/src/core/enums.dart new file mode 100644 index 0000000..c663abf --- /dev/null +++ b/lib/src/core/enums.dart @@ -0,0 +1,16 @@ +part of lrc; + +///The types of LRC +enum LrcTypes { + ///A simple LRC, with no extra formatting, etc + simple, + + ///LRC with modifiers at the start in the form `A: foo` + extended, + + ///LRC with additional timestamps per line in the form `<00:00.00> foo` + enhanced, + + ///LRC that some lines are extended and some are enhanced + extended_enhanced +} diff --git a/lib/src/core/extensions.dart b/lib/src/core/extensions.dart new file mode 100644 index 0000000..207ff9f --- /dev/null +++ b/lib/src/core/extensions.dart @@ -0,0 +1,36 @@ +part of lrc; + +/// Handy extensions on lists of LrcLine +extension LrcLineExtensions on List { + /// Creates a stream for each lyric using their durations + Stream toStream() async* { + for (var i = 0; i < length; i++) { + var lineCurrent = this[i]; + var lineNext = (i + 1 < length) ? this[i + 1] : null; + var durationToNext = (lineNext != null) + ? Duration( + milliseconds: lineNext.timestamp.inMilliseconds - + lineCurrent.timestamp.inMilliseconds) + : null; + yield LrcStream( + duration: durationToNext, + previous: (i != 0) ? this[i - 1] : null, + current: lineCurrent, + next: lineNext, + position: i, + length: length - 1); + if (durationToNext != null) { + await Future.delayed(durationToNext); + } + } + } +} + +/// Handy extensions on strings +extension StringExtensions on String { + /// Handy extension method that parses the string to an [Lrc] + Lrc toLrc() => LrcParser.parse(this); + + /// Handy extension getter if the given string is a valid LRC + bool get isValidLrc => LrcParser.isValid(this); +} diff --git a/lib/src/lrc_main.dart b/lib/src/lrc_main.dart deleted file mode 100644 index 779d8c4..0000000 --- a/lib/src/lrc_main.dart +++ /dev/null @@ -1,519 +0,0 @@ -part 'multi_timestamp_parser.dart'; - -/// The parsed LRC class. -/// -/// You can instantiate this class directly -/// or parse a string using `Lrc.parse()`. -class Lrc { - /// The overall type of LRC for this object - final LrcTypes type; - - /// The name of the artist of the song (optional) - /// - /// This corresponds to the ID tag `[ar:]`. - final String? artist; - - /// The name of the album of the song (optional) - /// - /// This corresponds to the ID tag `[al:]`. - final String? album; - - /// The title of the song (optional) - /// - /// This corresponds to the ID tag `[ti:]`. - final String? title; - - /// The name of the author of the lyrics (optional) - /// - /// This corresponds to the ID tag `[au:]`. - final String? author; - - /// The name of the creator of the LRC file (optional) - /// - /// This corresponds to the ID tag `[by:]`. - final String? creator; - - /// The name of the program that created the LRC file (optional) - /// - /// This corresponds to the ID tag `[re:]`. - final String? program; - - /// The version of the program that created the LRC file (optional) - /// - /// This corresponds to the ID tag `[ve:]`. - final String? version; - - /// The length of the song (optional) - /// - /// This corresponds to the ID tag `[length:]`. - final String? length; - - /// The language of the song, using an IETF BCP 47 language tag (optional) - /// - /// This corresponds to the ID tag `[la:]`. - final String? language; - - /// Offset of time in milliseconds, can be positive [shifts time up] - /// or negative [shifts time down] (optional) - /// - /// This corresponds to the ID tag `[offset:]`. - final int? offset; - - /// The list of lyric lines - final List lyrics; - - final int personCount; - - /// Handy parameter to get a stream of the lyrics. - /// See `List.toStream()`. - Stream get stream => lyrics.toStream(); - - /// Use this constructor if you want to manually create an LRC from scratch. - /// Otherwise, parse an LRC string using [Lrc.parse]. - const Lrc({ - this.type = LrcTypes.simple, - required this.lyrics, - this.artist, - this.album, - this.title, - this.creator, - this.author, - this.program, - this.version, - this.length, - this.offset, - this.language, - this.personCount = 1, - }); - - /// Format the lrc to a readable string that can then be - /// outputted to an LRC file. - String format() { - var buffer = StringBuffer(); - - if (artist != null) buffer.writeln('[ar:$artist]'); - if (album != null) buffer.writeln('[al:$album]'); - if (title != null) buffer.writeln('[ti:$title]'); - if (length != null) buffer.writeln('[length:$length]'); - if (creator != null) buffer.writeln('[by:$creator]'); - if (author != null) buffer.writeln('[au:$author]'); - if (offset != null) buffer.writeln('[offset:${offset.toString()}]'); - if (program != null) buffer.writeln('[re:$program]'); - if (version != null) buffer.writeln('[ve:$version]'); - if (language != null) buffer.writeln('[la:$language]'); - - final lrcLength = lyrics.length; - for (var i = 0; i < lrcLength; i++) { - buffer.writeln(lyrics[i].formattedLine); - } - - return buffer.toString(); - } - - static List _splitLines(String data) { - var lines = []; - var end = data.length; - var sliceStart = 0; - var char = 0; - for (var i = 0; i < end; i++) { - var previousChar = char; - char = data.codeUnitAt(i); - if (char != 13) { - if (char != 10) continue; - if (previousChar == 13) { - sliceStart = i + 1; - continue; - } - } - lines.add(data.substring(sliceStart, i)); - sliceStart = i + 1; - } - if (sliceStart < end) lines.add(data.substring(sliceStart, end)); - return lines; - } - - static List splitMultiLanguageLine(String lyric) { - return lyric.split(RegExp(r'(\s{2,}|\|)(?=\S)')); - } - - static Iterable extractTimeStampPartFromLine(String line) sync* { - for (var j in RegExp(r'<(([0-9]{1,}):([0-9]{1,})\.([0-9]{1,})?)>([^<]+)') - .allMatches(line)) { - final timestamp = Duration( - minutes: int.tryParse(j.group(2)!) ?? 0, - seconds: int.tryParse(j.group(3)!) ?? 0, - milliseconds: (int.tryParse(j.group(4)!) ?? 0), - ); - yield LrcLinePart( - timestamp: timestamp, - lyrics: j.group(5)?.trim() ?? '', - ); - } - } - - /// Parses an LRC from a string. Throws a `FormatExeption` - /// if the inputted string is not valid. - static Lrc parse(String parsed) { - if (!isValid(parsed)) { - throw FormatException('The inputted string is not a valid LRC file'); - } - - // split string into lines, code from Linesplitter().convert(data) - var lines = _splitLines(parsed); - - // temporary storer variables - String? artist, - album, - title, - length, - author, - creator, - offset, - program, - version, - language; - LrcTypes? type; - var lyrics = []; - - final personToIndex = {}; - var registeredTimestamps = {}; - var shouldSortLyrics = false; - - String? setIfMatchTag(String toMatch, String tag) => - (RegExp(r'^\[' + tag + r':.*\]$').hasMatch(toMatch)) - ? toMatch.substring(tag.length + 2, toMatch.length - 1).trim() - : null; - - // loop thru each lines - for (var i = 0; i < lines.length; i++) { - var l = lines[i]; - artist ??= setIfMatchTag(l, 'ar'); - album ??= setIfMatchTag(l, 'al'); - title ??= setIfMatchTag(l, 'ti'); - author ??= setIfMatchTag(l, 'au'); - length ??= setIfMatchTag(l, 'length'); - creator ??= setIfMatchTag(l, 'by'); - offset ??= setIfMatchTag(l, 'offset'); - program ??= setIfMatchTag(l, 're'); - version ??= setIfMatchTag(l, 've'); - language ??= setIfMatchTag(l, 'la'); - - if (_LRCMultiTimestampParser._durRegex.hasMatch(l)) { - var lineType = LrcTypes.simple; - List? parts; - - final lrclineDetails = _LRCMultiTimestampParser.parseLine(l); - final lyric = lrclineDetails.lineText; - final timestamps = lrclineDetails.timestamps; - - if (shouldSortLyrics == false) { - if (timestamps.length > 1) { - // -- means it has multi timestamps - shouldSortLyrics = true; - } - if (timestamps - .any((element) => registeredTimestamps.contains(element))) { - // -- means it has multi language in the end of the file - shouldSortLyrics = true; - } - } - - int? person; - - // checkers for different types of LRCs - if (lyric.contains(RegExp(r'^\w:'))) { - //if extended - type = (type == LrcTypes.enhanced) - ? LrcTypes.extended_enhanced - : LrcTypes.extended; - - final personText = lyric[0]; - person = personToIndex[personText] ??= personToIndex.length + 1; - - parts = []; - parts.add(LrcLinePart( - timestamp: timestamps.first, - lyrics: lyric.substring(2), - )); - - lineType = LrcTypes.extended; - } else if (lyric - .contains(RegExp(r'<[0-9]{1,}:[0-9]{1,}(\.[0-9]{1,})?>'))) { - // if enhanced - type = (type == LrcTypes.extended) - ? LrcTypes.extended_enhanced - : LrcTypes.enhanced; - parts = []; - lineType = LrcTypes.enhanced; - - final personText = lyric.substring(0, lyric.indexOf('<')); - person = personToIndex[personText] ??= personToIndex.length + 1; - - parts.addAll(extractTimeStampPartFromLine(lyric)); - } - - final lyricSplit = splitMultiLanguageLine(lyric); - for (var i = 0; i < lyricSplit.length; i++) { - final part = lyricSplit[i]; - final readableText = parts != null && parts.isNotEmpty - ? parts.map((e) => e.lyrics).join(' ') - : part; - for (final linetimestamp in timestamps) { - registeredTimestamps.add(linetimestamp); - lyrics.add(LrcLine( - timestamp: linetimestamp, - lyrics: part, - readableText: readableText.isNotEmpty ? readableText : part, - type: lineType, - parts: parts, - person: person, - )); - } - } - } - } - - if (type == LrcTypes.enhanced || type == LrcTypes.extended_enhanced) { - // extract bg tags - for (final m in RegExp(r'\[bg:(.*)\]').allMatches(parsed)) { - final text = m.group(1); - if (text == null) continue; - final parts = extractTimeStampPartFromLine(text).toList(); - if (parts.isEmpty) continue; - final newLine = LrcLine( - timestamp: parts[0].timestamp, - lyrics: text, - readableText: parts.map((e) => e.lyrics).join(' '), - type: type ?? LrcTypes.enhanced, - parts: parts, - person: 0, - ); - lyrics.add(newLine); - shouldSortLyrics = true; - } - } - - if (shouldSortLyrics) { - lyrics.sort((a, b) => - a.timestamp.inMicroseconds.compareTo(b.timestamp.inMicroseconds)); - } - - var personCount = - personToIndex.values.where((element) => element > 0).length; - if (personCount <= 0) personCount = 1; - - return Lrc( - type: type ?? LrcTypes.simple, - artist: artist, - album: album, - title: title, - author: author, - length: length, - creator: creator, - offset: (offset != null) ? int.tryParse(offset) : null, - program: program, - version: version, - lyrics: lyrics, - language: language, - personCount: personCount, - ); - } - - /// Checks if the string [input] is a valid LRC using Regex. - static bool isValid(String input) => - RegExp(r'\[(\d{1,}).+\]').hasMatch(input); - - static String cleanPlainLyrics(String input) { - final regex = RegExp( - r'([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)'); - return input.replaceAll(regex, ''); - } - - @override - String toString() { - var lyrics = this.lyrics.join('\n'); - - return ''' - Type: '$type' - Artist: '$artist' - Album: '$album' - Title: '$title' - Author: '$author' - Creator: '$creator' - Program: '$program' - Length: '$length' - Language: '$language' - Offset: '$offset' - Lyrics: $lyrics - '''; - } -} - -///The types of LRC -enum LrcTypes { - ///A simple LRC, with no extra formatting, etc - simple, - - ///LRC with modifiers at the start in the form `A: foo` - extended, - - ///LRC with additional timestamps per line in the form `<00:00.00> foo` - enhanced, - - ///LRC that some lines are extended and some are enhanced - extended_enhanced -} - -///A line of lyrics, with its defined duration and raw lyrics -class LrcLine { - ///timestamp for the lyrics wherein it'll be displayed - final Duration timestamp; - - ///the raw lyrics for the line - final String lyrics; - - final String readableText; - - final List? parts; - - ///the type of lrc for this line - final LrcTypes type; - - final int? person; - - bool get isBGLyrics => person == 0; - - const LrcLine({ - required this.timestamp, - required this.lyrics, - required this.readableText, - required this.type, - required this.parts, - required this.person, - }); - - LrcLine withTimeStamp({required Duration newTimestamp}) { - return LrcLine( - timestamp: newTimestamp, - lyrics: lyrics, - readableText: readableText, - type: type, - person: person, - parts: parts, - ); - } - - ///get the string for a formatted line - String get formattedLine { - ///function to add leading zeros - String f(int x) => x.toString().padLeft(2, '0'); - - // LRC format doesn't accept hours. - final minutes = timestamp.inMinutes % 60, - seconds = timestamp.inSeconds % 60, - hundreds = timestamp.inMilliseconds % 1000 ~/ 10; - - return '[${f(minutes)}:${f(seconds)}.${f(hundreds)}]$lyrics'; - } - - @override - String toString() { - return ''' - Timestamp: '$timestamp' - Lyrics: '$lyrics' - Parts: '$parts' - Person: '$person' - '''; - } -} - -class LrcLinePart { - final Duration timestamp; - final String lyrics; - - const LrcLinePart({ - required this.timestamp, - required this.lyrics, - }); - - @override - String toString() { - return ''' - Timestamp: '$timestamp' - Lyrics: '$lyrics' - '''; - } -} - -/// A data class to store each yielding of the stream -class LrcStream { - /// The previous line. Is null if the current line is the fist position. - LrcLine? previous; - - /// Tthe current line - LrcLine current; - - /// The next line. Is null if the current line is the last position. - LrcLine? next; - - /// The duration from the current to the next. Is null if the current line is the last position. - Duration? duration; - - /// The position of the current line - int position; - - /// The total number of lines in the stream - int length; - - /// The main constructor for a LrcStream - LrcStream( - {this.previous, - required this.current, - this.next, - this.duration, - required this.position, - required this.length}) - //position should be greater than or equal to 0 - : assert(position >= 0), - //the length should be greater than or equal to the position - assert(length >= position), - //previous is null only if position is 0 - assert((previous == null) ? position == 0 : true), - //next is null only if position is the last - assert((next == null) ? position == length : true); -} - -/// Handy extensions on lists of LrcLine -extension LrcLineExtensions on List { - /// Creates a stream for each lyric using their durations - Stream toStream() async* { - for (var i = 0; i < length; i++) { - var lineCurrent = this[i]; - var lineNext = (i + 1 < length) ? this[i + 1] : null; - var durationToNext = (lineNext != null) - ? Duration( - milliseconds: lineNext.timestamp.inMilliseconds - - lineCurrent.timestamp.inMilliseconds) - : null; - yield LrcStream( - duration: durationToNext, - previous: (i != 0) ? this[i - 1] : null, - current: lineCurrent, - next: lineNext, - position: i, - length: length - 1); - if (durationToNext != null) { - await Future.delayed(durationToNext); - } - } - } -} - -/// Handy extensions on strings -extension StringExtensions on String { - /// Handy extension method that parses the string to an [Lrc] - Lrc toLrc() => Lrc.parse(this); - - /// Handy extension getter if the given string is a valid LRC - bool get isValidLrc => Lrc.isValid(this); -} diff --git a/lib/src/models/lrc.dart b/lib/src/models/lrc.dart new file mode 100644 index 0000000..fcb9408 --- /dev/null +++ b/lib/src/models/lrc.dart @@ -0,0 +1,133 @@ +part of lrc; + +/// The parsed LRC class. +/// +/// You can instantiate this class directly +/// or parse a string using `Lrc.parse()`. +class Lrc { + /// The overall type of LRC for this object + final LrcTypes type; + + /// The name of the artist of the song (optional) + /// + /// This corresponds to the ID tag `[ar:]`. + final String? artist; + + /// The name of the album of the song (optional) + /// + /// This corresponds to the ID tag `[al:]`. + final String? album; + + /// The title of the song (optional) + /// + /// This corresponds to the ID tag `[ti:]`. + final String? title; + + /// The name of the author of the lyrics (optional) + /// + /// This corresponds to the ID tag `[au:]`. + final String? author; + + /// The name of the creator of the LRC file (optional) + /// + /// This corresponds to the ID tag `[by:]`. + final String? creator; + + /// The name of the program that created the LRC file (optional) + /// + /// This corresponds to the ID tag `[re:]`. + final String? program; + + /// The version of the program that created the LRC file (optional) + /// + /// This corresponds to the ID tag `[ve:]`. + final String? version; + + /// The length of the song (optional) + /// + /// This corresponds to the ID tag `[length:]`. + final String? length; + + /// The language of the song, using an IETF BCP 47 language tag (optional) + /// + /// This corresponds to the ID tag `[la:]`. + final String? language; + + /// Offset of time in milliseconds, can be positive [shifts time up] + /// or negative [shifts time down] (optional) + /// + /// This corresponds to the ID tag `[offset:]`. + final int? offset; + + /// The list of lyric lines + final List lyrics; + + final int personCount; + + /// Handy parameter to get a stream of the lyrics. + /// See `List.toStream()`. + Stream get stream => lyrics.toStream(); + + /// Use this constructor if you want to manually create an LRC from scratch. + /// Otherwise, parse an LRC string using [Lrc.parse]. + const Lrc({ + this.type = LrcTypes.simple, + required this.lyrics, + this.artist, + this.album, + this.title, + this.creator, + this.author, + this.program, + this.version, + this.length, + this.offset, + this.language, + this.personCount = 1, + }); + + static Lrc parse(String text) => LrcParser.parse(text); + + /// Format the lrc to a readable string that can then be + /// outputted to an LRC file. + String format() { + var buffer = StringBuffer(); + + if (artist != null) buffer.writeln('[ar:$artist]'); + if (album != null) buffer.writeln('[al:$album]'); + if (title != null) buffer.writeln('[ti:$title]'); + if (length != null) buffer.writeln('[length:$length]'); + if (creator != null) buffer.writeln('[by:$creator]'); + if (author != null) buffer.writeln('[au:$author]'); + if (offset != null) buffer.writeln('[offset:${offset.toString()}]'); + if (program != null) buffer.writeln('[re:$program]'); + if (version != null) buffer.writeln('[ve:$version]'); + if (language != null) buffer.writeln('[la:$language]'); + + final lrcLength = lyrics.length; + for (var i = 0; i < lrcLength; i++) { + buffer.writeln(lyrics[i].formattedLine); + } + + return buffer.toString(); + } + + @override + String toString() { + var lyrics = this.lyrics.join('\n'); + + return ''' + Type: '$type' + Artist: '$artist' + Album: '$album' + Title: '$title' + Author: '$author' + Creator: '$creator' + Program: '$program' + Length: '$length' + Language: '$language' + Offset: '$offset' + Lyrics: $lyrics + '''; + } +} diff --git a/lib/src/models/lrc_line.dart b/lib/src/models/lrc_line.dart new file mode 100644 index 0000000..eb168cd --- /dev/null +++ b/lib/src/models/lrc_line.dart @@ -0,0 +1,64 @@ +part of lrc; + +///A line of lyrics, with its defined duration and raw lyrics +class LrcLine { + ///timestamp for the lyrics wherein it'll be displayed + final Duration timestamp; + + ///the raw lyrics for the line + final String lyrics; + + final String readableText; + + final List? parts; + + ///the type of lrc for this line + final LrcTypes type; + + final int? person; + + bool get isBGLyrics => person == 0; + + const LrcLine({ + required this.timestamp, + required this.lyrics, + required this.readableText, + required this.type, + required this.parts, + required this.person, + }); + + LrcLine withTimeStamp({required Duration newTimestamp}) { + return LrcLine( + timestamp: newTimestamp, + lyrics: lyrics, + readableText: readableText, + type: type, + person: person, + parts: parts, + ); + } + + ///get the string for a formatted line + String get formattedLine { + ///function to add leading zeros + String f(int x) => x.toString().padLeft(2, '0'); + + // LRC format doesn't accept hours. + final minutes = timestamp.inMinutes % 60, + seconds = timestamp.inSeconds % 60, + hundreds = timestamp.inMilliseconds % 1000 ~/ 10; + + return '[${f(minutes)}:${f(seconds)}.${f(hundreds)}]$lyrics'; + } + + @override + String toString() { + return ''' + Timestamp: '$timestamp' + Lyrics: '$lyrics' + Parts: '$parts' + Person: '$person' + '''; + } +} diff --git a/lib/src/models/lrc_line_part.dart b/lib/src/models/lrc_line_part.dart new file mode 100644 index 0000000..93c7a95 --- /dev/null +++ b/lib/src/models/lrc_line_part.dart @@ -0,0 +1,19 @@ +part of lrc; + +class LrcLinePart { + final Duration timestamp; + final String lyrics; + + const LrcLinePart({ + required this.timestamp, + required this.lyrics, + }); + + @override + String toString() { + return ''' + Timestamp: '$timestamp' + Lyrics: '$lyrics' + '''; + } +} diff --git a/lib/src/models/lrc_stream.dart b/lib/src/models/lrc_stream.dart new file mode 100644 index 0000000..a7cc759 --- /dev/null +++ b/lib/src/models/lrc_stream.dart @@ -0,0 +1,39 @@ +part of lrc; + +/// A data class to store each yielding of the stream +class LrcStream { + /// The previous line. Is null if the current line is the fist position. + LrcLine? previous; + + /// Tthe current line + LrcLine current; + + /// The next line. Is null if the current line is the last position. + LrcLine? next; + + /// The duration from the current to the next. Is null if the current line is the last position. + Duration? duration; + + /// The position of the current line + int position; + + /// The total number of lines in the stream + int length; + + /// The main constructor for a LrcStream + LrcStream( + {this.previous, + required this.current, + this.next, + this.duration, + required this.position, + required this.length}) + //position should be greater than or equal to 0 + : assert(position >= 0), + //the length should be greater than or equal to the position + assert(length >= position), + //previous is null only if position is 0 + assert((previous == null) ? position == 0 : true), + //next is null only if position is the last + assert((next == null) ? position == length : true); +} diff --git a/lib/src/parsers/lrc_parser.dart b/lib/src/parsers/lrc_parser.dart new file mode 100644 index 0000000..ff2db61 --- /dev/null +++ b/lib/src/parsers/lrc_parser.dart @@ -0,0 +1,222 @@ +part of lrc; + +class LrcParser { + static List _splitLines(String data) { + var lines = []; + var end = data.length; + var sliceStart = 0; + var char = 0; + for (var i = 0; i < end; i++) { + var previousChar = char; + char = data.codeUnitAt(i); + if (char != 13) { + if (char != 10) continue; + if (previousChar == 13) { + sliceStart = i + 1; + continue; + } + } + lines.add(data.substring(sliceStart, i)); + sliceStart = i + 1; + } + if (sliceStart < end) lines.add(data.substring(sliceStart, end)); + return lines; + } + + static List splitMultiLanguageLine(String lyric) { + return lyric.split(RegExp(r'(\s{2,}|\|)(?=\S)')); + } + + static Iterable extractTimeStampPartFromLine(String line) sync* { + for (var j in RegExp(r'<(([0-9]{1,}):([0-9]{1,})\.([0-9]{1,})?)>([^<]+)') + .allMatches(line)) { + final timestamp = Duration( + minutes: int.tryParse(j.group(2)!) ?? 0, + seconds: int.tryParse(j.group(3)!) ?? 0, + milliseconds: (int.tryParse(j.group(4)!) ?? 0), + ); + yield LrcLinePart( + timestamp: timestamp, + lyrics: j.group(5)?.trim() ?? '', + ); + } + } + + /// Parses an LRC from a string. Throws a `FormatExeption` + /// if the inputted string is not valid. + static Lrc parse(String content) { + if (!isValid(content)) { + throw FormatException('The inputted string is not a valid LRC file'); + } + + // split string into lines, code from Linesplitter().convert(data) + var lines = _splitLines(content); + + // temporary storer variables + String? artist, + album, + title, + length, + author, + creator, + offset, + program, + version, + language; + LrcTypes? type; + var lyrics = []; + + final personToIndex = {}; + var registeredTimestamps = {}; + var shouldSortLyrics = false; + + String? setIfMatchTag(String toMatch, String tag) => + (RegExp(r'^\[' + tag + r':.*\]$').hasMatch(toMatch)) + ? toMatch.substring(tag.length + 2, toMatch.length - 1).trim() + : null; + + // loop thru each lines + for (var i = 0; i < lines.length; i++) { + var l = lines[i]; + artist ??= setIfMatchTag(l, 'ar'); + album ??= setIfMatchTag(l, 'al'); + title ??= setIfMatchTag(l, 'ti'); + author ??= setIfMatchTag(l, 'au'); + length ??= setIfMatchTag(l, 'length'); + creator ??= setIfMatchTag(l, 'by'); + offset ??= setIfMatchTag(l, 'offset'); + program ??= setIfMatchTag(l, 're'); + version ??= setIfMatchTag(l, 've'); + language ??= setIfMatchTag(l, 'la'); + + if (_LRCMultiTimestampParser._durRegex.hasMatch(l)) { + var lineType = LrcTypes.simple; + List? parts; + + final lrclineDetails = _LRCMultiTimestampParser.parseLine(l); + final lyric = lrclineDetails.lineText; + final timestamps = lrclineDetails.timestamps; + + if (shouldSortLyrics == false) { + if (timestamps.length > 1) { + // -- means it has multi timestamps + shouldSortLyrics = true; + } + if (timestamps + .any((element) => registeredTimestamps.contains(element))) { + // -- means it has multi language in the end of the file + shouldSortLyrics = true; + } + } + + int? person; + + // checkers for different types of LRCs + if (lyric.contains(RegExp(r'^\w:'))) { + //if extended + type = (type == LrcTypes.enhanced) + ? LrcTypes.extended_enhanced + : LrcTypes.extended; + + final personText = lyric[0]; + person = personToIndex[personText] ??= personToIndex.length + 1; + + parts = []; + parts.add(LrcLinePart( + timestamp: timestamps.first, + lyrics: lyric.substring(2), + )); + + lineType = LrcTypes.extended; + } else if (lyric + .contains(RegExp(r'<[0-9]{1,}:[0-9]{1,}(\.[0-9]{1,})?>'))) { + // if enhanced + type = (type == LrcTypes.extended) + ? LrcTypes.extended_enhanced + : LrcTypes.enhanced; + parts = []; + lineType = LrcTypes.enhanced; + + final personText = lyric.substring(0, lyric.indexOf('<')); + person = personToIndex[personText] ??= personToIndex.length + 1; + + parts.addAll(extractTimeStampPartFromLine(lyric)); + } + + final lyricSplit = splitMultiLanguageLine(lyric); + for (var i = 0; i < lyricSplit.length; i++) { + final part = lyricSplit[i]; + final readableText = parts != null && parts.isNotEmpty + ? parts.map((e) => e.lyrics).join(' ') + : part; + for (final linetimestamp in timestamps) { + registeredTimestamps.add(linetimestamp); + lyrics.add(LrcLine( + timestamp: linetimestamp, + lyrics: part, + readableText: readableText.isNotEmpty ? readableText : part, + type: lineType, + parts: parts, + person: person, + )); + } + } + } + } + + if (type == LrcTypes.enhanced || type == LrcTypes.extended_enhanced) { + // extract bg tags + for (final m in RegExp(r'\[bg:(.*)\]').allMatches(content)) { + final text = m.group(1); + if (text == null) continue; + final parts = extractTimeStampPartFromLine(text).toList(); + if (parts.isEmpty) continue; + final newLine = LrcLine( + timestamp: parts[0].timestamp, + lyrics: text, + readableText: parts.map((e) => e.lyrics).join(' '), + type: type ?? LrcTypes.enhanced, + parts: parts, + person: 0, + ); + lyrics.add(newLine); + shouldSortLyrics = true; + } + } + + if (shouldSortLyrics) { + lyrics.sort((a, b) => + a.timestamp.inMicroseconds.compareTo(b.timestamp.inMicroseconds)); + } + + var personCount = + personToIndex.values.where((element) => element > 0).length; + if (personCount <= 0) personCount = 1; + + return Lrc( + type: type ?? LrcTypes.simple, + artist: artist, + album: album, + title: title, + author: author, + length: length, + creator: creator, + offset: (offset != null) ? int.tryParse(offset) : null, + program: program, + version: version, + lyrics: lyrics, + language: language, + personCount: personCount, + ); + } + + /// Checks if the string [input] is a valid LRC using Regex. + static bool isValid(String input) => + RegExp(r'\[(\d{1,}).+\]').hasMatch(input); + + static String cleanPlainLyrics(String input) { + final regex = RegExp( + r'([\r\n]*\[((ti)|(a[rlu])|(by)|([rv]e)|(length)|(offset)|(la)):.+\][\r\n]*)'); + return input.replaceAll(regex, ''); + } +} diff --git a/lib/src/multi_timestamp_parser.dart b/lib/src/parsers/multi_timestamp_parser.dart similarity index 98% rename from lib/src/multi_timestamp_parser.dart rename to lib/src/parsers/multi_timestamp_parser.dart index 03ddcdc..08f59e1 100644 --- a/lib/src/multi_timestamp_parser.dart +++ b/lib/src/parsers/multi_timestamp_parser.dart @@ -1,4 +1,4 @@ -part of 'lrc_main.dart'; +part of lrc; /// Parses Multi-timestamped lyrics lines effectively. class _LRCMultiTimestampParser { diff --git a/pubspec.yaml b/pubspec.yaml index 35b0448..76016dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.3.6 +version: 1.4.6 repository: https://github.com/Yivan000/lrc environment: From 63b7b090cddb7709a6a771093c6a8e9d39c86297 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 3 Dec 2025 19:44:35 +0200 Subject: [PATCH 14/32] chore: improve enhanced lyrics extraction v2 --- lib/src/models/lrc_line_part.dart | 9 ++-- lib/src/parsers/lrc_parser.dart | 72 +++++++++++++++++++++++-------- pubspec.yaml | 2 +- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/lib/src/models/lrc_line_part.dart b/lib/src/models/lrc_line_part.dart index 93c7a95..9baccfb 100644 --- a/lib/src/models/lrc_line_part.dart +++ b/lib/src/models/lrc_line_part.dart @@ -1,18 +1,21 @@ part of lrc; class LrcLinePart { - final Duration timestamp; + final Duration startTimestamp; + final Duration endTimestamp; final String lyrics; const LrcLinePart({ - required this.timestamp, + required this.startTimestamp, + required this.endTimestamp, required this.lyrics, }); @override String toString() { return ''' - Timestamp: '$timestamp' + startTimestamp: '$startTimestamp' + endTimestamp: '$endTimestamp' Lyrics: '$lyrics' '''; } diff --git a/lib/src/parsers/lrc_parser.dart b/lib/src/parsers/lrc_parser.dart index ff2db61..fb6cd27 100644 --- a/lib/src/parsers/lrc_parser.dart +++ b/lib/src/parsers/lrc_parser.dart @@ -27,18 +27,38 @@ class LrcParser { return lyric.split(RegExp(r'(\s{2,}|\|)(?=\S)')); } - static Iterable extractTimeStampPartFromLine(String line) sync* { - for (var j in RegExp(r'<(([0-9]{1,}):([0-9]{1,})\.([0-9]{1,})?)>([^<]+)') + static Iterable extractTimeStampPartFromLine(String line, + {Duration? startTimeStamp}) sync* { + var isFirst = true; + var latestTimeStamp = startTimeStamp; + for (var j in RegExp(r'([^<\]]*)<(([0-9]{1,}):([0-9]{1,})\.([0-9]{1,})?)>') .allMatches(line)) { - final timestamp = Duration( - minutes: int.tryParse(j.group(2)!) ?? 0, - seconds: int.tryParse(j.group(3)!) ?? 0, - milliseconds: (int.tryParse(j.group(4)!) ?? 0), + final lyrics = j.group(1) ?? ''; + final endTimestamp = Duration( + minutes: int.tryParse(j.group(3)!) ?? 0, + seconds: int.tryParse(j.group(4)!) ?? 0, + milliseconds: (int.tryParse(j.group(5)!) ?? 0), ); + final startTimestamp = latestTimeStamp; + latestTimeStamp = endTimestamp; + if (startTimestamp == null) { + // we set the start for the next part + continue; + } + if (isFirst) { + // -- skip first part that has `v1:` etc + if (lyrics.length < 5 && + lyrics.startsWith('v') && + (lyrics.endsWith(':') || lyrics.endsWith(': '))) { + continue; + } + } yield LrcLinePart( - timestamp: timestamp, - lyrics: j.group(5)?.trim() ?? '', + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + lyrics: lyrics, ); + isFirst = false; } } @@ -123,7 +143,8 @@ class LrcParser { parts = []; parts.add(LrcLinePart( - timestamp: timestamps.first, + startTimestamp: timestamps.first, + endTimestamp: timestamps.first, lyrics: lyric.substring(2), )); @@ -137,24 +158,37 @@ class LrcParser { parts = []; lineType = LrcTypes.enhanced; - final personText = lyric.substring(0, lyric.indexOf('<')); - person = personToIndex[personText] ??= personToIndex.length + 1; + final indexOfLT = lyric.indexOf('<'); + final personText = indexOfLT < 0 ? '' : lyric.substring(0, indexOfLT); + person = personToIndex[personText] ??= personText.startsWith('v1:') + ? 1 + : personText.startsWith('v2:') + ? 2 + : personText.startsWith('v3:') + ? 3 + : personToIndex.length + 1; - parts.addAll(extractTimeStampPartFromLine(lyric)); + parts.addAll( + extractTimeStampPartFromLine( + lyric, + startTimeStamp: timestamps.first, + ), + ); } final lyricSplit = splitMultiLanguageLine(lyric); for (var i = 0; i < lyricSplit.length; i++) { - final part = lyricSplit[i]; + var lyric = lyricSplit[i]; + if (lyric.length < 5 && lyric.startsWith('v3:')) lyric = ''; final readableText = parts != null && parts.isNotEmpty - ? parts.map((e) => e.lyrics).join(' ') - : part; + ? parts.map((e) => e.lyrics).join() + : lyric; for (final linetimestamp in timestamps) { registeredTimestamps.add(linetimestamp); lyrics.add(LrcLine( timestamp: linetimestamp, - lyrics: part, - readableText: readableText.isNotEmpty ? readableText : part, + lyrics: lyric, + readableText: readableText.isNotEmpty ? readableText : lyric, type: lineType, parts: parts, person: person, @@ -172,9 +206,9 @@ class LrcParser { final parts = extractTimeStampPartFromLine(text).toList(); if (parts.isEmpty) continue; final newLine = LrcLine( - timestamp: parts[0].timestamp, + timestamp: parts[0].startTimestamp, lyrics: text, - readableText: parts.map((e) => e.lyrics).join(' '), + readableText: parts.map((e) => e.lyrics).join(), type: type ?? LrcTypes.enhanced, parts: parts, person: 0, diff --git a/pubspec.yaml b/pubspec.yaml index 76016dd..9f694e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.4.6 +version: 1.5.0 repository: https://github.com/Yivan000/lrc environment: From d794a07bdc3de98a33e7c48d6ad73186c2040f90 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 3 Dec 2025 19:45:39 +0200 Subject: [PATCH 15/32] feat: parse ttml lyrics (xml files) --- lib/lrc.dart | 3 ++ lib/src/parsers/ttml_parser.dart | 57 ++++++++++++++++++++++++++++++++ pubspec.yaml | 5 ++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 lib/src/parsers/ttml_parser.dart diff --git a/lib/lrc.dart b/lib/lrc.dart index 229259f..78814cb 100644 --- a/lib/lrc.dart +++ b/lib/lrc.dart @@ -1,6 +1,8 @@ /// The main library for support for LRCs. library lrc; +import 'package:html_unescape/html_unescape.dart'; + part 'src/core/enums.dart'; part 'src/core/extensions.dart'; part 'src/models/lrc.dart'; @@ -9,3 +11,4 @@ part 'src/models/lrc_line_part.dart'; part 'src/models/lrc_stream.dart'; part 'src/parsers/lrc_parser.dart'; part 'src/parsers/multi_timestamp_parser.dart'; +part 'src/parsers/ttml_parser.dart'; diff --git a/lib/src/parsers/ttml_parser.dart b/lib/src/parsers/ttml_parser.dart new file mode 100644 index 0000000..ad7674d --- /dev/null +++ b/lib/src/parsers/ttml_parser.dart @@ -0,0 +1,57 @@ +part of lrc; + +class TtmlParser { + static final _regexp = RegExp( + r']*?begin="([\d:.]+)s?"[^>]*?end="([\d:.]+)s?"[^>]*?>(.*?)<\/p>'); + + static bool isValid(String content) => _regexp.hasMatch(content); + + static Lrc parse(String content) { + final lrcLines = []; + final personToIndex = {}; + final htmlUnescape = HtmlUnescape(); + var type = LrcTypes.simple; + + for (final m in _regexp.allMatches(content)) { + int? startMS; + // int? endMS; + try { + startMS = (double.parse(m.group(1)!) * 1000).round(); + // endMS = (double.parse(m.group(2)!) * 1000).round(); + } catch (_) {} + if (startMS != null) { + final textOriginal = m.group(3); + String textNormalized; + if (textOriginal != null && textOriginal.isNotEmpty) { + textNormalized = htmlUnescape.convert(textOriginal); + } else { + textNormalized = ''; + } + + final personText = + textNormalized.substring(0, textNormalized.indexOf('<')); + final person = personToIndex[personText] ??= personToIndex.length + 1; + var parts = + LrcParser.extractTimeStampPartFromLine(textNormalized).toList(); + final lineType = parts.isNotEmpty ? LrcTypes.enhanced : LrcTypes.simple; + if (lineType == LrcTypes.enhanced) type = LrcTypes.enhanced; + final lrcLine = LrcLine( + timestamp: Duration(milliseconds: startMS), + lyrics: textNormalized, + readableText: parts.isNotEmpty + ? parts.map((e) => e.lyrics).join() + : textNormalized, + type: lineType, + parts: parts, + person: person, + ); + lrcLines.add(lrcLine); + } + } + + return Lrc( + lyrics: lrcLines, + type: type, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9f694e4..61d97fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,15 @@ name: lrc description: A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -version: 1.5.0 +version: 1.6.0 repository: https://github.com/Yivan000/lrc environment: sdk: ">=3.6.0 <4.0.0" +dependencies: + html_unescape: ^2.0.0 + dev_dependencies: pedantic: ^1.10.0 test: ^1.16.0 From 212a400118e55a1c395acbba8f77c878f9dc6ebf Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 3 Dec 2025 19:47:59 +0200 Subject: [PATCH 16/32] test: add tests --- .gitignore | 3 + test/files/timed_lrc.lrc | 92 ++++++++++++++++++++++ test/files/timed_lrc_2.lrc | 61 +++++++++++++++ test/files/timed_lrc_3.lrc | 75 ++++++++++++++++++ test/files/timed_lrc_4.lrc | 155 +++++++++++++++++++++++++++++++++++++ test/files/ttml_lrc.xml | 84 ++++++++++++++++++++ test/lrc.dart | 24 +++++- 7 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 test/files/timed_lrc.lrc create mode 100644 test/files/timed_lrc_2.lrc create mode 100644 test/files/timed_lrc_3.lrc create mode 100644 test/files/timed_lrc_4.lrc create mode 100644 test/files/ttml_lrc.xml diff --git a/.gitignore b/.gitignore index 65c34dc..6cadb61 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ build/ # Omit committing pubspec.lock for library packages; see # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock + + +test/files/extra \ No newline at end of file diff --git a/test/files/timed_lrc.lrc b/test/files/timed_lrc.lrc new file mode 100644 index 0000000..3540afe --- /dev/null +++ b/test/files/timed_lrc.lrc @@ -0,0 +1,92 @@ +[ti:Runaway (feat. Pusha T)] +[ar:Kanye West] +[by:Generated using SongSync] +[00:40.363]v1: <00:40.363>Look <00:40.595>at <00:40.916>ya, <00:41.100>look <00:41.314>at <00:41.624>ya, <00:41.796>look <00:42.022>at <00:42.314>ya, <00:42.504>look <00:42.724>at <00:43.028>ya <00:43.235> +[00:44.515]v1: <00:44.515>Look <00:44.842>at <00:45.139>ya, <00:45.336>look <00:45.556>at <00:45.859>ya, <00:46.032>look <00:46.258>at <00:46.567>ya, <00:46.757>look <00:46.870>at <00:46.970>ya <00:47.363> +[00:51.414]v1: <00:51.414>Look <00:51.711>at <00:52.026>ya, <00:52.205>look <00:52.425>at <00:52.734>ya, <00:52.919>look <00:53.139>at <00:53.424>ya, <00:53.621>look <00:53.793>at <00:53.893>ya <00:54.250> +[00:56.948]v1: <00:56.948>Look <00:57.193>at <00:57.526>ya, <00:57.726>look <00:57.910>at <00:58.243>ya, <00:58.409>look <00:58.593>at <00:58.910>ya, <00:59.091>look <00:59.275>at <00:59.593>ya, <00:59.777>look <00:59.977>at <01:00.259>ya <01:00.440> +[bg: <00:56.955>Ladies <00:57.283>and <00:57.467>gentlemen, <00:58.334>ladies, <00:58.819>ladies <00:59.086>and <00:59.286>gentlemen <01:00.440>] +[01:02.297]v2: <01:02.297>And <01:02.564>I <01:02.784>always <01:03.480>find, <01:03.843>yeah, <01:03.968>I <01:04.182>always <01:04.676>find <01:04.980>something <01:05.563>wrong <01:06.247> +[01:07.926]v2: <01:07.926>You <01:08.217>been <01:08.407>puttin' <01:08.764>up <01:08.984>with <01:09.270>my <01:09.478>shit <01:09.984>just <01:10.299>way <01:10.823>too <01:11.049>long <01:11.633> +[01:13.334]v2: <01:13.334>I'm <01:13.434>so <01:13.839>gifted <01:14.202>at <01:14.301>finding <01:15.077>what <01:15.261>I <01:15.630>don't <01:15.939>like <01:16.308>the <01:16.487>most <01:17.162> +[01:18.899]v2: <01:18.899>So <01:19.327>I <01:19.499>think <01:19.749>it's <01:20.041>time <01:20.564>for <01:20.725>us <01:21.271>to <01:21.408>have <01:21.956>a <01:22.105>toast <01:22.793> +[01:22.947]v2: <01:22.947>Let's <01:23.184>have <01:23.393>a <01:23.530>toast <01:23.881>for <01:24.071>the <01:24.238>douche<01:24.580>bags <01:25.515> +[01:25.699]v2: <01:25.699>Let's <01:26.002>have <01:26.210>a <01:26.347>toast <01:26.704>for <01:26.906>the <01:27.055>assholes <01:28.275> +[01:26.699]v3: <01:25.699>Let's <01:26.002>have <01:26.210>a <01:26.347>toast <01:26.704>for <01:26.906>the <01:27.055>assholes <01:28.275> +[01:28.472]v2: <01:28.472>Let's <01:28.632>have <01:28.846>a <01:28.971>toast <01:29.322>for <01:29.501>the <01:29.601>scum<01:30.128>bags <01:31.048> +[01:31.379]v2: <01:31.379>Every <01:31.807>one <01:32.015>of <01:32.146>them <01:32.360>that <01:32.515>I <01:32.800>know <01:33.808> +[01:33.991]v2: <01:33.991>Let's <01:34.241>have <01:34.442>a <01:34.594>toast <01:34.941>for <01:35.125>the <01:35.274>jerk-<01:35.626>offs <01:36.554> +[01:36.890]v2: <01:36.890>That'll <01:37.259>never <01:37.598>take <01:37.961>work <01:38.348>off <01:39.336> +[01:39.336]v2: <01:39.336>Baby, <01:39.764>I <01:40.091>got <01:40.496>a <01:40.769>plan <01:41.151> +[01:41.402]v2: <01:41.402>Run <01:41.676>away <01:42.087>fast <01:42.586>as <01:42.914>you <01:43.580>can <01:44.931> +[01:45.020]v2: <01:45.020>She <01:45.192>find <01:45.549>pictures <01:45.930>in <01:46.084>my <01:46.257>email <01:47.276> +[01:47.809]v2: <01:47.809>I <01:47.926>sent <01:48.201>this <01:48.374>bitch <01:48.576>a <01:48.719>picture <01:49.064>of <01:49.241>my <01:49.456>dick <01:50.203> +[01:50.537]v2: <01:50.537>I <01:50.822>don't <01:51.024>know <01:51.215>what <01:51.411>it <01:51.536>is <01:51.726>with <01:51.828>females <01:53.049> +[01:53.463]v2: <01:53.463>But <01:53.683>I'm <01:53.837>not <01:54.057>too <01:54.212>good <01:54.420>at <01:54.587>that <01:54.825>shit <01:55.831> +[01:56.078]v2: <01:56.078>See, <01:56.339>I <01:56.530>could <01:56.678>have <01:57.006>me <01:57.196>a <01:57.369>good <01:57.714>girl <01:58.509> +[01:58.513]v2: <01:58.513>And <01:58.727>still <01:59.179>be <01:59.357>addicted <01:59.842>to <02:00.024>them <02:00.178>hood<02:00.574>rats <02:01.418> +[02:01.652]v2: <02:01.652>And <02:01.875>I <02:02.057>just <02:02.222>blame <02:02.523>everything <02:03.057>on <02:03.222>you <02:04.081> +[02:04.081]v2: <02:04.081>At <02:04.286>least <02:04.467>you <02:04.619>know <02:04.966>that's <02:05.299>what <02:05.499>I'm <02:05.654>good <02:05.982>at <02:06.934> +[02:07.129]v2: <02:07.129>And <02:07.396>I <02:07.587>always <02:08.300>find, <02:08.699>yeah, <02:08.799>I <02:08.899>always <02:09.520>find <02:09.785> +[02:09.918]v2: <02:09.918>Yeah, <02:10.018>I <02:10.233>always <02:10.905>find <02:11.187>something <02:11.815>wrong <02:12.462> +[02:14.158]v2: <02:14.158>You <02:14.419>been <02:14.603>puttin' <02:14.936>up <02:15.171>with <02:15.469>my <02:15.803>shit <02:16.136>just <02:16.469>way <02:16.971>too <02:17.187>long <02:17.953> +[02:19.489]v2: <02:19.489>I'm <02:19.649>so <02:20.095>gifted <02:20.339>at <02:20.542>finding <02:21.327>what <02:21.523>I <02:21.874>don't <02:22.201>like <02:22.558>the <02:22.749>most <02:23.416> +[02:25.062]v2: <02:25.062>So <02:25.377>I <02:25.555>think <02:25.817>it's <02:26.085>time <02:26.805>for <02:26.953>us <02:27.554>to <02:27.727>have <02:28.215>a <02:28.357>toast <02:29.077> +[02:29.190]v2: <02:29.190>Let's <02:29.422>have <02:29.636>a <02:29.773>toast <02:30.118>for <02:30.308>the <02:30.475>douche<02:30.808>bags <02:31.591> +[02:31.958]v2: <02:31.958>Let's <02:32.255>have <02:32.445>a <02:32.588>toast <02:32.892>for <02:33.082>the <02:33.182>ass<02:33.648>holes <02:34.534> +[02:34.683]v2: <02:34.683>Let's <02:34.873>have <02:35.093>a <02:35.230>toast <02:35.581>for <02:35.747>the <02:35.884>scum<02:36.233>bags <02:37.238> +[02:37.588]v2: <02:37.588>Every <02:38.035>one <02:38.235>of <02:38.403>them <02:38.585>that <02:38.753>I <02:39.070>know <02:39.863> +[02:40.247]v2: <02:40.247>Let's <02:40.492>have <02:40.654>a <02:40.809>toast <02:41.121>for <02:41.337>the <02:41.508>jerk-<02:41.838>offs <02:42.683> +[02:43.100]v2: <02:43.100>That'll <02:43.510>never <02:43.849>take <02:44.206>work <02:44.599>off <02:45.535> +[02:45.537]v2: <02:45.537>Baby, <02:46.012>I <02:46.340>got <02:46.744>a <02:47.012>plan <02:47.357> +[02:47.639]v2: <02:47.639>Run <02:47.936>away <02:48.341>fast <02:48.840>as <02:49.007>you <02:49.632>can <02:51.071> +[02:51.239]v2: <02:51.239>Run <02:51.465>away <02:52.006>from <02:52.274>me, <02:52.494>baby <02:53.174> +[02:54.204]v2: <02:54.204>Ah, <02:55.316>run <02:55.524>away <02:55.959> +[02:56.761]v2: <02:56.761>Run <02:57.112>away <02:57.463>from <02:57.784>me, <02:57.974>baby <02:58.946> +[03:00.734]v2: <03:00.734>Run <03:00.983>away <03:01.722> +[03:02.257]v2: <03:02.257>When <03:02.530>it <03:02.726>starts <03:03.066>to <03:03.262>get <03:03.565>crazy <03:04.663> +[03:05.894]v2: <03:05.894>Then <03:06.155>run <03:06.435>away <03:07.106> +[03:07.582]v2: <03:07.582>Babe, <03:08.072>I <03:08.405>got <03:08.723>a <03:08.923>plan, <03:09.789>run <03:09.955>away <03:10.323>as <03:10.477>fast <03:10.824>as <03:11.173>you <03:11.805>can <03:12.490> +[03:13.342]v2: <03:13.342>Run <03:13.484>away <03:13.853>from <03:14.305>me, <03:14.531>baby <03:15.267> +[03:17.281]v2: <03:17.281>Run <03:17.524>away <03:18.071> +[03:18.838]v2: <03:18.838>Run <03:19.135>away <03:19.670>from <03:20.010>me, <03:20.200>baby <03:20.973> +[03:22.790]v2: <03:22.790>Run <03:23.004>away <03:23.708> +[03:24.339]v2: <03:24.339>When <03:24.547>it <03:24.737>starts <03:25.082>to <03:25.267>get <03:25.576>crazy <03:26.265> +[03:26.764]v2: <03:26.764>Why <03:27.009>can't <03:27.361>she <03:27.710>just <03:28.443>run <03:28.609>away? <03:29.014> +[03:29.710]v2: <03:29.710>Baby, <03:30.037>I <03:30.376>got <03:30.768>a <03:31.042>plan <03:31.460> +[03:31.763]v2: <03:31.763>Run <03:31.983>away <03:32.286>as <03:32.482>fast <03:32.905>as <03:33.214>you <03:33.892>can <03:34.528> +[03:34.526]v2: <03:34.526>Twenty-four <03:35.293>seven, <03:35.822>three <03:36.197>sixty-five, <03:37.292>pussy <03:37.583>stays <03:38.071>on <03:38.291>my <03:38.595>mind <03:39.107> +[03:39.259]v2: <03:39.259>I-<03:39.688>I-<03:40.021>I-<03:40.353>I <03:40.488>did <03:40.653>it, <03:41.021>alright, <03:41.338>alright, <03:41.738>I <03:42.037>admit <03:42.353>it <03:42.523> +[03:42.672]v2: <03:42.672>Now <03:42.904>pick <03:43.112>your <03:43.212>next <03:43.552>move, <03:43.855>you <03:44.028>could <03:44.242>leave <03:44.522>or <03:44.766>live <03:45.140>with <03:45.295>it <03:45.395> +[03:45.619]v2: <03:45.619>Ichabod <03:46.315>Crane <03:46.666>with <03:46.791>that <03:46.963>motherfuckin' <03:47.689>top <03:48.046>off <03:48.328> +[03:48.632]v2: <03:48.632>Split <03:48.941>and <03:49.096>go <03:49.351>where, <03:49.821>back <03:50.030>to <03:50.155>wearing <03:50.458>knockoffs? <03:50.944> +[03:50.947]v2: <03:50.947>Haha, <03:51.509>knock <03:51.709>it <03:51.893>off, <03:52.426>Neiman's, <03:52.877>shop <03:53.093>it <03:53.261>off <03:53.547> +[03:53.734]v2: <03:53.734>Let's <03:53.888>talk <03:54.203>over <03:54.501>mai <03:54.792>tais, <03:55.138>waitress, <03:55.614>top <03:55.881>it <03:55.982>off <03:56.299> +[03:56.638]v2: <03:56.638>Hoes <03:57.078>like <03:57.328>vultures, <03:57.768>wanna <03:57.988>fly <03:58.178>in <03:58.589>your <03:58.689>Freddy <03:59.106>loafers <03:59.428> +[03:59.575]v2: <03:59.575>You <03:59.822>can't <04:00.108>blame <04:00.289>'em, <04:00.473>they <04:00.673>ain't <04:00.854>never <04:01.140>seen <04:01.374>Versace <04:01.841>sofas <04:02.285> +[04:02.527]v2: <04:02.527>Every <04:02.824>bag, <04:03.187>every <04:03.526>blouse, <04:03.990>every <04:04.258>bracelet <04:04.945> +[04:05.226]v2: <04:05.226>Comes <04:05.707>with <04:05.826>a <04:06.029>price <04:06.374>tag, <04:06.713>baby, <04:07.135>face <04:07.558>it <04:07.730> +[04:08.015]v2: <04:08.015>You <04:08.247>should <04:08.520>leave <04:08.847>if <04:09.002>you <04:09.181>can't <04:09.365>accept <04:09.645>the <04:09.746>basics <04:10.463> +[04:10.762]v2: <04:10.762>Plenty <04:11.215>hoes <04:11.548>in <04:11.700>the <04:11.881>baller-<04:12.183>**** <04:12.548>matrix <04:13.222> +[04:13.339]v2: <04:13.339>Invisibly <04:14.023>set, <04:14.451>the <04:14.653>Rolex <04:15.171>is <04:15.403>faceless <04:15.949> +[04:16.184]v2: <04:16.184>I'm <04:16.487>just <04:16.654>young, <04:17.021>rich, <04:17.204>and <04:17.404>tasteless, <04:18.373>P <04:18.745> +[04:19.531]v2: <04:19.531>Never <04:19.893>was <04:20.077>much <04:20.286>of <04:20.435>a <04:20.624>romantic <04:21.403> +[04:22.267]v2: <04:22.267>I <04:22.541>could <04:22.725>never <04:23.008>take <04:23.242>the <04:23.410>intimacy <04:24.523> +[04:25.188]v2: <04:25.188>And <04:25.462>I <04:25.630>know <04:25.795>I <04:25.979>did <04:26.297>damage <04:27.168> +[04:27.601]v2: <04:27.601>'Cause <04:27.821>the <04:27.987>look <04:28.189>in <04:28.320>your <04:28.511>eyes <04:28.874>is <04:29.094>killing <04:29.433>me <04:29.941> +[04:30.556]v2: <04:30.556>I <04:30.811>guess <04:31.168>you <04:31.268>knew <04:31.377>of <04:31.519>that <04:31.704>advantage <04:32.789> +[04:32.953]v2: <04:32.953>'Cause <04:33.243>you <04:33.411>could <04:33.582>blame <04:33.909>me <04:34.246>for <04:34.595>every<04:35.046>thing <04:35.998> +[04:36.123]v2: <04:36.123>And <04:36.295>I <04:36.462>don't <04:36.610>know <04:36.819>how <04:36.955>I'ma <04:37.324>manage <04:38.265> +[04:38.614]v2: <04:38.614>If <04:39.088>one <04:39.421>day, <04:39.771>you <04:39.955>just <04:40.120>up <04:40.320>and <04:40.475>leave <04:41.494> +[04:43.009]v2: <04:43.009>And <04:43.145>I <04:43.348>always <04:44.056>find, <04:44.448>yeah, <04:44.548>I <04:44.764>always <04:45.454>find <04:45.757>something <04:46.340>wrong <04:46.919> +[04:48.654]v2: <04:48.654>You <04:48.987>been <04:49.183>puttin' <04:49.534>up <04:49.634>with <04:49.837>my <04:50.052>shit <04:50.557>just <04:50.873>way <04:51.396>too <04:51.622>long <04:52.438> +[04:54.003]v2: <04:54.003>I'm <04:54.157>so <04:54.621>gifted <04:54.972>at <04:55.072>finding <04:55.859>what <04:56.031>I <04:56.406>don't <04:56.709>like <04:57.084>the <04:57.263>most <04:57.886> +[04:59.534]v2: <04:59.534>So <04:59.902>I <05:00.069>think <05:00.319>it's <05:00.616>time <05:01.181>for <05:01.497>us <05:02.044>to <05:02.217>have <05:02.728>a <05:02.883>toast <05:03.494> +[05:03.665]v2: <05:03.665>Let's <05:03.915>have <05:04.115>a <05:04.267>toast <05:04.582>for <05:04.798>the <05:04.947>douche<05:05.306>bags <05:06.150> +[05:06.401]v2: <05:06.401>Let's <05:06.573>have <05:06.775>a <05:06.912>toast <05:07.275>for <05:07.477>the <05:07.626>ass<05:08.065>holes <05:08.998> +[05:09.177]v2: <05:09.177>Let's <05:09.409>have <05:09.623>a <05:09.748>toast <05:10.099>for <05:10.277>the <05:10.377>scum<05:10.775>bags <05:11.718> +[05:12.054]v2: <05:12.054>Every <05:12.533>one <05:12.699>of <05:12.885>them <05:13.067>that <05:13.235>I <05:13.549>know <05:14.420> +[05:14.685]v2: <05:14.685>Let's <05:14.919>have <05:15.135>a <05:15.300>toast <05:15.618>for <05:15.834>the <05:15.967>jerk-<05:16.303>offs <05:17.150> +[05:17.600]v2: <05:17.600>That'll <05:18.034>never <05:18.373>take <05:18.730>work <05:19.123>off <05:20.020> +[05:20.021]v2: <05:20.021>Baby, <05:20.482>I <05:20.815>got <05:21.148>a <05:21.498>plan <05:21.826> +[05:22.091]v2: <05:22.091>Run <05:22.378>away <05:22.861>fast <05:23.194>as <05:23.562>you <05:24.210>can <05:25.499> \ No newline at end of file diff --git a/test/files/timed_lrc_2.lrc b/test/files/timed_lrc_2.lrc new file mode 100644 index 0000000..c54275b --- /dev/null +++ b/test/files/timed_lrc_2.lrc @@ -0,0 +1,61 @@ +[ti:Brooklyn Baby] +[ar:Lana Del Rey] +[offset:+0] +[by:Generated using SongSync] +[00:18.653]v1:<00:18.653>They <00:18.900>say <00:19.352>I'm <00:19.588>too <00:20.298>young <00:20.845>to <00:21.184>love <00:21.648>you <00:22.543> +[00:23.256]v1:<00:23.256>I <00:23.544>don't <00:24.092>know <00:24.556>what <00:24.942>I <00:25.603>need <00:27.213> +[00:27.790]v1:<00:27.790>They <00:28.013>think <00:28.298>I <00:28.554>don't <00:28.997>understand <00:31.076> +[00:31.145]v1:<00:31.145>The <00:31.392>freedom <00:32.201>land <00:32.960>of <00:33.168>the <00:33.403>sev<00:34.053>en<00:34.686>ties <00:36.506> +[00:36.556]v1:<00:36.556>I <00:36.697>think <00:37.076>I'm <00:37.385>too <00:37.950>cool <00:38.563>to <00:38.800>know <00:39.252>ya <00:40.174> +[00:40.738]v1:<00:40.738>You <00:40.928>say <00:41.212>I'm <00:41.539>like <00:41.777>the <00:42.122>ice <00:42.746>I <00:43.282>freeze <00:44.918> +[00:45.362]v1:<00:45.362>I'm <00:45.496>churning <00:46.192>out <00:46.709>novels <00:47.584>like <00:48.383> +[00:48.293]v1:<00:48.293>Beat <00:48.804>poetry <00:50.081><00:50.422>on <00:50.810>am<00:51.066>phe<00:51.433>ta<00:52.022>mines <00:55.180> +[00:55.966]v1:<00:55.966>I <00:56.403>say <01:00.424> +[01:04.696]v1:<01:04.696>I <01:05.062>say <01:09.047> +[01:10.260]v1:<01:10.260>Well, <01:10.525>my <01:10.870>boyfriend's <01:12.155>in <01:12.458>a <01:12.732>band <01:13.611> +[01:14.214]v1:<01:14.214>He <01:14.474>plays <01:14.930>guitar <01:15.632>while <01:16.049>I <01:16.274>sing <01:16.501>Lou <01:17.003>Reed <01:18.122> +[01:19.056]v1:<01:19.056>I've <01:19.362>got <01:19.576>feathers <01:20.582>in <01:21.082>my <01:21.433>hair <01:22.449> +[01:23.453]v1:<01:23.453>I <01:23.622>get <01:23.932>down <01:24.388>to <01:24.672>beat <01:25.050>poetry <01:26.763> +[01:27.804]v1:<01:27.804>And <01:28.128>my <01:28.420>jazz <01:28.878>collection's <01:30.264>rare <01:31.794> +[01:32.152]v1:<01:32.152>I <01:32.320>can <01:32.616>play <01:33.313>most <01:33.968>anything <01:35.705> +[01:36.648]v1:<01:36.648>I'm <01:37.054>a <01:37.311>Brooklyn <01:38.229>baby <01:40.161> +[01:41.029]v1:<01:41.029>I'm <01:41.353>a <01:41.656>Brooklyn <01:42.757>baby <01:44.788> +[01:55.700]v1:<01:55.700>They <01:55.920>say <01:56.275>I'm <01:56.566>too <01:57.197>young <01:57.733>to <01:58.113>love <01:58.548>you <01:59.523> +[01:59.977]v1:<01:59.977>They <02:00.256>say <02:00.665>I'm <02:00.839>too <02:01.440>dumb <02:01.926>to <02:02.219>see <02:03.994> +[02:04.538]v1:<02:04.538>They <02:04.719>judge <02:05.118>me <02:05.386>like <02:05.683>a <02:05.992>picture <02:06.790>book <02:07.511> +[02:07.548]v1:<02:07.548>By <02:07.836>the <02:08.098>colors, <02:08.872>like <02:09.508>they <02:09.716>forgot <02:10.722>to <02:11.049>read <02:12.902> +[02:13.332]v1:<02:13.332>I <02:13.567>think <02:13.858>we're <02:14.120>like <02:14.709>fire <02:15.600>and <02:15.863>water <02:17.032> +[02:17.652]v1:<02:17.652>I <02:17.839>think <02:18.125>we're <02:18.387>like <02:18.678>the <02:18.999>wind <02:19.588>and <02:19.933>sea <02:21.715> +[02:22.070]v1:<02:22.070>You're <02:22.323>burning <02:22.876>up, <02:23.132>I'm <02:23.429>cooling <02:24.280>down <02:25.134> +[02:25.227]v1:<02:25.227>You're <02:25.390>up, <02:25.694>I'm <02:26.216>down <02:26.969> +[02:27.357]v1:<02:27.357>You're <02:27.550>blind, <02:28.145>I <02:28.603>see <02:31.641> +[02:32.630]v1:<02:32.630>But <02:32.770>I'm <02:33.138>free <02:36.187> +[02:41.383]v1:<02:41.383>I'm <02:41.796>free <02:45.916> +[02:46.816]v1:<02:46.816>Well, <02:47.027>my <02:47.341>boyfriend's <02:48.627>in <02:48.883>a <02:49.199>band <02:50.102> +[02:50.738]v1:<02:50.738>He <02:51.042>plays <02:51.385>guitar <02:52.188>while <02:52.611>I <02:52.711>sing <02:53.152>Lou <02:53.545>Reed <02:54.574> +[02:55.513]v1:<02:55.513>I've <02:55.825>got <02:56.039>feathers <02:57.223>in <02:57.533>my <02:57.925>hair <02:58.909> +[02:59.900]v1:<02:59.900>I <03:00.081>get <03:00.397>down <03:01.003>to <03:01.265>beat <03:01.515>poetry <03:02.977> +[03:04.349]v1:<03:04.349>And <03:04.590>my <03:04.899>jazz <03:05.345>collection's <03:06.732>rare <03:07.933> +[03:08.664]v1:<03:08.664>I <03:08.887>can <03:09.083>play <03:09.773>most <03:10.356>anything <03:12.185> +[03:13.138]v1:<03:13.138>I'm <03:13.409>a <03:13.792>Brooklyn <03:14.615>baby <03:17.026> +[03:17.553]v1:<03:17.553>I'm <03:17.818>a <03:18.127>Brooklyn <03:19.222>baby <03:21.316> +[03:23.468]v1:<03:23.468>I'm <03:23.568>talking <03:24.143>about <03:24.411>my <03:24.702>generation <03:27.222> +[03:27.978]v1:<03:27.978>Talking <03:28.386>about <03:28.753>that <03:29.143>newer <03:29.969>nation <03:31.491> +[03:32.111]v1:<03:32.111>And <03:32.399>if <03:32.709>you <03:32.982>don't <03:33.559>like <03:34.154>it <03:34.698> +[03:34.708]v1:<03:34.708>You <03:35.074>can <03:35.675>beat <03:36.793>it <03:37.621> +[03:37.884]v1:<03:37.884>Beat <03:38.137>it, <03:38.619>baby <03:40.228> +[03:40.844]v1:<03:40.844>You <03:41.097>never <03:41.579>liked <03:41.858>the <03:42.168>way <03:42.566>I <03:42.870>said <03:43.494>it <03:44.535> +[03:44.905]v1:<03:44.905>If <03:45.128>you <03:45.433>don't <03:45.638>get <03:45.814>it, <03:46.309>then <03:46.808>forget <03:48.051>it <03:48.770> +[03:49.502]v1:<03:49.502>'Cause <03:49.897>I <03:49.997>don't <03:50.195>have <03:50.469>to <03:50.599>fucking <03:51.676>explain <03:53.937>it <03:55.377> +[03:56.761]v1:<03:56.761>And <03:57.014>my <03:57.347>boyfriend's <03:58.561>in <03:59.078>a <03:59.334>band <04:00.206> +[04:00.941]v1:<04:00.941>He <04:01.185>plays <04:01.444>guitar <04:02.187>while <04:02.598>I <04:02.698>sing <04:03.145>Lou <04:03.603>Reed <04:04.680> +[04:05.520]v1:<04:05.520>I've <04:05.856>got <04:06.082>feathers <04:07.314>in <04:07.661>my <04:07.928>hair <04:08.975> +[04:10.047]v1:<04:10.047>I <04:10.151>get <04:10.454>high <04:10.788>on <04:11.031>hydroponic <04:12.477>weed <04:13.575> +[04:14.530]v1:<04:14.530>And <04:14.759>my <04:15.128>jazz <04:15.717>collection's <04:17.144>rare <04:18.492> +[04:18.992]v1:<04:18.992>I <04:19.202>get <04:19.483>down <04:20.005>to <04:20.286>beat <04:20.582>poetry <04:22.868> +[04:23.393]v1:<04:23.393>I'm <04:23.664>a <04:23.991>Brooklyn <04:25.097>baby <04:26.788> +[04:27.736]v1:<04:27.736>I'm <04:28.036>a <04:28.363>Brooklyn <04:29.440>baby <04:31.619> +[04:50.175]v1:<04:50.175>Yeah, <04:50.275>my <04:50.493>boyfriend's <04:51.766>pretty <04:52.569>cool <04:53.980> +[04:54.491]v1:<04:54.491>But <04:54.821>he's <04:55.172>not <04:55.630>as <04:56.219>cool <04:56.814>as <04:57.219>me <04:58.437> +[04:58.599]v1:<04:58.599>'Cause <04:58.911>I'm <04:59.197>a <04:59.641>Brooklyn <05:00.595>baby <05:02.807> +[05:03.417]v1:<05:03.417>I'm <05:03.765>a <05:04.080>Brooklyn <05:05.514>baby <05:07.710> \ No newline at end of file diff --git a/test/files/timed_lrc_3.lrc b/test/files/timed_lrc_3.lrc new file mode 100644 index 0000000..6e80a6f --- /dev/null +++ b/test/files/timed_lrc_3.lrc @@ -0,0 +1,75 @@ +[ti:KONTINUUM] +[ar:SennaRin] +[offset:+0] +[by:Generated using SongSync] +[00:00.000]v1:<00:00.000>KONTINUUM <00:00.087><00:00.087>- <00:00.174><00:00.174>SennaRin<00:00.261><00:00.261> +[00:02.110]v1:<00:02.110>Deeper <00:04.756><00:04.756>cover <00:07.620><00:07.620>Spotlight <00:10.339><00:10.339>lover<00:12.308><00:12.308> +[00:12.308]v1:<00:12.308>The <00:12.507><00:12.507>stars <00:13.235><00:13.235>circle <00:14.314><00:14.314>around <00:14.907><00:14.907>your <00:15.499><00:15.499>head<00:16.786><00:16.786> +[00:16.786]v1:<00:16.786>Nations <00:17.131><00:17.131>stoop <00:17.464><00:17.464>to <00:17.722><00:17.722>kiss <00:18.128><00:18.128>the <00:18.408><00:18.408>ground <00:18.751><00:18.751>you <00:19.016><00:19.016>walk <00:19.906><00:19.906>on<00:21.178><00:21.178> +[00:22.361]v1:<00:22.361>Shiller<00:23.221><00:23.221> +[00:23.221]v1:<00:23.221>Better <00:23.388><00:23.388>a <00:23.556><00:23.556>lie?<00:23.876><00:23.876> +[00:23.876]v1:<00:23.876>Better <00:24.060><00:24.060>I <00:24.259><00:24.259>die?<00:24.707><00:24.707> +[00:24.707]v1:<00:24.707>Better <00:24.924><00:24.924>I <00:25.126><00:25.126>make <00:25.299><00:25.299>it <00:25.492><00:25.492>stop?<00:26.051><00:26.051> +[00:26.051]v1:<00:26.051>Gravity <00:26.343><00:26.343>shift<00:26.692><00:26.692> +[00:26.692]v1:<00:26.692>In <00:26.875><00:26.875>the <00:27.059><00:27.059>abyss<00:27.419><00:27.419> +[00:27.419]v1:<00:27.419>Show <00:27.596><00:27.596>me <00:27.755><00:27.755>the <00:27.908><00:27.908>shoe <00:28.076><00:28.076>will <00:28.259><00:28.259>drop<00:28.778><00:28.778> +[00:28.778]v1:<00:28.778>Could <00:29.074><00:29.074>be <00:29.367><00:29.367>I'm <00:29.735><00:29.735>just <00:30.110><00:30.110>apassing <00:30.934><00:30.934>trend<00:31.704><00:31.704> +[00:31.704]v1:<00:31.704>You <00:31.958><00:31.958>have <00:32.278><00:32.278>to <00:32.518><00:32.518>tune <00:33.206><00:33.206>in <00:33.709><00:33.709>cuz <00:34.285><00:34.285>it's <00:35.049><00:35.049>not <00:35.721><00:35.721>the <00:36.320><00:36.320>end<00:37.457><00:37.457> +[00:38.273]v1:<00:38.273>Kill <00:38.968><00:38.968>this <00:39.585><00:39.585>destiny <00:41.428><00:41.428>that's <00:41.757><00:41.757>tryna <00:42.093><00:42.093>to <00:42.445><00:42.445>tell <00:42.764><00:42.764>you <00:43.474><00:43.474>what <00:44.090><00:44.090>you <00:44.713><00:44.713>should <00:44.924><00:44.924>be<00:45.457><00:45.457> +[00:45.457]v1:<00:45.457>Into <00:45.920><00:45.920>the <00:46.533><00:46.533>void <00:46.893><00:46.893>I <00:47.606><00:47.606>scream<00:48.271><00:48.271> +[00:48.271]v1:<00:48.271>Someone <00:48.524><00:48.524>try <00:48.910><00:48.910>and <00:49.414><00:49.414>stop <00:49.869><00:49.869>me<00:50.563><00:50.563> +[00:50.563]v1:<00:50.563>Check <00:50.842><00:50.842>out <00:51.178><00:51.178>my <00:51.453><00:51.453>face<00:52.193><00:52.193> +[00:52.193]v1:<00:52.193>I <00:52.505><00:52.505>got <00:53.258><00:53.258>the <00:53.609><00:53.609>look<00:54.041><00:54.041> +[00:54.041]v1:<00:54.041>Not <00:54.288><00:54.288>gonna <00:54.920><00:54.920>play <00:55.336><00:55.336>by <00:55.991><00:55.991>the <00:56.263><00:56.263>book<00:56.711><00:56.711> +[00:56.711]v1:<00:56.711>This <00:57.040><00:57.040>mic <00:57.400><00:57.400>just <00:57.792><00:57.792>gotturned <00:58.751><00:58.751>on<00:59.476><00:59.476> +[00:59.476]v1:<00:59.476>Make <00:59.757><00:59.757>your <01:00.172><01:00.172>decision<01:02.127><01:02.127> +[01:02.127]v1:<01:02.127>Dare <01:02.452><01:02.452>to <01:02.700><01:02.700>believe<01:04.004><01:04.004> +[01:04.004]v1:<01:04.004>To <01:04.181><01:04.181>become<01:05.124><01:05.124> +[01:05.124]v1:<01:05.124>To <01:05.412><01:05.412>become<01:06.885><01:06.885> +[01:06.885]v1:<01:06.885>Spotlight <01:08.308><01:08.308>lover<01:09.735><01:09.735> +[01:09.735]v1:<01:09.735>Deepest <01:11.053><01:11.053>cover<01:12.213><01:12.213> +[01:12.213]v1:<01:12.213>If <01:12.421><01:12.421>you <01:12.830><01:12.830>can <01:13.135><01:13.135>dare <01:13.544><01:13.544>to <01:13.823><01:13.823>believe<01:15.028><01:15.028> +[01:15.028]v1:<01:15.028>To <01:15.254><01:15.254>become<01:16.223><01:16.223> +[01:16.223]v1:<01:16.223>To <01:16.436><01:16.436>become<01:18.045><01:18.045> +[01:18.045]v1:<01:18.045>Spotlight <01:19.390><01:19.390>lover<01:20.839><01:20.839> +[01:20.839]v1:<01:20.839>Deepest <01:22.190><01:22.190>cover<01:23.303><01:23.303> +[01:23.757]v1:<01:23.757>Spotlight<01:26.184><01:26.184> +[01:27.647]v1:<01:27.647>This <01:27.846><01:27.846>star's <01:28.502><01:28.502>future <01:29.046><01:29.046>hangs <01:29.478><01:29.478>by <01:29.854><01:29.854>a <01:30.453><01:30.453>thread<01:31.807><01:31.807> +[01:32.416]v1:<01:32.416>Am <01:32.624><01:32.624>I <01:32.784><01:32.784>strong <01:33.194><01:33.194>enough?<01:33.556><01:33.556> +[01:33.556]v1:<01:33.556>To <01:33.742><01:33.742>be <01:34.130><01:34.130>continued<01:36.698><01:36.698> +[01:37.538]v1:<01:37.538>Chiller<01:38.510><01:38.510> +[01:38.510]v1:<01:38.510>Follow <01:38.670><01:38.670>a <01:38.838><01:38.838>key<01:39.175><01:39.175> +[01:39.175]v1:<01:39.175>Follow <01:39.366><01:39.366>a <01:39.574><01:39.574>face<01:39.897><01:39.897> +[01:39.897]v1:<01:39.897>Follow <01:40.102><01:40.102>a <01:40.325><01:40.325>clue <01:40.582><01:40.582>from <01:40.806><01:40.806>space<01:41.227><01:41.227> +[01:41.227]v1:<01:41.227>Evil <01:41.403><01:41.403>'n' <01:41.579><01:41.579>good<01:41.955><01:41.955> +[01:41.955]v1:<01:41.955>Deep <01:42.130><01:42.130>in <01:42.298><01:42.298>the <01:42.443><01:42.443>wood<01:42.748><01:42.748> +[01:42.748]v1:<01:42.748>Nobody <01:42.907><01:42.907>left <01:43.093><01:43.093>a <01:43.307><01:43.307>trace<01:44.098><01:44.098> +[01:44.098]v1:<01:44.098>They <01:44.379><01:44.379>left <01:44.715><01:44.715>me <01:45.018><01:45.018>on <01:45.426><01:45.426>this <01:45.770><01:45.770>cliff <01:46.093><01:46.093>to <01:46.357><01:46.357>die<01:46.937><01:46.937> +[01:46.937]v1:<01:46.937>My <01:47.237><01:47.237>fingers <01:48.014><01:48.014>slipping<01:49.204><01:49.204> +[01:49.204]v1:<01:49.204>It's <01:49.686><01:49.686>my <01:50.389><01:50.389>last <01:51.045><01:51.045>goodbye<01:53.110><01:53.110> +[01:53.541]v1:<01:53.541>Kill <01:54.238><01:54.238>this <01:55.037><01:55.037>destiny <01:56.615><01:56.615>that's <01:56.965><01:56.965>tryna <01:57.470><01:57.470>to <01:57.757><01:57.757>tell <01:58.127><01:58.127>you <01:58.740><01:58.740>what <01:59.348><01:59.348>you <02:00.084><02:00.084>should <02:00.519><02:00.519>be<02:00.880><02:00.880> +[02:00.880]v1:<02:00.880>Into <02:01.510><02:01.510>the <02:01.822><02:01.822>void <02:02.244><02:02.244>I <02:02.852><02:02.852>scream<02:03.572><02:03.572> +[02:03.572]v1:<02:03.572>Someone <02:03.905><02:03.905>try <02:04.247><02:04.247>and <02:04.616><02:04.616>stop <02:04.912><02:04.912>me<02:05.746><02:05.746> +[02:05.746]v1:<02:05.746>Check <02:05.969><02:05.969>out <02:06.320><02:06.320>my <02:06.690><02:06.690>face<02:07.416><02:07.416> +[02:07.416]v1:<02:07.416>I <02:07.736><02:07.736>got <02:08.426><02:08.426>the <02:08.701><02:08.701>look<02:09.294><02:09.294> +[02:09.294]v1:<02:09.294>Not <02:09.621><02:09.621>gonna <02:10.221><02:10.221>play <02:10.621><02:10.621>by <02:11.220><02:11.220>the <02:11.434><02:11.434>book<02:12.003><02:12.003> +[02:12.003]v1:<02:12.003>This <02:12.299><02:12.299>mic <02:12.666><02:12.666>just <02:13.049><02:13.049>got <02:13.442><02:13.442>turned <02:14.058><02:14.058>on<02:15.617><02:15.617> +[02:15.914]v1:<02:15.914>Kill <02:16.537><02:16.537>this <02:17.330><02:17.330>destiny <02:18.956><02:18.956>that's <02:19.273><02:19.273>tryna <02:19.668><02:19.668>to <02:20.041><02:20.041>tell <02:20.377><02:20.377>you <02:21.113><02:21.113>what <02:21.764><02:21.764>you <02:22.458><02:22.458>should <02:22.793><02:22.793>be<02:23.195><02:23.195> +[02:23.195]v1:<02:23.195>Into <02:23.872><02:23.872>the <02:24.201><02:24.201>void <02:24.705><02:24.705>I <02:25.263><02:25.263>scream<02:25.895><02:25.895> +[02:25.895]v1:<02:25.895>Someone <02:26.249><02:26.249>try <02:26.593><02:26.593>and <02:27.057><02:27.057>stop <02:27.525><02:27.525>me<02:28.185><02:28.185> +[02:28.185]v1:<02:28.185>Check <02:28.442><02:28.442>out <02:28.914><02:28.914>my <02:29.214><02:29.214>face<02:29.743><02:29.743> +[02:29.743]v1:<02:29.743>I <02:30.087><02:30.087>got <02:30.810><02:30.810>the <02:31.073><02:31.073>look<02:31.627><02:31.627> +[02:31.627]v1:<02:31.627>Not <02:31.907><02:31.907>gonna <02:32.523><02:32.523>play <02:32.948><02:32.948>by <02:33.628><02:33.628>the <02:33.859><02:33.859>book<02:34.322><02:34.322> +[02:34.322]v1:<02:34.322>This <02:34.651><02:34.651>mic <02:35.018><02:35.018>just <02:35.355><02:35.355>got <02:35.747><02:35.747>turned <02:36.236><02:36.236>on<02:37.091><02:37.091> +[02:37.091]v1:<02:37.091>Make <02:37.419><02:37.419>your <02:37.939><02:37.939>decision<02:39.763><02:39.763> +[02:39.763]v1:<02:39.763>Dare <02:40.035><02:40.035>to <02:40.283><02:40.283>believe<02:41.547><02:41.547> +[02:41.547]v1:<02:41.547>To <02:41.788><02:41.788>become<02:42.751><02:42.751> +[02:42.751]v1:<02:42.751>To <02:43.003><02:43.003>become<02:44.459><02:44.459> +[02:44.459]v1:<02:44.459>Spotlight <02:45.947><02:45.947>lover<02:47.308><02:47.308> +[02:47.308]v1:<02:47.308>Deepest <02:48.656><02:48.656>cover<02:49.711><02:49.711> +[02:49.711]v1:<02:49.711>If <02:49.999><02:49.999>you <02:50.408><02:50.408>can <02:50.777><02:50.777>dare <02:51.135><02:51.135>to <02:51.463><02:51.463>believe<02:52.680><02:52.680> +[02:52.680]v1:<02:52.680>To <02:52.879><02:52.879>become<02:53.943><02:53.943> +[02:53.943]v1:<02:53.943>To <02:54.183><02:54.183>become<02:55.666><02:55.666> +[02:55.666]v1:<02:55.666>Spotlight <02:57.461><02:57.461>lover<02:58.462><02:58.462> +[02:58.462]v1:<02:58.462>Deepest <02:59.814><02:59.814>cover<03:01.157><03:01.157> +[03:01.157]v1:<03:01.157>Spotlight<03:03.710><03:03.710> \ No newline at end of file diff --git a/test/files/timed_lrc_4.lrc b/test/files/timed_lrc_4.lrc new file mode 100644 index 0000000..f15839d --- /dev/null +++ b/test/files/timed_lrc_4.lrc @@ -0,0 +1,155 @@ +[ti:We Cry Together] +[ar:Kendrick Lamar & Taylour Paige] +[offset:+0] +[by:Generated using SongSync] +[00:03.151]v2:<00:03.151>Oh-<00:03.716>ooh-<00:04.135>oh-<00:04.583>ooh, <00:04.999>whoa <00:07.861> +[00:09.870]v2:<00:09.870>Oh-<00:10.353>ooh-<00:10.769>oh-<00:11.153>ooh, <00:11.569>whoa-<00:14.315>oh <00:15.595> +[00:16.441]v2:<00:16.441>Hold <00:19.454>on<00:20.185>to <00:20.618>each <00:21.050>o<00:21.254>ther <00:22.270> +[00:22.990]v2:<00:22.990>Hold <00:25.691>on<00:26.691>to <00:27.107>each <00:27.526>o<00:27.779>ther <00:28.955> +[00:31.088]v2:<00:31.088>This <00:31.256>is <00:31.421>what <00:31.584>the <00:31.723>world <00:32.005>sounds <00:32.272>like <00:32.595> +[00:31.767]v1:<00:31.767>Nah, <00:32.250>fuck <00:32.601>you, <00:32.818>****! <00:33.330> +[00:33.079]v2:<00:33.079>Fuck <00:33.514>you, <00:33.730>bitch! <00:34.109> +[00:38.799]v1:<00:38.799>You <00:38.999>got <00:39.215>me <00:39.399>fucked <00:39.748>up <00:40.130> +[00:41.950]v1:<00:41.950>Fuck <00:42.283>you, <00:42.858><00:43.771>fuck <00:44.343><00:44.670>you <00:45.220> +[00:45.645]v2:<00:45.645>I <00:45.845>swear <00:46.061>I'm <00:46.261>tired <00:46.534>of <00:46.634>these <00:46.978>emotional <00:47.527>ass, <00:47.861>ungrateful <00:48.343>ass <00:48.694>bitches <00:49.123> +[00:48.540]v1:<00:48.540>Shut <00:48.890>the <00:49.050>fuck <00:49.224>up <00:49.350> +[00:49.360]v2:<00:49.360>Un<00:49.608>stable <00:49.960>ass, <00:50.349><00:50.394>confrontational <00:51.226>ass, <00:51.617><00:51.775>dumb <00:52.092>bitches <00:52.533> +[00:52.570]v2:<00:52.570>You <00:52.786>wanna <00:53.030>bring <00:53.219>a <00:53.319>**** <00:53.701>down, <00:54.089><00:54.218>even <00:54.488>when <00:54.650>I'm <00:54.837>tryna <00:55.021>do <00:55.205>right <00:55.423> +[00:55.669]v2:<00:55.669>We <00:55.818>could <00:55.986>go <00:56.135>our <00:56.303>separate <00:56.618>ways <00:56.885>right <00:57.170>now, <00:57.429><00:57.556>you <00:57.742>could <00:57.921>move <00:58.089>on <00:58.356>with <00:58.521>your <00:58.654>life, <00:58.858><00:58.982>I <00:59.250>swear <00:59.583>to <00:59.751>God <01:00.080> +[00:58.929]v1:<00:58.929>Fuck <00:59.212>you, <00:59.446>****, <00:59.780>you <01:00.012>love <01:00.329>a <01:00.529>pity <01:00.846>party, <01:01.212>I <01:01.452>won't <01:01.671>show <01:01.921>up <01:02.292> +[01:02.396]v1:<01:02.396>Always <01:02.729>act <01:03.012>like <01:03.180>your <01:03.360>shit <01:03.663>don't <01:03.980>stink, <01:04.345>motherfucka, <01:05.144>grow <01:05.428>up <01:05.679> +[01:05.767]v1:<01:05.767>Forever <01:06.167>late <01:06.500>for <01:06.700>shit, <01:06.908><01:06.983>won't <01:07.250>buy <01:07.447>shit, <01:07.768><01:07.889>sit <01:08.121>around <01:08.474>and <01:08.574>deny <01:09.089>shit <01:09.460> +[01:08.960]v2:<01:08.960>Man <01:10.018> +[01:09.488]v1:<01:09.488>Fuck <01:09.760>around <01:10.106>on <01:10.288>a <01:10.472>side <01:10.786>bitch, <01:11.066>then <01:11.249>come <01:11.484>fuckin' <01:11.922>up <01:12.189>my <01:12.483>shit? <01:12.770> +[01:12.447]v2:<01:12.447>What? <01:12.781>Fuckin' <01:13.130>up <01:13.413>yo' <01:13.647>shit? <01:13.944><01:14.204>You <01:14.404>must <01:14.583>be <01:14.788>bleedin' <01:15.102>and <01:15.327>some <01:15.558>more <01:15.755>shit <01:16.084> +[01:16.181]v2:<01:16.181>Bitch, <01:16.455>I <01:16.609>don't <01:16.760>know <01:16.981>shit, <01:17.226><01:17.240>fuck <01:17.565>yo' <01:17.848>feelings <01:18.254> +[01:18.264]v1:<01:18.264>You <01:18.480>on <01:18.664>some <01:18.997>ho <01:19.280>shit <01:19.549> +[01:19.603]v1:<01:19.603>See, <01:19.819>I <01:19.982>don't <01:20.146>know <01:20.336>why <01:20.551>you <01:20.771>like <01:21.003>playin' <01:21.320>mind <01:21.670>games <01:21.952>with <01:22.136>me <01:22.414> +[01:22.245]v2:<01:22.245>Mind <01:22.578>games? <01:22.852>Okay <01:23.269> +[01:22.517]v1:<01:22.517>Bitch, <01:22.834>I <01:23.002>ain't <01:23.250>slow, <01:23.481>nor <01:23.749>ditsy, <01:24.162><01:24.250>I <01:24.424>know <01:24.679>when <01:24.901>you <01:25.117>bein' <01:25.349>distant <01:25.815> +[01:25.825]v1:<01:25.825>I <01:26.116>know <01:26.300>when <01:26.513>you <01:26.748>fake <01:27.014>busy, <01:27.393><01:27.497>get <01:27.748>out <01:27.908>yo' <01:28.168>feelings <01:28.497>and <01:28.699>miss <01:28.985>me <01:29.126>with <01:29.315>that <01:29.473>re<01:29.714>verse <01:30.085>psycholo<01:30.749>gy <01:31.271> +[01:30.679]v2:<01:30.679>Man, <01:31.047>bitch, <01:31.279>you <01:31.479>trippin', <01:31.879>who <01:32.113>got <01:32.345>you <01:32.529>that <01:32.713>Rollie <01:33.063>chain? <01:33.543> +[01:33.335]v1:<01:33.335>And <01:33.503>who <01:33.684>put <01:33.902>that <01:34.170>car <01:34.436>in <01:34.618>my <01:34.868>name? <01:35.215> +[01:34.941]v2:<01:34.941>What, <01:35.120>you <01:35.341>think <01:35.547>I'ma <01:35.845><01:35.940>kiss <01:36.197>yo' <01:36.448>ass? <01:36.771> +[01:36.782]v1:<01:36.782>Nah, <01:37.115>****, <01:37.382>you <01:37.715>fuckin' <01:38.098>lame <01:38.502> +[01:38.252]v2:<01:38.252>You <01:38.463>know <01:38.650>what? <01:38.917>Fuck <01:39.268>you, <01:39.535>bitch <01:39.668> +[01:39.768]v1:<01:39.768>Fuck <01:40.031>you, <01:40.211>****! <01:40.584> +[01:40.594]v2:<01:40.594>Nah, <01:41.078><01:41.140>fuck <01:41.520>you, <01:41.752>bitch! <01:42.181> +[01:41.866]v1:<01:41.866>Nah, <01:42.118>f<01:42.334>uck <01:42.667>you, <01:42.866>****! <01:43.238> +[01:43.045]v2:<01:43.045>Fuck <01:43.446>you, <01:43.728>bitch! <01:44.035> +[01:44.045]v1:<01:44.045>Nah, <01:44.277>f<01:44.513>uck <01:44.686>you, <01:44.920>****! <01:45.438> +[01:45.170]v2:<01:45.170>Nah, <01:45.618><01:45.687>fuck <01:46.003>you, <01:46.235>bitch! <01:46.542> +[01:46.558]v1:<01:46.558>Fuck <01:46.843>you, <01:47.049>****! <01:47.441>Fuck <01:47.774>you, <01:47.929>****! <01:48.313> +[01:46.606]v2:<01:46.606>Fuck <01:46.889>you, <01:47.206>bitch! <01:47.542><01:47.881>Fuck <01:48.163>you, <01:48.419><01:48.497>fuck <01:48.798>you, <01:49.035><01:49.065>fuck <01:49.347>you, <01:49.561><01:49.579>fuck <01:49.913>you, <01:50.167>bitch! <01:50.462> +[01:49.862]v1:<01:49.862>Fuck <01:50.198>you, <01:50.497>****! <01:50.942> +[01:51.161]v2:<01:51.161>Fuck <01:51.509>you, <01:51.761>bitch! <01:52.013> +[01:52.023]v1:<01:52.023>Nah, <01:52.258>f<01:52.407>uck <01:52.706>you! <01:53.100> +[01:53.212]v1:<01:53.212>Wastin' <01:53.530>my <01:53.828>time <01:54.095>and <01:54.330>energy <01:54.762>tryna <01:55.079>be <01:55.330>good <01:55.644>to <01:55.863>you <01:56.140> +[01:56.221]v1:<01:56.221>Lost <01:56.573>friends, <01:57.021>family, <01:57.405>gained <01:57.821>more <01:58.122>enemies <01:58.688>'cause <01:58.955>of <01:59.170>you <01:59.413> +[01:59.482]v1:<01:59.482>Bitches <01:59.866>starin' <02:00.216>at <02:00.482>me <02:00.749>in <02:00.930>Zara, <02:01.355><02:01.501>hoes <02:01.919>scratchin' <02:02.367>my <02:02.653>cars <02:02.885>up <02:03.130> +[02:03.242]v1:<02:03.242>Shoulda <02:03.709>followed <02:04.010>my <02:04.274>mind <02:04.525>in <02:04.709>'09 <02:05.138>and <02:05.303>just <02:05.570>moved <02:05.809>to <02:05.909>Georgia <02:06.524> +[02:05.966]v2:<02:05.966>Oh, <02:06.284>what <02:06.534>that's <02:06.766>my <02:07.033>fault <02:07.284>now? <02:07.553> +[02:07.786]v2:<02:07.786>Bitch, <02:08.103>you <02:08.287>power <02:08.651>trippin' <02:08.970>or <02:09.169>guilt <02:09.487>trippin', <02:09.820>I <02:10.036>held <02:10.303>yo' <02:10.519>ass <02:10.786>down <02:11.266> +[02:10.936]v1:<02:10.936>You <02:11.219>just <02:11.438>kept <02:11.670>me <02:11.846>down, <02:12.168>that's <02:12.387>a <02:12.536>big <02:12.853>difference <02:13.450> +[02:13.460]v1:<02:13.460>Stressin' <02:13.913>myself, <02:14.425>tryna <02:14.793>figure <02:15.209>why <02:15.492>I'm <02:15.708>not <02:15.950>good <02:16.292>enough <02:16.673> +[02:16.846]v1:<02:16.846>Goin' <02:17.179>to <02:17.364>church, <02:17.795>prayin' <02:18.179>for <02:18.411>you, <02:18.653><02:18.713>searchin' <02:19.137>for <02:19.353>good <02:19.585>in <02:19.737>us <02:20.116> +[02:20.214]v1:<02:20.214>Lil' <02:20.614>dick-<02:20.978>ass <02:21.296>**** <02:21.696>that's <02:21.947>tryna <02:22.262>go <02:22.528>big <02:22.944> +[02:22.947]v2:<02:22.947>But <02:23.130>you <02:23.332>were <02:23.518>s<02:23.618>uckin' <02:23.998>this <02:24.198>dick, <02:24.451>though <02:24.799> +[02:24.578]v1:<02:24.578>Well, <02:24.797>shit, <02:25.045>I <02:25.260>shoulda <02:25.647>sucked <02:26.045>his <02:26.541> +[02:26.341]v2:<02:26.341>What <02:26.509>you <02:26.609>say? <02:27.029> +[02:27.029]v1:<02:27.029>I <02:27.277>shoulda <02:27.643>found <02:28.011>a <02:28.195>bigger <02:28.576>dick! <02:29.001> +[02:28.636]v2:<02:28.636>Bitch, <02:28.953>get <02:29.137>the <02:29.319>fuck <02:29.569>out <02:29.769>my <02:29.969>face! <02:30.353> +[02:30.419]v1:<02:30.419>Oh, <02:30.600>what, <02:30.901>you <02:31.072>mad? <02:31.394> +[02:31.309]v2:<02:31.309>Shut <02:31.560>up, <02:31.858>bitch! <02:32.155>You <02:32.372>got <02:32.535>me <02:32.699>fucked <02:33.035>up <02:33.293>today, <02:33.671>on <02:33.903>God! <02:34.531> +[02:33.841]v1:<02:33.841>Ah-<02:34.142>ha, <02:34.388>you <02:34.616>mad, <02:34.924>lil' <02:35.276>feelings <02:35.677>is <02:35.892>shot <02:36.249> +[02:36.385]v1:<02:36.385>Go <02:36.603>text <02:36.887>that <02:37.137>raggedy <02:37.569>bitch <02:37.918>and <02:38.031>tell <02:38.245>her <02:38.436>you <02:38.688>all <02:38.848>that <02:38.991>she <02:39.337>got <02:39.585> +[02:39.405]v2:<02:39.405>Man, <02:39.755>what <02:40.123>bitch? <02:40.517> +[02:39.751]v1:<02:39.751>Let <02:39.966>her <02:40.098>know <02:40.316>you <02:40.442>packin' <02:40.884>yo' <02:41.084>shit <02:41.426><02:41.495>and <02:41.676>gotta <02:41.925>move <02:42.191>by <02:42.380>the <02:42.584>first <02:42.911> +[02:42.817]v2:<02:42.817>Man, <02:43.053>give <02:43.194>me <02:43.293>these <02:43.442>fuckin' <02:43.751>keys, <02:44.050>dawg <02:44.191> +[02:44.201]v1:<02:44.201>Give <02:44.367>me <02:44.551>my <02:44.751>keys, <02:45.178>I'ma <02:45.433>be <02:45.700>late <02:45.951>for <02:46.151>work <02:46.430> +[02:46.225]v2:<02:46.225>Fuck <02:46.546>yo' <02:46.758>job, <02:47.126>today <02:47.535>gon' <02:47.852>be <02:48.002>the <02:48.151>day <02:48.402>you <02:48.634>walk <02:48.918>to <02:49.084>that <02:49.349>bitch <02:49.692> +[02:49.746]v1:<02:49.746>Give <02:49.962>me <02:50.112>my <02:50.312>fuckin' <02:50.813>keys <02:51.357> +[02:51.024]v2:<02:51.024>Nah, <02:51.291>I <02:51.608>like <02:51.875>you <02:52.109>parked <02:52.370>in <02:52.586>that <02:52.817>bitch <02:53.200> +[02:53.214]v1:<02:53.214>Give <02:53.397>me <02:53.579>my <02:53.744>keys, <02:54.289>bro <02:54.726> +[02:54.321]v2:<02:54.321>On <02:54.771>God, <02:55.144>you <02:55.321>ain't <02:55.521>gettin' <02:55.755>these <02:56.041>keys <02:56.475> +[02:56.497]v1:<02:56.497>Give <02:56.697>me <02:56.881>my <02:57.110>fuckin' <02:57.630>keys! <02:58.225> +[02:57.894]v2:<02:57.894>Ha, <02:58.380>now <02:58.713>you <02:58.932>mad <02:59.214>at <02:59.446>me, <02:59.841>I <03:00.028>got <03:00.246>you <03:00.510>hollerin' <03:00.860>for <03:01.073>nothin' <03:01.481> +[03:01.494]v1:<03:01.494>I <03:01.678>do <03:01.894>the <03:02.094>same <03:02.377>when <03:02.577>we <03:02.811>fuckin' <03:03.355> +[03:03.127]v2:<03:03.127>Act <03:03.432>like <03:03.624>that <03:03.828>pussy <03:04.289>ain't <03:04.594>loose <03:04.925> +[03:04.808]v1:<03:04.808>I'd <03:05.008>rather <03:05.408>act <03:05.675>like <03:05.881>I'm <03:06.072>cummin' <03:06.637> +[03:06.345]v2:<03:06.345>I'd <03:06.652>rather <03:07.116>fuck <03:07.428>off <03:07.663>the <03:07.780>juice <03:08.108> +[03:08.118]v1:<03:08.118>I'd <03:08.392>rather <03:08.818>fuck <03:09.200>on <03:09.304>yo' <03:09.465>cousin <03:09.976> +[03:09.891]v2:<03:09.891>Bitch, <03:10.129>you <03:10.260>said <03:10.491>you <03:10.679>gon' <03:10.860>fuck <03:11.101>who? <03:11.492> +[03:11.494]v1:<03:11.494>You <03:11.694>heard <03:11.878>me, <03:12.176>****, <03:12.579>it's <03:12.846>nothin' <03:13.417> +[03:12.340]v2:<03:12.340>A'ight <03:12.870> +[03:13.219]v2:<03:13.219>You <03:13.385>know <03:13.548>what? <03:13.822><03:13.912>Fuck <03:14.174>you, <03:14.406>bitch! <03:14.808> +[03:13.785]v1:<03:13.785>Fuck <03:14.036>you, <03:14.302>****! <03:14.742> +[03:15.111]v2:<03:15.111>Fuck <03:15.429>you, <03:15.711>bitch! <03:16.122> +[03:15.790]v1:<03:15.790>Nah, <03:16.209>fuck <03:16.542>you, <03:16.792>****! <03:17.337> +[03:16.716]v2:<03:16.716>No, <03:17.065><03:17.268>fuck <03:17.516>you, <03:17.860>bitch! <03:18.156> +[03:18.156]v1:<03:18.156>Nah, <03:18.556>fuck <03:18.873>you, <03:19.063>****! <03:19.505> +[03:19.228]v2:<03:19.228>Fuck <03:19.561>you, <03:19.844>bitch! <03:20.172> +[03:19.694]v1:<03:19.694>Fuck <03:19.977>you! <03:20.267>I'm <03:20.451>sick <03:20.619>of <03:20.803>this <03:20.969>**** <03:21.233> +[03:20.406]v2:<03:20.406>Fuck <03:20.704>you, <03:21.022>bitch! <03:21.227> +[03:21.237]v1:<03:21.237>Fuck <03:21.539>you! <03:21.856>Fuck <03:22.103>you! <03:22.667><03:22.819>Fuck <03:23.136>you! <03:23.421> +[03:21.719]v2:<03:21.719>Fuck <03:22.001>you, <03:22.268>fuck <03:22.503>you, <03:22.699>fuck <03:22.942>you, <03:23.150>fuck <03:23.397>you, <03:23.594>bi- <03:23.852> +[03:23.989]v2:<03:23.989>Fuck <03:24.204>you, <03:24.472>bitch! <03:24.888> +[03:25.225]v2:<03:25.225>Stupid <03:25.807>ass <03:26.276>bitch <03:26.657> +[03:26.719]v2:<03:26.719>I <03:26.938>don't <03:27.154>even <03:27.453>know <03:27.703>why <03:27.903>I <03:28.119>fuck <03:28.322>with <03:28.535>you <03:28.736> +[03:28.746]v1:<03:28.746>I'll <03:29.042>be <03:29.242>damned <03:29.541>if <03:29.709>I <03:29.893>stuck <03:30.242>with <03:30.426>you <03:30.738> +[03:30.279]v2:<03:30.279>Changed <03:30.746>my <03:30.994>number, <03:31.383>I'm <03:31.628>duckin' <03:32.012>you, <03:32.279>bitch <03:32.623> +[03:32.105]v1:<03:32.105>Bitch, <03:32.505>whatever <03:32.953>is <03:33.121>comfortable <03:33.878> +[03:33.878]v2:<03:33.878>This <03:34.062>the <03:34.195>type <03:34.454>of <03:34.554>shit <03:34.837>couples <03:35.278>do? <03:35.547>Shoulda <03:35.846>thought <03:36.081>about <03:36.441><03:36.493>cuffin' <03:36.928>you, <03:37.144>bitch <03:37.642> +[03:37.168]v1:<03:37.168>****, <03:37.466>you <03:37.769>dirty <03:38.086>and <03:38.369>you <03:38.686>broke <03:39.172> +[03:38.768]v2:<03:38.768>Ho, <03:39.066>you <03:39.317>goofy <03:39.733>and <03:40.000>gullible, <03:40.538><03:40.611>fuck <03:40.877>you <03:41.144>talkin' <03:41.411>'bout? <03:41.840> +[03:40.086]v1:<03:40.086>Mmm, <03:40.436>the <03:40.665>insecurities <03:41.746>you <03:41.918>got <03:42.178>won't <03:42.451>mind-<03:42.750>fuck <03:43.051>me <03:43.358> +[03:43.469]v1:<03:43.469>Womanizer, <03:44.285>got <03:44.536>no <03:44.802>affection <03:45.250>from <03:45.485>yo' <03:45.752>momma, <03:46.191>I <03:46.425>see <03:46.724> +[03:46.482]v2:<03:46.482>Don't <03:46.691>speak <03:46.949>on <03:47.094>my <03:47.265>momma, <03:47.623>the <03:47.789>fuck <03:48.106>is <03:48.271>yo' <03:48.454>problem? <03:48.868> +[03:48.878]v1:<03:48.878>That <03:49.129>bitch <03:49.396>don't <03:49.579>like <03:49.827>me <03:50.039>anyways <03:50.732> +[03:50.289]v2:<03:50.289>Bitch, <03:50.543>she <03:50.800>gave <03:51.061>you <03:51.197>the <03:51.358>Hon<03:51.558>da <03:51.783> +[03:51.825]v1:<03:51.825>And <03:52.041>used <03:52.241>that <03:52.524>shit <03:52.758>to <03:52.958>throw <03:53.124>it <03:53.292>in <03:53.441>my <03:53.692>face <03:54.121> +[03:53.783]v2:<03:53.783>Man, <03:54.119>hol' <03:54.300>on, <03:54.583>bro <03:55.103> +[03:54.153]v1:<03:54.153>Find <03:54.385>it <03:54.585>funny <03:54.918>you <03:55.201>just <03:55.452>can't <03:55.719>apologize <03:56.731> +[03:56.741]v2:<03:56.741>Bitch, <03:57.049>dawg <03:57.380> +[03:57.024]v1:<03:57.024>Egotistic, <03:57.875>narcissistic, <03:58.707>love <03:59.008>your <03:59.256>own <03:59.624>lies <03:59.954> +[04:00.032]v2:<04:00.032>Bro <04:00.303> +[04:00.313]v1:<04:00.313>See <04:00.548>you <04:00.732>the <04:00.881>reason <04:01.249>why <04:01.548>strong <04:01.814>women <04:02.081>fucked <04:02.398>up! <04:02.753> +[04:02.863]v1:<04:02.863>Why <04:03.066>they <04:03.282>say <04:03.466>it's <04:03.615>a <04:03.815>man's <04:04.164>world, <04:04.506><04:04.615>see <04:04.831>you <04:04.999>the <04:05.164>reason <04:05.450>for <04:05.666>Trump <04:05.994> +[04:05.786]v2:<04:05.786>You <04:06.002>crying, <04:06.292>bro? <04:06.749> +[04:06.031]v1:<04:06.031>You <04:06.316>the <04:06.516>reason, <04:06.922><04:06.999>we <04:07.226>overlooked, <04:07.900><04:08.015>underpaid, <04:08.756><04:08.898>under<04:09.266>booked, <04:09.642><04:09.764>under <04:10.082>shame <04:10.509> +[04:10.632]v1:<04:10.632>If <04:10.784>you <04:10.984>look, <04:11.274><04:11.412>I <04:11.512>don't <04:11.689>speak, <04:12.080><04:12.277>then <04:12.429>I'm <04:12.587>called <04:12.939><04:13.120>out <04:13.261>my <04:13.429>name <04:13.821> +[04:13.953]v1:<04:13.953>I <04:14.171>am <04:14.353>flawed, <04:14.745><04:14.870>I <04:15.054>am <04:15.164>pained, <04:15.590><04:15.689>never <04:15.934>yours, <04:16.462><04:16.522>I <04:16.701>remained <04:17.244> +[04:17.353]v1:<04:17.353>You <04:17.540>the <04:17.665>reason <04:18.071>bitches <04:18.380>start <04:18.755>fuckin' <04:19.048>with <04:19.281>bitches <04:19.665>when <04:19.916>they <04:20.132>change <04:20.495> +[04:20.593]v1:<04:20.593>You <04:20.793>the <04:21.009>reason <04:21.358>bitches <04:21.755>start <04:22.094>callin' <04:22.387>y'all <04:22.694>bitches, <04:23.010>'cause <04:23.273>y'all <04:23.461>useless <04:23.934> +[04:23.977]v1:<04:23.977>You <04:24.259>the <04:24.429>reason <04:24.764>Harvey <04:25.119>Weinstein <04:25.774>had <04:25.974>to <04:26.193>see <04:26.425>his <04:26.570>conclusion <04:27.303> +[04:27.312]v1:<04:27.312>You <04:27.496>the <04:27.731>reason <04:28.096>R. <04:28.363>Kelly <04:28.691>can't <04:28.928>recognize <04:29.505>that <04:29.744>he's <04:29.963>abusive <04:30.650> +[04:30.421]v2:<04:30.421>Man, <04:30.662>shut <04:30.923>the <04:31.139>fuck <04:31.424>up, <04:31.630>we <04:31.872>all <04:32.056>know <04:32.323>you <04:32.507>still <04:32.872>playin' <04:33.291>his <04:33.482>music <04:34.000> +[04:34.014]v2:<04:34.014>Said, <04:34.271>I'm <04:34.414>tired <04:34.716>of <04:34.865>these <04:35.121>emotional <04:35.670>ass, <04:36.121>ungrateful <04:36.622>ass <04:36.921>bitches <04:37.492> +[04:37.554]v2:<04:37.554>Fake <04:37.855>innocent, <04:38.508><04:38.663>fake <04:39.031>feminist, <04:39.783><04:39.817>stop <04:40.151>pretendin' <04:40.761> +[04:40.818]v2:<04:40.818>Y'all <04:41.148>sentiments <04:41.847><04:42.053>ain't <04:42.437>realer <04:42.821>than <04:43.068><04:43.149>what <04:43.349>you <04:43.533>defendin' <04:44.143> +[04:43.566]v1:<04:43.566>Here <04:43.848>he <04:44.064>go, <04:44.766>shut <04:45.082>the <04:45.232>fuck <04:45.476>up, <04:45.812><04:45.816>look <04:46.080>at <04:46.262>you, <04:46.569><04:46.696>look <04:46.944>at <04:47.211>you! <04:47.692> +[04:44.235]v2:<04:44.235>It's <04:44.370>a <04:44.470>split <04:44.676>decision, <04:45.350><04:45.483>broads <04:45.734>like <04:46.017>you <04:46.406><04:46.457>and <04:46.739>real <04:47.022>victims <04:47.585> +[04:47.582]v2:<04:47.582>Let's <04:47.881>talk <04:48.113>the <04:48.223>truth, <04:48.659><04:48.859>women <04:49.236>in <04:49.391>general <04:49.821>just <04:50.137>can't <04:50.470>get <04:50.718>along <04:51.217> +[04:48.574]v1:<04:48.574>Okay, <04:49.107><04:50.898>explain, <04:51.531>**** <04:52.009> +[04:51.641]v2:<04:51.641>Hmm, <04:52.041>when <04:52.241>Tash <04:52.524>got <04:52.692>a <04:52.841>man, <04:53.092><04:53.201>you <04:53.385>didn't <04:53.668>pick <04:53.902>up <04:54.068>the <04:54.217>phone <04:54.513> +[04:54.298]v1:<04:54.298>Explain, <04:54.898>****? <04:55.332> +[04:54.837]v2:<04:54.837>Uh-<04:55.085>huh, <04:55.437>when <04:55.634>Nate <04:55.869>got <04:56.018>a <04:56.170>job, <04:56.392><04:56.519>you <04:56.748>said <04:56.922>you <04:57.133>stayin' <04:57.430>home <04:57.700> +[04:57.710]v1:<04:57.710>Explain <04:58.466> +[04:58.287]v2:<04:58.287>Why <04:58.503>R&B <04:58.956>bitches <04:59.351>don't <04:59.636>feature <05:00.020>on <05:00.236>each <05:00.436>other <05:00.669>songs, <05:01.105>then? <05:01.484> +[05:00.864]v1:<05:00.864>What <05:01.066>the <05:01.298>fuck <05:01.549>is <05:01.749>you <05:01.965>talkin' <05:02.349>'bout? <05:02.649> +[05:02.659]v2:<05:02.659>Nevermind, <05:03.360>bitch, <05:03.627>I'm <05:03.787>walkin' <05:04.160>out <05:04.371> +[05:04.381]v1:<05:04.381>Whatever, <05:04.848>****, <05:05.283>I'm <05:05.499>off <05:05.781>you <05:05.960>now <05:06.413> +[05:05.850]v2:<05:05.850>Yo' <05:06.183>evil <05:06.516>ass <05:06.799>kept <05:07.034>me <05:07.250>well <05:07.482>in <05:07.700>doubt <05:08.060> +[05:07.730]v1:<05:07.730>Pussy <05:08.162>****, <05:08.546>best <05:08.781>watch <05:09.029>your <05:09.191>mouth <05:09.485> +[05:09.495]v2:<05:09.495>Pussy <05:09.879>and <05:10.042>mouth <05:10.378>is <05:10.562>all <05:10.794>you <05:10.978>got <05:11.322> +[05:11.132]v1:<05:11.132>Lay <05:11.449>this <05:11.676>pussy <05:12.065>back <05:12.348>on <05:12.548>the <05:12.732>couch <05:13.521> +[05:12.729]v2:<05:12.729>Doggie <05:13.096>style, <05:13.526>then <05:13.780>you <05:13.980>get <05:14.145>on <05:14.257>top <05:14.617> +[05:14.627]v1:<05:14.627>Fuck <05:14.894>me, <05:15.184>**** <05:15.569> +[05:15.579]v2:<05:15.579>I'ma <05:15.913>fuck <05:16.230>you, <05:16.448>bitch <05:16.832> +[05:16.843]v1:<05:16.843>Nah, <05:17.185>fuck <05:17.500>me, <05:17.832>****, <05:18.115>fuck <05:18.472>me <05:19.228> +[05:18.889]v1:<05:18.889>I'ma <05:19.270>fuck <05:19.604>you, <05:19.870>bitch <05:20.425> +[05:19.906]v1:<05:19.906>Nah, <05:20.239>fuck <05:20.506>you, <05:20.738>fuck <05:21.013>me <05:21.402> +[05:21.412]v2:<05:21.412>You <05:21.713>playin', <05:21.960>man <05:22.367> +[05:22.450]v1:<05:22.450>Fuck <05:22.866>me <05:23.511> +[05:23.980]v1:<05:23.980>Nah, <05:24.313>you <05:24.580>playin' <05:25.057> +[05:38.801]v2:<05:38.801>Stop <05:39.185>tap <05:39.503>dancing <05:39.969>around <05:40.284>the <05:40.432>conversation <05:41.274> \ No newline at end of file diff --git a/test/files/ttml_lrc.xml b/test/files/ttml_lrc.xml new file mode 100644 index 0000000..a936c60 --- /dev/null +++ b/test/files/ttml_lrc.xml @@ -0,0 +1,84 @@ + + + + + + + +