diff options
author | Profpatsch <mail@profpatsch.de> | 2024-09-27T23·30+0200 |
---|---|---|
committer | Profpatsch <mail@profpatsch.de> | 2024-10-05T13·49+0000 |
commit | 9bec21ea1cb9137d8c5dae6ee2cb78aac1b5601a (patch) | |
tree | ec649db1716bc7b0a7a5d42b8adb4f0c81410659 /users/Profpatsch/lyric/extension | |
parent | 970dcaa04f3c4bda473674f7e7bb2d2d87ab13e8 (diff) |
feat(users/Profpatsch/lyric): add vscode extension & helpers r/8759
* 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>
Diffstat (limited to 'users/Profpatsch/lyric/extension')
-rw-r--r-- | users/Profpatsch/lyric/extension/.gitignore | 5 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/.prettierrc | 8 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/LICENSE | 1 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/eslint.config.mjs | 42 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/package.json | 52 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/src/extension.ts | 236 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/tsconfig.json | 22 |
7 files changed, 366 insertions, 0 deletions
diff --git a/users/Profpatsch/lyric/extension/.gitignore b/users/Profpatsch/lyric/extension/.gitignore new file mode 100644 index 000000000000..d2405ad640c3 --- /dev/null +++ b/users/Profpatsch/lyric/extension/.gitignore @@ -0,0 +1,5 @@ +/node_modules/ +/out/ + +# ignore for now +/package-lock.json diff --git a/users/Profpatsch/lyric/extension/.prettierrc b/users/Profpatsch/lyric/extension/.prettierrc new file mode 100644 index 000000000000..e97f84abaf62 --- /dev/null +++ b/users/Profpatsch/lyric/extension/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 90, + "arrowParens": "avoid" +} diff --git a/users/Profpatsch/lyric/extension/LICENSE b/users/Profpatsch/lyric/extension/LICENSE new file mode 100644 index 000000000000..4292032e6223 --- /dev/null +++ b/users/Profpatsch/lyric/extension/LICENSE @@ -0,0 +1 @@ +same as toplevel diff --git a/users/Profpatsch/lyric/extension/eslint.config.mjs b/users/Profpatsch/lyric/extension/eslint.config.mjs new file mode 100644 index 000000000000..996dd8ca58ee --- /dev/null +++ b/users/Profpatsch/lyric/extension/eslint.config.mjs @@ -0,0 +1,42 @@ +import tseslint from "typescript-eslint"; +import tsplugin from "@typescript-eslint/eslint-plugin"; +import parser from "@typescript-eslint/parser"; + +export default tseslint.config(tseslint.configs.eslintRecommended, { + languageOptions: { + parser: parser, + parserOptions: { + projectService: true, + }, + }, + ignores: ["node_modules/", "eslint.config.mjs"], + plugins: { "@typescript-eslint": tsplugin }, + rules: { + "prettier/prettier": "off", + "prefer-const": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "warn", + "@typescript-eslint/no-duplicate-enum-values": "warn", + "@typescript-eslint/no-empty-object-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-extra-non-null-assertion": "warn", + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "@typescript-eslint/no-require-imports": "warn", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-unnecessary-type-constraint": "warn", + "@typescript-eslint/no-unsafe-declaration-merging": "warn", + "@typescript-eslint/no-unsafe-function-type": "warn", + "@typescript-eslint/strict-boolean-expressions": ["warn"], + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-wrapper-object-types": "warn", + "@typescript-eslint/prefer-as-const": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/triple-slash-reference": "warn", + }, +}); diff --git a/users/Profpatsch/lyric/extension/package.json b/users/Profpatsch/lyric/extension/package.json new file mode 100644 index 000000000000..722da81b4719 --- /dev/null +++ b/users/Profpatsch/lyric/extension/package.json @@ -0,0 +1,52 @@ +{ + "name": "profpatsch-jump-to-lrc-position", + "displayName": "Jump to .lrc Position in mpv", + "description": "Reads a timestamp from the current file and pipes it to a mpv socket", + "version": "0.0.1", + "engines": { + "vscode": "^1.75.0" + }, + "categories": [ + "Other" + ], + "main": "./out/extension.js", + "activationEvents": [ + "onLanguage:lrc" + ], + "contributes": { + "commands": [ + { + "command": "extension.jumpToLrcPosition", + "title": "Jump to .lrc Position", + "category": "LRC" + } + ], + "languages": [ + { + "id": "lrc", + "extensions": [ + ".lrc" + ], + "aliases": [ + "Lyric file" + ] + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc", + "install-extension": "vsce package --allow-missing-repository --out ./jump-to-lrc-position.vsix && code --install-extension ./jump-to-lrc-position.vsix" + }, + "devDependencies": { + "vscode": "^1.1.37", + "@eslint/js": "^9.10.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.5.5", + "@typescript-eslint/parser": "^8.7.0", + "eslint": "^9.10.0", + "globals": "^15.9.0", + "typescript": "^5.6.2", + "typescript-eslint": "^8.6.0" + } +} diff --git a/users/Profpatsch/lyric/extension/src/extension.ts b/users/Profpatsch/lyric/extension/src/extension.ts new file mode 100644 index 000000000000..4f46e0a1ed7c --- /dev/null +++ b/users/Profpatsch/lyric/extension/src/extension.ts @@ -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(); diff --git a/users/Profpatsch/lyric/extension/tsconfig.json b/users/Profpatsch/lyric/extension/tsconfig.json new file mode 100644 index 000000000000..44a5195795da --- /dev/null +++ b/users/Profpatsch/lyric/extension/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "lib": [ + "ES6" + ], + "outDir": "./out", + "rootDir": "./src", + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ] +} |