2016-02-08 21:11:03 +01:00
using Nikse.SubtitleEdit.Core.Enums ;
using System ;
using System.Collections.Generic ;
using System.Text ;
using System.Text.RegularExpressions ;
namespace Nikse.SubtitleEdit.Core.SubtitleFormats
{
/// <summary>
/// LRC is a format that synchronizes song lyrics with an audio/video file, [mm:ss.xx] where mm is minutes, ss is seconds and xx is hundredths of a second.
2019-08-16 21:48:22 +02:00
///
2017-02-22 22:01:42 +01:00
/// http://wiki.nicksoft.info/specifications:lrc-file
2019-08-16 21:48:22 +02:00
///
2017-02-22 22:01:42 +01:00
/// Tags:
/// [al:''Album where the song is from'']
/// [ar:''Lyrics artist'']
/// [by:''Creator of the LRC file'']
/// [offset:''+/- Overall timestamp adjustment in milliseconds, + shifts time up, - shifts down'']
/// [re:''The player or editor that creates LRC file'']
/// [ti:''Lyrics(song) title'']
/// [ve:''version of program'']
2016-02-08 21:11:03 +01:00
/// </summary>
public class Lrc : SubtitleFormat
{
2016-02-27 00:10:29 +01:00
private static readonly Regex RegexTimeCodes = new Regex ( @"^\[\d+:\d\d\.\d\d\].*$" , RegexOptions . Compiled ) ;
2016-02-08 21:11:03 +01:00
2017-02-22 22:01:42 +01:00
public override string Extension = > ".lrc" ;
2016-02-08 21:11:03 +01:00
2017-02-22 22:01:42 +01:00
public override string Name = > "LRC Lyrics" ;
2016-02-08 21:11:03 +01:00
public override bool IsMine ( List < string > lines , string fileName )
{
var subtitle = new Subtitle ( ) ;
LoadSubtitle ( subtitle , lines , fileName ) ;
if ( subtitle . Paragraphs . Count > 4 )
{
bool allStartWithNumber = true ;
foreach ( Paragraph p in subtitle . Paragraphs )
{
if ( p . Text . Length > 1 & & ! Utilities . IsInteger ( p . Text . Substring ( 0 , 2 ) ) )
{
allStartWithNumber = false ;
break ;
}
}
if ( allStartWithNumber )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
return false ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
if ( subtitle . Paragraphs . Count > _errorCount )
{
if ( new UnknownSubtitle33 ( ) . IsMine ( lines , fileName ) | | new UnknownSubtitle36 ( ) . IsMine ( lines , fileName ) | | new TMPlayer ( ) . IsMine ( lines , fileName ) )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
return false ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
return true ;
}
return false ;
}
public override string ToText ( Subtitle subtitle , string title )
{
var sb = new StringBuilder ( ) ;
2017-02-22 22:01:42 +01:00
if ( ! string . IsNullOrEmpty ( subtitle . Header ) & & ( subtitle . Header . Contains ( "[ar:" ) | | subtitle . Header . Contains ( "[ti:" ) | | subtitle . Header . Contains ( "[by:" ) | | subtitle . Header . Contains ( "[id:" ) ) )
2019-01-19 14:40:37 +01:00
{
2017-02-22 22:01:42 +01:00
sb . AppendLine ( subtitle . Header . Trim ( ) ) ;
2019-01-19 14:40:37 +01:00
}
2017-02-22 22:01:42 +01:00
else if ( ! string . IsNullOrEmpty ( title ) )
2019-01-19 14:40:37 +01:00
{
2017-02-22 22:01:42 +01:00
sb . AppendLine ( "[ti:" + title . Replace ( "[" , string . Empty ) . Replace ( "]" , string . Empty ) + "]" ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
2016-02-10 21:51:36 +01:00
const string timeCodeFormat = "[{0:00}:{1:00}.{2:00}]{3}" ;
2016-02-08 21:11:03 +01:00
for ( int i = 0 ; i < subtitle . Paragraphs . Count ; i + + )
{
Paragraph p = subtitle . Paragraphs [ i ] ;
2016-02-10 21:51:36 +01:00
Paragraph next = subtitle . GetParagraphOrDefault ( i + 1 ) ;
2016-02-08 21:11:03 +01:00
string text = HtmlUtil . RemoveHtmlTags ( p . Text ) ;
2017-02-22 22:01:42 +01:00
text = text . Replace ( Environment . NewLine , " " ) ;
2016-02-10 21:51:36 +01:00
sb . AppendLine ( string . Format ( timeCodeFormat , p . StartTime . Hours * 60 + p . StartTime . Minutes , p . StartTime . Seconds , ( int ) Math . Round ( p . StartTime . Milliseconds / 10.0 ) , text ) ) ;
2016-02-08 21:11:03 +01:00
if ( next = = null | | next . StartTime . TotalMilliseconds - p . EndTime . TotalMilliseconds > 100 )
{
2016-02-10 21:51:36 +01:00
var tc = new TimeCode ( p . EndTime . TotalMilliseconds ) ;
sb . AppendLine ( string . Format ( timeCodeFormat , tc . Hours * 60 + tc . Minutes , tc . Seconds , ( int ) Math . Round ( tc . Milliseconds / 10.0 ) , string . Empty ) ) ;
2016-02-08 21:11:03 +01:00
}
}
return sb . ToString ( ) . Trim ( ) ;
}
public override void LoadSubtitle ( Subtitle subtitle , List < string > lines , string fileName )
{ //[01:05.99]I've been walking in the same way as I do
_errorCount = 0 ;
2017-02-22 22:38:08 +01:00
var offsetInMilliseconds = 0.0d ;
2016-02-08 21:11:03 +01:00
var header = new StringBuilder ( ) ;
2016-02-10 21:51:36 +01:00
char [ ] splitChars = { ':' , '.' } ;
2016-02-08 21:11:03 +01:00
foreach ( string line in lines )
{
2016-02-27 00:10:29 +01:00
if ( line . StartsWith ( '[' ) & & RegexTimeCodes . Match ( line ) . Success )
2016-02-08 21:11:03 +01:00
{
2016-06-07 06:31:03 +02:00
string s = line . Substring ( 1 , 8 ) ;
2016-02-10 21:51:36 +01:00
string [ ] parts = s . Split ( splitChars , StringSplitOptions . RemoveEmptyEntries ) ;
2016-02-08 21:11:03 +01:00
if ( parts . Length = = 3 )
{
try
{
int minutes = int . Parse ( parts [ 0 ] ) ;
int seconds = int . Parse ( parts [ 1 ] ) ;
int milliseconds = int . Parse ( parts [ 2 ] ) * 10 ;
string text = line . Remove ( 0 , 9 ) . Trim ( ) . TrimStart ( ']' ) . Trim ( ) ;
var start = new TimeCode ( 0 , minutes , seconds , milliseconds ) ;
2016-10-29 16:56:39 +02:00
var p = new Paragraph ( start , new TimeCode ( ) , text ) ;
2016-02-08 21:11:03 +01:00
subtitle . Paragraphs . Add ( p ) ;
}
catch
{
_errorCount + + ;
}
}
else
{
_errorCount + + ;
}
}
2016-02-10 21:51:36 +01:00
else if ( line . StartsWith ( "[ar:" , StringComparison . Ordinal ) ) // [ar:Lyrics artist]
2016-02-08 21:11:03 +01:00
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
2017-02-22 22:01:42 +01:00
else if ( line . StartsWith ( "[id:" , StringComparison . Ordinal ) ) // [ar:Lyrics artist]
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2017-02-22 22:01:42 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2017-02-22 22:01:42 +01:00
}
2016-02-10 21:51:36 +01:00
else if ( line . StartsWith ( "[al:" , StringComparison . Ordinal ) ) // [al:Album where the song is from]
2016-02-08 21:11:03 +01:00
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
2016-02-10 21:51:36 +01:00
else if ( line . StartsWith ( "[ti:" , StringComparison . Ordinal ) ) // [ti:Lyrics (song) title]
2016-02-08 21:11:03 +01:00
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
2016-02-10 21:51:36 +01:00
else if ( line . StartsWith ( "[au:" , StringComparison . Ordinal ) ) // [au:Creator of the Songtext]
2016-02-08 21:11:03 +01:00
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
2016-02-10 21:51:36 +01:00
else if ( line . StartsWith ( "[length:" , StringComparison . Ordinal ) ) // [length:How long the song is]
2016-02-08 21:11:03 +01:00
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
2017-02-22 22:01:42 +01:00
else if ( line . StartsWith ( "[offset:" , StringComparison . Ordinal ) ) // [length:How long the song is]
{
2017-12-10 22:18:13 +01:00
var temp = line . Replace ( "[offset:" , string . Empty ) . Replace ( "]" , string . Empty ) . Replace ( "'" , string . Empty ) . RemoveChar ( ' ' ) . TrimEnd ( ) ;
2017-02-22 22:01:42 +01:00
double d ;
if ( double . TryParse ( temp , out d ) )
{
offsetInMilliseconds = d ;
}
}
2016-02-10 21:51:36 +01:00
else if ( line . StartsWith ( "[by:" , StringComparison . Ordinal ) ) // [by:Creator of the LRC file]
2016-02-08 21:11:03 +01:00
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
}
else if ( ! string . IsNullOrWhiteSpace ( line ) )
{
if ( subtitle . Paragraphs . Count < 1 )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
header . AppendLine ( line ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
_errorCount + + ;
}
else if ( subtitle . Paragraphs . Count < 1 )
{
header . AppendLine ( line ) ;
}
}
subtitle . Header = header . ToString ( ) ;
int max = subtitle . Paragraphs . Count ;
for ( int i = 0 ; i < max ; i + + )
{
Paragraph p = subtitle . Paragraphs [ i ] ;
2016-02-27 00:10:29 +01:00
while ( RegexTimeCodes . Match ( p . Text ) . Success )
2016-02-08 21:11:03 +01:00
{
string s = p . Text . Substring ( 1 , 8 ) ;
p . Text = p . Text . Remove ( 0 , 10 ) . Trim ( ) ;
2016-02-10 21:51:36 +01:00
string [ ] parts = s . Split ( splitChars , StringSplitOptions . RemoveEmptyEntries ) ;
2016-02-08 21:11:03 +01:00
try
{
int minutes = int . Parse ( parts [ 0 ] ) ;
int seconds = int . Parse ( parts [ 1 ] ) ;
int milliseconds = int . Parse ( parts [ 2 ] ) * 10 ;
string text = GetTextAfterTimeCodes ( p . Text ) ;
var start = new TimeCode ( 0 , minutes , seconds , milliseconds ) ;
2016-10-29 16:56:39 +02:00
var newParagraph = new Paragraph ( start , new TimeCode ( ) , text ) ;
2016-02-08 21:11:03 +01:00
subtitle . Paragraphs . Add ( newParagraph ) ;
}
catch
{
_errorCount + + ;
}
}
}
subtitle . Sort ( SubtitleSortCriteria . StartTime ) ;
int index = 0 ;
foreach ( Paragraph p in subtitle . Paragraphs )
{
p . Text = Utilities . AutoBreakLine ( p . Text ) ;
Paragraph next = subtitle . GetParagraphOrDefault ( index + 1 ) ;
if ( next ! = null )
{
if ( string . IsNullOrEmpty ( next . Text ) )
{
p . EndTime = new TimeCode ( next . StartTime . TotalMilliseconds ) ;
}
else
{
p . EndTime . TotalMilliseconds = next . StartTime . TotalMilliseconds - Configuration . Settings . General . MinimumMillisecondsBetweenLines ;
}
if ( p . Duration . TotalMilliseconds > Configuration . Settings . General . SubtitleMaximumDisplayMilliseconds )
{
double duration = Configuration . Settings . General . SubtitleMaximumDisplayMilliseconds ;
p . EndTime = new TimeCode ( p . StartTime . TotalMilliseconds + duration ) ;
}
}
else
{
double duration = Utilities . GetOptimalDisplayMilliseconds ( p . Text , 16 ) + 1500 ;
p . EndTime = new TimeCode ( p . StartTime . TotalMilliseconds + duration ) ;
}
index + + ;
}
subtitle . RemoveEmptyLines ( ) ;
subtitle . Renumber ( ) ;
2017-02-22 22:01:42 +01:00
if ( Math . Abs ( offsetInMilliseconds ) > 0.01 )
{
foreach ( var paragraph in subtitle . Paragraphs )
{
paragraph . StartTime . TotalMilliseconds + = offsetInMilliseconds ;
paragraph . EndTime . TotalMilliseconds + = offsetInMilliseconds ;
}
}
2016-02-08 21:11:03 +01:00
}
private static string GetTextAfterTimeCodes ( string s )
{
2016-02-27 00:10:29 +01:00
while ( RegexTimeCodes . IsMatch ( s ) )
2019-01-19 14:40:37 +01:00
{
2016-02-08 21:11:03 +01:00
s = s . Remove ( 0 , 10 ) . Trim ( ) ;
2019-01-19 14:40:37 +01:00
}
2016-02-08 21:11:03 +01:00
return s ;
}
}
}