2023-03-25 18:25:55 -04:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Drawing ;
using System.IO ;
using System.Linq ;
2023-05-16 16:59:47 -04:00
using System.Net ;
2023-03-25 18:25:55 -04:00
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading.Tasks ;
using static TJAPlayer3 . CDTX ;
namespace TJAPlayer3
{
public class VTTParser : IDisposable
{
/ *
TO - DO :
2023-05-16 16:59:47 -04:00
- timestamp tag support
2023-03-25 18:25:55 -04:00
* /
2023-05-16 16:59:47 -04:00
[Flags]
private enum ParseMode
{
None = 0 ,
Tag = 1 ,
TagEnd = 2 ,
Rt = 4
}
internal struct LyricData
{
public long timestamp ; // WIP, only first timestamp is accounted for
public string Text ;
public FontStyle Style ;
public Color ForeColor ;
public Color BackColor ;
public bool IsRuby ;
public string RubyText ;
public int line ;
public string Language ;
}
2023-03-25 18:25:55 -04:00
private static string [ ] _vttdelimiter ;
private static Regex regexTimestamp ;
private static Regex regexOffset ;
private static Regex regexLang ;
2023-05-16 16:59:47 -04:00
private static bool isUsingLang ;
2023-03-25 18:25:55 -04:00
private bool _isDisposed ;
public VTTParser ( )
{
_vttdelimiter = new [ ] { "-->" , "- >" , "->" } ;
regexTimestamp = new Regex ( @"(-)?(([0-9]+):)?([0-9]+):([0-9]+)[,\\.]([0-9]+)" ) ;
2023-05-16 16:59:47 -04:00
regexOffset = new Regex ( @"Offset:\s*(.*)\b;?" ) ; // i.e. "WEBVTT Offset: 00:01.001;" , "WEBVTT Offset: 1.001;"
regexLang = new Regex ( @"Language:\s*([A-Za-z]+);?" ) ; // i.e. "WEBVTT Language: ja;"
isUsingLang = false ;
2023-03-25 18:25:55 -04:00
}
#region Dispose stuff
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
protected virtual void Dispose ( bool disposing )
{
if ( ! _isDisposed & & disposing )
{
_vttdelimiter = null ;
regexTimestamp = null ;
regexOffset = null ;
regexLang = null ;
2023-05-16 16:59:47 -04:00
isUsingLang = false ;
2023-03-25 18:25:55 -04:00
_isDisposed = true ;
}
}
#endregion
2023-06-27 07:08:40 -04:00
internal List < STLYRIC > ParseVTTFile ( string filepath , int order , long offset )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
List < STLYRIC > lrclist = new List < STLYRIC > ( ) ;
2023-03-25 18:25:55 -04:00
List < string > lines = File . ReadAllLines ( filepath ) . ToList ( ) ;
2023-05-16 16:59:47 -04:00
#region Header data
2023-03-25 18:25:55 -04:00
if ( lines [ 0 ] . StartsWith ( "WEBVTT" ) )
{
Match languageMatch = regexLang . Match ( lines [ 0 ] ) ;
if ( languageMatch . Success )
{
if ( ! ( languageMatch . Groups [ 1 ] . Value . ToLower ( ) = = CLangManager . fetchLang ( ) ) )
{
2023-05-16 16:59:47 -04:00
Trace . TraceWarning ( "Aborting VTT parse at {0}. WebVTT header language does not match user's selected language." , filepath ) ;
2023-03-25 18:25:55 -04:00
return lrclist ;
}
}
Match offsetMatch = regexOffset . Match ( lines [ 0 ] ) ;
2023-05-16 16:59:47 -04:00
if ( offsetMatch . Success & & regexTimestamp . Match ( offsetMatch . Groups [ 1 ] . Value ) . Success )
2023-03-25 18:25:55 -04:00
{
2023-06-27 07:08:40 -04:00
offset + = ParseTimestamp ( offsetMatch . Groups [ 1 ] . Value ) ;
2023-03-25 18:25:55 -04:00
}
2023-05-16 16:59:47 -04:00
else if ( offsetMatch . Success & & float . TryParse ( offsetMatch . Groups [ 1 ] . Value , out float result ) )
{
2023-06-27 07:08:40 -04:00
offset + = ( long ) ( result * 1000 ) ;
2023-05-16 16:59:47 -04:00
}
2023-03-25 18:25:55 -04:00
}
else
{
2023-05-16 16:59:47 -04:00
Trace . TraceWarning ( "Aborting VTT parse at {0}. WebVTT header does not start with \"WEBVTT\"." , filepath ) ;
2023-03-25 18:25:55 -04:00
return lrclist ;
}
2023-05-16 16:59:47 -04:00
#endregion
2023-03-25 18:25:55 -04:00
long startTime = - 1 ;
long endTime = - 1 ;
2023-05-16 16:59:47 -04:00
bool ignoreLyrics = false ;
2023-06-27 07:08:40 -04:00
bool langIsMatch = false ;
2023-05-16 16:59:47 -04:00
List < LyricData > lyricData = new List < LyricData > ( ) ;
LyricData data = new LyricData ( )
{
ForeColor = TJAPlayer3 . Skin . Game_Lyric_ForeColor ,
BackColor = TJAPlayer3 . Skin . Game_Lyric_BackColor
} ;
2023-03-25 18:25:55 -04:00
for ( int i = 1 ; i < lines . Count ; i + + ) // Skip header line (line 0)
{
long start ;
long end ;
2023-05-16 16:59:47 -04:00
// Check for blank (process data if true), or else check for NOTE, or else check for Timestamps, or else parse text
if ( String . IsNullOrEmpty ( lines [ i ] ) )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
if ( ! ignoreLyrics & & lyricData . Count ! = 0 )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
lyricData . RemoveAll ( empty = > String . IsNullOrEmpty ( empty . Text ) ) ;
lrclist . Add ( CreateLyric ( lyricData , order ) ) ;
2023-03-25 18:25:55 -04:00
}
2023-05-16 16:59:47 -04:00
lyricData = new List < LyricData > ( ) ;
data = new LyricData ( )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
timestamp = startTime ,
ForeColor = TJAPlayer3 . Skin . Game_Lyric_ForeColor ,
BackColor = TJAPlayer3 . Skin . Game_Lyric_BackColor
} ;
ignoreLyrics = false ;
2023-06-27 07:08:40 -04:00
isUsingLang = false ;
langIsMatch = false ;
2023-05-16 16:59:47 -04:00
continue ;
}
if ( lines [ i ] . StartsWith ( "NOTE " ) ) { ignoreLyrics = true ; }
else if ( ! ignoreLyrics & & TryParseTimestamp ( lines [ i ] , out start , out end ) )
{
if ( start > endTime & & endTime ! = - 1 )
{
lrclist . Add ( CreateLyric ( new List < LyricData > ( ) { new LyricData ( ) { timestamp = endTime + offset } } , order ) ) ;
// If new start timestamp is greater than old end timestamp,
// it is assumed there is a gap in-between displaying lyrics.
2023-03-25 18:25:55 -04:00
}
startTime = start ;
endTime = end ;
2023-05-16 16:59:47 -04:00
data . timestamp = start + offset ;
2023-03-25 18:25:55 -04:00
}
2023-05-16 16:59:47 -04:00
else if ( ! ignoreLyrics ) // If all else fails, let's assume it's a lyric. ¯\_(ツ)_/¯
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
int j = 0 ;
var parseMode = ParseMode . None ;
string tagdata = String . Empty ;
data . Text = String . Empty ;
for ( j = 0 ; j < lines [ i ] . Length ; j + + )
{
switch ( lines [ i ] . Substring ( j , 1 ) )
{
#region HTML Tags
case "<" :
parseMode | = ParseMode . Tag ;
break ;
case "/" :
if ( parseMode . HasFlag ( ParseMode . Tag ) ) { parseMode | = ParseMode . TagEnd ; }
else { goto default ; }
break ;
case ">" :
if ( parseMode . HasFlag ( ParseMode . TagEnd ) )
{
if ( tagdata = = ( "c" ) )
{
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_ForeColor ;
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_BackColor ;
}
else if ( tagdata . StartsWith ( "lang" ) ) { data . Language = String . Empty ; }
else if ( tagdata = = "ruby" )
{
lyricData . Add ( data ) ;
data . IsRuby = false ;
data . RubyText = String . Empty ;
data . Text = String . Empty ;
}
else if ( tagdata = = "rt" ) { parseMode & = ~ ParseMode . Rt ; }
else if ( tagdata = = "b" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style & = ~ FontStyle . Bold ; }
else if ( tagdata = = "i" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style & = ~ FontStyle . Italic ; }
else if ( tagdata = = "u" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style & = ~ FontStyle . Underline ; }
else if ( tagdata = = "s" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style & = ~ FontStyle . Strikeout ; }
}
else if ( parseMode . HasFlag ( ParseMode . Tag ) )
{
if ( tagdata . StartsWith ( "c." ) )
{
lyricData . Add ( data ) ;
data . Text = String . Empty ;
string [ ] colordata = tagdata . Split ( '.' ) ;
foreach ( string clr in colordata )
{
switch ( clr )
{
case "white" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 0 ] ;
break ;
case "lime" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 1 ] ;
break ;
case "cyan" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 2 ] ;
break ;
case "red" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 3 ] ;
break ;
case "yellow" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 4 ] ;
break ;
case "magenta" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 5 ] ;
break ;
case "blue" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 6 ] ;
break ;
case "black" :
data . ForeColor = TJAPlayer3 . Skin . Game_Lyric_VTTForeColor [ 7 ] ;
break ;
case "bg_white" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 0 ] ;
break ;
case "bg_lime" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 1 ] ;
break ;
case "bg_cyan" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 2 ] ;
break ;
case "bg_red" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 3 ] ;
break ;
case "bg_yellow" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 4 ] ;
break ;
case "bg_magenta" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 5 ] ;
break ;
case "bg_blue" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 6 ] ;
break ;
case "bg_black" :
data . BackColor = TJAPlayer3 . Skin . Game_Lyric_VTTBackColor [ 7 ] ;
break ;
default :
break ;
}
}
}
else if ( tagdata . StartsWith ( "lang" ) )
{
string [ ] langdata = tagdata . Split ( ' ' ) ;
foreach ( string lng in langdata )
{
2023-06-27 07:08:40 -04:00
if ( lng ! = "lang" )
{
data . Language = lng ;
isUsingLang = true ;
if ( data . Language = = CLangManager . fetchLang ( ) ) { data . line = 0 ; lyricData . Clear ( ) ; langIsMatch = true ; } // Wipe current lyric data if matching lang is found
else { langIsMatch = false ; }
}
2023-05-16 16:59:47 -04:00
}
}
else if ( tagdata = = "ruby" )
{
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . IsRuby = true ;
}
else if ( tagdata = = "rt" ) { parseMode | = ParseMode . Rt ; }
else if ( tagdata = = "b" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style | = FontStyle . Bold ; }
else if ( tagdata = = "i" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style | = FontStyle . Italic ; }
else if ( tagdata = = "u" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style | = FontStyle . Underline ; }
else if ( tagdata = = "s" ) {
lyricData . Add ( data ) ;
data . Text = String . Empty ;
data . Style | = FontStyle . Strikeout ; }
}
else { goto default ; }
parseMode & = ~ ParseMode . Tag & ~ ParseMode . TagEnd ;
tagdata = String . Empty ;
break ;
#endregion
default :
if ( parseMode . HasFlag ( ParseMode . Tag ) ) { tagdata + = lines [ i ] . Substring ( j , 1 ) ; }
2023-06-27 07:08:40 -04:00
else if ( ! isUsingLang | | ( isUsingLang & & langIsMatch ) )
2023-05-16 16:59:47 -04:00
{
if ( parseMode . HasFlag ( ParseMode . Rt ) ) { data . RubyText + = lines [ i ] . Substring ( j , 1 ) ; }
else { data . Text + = lines [ i ] . Substring ( j , 1 ) ; }
}
break ;
}
}
data . Text = WebUtility . HtmlDecode ( data . Text ) ;
data . RubyText = WebUtility . HtmlDecode ( data . RubyText ) ;
2023-06-27 07:08:40 -04:00
2023-05-16 16:59:47 -04:00
lyricData . Add ( data ) ;
2023-06-27 07:08:40 -04:00
2023-05-16 16:59:47 -04:00
data . line + + ;
2023-03-25 18:25:55 -04:00
}
}
2023-05-16 16:59:47 -04:00
if ( lyricData . Count > 0 ) { lrclist . Add ( CreateLyric ( lyricData , order ) ) ; }
lrclist . Add ( CreateLyric ( new List < LyricData > ( ) { new LyricData ( ) { timestamp = endTime + offset } } , order ) ) ;
2023-03-25 18:25:55 -04:00
return lrclist ;
}
internal bool TryParseTimestamp ( string input , out long startTime , out long endTime )
{
var split = input . Split ( _vttdelimiter , StringSplitOptions . None ) ;
if ( split . Length = = 2 & & regexTimestamp . IsMatch ( split [ 0 ] ) & & regexTimestamp . IsMatch ( split [ 1 ] ) )
{
startTime = ParseTimestamp ( split [ 0 ] ) ;
endTime = ParseTimestamp ( split [ 1 ] ) ;
return true ;
}
else
{
startTime = - 1 ;
endTime = - 1 ;
return false ;
}
}
internal long ParseTimestamp ( string input )
{
int hours ;
int minutes ;
int seconds ;
int milliseconds = - 1 ;
Match match = regexTimestamp . Match ( input ) ;
if ( match . Success )
{
hours = ! string . IsNullOrEmpty ( match . Groups [ 3 ] . Value ) ? int . Parse ( match . Groups [ 3 ] . Value ) : 0 ; // Hours are sometimes not included in timestamps.
minutes = int . Parse ( match . Groups [ 4 ] . Value ) ;
seconds = int . Parse ( match . Groups [ 5 ] . Value ) ;
milliseconds = int . Parse ( match . Groups [ 6 ] . Value ) ;
TimeSpan result = new TimeSpan ( 0 , hours , minutes , seconds , milliseconds ) ;
return ( long ) result . TotalMilliseconds * ( match . Groups [ 1 ] . Value = = "-" ? - 1 : 1 ) ;
}
return - 1 ;
}
2023-05-16 16:59:47 -04:00
internal STLYRIC CreateLyric ( List < LyricData > datalist , int order )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
long timestamp = datalist [ 0 ] . timestamp ; // Function will change later w/ timestamp tag implementation
List < List < Bitmap > > textures = new List < List < Bitmap > > ( ) ;
List < List < int > > rubywidthoffset = new List < List < int > > ( ) ; // Save for when text is combined, in case ruby text is longer than original text
List < int > rubyheightoffset = new List < int > ( ) ;
int linecount = datalist . Max ( ( data = > data . line ) ) + 1 ;
for ( int i = 0 ; i < linecount ; i + + )
{
textures . Add ( new List < Bitmap > ( ) ) ;
rubywidthoffset . Add ( new List < int > ( ) ) ;
rubyheightoffset . Add ( 0 ) ;
}
string text = String . Empty ;
2023-03-25 18:25:55 -04:00
2023-05-16 16:59:47 -04:00
// HATE. LET ME TELL YOU HOW MUCH I'VE COME TO HATE YOU SINCE I BEGAN TO CODE.
// THERE ARE 387.44 MILLION LINES OF PRINTED CODE IN REGIONED THIN LAYERS THAT FILL THIS PROGRAM.
// IF THE WORD 'HATE' WAS COMMENTED ON EACH LINE OF THOSE HUNDREDS OF MILLIONS OF LINES OF CODE
// IT WOULD NOT EQUAL ONE ONE BILLIONITH OF THE HATE I FEEL FOR VTTParser.cs, CPrivateFont.cs, AND CPrivateFastFont.cs AT THIS MICRO-INSTANT.
// HATE. HATE.
foreach ( LyricData data in datalist )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
FontFamily fontfamily = ! string . IsNullOrEmpty ( TJAPlayer3 . Skin . Game_Lyric_FontName ) ?
new FontFamily ( TJAPlayer3 . Skin . Game_Lyric_FontName ) : new FontFamily ( "MS UI Gothic" ) ; // everytime CPrivateFont is disposed, it also disposes fontfamily, so I gotta reinitialize this everytime :(
using ( CPrivateFastFont fastdraw = new CPrivateFastFont ( fontfamily , TJAPlayer3 . Skin . Game_Lyric_FontSize , data . Style ) )
{
Bitmap textdrawing = fastdraw . DrawPrivateFont ( data . Text , data . ForeColor , data . BackColor ) ; // Draw main text
if ( data . IsRuby ) // hell yeah ruby time
{
using ( CPrivateFastFont rubydraw = new CPrivateFastFont ( fontfamily , TJAPlayer3 . Skin . Game_Lyric_FontSize / 2 , data . Style ) )
{
Bitmap ruby = rubydraw . DrawPrivateFont ( data . RubyText , data . ForeColor , data . BackColor ) ;
Size size = new Size ( textdrawing . Width > ruby . Width ? textdrawing . Width : ruby . Width , textdrawing . Height + ( TJAPlayer3 . Skin . Game_Lyric_VTTRubyOffset + ( ruby . Height / 2 ) ) ) ;
Bitmap fullruby = new Bitmap ( size . Width , size . Height ) ;
using ( Graphics canvas = Graphics . FromImage ( fullruby ) )
{
canvas . SmoothingMode = System . Drawing . Drawing2D . SmoothingMode . HighQuality ;
canvas . DrawImage ( textdrawing , ( fullruby . Width / 2 ) - ( textdrawing . Width / 2 ) , ( fullruby . Height / 2 ) - ( textdrawing . Height / 2 ) ) ;
canvas . DrawImage ( ruby , ( fullruby . Width - ruby . Width ) / 2 , ( fullruby . Height / 2 ) - ( ruby . Height / 2 ) - TJAPlayer3 . Skin . Game_Lyric_VTTRubyOffset ) ;
}
textures [ data . line ] . Add ( fullruby ) ;
rubywidthoffset [ data . line ] . Add ( ( fullruby . Width - textdrawing . Width ) / 2 > 0 ? ( fullruby . Width - textdrawing . Width ) / 2 : 0 ) ;
rubyheightoffset [ data . line ] = ( fullruby . Height - ( fullruby . Height - ruby . Height ) ) / 2 > rubyheightoffset [ data . line ] ? ( fullruby . Height - ( fullruby . Height - ruby . Height ) ) / 2 : rubyheightoffset [ data . line ] ;
}
}
else
{
textures [ data . line ] . Add ( textdrawing ) ;
rubywidthoffset [ data . line ] . Add ( 0 ) ;
}
}
text + = data . IsRuby ? data . Text + "(" + data . RubyText + ")" : data . Text ;
2023-03-25 18:25:55 -04:00
}
2023-05-16 16:59:47 -04:00
int [ ] width = new int [ textures . Count ] ;
int [ ] height = new int [ textures . Count ] ;
int max_width = 0 ;
int max_height = 0 ;
2023-03-25 18:25:55 -04:00
2023-05-16 16:59:47 -04:00
for ( int i = 0 ; i < textures . Count ; i + + )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
for ( int j = 0 ; j < textures [ i ] . Count ; j + + )
{
width [ i ] + = textures [ i ] [ j ] . Width ;
height [ i ] = textures [ i ] [ j ] . Height > height [ i ] ? textures [ i ] [ j ] . Height : height [ i ] ;
}
max_width = width [ i ] > max_width ? width [ i ] : max_width ;
max_height + = height [ i ] ;
}
Bitmap lyrictex = new Bitmap ( max_width > 0 ? max_width : 1 , max_height - rubyheightoffset . Sum ( ) > 0 ? max_height - rubyheightoffset . Sum ( ) : 1 ) ; // Prevent exception with 0x0y Bitmap
if ( textures . Count > 0 )
{
using ( Graphics canvas = Graphics . FromImage ( lyrictex ) )
2023-03-25 18:25:55 -04:00
{
canvas . Clear ( Color . Transparent ) ;
2023-05-16 16:59:47 -04:00
canvas . SmoothingMode = System . Drawing . Drawing2D . SmoothingMode . HighQuality ;
2023-03-25 18:25:55 -04:00
2023-05-16 16:59:47 -04:00
int x = 0 ;
int y = 0 ;
for ( int i = 0 ; i < textures . Count ; i + + )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
int tempwidth = ( 10 * TJAPlayer3 . Skin . Game_Lyric_FontSize / TJAPlayer3 . Skin . Font_Edge_Ratio * 2 ) * ( textures [ i ] . Count - 1 ) ;
x = ( max_width - width [ i ] ) / 2 ;
for ( int j = 0 ; j < textures [ i ] . Count ; j + + )
{
canvas . DrawImage ( textures [ i ] [ j ] , x + tempwidth , y + ( ( height [ i ] - textures [ i ] [ j ] . Height ) / 2 ) + ( rubyheightoffset [ i ] / 2 ) ) ;
tempwidth + = textures [ i ] [ j ] . Width - ( 10 * TJAPlayer3 . Skin . Game_Lyric_FontSize / TJAPlayer3 . Skin . Font_Edge_Ratio * 4 ) + 2 - j ; // i don't know why this works, please don't ask me why this works
// disabled ruby width adjustment by コミ's request, original code below
// textures[i][j].Width - (10 * TJAPlayer3.Skin.Game_Lyric_FontSize / TJAPlayer3.Skin.Font_Edge_Ratio * 4) + 2 - j - rubywidthoffset[i][j];
}
y + = height [ i ] - rubyheightoffset [ i ] ;
2023-03-25 18:25:55 -04:00
}
}
}
2023-05-16 16:59:47 -04:00
STLYRIC st = new STLYRIC ( )
2023-03-25 18:25:55 -04:00
{
2023-05-16 16:59:47 -04:00
index = order ,
Text = text ,
TextTex = lyrictex ,
Time = timestamp
2023-03-25 18:25:55 -04:00
} ;
2023-05-16 16:59:47 -04:00
return st ;
2023-03-25 18:25:55 -04:00
}
}
}