From f878ab62967b7b4fbce9194f9fddc7c2fbff7810 Mon Sep 17 00:00:00 2001 From: Peter Prikryl Date: Tue, 13 Jul 2021 20:53:09 -0700 Subject: [PATCH] Add option to convert timestamp format --- CHANGELOG.md | 4 ++ README.md | 2 + package-lock.json | 94 ++++++++++++++++++------------------- package.json | 9 +++- src/extension.ts | 78 ++++++++++++++++++++++++++++++ src/model/Time.ts | 5 ++ src/model/TimeLine.ts | 6 +++ src/test/model/Time.test.ts | 20 ++++++++ 8 files changed, 168 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b413f8..d39c6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the "subtitles-editor" extension are documented in this f Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [1.1.6] - 2021-07-13 + +- Added option to format timestamp format. + ## [1.1.5] - 2021-05-19 - Fix translation: Skip sequence and empty lines when translating. diff --git a/README.md b/README.md index 38dd3ce..05efda1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ With **Subtitles: Renumber**, you can renumber the frames using the sequence wit The command **Subtitles: Linear Correction** prompts you for two timestamp mappings. The input is in form "original timestamp -> new timestamp". Pick one point from the beginning of the movie and second one from the end of the movie to get the best approximation. +The command **Subtitles: Convert Time Format** converts timestamps in the file into one of the supported formats you can choose from (SRT, VTT, and SBV). Converting changes separator between start and end timestamp, separator for milliseconds, and number of digits used for milliseconds. You can also use this command to normalize the format of the timestamps if the file contains mixed timestamp formats. + Use command **Subtitles: Translate** to translate subtitles to another language using Google translation service. With **Subtitles: Reorder**, you can reorder the frames based on their sequence number. This can be useful if you want to work with translated and original subtitles at the same time. You can first translate the subtitles (which will replace the original ones) and append the original subtitles at the end. Then, you can reorder them so you will have translated and original frames near each other. diff --git a/package-lock.json b/package-lock.json index 3d26c34..2ca228f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "subtitles-editor", - "version": "1.1.5", + "version": "1.1.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -357,9 +357,9 @@ "dev": true }, "azure-devops-node-api": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-10.2.2.tgz", - "integrity": "sha512-4TVv2X7oNStT0vLaEfExmy3J4/CzfuXolEcQl/BRUmvGySqKStTG2O55/hUQ0kM7UJlZBLgniM0SBq4d/WkKow==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", + "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", "dev": true, "requires": { "tunnel": "0.0.6", @@ -498,13 +498,13 @@ } }, "cheerio": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.9.tgz", - "integrity": "sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng==", + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", "dev": true, "requires": { - "cheerio-select": "^1.4.0", - "dom-serializer": "^1.3.1", + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", "domhandler": "^4.2.0", "htmlparser2": "^6.1.0", "parse5": "^6.0.1", @@ -513,24 +513,24 @@ }, "dependencies": { "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true } } }, "cheerio-select": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.4.0.tgz", - "integrity": "sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", "dev": true, "requires": { - "css-select": "^4.1.2", - "css-what": "^5.0.0", + "css-select": "^4.1.3", + "css-what": "^5.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0", - "domutils": "^2.6.0" + "domutils": "^2.7.0" } }, "chokidar": { @@ -669,9 +669,9 @@ } }, "css-select": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.2.tgz", - "integrity": "sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", "dev": true, "requires": { "boolbase": "^1.0.0", @@ -771,9 +771,9 @@ } }, "domutils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.6.0.tgz", - "integrity": "sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", "dev": true, "requires": { "dom-serializer": "^1.0.1", @@ -1883,9 +1883,9 @@ } }, "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", "dev": true }, "once": { @@ -2050,6 +2050,15 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2375,12 +2384,12 @@ "dev": true }, "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, "requires": { - "os-tmpdir": "~1.0.1" + "rimraf": "^3.0.0" } }, "to-regex-range": { @@ -2472,17 +2481,6 @@ "qs": "^6.9.1", "tunnel": "0.0.6", "underscore": "^1.12.1" - }, - "dependencies": { - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - } } }, "typescript": { @@ -2563,14 +2561,14 @@ "dev": true }, "vsce": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.88.0.tgz", - "integrity": "sha512-FS5ou3G+WRnPPr/tWVs8b/jVzeDacgZHy/y7/QQW7maSPFEAmRt2bFGUJtJVEUDLBqtDm/3VGMJ7D31cF2U1tw==", + "version": "1.95.1", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.95.1.tgz", + "integrity": "sha512-2v8g3ZtZkaOTscRjjCAtM3Au6YYWJtg9UNt1iyyWko7ZHejbt5raClcNzQ7/WYVLYhYHc+otHQifV0gCBREgNg==", "dev": true, "requires": { - "azure-devops-node-api": "^10.2.2", + "azure-devops-node-api": "^11.0.1", "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.1", + "cheerio": "^1.0.0-rc.9", "commander": "^6.1.0", "denodeify": "^1.2.1", "glob": "^7.0.6", @@ -2583,7 +2581,7 @@ "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^5.1.0", - "tmp": "0.0.29", + "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^1.1.0", "yauzl": "^2.3.1", diff --git a/package.json b/package.json index a05293c..80aec8e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "icon": "res/icon.png", "displayName": "Subtitles Editor", "description": "Edit subtitles in SRT, WebVTT, and SBV in VS Code.", - "version": "1.1.5", + "version": "1.1.6", "publisher": "pepri", "license": "MIT", "repository": "https://github.com/pepri/subtitles-editor.git", @@ -18,6 +18,7 @@ "onCommand:subtitles.renumber", "onCommand:subtitles.reorder", "onCommand:subtitles.linearCorrection", + "onCommand:subtitles.convertTimeFormat", "onCommand:subtitles.translate" ], "main": "./out/extension", @@ -68,6 +69,10 @@ "command": "subtitles.linearCorrection", "title": "Subtitles: Linear Correction" }, + { + "command": "subtitles.convertTimeFormat", + "title": "Subtitles: Convert Time Format" + }, { "command": "subtitles.translate", "title": "Subtitles: Translate" @@ -98,6 +103,6 @@ "typescript": "^4.2.4", "@types/vscode": "^1.25.0", "vscode-test": "^1.5.2", - "vsce": "^1.88.0" + "vsce": "^1.95.1" } } diff --git a/src/extension.ts b/src/extension.ts index 78e5d66..25eb937 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -244,6 +244,83 @@ async function linearCorrection() { return true; } +interface SubtitleType { + name: string; + extensions: string[]; + timeSeparator: string; + millisSeparator: string; + shortMillis: boolean; +} + +async function convertTimeFormat() { + const textEditor = vscode.window.activeTextEditor; + + if (typeof textEditor === 'undefined') { + return false; + } + + const subtitleTypes: { [name: string]: SubtitleType } = { + ['SubRip Text']: { + name: 'SubRip Text', + extensions: ['srt'], + timeSeparator: ' --> ', + millisSeparator: ',', + shortMillis: false, + }, + ['Web Video Text Tracks']: { + name: 'Web Video Text Tracks', + extensions: ['vtt'], + timeSeparator: ' --> ', + millisSeparator: '.', + shortMillis: false, + }, + ['SubViewer']: { + name: 'SubViewer', + extensions: ['sbv', 'sub'], + timeSeparator: ',', + millisSeparator: '.', + shortMillis: true, + }, + }; + const items = Object.values(subtitleTypes) + .map(({ name, extensions }) => ({ label: name, detail: extensions.join(', ') })); + + const quickPickOpts: vscode.QuickPickOptions = { + placeHolder: 'Type', + matchOnDetail: true + }; + + const value = await vscode.window.showQuickPick(items, quickPickOpts); + + if (typeof value === 'undefined') { + return false; + } + + const subtitleType = subtitleTypes[value.label]; + const workspaceEdit = new vscode.WorkspaceEdit(); + const documentUri = textEditor.document.uri; + const selections = !textEditor.selection.isEmpty + ? textEditor.selections + : [new vscode.Selection(textEditor.document.positionAt(0), textEditor.document.lineAt(textEditor.document.lineCount - 1).range.end)]; + + for (const selection of selections) { + for (let lineIndex = selection.start.line; lineIndex <= selection.end.line; ++lineIndex) { + const line = textEditor.document.lineAt(lineIndex); + if (!line.isEmptyOrWhitespace) { + const timeLine = TimeLine.parse(line.text); + if (timeLine) { + timeLine.convert(subtitleType.timeSeparator, subtitleType.millisSeparator, subtitleType.shortMillis); + workspaceEdit.replace(documentUri, line.range, timeLine.format()); + } + } + } + } + + await vscode.workspace.applyEdit(workspaceEdit); + + return true; +} + async function translate() { const textEditor = vscode.window.activeTextEditor; @@ -359,6 +436,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('subtitles.renumber', renumber)); context.subscriptions.push(vscode.commands.registerCommand('subtitles.reorder', reorder)); context.subscriptions.push(vscode.commands.registerCommand('subtitles.linearCorrection', linearCorrection)); + context.subscriptions.push(vscode.commands.registerCommand('subtitles.convertTimeFormat', convertTimeFormat)); context.subscriptions.push(vscode.commands.registerCommand('subtitles.translate', translate)); } diff --git a/src/model/Time.ts b/src/model/Time.ts index 615ab1c..12e5002 100644 --- a/src/model/Time.ts +++ b/src/model/Time.ts @@ -21,6 +21,11 @@ export class Time { this.value = (this.value - original.startTime.value) * factor + updated.startTime.value; } + convert(separator: string, shortMillis: boolean) { + this.separator = separator; + this.shortMillis = shortMillis; + } + normalize(): void { this.separator = ','; this.shortMillis = false; diff --git a/src/model/TimeLine.ts b/src/model/TimeLine.ts index 477e2cf..3b8c8a4 100644 --- a/src/model/TimeLine.ts +++ b/src/model/TimeLine.ts @@ -35,6 +35,12 @@ export class TimeLine { this.endTime.applyLinearCorrection(original, updated); } + convert(timeSeparator: string, millisSeparator: string, shortMillis: boolean) { + this.startTime.convert(millisSeparator, shortMillis); + this.endTime.convert(millisSeparator, shortMillis); + this.separator = timeSeparator; + } + format() { return `${this.startTime.format()}${this.separator}${this.endTime.format()}${this.extraData}`; } diff --git a/src/test/model/Time.test.ts b/src/test/model/Time.test.ts index 8a89bd7..c4e5f25 100644 --- a/src/test/model/Time.test.ts +++ b/src/test/model/Time.test.ts @@ -22,6 +22,15 @@ function applyLinearCorrection(time: string, originalTimeLine: string, updatedTi return value.format(); } +function convert(timeLine: string, timeSeparator: string, millisSeparator: string, shortMillis: boolean): string { + const value = TimeLine.parse(timeLine); + if (!value) { + return timeLine; + } + value.convert(timeSeparator, millisSeparator, shortMillis); + return value.format(); +} + suite('Time Tests', function() { test('format', function() { assert.strictEqual(format('12:34:56,7891'), '12:34:56,789'); @@ -70,4 +79,15 @@ suite('Time Tests', function() { test('linear correction', function() { assert.strictEqual(applyLinearCorrection('01:00:00,000', '00:00:00,000-->02:00:00,000', '01:00:00,000-->05:00:00,000'), '03:00:00,000'); }); + + test('convert', function() { + assert.strictEqual(convert('01:23:45,678-->23:45:12,345', ' --> ', '.', false), '01:23:45.678 --> 23:45:12.345'); + assert.strictEqual(convert('01:23:45,678-->23:45:12,345', ',', '.', true), '01:23:45.68,23:45:12.35'); + + assert.strictEqual(convert('11:23:45.678-->23:45:12.345', ' --> ', ',', false), '11:23:45,678 --> 23:45:12,345'); + assert.strictEqual(convert('11:23:45.678-->23:45:12.345', ',', '.', true), '11:23:45.68,23:45:12.35'); + + assert.strictEqual(convert('21:23:45.68,23:45:12.35', ' --> ', ',', false), '21:23:45,680 --> 23:45:12,350'); + assert.strictEqual(convert('21:23:45.68,23:45:12.35', ' --> ', '.', false), '21:23:45.680 --> 23:45:12.350'); + }); });