-
Notifications
You must be signed in to change notification settings - Fork 7
Added subtitles plugin #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
afcbbaa
ccc62f6
9a52ced
22fca8c
2cc68a7
5d49f22
4f89744
b235980
a113ab8
b095094
be698d7
99e2730
fcaa26c
3b77e58
b7b79ec
e751fd8
191d353
a8449ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Heavily based on https://joelhooks.com/jest-and-github-actions | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvamshi-metrological Could you please remove this file from your PR? Regrettably, we can't use Github actions with our Github subscription. This will not change; the repo will most likely move to a Bitbucket environment somewhere in the upcoming months. So basically; we'll never get this to work I'm afraid 😩 |
||
| name: Tests CI | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths-ignore: | ||
| - 'README.md' | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
|
|
||
| - name: Reconfigure git to use HTTPS authentication | ||
| uses: GuillaumeFalourd/SSH-to-HTTPS@v1 | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v1 | ||
| with: | ||
| node-version: '17' | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
|
||
|
|
||
| - name: Install | ||
| run: | | ||
| npm ci | ||
|
|
||
| - name: Run ESLint | ||
| run: npm run lint | ||
|
|
||
| - name: Run Jest tests | ||
| run: npm run test:ci | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
|
||
|
|
||
| - name: Tests ✅ | ||
| if: ${{ success() }} | ||
| run: | | ||
| curl --request POST \ | ||
| --url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \ | ||
| --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ | ||
| --header 'content-type: application/json' \ | ||
| --data '{ | ||
| "context": "tests", | ||
| "state": "success", | ||
| "description": "Tests passed", | ||
| "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | ||
| }' | ||
|
|
||
| - name: Tests 🚨 | ||
| if: ${{ failure() }} | ||
| run: | | ||
| curl --request POST \ | ||
| --url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \ | ||
| --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ | ||
| --header 'content-type: application/json' \ | ||
| --data '{ | ||
| "context": "tests", | ||
| "state": "failure", | ||
| "description": "Tests failed", | ||
| "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | ||
| }' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Subtitles | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
robbertvancaem marked this conversation as resolved.
Outdated
|
||
|
|
||
| subtitle plugin allows you to fetch and parse subtitle file from a URL and you an read text from the parsed file | ||
|
|
||
| ## Usage | ||
|
|
||
| If you want to access Subtitles in your App code directly, import the *Subtitles* plugin from the Lightning SDK: | ||
|
|
||
| ```js | ||
| import { Subtitles } from '@lightningjs/sdk' | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
|
|
||
| ## Available methods | ||
|
|
||
| ### fetchAndParseSubs | ||
|
|
||
| `fetchAndParseSubs` method expects a valid file URL as an argument. | ||
| This method will fetch a file from the URL and parse it to create a list of objects. created subtitles list is stored in the plugin. | ||
| This method returns a promise that resolves to parsed subtitles as a list of objects containing {start, end, payload}. | ||
| ```js | ||
| Subtitles.fetchAndParseSubs(URL) | ||
| ``` | ||
|
|
||
| ### getSubtitleByTimeIndex | ||
| From the stored subtitles you can get subtitles as text, when you pass currentTime(in seconds) as an argument to the method. | ||
|
|
||
| ```js | ||
| Subtitles.getSubtitleByTimeIndex(currentTime) | ||
| ``` | ||
|
|
||
| ### clearCurrentSubtitle | ||
|
|
||
| Parsed subtitles will be stored in the plugin, `clearCurrentSubtitle` clears all the stored subtitles in the plugin. | ||
|
|
||
| ```js | ||
| Subtitles.clearCurrentSubtitle() | ||
| ``` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* eslint-disable no-console */ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvamshi-metrological Can you please remove this file, also because we can't use Github actions 😩 |
||
| class GithubActionsReporter { | ||
| constructor(globalConfig, options) { | ||
| this._globalConfig = globalConfig | ||
| this._options = options | ||
| } | ||
|
|
||
| onRunComplete(contexts, results) { | ||
| results.testResults.forEach(testResultItem => { | ||
| const testFilePath = testResultItem.testFilePath | ||
|
|
||
| testResultItem.testResults.forEach(result => { | ||
| if (result.status !== 'failed') { | ||
| return | ||
| } | ||
|
|
||
| result.failureMessages.forEach(failureMessages => { | ||
| const newLine = '%0A' | ||
| const message = failureMessages.replace(/\n/g, newLine) | ||
| const captureGroup = message.match(/:([0-9]+):([0-9]+)/) | ||
|
|
||
| if (!captureGroup) { | ||
| console.log('Unable to extract line number from call stack') | ||
| return | ||
| } | ||
|
|
||
| const [, line, col] = captureGroup | ||
| console.log(`::error file=${testFilePath},line=${line},col=${col}::${message}`) | ||
| }) | ||
| }) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // eslint-disable-next-line no-undef | ||
| module.exports = GithubActionsReporter | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| /* | ||
| * If not stated otherwise in this file or this component's LICENSE file the | ||
| * following copyright and licenses apply: | ||
| * | ||
| * Copyright 2020 Metrological | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the License); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| export default class SubtitlesParser { | ||
| // @ params url: subtitle file URL | ||
| // @return parsed subtitles as list of objects | ||
| // also stores parsed data | ||
|
robbertvancaem marked this conversation as resolved.
|
||
| constructor() { | ||
| SubtitlesParser.removeSubtitleTextStyles = false | ||
| SubtitlesParser.clearCurrentSubtitle() | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
|
||
| } | ||
| // static _currentSubtitle = null; | ||
| // static _nextSubtitle = null; | ||
| // static _captions = null; | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
|
||
| static fetchAndParseSubs(url, customParser = false, ParseOptions = {}) { | ||
| const _url = new URL(url) | ||
| if (_url.protocol !== 'https:' || _url.protocol !== 'https:' || !_url.hostname) { | ||
| console.log('Invalid URL') | ||
| return Promise.reject(new Error('Invalid URL')) | ||
| } | ||
| if (ParseOptions && 'removeSubtitleTextStyles' in ParseOptions) { | ||
| this.removeSubtitleTextStyles = ParseOptions.removeSubtitleTextStyles | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| fetch(url) | ||
| .then(data => { | ||
| let subtitleData = data.text() | ||
| this.clearCurrentSubtitle() | ||
| if (customParser && typeof customParser === 'function') { | ||
| this._captions = customParser(subtitleData) | ||
| } else { | ||
| this._captions = this.parseSubtitles(subtitleData) | ||
| } | ||
| if (this._captions && this._captions.length) { | ||
| resolve(this._captions) | ||
| } else { | ||
| reject('Failed to parse subtitles: invalid subtitles length') | ||
| } | ||
|
robbertvancaem marked this conversation as resolved.
Outdated
|
||
| }) | ||
| .catch(error => { | ||
| console.log('Fetching file Failed:', error) | ||
| this.clearCurrentSubtitle() | ||
| reject('Fetching file Failed') | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| // clears stored subtitles data | ||
| static clearCurrentSubtitle() { | ||
| this._currentSubtitle = null | ||
| this._nextSubtitle = null | ||
| } | ||
| static set removeSubtitleTextStyles(v) { | ||
| this._subtitleTextStyles = !v | ||
| } | ||
|
|
||
| // @params currentTime: time as seconds | ||
| // @return subtitle as text at passed currentTime | ||
| static getSubtitleByTimeIndex(currentTime) { | ||
| console.log('currentTime:', currentTime) | ||
| console.log('this._nextSubtitle:', this._nextSubtitle) | ||
| console.log('this._currentSubtitle:', this._currentSubtitle) | ||
| if (!currentTime || isNaN(currentTime)) { | ||
| console.log('invalid currentTime') | ||
| return | ||
| } | ||
| let self = this | ||
| if ( | ||
| this._captions && | ||
| this._captions.length && | ||
| this._currentSubtitle && | ||
| this._nextSubtitle && | ||
| Number(currentTime.toFixed(0)) < Number(this._nextSubtitle.end.toFixed(0)) && | ||
| Number(currentTime.toFixed(0)) >= Number(this._currentSubtitle.start.toFixed(0)) | ||
| ) { | ||
| if ( | ||
| Number(currentTime.toFixed(0)) >= Number(this._currentSubtitle.start.toFixed(0)) && | ||
| Number(currentTime.toFixed(0)) < Number(this._currentSubtitle.end.toFixed(0)) | ||
| ) { | ||
| return this._currentSubtitle.payload | ||
| } else if ( | ||
| Number(currentTime.toFixed(0)) >= Number(this._nextSubtitle.start.toFixed(0)) && | ||
| Number(currentTime.toFixed(0)) < Number(this._nextSubtitle.end.toFixed(0)) | ||
| ) { | ||
| return this._nextSubtitle.payload | ||
| } else { | ||
| return '' | ||
| } | ||
| } else { | ||
| updateSubtitles() | ||
| } | ||
|
|
||
| function updateSubtitles() { | ||
| // updates current and next subtitle text values | ||
| if (self._captions && self._captions.length) { | ||
| if ( | ||
| Number(currentTime.toFixed(0)) <= | ||
| Number(self._captions[self._captions.length - 1].start.toFixed(0)) | ||
| ) { | ||
| if (Number(currentTime.toFixed(0)) < Number(self._captions[0].end.toFixed(0))) { | ||
| if (self._captions[1] && self._captions[1].payload) { | ||
| self._nextSubtitle = self._captions[1] | ||
| } | ||
| self._currentSubtitle = self._captions[0] | ||
| } else { | ||
| for (let i = 0; i < self._captions.length; i++) { | ||
| if (Number(self._captions[i].start.toFixed(0)) >= Number(currentTime.toFixed(0))) { | ||
| self._captions[i + 1] && self._captions[i + 1].payload | ||
| ? (self._nextSubtitle = self._captions[i + 1]) | ||
| : { payload: '' } | ||
| self._currentSubtitle = self._captions[i] | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // parses subtitle file and returns list of time, text objects | ||
| static parseSubtitles(plainSub) { | ||
| let linesArray = plainSub | ||
| .trim() | ||
| .replace('\r\n', '\n') | ||
| .split(/[\r\n]/) | ||
| .map(line => { | ||
| return line.trim() | ||
| }) | ||
| let cues = [] | ||
| let start = null | ||
| let end = null | ||
| let payload = '' | ||
| let lines = linesArray.filter(item => item !== '' && isNaN(item)) | ||
| // linesArray = [] | ||
| for (let i = 0; i < lines.length; i++) { | ||
| if (lines[i].indexOf('-->') >= 0) { | ||
| let splitted = lines[i].split(/[ \t]+-->[ \t]+/) | ||
|
|
||
| start = SubtitlesParser.parseTimeStamp(splitted[0]) | ||
| end = SubtitlesParser.parseTimeStamp(splitted[1]) | ||
| } else if (lines[i] !== '') { | ||
| if (start && end) { | ||
| if (i + 1 < lines.length && lines[i + 1].indexOf('-->') >= 0) { | ||
| let subPayload = payload ? payload + ' ' + lines[i] : lines[i] | ||
| let cue = { | ||
| start, | ||
| end, | ||
| payload: subPayload | ||
| ? this._subtitleTextStyles | ||
| ? subPayload | ||
| : subPayload.replace(/<(.*?)>/g, '') | ||
| : '', // Remove <v- >, etc tags in subtitle text | ||
| } | ||
| cues.push(cue) | ||
| start = null | ||
| end = null | ||
| payload = '' | ||
| subPayload = null | ||
| } else { | ||
| payload = payload ? payload + ' ' + lines[i] : lines[i] | ||
| } | ||
| } | ||
| } else if (start && end) { | ||
| if (payload == null) { | ||
| payload = lines[i] | ||
| } else { | ||
| payload += ' ' + lines[i] | ||
| } | ||
| } | ||
| } | ||
| if (start && end) { | ||
| // let match = /<(.*?)>/g | ||
| // if (payload) { | ||
| // payload.replace(match, '') | ||
| // } | ||
| let cue = { | ||
| start, | ||
| end, | ||
| payload: payload | ||
| ? this._subtitleTextStyles | ||
| ? payload | ||
| : payload.replace(/<(.*?)>/g, '') // Remove <v- >, etc tags in subtitle text | ||
| : '', | ||
| } | ||
| cues.push(cue) | ||
| } | ||
| return cues | ||
| } | ||
|
|
||
| // parses timestamp in subtitle file into seconds | ||
| static parseTimeStamp(s) { | ||
| const match = s.match(/^(?:([0-9]+):)?([0-5][0-9]):([0-5][0-9](?:[.,][0-9]{0,3})?)/) | ||
|
|
||
| const hours = parseInt(match[1], 10) || '0' | ||
| const minutes = parseInt(match[2], 10) | ||
| const seconds = parseFloat(match[3].replace(',', '.')) | ||
| return seconds + 60 * minutes + 60 * 60 * hours | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.