2024-09-28 01:30:49 +02:00
import * as vscode from 'vscode' ;
import * as net from 'net' ;
2024-10-01 02:06:54 +02:00
import { adjustTimestampToEighthNote , bpmToEighthNoteDuration } from './quantize-lrc' ;
2024-10-01 19:10:52 +02:00
import { publishLyrics , PublishRequest } from './upload-lrc' ;
const channel_global = vscode . window . createOutputChannel ( 'LRC' ) ;
2024-09-28 01:30:49 +02:00
export function activate ( context : vscode.ExtensionContext ) {
context . subscriptions . push ( . . . registerCheckLineTimestamp ( context ) ) ;
context . subscriptions . push (
2024-09-29 14:44:21 +02:00
vscode . commands . registerCommand ( 'extension.jumpToLrcPosition' , jumpToLrcPosition ) ,
2024-09-29 15:56:00 +02:00
vscode . commands . registerCommand ( 'extension.shiftLyricsDown' , shiftLyricsDown ) ,
vscode . commands . registerCommand ( 'extension.shiftLyricsUp' , shiftLyricsUp ) ,
2024-10-01 21:37:30 +02:00
vscode . commands . registerCommand ( 'extension.tapBpm' , tapBpm ) ,
2024-10-01 19:10:52 +02:00
vscode . commands . registerCommand ( 'extension.quantizeToEigthNote' , quantizeToEigthNote ) ,
vscode . commands . registerCommand (
'extension.fineTuneTimestampDown100MsAndPlay' ,
fineTuneTimestampAndPlay ( - 100 ) ,
) ,
vscode . commands . registerCommand (
'extension.fineTuneTimestampUp100MsAndPlay' ,
fineTuneTimestampAndPlay ( 100 ) ,
) ,
vscode . commands . registerCommand (
'extension.uploadLyricsToLrclibDotNet' ,
uploadToLrclibDotNet ,
) ,
2024-09-29 14:44:21 +02:00
) ;
}
2024-09-28 01:30:49 +02:00
2024-09-29 15:56:00 +02:00
/ * *
* Jumps to the position in the lyric file corresponding to the current cursor position in the active text editor .
* Sends a command to a socket to seek to the specified position in mpv at the socket path ` ~/tmp/mpv-socket ` .
* @remarks
* This function requires the following dependencies :
* - ` vscode ` module for accessing the active text editor and displaying messages .
* - ` net ` module for creating a socket connection .
* @throws { Error } If there is an error sending the command to the socket .
* /
2024-09-29 14:44:21 +02:00
function jumpToLrcPosition() {
const editor = vscode . window . activeTextEditor ;
2024-09-28 01:30:49 +02:00
2024-09-29 14:44:21 +02:00
if ( ! editor ) {
2024-10-01 21:37:30 +02:00
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
2024-09-29 14:44:21 +02:00
return ;
}
2024-09-28 01:30:49 +02:00
2024-09-29 15:10:01 +02:00
const ext = new Ext ( editor . document ) ;
2024-09-29 14:44:21 +02:00
const position = editor . selection . active ;
2024-09-29 15:56:00 +02:00
const res = ext . getTimestampFromLine ( position . line ) ;
2024-09-28 01:30:49 +02:00
2024-09-29 14:44:21 +02:00
if ( ! res ) {
return ;
}
const { milliseconds , seconds } = res ;
2024-09-28 01:30:49 +02:00
2024-09-29 14:44:21 +02:00
// Prepare JSON command to send to the socket
2024-10-01 19:10:52 +02:00
const seekCommand = {
2024-09-29 14:44:21 +02:00
command : [ 'seek' , seconds , 'absolute' ] ,
} ;
2024-10-01 19:10:52 +02:00
const reloadSubtitlesCommand = {
command : [ 'sub-reload' ] ,
} ;
2024-09-28 01:30:49 +02:00
2024-09-29 14:44:21 +02:00
const socket = new net . Socket ( ) ;
2024-09-28 01:30:49 +02:00
2024-09-29 14:44:21 +02:00
const socketPath = process . env . HOME + '/tmp/mpv-socket' ;
socket . connect ( socketPath , ( ) = > {
2024-10-01 19:10:52 +02:00
socket . write ( JSON . stringify ( seekCommand ) ) ;
socket . write ( '\n' ) ;
socket . write ( JSON . stringify ( reloadSubtitlesCommand ) ) ;
2024-09-29 14:44:21 +02:00
socket . write ( '\n' ) ;
vscode . window . showInformationMessage (
2024-10-01 19:10:52 +02:00
` Sent command to jump to [ ${ formatTimestamp ( milliseconds ) } ] and sync subtitles. ` ,
2024-09-29 14:44:21 +02:00
) ;
socket . end ( ) ;
} ) ;
socket . on ( 'error' , err = > {
vscode . window . showErrorMessage ( ` Failed to send command: ${ err . message } ` ) ;
} ) ;
2024-09-28 01:30:49 +02:00
}
2024-09-29 15:56:00 +02:00
/ * *
* Shifts the lyrics down by one line starting from the current cursor position in the active text editor .
* @remarks
* This function requires the following dependencies :
* - ` vscode ` module for accessing the active text editor and displaying messages .
* /
async function shiftLyricsDown() {
const editor = vscode . window . activeTextEditor ;
if ( ! editor ) {
2024-10-01 21:37:30 +02:00
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
2024-09-29 15:56:00 +02:00
return ;
}
const ext = new Ext ( editor . document ) ;
const getLine = ( line : number ) = > ( {
number : line ,
range : editor.document.lineAt ( line ) ,
} ) ;
// get the document range from the beginning of the current line to the end of the file
const documentRange = new vscode . Range (
getLine ( editor . selection . active . line ) . range . range . start ,
editor . document . lineAt ( editor . document . lineCount - 1 ) . range . end ,
) ;
let newLines : string = '' ;
// iterate through all lines under the current line, save the lyric text from the current line, and replace it with the lyric text from the previous line
let previousLineText = '' ;
for (
// get the current line range
let line = getLine ( editor . selection . active . line ) ;
line . number < editor . document . lineCount - 1 ;
// next line as position from line number
line = getLine ( line . number + 1 )
) {
const timestamp = ext . getTimestampFromLine ( line . number ) ;
if ( timestamp === undefined ) {
newLines += line . range . text + '\n' ;
continue ;
}
newLines += ` [ ${ formatTimestamp ( timestamp . milliseconds ) } ] ` + previousLineText + '\n' ;
previousLineText = timestamp . text ;
}
// replace documentRange with newLines
await editor . edit ( editBuilder = > {
editBuilder . replace ( documentRange , newLines ) ;
} ) ;
}
/ * *
* Shifts the lyrics up by one line starting from the current cursor position in the active text editor .
* @remarks
* This function requires the following dependencies :
* - ` vscode ` module for accessing the active text editor and displaying messages .
* /
async function shiftLyricsUp() {
const editor = vscode . window . activeTextEditor ;
if ( ! editor ) {
2024-10-01 21:37:30 +02:00
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
2024-09-29 15:56:00 +02:00
return ;
}
const ext = new Ext ( editor . document ) ;
const getLine = ( line : number ) = > ( {
number : line ,
range : editor.document.lineAt ( line ) ,
} ) ;
// get the document range from the beginning of the current line to the end of the file
const documentRange = new vscode . Range (
getLine ( editor . selection . active . line ) . range . range . start ,
editor . document . lineAt ( editor . document . lineCount - 1 ) . range . end ,
) ;
let newLines : string = '' ;
// iterate through all lines under the current line, save the lyric text from the current line, and replace it with the lyric text from the next line
for (
// get the current line range
let line = getLine ( editor . selection . active . line ) ;
line . number < editor . document . lineCount - 2 ;
// next line as position from line number
line = getLine ( line . number + 1 )
) {
const nextLineText =
ext . getTimestampFromLine ( line . number + 1 ) ? . text ? ?
ext . document . lineAt ( line . number + 1 ) . text ;
const timestamp = ext . getTimestampFromLine ( line . number ) ;
if ( timestamp === undefined ) {
continue ;
}
newLines += ` [ ${ formatTimestamp ( timestamp . milliseconds ) } ] ` + nextLineText + '\n' ;
}
// replace documentRange with newLines
await editor . edit ( editBuilder = > {
editBuilder . replace ( documentRange , newLines ) ;
} ) ;
}
2024-10-01 21:37:30 +02:00
/ * *
* Tap the BPM of the track and write it to the header of the active text editor .
* @remarks
* This function requires the following dependencies :
* - ` vscode ` module for accessing the active text editor and displaying messages .
* /
async function tapBpm() {
const editor = vscode . window . activeTextEditor ;
if ( ! editor ) {
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
return ;
}
const ext = new Ext ( editor . document ) ;
const startBpm = ext . findBpmHeader ( ) ;
const bpm = await timeInputBpm ( startBpm ) ;
if ( bpm === undefined ) {
return ;
}
await ext . writeHeader ( 'bpm' , bpm . toString ( ) ) ;
}
2024-10-01 02:06:54 +02:00
/** first ask the user for the BPM of the track, then quantize the timestamps in the active text editor to the closest eighth note based on the given BPM */
2024-10-01 19:10:52 +02:00
async function quantizeToEigthNote() {
2024-10-01 02:06:54 +02:00
const editor = vscode . window . activeTextEditor ;
if ( ! editor ) {
2024-10-01 21:37:30 +02:00
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
2024-10-01 02:06:54 +02:00
return ;
}
const ext = new Ext ( editor . document ) ;
2024-10-01 21:37:30 +02:00
const startBpm = ext . findBpmHeader ( ) ;
2024-10-01 03:24:57 +02:00
const bpm = await timeInputBpm ( startBpm ) ;
if ( bpm === undefined ) {
return ;
}
await ext . writeHeader ( 'bpm' , bpm . toString ( ) ) ;
2024-10-01 02:06:54 +02:00
const getLine = ( line : number ) = > ( {
number : line ,
range : editor.document.lineAt ( line ) ,
} ) ;
const documentRange = new vscode . Range (
getLine ( 0 ) . range . range . start ,
editor . document . lineAt ( editor . document . lineCount - 1 ) . range . end ,
) ;
const eighthNoteDuration = bpmToEighthNoteDuration ( bpm ) ;
let newLines : string = '' ;
for (
let line = getLine ( 0 ) ;
line . number < editor . document . lineCount - 1 ;
line = getLine ( line . number + 1 )
) {
const timestamp = ext . getTimestampFromLine ( line . number ) ;
if ( timestamp === undefined ) {
newLines += line . range . text + '\n' ;
continue ;
}
const adjustedMs = adjustTimestampToEighthNote (
timestamp . milliseconds ,
eighthNoteDuration ,
) ;
newLines += ` [ ${ formatTimestamp ( adjustedMs ) } ] ${ timestamp . text } \ n ` ;
}
await editor . edit ( editBuilder = > {
editBuilder . replace ( documentRange , newLines ) ;
} ) ;
}
2024-10-01 19:10:52 +02:00
/** fine tune the timestamp of the current line by the given amount (in milliseconds) and play the track at slightly before the new timestamp */
function fineTuneTimestampAndPlay ( amountMs : number ) {
return async ( ) = > {
const editor = vscode . window . activeTextEditor ;
if ( ! editor ) {
2024-10-01 21:37:30 +02:00
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
2024-10-01 19:10:52 +02:00
return ;
}
const ext = new Ext ( editor . document ) ;
const position = editor . selection . active ;
const res = ext . getTimestampFromLine ( position . line ) ;
if ( ! res ) {
return ;
}
const { milliseconds } = res ;
const newMs = milliseconds + amountMs ;
// adjust the timestamp
const documentRange = editor . document . lineAt ( position . line ) . range ;
await editor . edit ( editBuilder = > {
editBuilder . replace ( documentRange , ` [ ${ formatTimestamp ( newMs ) } ] ${ res . text } ` ) ;
} ) ;
const PLAY_BEFORE_TIMESTAMP_MS = 2000 ;
const seekCommand = {
command : [ 'seek' , ( newMs - PLAY_BEFORE_TIMESTAMP_MS ) / 1000 , 'absolute' ] ,
} ;
const reloadSubtitlesCommand = {
command : [ 'sub-reload' ] ,
} ;
const socket = new net . Socket ( ) ;
const socketPath = process . env . HOME + '/tmp/mpv-socket' ;
socket . connect ( socketPath , ( ) = > {
socket . write ( JSON . stringify ( seekCommand ) ) ;
socket . write ( '\n' ) ;
socket . write ( JSON . stringify ( reloadSubtitlesCommand ) ) ;
socket . write ( '\n' ) ;
vscode . window . showInformationMessage (
` Sent command to jump to [ ${ formatTimestamp ( newMs ) } ] and sync subtitles. ` ,
) ;
socket . end ( ) ;
} ) ;
socket . on ( 'error' , err = > {
vscode . window . showErrorMessage ( ` Failed to send command: ${ err . message } ` ) ;
} ) ;
} ;
}
2024-10-01 03:24:57 +02:00
// convert the given bpm to miliseconds
function bpmToMs ( bpm : number ) {
return Math . floor ( ( 60 / bpm ) * 1000 ) ;
}
2024-10-01 02:06:54 +02:00
// Show input boxes in a loop, and record the time between each input, averaging the last 5 inputs over a sliding window, then calculate the BPM of the average
2024-10-01 03:24:57 +02:00
async function timeInputBpm ( startBpm? : number ) {
const startBpmMs = bpmToMs ( startBpm ? ? 120 ) ;
const timeDifferences : number [ ] = [
startBpmMs ,
startBpmMs ,
startBpmMs ,
startBpmMs ,
startBpmMs ,
] ;
2024-10-01 02:06:54 +02:00
// assign a weight to the time differences, so that the most recent time differences have more weight
const weights = [ 0.1 , 0.1 , 0.2 , 0.3 , 0.3 ] ;
const calculateBPM = ( ) = > {
// use a weighted average here
let avg = 0 ;
for ( let i = 0 ; i < timeDifferences . length ; i ++ ) {
avg += timeDifferences [ i ] * weights [ i ] ;
}
return Math . floor ( 60000 / avg ) ;
} ;
let lastPressTime = Date . now ( ) ;
2024-10-01 19:10:52 +02:00
let firstLoop = true ;
2024-10-01 02:06:54 +02:00
while ( true ) {
const res = await vscode . window . showInputBox ( {
prompt : ` Press enter to record BPM (current BPM: ${ calculateBPM ( ) } ), enter the final BPM once you know, or press esc to finish ` ,
placeHolder : 'BPM' ,
2024-10-01 19:10:52 +02:00
value : startBpm !== undefined && firstLoop ? startBpm . toString ( ) : undefined ,
2024-10-01 02:06:54 +02:00
} ) ;
2024-10-01 19:10:52 +02:00
firstLoop = false ;
2024-10-01 02:06:54 +02:00
if ( res === undefined ) {
return undefined ;
}
if ( res !== '' ) {
const resBpm = parseInt ( res , 10 ) ;
if ( isNaN ( resBpm ) ) {
vscode . window . showErrorMessage ( 'Invalid BPM' ) ;
continue ;
}
return resBpm ;
}
const now = Date . now ( ) ;
const timeDiff = now - lastPressTime ;
// Add the time difference to the array (limit to last 5 key presses)
timeDifferences . shift ( ) ; // Remove the oldest time difference
timeDifferences . push ( timeDiff ) ;
lastPressTime = now ;
}
}
2024-10-01 19:10:52 +02:00
/ * *
* Uploads the lyrics in the active text editor to the LrclibDotNet API .
* @remarks
* This function requires the following dependencies :
* - ` vscode ` module for accessing the active text editor and displaying messages .
* - ` fetch ` module for making HTTP requests .
* @throws { Error } If there is an HTTP error .
* /
async function uploadToLrclibDotNet() {
const editor = vscode . window . activeTextEditor ;
if ( ! editor ) {
2024-10-01 21:37:30 +02:00
vscode . window . showErrorMessage ( 'No active editor found.' ) ;
2024-10-01 19:10:52 +02:00
return ;
}
const ext = new Ext ( editor . document ) ;
const title = ext . findHeader ( 'ti' ) ? . value ;
const artist = ext . findHeader ( 'ar' ) ? . value ;
const album = ext . findHeader ( 'al' ) ? . value ;
const lengthString = ext . findHeader ( 'length' ) ? . value ;
if (
title === undefined ||
artist === undefined ||
album === undefined ||
lengthString === undefined
) {
vscode . window . showErrorMessage (
'Missing required headers: title, artist, album, length' ,
) ;
return ;
}
// parse length as mm:ss
const [ minutes , seconds ] = lengthString ? . split ( ':' ) ? ? [ ] ;
if (
! minutes ||
! seconds ||
isNaN ( parseInt ( minutes , 10 ) ) ||
isNaN ( parseInt ( seconds , 10 ) )
) {
vscode . window . showErrorMessage ( 'Invalid length header, expected format: mm:ss' ) ;
return ;
}
const length = parseInt ( minutes , 10 ) * 60 + parseInt ( seconds , 10 ) ;
const syncedLyrics = ext . getLyricsPart ( ) ;
const plainLyrics = plainLyricsFromLyrics ( syncedLyrics ) ;
// open a html preview with the lyrics saying
//
// Uploading these lyrics to lrclib.net:
// <metadata as table>
// Lyrics:
// ```<lyrics>```
// Plain lyrics:
// ```<plainLyrics>```
//
// Is this ok?
// <button to upload>
const previewTitle = 'Lyric Preview' ;
const metadataTable = `
< table >
< tr >
< th > Title < / th >
< td > $ { title } < / td >
< / tr >
< tr >
< th > Artist < / th >
< td > $ { artist } < / td >
< / tr >
< tr >
< th > Album < / th >
< td > $ { album } < / td >
< / tr >
< tr >
< th > Length < / th >
< td > $ { lengthString } < / td >
< / tr >
< / table >
` ;
const previewContent = `
< p > Uploading these lyrics to lrclib . net : < / p >
$ { metadataTable }
< p > Lyrics : < / p >
< pre > $ { syncedLyrics } < / pre >
< p > Plain lyrics : < / p >
< pre > $ { plainLyrics } < / pre >
< p > Is this ok ? < / p >
< button > Upload < / button >
` ;
const panel = vscode . window . createWebviewPanel (
'lyricPreview' ,
previewTitle ,
vscode . ViewColumn . One ,
{ enableScripts : true } ,
) ;
panel . webview . html = `
< ! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Markdown Preview < / title >
< / head >
< body >
$ { previewContent }
< / body >
< script >
const vscode = acquireVsCodeApi ( ) ;
document . querySelector ( 'button' ) . addEventListener ( 'click' , ( ) = > {
vscode . postMessage ( { command : 'upload' } ) ;
} ) ;
< / script >
< / html >
` ;
let isDisposed = false ;
panel . onDidDispose ( ( ) = > {
isDisposed = true ;
} ) ;
await new Promise ( ( resolve , _reject ) = > {
panel . webview . onDidReceiveMessage ( ( message : { command : string } ) = > {
if ( isDisposed ) {
return ;
}
if ( message . command === 'upload' ) {
panel . dispose ( ) ;
resolve ( true ) ;
}
} ) ;
} ) ;
const toUpload : PublishRequest = {
trackName : title ,
artistName : artist ,
albumName : album ,
duration : length ,
plainLyrics : plainLyrics ,
syncedLyrics : syncedLyrics ,
} ;
// log the data to our extension output buffer
channel_global . appendLine ( 'Uploading lyrics to LrclibDotNet' ) ;
const json = JSON . stringify ( toUpload , null , 2 ) ;
channel_global . appendLine ( json ) ;
const res = await publishLyrics ( toUpload ) ;
if ( res ) {
vscode . window . showInformationMessage ( 'Lyrics successfully uploaded.' ) ;
2024-10-05 14:20:40 +02:00
channel_global . appendLine ( 'Lyrics successfully uploaded.' ) ;
2024-10-01 19:10:52 +02:00
} else {
vscode . window . showErrorMessage ( 'Failed to upload lyrics.' ) ;
2024-10-05 14:20:40 +02:00
channel_global . appendLine ( 'Failed to upload lyrics.' ) ;
2024-10-01 19:10:52 +02:00
}
}
2024-10-01 21:37:30 +02:00
// If the difference to the timestamp on the next line is larger than 10 seconds (for 120 BPM), underline the next line and show a warning message on hover
2024-09-28 01:30:49 +02:00
export function registerCheckLineTimestamp ( _context : vscode.ExtensionContext ) {
const changesToCheck : Set < vscode.TextDocument > = new Set ( ) ;
const everSeen = new Set < vscode.TextDocument > ( ) ;
return [
vscode . workspace . onDidChangeTextDocument ( e = > {
changesToCheck . add ( e . document ) ;
if ( vscode . window . activeTextEditor ? . document === e . document ) {
2024-09-29 15:10:01 +02:00
doEditorChecks ( vscode . window . activeTextEditor . document , everSeen , changesToCheck ) ;
2024-09-28 01:30:49 +02:00
}
} ) ,
vscode . workspace . onDidOpenTextDocument ( e = > {
changesToCheck . add ( e ) ;
everSeen . add ( e ) ;
if ( vscode . window . activeTextEditor ? . document === e ) {
2024-09-29 15:10:01 +02:00
doEditorChecks ( vscode . window . activeTextEditor . document , everSeen , changesToCheck ) ;
2024-09-28 01:30:49 +02:00
}
} ) ,
vscode . window . onDidChangeActiveTextEditor ( editor = > {
if ( editor ) {
2024-09-29 15:10:01 +02:00
doEditorChecks ( editor . document , everSeen , changesToCheck ) ;
2024-09-28 01:30:49 +02:00
}
} ) ,
vscode . window . onDidChangeVisibleTextEditors ( editors = > {
for ( const editor of editors ) {
2024-09-29 15:10:01 +02:00
doEditorChecks ( editor . document , everSeen , changesToCheck ) ;
2024-09-28 01:30:49 +02:00
}
} ) ,
] ;
}
function doEditorChecks (
2024-09-29 15:10:01 +02:00
document : vscode . TextDocument ,
2024-09-28 01:30:49 +02:00
everSeen : Set < vscode.TextDocument > ,
changesToCheck : Set < vscode.TextDocument > ,
) {
2024-09-29 15:10:01 +02:00
const ext = new Ext ( document ) ;
2024-09-28 01:30:49 +02:00
2024-09-29 15:10:01 +02:00
if ( ! everSeen . has ( document ) ) {
changesToCheck . add ( document ) ;
everSeen . add ( document ) ;
2024-09-28 01:30:49 +02:00
}
if ( ! changesToCheck . has ( document ) ) {
return ;
}
changesToCheck . delete ( document ) ;
const from = 0 ;
const to = document . lineCount - 1 ;
for ( let line = from ; line <= to ; line ++ ) {
const warnings : string [ ] = [ ] ;
const timeDiff = timeDifferenceTooLarge ( ext , line ) ;
if ( timeDiff !== undefined ) {
warnings . push ( timeDiff ) ;
}
const nextTimestampSmaller = nextLineTimestampSmallerThanCurrent ( ext , line ) ;
if ( nextTimestampSmaller !== undefined ) {
warnings . push ( nextTimestampSmaller ) ;
}
for ( const warning of warnings ) {
global_manageWarnings . setWarning ( document , line , warning ) ;
}
// unset any warnings if this doesn’ t apply anymore
if ( warnings . length === 0 ) {
global_manageWarnings . setWarning ( document , line ) ;
}
}
}
2024-10-01 21:37:30 +02:00
/ * * W a r n i f t h e d i f f e r e n c e t o t h e t i m e s t a m p o n t h e n e x t l i n e i s l a r g e r t h a n
* * 10 seconds at 120 BPM
* * 5 seconds at 240 BPM
* * 20 seconds at 60 BPM
* * etc
* /
2024-09-28 01:30:49 +02:00
function timeDifferenceTooLarge ( ext : Ext , line : number ) : string | undefined {
2024-10-01 21:37:30 +02:00
const bpm = ext . findBpmHeader ( ) ? ? 120 ;
const maxTimeDifference = 10000 * ( 120 / bpm ) ;
2024-09-28 01:30:49 +02:00
const timeDifference = ext . getTimeDifferenceToNextLineTimestamp (
new vscode . Position ( line , 0 ) ,
) ;
if (
! timeDifference ||
timeDifference . thisLineIsEmpty ||
2024-10-01 21:37:30 +02:00
timeDifference . difference <= maxTimeDifference
2024-09-28 01:30:49 +02:00
) {
return ;
}
2024-10-01 21:37:30 +02:00
return ` Time difference to next line is ${ formatTimestamp (
timeDifference . difference ,
) } , should there be silence here ? At $ { bpm } BPM , we assume anything more than $ { (
maxTimeDifference / 1000
) . toFixed ( 2 ) } seconds is a mistake . ` ;
2024-09-28 01:30:49 +02:00
}
/** Warn if the timestamp on the next line is smaller or equal to the current timestamp */
function nextLineTimestampSmallerThanCurrent ( ext : Ext , line : number ) : string | undefined {
const timeDifference = ext . getTimeDifferenceToNextLineTimestamp (
new vscode . Position ( line , 0 ) ,
) ;
if ( ! timeDifference ) {
return ;
}
if ( timeDifference . difference == 0 ) {
return ` The timestamp to the next line is not increasing ` ;
}
if ( timeDifference . difference < 0 ) {
return ` The timestamp to the next line is decreasing ` ;
}
}
class Ext {
2024-09-29 15:10:01 +02:00
constructor ( public document : vscode . TextDocument ) { }
2024-09-28 01:30:49 +02:00
getTimeDifferenceToNextLineTimestamp ( position : vscode.Position ) {
2024-09-29 15:56:00 +02:00
const thisLineTimestamp = this . getTimestampFromLine ( position . line ) ;
const nextLineTimestamp = this . getTimestampFromLine ( position . line + 1 ) ;
2024-09-28 01:30:49 +02:00
if ( ! thisLineTimestamp || ! nextLineTimestamp ) {
return ;
}
return {
difference : nextLineTimestamp.milliseconds - thisLineTimestamp . milliseconds ,
thisLineIsEmpty : thisLineTimestamp.text.trim ( ) === '' ,
} ;
}
2024-09-29 15:56:00 +02:00
/ * *
* Retrieves the timestamp and text from the line at the given position in the active text editor .
*
* @param position - The position of the line in the editor .
* @returns An object containing the milliseconds , seconds , and text extracted from the line .
* /
getTimestampFromLine ( line : number ) {
const lineText = this . document . lineAt ( line ) . text ;
return this . getTimestampFromLineText ( lineText ) ;
}
2024-09-28 01:30:49 +02:00
2024-09-29 15:56:00 +02:00
getTimestampFromLineText ( lineText : string ) {
2024-09-28 01:30:49 +02:00
// Extract timestamp [mm:ss.ms] from the current line
const match = lineText . match ( /\[(\d+:\d+\.\d+)\](.*)/ ) ;
if ( ! match ) {
return ;
}
2024-10-01 03:54:36 +02:00
const [ , timestamp , text ] = match ;
2024-09-28 01:30:49 +02:00
const milliseconds = parseTimestamp ( timestamp ) ;
const seconds = milliseconds / 1000 ;
return { milliseconds , seconds , text } ;
}
2024-10-01 03:24:57 +02:00
// Find a header line of the format
// [header:value]
// at the beginning of the lrc file (before the first empty line)
findHeader ( headerName : string ) {
for ( let line = 0 ; line < this . document . lineCount ; line ++ ) {
const text = this . document . lineAt ( line ) . text ;
if ( text . trim ( ) === '' ) {
return ;
}
const match = text . match ( /^\[(\w+):(.*)\]$/ ) ;
if ( match && match [ 1 ] === headerName ) {
return { key : match [ 1 ] , value : match [ 2 ] , line : line } ;
}
}
}
2024-10-01 21:37:30 +02:00
/** Find the bpm header and return the bpm as number, if any */
findBpmHeader() {
const startBpmStr = this . findHeader ( 'bpm' ) ? . value ;
let bpm ;
if ( startBpmStr !== undefined ) {
bpm = parseInt ( startBpmStr , 10 ) ;
if ( isNaN ( bpm ) ) {
bpm = undefined ;
}
}
return bpm ;
}
2024-10-01 03:24:57 +02:00
// check if the given line is a header line
isHeaderLine ( line : string ) {
return (
line . trim ( ) !== '' &&
line . match ( /^\[(\w+):(.*)\]$/ ) !== null &&
line . match ( /^\[\d\d:\d\d.\d+\]/ ) === null
) ;
}
// write the given header to the lrc file, if the header already exists, update the value
async writeHeader ( headerName : string , value : string ) {
const header = this . findHeader ( headerName ) ;
const editor = findActiveEditor ( this . document ) ;
if ( ! editor ) {
return ;
}
if ( header ) {
const lineRange = this . document . lineAt ( header . line ) . range ;
await editor . edit ( editBuilder = > {
editBuilder . replace ( lineRange , ` [ ${ headerName } : ${ value } ] ` ) ;
} ) ;
} else {
// insert before the first timestamp line if no header is found, or after the last header if there are multiple headers
let insertLine = 0 ;
let extraNewline = '' ;
for ( let line = 0 ; line < this . document . lineCount ; line ++ ) {
const text = this . document . lineAt ( line ) . text ;
// check if header
if ( this . isHeaderLine ( text ) ) {
insertLine = line + 1 ;
} else if ( text . trim ( ) === '' ) {
insertLine = line ;
break ;
} else {
insertLine = line ;
if ( line == 0 ) {
extraNewline = '\n' ;
}
break ;
}
}
await editor . edit ( editBuilder = > {
editBuilder . insert (
new vscode . Position ( insertLine , 0 ) ,
` [ ${ headerName } : ${ value } ] \ n ${ extraNewline } ` ,
) ;
} ) ;
}
}
2024-10-01 19:10:52 +02:00
// get the lyrics part of the lrc file (after the headers)
getLyricsPart() {
const first = this . document . lineAt ( 0 ) . text ;
let line = 0 ;
if ( this . isHeaderLine ( first ) ) {
// skip all headers (until the first empty line)
for ( ; line < this . document . lineCount ; line ++ ) {
const text = this . document . lineAt ( line ) . text ;
if ( text . trim ( ) === '' ) {
line ++ ;
break ;
}
}
}
// get the range from the current line to the end of the file
const documentRange = new vscode . Range (
new vscode . Position ( line , 0 ) ,
this . document . lineAt ( this . document . lineCount - 1 ) . range . end ,
) ;
return this . document . getText ( documentRange ) ;
}
2024-10-01 03:24:57 +02:00
}
// find an active editor that has the given document opened
function findActiveEditor ( document : vscode . TextDocument ) {
return vscode . window . visibleTextEditors . find ( editor = > editor . document === document ) ;
2024-09-28 01:30:49 +02:00
}
2024-10-01 19:10:52 +02:00
function plainLyricsFromLyrics ( lyrics : string ) {
// remove the timestamp from the beginning of every line
return (
lyrics
. replace ( /\[\d\d:\d\d\.\d\d\]\s?/gm , '' )
// remove empty lines
. replace ( /\n{2,}/g , '\n' )
) ;
}
2024-09-28 01:30:49 +02:00
function parseTimestamp ( timestamp : string ) : number {
// Parse [mm:ss.ms] format into milliseconds
const [ min , sec ] = timestamp . split ( ':' ) ;
const minutes = parseInt ( min , 10 ) ;
const seconds = parseFloat ( sec ) ;
return minutes * 60 * 1000 + seconds * 1000 ;
}
function formatTimestamp ( ms : number ) : string {
// Format milliseconds back into [mm:ss.ms]
const minutes = Math . floor ( ms / 60000 ) ;
ms %= 60000 ;
const seconds = ( ms / 1000 ) . toFixed ( 2 ) ;
2024-10-01 02:06:54 +02:00
return ` ${ String ( minutes ) . padStart ( 2 , '0' ) } : ${ seconds . padStart ( 5 , '0' ) } ` ;
2024-09-28 01:30:49 +02:00
}
class ManageWarnings {
private warnings : Map < number , string > = new Map ( ) ;
private diagnostics : vscode.DiagnosticCollection ;
constructor ( ) {
this . diagnostics = vscode . languages . createDiagnosticCollection ( ) ;
}
/** Set a warning message on a line in a document, if null then unset */
setWarning ( document : vscode . TextDocument , line : number , message? : string ) {
if ( message !== undefined ) {
this . warnings . set ( line , message ) ;
} else {
this . warnings . delete ( line ) ;
}
this . updateDiagnostics ( document ) ;
}
private updateDiagnostics ( document : vscode . TextDocument ) {
const mkWarning = ( line : number , message : string ) = > {
const lineRange = document . lineAt ( line ) . range ;
return new vscode . Diagnostic ( lineRange , message , vscode . DiagnosticSeverity . Warning ) ;
} ;
const diagnostics : vscode.Diagnostic [ ] = [ ] ;
for ( const [ line , message ] of this . warnings ) {
diagnostics . push ( mkWarning ( line , message ) ) ;
}
this . diagnostics . delete ( document . uri ) ;
this . diagnostics . set ( document . uri , diagnostics ) ;
}
}
const global_manageWarnings = new ManageWarnings ( ) ;