diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c78ccf8..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,184 +0,0 @@ -defaults: &defaults - docker: - - image: circleci/node:16 - working_directory: ~/project - -version: 2 -jobs: - noop: - docker: - - image: alpine:3.11.3 - steps: - - run: exit 0 - -workflows: - version: 2 - build: - jobs: - - noop - -# jobs: -# Checkout Code: -# <<: *defaults -# steps: -# - checkout -# - attach_workspace: -# at: ~/project -# - restore_cache: -# keys: -# - yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} -# - yarn-cache-{{ .Branch }} -# - yarn-cache- -# - run: yarn install -# - run: yarn build -# - save_cache: -# key: yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} -# paths: -# - node_modules -# - persist_to_workspace: -# root: . -# paths: -# - . -# Check for vulnerabilities: -# <<: *defaults -# steps: -# - attach_workspace: -# at: ~/project -# - run: yarn install -# - run: yarn validate:dependencies -# Build (tag): -# <<: *defaults -# steps: -# - setup_remote_docker: -# # docker_layer_caching: true -# version: 18.06.0-ce -# - attach_workspace: -# at: ~/project -# - run: -# name: Build Docker Image -# command: docker build -f Dockerfile . -t superflytv/sofie-spreadsheet-gateway:$CIRCLE_TAG -# - run: -# name: Publish Docker Image to Docker Hub -# command: | -# echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin -# docker push superflytv/sofie-spreadsheet-gateway:$CIRCLE_BRANCH -# Build (branch): -# <<: *defaults -# steps: -# - setup_remote_docker: -# # docker_layer_caching: true -# version: 18.06.0-ce -# - attach_workspace: -# at: ~/project -# - run: -# name: Build Docker Image -# command: docker build -f Dockerfile . -t superflytv/sofie-spreadsheet-gateway:$CIRCLE_BRANCH -# - run: -# name: Publish Docker Image to Docker Hub -# command: | -# echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin -# docker push superflytv/sofie-spreadsheet-gateway:$CIRCLE_BRANCH -# Test: -# <<: *defaults -# steps: -# - attach_workspace: -# at: ~/project -# - run: yarn install -# - run: yarn test -# - run: yarn build -# Send Coverage: -# <<: *defaults -# steps: -# - attach_workspace: -# at: ~/project -# - run: yarn install -# - run: yarn send-coverage -# - store_artifacts: -# path: ./coverage/clover.xml -# prefix: tests -# - store_artifacts: -# path: coverage -# prefix: coverage -# - store_test_results: -# path: ./coverage/clover.xml -# Git Release: -# <<: *defaults -# steps: -# - attach_workspace: -# at: ~/project -# - add_ssh_keys: -# fingerprints: -# - "c3:ca:91:e7:34:5a:e6:21:79:13:59:dd:fa:a1:ea:0c" -# - run: yarn install -# - run: mkdir -p ~/.ssh -# - run: -# name: Keyscan Github -# command: ssh-keyscan -H github.com >> ~/.ssh/known_hosts -# - run: git config --global user.email "info@superfly.tv" -# - run: git config --global user.name "superflytvab" -# - run: yarn release -# - run: git push --follow-tags origin HEAD -# - persist_to_workspace: -# root: . -# paths: -# - . - -# workflows: -# version: 2 -# Test build and deploy: -# jobs: -# - Checkout Code: -# filters: -# tags: -# only: /.*/ -# branches: -# only: /.*/ -# - Check for vulnerabilities: -# requires: -# - Checkout Code -# filters: -# tags: -# only: /.*/ -# branches: -# only: /.*/ -# - Test: -# requires: -# - Checkout Code -# filters: -# tags: -# only: /.*/ -# branches: -# only: /.*/ -# - Build (tag): -# requires: -# - Check for vulnerabilities -# - Test -# filters: -# tags: -# only: /v.*/ -# branches: -# ignore: /.*/ -# - Build (branch): -# requires: -# - Check for vulnerabilities -# - Test -# filters: -# branches: -# only: -# - master -# - develop -# - Send Coverage: -# requires: -# - Check for vulnerabilities -# - Test -# filters: -# branches: -# only: -# - master -# - Git Release: -# requires: -# - Send Coverage -# filters: -# branches: -# only: -# - master \ No newline at end of file diff --git a/.github/workflows/dependencies.yaml b/.github/workflows/dependencies.yaml deleted file mode 100644 index 9ad10e2..0000000 --- a/.github/workflows/dependencies.yaml +++ /dev/null @@ -1,64 +0,0 @@ -name: Dependencies Check - -on: - push: - branches: - - "**" - tags: - - "v**" - -jobs: - validate-prod-dependencies-packages: - name: Validate Package production dependencies - runs-on: ubuntu-latest - continue-on-error: true - timeout-minutes: 15 - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - name: Prepare Environment - run: | - yarn install - env: - CI: true - - name: Validate production dependencies - run: | - if ! git log --format=oneline -n 1 | grep -q "\[ignore-audit\]"; then - yarn validate:dependencies - else - echo "Skipping audit" - fi - env: - CI: true - - validate-all-dependencies-packages: - name: Validate all Package dependencies - runs-on: ubuntu-latest - continue-on-error: true - timeout-minutes: 15 - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - name: Prepare Environment - run: | - yarn install - env: - CI: true - - name: Validate production dependencies - run: | - yarn validate:dependencies - env: - CI: true - - name: Validate dev dependencies - run: | - yarn validate:dev-dependencies - env: - CI: true diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml deleted file mode 100644 index 6bb0ab1..0000000 --- a/.github/workflows/node.yaml +++ /dev/null @@ -1,97 +0,0 @@ -name: Node CI - -on: - push: - branches: - - "**" - tags: - - "v**" - pull_request: - -jobs: - build-gateways: - # TODO - should this be dependant on tests or something passing if we are on a tag? - name: Build gateways - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v2 - - name: Get the Docker tag - id: docker-tag - uses: yuya-takeyama/docker-tag-from-github-ref-action@2b0614b1338c8f19dd9d3ea433ca9bc0cc7057ba - with: - remove-version-tag-prefix: false - - name: Determine images to publish - id: image-tags - # TODO - image needs changing... - run: | - IMAGES= - DOCKER_TAG=${{ steps.docker-tag.outputs.tag }} - # check if a release branch, or master, or a tag - if [[ $DOCKER_TAG =~ ^release([0-9]+)$ || $DOCKER_TAG == "latest" || "${{ github.ref }}" == refs/tags/* ]] - then - DOCKERHUB_PUBLISH="1" - IMAGES="superflytv/sofie-spreadsheet-gateway:$DOCKER_TAG"$'\n'$IMAGES - # debug output - echo dockerhub-publish $DOCKERHUB_PUBLISH - echo images $IMAGES - echo ::set-output name=images::"$IMAGES" - echo ::set-output name=dockerhub-publish::"$DOCKERHUB_PUBLISH" - else - echo "Skipping docker build" - fi - - name: Build libs - if: ${{ steps.image-tags.outputs.images }} - run: | - yarn install - yarn build - yarn install --prod --ignore-scripts - - name: Set up Docker Buildx - if: ${{ steps.image-tags.outputs.images }} - uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub - if: steps.image-tags.outputs.images && steps.image-tags.outputs.dockerhub-publish == '1' - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - # TODO - do we want this? - # - name: Login to GitHub Container Registry - # uses: docker/login-action@v1 - # with: - # registry: ghcr.io - # username: ${{ github.repository_owner }} - # password: ${{ secrets.CR_PAT }} - - name: Build and push - uses: docker/build-push-action@v2 - if: ${{ steps.image-tags.outputs.images }} - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ steps.image-tags.outputs.images }} - - lint-packages: - name: Lint Package - runs-on: ubuntu-latest - continue-on-error: true - timeout-minutes: 15 - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - name: Prepare Environment - run: | - yarn install - yarn build - env: - CI: true - - name: Run typecheck and linter - run: | - yarn lint - env: - CI: true diff --git a/.gitignore b/.gitignore index a729f23..e18cc04 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ downloads credentials.json token.json .env +coverage/ diff --git a/.node-version b/.node-version index dcf74e2..b6a7d89 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -16.14 \ No newline at end of file +16 diff --git a/README.md b/README.md index 28f9f45..17dec43 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Spreadsheet Gateway - -An application for piping data between [**Sofie Server Core**](https://github.com/nrkno/sofie-core) and Spreadsheets on Gogle Drive. Works with the [`sofie-demo-blueprints`](https://github.com/SuperFlyTV/sofie-demo-blueprints). +An application for piping data between [**Sofie Server Core**](https://github.com/nrkno/sofie-core) and Spreadsheets on Google Drive. This application is a part of the [**Sofie** TV News Studio Automation System](https://github.com/nrkno/Sofie-TV-automation/). ## Usage + ``` // Development: npm run start -host 127.0.0.1 -port 3000 -log "log.log" @@ -15,23 +15,70 @@ npm run start To set up, follow the instructions in your Sofie Core interface (in the settings for the device). +This gateway app will read all spreadsheet files in specified Google Drive folder that don't start with underscore "\_" and that are version compliant with the app. +Read more about the version specification in the [**Spreadsheet Schema**](./SPREADSHEET-SCHEMA.md). + **CLI arguments:** -| Argument | Description | Environment variable | -| ------------- | ------------- | --- | -| -host | Hostname or IP of Core | CORE_HOST | -| -port | Port of Core | CORE_PORT | -| -log | Path to output log | CORE_LOG | +| Argument | Description | Environment variable | +| -------- | ---------------------- | -------------------- | +| -host | Hostname or IP of Core | CORE_HOST | +| -port | Port of Core | CORE_PORT | +| -log | Path to output log | CORE_LOG | ## Installation (for developers) +### Build and start + ``` yarn - yarn build +yarn start +``` + +or + +``` +yarn +yarn buildstart ``` ### Dev dependencies: -* yarn - * https://yarnpkg.com +- yarn + - https://yarnpkg.com + +## Set up Google Drive API + +Start Sofie and Spreadsheet gateway. Make sure that Sofie Studio has Sofie Host URL (Settings > Studios > Sofie Host URL). Connect the Spreadsheet gateway to the Studio. + +1. Go to Google Cloud Platform ([https://console.cloud.google.com/](https://console.cloud.google.com/)) In the upper left corner choose "Select a project" and then "NEW PROJECT" in the upper right corner of the popup. The project name does not matter. + If "Select a project" does not exist on the page, go to the menubar on the left > Home > Dashboard. + +### Set up the OAuth consent screen + +1. Make sure the newly created project is selected. Then go to the menubar on the left > APIs & Services > OAuth consent screen. Select "External" User Type, and enter the App name, support email, and developer email. + +2. Under "Authorized domains", add a redirect URL that should look like this: `SOFIE_CORE_URL/devices/SPREADSHEET_GATEWAY_ID/oauthResponse`. Replace `SOFIE_CORE_URL` with the real URL (probably [http://localhost:3000](http://localhost:3000)) and `SPREADSHEET_GATEWAY_ID` with the real ID (copy from the Studio page). + +3. Skip scopes, on the "Test users" page enter your Google Account email which you will use for the Spreadsheet Gateway. + +### Enable Google Drive API + +1. Go to APIs & Services > Library, search for "Google Drive API" and enable it, do the same fot the "Google Sheets API". + +### Create credentials + +1. Go to APIs & Services > Credentials. Click on "CREATE CREDENTIALS" on the top and choose "OAuth Client ID". Under "Application type" choose "Desktop app". Once created, click on "DOWNLOAD JSON" from the popup. + +Now just upload that JSON file into Sofie. + +## Credentials expiration after 7 days + +Google's access token will be (automatically revoked after 7 days)[https://developers.google.com/identity/protocols/oauth2]. +In case the user's token has expired or has been revoked, Spreadsheet Gateway will send status updates to the Sofie a message like "Invalid Credentials, try resetting user credentials" and set the device status to bad. It's Sofie's responsibility to allow a smooth reset of user credentials in the UI. + +## API Rate Limitations + +Google Sheets API has a limitation of (60 read requests per minute per user per project)[https://developers.google.com/sheets/api/limits]. +Spreadsheet Gateway automatically sets intervals for checking and downloading new spreadsheet documents based on the number of spreadsheets found in the drive folder. Assumption is that, aside from standard regular fetching of documents, there will be maximum of 30 additional document edits. diff --git a/jest.config.js b/jest.config.js index 1fa0dd6..44f23f0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,4 +4,34 @@ module.exports = { tsconfig: 'tsconfig.json', }, }, + moduleFileExtensions: ['js', 'ts'], + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, + testMatch: ['**/__tests__/**/*.spec.(ts|js)'], + testPathIgnorePatterns: ['integrationTests'], + testEnvironment: 'node', + coverageThreshold: { + global: { + branches: 0, + functions: 0, + lines: 0, + statements: 0, + }, + }, + coverageDirectory: './coverage/', + collectCoverage: true, + collectCoverageFrom: [ + '**/src/**', + '!**/src/copy/**', + '!**/__tests__/**', + '!**/__mocks__/**', + '!**/node_modules/**', + '!**/dist/**', + ], + preset: 'ts-jest', + roots: [''], + modulePaths: [''], + moduleDirectories: ['node_modules', 'src'], + setupFilesAfterEnv: ['/src/__tests__/setupTests.ts'], } diff --git a/package.json b/package.json index ea34ccb..12cb992 100644 --- a/package.json +++ b/package.json @@ -1,121 +1,123 @@ { - "name": "sofie-spreadsheet-gateway", - "version": "0.1.3", - "description": "", - "main": "dist/index.js", - "contributors": [ - { - "name": "Johan Nyman", - "email": "johan@superfly.tv", - "url": "http://superfly.tv" - }, - { - "name": "Stephan Nordnes Eriksen", - "url": "https://github.com/stephan-nordnes-eriksen" - } - ], - "author": "SuperFly.tv", - "scripts": { - "info": "npm-scripts-info", - "build": "trash dist && yarn build:main", - "buildstart": "yarn build && yarn start", - "buildinspect": "yarn build && yarn inspect", - "build:main": "tsc -p tsconfig.json", - "unit": "yarn jest", - "test": "yarn lint && yarn unit", - "test:integration": "yarn lint && jest --config=jest-integration.config.js", - "watch": "jest --watch", - "cov": "jest; open-cli coverage/lcov-report/index.html", - "cov-open": "open-cli coverage/lcov-report/index.html", - "send-coverage": "jest && codecov", - "docs": "yarn docs:html && open-cli docs/index.html", - "docs:html": "typedoc src/index.ts --excludePrivate --mode file --theme minimal --out docs", - "docs:json": "typedoc --mode file --json docs/typedoc.json src/index.ts", - "docs:publish": "yarn docs:html && gh-pages -d docs", - "changelog": "standard-version", - "inspect": "node --inspect dist/index.js", - "release": "yarn reset && yarn test && yarn changelog", - "reset": "git clean -dfx && git reset --hard && yarn", - "ci": "yarn test", - "validate:dependencies": "yarn audit --groups dependencies && yarn license-validate", - "validate:dev-dependencies": "yarn audit --groups devDependencies", - "start": "node dist/index.js", - "unlinkall": "yarn unlink @sofie-automation/server-core-integration && yarn --check-files", - "prepare": "husky install", - "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist", - "lint": "yarn lint:raw .", - "lint-fix": "yarn lint --fix", - "license-validate": "yarn sofie-licensecheck" - }, - "scripts-info": { - "info": "Display information about the scripts", - "build": "(Trash and re)build the library", - "lint": "Lint all typescript source files", - "unit": "Build the library and run unit tests", - "test": "Lint, build, and test the library", - "watch": "Watch source files, rebuild library on changes, rerun relevant tests", - "cov": "Run tests, generate the HTML coverage report, and open it in a browser", - "docs": "Generate HTML API documentation and open it in a browser", - "docs:publish": "Generate HTML API documentation and push it to GitHub Pages", - "docs:json": "Generate API documentation in typedoc JSON format", - "changelog": "Bump package.json version, update CHANGELOG.md, tag a release", - "reset": "Delete all untracked files and reset the repo to the last commit", - "release": "Clean, build, test, publish docs, and prepare release (a one-step publish process)", - "ci": "Test script for running by the CI (CircleCI)", - "validate:dependencies": "Scan dependencies for vulnerabilities and check licenses" - }, - "license": "MIT", - "dependencies": { - "@sofie-automation/blueprints-integration": "1.41.0-in-testing.0", - "@sofie-automation/server-core-integration": "1.41.0-in-testing.0", - "clone": "^2.1.2", - "dotenv": "^16.0.0", - "googleapis": "^100.0.0", - "lodash": "^4.17.21", - "marked": "^4.0.15", - "request": "^2.88.2", - "request-promise": "^4.2.6", - "underscore": "^1.13.3", - "uuid": "^8.3.2", - "winston": "^3.7.2" - }, - "devDependencies": { - "@sofie-automation/code-standard-preset": "^2.0.1", - "@types/clone": "^2.1.1", - "@types/jest": "^27.5.0", - "@types/node": "^17.0.31", - "@types/request-promise": "^4.1.48", - "@types/underscore": "^1.11.4", - "@types/uuid": "^8.3.4", - "codecov": "^3.8.3", - "gh-pages": "^3.2.3", - "jest": "^28.0.3", - "jest-haste-map": "^28.0.2", - "jest-resolve": "^28.0.3", - "mkdirp": "^1.0.4", - "npm-scripts-info": "^0.3.9", - "open-cli": "^7.0.1", - "standard-version": "^9.3.2", - "trash-cli": "^5.0.0", - "ts-jest": "^28.0.0", - "ts-lib": "^0.0.5", - "typedoc": "^0.22.15", - "typescript": "^4.6.4" - }, - "engines" : { - "node" : ">=16.0.0" - }, - "standard-version": { - "message": "chore(release): %s", - "tagPrefix": "v" - }, - "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{css,json,md,scss}": [ - "prettier --write" - ], - "*.{ts,tsx,js,jsx}": [ - "yarn lint:raw --fix" - ] - } + "name": "sofie-spreadsheet-gateway", + "version": "0.1.3", + "description": "", + "main": "dist/index.js", + "contributors": [ + { + "name": "Johan Nyman", + "email": "johan@superfly.tv", + "url": "http://superfly.tv" + }, + { + "name": "Stephan Nordnes Eriksen", + "url": "https://github.com/stephan-nordnes-eriksen" + } + ], + "author": "SuperFly.tv", + "scripts": { + "start": "node dist/index.js", + "build": "trash dist && yarn build:main", + "buildstart": "yarn build && yarn start", + "buildinspect": "yarn build && yarn inspect", + "build:main": "tsc -p tsconfig.build.json", + "unit": "yarn jest", + "test": "yarn lint && yarn unit", + "test:integration": "yarn lint && jest --config=jest-integration.config.js", + "watch": "jest --watch", + "cov": "jest; open-cli coverage/lcov-report/index.html", + "cov-open": "open-cli coverage/lcov-report/index.html", + "send-coverage": "jest && codecov", + "docs": "yarn docs:html && open-cli docs/index.html", + "docs:html": "typedoc src/index.ts --excludePrivate --mode file --theme minimal --out docs", + "docs:json": "typedoc --mode file --json docs/typedoc.json src/index.ts", + "docs:publish": "yarn docs:html && gh-pages -d docs", + "changelog": "standard-version", + "inspect": "node --inspect dist/index.js", + "release": "yarn reset && yarn test && yarn changelog", + "reset": "git clean -dfx && git reset --hard && yarn", + "ci": "yarn test", + "validate:dependencies": "yarn audit --groups dependencies && yarn license-validate", + "validate:dev-dependencies": "yarn audit --groups devDependencies", + "unlinkall": "yarn unlink @sofie-automation/server-core-integration && yarn --check-files", + "prepare": "husky install", + "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist", + "lint": "yarn lint:raw .", + "lint-fix": "yarn lint --fix", + "license-validate": "yarn sofie-licensecheck", + "info": "npm-scripts-info" + }, + "scripts-info": { + "info": "Display information about the scripts", + "build": "(Trash and re)build the library", + "lint": "Lint all typescript source files", + "unit": "Build the library and run unit tests", + "test": "Lint, build, and test the library", + "watch": "Watch source files, rebuild library on changes, rerun relevant tests", + "cov": "Run tests, generate the HTML coverage report, and open it in a browser", + "docs": "Generate HTML API documentation and open it in a browser", + "docs:publish": "Generate HTML API documentation and push it to GitHub Pages", + "docs:json": "Generate API documentation in typedoc JSON format", + "changelog": "Bump package.json version, update CHANGELOG.md, tag a release", + "reset": "Delete all untracked files and reset the repo to the last commit", + "release": "Clean, build, test, publish docs, and prepare release (a one-step publish process)", + "ci": "Test script for running by the CI (CircleCI)", + "validate:dependencies": "Scan dependencies for vulnerabilities and check licenses" + }, + "license": "MIT", + "dependencies": { + "@sofie-automation/blueprints-integration": "1.46.0-in-testing.0", + "@sofie-automation/server-core-integration": "1.46.0-in-testing.0", + "@sofie-automation/shared-lib": "1.46.0-in-testing.0", + "clone": "^2.1.2", + "dotenv": "^16.0.0", + "googleapis": "^100.0.0", + "lodash": "^4.17.21", + "marked": "^4.0.15", + "request": "^2.88.2", + "request-promise": "^4.2.6", + "underscore": "^1.13.3", + "uuid": "^8.3.2", + "winston": "^3.7.2" + }, + "devDependencies": { + "@sofie-automation/code-standard-preset": "^2.0.1", + "@types/clone": "^2.1.1", + "@types/jest": "^27.5.0", + "@types/lodash": "^4.14.184", + "@types/node": "^17.0.31", + "@types/request-promise": "^4.1.48", + "@types/underscore": "^1.11.4", + "@types/uuid": "^8.3.4", + "codecov": "^3.8.3", + "gh-pages": "^3.2.3", + "jest": "^28.0.3", + "jest-haste-map": "^28.0.2", + "jest-resolve": "^28.0.3", + "mkdirp": "^1.0.4", + "npm-scripts-info": "^0.3.9", + "open-cli": "^7.0.1", + "standard-version": "^9.3.2", + "trash-cli": "^5.0.0", + "ts-jest": "^28.0.0", + "ts-lib": "^0.0.5", + "typedoc": "^0.22.15", + "typescript": "^4.6.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "standard-version": { + "message": "chore(release): %s", + "tagPrefix": "v" + }, + "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", + "lint-staged": { + "*.{css,json,md,scss}": [ + "prettier --write" + ], + "*.{ts,tsx,js,jsx}": [ + "yarn lint:raw --fix" + ] + } } diff --git a/src/__tests__/diffRundowns.spec.ts b/src/__tests__/diffRundowns.spec.ts new file mode 100644 index 0000000..0fe219e --- /dev/null +++ b/src/__tests__/diffRundowns.spec.ts @@ -0,0 +1,351 @@ +import { SheetPart } from '../classes/Part' +import { SheetSegment } from '../classes/Segment' +import { SheetRundown } from '../classes/Rundown' +import { diffRundowns, RundownChangeType } from '../diffRundowns' + +function createEmptySegment(rundownId: string, externalId: string, name: string, rank: number): SheetSegment { + return new SheetSegment(rundownId, externalId, rank, name, false, []) +} + +function createSegmentWithParts( + rundownId: string, + externalId: string, + name: string, + rank: number, + parts: SheetPart[] +): SheetSegment { + return new SheetSegment(rundownId, externalId, rank, name, false, parts) +} + +function createEmptyPart(segmentId: string, externalId: string, name: string, rank: number): SheetPart { + return new SheetPart('test', segmentId, externalId, rank, name, false, '', []) +} + +describe('Diff Rundowns', () => { + it('Does nothing if passed null rundowns', () => { + expect(diffRundowns(null, null)).toEqual([]) + }) + + it('Identifies created Rundowns', () => { + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, []) + expect(diffRundowns(null, newRundown)).toEqual([ + { + type: RundownChangeType.RundownCreate, + rundownId: 'test-rundown', + }, + ]) + }) + + it('Identifies deleted Rundowns', () => { + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, []) + expect(diffRundowns(oldRundown, null)).toEqual([ + { + type: RundownChangeType.RundownDelete, + rundownId: 'test-rundown', + }, + ]) + }) + + it('Identifies changed Rundowns', () => { + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, []) + const newRundown = new SheetRundown('test-rundown', 'Test Rundown Changed', 'v0.0', 0, 0, []) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.RundownUpdate, + rundownId: 'test-rundown', + }, + ]) + }) + + it('Identifies created Segments', () => { + const oldSegments = [createEmptySegment('test-rundown', 'test-segment-3', 'Test Segment 3', 2)] + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, oldSegments) + const newSegments = [ + createEmptySegment('test-rundown', 'test-segment-1', 'Test Segment 1', 0), + createEmptySegment('test-rundown', 'test-segment-2', 'Test Segment 2', 1), + createEmptySegment('test-rundown', 'test-segment-3', 'Test Segment 3', 2), + ] + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, newSegments) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.SegmentCreate, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + }, + { + type: RundownChangeType.SegmentCreate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + }, + ]) + }) + + it('Identifies deleted Segments', () => { + const oldSegments = [ + createEmptySegment('test-rundown', 'test-segment-1', 'Test Segment 1', 0), + createEmptySegment('test-rundown', 'test-segment-2', 'Test Segment 2', 1), + createEmptySegment('test-rundown', 'test-segment-3', 'Test Segment 3', 2), + ] + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, oldSegments) + const newSegments = [createEmptySegment('test-rundown', 'test-segment-2', 'Test Segment 2', 1)] + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, newSegments) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.SegmentDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + }, + { + type: RundownChangeType.SegmentDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-3', + }, + ]) + }) + + // When a rundown_update event is sent, Segments will be re-evaluated anyway + it('Prioritises created Rundown events over created Segment events', () => { + const newSegments = [ + createEmptySegment('test-rundown', 'test-segment-1', 'Test Segment 1', 0), + createEmptySegment('test-rundown', 'test-segment-2', 'Test Segment 2', 1), + createEmptySegment('test-rundown', 'test-segment-3', 'Test Segment 3', 2), + ] + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, newSegments) + expect(diffRundowns(null, newRundown)).toEqual([ + { + type: RundownChangeType.RundownCreate, + rundownId: 'test-rundown', + }, + ]) + }) + + it('Identifies updated Segments', () => { + const oldSegments = [ + createEmptySegment('test-rundown', 'test-segment-1', 'Test Segment 1', 0), + createEmptySegment('test-rundown', 'test-segment-2', 'Test Segment 2', 1), + createEmptySegment('test-rundown', 'test-segment-3', 'Test Segment 3', 2), + ] + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, oldSegments) + const newSegments = [ + createEmptySegment('test-rundown', 'test-segment-1', 'Test Segment 1', 0), + createEmptySegment('test-rundown', 'test-segment-3', 'Test Segment 3', 1), + createEmptySegment('test-rundown', 'test-segment-2', 'Test Segment 2', 2), + ] + const newRundowns = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, newSegments) + expect(diffRundowns(oldRundown, newRundowns)).toEqual([ + { + type: RundownChangeType.SegmentUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-3', + }, + { + type: RundownChangeType.SegmentUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + }, + ]) + }) + + it('Identifies created Parts', () => { + const oldPartsSegment1 = [createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0)] + const oldSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, oldPartsSegment1) + const oldPartsSegment2 = [createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 1)] + const oldSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, oldPartsSegment2) + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [oldSegment1, oldSegment2]) + const newPartsSegment1 = [ + createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0), + createEmptyPart('test-segment-1', 'test-part-2', 'Test Part 2', 1), + createEmptyPart('test-segment-1', 'test-part-3', 'Test Part 3', 2), + ] + const newSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, newPartsSegment1) + const newPartsSegment2 = [ + createEmptyPart('test-segment-2', 'test-part-4', 'Test Part 4', 0), + createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 1), + createEmptyPart('test-segment-2', 'test-part-6', 'Test Part 6', 2), + ] + const newSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, newPartsSegment2) + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [newSegment1, newSegment2]) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.PartCreate, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + partId: 'test-part-2', + }, + { + type: RundownChangeType.PartCreate, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + partId: 'test-part-3', + }, + { + type: RundownChangeType.PartCreate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-4', + }, + { + type: RundownChangeType.PartCreate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-6', + }, + ]) + }) + + it('Ientifies deleted Parts', () => { + const oldPartsSegment1 = [ + createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0), + createEmptyPart('test-segment-1', 'test-part-2', 'Test Part 2', 1), + createEmptyPart('test-segment-1', 'test-part-3', 'Test Part 3', 2), + ] + const oldSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, oldPartsSegment1) + const oldPartsSegment2 = [ + createEmptyPart('test-segment-2', 'test-part-4', 'Test Part 4', 0), + createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 1), + createEmptyPart('test-segment-2', 'test-part-6', 'Test Part 6', 2), + ] + const oldSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, oldPartsSegment2) + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [oldSegment1, oldSegment2]) + const newPartsSegment1 = [createEmptyPart('test-segment-1', 'test-part-2', 'Test Part 2', 1)] + const newSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, newPartsSegment1) + const newPartsSegment2: SheetPart[] = [] + const newSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, newPartsSegment2) + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [newSegment1, newSegment2]) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.PartDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + partId: 'test-part-1', + }, + { + type: RundownChangeType.PartDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + partId: 'test-part-3', + }, + { + type: RundownChangeType.PartDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-4', + }, + { + type: RundownChangeType.PartDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-5', + }, + { + type: RundownChangeType.PartDelete, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-6', + }, + ]) + }) + + it('Identifies updated Parts', () => { + const oldPartsSegment1 = [ + createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0), + createEmptyPart('test-segment-1', 'test-part-2', 'Test Part 2', 1), + createEmptyPart('test-segment-1', 'test-part-3', 'Test Part 3', 2), + ] + const oldSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, oldPartsSegment1) + const oldPartsSegment2 = [ + createEmptyPart('test-segment-2', 'test-part-4', 'Test Part 4', 0), + createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 1), + createEmptyPart('test-segment-2', 'test-part-6', 'Test Part 6', 2), + ] + const oldSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, oldPartsSegment2) + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [oldSegment1, oldSegment2]) + const newPartsSegment1 = [ + createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0), + createEmptyPart('test-segment-1', 'test-part-3', 'Test Part 3', 2), + createEmptyPart('test-segment-1', 'test-part-2', 'Changed Test Part 2', 4), + ] + const newSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, newPartsSegment1) + const newPartsSegment2 = [ + createEmptyPart('test-segment-2', 'test-part-4', 'Test Part 4 Changed', 0), + createEmptyPart('test-segment-2', 'test-part-6', 'Test Part 6', 1), + createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 2), + ] + const newSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, newPartsSegment2) + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [newSegment1, newSegment2]) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.PartUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + partId: 'test-part-2', + }, + { + type: RundownChangeType.PartUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-4', + }, + { + type: RundownChangeType.PartUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-6', + }, + { + type: RundownChangeType.PartUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + partId: 'test-part-5', + }, + ]) + }) + + // When a segment_update event is sent, Parts will be re-evaluated anyway + it('Prioritises Segment updates over Part updates', () => { + const oldPartsSegment1 = [ + createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0), + createEmptyPart('test-segment-1', 'test-part-2', 'Test Part 2', 1), + createEmptyPart('test-segment-1', 'test-part-3', 'Test Part 3', 2), + ] + const oldSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 0, oldPartsSegment1) + const oldPartsSegment2 = [ + createEmptyPart('test-segment-2', 'test-part-4', 'Test Part 4', 0), + createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 1), + createEmptyPart('test-segment-2', 'test-part-6', 'Test Part 6', 2), + ] + const oldSegment2 = createSegmentWithParts('test-rundown', 'test-segment-2', 'Test Segment 2', 1, oldPartsSegment2) + const oldRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [oldSegment1, oldSegment2]) + const newPartsSegment1 = [ + createEmptyPart('test-segment-1', 'test-part-1', 'Test Part 1', 0), + createEmptyPart('test-segment-1', 'test-part-3', 'Test Part 3', 2), + createEmptyPart('test-segment-1', 'test-part-2', 'Changed Test Part 2', 4), + ] + const newSegment1 = createSegmentWithParts('test-rundown', 'test-segment-1', 'Test Segment 1', 2, newPartsSegment1) + const newPartsSegment2 = [ + createEmptyPart('test-segment-2', 'test-part-4', 'Test Part 4 Changed', 0), + createEmptyPart('test-segment-2', 'test-part-6', 'Test Part 6', 1), + createEmptyPart('test-segment-2', 'test-part-5', 'Test Part 5', 2), + ] + const newSegment2 = createSegmentWithParts( + 'test-rundown', + 'test-segment-2', + 'Test Segment 2 Changed', + 1, + newPartsSegment2 + ) + const newRundown = new SheetRundown('test-rundown', 'Test Rundown', 'v0.0', 0, 0, [newSegment1, newSegment2]) + expect(diffRundowns(oldRundown, newRundown)).toEqual([ + { + type: RundownChangeType.SegmentUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-1', + }, + { + type: RundownChangeType.SegmentUpdate, + rundownId: 'test-rundown', + segmentId: 'test-segment-2', + }, + ]) + }) +}) diff --git a/src/__tests__/setupTests.ts b/src/__tests__/setupTests.ts new file mode 100644 index 0000000..2474e9e --- /dev/null +++ b/src/__tests__/setupTests.ts @@ -0,0 +1,3 @@ +import { addTestLogging } from '../logger' + +addTestLogging() diff --git a/src/classes/Rundown.ts b/src/classes/Rundown.ts index 67526fc..10caede 100644 --- a/src/classes/Rundown.ts +++ b/src/classes/Rundown.ts @@ -6,6 +6,8 @@ import { SheetUpdate, SheetsManager } from './SheetManager' import * as _ from 'underscore' import { IOutputLayer } from '@sofie-automation/blueprints-integration' +const SHEET_NAME = 'Rundown' + interface RundownMetaData { version: string startTime: number @@ -477,7 +479,7 @@ export class SheetRundown implements Rundown { rundown.addSegments(results.segments) if (sheetManager && results.sheetUpdates && results.sheetUpdates.length > 0) { - sheetManager.updateSheetWithSheetUpdates(sheetId, 'Rundown', results.sheetUpdates).catch(console.error) + sheetManager.updateSheetWithSheetUpdates(sheetId, SHEET_NAME, results.sheetUpdates).catch(console.error) } return rundown } diff --git a/src/classes/RunningOrderWatcher.ts b/src/classes/RunningOrderWatcher.ts index 1c7d875..39ba002 100644 --- a/src/classes/RunningOrderWatcher.ts +++ b/src/classes/RunningOrderWatcher.ts @@ -1,17 +1,20 @@ +import { IOutputLayer } from '@sofie-automation/blueprints-integration' +import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' +import * as clone from 'clone' +import * as dotenv from 'dotenv' import { EventEmitter } from 'events' +import { Auth, Common, drive_v3, google } from 'googleapis' import * as request from 'request-promise' -import * as dotenv from 'dotenv' -import { SheetRundown } from './Rundown' -import { Auth, Common } from 'googleapis' -import { google, drive_v3 } from 'googleapis' -import { SheetsManager, SheetUpdate } from './SheetManager' import * as _ from 'underscore' -import { SheetSegment } from './Segment' -import { SheetPart } from './Part' -import * as clone from 'clone' import { CoreHandler, WorkflowType } from '../coreHandler' +import { logger } from '../logger' +import { checkErrorType, getErrorMsg } from '../util' import { MediaDict } from './media' -import { IOutputLayer } from '@sofie-automation/blueprints-integration' +import { SheetPart } from './Part' +import { SheetRundown } from './Rundown' +import { SheetSegment } from './Segment' +import { SheetsManager, SheetUpdate } from './SheetManager' + dotenv.config() export class RunningOrderWatcher extends EventEmitter { @@ -43,7 +46,7 @@ export class RunningOrderWatcher extends EventEmitter { ) => this) // Fast = list diffs, Slow = fetch All - public pollIntervalFast: number = 2 * 1000 + public pollIntervalFast: number = 5 * 1000 public pollIntervalSlow: number = 10 * 1000 public pollIntervalMedia: number = 5 * 1000 @@ -60,10 +63,12 @@ export class RunningOrderWatcher extends EventEmitter { private _lastMedia: MediaDict = {} private _lastOutputLayers: IOutputLayer[] = [] private _lastWorkflow: WorkflowType | undefined + // private _lastOutputLayers: Array = [] + /** - * A Running Order watcher which will poll Google Drive for changes and emit events - * whenever a change occurs. + * A Running Order watcher which will poll Google Drive for changes + * and emit events whenever a change occurs. * * @param authClient Google OAuth2Clint containing connection information * @param coreHandler Handler for Sofie Core @@ -91,41 +96,342 @@ export class RunningOrderWatcher extends EventEmitter { /** * Add a Running Order from Google Sheets ID - * - * @param runningOrderId Id of Running Order Sheet on Google Sheets + * @param spreadsheetId Id of Spreadsheet on Google Sheets + * @param asNew If this spreadsheet should be considered as new one */ - async checkRunningOrderById(runningOrderId: string, asNew?: boolean): Promise { - const runningOrder = await this.sheetManager.downloadRunningOrder( - runningOrderId, - this.coreHandler.GetOutputLayers() - ) - - if (runningOrder.gatewayVersion === this.gatewayVersion) { - this.processUpdatedRunningOrder(runningOrder.externalId, runningOrder, asNew) + async fetchSheetRundown(spreadsheetId: string, asNew?: boolean): Promise { + const downloadedRundown = await this.sheetManager.downloadRundown(spreadsheetId, this.coreHandler.GetOutputLayers()) + + if (downloadedRundown) { + this.processUpdatedRunningOrder(downloadedRundown.externalId, downloadedRundown, asNew) } - return runningOrder + return downloadedRundown } - async checkDriveFolder(): Promise { - if (!this.sheetFolderName) return [] - - const runningOrderIds = await this.sheetManager.getSheetsInDriveFolder(this.sheetFolderName) - return Promise.all( - runningOrderIds.map(async (roId) => { - return this.checkRunningOrderById(roId) - }) - ) - } /** * Will add all currently available Running Orders from the first drive folder * matching the provided name - * * @param sheetFolderName Name of folder to add Running Orders from. Eg. "My Running Orders" */ async setDriveFolder(sheetFolderName: string): Promise { this.sheetFolderName = sheetFolderName - return this.checkDriveFolder() + return this.fetchAllSpreadsheetsInFolder() + } + + /** + * Method updates watcher's poll intervals based on the number of spreadsheet documents. + * Updating is important to make sure that optimum number of API calls will be made and API limitation won't be hit. + * @param numberOfSpreadsheets Number of spreadsheet documents in the folder + */ + updatePollIntervals(numberOfSpreadsheets: number): void { + // How long is one period of counting API calls + const GOOGLE_TIMEOUT_SECONDS = 60 + + // Maximum number of API calls that can be safely made in one period + const GOOGLE_MAX_QUERIES = 60 + + // Assumption of many documents will be edited (or created) in one period + const MAX_EDIT_SHEETS_ASSUMPTION = 30 + + let slowInterval = + (1000 * GOOGLE_TIMEOUT_SECONDS) / ((GOOGLE_MAX_QUERIES - MAX_EDIT_SHEETS_ASSUMPTION) / numberOfSpreadsheets) + + if (slowInterval < 10000) { + slowInterval = 10000 + } + + if (slowInterval === this.pollIntervalSlow) { + // Nothing has changed + return + } + + logger.info('Updating slow interval to ' + slowInterval) + this.pollIntervalSlow = slowInterval + + this.stopWatcher() + this.startWatcher() + } + + /** + * Returns all sheets in selected folder on the drive + */ + async fetchAllSpreadsheetsInFolder(): Promise { + if (!this.sheetFolderName) return [] + + const spreadsheetIds = await this.sheetManager.getSpreadsheetsInDriveFolder(this.sheetFolderName) + + const sheets: SheetRundown[] = [] + + this.updatePollIntervals(spreadsheetIds.length) + + for (const spreadsheetId of spreadsheetIds) { + const sheet = await this.fetchSheetRundown(spreadsheetId) + if (sheet) { + sheets.push(sheet) + } + } + + return sheets + } + + /** + * Start the watcher + */ + startWatcher(): void { + logger.info('Starting Watcher') + this.stopWatcher() + + /** + * FAST check - only perform fetching if changes are detected + */ + this.fastInterval = setInterval(() => { + if (this.currentlyChecking) { + return + } + logger.info('Running fast check') + this.currentlyChecking = true + this.checkForChanges() + .catch((error) => { + let msg = getErrorMsg(error) + logger.error('Something went wrong during fast check: ' + msg) + logger.debug(error) + if (checkErrorType(error, ['invalid_grant', 'authError'])) { + msg += ', try resetting user credentials' + } + this.coreHandler.setStatus(StatusCode.BAD, [msg]) + }) + .then(() => { + this.currentlyChecking = false + }) + .catch((error) => { + logger.error('Error after checking for changes in fast check') + logger.debug(error) + }) + }, this.pollIntervalFast) + + /** + * SLOW check - fetch all spreadsheets in the folder + */ + this.slowinterval = setInterval(() => { + if (this.currentlyChecking) { + return + } + + logger.info('Running slow check') + this.currentlyChecking = true + + this.fetchAllSpreadsheetsInFolder() + .then(() => { + this.currentlyChecking = false + }) + .catch((error) => { + let msg = getErrorMsg(error) + logger.error('Something went wrong during slow check: ' + msg) + logger.debug(error) + if (checkErrorType(error, ['invalid_grant', 'authError'])) { + msg += ', try resetting user credentials' + } + this.coreHandler.setStatus(StatusCode.BAD, [msg]) + }) + }, this.pollIntervalSlow) + + // this.mediaPollInterval = setInterval(() => { + // if (this.currentlyChecking) { + // return + // } + // this.currentlyChecking = true + // this.updateAvailableMedia() + // .catch((error) => { + // console.log('Something went wrong during siper slow check', error, error.stack) + // }) + // .then(() => { + // this.updateAvailableOutputs() + // .catch((error) => { + // console.log('Something went wrong during super slow check', error, error.stack) + // }) + // .then(() => { + // this.updateAvailableTransitions() + // .catch((error) => { + // console.log('Something went wrong during super slow check', error, error.stack) + // }) + // .then(() => { + // this.currentlyChecking = false + // }) + // .catch(console.error) + // }) + // .catch(console.error) + // }) + // .catch(console.error) + // }, this.pollIntervalMedia) + } + + /** + * Stop the watcher + */ + stopWatcher(): void { + if (this.fastInterval) { + clearInterval(this.fastInterval) + this.fastInterval = undefined + } + if (this.slowinterval) { + clearInterval(this.slowinterval) + this.slowinterval = undefined + } + if (this.mediaPollInterval) { + clearInterval(this.mediaPollInterval) + this.mediaPollInterval = undefined + } + } + + dispose(): void { + this.stopWatcher() + } + + /** + * Method checks there have been any changes made to spreadsheet files. + * Checking is done by calling Drive API Changes method. + * If there are changes to files, they will be processed by processChange() method. + */ + private async checkForChanges(): Promise { + let pageToken: string | null | undefined = await this.getChangesStartPageToken() + + while (pageToken) { + const listData: Common.GaxiosResponse = await this.drive.changes.list({ + restrictToMyDrive: true, + pageToken: pageToken, + fields: '*', + }) + + if (listData.data.changes) { + for (const change of listData.data.changes) { + await this.processChange(change) + } + } + pageToken = listData.data.nextPageToken + + if (listData.data.newStartPageToken) { + // This was the end. No more changes + this.pageToken = listData.data.newStartPageToken + } + } + } + + /** + * Method returns start page token of Google Drive API Changes. + * @returns Start page token + */ + private async getChangesStartPageToken(): Promise { + if (this.pageToken) { + return this.pageToken + } + + const result = await this.drive.changes.getStartPageToken({}) + if (!result.data.startPageToken) { + throw new Error('No startPageToken found') + } + return result.data.startPageToken + } + + /** + * Method receives a Google Drive API Change object and fetches spreadsheet + * on which that change has been detected. + * @param change Change that has been detected + */ + private async processChange(change: drive_v3.Schema$Change) { + const fileId = change.fileId + if (fileId) { + const valid = await this.sheetManager.checkSheetIsValid(fileId) + if (valid) { + if (change.removed) { + // File was removed + console.log('Sheet was deleted', fileId) + this.processUpdatedRunningOrder(fileId, null) + } else { + // File was updated + console.log('Sheet was updated', fileId) + const newRundown = await this.sheetManager.downloadRundown(fileId, this.coreHandler.GetOutputLayers()) + + if (newRundown && newRundown.gatewayVersion === this.gatewayVersion) { + this.processUpdatedRunningOrder(fileId, newRundown) + } + } + } + } + } + + private processUpdatedRunningOrder(rundownId: string, rundown: SheetRundown | null, asNew?: boolean) { + const oldRundown = !asNew && this.runningOrders[rundownId] + + // Check if runningOrders have changed: + + if (!rundown && oldRundown) { + this.emit('rundown_delete', rundownId) + } else if (rundown && !oldRundown) { + this.emit('rundown_create', rundownId, rundown) + // this.fillRundownData().catch(console.error) + } else if (rundown && oldRundown) { + if (!_.isEqual(rundown.serialize(), oldRundown.serialize())) { + // console.log(rundown.serialize()) // debug + + this.emit('rundown_update', rundownId, rundown) + } else { + const newRundown: SheetRundown = rundown + + // Go through the sections for changes: + _.uniq( + oldRundown.segments + .map((segment) => segment.externalId) + .concat(newRundown.segments.map((segment) => segment.externalId)) + ).forEach((segmentId: string) => { + const oldSection: SheetSegment = oldRundown.segments.find( + (segment) => segment.externalId === segmentId + ) as SheetSegment // TODO: handle better + const newSection: SheetSegment = rundown.segments.find( + (segment) => segment.externalId === segmentId + ) as SheetSegment + + if (!newSection && oldSection) { + this.emit('segment_delete', rundownId, segmentId) + } else if (newSection && !oldSection) { + this.emit('segment_create', rundownId, segmentId, newSection) + } else if (newSection && oldSection) { + if (!_.isEqual(newSection.serialize(), oldSection.serialize())) { + // console.log(newSection.serialize(), oldSection.serialize()) // debug + this.emit('segment_update', rundownId, segmentId, newSection) + } else { + // Go through the stories for changes: + _.uniq( + oldSection.parts.map((part) => part.externalId).concat(newSection.parts.map((part) => part.externalId)) + ).forEach((storyId: string) => { + const oldStory: SheetPart = oldSection.parts.find((part) => part.externalId === storyId) as SheetPart // TODO handle the possibility of a missing id better + const newStory: SheetPart = newSection.parts.find((part) => part.externalId === storyId) as SheetPart + + if (!newStory && oldStory) { + this.emit('part_delete', rundownId, segmentId, storyId) + } else if (newStory && !oldStory) { + this.emit('part_create', rundownId, segmentId, storyId, newStory) + } else if (newStory && oldStory) { + if (!_.isEqual(newStory.serialize(), oldStory.serialize())) { + // console.log(newStory.serialize(), oldStory.serialize()) // debug + this.emit('part_update', rundownId, segmentId, storyId, newStory) + } else { + // At this point, we've determined that there are no changes. + // Do nothing + } + } + }) + } + } + }) + } + } + // Update the stored data: + if (rundown) { + this.runningOrders[rundownId] = clone(rundown) + } else { + delete this.runningOrders[rundownId] + } } async sendMediaViaGAPI(): Promise { @@ -285,7 +591,7 @@ export class RunningOrderWatcher extends EventEmitter { const updates: SheetUpdate[] = [] const cell = 2 - const objs = ['FULL', 'HEAD', 'CAM', 'DVE', 'SECTION', 'TITLES', 'BREAKER', 'PACKAGE'] + const objs = ['FULL', 'HEAD', 'CAM', 'COMPOSITION', 'SECTION', 'TITLES', 'BREAKER', 'PACKAGE'] objs.forEach((obj) => { updates.push({ @@ -403,233 +709,4 @@ export class RunningOrderWatcher extends EventEmitter { return Promise.resolve() } - - /** - * Start the watcher - */ - startWatcher(): void { - console.log('Starting Watcher') - this.stopWatcher() - - this.fastInterval = setInterval(() => { - if (this.currentlyChecking) { - return - } - // console.log('Running fast check') - this.currentlyChecking = true - this.checkForChanges() - .catch((error) => { - console.error('Something went wrong during fast check', error, error.stack) - }) - .then(() => { - // console.log('fast check done') - this.currentlyChecking = false - }) - .catch(console.error) - }, this.pollIntervalFast) - - this.slowinterval = setInterval(() => { - if (this.currentlyChecking) { - return - } - console.log('Running slow check') - this.currentlyChecking = true - - this.checkDriveFolder() - .catch((error) => { - console.error('Something went wrong during slow check', error, error.stack) - }) - .then(() => { - // console.log('slow check done') - this.currentlyChecking = false - }) - .catch(console.error) - }, this.pollIntervalSlow) - - this.mediaPollInterval = setInterval(() => { - if (this.currentlyChecking) { - return - } - this.currentlyChecking = true - this.updateAvailableMedia() - .catch((error) => { - console.log('Something went wrong during siper slow check', error, error.stack) - }) - .then(() => { - this.updateAvailableOutputs() - .catch((error) => { - console.log('Something went wrong during super slow check', error, error.stack) - }) - .then(() => { - this.updateAvailableTransitions() - .catch((error) => { - console.log('Something went wrong during super slow check', error, error.stack) - }) - .then(() => { - this.currentlyChecking = false - }) - .catch(console.error) - }) - .catch(console.error) - }) - .catch(console.error) - }, this.pollIntervalMedia) - } - - /** - * Stop the watcher - */ - stopWatcher(): void { - if (this.fastInterval) { - clearInterval(this.fastInterval) - this.fastInterval = undefined - } - if (this.slowinterval) { - clearInterval(this.slowinterval) - this.slowinterval = undefined - } - if (this.mediaPollInterval) { - clearInterval(this.mediaPollInterval) - this.mediaPollInterval = undefined - } - } - dispose(): void { - this.stopWatcher() - } - - private processUpdatedRunningOrder(rundownId: string, rundown: SheetRundown | null, asNew?: boolean) { - const oldRundown = !asNew && this.runningOrders[rundownId] - - // Check if runningOrders have changed: - - if (!rundown && oldRundown) { - this.emit('rundown_delete', rundownId) - } else if (rundown && !oldRundown) { - this.emit('rundown_create', rundownId, rundown) - this.fillRundownData().catch(console.error) - } else if (rundown && oldRundown) { - if (!_.isEqual(rundown.serialize(), oldRundown.serialize())) { - console.log(rundown.serialize()) // debug - - this.emit('rundown_update', rundownId, rundown) - } else { - const newRundown: SheetRundown = rundown - - // Go through the sections for changes: - _.uniq( - oldRundown.segments - .map((segment) => segment.externalId) - .concat(newRundown.segments.map((segment) => segment.externalId)) - ).forEach((segmentId: string) => { - const oldSection: SheetSegment = oldRundown.segments.find( - (segment) => segment.externalId === segmentId - ) as SheetSegment // TODO: handle better - const newSection: SheetSegment = rundown.segments.find( - (segment) => segment.externalId === segmentId - ) as SheetSegment - - if (!newSection && oldSection) { - this.emit('segment_delete', rundownId, segmentId) - } else if (newSection && !oldSection) { - this.emit('segment_create', rundownId, segmentId, newSection) - } else if (newSection && oldSection) { - if (!_.isEqual(newSection.serialize(), oldSection.serialize())) { - console.log(newSection.serialize(), oldSection.serialize()) // debug - this.emit('segment_update', rundownId, segmentId, newSection) - } else { - // Go through the stories for changes: - _.uniq( - oldSection.parts.map((part) => part.externalId).concat(newSection.parts.map((part) => part.externalId)) - ).forEach((storyId: string) => { - const oldStory: SheetPart = oldSection.parts.find((part) => part.externalId === storyId) as SheetPart // TODO handle the possibility of a missing id better - const newStory: SheetPart = newSection.parts.find((part) => part.externalId === storyId) as SheetPart - - if (!newStory && oldStory) { - this.emit('part_delete', rundownId, segmentId, storyId) - } else if (newStory && !oldStory) { - this.emit('part_create', rundownId, segmentId, storyId, newStory) - } else if (newStory && oldStory) { - if (!_.isEqual(newStory.serialize(), oldStory.serialize())) { - console.log(newStory.serialize(), oldStory.serialize()) // debug - this.emit('part_update', rundownId, segmentId, storyId, newStory) - } else { - // At this point, we've determined that there are no changes. - // Do nothing - } - } - }) - } - } - }) - } - } - // Update the stored data: - if (rundown) { - this.runningOrders[rundownId] = clone(rundown) - } else { - delete this.runningOrders[rundownId] - } - } - - private async processChange(change: drive_v3.Schema$Change) { - const fileId = change.fileId - if (fileId) { - const valid = await this.sheetManager.checkSheetIsValid(fileId) - if (valid) { - if (change.removed) { - // file was removed - console.log('Sheet was deleted', fileId) - - this.processUpdatedRunningOrder(fileId, null) - } else { - // file was updated - console.log('Sheet was updated', fileId) - const newRunningOrder = await this.sheetManager.downloadRunningOrder( - fileId, - this.coreHandler.GetOutputLayers() - ) - - if (newRunningOrder.gatewayVersion === this.gatewayVersion) { - this.processUpdatedRunningOrder(fileId, newRunningOrder) - } - } - } - } - } - - private async getPageToken(): Promise { - if (this.pageToken) { - return this.pageToken - } - - const result = await this.drive.changes.getStartPageToken({}) - if (!result.data.startPageToken) { - throw new Error('No startPageToken found') - } - return result.data.startPageToken - } - private async checkForChanges(): Promise { - let pageToken: string | null | undefined = await this.getPageToken() - - while (pageToken) { - const listData: Common.GaxiosResponse = await this.drive.changes.list({ - restrictToMyDrive: true, - pageToken: pageToken, - fields: '*', - }) - - if (listData.data.changes) { - for (const change of listData.data.changes) { - await this.processChange(change) - } - } - pageToken = listData.data.nextPageToken - - if (listData.data.newStartPageToken) { - // This was the end. No more changes - this.pageToken = listData.data.newStartPageToken - } - } - return - } } diff --git a/src/classes/SheetManager.ts b/src/classes/SheetManager.ts index f358b52..2944c44 100644 --- a/src/classes/SheetManager.ts +++ b/src/classes/SheetManager.ts @@ -1,7 +1,9 @@ +import { IOutputLayer } from '@sofie-automation/blueprints-integration' import { Auth, Common, google, sheets_v4 } from 'googleapis' +import { logger } from '../logger' +import { getErrorMsg } from '../util' import { SheetRundown } from './Rundown' -import { IOutputLayer } from '@sofie-automation/blueprints-integration' -const sheets = google.sheets('v4') +const sheets = google.sheets({ version: 'v4', timeout: 5000 }) const drive = google.drive('v3') const SHEET_NAME = process.env.SHEET_NAME || 'Rundown' @@ -11,10 +13,14 @@ export interface SheetUpdate { cellPosition: string } +export interface SplittedSheets { + mainSheet: sheets_v4.Schema$ValueRange | undefined +} + export class SheetsManager { private currentFolder = '' - constructor(private auth: Auth.OAuth2Client) {} + constructor(private _oAuth2Client: Auth.OAuth2Client) {} /** * Creates a Google Sheets api-specific change element @@ -30,53 +36,77 @@ export class SheetsManager { } } - /** - * Downloads and parses a Running Order for google sheets - * - * @param rundownSheetId Id of the google sheet containing the Running Order - */ - async downloadRunningOrder(rundownSheetId: string, outputLayers: IOutputLayer[]): Promise { - return this.downloadSheet(rundownSheetId).then((data) => { - const runningOrderTitle = data.meta.properties ? data.meta.properties.title || 'unknown' : 'unknown' - return SheetRundown.fromSheetCells( - rundownSheetId, - runningOrderTitle, - data.values.values || [], - outputLayers, - this - ) + async downloadRundown(spreadsheetId: string, outputLayers: IOutputLayer[]): Promise { + try { + const downloadedSpreadsheet = await this.fetchSpreadsheetSheets(spreadsheetId) + + if (!downloadedSpreadsheet) { + return undefined + } + + const downloadedMainSheet = downloadedSpreadsheet?.mainSheet + if (!downloadedMainSheet) { + logger.warn(`Rundown main sheet is undefined`) + return undefined + } + + return SheetRundown.fromSheetCells(spreadsheetId, SHEET_NAME, downloadedMainSheet.values || [], outputLayers) + } catch (error) { + logger.error(`Error while downloading rundown`) + logger.debug(error) + return undefined + } + } + + async fetchSpreadsheetFromServer( + spreadsheetId: string + ): Promise> { + const res = await sheets.spreadsheets.values.batchGet({ + spreadsheetId, + ranges: [SHEET_NAME], + auth: this._oAuth2Client, }) + return res } /** - * Downloads raw data from google spreadsheets - * - * @param spreadsheetId Id of the google spreadsheet to download + * Method downloads specific Google spreadsheet document + * @param spreadsheetId Id of the Google spreadsheet to download + * @returns Object containing all splitted sheets */ - async downloadSheet(spreadsheetId: string): Promise<{ - meta: sheets_v4.Schema$Spreadsheet - values: sheets_v4.Schema$ValueRange - }> { - const request = { - // The spreadsheet to request. - auth: this.auth, - spreadsheetId, - // The ranges to retrieve from the spreadsheet. - range: SHEET_NAME, // Get all cells in Rundown sheet + async fetchSpreadsheetSheets(spreadsheetId: string): Promise { + try { + const res = await this.fetchSpreadsheetFromServer(spreadsheetId) + return this.splitSheets(res) + } catch (error) { + logger.error(`Error while executing batch get for spreadsheet ${spreadsheetId}: ${getErrorMsg(error)}`) + logger.debug(error) + return undefined } - return Promise.all([ - sheets.spreadsheets.get({ - auth: this.auth, - spreadsheetId, - fields: 'spreadsheetId,properties.title', - }), - sheets.spreadsheets.values.get(request), - ]).then(([meta, values]) => { - return { - meta: meta.data, - values: values.data, + } + + splitSheets(response: Common.GaxiosResponse): SplittedSheets { + return { + mainSheet: this.extractSheet(response.data.valueRanges || [], SHEET_NAME), + } + } + + /** + * Helper method that extracts specific sheet from the array of downloaded sheets. + * @param sheetValueRanges Array of downloaded ranges + * @param sheetName Name of the sheet that should be returned + * @returns Sheet value range of the desired sheet + */ + extractSheet( + sheetValueRanges: sheets_v4.Schema$ValueRange[], + sheetName: string + ): sheets_v4.Schema$ValueRange | undefined { + for (const sheetValueRange of sheetValueRanges) { + if (sheetValueRange.range?.includes(sheetName)) { + return sheetValueRange } - }) + } + return undefined } /** @@ -104,21 +134,30 @@ export class SheetsManager { */ async updateSheet( spreadsheetId: string, - sheetUpdates: sheets_v4.Schema$ValueRange[] + _sheetUpdates: sheets_v4.Schema$ValueRange[] ): Promise> { const request: sheets_v4.Params$Resource$Spreadsheets$Values$Batchupdate = { spreadsheetId: spreadsheetId, requestBody: { valueInputOption: 'RAW', - data: sheetUpdates, - // [{ - // range: 'A1:A1', - // values: [[1]] - // }] }, - auth: this.auth, + auth: this._oAuth2Client, } return sheets.spreadsheets.values.batchUpdate(request) + + // const request: sheets_v4.Params$Resource$Spreadsheets$Values$Batchupdate = { + // spreadsheetId: spreadsheetId, + // requestBody: { + // valueInputOption: 'RAW', + // data: sheetUpdates, + // // [{ + // // range: 'A1:A1', + // // values: [[1]] + // // }] + // }, + // auth: this.auth, + // } + // return sheets.spreadsheets.values.batchUpdate(request) } /** @@ -127,8 +166,8 @@ export class SheetsManager { * * @param folderName Name of Google Drive folder */ - async getSheetsInDriveFolder(folderName: string): Promise { - const drive = google.drive({ version: 'v3', auth: this.auth }) + async getSpreadsheetsInDriveFolder(folderName: string): Promise { + const drive = google.drive({ version: 'v3', auth: this._oAuth2Client }) const fileList = await drive.files.list({ // q: `mimeType='application/vnd.google-apps.spreadsheet' and '${folderId}' in parents`, @@ -137,10 +176,12 @@ export class SheetsManager { spaces: 'drive', fields: 'nextPageToken, files(*)', }) + // Use first hit only. We assume that that would be the correct folder. // If you have multiple folders with the same name, it will become un-deterministic if (fileList.data.files && fileList.data.files[0] && fileList.data.files[0].id) { - return this.getSheetsInDriveFolderId(fileList.data.files[0].id) + const folderId = fileList.data.files[0].id + return this.getSpreadsheetsInDriveFolderId(folderId) } else { return [] } @@ -151,35 +192,31 @@ export class SheetsManager { * @param folderId Id of Google Drive folder to retrieve spreadsheets from * @param nextPageToken Google drive nextPageToken pagination token. */ - async getSheetsInDriveFolderId(folderId: string, nextPageToken?: string): Promise { - const drive = google.drive({ version: 'v3', auth: this.auth }) - + async getSpreadsheetsInDriveFolderId(folderId: string, nextPageToken?: string): Promise { + const drive = google.drive({ version: 'v3', auth: this._oAuth2Client }) this.currentFolder = folderId - const fileList = await drive.files.list({ - q: `mimeType='application/vnd.google-apps.spreadsheet' and '${folderId}' in parents`, - spaces: 'drive', - fields: 'nextPageToken, files(*)', - pageToken: nextPageToken, - }) - - const resultData = (fileList.data.files || []) - .filter((file) => { - if (file.name && file.name[0] !== '_' && !file.trashed) { - return file.id - } - return - }) - .map((file) => { - return file.id || '' + try { + const fileList = await drive.files.list({ + q: `mimeType='application/vnd.google-apps.spreadsheet' and '${folderId}' in parents`, + // q: `mimeType='application/vnd.google-apps.spreadsheet'`, + spaces: 'drive', + fields: 'nextPageToken, files(*)', + pageToken: nextPageToken, }) - if (fileList.data.nextPageToken) { - const result = await this.getSheetsInDriveFolderId(folderId, fileList.data.nextPageToken) + const resultDataFileIds = (fileList.data.files || []) + .filter((file) => file.name && file.name[0] !== '_' && !file.trashed) + .map((file) => file.id || '') - return resultData.concat(result) - } else { - return resultData + if (fileList.data.nextPageToken) { + const nextPageDataFileIds = await this.getSpreadsheetsInDriveFolderId(folderId, fileList.data.nextPageToken) + return resultDataFileIds.concat(nextPageDataFileIds) + } + return resultDataFileIds + } catch (error) { + console.log('Error while fetching spreadsheets from folder', JSON.stringify(error)) + return [] } } @@ -191,7 +228,7 @@ export class SheetsManager { const spreadsheet = await sheets.spreadsheets .get({ spreadsheetId: sheetid, - auth: this.auth, + auth: this._oAuth2Client, }) .catch(console.error) @@ -203,7 +240,7 @@ export class SheetsManager { .get({ fileId: sheetid, fields: 'parents', - auth: this.auth, + auth: this._oAuth2Client, }) .catch(console.error) diff --git a/src/classes/__tests__/Items.spec.ts b/src/classes/__tests__/Items.spec.ts deleted file mode 100644 index 658fee2..0000000 --- a/src/classes/__tests__/Items.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -// jest.mock('deep-equal') - -describe('Items', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - it('Fake', () => { - expect(true).toBeTruthy() - }) -}) diff --git a/src/classes/__tests__/Rundown.spec.ts b/src/classes/__tests__/Rundown.spec.ts new file mode 100644 index 0000000..1e8efbc --- /dev/null +++ b/src/classes/__tests__/Rundown.spec.ts @@ -0,0 +1,22 @@ +import { Common, google, sheets_v4 } from 'googleapis' +import { SheetRundown } from '../Rundown' +import { SheetsManager } from '../SheetManager' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const responseMock = require('./cellValues.json') as Common.GaxiosResponse +const SHEET_NAME = 'Rundown' + +describe('Rundown', () => { + const oAuth2ClientMock = new google.auth.OAuth2() + const sheetsManager = new SheetsManager(oAuth2ClientMock) + + describe('Rundown parsing', () => { + it('Parse rundown', () => { + const mainSheet = sheetsManager.extractSheet(responseMock.data.valueRanges || [], SHEET_NAME) + + const rundown = SheetRundown.fromSheetCells('spreadsheet-id', 'Rundown name', mainSheet?.values || [], []) + + expect(rundown).toBeTruthy() + }) + }) +}) diff --git a/src/classes/__tests__/RunningOrders.spec.ts b/src/classes/__tests__/RunningOrders.spec.ts deleted file mode 100644 index e987609..0000000 --- a/src/classes/__tests__/RunningOrders.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { SheetRundown } from '../Rundown' - -// import * as cellData from './cellValues.json' - -describe('RunningOrders', () => { - it('should exist', () => { - const a = new SheetRundown('test', 'some name', 'v0.2', 1, 2) - expect(a).toBeTruthy() - }) - // it('should correctly parse 2d cell array', () => { - // let a = SheetRundown.fromSheetCells('sheetId123', 'name', (cellData as any).values) - // expect(a).toBeTruthy() - - // expect(a.id).toEqual('sheetId123') - // expect(a.name).toEqual('name') - // expect(a.expectedStart).toEqual(new Date(1577685600000)) - // expect(a.expectedEnd).toEqual(new Date(1577685600000 + 30 * 60 * 1000)) - // expect(a.segments.length).toBe(10) - // }) - // it('Diff undefined should return "Deleted" change', () => { - // let a = SheetRundown.fromSheetCells('sheetId123', 'name', (cellData as any).values) - // let diff = a.diff(undefined) - // expect(diff.changeType).toEqual('Deleted') - // }) - // it('Diff itself should return "Unchanged" change', () => { - // let a = SheetRundown.fromSheetCells('sheetId123', 'name', (cellData as any).values) - // let diff = a.diff(a) - // expect(diff.changeType).toEqual('Unchanged') - // expect(diff.sections.length).toEqual(0) - // }) - // it('Diff identical should return "Unchanged" change', () => { - // let a = SheetRundown.fromSheetCells('sheetId123', 'name', (cellData as any).values) - // let b = SheetRundown.fromSheetCells('sheetId123', 'name', (cellData as any).values) - // let diff = a.diff(b) - // expect(diff.changeType).toEqual('Unchanged') - // expect(diff.sections.length).toEqual(0) // Will not work right now as the id's are not set - // }) - describe('#columnToLetter', () => { - it('returns correct values', () => { - expect(SheetRundown.columnToLetter(0)).toEqual('') - expect(SheetRundown.columnToLetter(1)).toEqual('A') - expect(SheetRundown.columnToLetter(2)).toEqual('B') - expect(SheetRundown.columnToLetter(3)).toEqual('C') - expect(SheetRundown.columnToLetter(4)).toEqual('D') - expect(SheetRundown.columnToLetter(5)).toEqual('E') - expect(SheetRundown.columnToLetter(6)).toEqual('F') - expect(SheetRundown.columnToLetter(7)).toEqual('G') - expect(SheetRundown.columnToLetter(8)).toEqual('H') - expect(SheetRundown.columnToLetter(9)).toEqual('I') - expect(SheetRundown.columnToLetter(10)).toEqual('J') - expect(SheetRundown.columnToLetter(11)).toEqual('K') - expect(SheetRundown.columnToLetter(12)).toEqual('L') - expect(SheetRundown.columnToLetter(13)).toEqual('M') - expect(SheetRundown.columnToLetter(14)).toEqual('N') - expect(SheetRundown.columnToLetter(15)).toEqual('O') - expect(SheetRundown.columnToLetter(16)).toEqual('P') - expect(SheetRundown.columnToLetter(17)).toEqual('Q') - expect(SheetRundown.columnToLetter(18)).toEqual('R') - expect(SheetRundown.columnToLetter(19)).toEqual('S') - expect(SheetRundown.columnToLetter(20)).toEqual('T') - expect(SheetRundown.columnToLetter(21)).toEqual('U') - expect(SheetRundown.columnToLetter(22)).toEqual('V') - expect(SheetRundown.columnToLetter(23)).toEqual('W') - expect(SheetRundown.columnToLetter(24)).toEqual('X') - expect(SheetRundown.columnToLetter(25)).toEqual('Y') - expect(SheetRundown.columnToLetter(26)).toEqual('Z') - expect(SheetRundown.columnToLetter(27)).toEqual('AA') - expect(SheetRundown.columnToLetter(28)).toEqual('AB') - expect(SheetRundown.columnToLetter(29)).toEqual('AC') - expect(SheetRundown.columnToLetter(30)).toEqual('AD') - expect(SheetRundown.columnToLetter(31)).toEqual('AE') - expect(SheetRundown.columnToLetter(32)).toEqual('AF') - expect(SheetRundown.columnToLetter(33)).toEqual('AG') - expect(SheetRundown.columnToLetter(34)).toEqual('AH') - expect(SheetRundown.columnToLetter(35)).toEqual('AI') - expect(SheetRundown.columnToLetter(36)).toEqual('AJ') - expect(SheetRundown.columnToLetter(37)).toEqual('AK') - expect(SheetRundown.columnToLetter(38)).toEqual('AL') - expect(SheetRundown.columnToLetter(39)).toEqual('AM') - expect(SheetRundown.columnToLetter(40)).toEqual('AN') - expect(SheetRundown.columnToLetter(41)).toEqual('AO') - expect(SheetRundown.columnToLetter(42)).toEqual('AP') - expect(SheetRundown.columnToLetter(43)).toEqual('AQ') - expect(SheetRundown.columnToLetter(44)).toEqual('AR') - expect(SheetRundown.columnToLetter(45)).toEqual('AS') - expect(SheetRundown.columnToLetter(46)).toEqual('AT') - expect(SheetRundown.columnToLetter(47)).toEqual('AU') - expect(SheetRundown.columnToLetter(48)).toEqual('AV') - expect(SheetRundown.columnToLetter(49)).toEqual('AW') - expect(SheetRundown.columnToLetter(50)).toEqual('AX') - expect(SheetRundown.columnToLetter(51)).toEqual('AY') - expect(SheetRundown.columnToLetter(52)).toEqual('AZ') - expect(SheetRundown.columnToLetter(53)).toEqual('BA') - expect(SheetRundown.columnToLetter(54)).toEqual('BB') - expect(SheetRundown.columnToLetter(55)).toEqual('BC') - expect(SheetRundown.columnToLetter(56)).toEqual('BD') - expect(SheetRundown.columnToLetter(57)).toEqual('BE') - expect(SheetRundown.columnToLetter(26 * 26 + 27)).toEqual('AAA') - expect(SheetRundown.columnToLetter(26 * 26 * 27 + 27)).toEqual('AAAA') - }) - }) -}) diff --git a/src/classes/__tests__/Section.spec.ts b/src/classes/__tests__/Section.spec.ts deleted file mode 100644 index 45c50db..0000000 --- a/src/classes/__tests__/Section.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { SheetSection, SheetSectionDiffWithType } from '../Section' -// import { SheetStory } from '../Story' - -describe('Items', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - it('Fake', () => { - expect(true).toBeTruthy() - }) - - // it('Add story works', () => { - // let a = new SheetSection('orderId123', 'sectionId123', 1, 'Order Name', false) - - // expect(a.stories.length).toBe(0) - - // let storyOne = new SheetStory('type', 'sectionId', 'storyId1', 0, 'name', false, 'script') - // let storyTwo = new SheetStory('type', 'sectionId', 'storyId2', 0, 'name', false, 'script') - // a.addStories([storyOne]) - // expect(a.stories.length).toBe(1) - // expect(a.stories.indexOf(storyOne)).toBe(0) - // a.addStory(storyTwo) - // expect(a.stories.length).toBe(2) - // expect(a.stories.indexOf(storyOne)).toBe(0) - // expect(a.stories.indexOf(storyTwo)).toBe(1) - - // }) -}) diff --git a/src/classes/__tests__/SheetManager.spec.ts b/src/classes/__tests__/SheetManager.spec.ts new file mode 100644 index 0000000..1700eaf --- /dev/null +++ b/src/classes/__tests__/SheetManager.spec.ts @@ -0,0 +1,63 @@ +import { Common, google, sheets_v4 } from 'googleapis' +import { SheetsManager } from '../SheetManager' +import * as _ from 'lodash' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const responseMock = require('./cellValues.json') as Common.GaxiosResponse +const SHEET_NAME = 'Rundown' + +describe('Sheet Manager', () => { + const oAuth2ClientMock = new google.auth.OAuth2() + const sheetsManager = new SheetsManager(oAuth2ClientMock) + + function deleteSheet(response: Common.GaxiosResponse, sheetName: string) { + const noSheetResponseMock = _.cloneDeep(response) + const foundSheet = sheetsManager.extractSheet(noSheetResponseMock.data.valueRanges || [], sheetName) + noSheetResponseMock.data.valueRanges = noSheetResponseMock.data.valueRanges?.filter((vr) => vr !== foundSheet) + return noSheetResponseMock + } + + beforeAll(() => { + jest + .spyOn(sheetsManager, 'fetchSpreadsheetFromServer') + .mockReturnValue(new Promise((resolve) => resolve(responseMock))) + }) + + it('Download Rundown', async () => { + const validRundown = await sheetsManager.downloadRundown('spreadsheet-id', []) + expect(validRundown).toBeTruthy() + }) + + it('Missing meta sheet', async () => { + const noMetaResponseMock = deleteSheet(responseMock, SHEET_NAME) + + jest + .spyOn(sheetsManager, 'fetchSpreadsheetFromServer') + .mockReturnValue(new Promise((resolve) => resolve(noMetaResponseMock))) + + const validRundown = await sheetsManager.downloadRundown('spreadsheet-id', []) + expect(validRundown).toBeUndefined() + }) + + it('Fetch Spreadsheet Sheets', async () => { + const fetchedSheets = await sheetsManager.fetchSpreadsheetSheets('spreadsheet-id') + expect(fetchedSheets).toBeTruthy() + }) + + it('Split sheets', () => { + const splitted = sheetsManager.splitSheets(responseMock) + + expect(splitted.mainSheet).toBeTruthy() + }) + + it('Extract sheet', () => { + const mainSheet = sheetsManager.extractSheet(responseMock.data.valueRanges || [], SHEET_NAME) + + expect(mainSheet?.range?.includes(SHEET_NAME)).toBeTruthy() + }) + + it('Extract invalid sheet', () => { + const unknownSheet = sheetsManager.extractSheet(responseMock.data.valueRanges || [], 'unknown-sheet') + expect(unknownSheet).toBe(undefined) + }) +}) diff --git a/src/classes/__tests__/cellValues.json b/src/classes/__tests__/cellValues.json index d49fde9..4632e4b 100644 --- a/src/classes/__tests__/cellValues.json +++ b/src/classes/__tests__/cellValues.json @@ -1 +1,1602 @@ -{"values":[["Expected show start","12/30/2019 7:00:00","Expected end","12/30/2019 7:30:00","Total content duration","-0.23.43","","0.06.17"],["name","type","id","float","script","objectType","objectTime","","duration","clipName","","attr: name","","attr1","","attr2","","feedback"],["Name","Story Type","Identifier (Optional)","Float","Script","Object type","Time","","Duration","Clip name","","Attribute 1","","Attribute 2","","Attribute 3","","Feedback"],["Intro","SECTION","section-1","FALSE"],["INTRO","FULL","intro","FALSE","","video","00.00.00","0.00.07","0.00.07","std/vinjett.mp4"],["Head 1","HEAD","8b47dde0-4741-11e9-bace-892f501e2351","FALSE","The ducks in the pond are swimming and quacking. They don't seem to be aware of what awaits them","video","00.00.00","0.00.05","0.00.05","clips/head_ducks.mp4","","","","","","","","Video clip missing!"],["","","8b47dde1-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.02","","","gfx/head","","Ducks happily unaware"],["Head 2","HEAD","8b47dde2-4741-11e9-bace-892f501e2351","FALSE","The deers in the forest seem worried. Last nights blood moon seem to have stirred some uneasiness","video","00.00.00","0.00.05","0.00.05","clips/head_deer.mp4"],["","","8b47dde3-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.02","","","gfx/head","","Deer are uneasy"],["Head 3","HEAD","8b47dde4-4741-11e9-bace-892f501e2351","FALSE","A new tv studio automation solution takes its first baby steps, after an intense year of development","video","00.00.00","0.00.05","0.00.05","clips/head_sofie.mp4"],["","","8b47dde5-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.02","","","gfx/head","","\"Sofie\" breaks new ground"],["Studio","CAM","8b47dde6-4741-11e9-bace-892f501e2351","FALSE","Hello and welcome to super-news, your daily source for the most interesting news!","camera","00.00.00","0.00.05","0.00.05","","","kam3"],["","","8b47dde7-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.02","","0.00.04","gfx/name_left","","Sofie Automationsson"],["","SECTION","8b47dde8-4741-11e9-bace-892f501e2351","FALSE"],["New story","FULL","8b47dde9-4741-11e9-bace-892f501e2351","FALSE"],["Egg story","SECTION","8b47ddea-4741-11e9-bace-892f501e2351","FALSE"],["Egg story, intro","CAM","8b47ddeb-4741-11e9-bace-892f501e2351","FALSE","But first, we're going to turn our attention to the newly hatched egg in the back of the studio","camera","00.00.00","","","","","kam1"],["Egg story","FULL","8b47ddec-4741-11e9-bace-892f501e2351","TRUE","","video","00.00.00","","","clips/egg.mp4"],["Egg, interview","CAM","8b47dded-4741-11e9-bace-892f501e2351","FALSE","With me in the studio, I've got Abe Bergsson, who works at the local farm. [..]","camera","00.00.00","0.01.20","0.01.20","","","kam2"],["","","","FALSE","","graphic","","","","gfx/name_left","","Abe Bergsson"],["","","","FALSE","How did you discover the egg?"],["","","8b47ddf0-4741-11e9-bace-892f501e2351","FALSE","What is the thing that hatched?"],["","","8b47ddf1-4741-11e9-bace-892f501e2351","FALSE","","video","","","0.00.15","clips/egghatching.mp4"],["","SECTION","8b47ddf2-4741-11e9-bace-892f501e2351","FALSE"],["Wow!!","FULL","8b47ddf3-4741-11e9-bace-892f501e2351","FALSE"],["Ducks","SECTION","8b47ddf4-4741-11e9-bace-892f501e2351","FALSE"],["Ducks, intro","CAM","","FALSE","We've got our reporter, Charlie Deidriksson with us out by the pond, what can you tell us, Charlie?","camera","00.00.00","","","","","kam1"],["","","","FALSE"],["","","8b47ddf7-4741-11e9-bace-892f501e2351","FALSE"],["Split","SPLIT","8b47ddf8-4741-11e9-bace-892f501e2351","FALSE","","split","","","","","","kam1","","remote1"],["","","8b47ddf9-4741-11e9-bace-892f501e2351","FALSE","Thank you, Charlie. For more updates on this story, please visit our homepage, www.supernews.com","camera","00.00.00","","","","","kam2"],["","","8b47ddfa-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.02","","0.00.04","gfx/lower_right","","www.supernews.com"],["Forest","SECTION","8b47ddfb-4741-11e9-bace-892f501e2351","FALSE"],["Forest, intro","CAM","8b47ddfc-4741-11e9-bace-892f501e2351","FALSE","In the forest behind the old mill, there's been quite a disturbance yesterday...","camera","00.00.00","","","","","kam1"],["Forest","FULL","8b47ddfd-4741-11e9-bace-892f501e2351","FALSE","","video","00.00.00","0.00.52","0.00.52","clips/deer.mp4"],["","","8b47ddfe-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.02","","0.00.07","gfx/name_left","","Reporter: Elon Fergusson"],["","","8b47ddff-4741-11e9-bace-892f501e2351","FALSE","","graphic","00.00.46","","0.00.03","gfx/name_left","","Editing: George Hoff"],["Sofie","SECTION","8b47de00-4741-11e9-bace-892f501e2351","FALSE"],["Sofie, intro","CAM","8b47de01-4741-11e9-bace-892f501e2351","FALSE","The biggest event of the century, the TV Automation system \"Sofie\" opens it's doors to the public. The founders, Superfly.tv thinks it's going to be an industry-changing solution.","camera","","","","","","kam1"],["Sofie","FULL","8b47de02-4741-11e9-bace-892f501e2351","FALSE","","video","00.00.00","0.00.35","0.00.35","clips/sofie.mp4"],["Sofie, guests","CAM","8b47de03-4741-11e9-bace-892f501e2351","FALSE","The studio is full of guests, we've got","camera","","0.02.13","0.02.13","","","kam3"],["","","8b47de04-4741-11e9-bace-892f501e2351","FALSE","Jonas Hummelstrand","graphic","","","","gfx/name_right","","Jonas Hummelstrand"],["","","8b47de05-4741-11e9-bace-892f501e2351","FALSE","Jesper Stærkær","graphic","","","","gfx/name_right","","Jesper Stærkær"],["","","8b47de06-4741-11e9-bace-892f501e2351","FALSE","Johan Nyman","graphic","","","","gfx/name_right","","Johan Nyman"],["","","8b47de07-4741-11e9-bace-892f501e2351","FALSE","Jan Starzak","graphic","","","","gfx/name_right","","Jan Starzak"],["","","8b47de08-4741-11e9-bace-892f501e2351","FALSE","Julian Waller","graphic","","","","gfx/name_right","","Julian Waller"],["","","8b47de09-4741-11e9-bace-892f501e2351","FALSE","Balte de Wit","graphic","","","","gfx/name_right","","Balte de Wit"],["","","8b47de0a-4741-11e9-bace-892f501e2351","FALSE","Stig Roar Aftret","graphic","","","","gfx/name_right","","Stig Roar Aftret"],["Weather","SECTION","8b47de0b-4741-11e9-bace-892f501e2351","FALSE"],["Weather, intro","CAM","8b47de0c-4741-11e9-bace-892f501e2351","FALSE","...And now it's time for the weather.","camera","","","","","","kam1"],["Weather","LS","8b47de0d-4741-11e9-bace-892f501e2351","FALSE","Today it's gonna be cloudy with a chance of meatballs. Later tonight, NASA has issued a class 3 warning for falling debris, so be safe out there.","video","00.00.00","0.00.35","0.00.35","clips/sofie.mp4"],["Outro","SECTION","8b47de0e-4741-11e9-bace-892f501e2351","FALSE"],["Outro","CAM","8b47de0f-4741-11e9-bace-892f501e2351","FALSE","That's all from us guys, see you all next time! Don't forget to like and subscribe!","camera","","","","","","kam1"],["Outro","FULL","8b47de10-4741-11e9-bace-892f501e2351","FALSE","","camera","00.00.00","0.00.15","0.00.15","","","kam3"],["","","8b47de11-4741-11e9-bace-892f501e2351","FALSE","","overlay","00.00.00","","","std/outro.mp4"],["","","8b47de12-4741-11e9-bace-892f501e2351","FALSE","","lights","00.00.00","","","","","studio","","0","","fade"],["Brexit","SECTION","8b4804f0-4741-11e9-bace-892f501e2351","FALSE"],["Brexit, intro","CAM","8b4804f1-4741-11e9-bace-892f501e2351","FALSE","Nobody knows what's going on! Run for the hills! Run away! Run away!","camera","","","","","","kam1"],["","","8b4804f2-4741-11e9-bace-892f501e2351","FALSE","","video","00.00.00","","0.00.35","clips/sofie.mp4","","screen 1"],["","","8b4804f3-4741-11e9-bace-892f501e2351","FALSE","","video","00.00.00","","0.00.07","std/vinjett.mp4","","screen 2"],["","","","FALSE","","video","00.00.00","","0.00.35","clips/sofie.mp4","","screen 3"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"],["","","","FALSE"]]} \ No newline at end of file +{ + "config": { + "timeout": 5000, + "url": "https://sheets.googleapis.com/v4/spreadsheets/1vp_Gniaf-rK0RkdgxryCcldEAv2547srXcDbj0Us4pA/values:batchGet?ranges=rundown-meta&ranges=rundown-segments&ranges=rundown-parts&ranges=rundown-details", + "method": "GET", + "userAgentDirectives": [{ "product": "google-api-nodejs-client", "version": "5.1.0", "comment": "gzip" }], + "headers": { + "x-goog-api-client": "gdcl/5.1.0 gl-node/16.14.0 auth/7.14.1", + "Accept-Encoding": "gzip", + "User-Agent": "google-api-nodejs-client/5.1.0 (gzip)", + "Authorization": "Bearer ya29.A0AVA9y1vRuyqxNf-iwBfyMQ0sZuuYyEwmM5jKtMhQ87ndhmHsPCk9NBAHWE2V-TGAC2CxoK1HxhqHDsRioG_Gf-7k16u8cb2atlQ_xMMUQWY8AJqOrZ5oTp2XZRARvILYNNZq7Rkop2ffQ-z1wiPIy8xFVtZ7LwaCgYKATASATASFQE65dr8z4D8lVCelwDW8oU4WaKs6A0165", + "Accept": "application/json" + }, + "params": { "ranges": ["Rundown"] }, + "retry": true, + "responseType": "json" + }, + "data": { + "spreadsheetId": "1vp_Gniaf-rK0RkdgxryCcldEAv2547srXcDbj0Us4pA", + "valueRanges": [ + { + "range": "'Rundown'!A1:Z1001", + "majorDimension": "ROWS", + "values": [ + [ + "Expected show start", + "12/30/2019 7:00:00", + "Expected end", + "12/30/2019 7:30:00", + "Total content duration", + "-0.23.43", + "", + "0.06.17" + ], + [ + "name", + "type", + "id", + "float", + "script", + "objectType", + "objectTime", + "", + "duration", + "clipName", + "", + "attr: name", + "", + "attr1", + "", + "attr2", + "", + "feedback" + ], + [ + "Name", + "Story Type", + "Identifier (Optional)", + "Float", + "Script", + "Object type", + "Time", + "", + "Duration", + "Clip name", + "", + "Attribute 1", + "", + "Attribute 2", + "", + "Attribute 3", + "", + "Feedback" + ], + ["Intro", "SECTION", "section-1", "FALSE"], + ["INTRO", "FULL", "intro", "FALSE", "", "video", "00.00.00", "0.00.07", "0.00.07", "std/vinjett.mp4"], + [ + "Head 1", + "HEAD", + "8b47dde0-4741-11e9-bace-892f501e2351", + "FALSE", + "The ducks in the pond are swimming and quacking. They don't seem to be aware of what awaits them", + "video", + "00.00.00", + "0.00.05", + "0.00.05", + "clips/head_ducks.mp4", + "", + "", + "", + "", + "", + "", + "", + "Video clip missing!" + ], + [ + "", + "", + "8b47dde1-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.02", + "", + "", + "gfx/head", + "", + "Ducks happily unaware" + ], + [ + "Head 2", + "HEAD", + "8b47dde2-4741-11e9-bace-892f501e2351", + "FALSE", + "The deers in the forest seem worried. Last nights blood moon seem to have stirred some uneasiness", + "video", + "00.00.00", + "0.00.05", + "0.00.05", + "clips/head_deer.mp4" + ], + [ + "", + "", + "8b47dde3-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.02", + "", + "", + "gfx/head", + "", + "Deer are uneasy" + ], + [ + "Head 3", + "HEAD", + "8b47dde4-4741-11e9-bace-892f501e2351", + "FALSE", + "A new tv studio automation solution takes its first baby steps, after an intense year of development", + "video", + "00.00.00", + "0.00.05", + "0.00.05", + "clips/head_sofie.mp4" + ], + [ + "", + "", + "8b47dde5-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.02", + "", + "", + "gfx/head", + "", + "\"Sofie\" breaks new ground" + ], + [ + "Studio", + "CAM", + "8b47dde6-4741-11e9-bace-892f501e2351", + "FALSE", + "Hello and welcome to super-news, your daily source for the most interesting news!", + "camera", + "00.00.00", + "0.00.05", + "0.00.05", + "", + "", + "kam3" + ], + [ + "", + "", + "8b47dde7-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.02", + "", + "0.00.04", + "gfx/name_left", + "", + "Sofie Automationsson" + ], + ["", "SECTION", "8b47dde8-4741-11e9-bace-892f501e2351", "FALSE"], + ["New story", "FULL", "8b47dde9-4741-11e9-bace-892f501e2351", "FALSE"], + ["Egg story", "SECTION", "8b47ddea-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Egg story, intro", + "CAM", + "8b47ddeb-4741-11e9-bace-892f501e2351", + "FALSE", + "But first, we're going to turn our attention to the newly hatched egg in the back of the studio", + "camera", + "00.00.00", + "", + "", + "", + "", + "kam1" + ], + [ + "Egg story", + "FULL", + "8b47ddec-4741-11e9-bace-892f501e2351", + "TRUE", + "", + "video", + "00.00.00", + "", + "", + "clips/egg.mp4" + ], + [ + "Egg, interview", + "CAM", + "8b47dded-4741-11e9-bace-892f501e2351", + "FALSE", + "With me in the studio, I've got Abe Bergsson, who works at the local farm. [..]", + "camera", + "00.00.00", + "0.01.20", + "0.01.20", + "", + "", + "kam2" + ], + ["", "", "", "FALSE", "", "graphic", "", "", "", "gfx/name_left", "", "Abe Bergsson"], + ["", "", "", "FALSE", "How did you discover the egg?"], + ["", "", "8b47ddf0-4741-11e9-bace-892f501e2351", "FALSE", "What is the thing that hatched?"], + [ + "", + "", + "8b47ddf1-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "video", + "", + "", + "0.00.15", + "clips/egghatching.mp4" + ], + ["", "SECTION", "8b47ddf2-4741-11e9-bace-892f501e2351", "FALSE"], + ["Wow!!", "FULL", "8b47ddf3-4741-11e9-bace-892f501e2351", "FALSE"], + ["Ducks", "SECTION", "8b47ddf4-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Ducks, intro", + "CAM", + "", + "FALSE", + "We've got our reporter, Charlie Deidriksson with us out by the pond, what can you tell us, Charlie?", + "camera", + "00.00.00", + "", + "", + "", + "", + "kam1" + ], + ["", "", "", "FALSE"], + ["", "", "8b47ddf7-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Split", + "SPLIT", + "8b47ddf8-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "split", + "", + "", + "", + "", + "", + "kam1", + "", + "remote1" + ], + [ + "", + "", + "8b47ddf9-4741-11e9-bace-892f501e2351", + "FALSE", + "Thank you, Charlie. For more updates on this story, please visit our homepage, www.supernews.com", + "camera", + "00.00.00", + "", + "", + "", + "", + "kam2" + ], + [ + "", + "", + "8b47ddfa-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.02", + "", + "0.00.04", + "gfx/lower_right", + "", + "www.supernews.com" + ], + ["Forest", "SECTION", "8b47ddfb-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Forest, intro", + "CAM", + "8b47ddfc-4741-11e9-bace-892f501e2351", + "FALSE", + "In the forest behind the old mill, there's been quite a disturbance yesterday...", + "camera", + "00.00.00", + "", + "", + "", + "", + "kam1" + ], + [ + "Forest", + "FULL", + "8b47ddfd-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "video", + "00.00.00", + "0.00.52", + "0.00.52", + "clips/deer.mp4" + ], + [ + "", + "", + "8b47ddfe-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.02", + "", + "0.00.07", + "gfx/name_left", + "", + "Reporter: Elon Fergusson" + ], + [ + "", + "", + "8b47ddff-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "graphic", + "00.00.46", + "", + "0.00.03", + "gfx/name_left", + "", + "Editing: George Hoff" + ], + ["Sofie", "SECTION", "8b47de00-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Sofie, intro", + "CAM", + "8b47de01-4741-11e9-bace-892f501e2351", + "FALSE", + "The biggest event of the century, the TV Automation system \"Sofie\" opens it's doors to the public. The founders, Superfly.tv thinks it's going to be an industry-changing solution.", + "camera", + "", + "", + "", + "", + "", + "kam1" + ], + [ + "Sofie", + "FULL", + "8b47de02-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "video", + "00.00.00", + "0.00.35", + "0.00.35", + "clips/sofie.mp4" + ], + [ + "Sofie, guests", + "CAM", + "8b47de03-4741-11e9-bace-892f501e2351", + "FALSE", + "The studio is full of guests, we've got", + "camera", + "", + "0.02.13", + "0.02.13", + "", + "", + "kam3" + ], + [ + "", + "", + "8b47de04-4741-11e9-bace-892f501e2351", + "FALSE", + "Jonas Hummelstrand", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Jonas Hummelstrand" + ], + [ + "", + "", + "8b47de05-4741-11e9-bace-892f501e2351", + "FALSE", + "Jesper Stærkær", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Jesper Stærkær" + ], + [ + "", + "", + "8b47de06-4741-11e9-bace-892f501e2351", + "FALSE", + "Johan Nyman", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Johan Nyman" + ], + [ + "", + "", + "8b47de07-4741-11e9-bace-892f501e2351", + "FALSE", + "Jan Starzak", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Jan Starzak" + ], + [ + "", + "", + "8b47de08-4741-11e9-bace-892f501e2351", + "FALSE", + "Julian Waller", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Julian Waller" + ], + [ + "", + "", + "8b47de09-4741-11e9-bace-892f501e2351", + "FALSE", + "Balte de Wit", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Balte de Wit" + ], + [ + "", + "", + "8b47de0a-4741-11e9-bace-892f501e2351", + "FALSE", + "Stig Roar Aftret", + "graphic", + "", + "", + "", + "gfx/name_right", + "", + "Stig Roar Aftret" + ], + ["Weather", "SECTION", "8b47de0b-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Weather, intro", + "CAM", + "8b47de0c-4741-11e9-bace-892f501e2351", + "FALSE", + "...And now it's time for the weather.", + "camera", + "", + "", + "", + "", + "", + "kam1" + ], + [ + "Weather", + "LS", + "8b47de0d-4741-11e9-bace-892f501e2351", + "FALSE", + "Today it's gonna be cloudy with a chance of meatballs. Later tonight, NASA has issued a class 3 warning for falling debris, so be safe out there.", + "video", + "00.00.00", + "0.00.35", + "0.00.35", + "clips/sofie.mp4" + ], + ["Outro", "SECTION", "8b47de0e-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Outro", + "CAM", + "8b47de0f-4741-11e9-bace-892f501e2351", + "FALSE", + "That's all from us guys, see you all next time! Don't forget to like and subscribe!", + "camera", + "", + "", + "", + "", + "", + "kam1" + ], + [ + "Outro", + "FULL", + "8b47de10-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "camera", + "00.00.00", + "0.00.15", + "0.00.15", + "", + "", + "kam3" + ], + ["", "", "8b47de11-4741-11e9-bace-892f501e2351", "FALSE", "", "overlay", "00.00.00", "", "", "std/outro.mp4"], + [ + "", + "", + "8b47de12-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "lights", + "00.00.00", + "", + "", + "", + "", + "studio", + "", + "0", + "", + "fade" + ], + ["Brexit", "SECTION", "8b4804f0-4741-11e9-bace-892f501e2351", "FALSE"], + [ + "Brexit, intro", + "CAM", + "8b4804f1-4741-11e9-bace-892f501e2351", + "FALSE", + "Nobody knows what's going on! Run for the hills! Run away! Run away!", + "camera", + "", + "", + "", + "", + "", + "kam1" + ], + [ + "", + "", + "8b4804f2-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "video", + "00.00.00", + "", + "0.00.35", + "clips/sofie.mp4", + "", + "screen 1" + ], + [ + "", + "", + "8b4804f3-4741-11e9-bace-892f501e2351", + "FALSE", + "", + "video", + "00.00.00", + "", + "0.00.07", + "std/vinjett.mp4", + "", + "screen 2" + ], + ["", "", "", "FALSE", "", "video", "00.00.00", "", "0.00.35", "clips/sofie.mp4", "", "screen 3"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"], + ["", "", "", "FALSE"] + ] + } + ] + }, + "headers": { + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"", + "cache-control": "private", + "connection": "close", + "content-encoding": "gzip", + "content-type": "application/json; charset=UTF-8", + "date": "Tue, 23 Aug 2022 11:28:38 GMT", + "server": "ESF", + "transfer-encoding": "chunked", + "vary": "Origin, X-Origin, Referer", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-xss-protection": "0" + }, + "status": 200, + "statusText": "OK", + "request": { + "responseURL": "https://sheets.googleapis.com/v4/spreadsheets/1vp_Gniaf-rK0RkdgxryCcldEAv2547srXcDbj0Us4pA/values:batchGet?ranges=rundown-meta&ranges=rundown-segments&ranges=rundown-parts&ranges=rundown-details" + } +} diff --git a/src/configManifest.ts b/src/configManifest.ts index 3c98014..89ca286 100644 --- a/src/configManifest.ts +++ b/src/configManifest.ts @@ -14,8 +14,7 @@ export const SPREADSHEET_DEVICE_CONFIG_MANIFEST: DeviceConfigManifest = { }, ], deviceOAuthFlow: { - credentialsHelp: - 'Go to the url below and click on the "Enable the Drive API button". Then click on "Download Client configuration", save the credentials.json file and upload it here.', - credentialsURL: 'https://developers.google.com/drive/api/v3/quickstart/nodejs', + credentialsHelp: 'Upload Google Account credentials. For more instructions, visit spreadsheet-gateway repo README.', + credentialsURL: 'https://github.com/SuperFlyTV/spreadsheet-gateway', }, } diff --git a/src/connector.ts b/src/connector.ts index 58b32f1..aa5f4ac 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -1,7 +1,7 @@ import { SpreadsheetHandler, SpreadsheetConfig } from './spreadsheetHandler' import { CoreHandler, CoreConfig } from './coreHandler' -import * as winston from 'winston' import { Process } from './process' +import { logger } from './logger' export interface Config { process: ProcessConfig @@ -23,47 +23,42 @@ export class Connector { private spreadsheetHandler: SpreadsheetHandler private coreHandler: CoreHandler private _config: Config - private _logger: winston.Logger private _process: Process - constructor(logger: winston.Logger, config: Config) { - this._logger = logger + constructor(config: Config) { this._config = config - this._process = new Process(this._logger) - this.coreHandler = new CoreHandler(this._logger, this._config.device) - this.spreadsheetHandler = new SpreadsheetHandler(this._logger, this._config, this.coreHandler) + this._process = new Process() + this.coreHandler = new CoreHandler(this._config.device) + this.spreadsheetHandler = new SpreadsheetHandler(this._config, this.coreHandler) } async init(): Promise { return Promise.resolve() .then(() => { - this._logger.info('Initializing Process...') + logger.info('Initializing Process...') return this.initProcess() }) .then(async () => { - this._logger.info('Process initialized') - this._logger.info('Initializing Core...') + logger.info('Process initialized') + logger.info('Initializing Core...') return this.initCore() }) .then(async () => { - this._logger.info('Initializing Spreadsheet-monitor...') + logger.info('Initializing Spreadsheet-monitor...') return this.initSpreadsheetHandler() }) .then(() => { - this._logger.info('Initialization done') + logger.info('Initialization done') return }) .catch((e) => { - this._logger.error('Error during initialization:', e, e.stack) - // this._logger.error(e) - // this._logger.error(e.stack) - - this._logger.info('Shutting down in 10 seconds!') + logger.error('Error during initialization:', e, e.stack) + logger.info('Shutting down in 10 seconds!') try { - this.dispose().catch((e) => this._logger.error(e)) + this.dispose().catch((e) => logger.error(e)) } catch (e) { - this._logger.error(e) + logger.error(e) } setTimeout(() => { diff --git a/src/coreHandler.ts b/src/coreHandler.ts index 06e78e2..2baa190 100644 --- a/src/coreHandler.ts +++ b/src/coreHandler.ts @@ -1,21 +1,23 @@ -import { - CoreConnection, - CoreOptions, - PeripheralDeviceAPI as P, - DDPConnectorOptions, -} from '@sofie-automation/server-core-integration' -import * as winston from 'winston' +import { CoreConnection, CoreOptions, DDPConnectorOptions } from '@sofie-automation/server-core-integration' import * as fs from 'fs' import { Process } from './process' -import * as _ from 'underscore' - import { DeviceConfig } from './connector' import { MediaDict } from './classes/media' import { IOutputLayer } from '@sofie-automation/blueprints-integration' +import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' import { SPREADSHEET_DEVICE_CONFIG_MANIFEST } from './configManifest' import { SpreadsheetHandler } from './spreadsheetHandler' -// import { STATUS_CODES } from 'http' +import { logger } from './logger' +import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' +import { + PeripheralDeviceCategory, + PeripheralDeviceType, + PERIPHERAL_SUBTYPE_PROCESS, +} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' + export interface PeripheralDeviceCommand { _id: string @@ -42,7 +44,6 @@ export class CoreHandler { public core: CoreConnection public doReceiveAuthToken?: (authToken: string) => Promise - private logger: winston.Logger private _observers: Array = [] private _onConnected?: () => any private _subscriptions: Array = [] @@ -56,8 +57,7 @@ export class CoreHandler { private _workflow: WorkflowType private _spreadsheetHandler: SpreadsheetHandler | undefined - constructor(logger: winston.Logger, deviceOptions: DeviceConfig) { - this.logger = logger + constructor(deviceOptions: DeviceConfig) { this._workflow = 'ATEM' this.core = new CoreConnection(this.getCoreConnectionOptions(deviceOptions, 'Spreadsheet Gateway')) } @@ -75,14 +75,14 @@ export class CoreHandler { this._spreadsheetHandler = spreadsheetHandler this.core.onConnected(() => { - this.logger.info('Core Connected!') + logger.info('Core Connected!') if (this._isInitialized) this.onConnectionRestored() }) this.core.onDisconnected(() => { - this.logger.info('Core Disconnected!') + logger.info('Core Disconnected!') }) this.core.onError((err) => { - this.logger.error('Core Error: ' + (err.message || err.toString() || err)) + logger.error('Core Error: ' + (err.toString() || err)) }) const ddpConfig: DDPConnectorOptions = { @@ -99,10 +99,10 @@ export class CoreHandler { .then((_id: string) => { this.core .setStatus({ - statusCode: P.StatusCode.UNKNOWN, + statusCode: StatusCode.UNKNOWN, messages: ['Starting up'], }) - .catch((e) => this.logger.warn('Error when setting status:' + e)) + .catch((e) => logger.warn('Error when setting status:' + e)) // nothing }) .then(async () => { @@ -112,10 +112,11 @@ export class CoreHandler { this._isInitialized = true }) } + async dispose(): Promise { return this.core .setStatus({ - statusCode: P.StatusCode.FATAL, + statusCode: StatusCode.FATAL, messages: ['Shutting down'], }) .then(async () => { @@ -125,40 +126,41 @@ export class CoreHandler { // nothing }) } - setStatus(statusCode: P.StatusCode, messages: string[]): void { + + setStatus(statusCode: StatusCode, messages: string[]): void { this.core .setStatus({ statusCode: statusCode, messages: messages, }) - .catch((e) => this.logger.warn('Error when setting status:' + e)) + .catch((e) => logger.warn('Error when setting status:' + e)) } + getCoreConnectionOptions(deviceOptions: DeviceConfig, name: string): CoreOptions { let credentials: { - deviceId: string + deviceId: PeripheralDeviceId deviceToken: string } if (deviceOptions.deviceId && deviceOptions.deviceToken) { credentials = { - deviceId: deviceOptions.deviceId, + deviceId: protectString(deviceOptions.deviceId), deviceToken: deviceOptions.deviceToken, } - } else if (deviceOptions.deviceId) { - this.logger.warn('Token not set, only id! This might be unsecure!') + } else { + logger.warn('Token not set, only id! This might be unsecure!') credentials = { - deviceId: deviceOptions.deviceId + name, + deviceId: protectString(deviceOptions.deviceId + name), deviceToken: 'unsecureToken', } - } else { - credentials = CoreConnection.getCredentials(name.replace(/ /g, '')) } + const options: CoreOptions = { ...credentials, - deviceCategory: P.DeviceCategory.INGEST, - deviceType: P.DeviceType.SPREADSHEET, - deviceSubType: P.SUBTYPE_PROCESS, + deviceCategory: PeripheralDeviceCategory.INGEST, + deviceType: PeripheralDeviceType.SPREADSHEET, + deviceSubType: PERIPHERAL_SUBTYPE_PROCESS, deviceName: name, watchDog: this._coreConfig ? this._coreConfig.watchdog : true, @@ -168,24 +170,27 @@ export class CoreHandler { options.versions = this._getVersions() return options } + onConnectionRestored(): void { this.setupSubscriptionsAndObservers().catch((e) => { - this.logger.error(e) + logger.error(e) }) if (this._onConnected) this._onConnected() // this._coreMosHandlers.forEach((cmh: CoreMosDeviceHandler) => { // cmh.setupSubscriptionsAndObservers() // }) } + onConnected(fcn: () => any): void { this._onConnected = fcn } + /** * Subscribes to events in the core. */ async setupSubscriptionsAndObservers(): Promise { if (this._observers.length) { - this.logger.info('Core: Clearing observers..') + logger.info('Core: Clearing observers..') this._observers.forEach((obs) => { obs.stop() }) @@ -193,7 +198,8 @@ export class CoreHandler { } this._subscriptions = [] - this.logger.info('Core: Setting up subscriptions for ' + this.core.deviceId + '..') + logger.info('Core: Setting up subscriptions for ' + this.core.deviceId + '..') + this._spreadsheetHandler?.setDeviceId(unprotectString(this.core.deviceId)) return Promise.all([ this.core.autoSubscribe('peripheralDevices', { _id: this.core.deviceId, @@ -226,6 +232,7 @@ export class CoreHandler { return }) } + /** * Subscribes to the 'showStyleBases' collection. * @param studioId The studio the showstyles belong to. @@ -246,7 +253,7 @@ export class CoreHandler { async executeFunction(cmd: PeripheralDeviceCommand): Promise { if (cmd) { if (this._executedFunctions[cmd._id]) return // prevent it from running multiple times - this.logger.debug(cmd.functionName, cmd.args) + logger.debug(cmd.functionName, cmd.args) this._executedFunctions[cmd._id] = true let success = false @@ -255,54 +262,72 @@ export class CoreHandler { case 'triggerReloadRundown': { const reloadRundownResult = await Promise.resolve(this.triggerReloadRundown(cmd.args[0])) success = true - await this.core.callMethod(P.methods.functionReply, [cmd._id, null, reloadRundownResult]) + await this.core.callMethod(PeripheralDeviceAPIMethods.functionReply, [cmd._id, null, reloadRundownResult]) break } case 'pingResponse': { const pingResponseResult = await Promise.resolve(this.pingResponse(cmd.args[0])) success = true - await this.core.callMethod(P.methods.functionReply, [cmd._id, null, pingResponseResult]) + await this.core.callMethod(PeripheralDeviceAPIMethods.functionReply, [cmd._id, null, pingResponseResult]) break } case 'retireExecuteFunction': { const retireExecuteFunctionResult = await Promise.resolve(this.retireExecuteFunction(cmd.args[0])) success = true - await this.core.callMethod(P.methods.functionReply, [cmd._id, null, retireExecuteFunctionResult]) + await this.core.callMethod(PeripheralDeviceAPIMethods.functionReply, [ + cmd._id, + null, + retireExecuteFunctionResult, + ]) break } case 'killProcess': { const killProcessFunctionResult = await Promise.resolve(this.killProcess(cmd.args[0])) success = true - await this.core.callMethod(P.methods.functionReply, [cmd._id, null, killProcessFunctionResult]) + await this.core.callMethod(PeripheralDeviceAPIMethods.functionReply, [ + cmd._id, + null, + killProcessFunctionResult, + ]) break } case 'getSnapshot': { const getSnapshotResult = await Promise.resolve(this.getSnapshot()) success = true - await this.core.callMethod(P.methods.functionReply, [cmd._id, null, getSnapshotResult]) + await this.core.callMethod(PeripheralDeviceAPIMethods.functionReply, [cmd._id, null, getSnapshotResult]) + break + } + case 'receiveAuthToken': { + const authTokenResult = await Promise.resolve(this.receiveAuthToken(cmd.args[0])) + success = true + await this.core.callMethod(PeripheralDeviceAPIMethods.functionReply, [cmd._id, null, authTokenResult]) break } default: throw Error('Function "' + cmd.functionName + '" not found!') } } catch (err) { - this.logger.error(`executeFunction error ${success ? 'during execution' : 'on reply'}`, err, (err as any).stack) + logger.error(`executeFunction error ${success ? 'during execution' : 'on reply'}`, err, (err as any).stack) if (!success) { await this.core - .callMethod(P.methods.functionReply, [cmd._id, (err as any).toString(), null]) - .catch((e) => this.logger.error('executeFunction reply error after execution failure', e, e.stack)) + .callMethod(PeripheralDeviceAPIMethods.functionReply, [cmd._id, (err as any).toString(), null]) + .catch((e) => logger.error('executeFunction reply error after execution failure', e, e.stack)) } } } } + retireExecuteFunction(cmdId: string): void { delete this._executedFunctions[cmdId] } - async receiveAuthToken(authToken: string): Promise { - console.log('received AuthToken', authToken) + async receiveAuthToken(authToken: string): Promise { if (this.doReceiveAuthToken) { - return this.doReceiveAuthToken(authToken) + try { + await this.doReceiveAuthToken(authToken) + } catch (e) { + this.setStatus(StatusCode.BAD, [`Failed to authenticate`, String(e)]) + } } else { throw new Error('doReceiveAuthToken not set!') } @@ -325,7 +350,7 @@ export class CoreHandler { if (!cmds) throw Error('"peripheralDeviceCommands" collection not found!') const cmd = cmds.findOne(id) as PeripheralDeviceCommand if (!cmd) throw Error('PeripheralCommand "' + id + '" not found!') - if (cmd.deviceId === this.core.deviceId) { + if (cmd.deviceId === unprotectString(this.core.deviceId)) { void this.executeFunction(cmd) } } @@ -342,11 +367,12 @@ export class CoreHandler { if (!cmds) throw Error('"peripheralDeviceCommands" collection not found!') cmds.find({}).forEach((cmd0) => { const cmd = cmd0 as PeripheralDeviceCommand - if (cmd.deviceId === this.core.deviceId) { + if (cmd.deviceId === unprotectString(this.core.deviceId)) { void this.executeFunction(cmd) } }) } + /** * Subscribes to changes to media objects to populate spreadsheet data. */ @@ -423,6 +449,7 @@ export class CoreHandler { constructMediaObject(file) }) } + setupObserverForShowStyleBases(): void { const observerStyles = this.core.observe('showStyleBases') this.killProcess(false) @@ -453,19 +480,24 @@ export class CoreHandler { } }) - const settings = studio['config'] as Array<{ _id: string; value: string | boolean }> + const settings: { [id: string]: string | boolean } | undefined = studio['blueprintConfig'] if (!settings) { this._workflow = 'ATEM' // default } else { - settings.forEach((setting) => { - if (setting._id.match(/^vmix$/i)) { - if (setting.value === true) { + for (const [id, value] of Object.entries(settings)) { + if (id.match(/^vmix$/i)) { + if (value === true) { this._workflow = 'VMIX' } else { this._workflow = 'ATEM' } } - }) + } + } + + const sofieUrl = studio['settings']['sofieUrl'] as string | undefined + if (sofieUrl) { + this._spreadsheetHandler?.setCoreUrl(new URL(sofieUrl)) } } } @@ -480,6 +512,7 @@ export class CoreHandler { addedChanged() } + /** * Subscribes to changes to the device to get its associated studio ID. */ @@ -503,11 +536,11 @@ export class CoreHandler { if (this._studioId) { // Subscribe to mediaObjects collection. this.setupSubscriptionForMediaObjects(this._studioId).catch((er) => { - this.logger.error(er) + logger.error(er) }) this.setupSubscriptionForShowStyleBases().catch((er) => { - this.logger.error(er) + logger.error(er) }) } } @@ -523,11 +556,12 @@ export class CoreHandler { addedChanged(id) } - addedChanged(this.core.deviceId) + addedChanged(unprotectString(this.core.deviceId)) } + killProcess(actually: boolean): boolean { if (actually) { - this.logger.info('KillProcess command received, shutting down in 1000ms!') + logger.info('KillProcess command received, shutting down in 1000ms!') setTimeout(() => { // eslint-disable-next-line no-process-exit process.exit(0) @@ -536,17 +570,21 @@ export class CoreHandler { } return false } + triggerReloadRundown(rundownId: string): void { this._spreadsheetHandler?.triggerReloadRundown(rundownId) } + pingResponse(message: string): boolean { this.core.setPingResponse(message) return true } + getSnapshot(): any { - this.logger.info('getSnapshot') + logger.info('getSnapshot') return {} // TODO: send some snapshot data? } + private _getVersions() { const versions: { [packageName: string]: string } = {} @@ -560,7 +598,7 @@ export class CoreHandler { ] try { const nodeModulesDirectories = fs.readdirSync('node_modules') - _.each(nodeModulesDirectories, (dir) => { + for (const dir of nodeModulesDirectories) { try { if (dirNames.indexOf(dir) !== -1) { let file = 'node_modules/' + dir + '/package.json' @@ -569,11 +607,11 @@ export class CoreHandler { versions[dir] = json.version || 'N/A' } } catch (e) { - this.logger.error(e) + logger.error(e) } - }) + } } catch (e) { - this.logger.error(e) + logger.error(e) } return versions } diff --git a/src/diffRundowns.ts b/src/diffRundowns.ts new file mode 100644 index 0000000..0bb0c65 --- /dev/null +++ b/src/diffRundowns.ts @@ -0,0 +1,186 @@ +import { isDeepStrictEqual } from 'util' +import { SheetRundown } from './classes/Rundown' + +export enum RundownChangeType { + RundownCreate = 'rundown_create', + RundownDelete = 'rundown_delete', + RundownUpdate = 'rundown_update', + SegmentCreate = 'segment_create', + SegmentDelete = 'segment_delete', + SegmentUpdate = 'segment_update', + PartCreate = 'part_create', + PartDelete = 'part_delete', + PartUpdate = 'part_update', +} + +interface RundownChangeBase { + type: RundownChangeType + rundownId: string +} + +interface RundownChangeRundownCreate extends RundownChangeBase { + type: RundownChangeType.RundownCreate +} + +interface RundownChangeRundownDelete extends RundownChangeBase { + type: RundownChangeType.RundownDelete +} + +interface RundownChangeRundownUpdate extends RundownChangeBase { + type: RundownChangeType.RundownUpdate +} + +interface RundownChangeSegmentCreate extends RundownChangeBase { + type: RundownChangeType.SegmentCreate + segmentId: string +} + +interface RundownChangeSegmentDelete extends RundownChangeBase { + type: RundownChangeType.SegmentDelete + segmentId: string +} + +interface RundownChangeSegmentUpdate extends RundownChangeBase { + type: RundownChangeType.SegmentUpdate + segmentId: string +} + +interface RundownChangePartCreate extends RundownChangeBase { + type: RundownChangeType.PartCreate + segmentId: string + partId: string +} + +interface RundownChangePartDelete extends RundownChangeBase { + type: RundownChangeType.PartDelete + segmentId: string + partId: string +} + +interface RundownChangePartUpdate extends RundownChangeBase { + type: RundownChangeType.PartUpdate + segmentId: string + partId: string +} + +export type RundownChange = + | RundownChangeRundownCreate + | RundownChangeRundownDelete + | RundownChangeRundownUpdate + | RundownChangeSegmentCreate + | RundownChangeSegmentDelete + | RundownChangeSegmentUpdate + | RundownChangePartCreate + | RundownChangePartDelete + | RundownChangePartUpdate + +export function diffRundowns(oldRundown: SheetRundown | null, newRundown: SheetRundown | null): RundownChange[] { + const changes: RundownChange[] = [] + + if (oldRundown === null && newRundown === null) { + return [] + } + + if (oldRundown === null && newRundown !== null) { + return [ + { + type: RundownChangeType.RundownCreate, + rundownId: newRundown.externalId, + }, + ] + } + + if (oldRundown !== null && newRundown === null) { + return [ + { + type: RundownChangeType.RundownDelete, + rundownId: oldRundown.externalId, + }, + ] + } + + // Not possible but typescript needs some help here + if (oldRundown === null || newRundown === null) { + return [] + } + + if (!isDeepStrictEqual(oldRundown.serialize(), newRundown.serialize())) { + changes.push({ + type: RundownChangeType.RundownUpdate, + rundownId: newRundown.externalId, + }) + } + + const rundownId = newRundown.externalId + + const deletedSegments = oldRundown.segments.filter( + (oldSegment) => + newRundown.segments.find((newSegment) => newSegment.externalId === oldSegment.externalId) === undefined + ) + + for (const deletedSegment of deletedSegments) { + changes.push({ + type: RundownChangeType.SegmentDelete, + rundownId, + segmentId: deletedSegment.externalId, + }) + } + + for (const newSegment of newRundown.segments) { + const oldSegment = oldRundown.segments.find((s) => s.externalId === newSegment.externalId) + if (!oldSegment) { + changes.push({ + type: RundownChangeType.SegmentCreate, + rundownId, + segmentId: newSegment.externalId, + }) + continue + } + + if (!isDeepStrictEqual(newSegment.serialize(), oldSegment.serialize())) { + changes.push({ + type: RundownChangeType.SegmentUpdate, + rundownId, + segmentId: newSegment.externalId, + }) + continue + } + + const deletedParts = oldSegment.parts.filter( + (oldPart) => newSegment.parts.find((newPart) => newPart.externalId === oldPart.externalId) === undefined + ) + + for (const part of deletedParts) { + changes.push({ + type: RundownChangeType.PartDelete, + rundownId, + segmentId: newSegment.externalId, + partId: part.externalId, + }) + } + + for (const newPart of newSegment.parts) { + const oldPart = oldSegment.parts.find((p) => p.externalId === newPart.externalId) + if (!oldPart) { + changes.push({ + type: RundownChangeType.PartCreate, + rundownId, + segmentId: newSegment.externalId, + partId: newPart.externalId, + }) + continue + } + + if (!isDeepStrictEqual(newPart.serialize(), oldPart.serialize())) { + changes.push({ + type: RundownChangeType.PartUpdate, + rundownId, + segmentId: newSegment.externalId, + partId: newPart.externalId, + }) + } + } + } + + return changes +} diff --git a/src/index.ts b/src/index.ts index e123f34..8a8487b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ -import { Connector, Config } from './connector' -import * as winston from 'winston' -import _ = require('underscore') +import { Config, Connector } from './connector' +import { logger, addConsoleLogging, addFileLogging } from './logger' // CLI arguments / Environment variables -------------- let host: string = process.env.CORE_HOST || '127.0.0.1' @@ -10,7 +9,7 @@ let deviceId: string = process.env.DEVICE_ID || '' let deviceToken: string = process.env.DEVICE_TOKEN || '' let disableWatchdog: boolean = process.env.DISABLE_WATCHDOG === '1' || false let unsafeSSL: boolean = process.env.UNSAFE_SSL === '1' || false -const certs: string[] = (process.env.CERTIFICATES || '').split(';') || [] +const certs: string[] = process.env.CERTIFICATES?.split(';') || [] let debug = false let printHelp = false @@ -34,7 +33,7 @@ process.argv.forEach((val) => { } else if ((val + ' ').match(/-h(elp)? /i)) { printHelp = true } else if (prevProcessArg.match(/-certificates/i)) { - certs.push(val) + if (val) certs.push(val) nextPrevProcessArg = prevProcessArg // so that we can get multiple certificates // arguments with no options: @@ -68,56 +67,13 @@ CLI ENV } // Setup logging -------------------------------------- -const logger = winston.createLogger({}) - if (logPath) { // Log json to file, human-readable to console console.log('Logging to', logPath) - logger.add( - new winston.transports.Console({ - level: 'verbose', - handleExceptions: true, - format: winston.format.simple(), - }) - ) - logger.add( - new winston.transports.File({ - level: 'debug', - handleExceptions: true, - format: winston.format.json({ circularValue: null }), - filename: logPath, - }) - ) - // Hijack console.log: - const orgConsoleLog = console.log - console.log = function (...args: any[]) { - if (args.length >= 1) { - try { - logger.debug(args.join(' ')) - } catch (e) { - orgConsoleLog('CATCH') - orgConsoleLog(...args) - throw e - } - orgConsoleLog(...args) - } - } + addFileLogging(logPath) } else { console.log('Logging to Console') - // Log json to console - logger.add( - new winston.transports.Console({ - // level: 'verbose', - handleExceptions: true, - format: winston.format.json({ circularValue: null }), - }) - ) - // Hijack console.log: - console.log = function (...args: any[]) { - if (args.length >= 1) { - logger.debug(args.join(' ')) - } - } + addConsoleLogging() } // Because the default NodeJS-handler sucks and wont display error properly @@ -128,7 +84,6 @@ process.on('warning', (e: any) => { logger.warn('Unhandled warning:', e, e.reason || e.message, e.stack) }) -logger.info('------------------------------------------------------------------') logger.info('-----------------------------------') logger.info('Statup options:') @@ -148,7 +103,7 @@ logger.info('-----------------------------------') const config: Config = { process: { unsafeSSL: unsafeSSL, - certificates: _.compact(certs), + certificates: certs, }, device: { deviceId: deviceId, @@ -162,15 +117,8 @@ const config: Config = { spreadsheet: {}, } -const c = new Connector(logger, config) +const c = new Connector(config) -logger.info('Core: ' + config.core.host + ':' + config.core.port) -// logger.info('My Mos id: ' + config.mos.self.mosID) -// config.mos.devices.forEach((device) => { -// if (device.primary) logger.info('Mos Primary: ' + device.primary.host) -// if (device.secondary) logger.info('Mos Secondary: ' + device.secondary.host) -// }) -logger.info('------------------------------------------------------------------') +logger.info('Core: ' + config.core.host + ':' + config.core.port + ' ') +logger.info('-----------------------------------') c.init().catch(logger.error) - -// @todo: remove this line of comment diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..65dc2ae --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,65 @@ +import * as winston from 'winston' + +const logger = winston.createLogger({}) + +export function addConsoleLogging(): void { + // Log json to console + logger.add( + new winston.transports.Console({ + // level: 'verbose', + handleExceptions: true, + format: winston.format.json({ circularValue: null }), + }) + ) + // Hijack console.log: + console.log = function (...args: any[]) { + if (args.length >= 1) { + logger.debug(args.join(' ')) + } + } +} + +export function addTestLogging(): void { + logger.add( + new winston.transports.Console({ + // level: 'verbose', + handleExceptions: true, + format: winston.format.json({ circularValue: null }), + silent: true, + }) + ) +} + +export function addFileLogging(logPath: string): void { + logger.add( + new winston.transports.Console({ + level: 'verbose', + handleExceptions: true, + format: winston.format.simple(), + }) + ) + logger.add( + new winston.transports.File({ + level: 'debug', + handleExceptions: true, + format: winston.format.json({ circularValue: null }), + filename: logPath, + }) + ) + // Hijack console.log: + const orgConsoleLog = console.log + console.log = function (...args: any[]) { + if (args.length >= 1) { + try { + logger.debug(args.join(' ')) + } catch (e) { + orgConsoleLog('CATCH') + orgConsoleLog(...args) + throw e + } + orgConsoleLog(...args) + } + } +} + +export { logger } diff --git a/src/mutate.ts b/src/mutate.ts index 75d05a7..6874d8a 100644 --- a/src/mutate.ts +++ b/src/mutate.ts @@ -1,4 +1,3 @@ -import * as _ from 'underscore' import { SheetRundown } from './classes/Rundown' import { IngestRundown, IngestSegment, IngestPart } from '@sofie-automation/blueprints-integration' import { SheetSegment } from './classes/Segment' @@ -6,21 +5,23 @@ import { SheetPart } from './classes/Part' /** These are temorary mutation functions to convert sheet types to ingest types */ export function mutateRundown(rundown: SheetRundown): IngestRundown { + const { segments, ...payload } = rundown return { externalId: rundown.externalId, name: rundown.name, type: 'external', - payload: _.omit(rundown, 'segments'), - segments: _.values(rundown.segments || {}).map(mutateSegment), + payload: payload, + segments: segments.map(mutateSegment), } } export function mutateSegment(segment: SheetSegment): IngestSegment { + const { parts, ...payload } = segment return { externalId: segment.externalId, name: segment.name, rank: segment.rank, - payload: _.omit(segment, 'parts'), - parts: _.values(segment.parts || {}).map(mutatePart), + payload, + parts: parts.map(mutatePart), } } export function mutatePart(part: SheetPart): IngestPart { diff --git a/src/process.ts b/src/process.ts index 600fe5e..693402c 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,33 +1,27 @@ -import * as winston from 'winston' -import _ = require('underscore') import * as fs from 'fs' import { ProcessConfig } from './connector' +import { logger } from './logger' export class Process { - logger: winston.Logger - public certificates: Buffer[] = [] - constructor(logger: winston.Logger) { - this.logger = logger - } init(processConfig: ProcessConfig): void { if (processConfig.unsafeSSL) { - this.logger.info('Disabling NODE_TLS_REJECT_UNAUTHORIZED, be sure to ONLY DO THIS ON A LOCAL NETWORK!') + logger.info('Disabling NODE_TLS_REJECT_UNAUTHORIZED, be sure to ONLY DO THIS ON A LOCAL NETWORK!') process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' } else { // var rootCas = SSLRootCAs.create() } if (processConfig.certificates.length) { - this.logger.info(`Loading certificates...`) - _.each(processConfig.certificates, (certificate) => { + logger.info(`Loading certificates...`) + for (const certificate of processConfig.certificates) { try { this.certificates.push(fs.readFileSync(certificate)) - this.logger.info(`Using certificate "${certificate}"`) + logger.info(`Using certificate "${certificate}"`) } catch (error) { - this.logger.error(`Error loading certificate "${certificate}"`, error) + logger.error(`Error loading certificate "${certificate}"`, error) } - }) + } } } } diff --git a/src/spreadsheetHandler.ts b/src/spreadsheetHandler.ts index fdedae3..9c3eef7 100644 --- a/src/spreadsheetHandler.ts +++ b/src/spreadsheetHandler.ts @@ -1,17 +1,19 @@ -import * as winston from 'winston' -import { CollectionObj, PeripheralDeviceAPI as P } from '@sofie-automation/server-core-integration' -import { google } from 'googleapis' -import { Auth } from 'googleapis' - -import { CoreHandler } from './coreHandler' +import { CollectionObj } from '@sofie-automation/server-core-integration' +import { StatusCode } from '@sofie-automation/shared-lib/dist/lib/status' +import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' +import { Auth, google } from 'googleapis' import { RunningOrderWatcher } from './classes/RunningOrderWatcher' -import { mutateRundown, mutateSegment, mutatePart } from './mutate' +import { CoreHandler } from './coreHandler' +import { logger } from './logger' +import { mutatePart, mutateRundown, mutateSegment } from './mutate' +import { checkErrorType, getErrorMsg } from './util' // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SpreadsheetConfig { // Todo: add settings here? // self: IConnectionConfig } + export interface SpreadsheetDeviceSettings { /** Path / Name to the Drive folder */ folderPath: string @@ -21,6 +23,7 @@ export interface SpreadsheetDeviceSettings { secretCredentials: boolean secretAccessToken: boolean } + export interface SpreadsheetDeviceSecretSettings { credentials?: Credentials accessToken?: AccessToken @@ -48,50 +51,40 @@ export interface AccessToken { const ACCESS_SCOPES = ['https://www.googleapis.com/auth/drive.readonly', 'https://www.googleapis.com/auth/spreadsheets'] export class SpreadsheetHandler { - public options: SpreadsheetConfig + // public options: SpreadsheetConfig public debugLogging = false private spreadsheetWatcher?: RunningOrderWatcher - // private allMosDevices: {[id: string]: IMOSDevice} = {} - // private _ownMosDevices: {[deviceId: string]: MosDevice} = {} private _currentOAuth2Client: Auth.OAuth2Client | null = null - private _currentOAuth2ClientAuthorized = false - private _logger: winston.Logger private _disposed = false private _settings?: SpreadsheetDeviceSettings private _coreHandler: CoreHandler private _observers: Array = [] - private _triggerupdateDevicesTimeout: any = null + private _triggerUpdateDevicesTimeout: any = null + private _coreUrl: URL | undefined + private _deviceId: string | undefined - constructor(logger: winston.Logger, config: SpreadsheetConfig, coreHandler: CoreHandler) { - this._logger = logger - this.options = config + constructor(_config: SpreadsheetConfig, coreHandler: CoreHandler) { + // this.options = config this._coreHandler = coreHandler - coreHandler.doReceiveAuthToken = async (authToken: string) => { + coreHandler.doReceiveAuthToken = async (authToken: string): Promise => { return this.receiveAuthToken(authToken) } } + async init(coreHandler: CoreHandler): Promise { - return coreHandler.core - .getPeripheralDevice() - .then(async (peripheralDevice: any) => { - this._settings = peripheralDevice.settings || {} + const peripheralDevice = await coreHandler.core.getPeripheralDevice() - return this._initSpreadsheetConnection() - }) - .then(async () => { - this._coreHandler.onConnected(() => { - this.setupObservers() - }) - this.setupObservers() + this._settings = peripheralDevice.settings || {} - return this._updateDevices().catch((e) => { - if (e) throw e // otherwise just swallow it - }) - }) + this._coreHandler.onConnected(() => this.setupObservers()) + this.setupObservers() + + await this._updateThisDevice() } + async dispose(): Promise { this._disposed = true if (this.spreadsheetWatcher) { @@ -100,6 +93,25 @@ export class SpreadsheetHandler { return Promise.resolve() } } + + /** + * Method disposes spreadsheet watcher. + * Should be called when the minimum conditions are not met + * (missing acces token, auth token, auth client, folder path...) + */ + private disposeSpreadsheetWatcher(): void { + if (!this.spreadsheetWatcher) { + return + } + + this.spreadsheetWatcher.dispose() + delete this.spreadsheetWatcher + } + + /** + * Method initializes observers for changed to Sofie peripheral device. + * For example, when some of the settings change for this gateway app. + */ setupObservers(): void { if (this._observers.length) { this._observers.forEach((obs) => { @@ -107,7 +119,7 @@ export class SpreadsheetHandler { }) this._observers = [] } - this._logger.info('Renewing observers') + logger.info('Renewing observers') const deviceObserver = this._coreHandler.core.observe('peripheralDevices') deviceObserver.added = () => { @@ -123,246 +135,295 @@ export class SpreadsheetHandler { this._deviceOptionsChanged() } - debugLog(msg: string, ...args: any[]): void { - if (this.debugLogging) { - this._logger.debug(msg, ...args) - } - } - async receiveAuthToken(authToken: string): Promise { - return new Promise((resolve, reject) => { - if (this._currentOAuth2Client) { - const oAuth2Client = this._currentOAuth2Client - - oAuth2Client.getToken(authToken, (err, accessToken) => { - if (err) { - return reject(err) - } else if (!accessToken) { - return reject(new Error('No accessToken received')) - } else { - oAuth2Client.setCredentials(accessToken) - this._currentOAuth2ClientAuthorized = true - - // Store for later use: - this._coreHandler.core.callMethod(P.methods.storeAccessToken, [accessToken]).catch(this._logger.error) - - resolve() - } - }) - } else { - throw Error('No Authorization is currently in progress!') - } - }) - } - triggerReloadRundown(rundownId: string): void { - void this.spreadsheetWatcher?.checkRunningOrderById(rundownId, true) + + /** + * Method returns Spreadsheet Gateway as a Sofie core peripheral device + * @returns Spreadsheet Gateway as a Sofie core peripheral device + */ + private getThisPeripheralDevice(): CollectionObj | undefined { + const peripheralDevices = this._coreHandler.core.getCollection('peripheralDevices') + return peripheralDevices.findOne(this._coreHandler.core.deviceId) } + + /** + * Method invoked when Spreadsheet Gateway settings change in the Sofie + */ private _deviceOptionsChanged() { const peripheralDevice = this.getThisPeripheralDevice() if (peripheralDevice) { const settings: SpreadsheetDeviceSettings = peripheralDevice.settings || {} if (this.debugLogging !== settings.debugLogging) { - this._logger.info('Changing debugLogging to ' + settings.debugLogging) + logger.info('Changing debugLogging to ' + settings.debugLogging) this.debugLogging = settings.debugLogging - // this.spreadsheetWatcher.setDebug(settings.debugLogging) if (settings.debugLogging) { - this._logger.level = 'debug' + logger.level = 'debug' } else { - this._logger.level = 'info' + logger.level = 'info' } - this._logger.info('log level ' + this._logger.level) - this._logger.info('test log info') - console.log('test console.log') - this._logger.debug('test log debug') } } - if (this._triggerupdateDevicesTimeout) { - clearTimeout(this._triggerupdateDevicesTimeout) + if (this._triggerUpdateDevicesTimeout) { + clearTimeout(this._triggerUpdateDevicesTimeout) } - this._triggerupdateDevicesTimeout = setTimeout(() => { - this._updateDevices().catch((e) => { - if (e) this._logger.error(e) + this._triggerUpdateDevicesTimeout = setTimeout(() => { + this._updateThisDevice().catch((error) => { + logger.error(`Something went wrong wile updating this device`, error) }) }, 20) } - private async _initSpreadsheetConnection(): Promise { - if (this._disposed) return Promise.resolve() - if (!this._settings) throw Error('Spreadsheet-Settings are not set') - this._logger.info('Initializing Spreadsheet connection...') - } - private getThisPeripheralDevice(): CollectionObj | undefined { - const peripheralDevices = this._coreHandler.core.getCollection('peripheralDevices') - return peripheralDevices.findOne(this._coreHandler.core.deviceId) - } - private async _updateDevices(): Promise { - if (this._disposed) return Promise.resolve() - return (!this.spreadsheetWatcher ? this._initSpreadsheetConnection() : Promise.resolve()) - .then(async () => { - const peripheralDevice = this.getThisPeripheralDevice() - - if (peripheralDevice) { - const settings: SpreadsheetDeviceSettings = peripheralDevice.settings || {} - const secretSettings: SpreadsheetDeviceSecretSettings = peripheralDevice.secretSettings || {} - - if (!secretSettings.credentials) { - this._coreHandler.setStatus(P.StatusCode.BAD, ['Not set up: Credentials missing']) - return - } + /** + * Method invoked when some of the settings related to the Spreadsheet Gateway change. + * For example, update gateway status, check credentials etc. + */ + private async _updateThisDevice(): Promise { + if (this._disposed) { + return + } - const credentials = secretSettings.credentials - const accessToken = secretSettings.accessToken + const peripheralDevice = this.getThisPeripheralDevice() // This gateway app as Sofie peripheral device + if (!peripheralDevice) { + return + } - const authClient = await this.createAuthClient(credentials, accessToken) + const settings: SpreadsheetDeviceSettings = peripheralDevice.settings || {} + const secretSettings: SpreadsheetDeviceSecretSettings = peripheralDevice.secretSettings || {} - if (!secretSettings.accessToken) { - this._coreHandler.setStatus(P.StatusCode.BAD, ['Not set up: AccessToken missing']) - return - } + if (!secretSettings.credentials) { + this.disposeSpreadsheetWatcher() + this._coreHandler.setStatus(StatusCode.BAD, ['Not set up: Credentials missing']) + return + } - if (!authClient) { - this._coreHandler.setStatus(P.StatusCode.BAD, ['Internal error: authClient not set']) - return - } + const credentials = secretSettings.credentials + const accessToken = secretSettings.accessToken - if (!settings.folderPath) { - this._coreHandler.setStatus(P.StatusCode.BAD, ['Not set up: FolderPath missing']) - return - } + const authClient = await this.createAuthClient(credentials, accessToken) - // At this point we're authorized and good to go! - - if (!this.spreadsheetWatcher || this.spreadsheetWatcher.sheetFolderName !== settings.folderPath) { - this._coreHandler.setStatus(P.StatusCode.UNKNOWN, ['Initializing..']) - - // this._logger.info('GO!') - - if (this.spreadsheetWatcher) { - this.spreadsheetWatcher.dispose() - delete this.spreadsheetWatcher - } - const watcher = new RunningOrderWatcher(authClient, this._coreHandler, 'v0.2') - this.spreadsheetWatcher = watcher - - watcher - .on('info', (message: any) => { - this._logger.info(message) - }) - .on('error', (error: any) => { - this._logger.error(error) - }) - .on('warning', (warning: any) => { - this._logger.error(warning) - }) - // TODO - these event types should operate on the correct types and with better parameters - .on('rundown_delete', (rundownExternalId) => { - this._coreHandler.core - .callMethod(P.methods.dataRundownDelete, [rundownExternalId]) - .catch(this._logger.error) - }) - .on('rundown_create', (_rundownExternalId, rundown) => { - this._coreHandler.core - .callMethod(P.methods.dataRundownCreate, [mutateRundown(rundown)]) - .catch(this._logger.error) - }) - .on('rundown_update', (_rundownExternalId, rundown) => { - this._coreHandler.core - .callMethod(P.methods.dataRundownUpdate, [mutateRundown(rundown)]) - .catch(this._logger.error) - }) - .on('segment_delete', (rundownExternalId, sectionId) => { - this._coreHandler.core - .callMethod(P.methods.dataSegmentDelete, [rundownExternalId, sectionId]) - .catch(this._logger.error) - }) - .on('segment_create', (rundownExternalId, _sectionId, newSection) => { - this._coreHandler.core - .callMethod(P.methods.dataSegmentCreate, [rundownExternalId, mutateSegment(newSection)]) - .catch(this._logger.error) - }) - .on('segment_update', (rundownExternalId, _sectionId, newSection) => { - this._coreHandler.core - .callMethod(P.methods.dataSegmentUpdate, [rundownExternalId, mutateSegment(newSection)]) - .catch(this._logger.error) - }) - .on('part_delete', (rundownExternalId, sectionId, storyId) => { - this._coreHandler.core - .callMethod(P.methods.dataPartDelete, [rundownExternalId, sectionId, storyId]) - .catch(this._logger.error) - }) - .on('part_create', (rundownExternalId, sectionId, _storyId, newStory) => { - this._coreHandler.core - .callMethod(P.methods.dataPartCreate, [rundownExternalId, sectionId, mutatePart(newStory)]) - .catch(this._logger.error) - }) - .on('part_update', (rundownExternalId, sectionId, _storyId, newStory) => { - this._coreHandler.core - .callMethod(P.methods.dataPartUpdate, [rundownExternalId, sectionId, mutatePart(newStory)]) - .catch(this._logger.error) - }) - - if (settings.folderPath) { - this._logger.info(`Starting watch of folder "${settings.folderPath}"`) - watcher - .setDriveFolder(settings.folderPath) - .then(() => - this._coreHandler.setStatus(P.StatusCode.GOOD, [`Watching folder '${settings.folderPath}'`]) - ) - .catch((e) => { - console.log('Error in addSheetsFolderToWatch', e) - }) - } - } - } - return Promise.resolve() + if (!secretSettings.accessToken) { + this.disposeSpreadsheetWatcher() + this._coreHandler.setStatus(StatusCode.BAD, ['Not set up: AccessToken missing']) + return + } + + if (!authClient) { + this.disposeSpreadsheetWatcher() + this._coreHandler.setStatus(StatusCode.BAD, ['Internal error: authClient not set']) + return + } + + if (!settings.folderPath) { + this.disposeSpreadsheetWatcher() + this._coreHandler.setStatus(StatusCode.BAD, ['Not set up: FolderPath missing']) + return + } + + // At this point we're authorized and good to go! + + if (this.spreadsheetWatcher && this.spreadsheetWatcher.sheetFolderName === settings.folderPath) { + // Nothing new has happened + return + } + + this._logInitSpreadsheetConnection() + this._coreHandler.setStatus(StatusCode.UNKNOWN, ['Initializing..']) + this.disposeSpreadsheetWatcher() + + const watcher = new RunningOrderWatcher(authClient, this._coreHandler, 'v0.1') + this.spreadsheetWatcher = watcher + + watcher + .on('info', (message: any) => { + logger.info(message) + }) + .on('error', (error: any) => { + logger.error(error) + }) + .on('warning', (warning: any) => { + logger.error(warning) + }) + // TODO - these event types should operate on the correct types and with better parameters + .on('rundown_delete', (rundownExternalId) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataRundownDelete, [rundownExternalId]) + .catch(logger.error) + }) + .on('rundown_create', (_rundownExternalId, rundown) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataRundownCreate, [mutateRundown(rundown)]) + .catch(logger.error) + }) + .on('rundown_update', (_rundownExternalId, rundown) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataRundownUpdate, [mutateRundown(rundown)]) + .catch(logger.error) + }) + .on('segment_delete', (rundownExternalId, sectionId) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataSegmentDelete, [rundownExternalId, sectionId]) + .catch(logger.error) }) - .then(() => { - return + .on('segment_create', (rundownExternalId, _sectionId, newSection) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataSegmentCreate, [rundownExternalId, mutateSegment(newSection)]) + .catch(logger.error) }) + .on('segment_update', (rundownExternalId, _sectionId, newSection) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataSegmentUpdate, [rundownExternalId, mutateSegment(newSection)]) + .catch(logger.error) + }) + .on('part_delete', (rundownExternalId, sectionId, storyId) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataPartDelete, [rundownExternalId, sectionId, storyId]) + .catch(logger.error) + }) + .on('part_create', (rundownExternalId, sectionId, _storyId, newStory) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataPartCreate, [rundownExternalId, sectionId, mutatePart(newStory)]) + .catch(logger.error) + }) + .on('part_update', (rundownExternalId, sectionId, _storyId, newStory) => { + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.dataPartUpdate, [rundownExternalId, sectionId, mutatePart(newStory)]) + .catch(logger.error) + }) + + if (settings.folderPath) { + logger.info(`Starting watch of folder "${settings.folderPath}"`) + this._coreHandler.setStatus(StatusCode.GOOD, [`Starting watching folder '${settings.folderPath}'`]) + watcher + .setDriveFolder(settings.folderPath) + .then(() => { + this._coreHandler.setStatus(StatusCode.GOOD, [`Watching folder '${settings.folderPath}'`]) + }) + .catch((error) => { + let msg = getErrorMsg(error) + logger.error('Something went wrong during setting drive folder: ' + msg) + logger.error(error) + + if (checkErrorType(error, ['invalid_grant', 'authError'])) { + msg += ', try resetting user credentials' + } + this._coreHandler.setStatus(StatusCode.BAD, [msg]) + }) + } } + /** * Get an authentication client towards Google drive on behalf of the user, * or prompt for login. - * * @param credentials Credentials from credentials.json which you get from Google + * @param authCredentials Auth credentials if they already exists + * @returns OAuth2 client */ - private async createAuthClient(credentials: Credentials, accessToken?: any): Promise { - if (this._currentOAuth2Client) { - if (!this._currentOAuth2ClientAuthorized) { - // there is already a authentication in progress.. - return Promise.resolve(null) - } else { - return Promise.resolve(this._currentOAuth2Client) - } + private async createAuthClient( + credentials: Credentials, + authCredentials?: Auth.Credentials + ): Promise { + if (authCredentials && this._currentOAuth2Client) { + return this._currentOAuth2Client } + // Create OAuth2 Client this._currentOAuth2Client = new google.auth.OAuth2( credentials.installed.client_id, credentials.installed.client_secret, credentials.installed.redirect_uris[0] ) - if (accessToken) { - this._currentOAuth2Client.setCredentials(accessToken) - this._currentOAuth2ClientAuthorized = true - return Promise.resolve(this._currentOAuth2Client) + if (authCredentials) { + this._currentOAuth2Client.setCredentials(authCredentials) } else { - // If we don't have an accessToken, request it from the user. - this._logger.info('Requesting auth token from user..') + // If we don't have an authCredentials, request it from the user. + logger.info('Requesting auth token from user') + + if (!this._coreUrl) { + logger.error(`Core URL not set`) + this._coreHandler.setStatus(StatusCode.BAD, ['Core URL Not set on studio']) + return Promise.reject() + } const authUrl = this._currentOAuth2Client.generateAuthUrl({ access_type: 'offline', scope: ACCESS_SCOPES, prompt: 'consent', + redirect_uri: new URL(`devices/${this._deviceId}/oauthResponse`, this._coreUrl.toString()).toString(), }) - // This will prompt the user in Core, which will fillow the link, and provide us with an access token. - // user will eventually call this.receiveAuthToken() - return this._coreHandler.core.callMethod(P.methods.requestUserAuthToken, [authUrl]).then(async () => { - return Promise.resolve(null) + /** + * This will prompt the user in Sofie UI to authorize it's Google Account. + * Once authorized, this.receiveAuthToken() method will be invoked. + * Requesting user access token and receiving it is delegated to the Sofie Core, which forwards the data to this gateway app. + */ + await this._coreHandler.core.callMethod(PeripheralDeviceAPIMethods.requestUserAuthToken, [authUrl]) + } + + return this._currentOAuth2Client + } + + /** + * Method handles receivement of user's auth token from Sofie + * TODO: Rename receiveAuthToken to receiveAuthorizationCode + * @param authorizationCode Authorization code received from Sofie + */ + async receiveAuthToken(authorizationCode: string): Promise { + if (this._currentOAuth2Client) { + const oAuth2Client = this._currentOAuth2Client + + // Here redirect_uri just needs to match what was sent previously to satisfy Google's security requirements + const redirect_uri = new URL( + `devices/${this._deviceId}/oauthResponse`, + this._coreUrl?.toString() ?? '' + ).toString() + + this._currentOAuth2Client.getToken({ code: authorizationCode, redirect_uri }, (error, authCredentials) => { + if (error) { + throw error + } else if (!authCredentials) { + throw new Error('No authCredentials received') + } else { + oAuth2Client.setCredentials(authCredentials) + + // Store for later use: + this._coreHandler.core + .callMethod(PeripheralDeviceAPIMethods.storeAccessToken, [authCredentials]) + .catch(logger.error) + } }) + } else { + throw Error('No Authorization is currently in progress!') + } + } + + debugLog(msg: string, ...args: any[]): void { + if (this.debugLogging) { + logger.debug(msg, ...args) } } + + /** + * TODO - Useless? + */ + private _logInitSpreadsheetConnection(): void { + if (this._disposed) return + if (!this._settings) throw Error('Spreadsheet Settings are not set') + + logger.info('Initializing Spreadsheet connection...') + } + + triggerReloadRundown(spreadsheetId: string): void { + void this.spreadsheetWatcher?.fetchSheetRundown(spreadsheetId, true) + } + + public setCoreUrl(url: URL): void { + this._coreUrl = url + } + + public setDeviceId(id: string): void { + this._deviceId = id + } } diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..ad76de5 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,160 @@ +import { IOutputLayer } from '@sofie-automation/blueprints-integration' + +export function assertUnreachable(_unreachable: never, err: Error): Error { + return err +} + +interface ShowTime { + hour: number + minute: number + second: number + millis: number +} + +/** + * Converts a 12/24 hour date string to a ShowTime + * @param {string} timeString Time in the form `HH:MM:SS (AM|PM)` + */ +export function showTimeFromString(timeString: string): ShowTime { + const [time, mod] = timeString.split(' ') + // eslint-disable-next-line prefer-const + let [hours, mins, seconds] = time.includes('.') ? time.split('.') : time.split(':') + let h: number + const m = Number(mins) + const s = Number(seconds) + + if (hours === '12') { + hours = '00' + } + + if (mod === 'PM') { + h = parseInt(hours, 10) + 12 + } else { + h = parseInt(hours, 10) + } + + const mil = 1000 + + return { + hour: h, + minute: m, + second: s, + millis: s * mil + m * 60 * mil + h * 3600 * mil, + } +} + +/** + * Converts the start and end times to milliseconds + * @param {string} startString Start time in the form `HH:MM:SS (AM|PM)` + * @param {string} endString End time in the form `HH:MM:SS (AM|PM)` + */ +export function showTimesToMillis(startString: string, endString: string): [number, number] { + const startDay = new Date() + const endDay = new Date() + + const startTime: ShowTime = showTimeFromString(startString) + const endTime: ShowTime = showTimeFromString(endString) + + if (startTime.millis > endTime.millis) { + endDay.setDate(startDay.getDate() + 1) + } + + // Assume the show is happening today + const targetStart = new Date( + startDay.getFullYear(), + startDay.getMonth(), + startDay.getDate(), + startTime.hour, + startTime.minute, + startTime.second + ) + const targetEnd = new Date( + endDay.getFullYear(), + endDay.getMonth(), + endDay.getDate(), + endTime.hour, + endTime.minute, + endTime.second + ) + return [targetStart.getTime(), targetEnd.getTime()] +} + +export function getLayerByName(name: string, outputLayers: IOutputLayer[]): string { + let id = '' + outputLayers.forEach((layer) => { + if (layer.name === name) id = layer._id + }) + + return id +} + +export function HHMMSSToMs(input: string): number | undefined { + if (!input || input.length <= 0) { + return + } + + const splitted = input.split(':') + let sum = 0 + + if (splitted.length !== 3) { + return + } + + for (let i = 0; i < splitted.length; i++) { + const value = parseInt(splitted[i]) + if (i === 0) { + sum += value * 60 * 60 + } else if (i === 1) { + sum += value * 60 + } else if (i === 2) { + sum += value + } + } + return sum * 1000 +} + +export function getErrorMsg(error: unknown): string { + const e = error as any + + if (e?.response?.data?.error_description) { + return e.response.data.error_description + } else if (e?.response?.data?.error?.errors && e?.response?.data?.error?.errors[0]?.message) { + return e.response.data.error.errors[0].message + } else if (e?.code) { + return e.code + } + + return 'An error occured' +} + +export function getError(error: unknown): string { + const e = error as any + + if (e?.response?.data?.error) { + return e.response.data.error + } else if (e?.code) { + return e.code + } + + return 'An error occured' +} + +export function checkErrorType(error: unknown, suspiciousTypes: string[]): boolean { + const e = error as any + + if (e?.response?.data?.error) { + if (suspiciousTypes.includes(e.response.data.error)) { + return true + } + } + + if (e?.response?.data?.error?.errors) { + for (const singleError of e.response.data.error.errors) { + if (suspiciousTypes.includes(singleError.reason)) { + return true + } + } + } + + return false +} diff --git a/tsconfig.build.json b/tsconfig.build.json index f868e61..384e139 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,14 +1,18 @@ { "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.bin", "include": ["src/**/*.ts"], - "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"], + "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*", "src/__tests__/"], "compilerOptions": { + "module": "commonjs", + "target": "es6", + "moduleResolution": "node", "outDir": "./dist", "baseUrl": "./", "paths": { "*": ["./node_modules/*"], "sofie-spreadsheet-gateway": ["./src/index.ts"] }, - "types": ["node"] + "types": ["node"], + "lib": ["es6"] } } diff --git a/tsconfig.json b/tsconfig.json index 39cf967..da730ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.build.json", "exclude": ["node_modules/**"], "compilerOptions": { - "types": ["jest", "node"] + "types": ["jest", "node"], + "downlevelIteration": true } } diff --git a/yarn.lock b/yarn.lock index b806ace..811b48a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -634,15 +634,15 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@sofie-automation/blueprints-integration@1.41.0-in-testing.0": - version "1.41.0-in-testing.0" - resolved "https://registry.yarnpkg.com/@sofie-automation/blueprints-integration/-/blueprints-integration-1.41.0-in-testing.0.tgz#8a523ab88280d5305fe56912f25627cefc2b1039" - integrity sha512-7ovzOBbgti2X2b+0JqmWP7mc5Fl8KZY0kim+8BCEqVGezJHs0+wDW36yHw+Fn9AXifXjFMkGaz+98IjSFv0F2g== +"@sofie-automation/blueprints-integration@1.46.0-in-testing.0": + version "1.46.0-in-testing.0" + resolved "https://registry.yarnpkg.com/@sofie-automation/blueprints-integration/-/blueprints-integration-1.46.0-in-testing.0.tgz#29f2b0f1ca8b7512be27099b21d335794f76af9d" + integrity sha512-RNbw3oPnaT1xZiBoh7LEBicXJestxvYZ3lvzHKlA5rbKOzOJaFsnqBxUU4gOvvWWbaAGMmsloCltAmaB3STRyA== dependencies: - moment "2.29.1" - timeline-state-resolver-types "7.0.0-release41.0" - tslib "^2.3.1" - type-fest "^2.11.1" + "@sofie-automation/shared-lib" "1.46.0-in-testing.0" + timeline-state-resolver-types "7.4.0-release46.2" + tslib "^2.4.0" + type-fest "^2.19.0" "@sofie-automation/code-standard-preset@^2.0.1": version "2.0.1" @@ -664,17 +664,26 @@ read-pkg-up "^9.1.0" shelljs "^0.8.5" -"@sofie-automation/server-core-integration@1.41.0-in-testing.0": - version "1.41.0-in-testing.0" - resolved "https://registry.yarnpkg.com/@sofie-automation/server-core-integration/-/server-core-integration-1.41.0-in-testing.0.tgz#d58817c62382f7617a60317ed7a68689e0a0a2fa" - integrity sha512-ELU53riuZQKoTpXadlmf6+y5S/sjrKXy+d0mBOPMO8zSF9O1NEUZJnv5hPcDIgiY04CcRB5/LF/CbrO3+HQmtw== +"@sofie-automation/server-core-integration@1.46.0-in-testing.0": + version "1.46.0-in-testing.0" + resolved "https://registry.yarnpkg.com/@sofie-automation/server-core-integration/-/server-core-integration-1.46.0-in-testing.0.tgz#a0075781c1b7c5099d9215f6381596c65596fbf4" + integrity sha512-TkJpCUoyD8jiGyFpMXNJTo92V7cKmZKW6EVHnF3GOmmkzwkb9sCgROxelQ5jea9KtQ6Hw/w9ObCGXznLks1Kxg== dependencies: - data-store "^4.0.3" - ejson "^2.2.2" + "@sofie-automation/shared-lib" "1.46.0-in-testing.0" + ejson "^2.2.3" + eventemitter3 "^4.0.7" faye-websocket "^0.11.4" - got "^11.8.2" - tslib "^2.3.1" - underscore "^1.12.1" + got "^11.8.5" + tslib "^2.4.0" + underscore "^1.13.4" + +"@sofie-automation/shared-lib@1.46.0-in-testing.0": + version "1.46.0-in-testing.0" + resolved "https://registry.yarnpkg.com/@sofie-automation/shared-lib/-/shared-lib-1.46.0-in-testing.0.tgz#97e279c14399f98df13408cec6e50d1ce2ee7d95" + integrity sha512-DrYvnDRzGDJeeRFpqq+3UyV/jmv+xp5DkIJ00Nd8VFUaAXGh8kNrdinYeDv0Xy9jaG34HUSYE6aQtZm5gDecAQ== + dependencies: + tslib "^2.4.0" + type-fest "^2.19.0" "@stroncium/procfs@^1.2.1": version "1.2.1" @@ -812,6 +821,11 @@ dependencies: "@types/node" "*" +"@types/lodash@^4.14.184": + version "4.14.184" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe" + integrity sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q== + "@types/minimist@^1.2.0", "@types/minimist@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" @@ -1338,11 +1352,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - cacheable-lookup@^5.0.3: version "5.0.4" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" @@ -1596,7 +1605,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@^2.18.0: +commander@^2.18.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -1859,14 +1868,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-store@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/data-store/-/data-store-4.0.3.tgz#d055c55f3fa776daa3a5664cf715d3fe689c3afb" - integrity sha512-AhPSqGVCSrFgi1PtkOLxYRrVhzbsma/drtxNkwTrT2q+LpLTG3AhOW74O/IMRy1cWftBx93SuLLKF19YsKkUMg== - dependencies: - get-value "^3.0.1" - set-value "^3.0.1" - dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -1995,11 +1996,6 @@ diff-sequences@^28.0.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.0.2.tgz#40f8d4ffa081acbd8902ba35c798458d0ff1af41" integrity sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - dir-glob@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" @@ -2014,14 +2010,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -doctrine@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" - integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM= - dependencies: - esutils "^1.1.6" - isarray "0.0.1" - doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -2069,10 +2057,10 @@ ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: dependencies: safe-buffer "^5.0.1" -ejson@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ejson/-/ejson-2.2.2.tgz#095bf356a57295784cada6ff19b2b2abc70e4f9d" - integrity sha512-Wc/PZH7WMxuM6eBpAuD9+IX98q27hlBTD8nvhh7UyGy22eB9qTRG0oAzlviicADh9VVxYp+/onTyfYZC3DhI5w== +ejson@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/ejson/-/ejson-2.2.3.tgz#2b18c2d8f5d61a5cfc6e3eab72c3343909e7afd2" + integrity sha512-hsFvJp6OpGxFRQfBR3PSxFpaPALdHDY+SB3TRbMpLWNhvu8GzLiZutof5+/DFd2QekZo3KyXau75ngdJqQUSrw== electron-to-chromium@^1.4.118: version "1.4.131" @@ -2306,11 +2294,6 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -esutils@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" - integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U= - esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2321,6 +2304,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + execa@^2.0.1: version "2.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" @@ -2684,13 +2672,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-value@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" - integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== - dependencies: - isobject "^3.0.1" - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -2860,10 +2841,10 @@ googleapis@^100.0.0: google-auth-library "^7.0.2" googleapis-common "^5.0.2" -got@^11.8.2: - version "11.8.3" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" - integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== +got@^11.8.5: + version "11.8.5" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" + integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" @@ -3174,13 +3155,6 @@ is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - is-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" @@ -3205,11 +3179,6 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -3220,11 +3189,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -3806,11 +3770,6 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -legally@^3.5.10: - version "3.5.10" - resolved "https://registry.yarnpkg.com/legally/-/legally-3.5.10.tgz#561b769ded5dd550b2267de3403f783a497030ea" - integrity sha512-8wc15Uue8Cju65dOH1DSuHJJI3LzYqGTPzxUbE77471V03fFtchwSs3Qnb3pNIUMZLdd+a647BT+zWhFRgRLEQ== - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4189,11 +4148,6 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - mount-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mount-point/-/mount-point-3.0.0.tgz#665cb9edebe80d110e658db56c31d0aef51a8f97" @@ -5004,7 +4958,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== @@ -5079,7 +5033,7 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -5096,13 +5050,6 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -set-value@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== - dependencies: - is-plain-object "^2.0.4" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -5587,10 +5534,10 @@ through@2, "through@>=2.2.7 <3", through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -timeline-state-resolver-types@7.0.0-release41.0: - version "7.0.0-release41.0" - resolved "https://registry.yarnpkg.com/timeline-state-resolver-types/-/timeline-state-resolver-types-7.0.0-release41.0.tgz#913baac12244c92e703a499e4c47c29308720992" - integrity sha512-3HwdH2+3Sb0GWQ8aEqXiohBS99eUjdgHmksPS3N8QF7yUy1Da69zRh1CAj/og1IQOfFgWhAn0fLuVxyudEOsBA== +timeline-state-resolver-types@7.4.0-release46.2: + version "7.4.0-release46.2" + resolved "https://registry.yarnpkg.com/timeline-state-resolver-types/-/timeline-state-resolver-types-7.4.0-release46.2.tgz#cb0fd08c7537504eca3531206bc20501612e33f0" + integrity sha512-1WYIyBiKS+JR1sBXB5gYGvcMB7JZhY4N++36i8YD44pP5B8a1j0g5LHcuv6Exo4DizlS6JuvvheSj218gKy6PQ== dependencies: tslib "^2.3.1" @@ -5705,64 +5652,17 @@ ts-lib@^0.0.5: resolved "https://registry.yarnpkg.com/ts-lib/-/ts-lib-0.0.5.tgz#7f319885478de67f575a3b890a3f30fc6159352a" integrity sha1-fzGYhUeN5n9XWjuJCj8w/GFZNSo= -tslib@1.9.0, tslib@^1.8.1: +tslib@^1.8.1: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== -tslib@^1.8.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.1.0, tslib@^2.3.1: +tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslint-config-standard@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/tslint-config-standard/-/tslint-config-standard-8.0.1.tgz#e4dd3128e84b0e34b51990b68715a641f2b417e4" - integrity sha512-OWG+NblgjQlVuUS/Dmq3ax2v5QDZwRx4L0kEuDi7qFY9UI6RJhhNfoCV1qI4el8Fw1c5a5BTrjQJP0/jhGXY/Q== - dependencies: - tslint-eslint-rules "^5.3.1" - -tslint-eslint-rules@^5.3.1: - version "5.4.0" - resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5" - integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w== - dependencies: - doctrine "0.7.2" - tslib "1.9.0" - tsutils "^3.0.0" - -tslint@^5.11.0: - version "5.20.1" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" - integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.1" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.8.0" - tsutils "^2.29.0" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - -tsutils@^3.0.0, tsutils@^3.21.0: +tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== @@ -5828,11 +5728,16 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.0.0, type-fest@^2.11.1, type-fest@^2.5.0: +type-fest@^2.0.0, type-fest@^2.5.0: version "2.12.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.2.tgz#80a53614e6b9b475eb9077472fb7498dc7aa51d0" integrity sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5859,16 +5764,16 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.4.tgz#fa95c257e88f85614915b906204b9623d4fa340d" integrity sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA== -underscore@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" - integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== - underscore@^1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.3.tgz#54bc95f7648c5557897e5e968d0f76bc062c34ee" integrity sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA== +underscore@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee" + integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ== + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"