feat(users/Profpatsch/lyric): add vscode extension & helpers
* tap-bpm: simple CLI program that accepts key inputs and averages a BPM value * lyric-timing-mpv-script: If you press Ctrl+l, mpv attaches the current timestamp to a .lrc file named after the song. This is for manually timing missing songs for uploading them to https://lrclib.net/ * extension: vscode extension for `.lrc` files, currently with the following features: 1. A “jump to LRC position” command which reads an .lrc timestamp from the current line and expects mpv to listen on `~/tmp/mpv-socket` (via `--input-ipc-server`), and will seek to the exact timestamp (down to the ms) in the currently playing song. 2. Some initial linting warnings - A lint that warns if the difference to the next timestamp is more than 10s (which usually means there’s an instrumental and the previous line is stuck) - A lint that checks that timestamps are monotonically increasing Change-Id: I32a4ac0e2c5bbe3d94e45ffcf647f81bc7c08aa0 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12537 Tested-by: BuildkiteCI Reviewed-by: Profpatsch <mail@profpatsch.de>
This commit is contained in:
parent
970dcaa04f
commit
9bec21ea1c
17 changed files with 643 additions and 0 deletions
236
users/Profpatsch/lyric/extension/src/extension.ts
Normal file
236
users/Profpatsch/lyric/extension/src/extension.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as net from 'net';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(...registerCheckLineTimestamp(context));
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('extension.jumpToLrcPosition', () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!editor) {
|
||||
vscode.window.showInformationMessage('No active editor found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = new Ext(editor);
|
||||
const position = editor.selection.active;
|
||||
const res = ext.getTimestampFromLine(position);
|
||||
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
const { milliseconds, seconds } = res;
|
||||
|
||||
// Prepare JSON command to send to the socket
|
||||
const jsonCommand = {
|
||||
command: ['seek', seconds, 'absolute'],
|
||||
};
|
||||
|
||||
const socket = new net.Socket();
|
||||
|
||||
const socketPath = process.env.HOME + '/tmp/mpv-socket';
|
||||
socket.connect(socketPath, () => {
|
||||
socket.write(JSON.stringify(jsonCommand));
|
||||
socket.write('\n');
|
||||
vscode.window.showInformationMessage(
|
||||
`Sent command to jump to [${formatTimestamp(milliseconds)}].`,
|
||||
);
|
||||
socket.end();
|
||||
});
|
||||
|
||||
socket.on('error', err => {
|
||||
vscode.window.showErrorMessage(`Failed to send command: ${err.message}`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// If the difference to the timestamp on the next line is larger than 10 seconds, underline the next line and show a warning message on hover
|
||||
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) {
|
||||
doEditorChecks(vscode.window.activeTextEditor, everSeen, changesToCheck);
|
||||
}
|
||||
}),
|
||||
vscode.workspace.onDidOpenTextDocument(e => {
|
||||
changesToCheck.add(e);
|
||||
everSeen.add(e);
|
||||
if (vscode.window.activeTextEditor?.document === e) {
|
||||
doEditorChecks(vscode.window.activeTextEditor, everSeen, changesToCheck);
|
||||
}
|
||||
}),
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor) {
|
||||
doEditorChecks(editor, everSeen, changesToCheck);
|
||||
}
|
||||
}),
|
||||
vscode.window.onDidChangeVisibleTextEditors(editors => {
|
||||
for (const editor of editors) {
|
||||
doEditorChecks(editor, everSeen, changesToCheck);
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function doEditorChecks(
|
||||
editor: vscode.TextEditor,
|
||||
everSeen: Set<vscode.TextDocument>,
|
||||
changesToCheck: Set<vscode.TextDocument>,
|
||||
) {
|
||||
const ext = new Ext(editor);
|
||||
const document = editor.document;
|
||||
|
||||
if (!everSeen.has(editor.document)) {
|
||||
changesToCheck.add(editor.document);
|
||||
everSeen.add(editor.document);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Warn if the difference to the timestamp on the next line is larger than 10 seconds */
|
||||
function timeDifferenceTooLarge(ext: Ext, line: number): string | undefined {
|
||||
const timeDifference = ext.getTimeDifferenceToNextLineTimestamp(
|
||||
new vscode.Position(line, 0),
|
||||
);
|
||||
if (
|
||||
!timeDifference ||
|
||||
timeDifference.thisLineIsEmpty ||
|
||||
timeDifference.difference <= 10000
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return `Time difference to next line is ${formatTimestamp(timeDifference.difference)}`;
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
constructor(public editor: vscode.TextEditor) {}
|
||||
|
||||
getTimeDifferenceToNextLineTimestamp(position: vscode.Position) {
|
||||
const thisLineTimestamp = this.getTimestampFromLine(position);
|
||||
const nextLineTimestamp = this.getTimestampFromLine(
|
||||
position.with({ line: position.line + 1 }),
|
||||
);
|
||||
if (!thisLineTimestamp || !nextLineTimestamp) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
difference: nextLineTimestamp.milliseconds - thisLineTimestamp.milliseconds,
|
||||
thisLineIsEmpty: thisLineTimestamp.text.trim() === '',
|
||||
};
|
||||
}
|
||||
|
||||
getTimestampFromLine(position: vscode.Position) {
|
||||
const document = this.editor.document;
|
||||
const lineText = document.lineAt(position.line).text;
|
||||
|
||||
// Extract timestamp [mm:ss.ms] from the current line
|
||||
const match = lineText.match(/\[(\d+:\d+\.\d+)\](.*)/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const [, timestamp, text] = match!;
|
||||
const milliseconds = parseTimestamp(timestamp);
|
||||
const seconds = milliseconds / 1000;
|
||||
return { milliseconds, seconds, text };
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return `${String(minutes).padStart(2, '0')}:${seconds}`;
|
||||
}
|
||||
|
||||
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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue