diff --git a/.env b/.env deleted file mode 100644 index 7bffd96..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -SOURCE_DIR=../bes-lyrics/verified -OUT_DIR=./pp7-songs diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..98a4093 --- /dev/null +++ b/.env.local @@ -0,0 +1,4 @@ +TZ=Europe/Bucharest +LOCAL_SOURCE_DIR=../bes-lyrics/verified +LOCAL_OUT_DIR=./out_temp_for_local +CONNECT_TO_G_DRIVE=false diff --git a/.env.remote b/.env.remote index 29b1234..517ca0f 100644 --- a/.env.remote +++ b/.env.remote @@ -1,2 +1,5 @@ -SOURCE_DIR=../bes-lyrics/verified -OUT_DIR=/Users/ilucut/WORK/BES/CLOUD DATA/ProPresenter_Generated/ProPresenter_Generated_Version_10 +TZ=Europe/Bucharest +LOCAL_SOURCE_DIR=../bes-lyrics/verified +LOCAL_OUT_DIR=./out_temp_for_remote +GDRIVE_ROOT_FOLDER_ID=1jjGoKWLbYskXWsCcOgjtQiynCUs-lslB +CONNECT_TO_G_DRIVE=true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9e9ef5b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +#### Motivation and context + + + +#### Checklist: + +- [x] I covered my changes with tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4fbdcd2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +on: [pull_request] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Use Node.js 20.x + uses: actions/setup-node@v3 + with: + node-version: 20.x + + - name: Install dependencies + run: npm i + + - name: Build + run: npm run test + env: + CI: true + FORCE_COLOR: 2 diff --git a/.gitignore b/.gitignore index 965c5fb..28cb537 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ .idea -pp7-songs +out_temp_for_local +.env.debug +out_temp_for_remote diff --git a/.run/Run migrator.run.xml b/.run/Run migrator.run.xml deleted file mode 100644 index df0b636..0000000 --- a/.run/Run migrator.run.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/[Debug] Run converter.run.xml b/.run/[Debug] Run converter.run.xml new file mode 100644 index 0000000..f4a4566 --- /dev/null +++ b/.run/[Debug] Run converter.run.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.run/[Local] Run converter.run.xml b/.run/[Local] Run converter.run.xml new file mode 100644 index 0000000..373750e --- /dev/null +++ b/.run/[Local] Run converter.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md index d0f472e..70839af 100644 --- a/README.md +++ b/README.md @@ -36,26 +36,26 @@ Domn al veșniciei, în veci! Amin! - Assuming that the following config is fine: ```dotenv -SOURCE_DIR=directory-with-txt-songs -OUT_DIR=directory-with-pro-songs +LOCAL_SOURCE_DIR=directory-with-txt-songs +LOCAL_OUT_DIR=directory-with-pro-songs ``` -Simply run the `npm run migrate:local` +Simply run the `npm run convert:local` ### How to customize the runner - Pass the following env variables with your source and out directories ```dotenv -SOURCE_DIR=directory-with-txt-songs -OUT_DIR=directory-with-pro-songs +LOCAL_SOURCE_DIR=directory-with-txt-songs +LOCAL_OUT_DIR=directory-with-pro-songs ``` -- Use the `migrateSongsToPP7Format` method to do the conversion as follows: +- Use the `convertSongsToPP7FormatLocally` method to do the conversion as follows: ```typescript import dotenv from 'dotenv'; -import { Config, migrateSongsToPP7Format } from './'; +import { Config, convertSongsToPP7FormatLocally } from './'; import { Presentation_CCLI } from './proto/presentation'; import { Graphics_Text_Attributes_Font } from './proto/graphicsData'; @@ -87,10 +87,9 @@ const CONFIG = { 'A macro name for referencing the macro id in the first intro blank slide', }; -migrateSongsToPP7Format({ - sourceDir: process.env.SOURCE_DIR as string, - outDir: process.env.OUT_DIR as string, - clearOutputDirFirst: true, +convertSongsToPP7FormatLocally({ + sourceDir: process.env.LOCAL_SOURCE_DIR as string, + baseLocalDir: process.env.LOCAL_OUT_DIR as string, config: CONFIG, }); ``` diff --git a/environment.d.ts b/environment.d.ts index 29b6b3b..518c100 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -3,6 +3,13 @@ declare global { interface ProcessEnv { CANDIDATES_DIR: string; VERIFIED_DIR: string; + GDRIVE_ROOT_FOLDER_ID: string; + CONNECT_TO_G_DRIVE: string; + + // Injected from GH Secrets + GDRIVE_BES_CLIENT_ID: string; + GDRIVE_BES_CLIENT_SECRET: string; + GDRIVE_BES_CLIENT_REFRESH_TOKEN: string; } } } diff --git a/package-lock.json b/package-lock.json index 0c8f645..30e55f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "migrator_propres7-node", "version": "1.0.0", "dependencies": { + "@googleapis/drive": "^8.0.0", "dotenv": "^16.0.3", "dotenv-cli": "^7.2.1", "fs-extra": "^11.1.1", "google-protobuf": "^3.21.2", "lodash": "^4.17.21", + "p-map": "^4.0.0", "protobufjs": "^7.2.3", "recursive-readdir": "^2.2.3", "ts-proto": "^1.147.1", @@ -756,6 +758,17 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@googleapis/drive": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.0.0.tgz", + "integrity": "sha512-RMa1gto0Dwmy3pi7o8IkqAXiDUHl9ENn/VPm5kPfCl3lxa/oRRzhHlbQ7DAeJz+ukIaVm0lbtS7wYcT7jxAA1g==", + "dependencies": { + "googleapis-common": "^6.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -1702,6 +1715,29 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1794,6 +1830,14 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, "node_modules/babel-jest": { "version": "29.6.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.1.tgz", @@ -1890,6 +1934,33 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1960,12 +2031,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2061,6 +2149,14 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2148,7 +2244,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2282,6 +2377,14 @@ "detect-libc": "^1.0.3" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.341", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.341.tgz", @@ -2682,6 +2785,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2728,6 +2836,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -2838,8 +2951,33 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -2859,6 +2997,20 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2941,11 +3093,76 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-auth-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-auth-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/google-protobuf": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" }, + "node_modules/googleapis-common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2957,11 +3174,23 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2978,12 +3207,46 @@ "node": ">=8" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3055,6 +3318,14 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3185,7 +3456,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -4027,6 +4297,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4068,6 +4346,25 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4246,8 +4543,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -4261,6 +4557,33 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4302,6 +4625,14 @@ "node": ">= 0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4385,6 +4716,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -4615,6 +4960,20 @@ } ] }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4756,6 +5115,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", @@ -4808,6 +5186,19 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -5015,6 +5406,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -5311,6 +5707,11 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -5364,6 +5765,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6029,6 +6444,14 @@ "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true }, + "@googleapis/drive": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.0.0.tgz", + "integrity": "sha512-RMa1gto0Dwmy3pi7o8IkqAXiDUHl9ENn/VPm5kPfCl3lxa/oRRzhHlbQ7DAeJz+ukIaVm0lbtS7wYcT7jxAA1g==", + "requires": { + "googleapis-common": "^6.0.3" + } + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -6789,6 +7212,23 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6856,6 +7296,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "babel-jest": { "version": "29.6.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.1.tgz", @@ -6931,6 +7376,16 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6979,12 +7434,26 @@ "node-int64": "^0.4.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7036,6 +7505,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -7110,7 +7584,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -7203,6 +7676,14 @@ "detect-libc": "^1.0.3" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "electron-to-chromium": { "version": "1.4.341", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.341.tgz", @@ -7488,6 +7969,11 @@ "jest-util": "^29.6.1" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7530,6 +8016,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -7618,8 +8109,27 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } }, "gensync": { "version": "1.0.0-beta.2", @@ -7633,6 +8143,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -7688,11 +8209,63 @@ "slash": "^3.0.0" } }, + "google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "requires": { + "node-forge": "^1.3.1" + } + }, "google-protobuf": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" }, + "googleapis-common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -7704,11 +8277,20 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -7719,12 +8301,31 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7771,6 +8372,11 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7871,8 +8477,7 @@ "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "isexe": { "version": "2.0.0", @@ -8494,6 +9099,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -8527,6 +9140,25 @@ "universalify": "^2.0.0" } }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -8671,8 +9303,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "natural-compare": { "version": "1.4.0", @@ -8686,6 +9317,19 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8718,6 +9362,11 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8779,6 +9428,14 @@ } } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -8934,6 +9591,14 @@ "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", "dev": true }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9016,6 +9681,11 @@ "queue-microtask": "^1.2.2" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", @@ -9055,6 +9725,16 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9213,6 +9893,11 @@ "is-number": "^7.0.0" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -9409,6 +10094,11 @@ "punycode": "^2.1.0" } }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -9458,6 +10148,20 @@ "makeerror": "1.0.12" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 4729a47..81bcaf3 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,24 @@ { "name": "migrator_propres7-node", "version": "1.0.0", - "description": "A migrator from .txt song lyrics to PP7 pro format.", + "description": "A converter from .txt song lyrics to PP7 pro format.", "main": "index.ts", "scripts": { - "test:ci": "NODE_ENV=test jest --runInBand --no-cache", - "test:watch": "NODE_ENV=test TZ='Europe/Berlin' jest --watch --logHeapUsage", + "test:ci": "NODE_ENV=test TZ='Europe/Bucharest' jest --runInBand --no-cache", + "test:watch": "NODE_ENV=test TZ='Europe/Bucharest' jest --watch --logHeapUsage", "test": "is-ci-cli test:ci test:watch", - "migrate:local": "ts-node runner.ts", - "migrate:remote": "dotenv -e .env.remote ts-node runner.ts" + "convert:local": "dotenv -e .env.local ts-node runner.ts", + "convert:remote": "dotenv -e .env.remote ts-node runner.ts" }, "author": "Ioan Lucut", "dependencies": { + "@googleapis/drive": "^8.0.0", "dotenv": "^16.0.3", "dotenv-cli": "^7.2.1", "fs-extra": "^11.1.1", "google-protobuf": "^3.21.2", "lodash": "^4.17.21", + "p-map": "^4.0.0", "protobufjs": "^7.2.3", "recursive-readdir": "^2.2.3", "ts-proto": "^1.147.1", diff --git a/runner.ts b/runner.ts index 715349b..d910731 100644 --- a/runner.ts +++ b/runner.ts @@ -1,5 +1,9 @@ import dotenv from 'dotenv'; -import { Config, migrateSongsToPP7Format } from './'; +import { + Config, + convertSongsToPP7FormatLocally, + convertSongsToPP7FormatRemotely, +} from './'; import { Presentation_CCLI } from './proto/presentation'; import { Graphics_Text_Attributes_Font } from './proto/graphicsData'; @@ -27,13 +31,18 @@ const CONFIG = { presentationCategory: `Worship Songs ~ BES ${new Date().getFullYear()}`, refMacroId: '3ffd01b7-104f-499f-aac9-a13135006d0e', refMacroName: 'Songs', -}; +} as Config; (async () => { - await migrateSongsToPP7Format({ - sourceDir: process.env.SOURCE_DIR as string, - outDir: process.env.OUT_DIR as string, - clearOutputDirFirst: true, + const deploymentArgs = { + sourceDir: process.env.LOCAL_SOURCE_DIR as string, + baseLocalDir: process.env.LOCAL_OUT_DIR as string, config: CONFIG, - }); + }; + + if (process.env.CONNECT_TO_G_DRIVE !== 'true') { + await convertSongsToPP7FormatLocally(deploymentArgs); + } else { + await convertSongsToPP7FormatRemotely(deploymentArgs); + } })(); diff --git a/src/constants.ts b/src/constants.ts index 788a6c8..b7263e7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,8 @@ export const SLASH = '/'; export const HASH = '#'; +export const DASH = '-'; + export const TXT_EXTENSION = '.txt'; export const TEST_FILE = 'TEMPLATE.txt'; @@ -34,3 +36,5 @@ export const TEST_ENV = 'test'; export const EMPTY_SPACE = ' '; export const PRO_EXTENSION = `.pro`; + +export const MANIFEST_FILE_NAME = 'manifest.json'; diff --git a/src/converterService.ts b/src/converterService.ts new file mode 100644 index 0000000..5ca012f --- /dev/null +++ b/src/converterService.ts @@ -0,0 +1,151 @@ +import fs from 'fs'; +import path from 'path'; +import { isEqual } from 'lodash'; +import { Presentation } from '../proto/presentation'; +import { + Config, + convertSongToProPresenter7, +} from './proPresenter7SongConverter'; +import { MANIFEST_FILE_NAME, PRO_EXTENSION, TXT_EXTENSION } from './constants'; +import { + getClosestVersionedDir, + getVersionedDir, + parseDateFromVersionedDir, +} from './core'; +import { + ConvertedFileStats, + DeployableSong, + SongManifest, + SongsInventoryManifest, +} from './types'; +import recursive from 'recursive-readdir'; +import { parseSong } from './songsParser'; +import { generateManifest } from './manifestGenerator'; + +export const getConvertedAndWrittenToLocalOutDirSongs = ( + songsToGenerate: DeployableSong[], + dir: string, + config: Config, +) => + songsToGenerate.map(({ fileName, song }) => { + const songInProPresenter7Format = convertSongToProPresenter7(song, config); + + const songFileName = fileName.replace(TXT_EXTENSION, PRO_EXTENSION); + const songFilePath = path.join(dir, fileName); + + fs.writeFileSync( + songFilePath, + Buffer.from(Presentation.encode(songInProPresenter7Format).finish()), + ); + + console.log( + `Successfully converted "${songFilePath}" in the "${dir}" directory.`, + ); + + return { + songFileName, + songFilePath, + } as ConvertedFileStats; + }); + +export const getPreviousDeploymentDirectory = ( + deploymentDate: Date, + allPreviousDeploymentDirs: { + deploymentDirDate: Date; + deploymentDir: string; + }[], +) => { + const previousDeploymentDirDate = getClosestVersionedDir( + deploymentDate, + allPreviousDeploymentDirs.map(({ deploymentDirDate }) => deploymentDirDate), + ); + + return getVersionedDir(previousDeploymentDirDate); +}; + +export const transformManifestToHashMapForFasterRetrievals = ( + manifest: SongManifest[], +) => + manifest.reduce( + (accumulator, entry) => ({ + ...accumulator, + [entry.id]: entry, + }), + {} as { + [id: string]: SongManifest; + }, + ); + +export const getSongDiffFromManifest = ( + currentManifest: SongsInventoryManifest, + previousManifest: SongsInventoryManifest, +) => { + const previousManifestHashMap = transformManifestToHashMapForFasterRetrievals( + previousManifest.inventory, + ); + const currentManifestHashMap = transformManifestToHashMapForFasterRetrievals( + currentManifest.inventory, + ); + + const newOrUpdatedSongs = currentManifest.inventory.filter( + ({ id, fileName, contentHash }) => + // Is new song + !previousManifestHashMap[id] || + // Is an existing-updated song + !isEqual(previousManifestHashMap[id]?.contentHash, contentHash), + ); + + const toBeRemovedFileNames = previousManifest.inventory + .filter( + ({ id, fileName, contentHash }) => + // File-name has changed + !isEqual(currentManifestHashMap[id]?.fileName, fileName), + ) + .map(({ id }) => previousManifestHashMap[id]?.fileName); + + return { + newOrUpdatedSongs, + toBeRemovedFileNames, + }; +}; + +export const getDeployableSongs = async (sourceDir: string) => + (await recursive(sourceDir, ['.DS_Store'])).map((filePath) => { + const fileAsText = fs.readFileSync(filePath).toString(); + const fileName = path.basename(filePath); + const song = parseSong(fileAsText); + + return { song, fileName, fileAsText } as DeployableSong; + }); + +export const getBasicDeploymentInfo = async ( + sourceDir: string, + baseLocalDir: string, +) => { + const versionedDir = getVersionedDir(); + const deploymentDate = parseDateFromVersionedDir(versionedDir); + const deploymentVersionedDir = `${baseLocalDir}/${versionedDir}`; + + // --- + // Current deployment + const deployableSongs = await getDeployableSongs(sourceDir); + + const currentManifest = { + inventory: await generateManifest(deployableSongs), + updatedOn: versionedDir, + } as SongsInventoryManifest; + + const localManifestFilePath = path.join( + deploymentVersionedDir, + MANIFEST_FILE_NAME, + ); + + return { + versionedDir, + deploymentDate, + deploymentVersionedDir, + deployableSongs, + currentManifest, + localManifestFilePath, + }; +}; diff --git a/src/core.spec.ts b/src/core.spec.ts index 7c2f7a3..ab98b37 100644 --- a/src/core.spec.ts +++ b/src/core.spec.ts @@ -1,10 +1,17 @@ import { + parseDateFromVersionedDir, getMetaSectionsFromTitle, getSongInSectionTuples, getTitleWithoutMeta, + getVersionedDir, + getClosestVersionedDir, } from './core'; import { SIMPLE_SONG_MOCK_FILE_CONTENT } from '../mocks'; +jest + .useFakeTimers() + .setSystemTime(new Date('2023-02-19T11:12:13.000Z').getTime()); + describe('core', () => { describe('getSongInSectionTuples', () => { it('should work correctly', () => { @@ -74,4 +81,38 @@ describe('core', () => { `); }); }); + + describe('getClosestVersionedDir', () => { + it('should work correctly', () => { + expect(getVersionedDir()).toMatchInlineSnapshot(`"2023-02-19-13:12:13"`); + }); + }); + + describe('parseDateFromVersionedDir', () => { + it('should work correctly', () => { + expect( + parseDateFromVersionedDir('2023-02-19-11:12:13'), + ).toMatchInlineSnapshot(`2023-02-19T09:12:13.000Z`); + + expect( + parseDateFromVersionedDir('2023-08-08-19:17:25'), + ).toMatchInlineSnapshot(`2023-08-08T16:17:25.000Z`); + }); + }); + + describe('getClosestVersionedDir', () => { + it('should work correctly', () => { + expect( + getClosestVersionedDir( + parseDateFromVersionedDir('2023-26-20-11:12:13'), + [ + parseDateFromVersionedDir('2023-02-20-11:12:13'), + parseDateFromVersionedDir('2023-02-19-11:12:13'), + parseDateFromVersionedDir('2023-02-23-11:12:13'), + parseDateFromVersionedDir('2023-02-24-11:12:13'), + ], + ), + ).toMatchInlineSnapshot(`2023-02-24T09:12:13.000Z`); + }); + }); }); diff --git a/src/core.ts b/src/core.ts index 810cd4d..5e4ffde 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,15 +1,26 @@ -import { first, last, parseInt, trim } from 'lodash'; import chalk from 'chalk'; -import { SequenceChar } from './types'; +import { + filter, + first, + includes, + last, + parseInt, + size, + trim, + uniq, +} from 'lodash'; +import { SequenceChar, SongMeta } from './types'; import { COLON, COMMA, + DASH, DOUBLE_LINE_TUPLE, EMPTY_STRING, HASH, NEW_LINE_TUPLE, TEST_ENV, } from './constants'; +import assert from 'node:assert'; const MISSING_SEQUENCE_NUMBER = 1; @@ -84,7 +95,7 @@ export const getMetaSectionsFromTitle = (titleContent: string) => { const [sequence, content] = entry.split(COLON); return { ...accumulator, [sequence]: trim(content) }; - }, {}); + }, {}) as Record; }; export const createSongMock = ( @@ -119,3 +130,47 @@ ${tuples export const convertSequenceToNumber = (sequenceOrderQualifier: string) => parseInt(sequenceOrderQualifier) || MISSING_SEQUENCE_NUMBER; + +export const getVersionedDir = (now = new Date()) => + now.getFullYear() + + DASH + + ('0' + (now.getMonth() + 1)).slice(-2) + + DASH + + ('0' + now.getDate()).slice(-2) + + DASH + + ('0' + now.getHours()).slice(-2) + + COLON + + ('0' + now.getMinutes()).slice(-2) + + COLON + + ('0' + now.getSeconds()).slice(-2); + +export const parseDateFromVersionedDir = (versionFolder: string) => { + const [year, month, day, time] = versionFolder.split(DASH); + const [hour, minute, second] = time.split(COLON); + + return new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(day), + parseInt(hour), + parseInt(minute), + parseInt(second), + ); +}; + +export const getClosestVersionedDir = (diffDate: Date, dates: Date[]) => + first( + dates.sort((a, b) => { + // @ts-ignore + return Math.abs(diffDate - a) - Math.abs(diffDate - b); // sort a before b when the distance is smaller + }), + ); + +export const assertUniqueness = (array: string[]) => + assert.equal( + size(uniq(array)), + size(array), + `There are duplicates: ${filter(array, (value, index, iteratee) => + includes(iteratee, value, index + 1), + ).join(COMMA)}`, + ); diff --git a/src/gDriveConverterRunner.ts b/src/gDriveConverterRunner.ts new file mode 100644 index 0000000..f988f62 --- /dev/null +++ b/src/gDriveConverterRunner.ts @@ -0,0 +1,156 @@ +import fs from 'fs'; +import fsExtra from 'fs-extra'; +import assert from 'node:assert'; +import { isEmpty, isEqual } from 'lodash'; +import { Config } from './proPresenter7SongConverter'; +import { parseDateFromVersionedDir } from './core'; +import { + getBasicDeploymentInfo, + getConvertedAndWrittenToLocalOutDirSongs, + getPreviousDeploymentDirectory, + getSongDiffFromManifest, +} from './converterService'; +import { + getExistingFoldersFromRoot, + getPreviousManifestFileBy, + uploadSongsAndManifestToGDrive, +} from './gDriveService'; + +const getPreviousRemoteInventoryManifest = async ( + deploymentDate: Date, + allPreviousDeploymentDirs: { + deploymentDirDate: Date; + deploymentDir: string; + id: string; + }[], +) => { + const previousDeploymentDirectory = getPreviousDeploymentDirectory( + deploymentDate, + allPreviousDeploymentDirs, + ); + + const previousDeploymentDirFileId = allPreviousDeploymentDirs.find( + ({ deploymentDir }) => isEqual(deploymentDir, previousDeploymentDirectory), + )?.id as string; + + return getPreviousManifestFileBy(previousDeploymentDirFileId); +}; + +export const convertSongsToPP7FormatRemotely = async ({ + sourceDir, + baseLocalDir, + config, +}: { + sourceDir: string; + baseLocalDir: string; + config: Config; +}) => { + assert.ok( + process.env.GDRIVE_ROOT_FOLDER_ID, + 'No `GDRIVE_ROOT_FOLDER_ID` env variable set.', + ); + assert.ok( + process.env.GDRIVE_BES_CLIENT_ID, + 'No `GDRIVE_BES_CLIENT_ID` env variable set.', + ); + assert.ok( + process.env.GDRIVE_BES_CLIENT_SECRET, + 'No `GDRIVE_BES_CLIENT_SECRET` env variable set.', + ); + assert.ok( + process.env.GDRIVE_BES_CLIENT_REFRESH_TOKEN, + 'No `GDRIVE_BES_CLIENT_REFRESH_TOKEN` env variable set.', + ); + assert.ok( + process.env.CONNECT_TO_G_DRIVE, + 'No `CONNECT_TO_G_DRIVE` env variable set.', + ); + + const { + versionedDir, + deploymentDate, + deploymentVersionedDir, + deployableSongs, + currentManifest, + localManifestFilePath, + } = await getBasicDeploymentInfo(sourceDir, baseLocalDir); + + assert.ok( + !fsExtra.pathExistsSync(deploymentVersionedDir), + `The out directory "${deploymentVersionedDir}" does exists already.`, + ); + + // --- + // Create directory + fsExtra.ensureDirSync(deploymentVersionedDir); + + fs.writeFileSync(localManifestFilePath, JSON.stringify(currentManifest)); + + // --- + // Existing deployments + const allPreviousDeploymentDirs = (await getExistingFoldersFromRoot()).map( + ({ id, folderName: deploymentDir }) => ({ + id, + deploymentDir, + deploymentDirDate: parseDateFromVersionedDir(deploymentDir), + }), + ); + const isAFirstDeployment = isEmpty(allPreviousDeploymentDirs); + + if (isAFirstDeployment) { + console.log( + `[Remote]: No previous deployment found in. Skip incremental deployments by doing a full deployment. Please proceed with a full manual import in PP7.`, + ); + + await uploadSongsAndManifestToGDrive( + getConvertedAndWrittenToLocalOutDirSongs( + deployableSongs, + deploymentVersionedDir, + config, + ), + versionedDir, + localManifestFilePath, + ); + + return; + } + + const previousManifest = await getPreviousRemoteInventoryManifest( + deploymentDate, + allPreviousDeploymentDirs, + ); + + const { newOrUpdatedSongs, toBeRemovedFileNames } = getSongDiffFromManifest( + currentManifest, + previousManifest, + ); + + if (isEmpty(newOrUpdatedSongs)) { + console.log( + `[Remote]: Skip incremental local deployments as no changes have been found between the last two versions.`, + ); + return; + } + + const partialDeployableSongs = deployableSongs.filter(({ song: { id } }) => + newOrUpdatedSongs + .map(({ id: newOrUpdatedSongId }) => newOrUpdatedSongId) + .includes(id), + ); + + await uploadSongsAndManifestToGDrive( + getConvertedAndWrittenToLocalOutDirSongs( + partialDeployableSongs, + deploymentVersionedDir, + config, + ), + versionedDir, + localManifestFilePath, + ); + + if (!isEmpty(toBeRemovedFileNames)) { + console.log( + `[Remote]: The following songs have been removed: ${toBeRemovedFileNames} manually.`, + ); + } +}; diff --git a/src/gDriveService.ts b/src/gDriveService.ts new file mode 100644 index 0000000..5001299 --- /dev/null +++ b/src/gDriveService.ts @@ -0,0 +1,147 @@ +import fs from 'fs'; +import { auth, drive } from '@googleapis/drive'; +import pMap from 'p-map'; +import { drive_v3 } from '@googleapis/drive/v3'; +import { logProcessingFile } from './core'; +import { ConvertedFileStats, SongsInventoryManifest } from './types'; +import assert from 'node:assert'; +import { first, size } from 'lodash'; + +const MANIFEST_FILE_NAME = 'manifest.json'; +const FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder'; +const DEFAULT_PP7_MIME_FILE = 'application/octet-stream'; +const JSON_MIME_TYPE = 'application/json'; +const GDRIVE_FIELD_ID = 'id'; + +let gDriveClient: drive_v3.Drive; + +const getGoogleDriveClient: () => Promise = async () => { + if (gDriveClient) { + return gDriveClient as drive_v3.Drive; + } + + const oAuth2Client = new auth.OAuth2( + process.env.GDRIVE_BES_CLIENT_ID, + process.env.GDRIVE_BES_CLIENT_SECRET, + ); + + oAuth2Client.setCredentials({ + refresh_token: process.env.GDRIVE_BES_CLIENT_REFRESH_TOKEN, + }); + + gDriveClient = drive({ + version: 'v3', + auth: oAuth2Client, + }); + + return gDriveClient; +}; + +export const uploadSongsAndManifestToGDrive = async ( + convertedFilesStats: ConvertedFileStats[], + versionedDir: string, + localManifestFilePath: string, +) => { + // --- + // New GDrive folder + const newGDriveFolder = await ( + await getGoogleDriveClient() + ).files.create({ + requestBody: { + name: versionedDir, + mimeType: FOLDER_MIME_TYPE, + parents: [process.env.GDRIVE_ROOT_FOLDER_ID], + }, + media: { + mimeType: FOLDER_MIME_TYPE, + }, + fields: GDRIVE_FIELD_ID, + }); + + // --- + // Upload manifest + await ( + await getGoogleDriveClient() + ).files.create({ + requestBody: { + name: MANIFEST_FILE_NAME, + mimeType: JSON_MIME_TYPE, + parents: [newGDriveFolder.data.id as string], + }, + media: { + mimeType: JSON_MIME_TYPE, + body: fs.createReadStream(localManifestFilePath), + }, + }); + + ( + await pMap( + convertedFilesStats, + async ({ songFileName, songFilePath }) => + ( + await getGoogleDriveClient() + ).files.create({ + requestBody: { + name: songFileName, + mimeType: DEFAULT_PP7_MIME_FILE, + parents: [newGDriveFolder.data.id as string], + }, + media: { + mimeType: DEFAULT_PP7_MIME_FILE, + body: fs.createReadStream(songFilePath), + }, + }), + { concurrency: 5, stopOnError: true }, + ) + ).forEach(({ data }) => { + logProcessingFile(data.name as string, 'Uploaded successfully to GDrive.'); + }); +}; + +export const getExistingFoldersFromRoot = async () => { + const newGDriveFolder = await ( + await getGoogleDriveClient() + ).files.list({ + q: `'${process.env.GDRIVE_ROOT_FOLDER_ID}' in parents and mimeType = '${FOLDER_MIME_TYPE}'`, + fields: 'nextPageToken, files(id, name)', + spaces: 'drive', + }); + + return newGDriveFolder.data.files?.map(({ id, name: folderName }) => ({ + id, + folderName, + })) as { + id: string; + folderName: string; + }[]; +}; + +export const getPreviousManifestFileBy = async ( + previousDeploymentDirFileId: string, +) => { + const previousGDriveManifestFileInDeploymentFolder = await ( + await getGoogleDriveClient() + ).files.list({ + q: `'${previousDeploymentDirFileId}' in parents and name = '${MANIFEST_FILE_NAME}'`, + fields: 'nextPageToken, files(id, name)', + spaces: 'drive', + }); + + assert.ok( + size(previousGDriveManifestFileInDeploymentFolder.data.files) === 1, + 'There should be only one manifest file in the deployment folder.', + ); + + const manifestFileId = first( + previousGDriveManifestFileInDeploymentFolder.data.files, + )?.id as string; + + const { data } = await ( + await getGoogleDriveClient() + ).files.get({ + fileId: manifestFileId, + alt: 'media', + }); + + return data as SongsInventoryManifest; +}; diff --git a/src/index.ts b/src/index.ts index f979d07..bb3ebb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ -export * from './migratorRunner'; +export * from './gDriveConverterRunner'; +export * from './localConverterRunner'; export * from './proPresenter7SongConverter'; export * from './songsParser'; export * from './types'; diff --git a/src/localConverterRunner.ts b/src/localConverterRunner.ts new file mode 100644 index 0000000..bdf9c87 --- /dev/null +++ b/src/localConverterRunner.ts @@ -0,0 +1,139 @@ +import fs from 'fs'; +import fsExtra from 'fs-extra'; +import path from 'path'; +import assert from 'node:assert'; +import { isEmpty } from 'lodash'; +import { Config } from './proPresenter7SongConverter'; +import { MANIFEST_FILE_NAME } from './constants'; +import { + logFileWithLinkInConsole, + logProcessingFile, + parseDateFromVersionedDir, +} from './core'; +import { SongsInventoryManifest } from './types'; +import { + getBasicDeploymentInfo, + getConvertedAndWrittenToLocalOutDirSongs, + getPreviousDeploymentDirectory, + getSongDiffFromManifest, +} from './converterService'; + +const getPreviousLocalInventoryManifest = ( + deploymentDate: Date, + allPreviousDeploymentDirs: { + deploymentDirDate: Date; + deploymentDir: string; + }[], + baseLocalDir: string, +) => { + const previousDeploymentManifestFilePath = path.join( + baseLocalDir, + getPreviousDeploymentDirectory(deploymentDate, allPreviousDeploymentDirs), + MANIFEST_FILE_NAME, + ); + + console.log( + `Previous deployment manifest file path: ${previousDeploymentManifestFilePath}.`, + ); + + const previousInventoryManifest = JSON.parse( + fs.readFileSync(previousDeploymentManifestFilePath).toString(), + ) as SongsInventoryManifest; + + logProcessingFile( + previousDeploymentManifestFilePath, + 'Previous manifest inspection', + ); + logFileWithLinkInConsole(previousDeploymentManifestFilePath); + + return previousInventoryManifest; +}; + +export const convertSongsToPP7FormatLocally = async ({ + sourceDir, + baseLocalDir, + config, +}: { + sourceDir: string; + baseLocalDir: string; + config: Config; +}) => { + const { + deploymentDate, + deploymentVersionedDir, + deployableSongs, + currentManifest, + localManifestFilePath, + } = await getBasicDeploymentInfo(sourceDir, baseLocalDir); + + assert( + !fsExtra.pathExistsSync(deploymentVersionedDir), + `The out directory "${deploymentVersionedDir}" does exists already.`, + ); + + // --- + // Create directory + fsExtra.ensureDirSync(deploymentVersionedDir); + + fs.writeFileSync(localManifestFilePath, JSON.stringify(currentManifest)); + + // --- + // Existing deployments + const allPreviousDeploymentDirs = fsExtra + .readdirSync(baseLocalDir) + .map((deploymentDir) => ({ + deploymentDir, + deploymentDirDate: parseDateFromVersionedDir(deploymentDir), + })); + const isAFirstDeployment = isEmpty(allPreviousDeploymentDirs); + + if (isAFirstDeployment) { + console.log( + `No previous deployment found in "${baseLocalDir}". Skip incremental deployments by doing a full deployment. Please proceed with a full manual import in PP7.`, + ); + + getConvertedAndWrittenToLocalOutDirSongs( + deployableSongs, + deploymentVersionedDir, + config, + ); + + return; + } + + const previousManifest = getPreviousLocalInventoryManifest( + deploymentDate, + allPreviousDeploymentDirs, + baseLocalDir, + ); + + const { newOrUpdatedSongs, toBeRemovedFileNames } = getSongDiffFromManifest( + currentManifest, + previousManifest, + ); + + if (isEmpty(newOrUpdatedSongs)) { + console.log( + `Skip incremental local deployments as no changes have been found between the last two versions.`, + ); + return; + } + + const partialDeployableSongs = deployableSongs.filter(({ song: { id } }) => + newOrUpdatedSongs + .map(({ id: newOrUpdatedSongId }) => newOrUpdatedSongId) + .includes(id), + ); + + getConvertedAndWrittenToLocalOutDirSongs( + partialDeployableSongs, + deploymentVersionedDir, + config, + ); + + if (!isEmpty(toBeRemovedFileNames)) { + console.log( + `The following songs have been removed: ${toBeRemovedFileNames} manually.`, + ); + } +}; diff --git a/src/manifestGenerator.ts b/src/manifestGenerator.ts new file mode 100644 index 0000000..a8949cf --- /dev/null +++ b/src/manifestGenerator.ts @@ -0,0 +1,26 @@ +import dotenv from 'dotenv'; +import { assertUniqueness } from './core'; +import { Song, SongManifest } from './types'; + +dotenv.config(); + +export const generateManifest = async ( + deployableSongs: Array<{ + song: Song; + fileName: string; + fileAsText: string; + }>, +) => { + assertUniqueness(deployableSongs.map(({ song }) => song.id)); + + return deployableSongs + .map( + ({ song: { id, contentHash }, fileName }) => + ({ + id, + fileName, + contentHash, + } as SongManifest), + ) + .sort((a, b) => a.fileName.localeCompare(b.fileName)); +}; diff --git a/src/migratorRunner.ts b/src/migratorRunner.ts deleted file mode 100644 index 4d99958..0000000 --- a/src/migratorRunner.ts +++ /dev/null @@ -1,54 +0,0 @@ -import fs from 'fs'; -import fsExtra from 'fs-extra'; -import path from 'path'; -import recursive from 'recursive-readdir'; -import { Presentation } from '../proto/presentation'; -import { parseSong } from './songsParser'; -import { - Config, - convertSongToProPresenter7, -} from './proPresenter7SongConverter'; -import { EMPTY_STRING, PRO_EXTENSION, TXT_EXTENSION } from './constants'; - -/** - * Removes all the files from the out directory - */ -export const migrateSongsToPP7Format = async ({ - sourceDir, - outDir, - clearOutputDirFirst, - config, -}: { - sourceDir: string; - outDir: string; - clearOutputDirFirst?: boolean; - config: Config; -}) => { - if (clearOutputDirFirst) { - fsExtra.emptydirSync(outDir); - } - - await fsExtra.ensureDirSync(outDir); - - (await recursive(sourceDir, ['.DS_Store'])).forEach((filePath) => { - const fileAsText = fs.readFileSync(filePath).toString(); - const fileName = path.basename(filePath); - - console.log(`Processing "${filePath}"...`); - - const song = parseSong(fileAsText); - const presentation = convertSongToProPresenter7(song, config); - - const outFile = `${outDir}/${fileName.replace( - TXT_EXTENSION, - EMPTY_STRING, - )}${PRO_EXTENSION}`; - - fs.writeFileSync( - outFile, - Buffer.from(Presentation.encode(presentation).finish()), - ); - - console.log(`Successfully generated "${outFile}".`); - }); -}; diff --git a/src/songsParser.spec.ts b/src/songsParser.spec.ts index 0649229..0af7f98 100644 --- a/src/songsParser.spec.ts +++ b/src/songsParser.spec.ts @@ -116,6 +116,9 @@ describe('songsParser', () => { expect(parseSong(SONG_WITH_SUBSECTIONS_MOCK_FILE_CONTENT)) .toMatchInlineSnapshot(` { + "author": "CustomAuthor", + "contentHash": "#customHash", + "id": "customId", "sequence": [ "[v1.1]", "[v1.2]", @@ -287,6 +290,9 @@ describe('songsParser', () => { expect(parseSong(SONG_WITH_SUBSECTIONS_MOCK_FILE_CONTENT)) .toMatchInlineSnapshot(` { + "author": "CustomAuthor", + "contentHash": "#customHash", + "id": "customId", "sequence": [ "[v1.1]", "[v1.2]", diff --git a/src/songsParser.ts b/src/songsParser.ts index a79fd98..f8463af 100644 --- a/src/songsParser.ts +++ b/src/songsParser.ts @@ -20,6 +20,7 @@ export const parseSong = (songContent: string): Song => { const hashMap = {} as Record; const sections = [] as Section[]; + let rawTitle = ''; for ( let sectionIndex = 0; @@ -32,7 +33,8 @@ export const parseSong = (songContent: string): Song => { hashMap[sectionIdentifier] = songSectionContent; if (sectionIdentifier === SongSection.TITLE) { - hashMap[SongSection.TITLE] = first( + rawTitle = songSectionContent; + hashMap[sectionIdentifier] = first( getTitleBySections(songSectionContent), ) as string; } @@ -71,9 +73,10 @@ export const parseSong = (songContent: string): Song => { } const titleContent = hashMap[SongSection.TITLE]; - const metaSectionsFromTitle = getMetaSectionsFromTitle( - titleContent, - ) as Record; + const metaSectionsFromTitle = getMetaSectionsFromTitle(rawTitle) as Record< + SongMeta, + string + >; return pickBy( { diff --git a/src/types.ts b/src/types.ts index f3c3bb6..87a8e01 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,3 +115,25 @@ export type Song = { verses: Section[]; version?: string; }; + +export type SongManifest = { + id: string; + fileName: string; + contentHash: string; +}; + +export type SongsInventoryManifest = { + updatedOn: string; + inventory: SongManifest[]; +}; + +export type DeployableSong = { + song: Song; + fileName: string; + fileAsText: string; +}; + +export type ConvertedFileStats = { + songFileName: string; + songFilePath: string; +};