diff --git a/manifest.ts b/manifest.ts index 3ac74bc..d477e06 100755 --- a/manifest.ts +++ b/manifest.ts @@ -31,14 +31,7 @@ const manifest: chrome.runtime.ManifestV3 = { ], web_accessible_resources: [ { - resources: [ - "assets/js/*.js", - "assets/css/*.css", - // TODO Needed? - // "stealie-16.png", - // "stealie-48.png", - "stealie-128.png", - ], + resources: ["assets/js/*.js", "assets/css/*.css", "stealie-128.png"], matches: ["*://*/*"], }, ], diff --git a/package.json b/package.json index 3e323de..845d7b4 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ }, "type": "module", "dependencies": { + "jszip": "^3.10.1", "lodash-es": "^4.17.21", "react": "18.2.0", - "react-dom": "18.2.0", - "streamsaver": "^2.0.6" + "react-dom": "18.2.0" }, "devDependencies": { "@rollup/plugin-typescript": "^8.5.0", diff --git a/public/stealie-128.png b/public/stealie-128.png index 22c0c0a..e7ef775 100644 Binary files a/public/stealie-128.png and b/public/stealie-128.png differ diff --git a/public/stealie-16.png b/public/stealie-16.png deleted file mode 100644 index 17a15af..0000000 Binary files a/public/stealie-16.png and /dev/null differ diff --git a/public/stealie-48.png b/public/stealie-48.png deleted file mode 100644 index 0e14ed1..0000000 Binary files a/public/stealie-48.png and /dev/null differ diff --git a/src/pages/content/components/app.tsx b/src/pages/content/components/app.tsx index 3bdf2c5..84f5613 100644 --- a/src/pages/content/components/app.tsx +++ b/src/pages/content/components/app.tsx @@ -1,15 +1,14 @@ import { FC, useEffect, useState } from "react"; import { ArchiveFile, - MP3Url, ArchiveShow, + Track, } from "@pages/content/models/interfaces"; -import streamSaver from "streamsaver"; import pickBy from "lodash-es/pickBy"; import findKey from "lodash-es/findKey"; -import ZIP from "@pages/content/utils/zip-stream.js"; +import JSZip from "jszip"; -const getMP3Urls = (show: ArchiveShow) => { +const getTracks = (show: ArchiveShow): Track[] => { const mp3Files = pickBy(show.files, function (file: ArchiveFile) { return file.format === "VBR MP3"; }); @@ -19,7 +18,7 @@ const getMP3Urls = (show: ArchiveShow) => { const url = baseURL + key; let title = data.title || data.original; title = title.replace(/-|[^-_,A-Za-z0-9 ]+/g, "").trim(); - return { title: index + 1 + ". " + title, track: title, url: url }; + return { title: index + 1 + ". " + title, url: url } as Track; }); }; @@ -32,40 +31,52 @@ const getInfoFileUrl = (show: ArchiveShow) => { return `${baseURL}/${infoFile}`; }; -// TODO Allow option for selecting show format OR -// Prompt user for download title? window.prompt? const getShowTitle = (show: ArchiveShow) => { - return show.metadata.date[0]; + return prompt( + `Custom folder title? Default ${show.metadata.date[0]}`, + show.metadata.date[0] + ); }; -const createZip = async (show: ArchiveShow) => { - const fileStream = streamSaver.createWriteStream(`${getShowTitle(show)}.zip`); - const mp3s = getMP3Urls(show); - - const readableZipStream = new ZIP({ - async pull(ctrl) { - // Gets executed everytime zip.js asks for more data - const infoFile = await fetch(getInfoFileUrl(show)); - ctrl.enqueue({ - name: "info.txt", - stream: () => infoFile.body, - }); +async function fetchWithRedirect(url: string) { + const response = await fetch(url, { redirect: "follow" }); + if (response.status === 302) { + const redirectUrl = response.headers.get("Location"); + if (!redirectUrl) { + throw new Error("Redirect URL not found"); + } + return fetch(redirectUrl); // Fetch the redirect URL + } else if (response.ok) { + return response; + } else { + throw new Error("Network response was not ok"); + } +} - // TODO Implement range fetching: https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/ - await Promise.all( - mp3s.map(async (mp3) => { - const res = await fetch(mp3.url); - const stream = () => res.body; - ctrl.enqueue({ name: `${mp3.title}.mp3`, stream }); - }) +async function fetchWithRetry(url: string, maxRetries = 3): Promise { + let retries = 0; + while (true) { + try { + const response = await fetchWithRedirect(url); + if (response.ok) { + return response; + } else { + throw new Error(`Failed to fetch: ${response.statusText}`); + } + } catch (error) { + retries++; + if (retries >= maxRetries) { + throw error; // If max retries exceeded, propagate the error + } + console.log( + `Error occurred, retrying (${retries}/${maxRetries}):`, + error ); - - ctrl.close(); - }, - }); - - return readableZipStream.pipeTo(fileStream); -}; + // Wait for a short duration before retrying (you can adjust the duration as needed) + await new Promise((resolve) => setTimeout(resolve, 1000 * retries)); + } + } +} export default function App() { const [archiveShow, setArchiveShow] = useState(); @@ -99,6 +110,7 @@ export default function App() { >

Grateful Grabber

+ @@ -106,25 +118,199 @@ export default function App() { ); } +const DownloadIndividualSong: FC<{ show: ArchiveShow }> = ({ show }) => { + const [loadingTracks, setLoadingTracks] = useState([]); + const tracks = getTracks(show); + + const onDownload = async (event) => { + const selectedOption = event.target.selectedOptions[0]; + const title = selectedOption.text; + const url = selectedOption.value; + setLoadingTracks((prevState) => [...prevState, title]); + await downloadFile(url, title, ".mp3"); + setLoadingTracks(loadingTracks.filter((t) => t == title)); + }; + + return ( +
+ + + {loadingTracks.length > 0 ? ( +
+

Download queue

+ {loadingTracks.map((title) => ( + {title} + ))} +
+ ) : null} +
+ ); +}; + const DownloadButton: FC<{ show: ArchiveShow }> = ({ show }) => { const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [error, setError] = useState(null); const downloadShow = async (archiveShow: ArchiveShow) => { + setError(null); + + const showTitle = getShowTitle(archiveShow); + if (!showTitle) return; + setLoading(true); - await createZip(archiveShow); + + await createZip(archiveShow, setProgress) + .then((blob) => downloadZip(showTitle, blob, setProgress)) + .catch((error) => { + setError(error.toString()); + setLoading(false); + console.error("Error:", error); + }); + setLoading(false); + setProgress(0); }; return ( - + <> + + {progress ? : null} + {error && ( + + Something went wrong, restart the download, it should pick up where + you left off :) + + )} + ); }; + +async function downloadFile(url: string, fileName: string, extension = ".mp3") { + const response = await fetchWithRetry(url); + if (!response.ok) { + throw new Error(`Failed to download ${url}`); + } + + const blob = await response.blob(); + + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `${fileName}${extension}`; + + a.click(); + + URL.revokeObjectURL(a.href); +} + +async function getFileBlob( + url: string, + onProgress?: (progress: number) => void +): Promise { + const response = await fetchWithRetry(url); + if (!response.ok) { + throw new Error(`Failed to download ${url}`); + } + + const contentLength = parseInt( + response.headers.get("Content-Length") || "0", + 10 + ); + let receivedLength = 0; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + chunks.push(value); + receivedLength += value.length; + const progress = receivedLength / contentLength; + onProgress?.(progress); + } + + return new Blob(chunks); +} + +async function createZip( + show: ArchiveShow, + onProgress: (progress: number) => void +): Promise { + const zip = new JSZip(); + let completedCount = 0; + const mp3Urls = getTracks(show); + const infoFile = getInfoFileUrl(show); + + const infoBlob = await getFileBlob(infoFile); + zip.file("info.txt", infoBlob); + + for (const url of mp3Urls) { + const mp3Blob = await getFileBlob(url.url, (progress) => { + const overallProgress = (completedCount + progress) / mp3Urls.length; + onProgress(overallProgress); + }); + + zip.file(`${url.title}.mp3`, mp3Blob); + completedCount++; + onProgress(completedCount / mp3Urls.length); + } + + return await zip.generateAsync({ type: "blob" }); +} + +function downloadZip( + folderName: string, + blob: Blob, + onProgress: (progress: number) => void +) { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${folderName}.zip`; + a.click(); + onProgress(1); // Mark progress as completed +} diff --git a/src/pages/content/models/interfaces.ts b/src/pages/content/models/interfaces.ts index fe9b081..ce2c7df 100644 --- a/src/pages/content/models/interfaces.ts +++ b/src/pages/content/models/interfaces.ts @@ -21,8 +21,7 @@ export interface Metadata { date: string[]; } -export interface MP3Url { +export interface Track { title: string; - track: string; url: string; } diff --git a/src/pages/content/style.scss b/src/pages/content/style.scss index 0654416..be152c6 100644 --- a/src/pages/content/style.scss +++ b/src/pages/content/style.scss @@ -3,3 +3,17 @@ .content-view { font-size: 30px; } + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 120px; + height: 120px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/src/pages/content/utils/zip-stream.js b/src/pages/content/utils/zip-stream.js deleted file mode 100644 index dbbe727..0000000 --- a/src/pages/content/utils/zip-stream.js +++ /dev/null @@ -1,234 +0,0 @@ -class Crc32 { - constructor() { - this.crc = -1; - } - - append(data) { - var crc = this.crc | 0; - var table = this.table; - for (var offset = 0, len = data.length | 0; offset < len; offset++) { - crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xff]; - } - this.crc = crc; - } - - get() { - return ~this.crc; - } -} -Crc32.prototype.table = (() => { - var i; - var j; - var t; - var table = []; - for (i = 0; i < 256; i++) { - t = i; - for (j = 0; j < 8; j++) { - t = t & 1 ? (t >>> 1) ^ 0xedb88320 : t >>> 1; - } - table[i] = t; - } - return table; -})(); - -const getDataHelper = (byteLength) => { - var uint8 = new Uint8Array(byteLength); - return { - array: uint8, - view: new DataView(uint8.buffer), - }; -}; - -const pump = (zipObj) => - zipObj.reader.read().then((chunk) => { - if (chunk.done) return zipObj.writeFooter(); - const outputData = chunk.value; - zipObj.crc.append(outputData); - zipObj.uncompressedLength += outputData.length; - zipObj.compressedLength += outputData.length; - zipObj.ctrl.enqueue(outputData); - }); - -/** - * [createWriter description] - * @param {Object} underlyingSource [description] - * @return {Boolean} [description] - */ -function createWriter(underlyingSource) { - const files = Object.create(null); - const filenames = []; - const encoder = new TextEncoder(); - let offset = 0; - let activeZipIndex = 0; - let ctrl; - let activeZipObject, closed; - - function next() { - activeZipIndex++; - activeZipObject = files[filenames[activeZipIndex]]; - if (activeZipObject) processNextChunk(); - else if (closed) closeZip(); - } - - var zipWriter = { - enqueue(fileLike) { - if (closed) - throw new TypeError( - "Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed" - ); - - let name = fileLike.name.trim(); - const date = new Date( - typeof fileLike.lastModified === "undefined" - ? Date.now() - : fileLike.lastModified - ); - - if (fileLike.directory && !name.endsWith("/")) name += "/"; - if (files[name]) throw new Error("File already exists."); - - const nameBuf = encoder.encode(name); - filenames.push(name); - - const zipObject = (files[name] = { - level: 0, - ctrl, - directory: !!fileLike.directory, - nameBuf, - comment: encoder.encode(fileLike.comment || ""), - compressedLength: 0, - uncompressedLength: 0, - writeHeader() { - var header = getDataHelper(26); - var data = getDataHelper(30 + nameBuf.length); - - zipObject.offset = offset; - zipObject.header = header; - if (zipObject.level !== 0 && !zipObject.directory) { - header.view.setUint16(4, 0x0800); - } - header.view.setUint32(0, 0x14000808); - header.view.setUint16( - 6, - (((date.getHours() << 6) | date.getMinutes()) << 5) | - (date.getSeconds() / 2), - true - ); - header.view.setUint16( - 8, - ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << - 5) | - date.getDate(), - true - ); - header.view.setUint16(22, nameBuf.length, true); - data.view.setUint32(0, 0x504b0304); - data.array.set(header.array, 4); - data.array.set(nameBuf, 30); - offset += data.array.length; - ctrl.enqueue(data.array); - }, - writeFooter() { - var footer = getDataHelper(16); - footer.view.setUint32(0, 0x504b0708); - - if (zipObject.crc) { - zipObject.header.view.setUint32(10, zipObject.crc.get(), true); - zipObject.header.view.setUint32( - 14, - zipObject.compressedLength, - true - ); - zipObject.header.view.setUint32( - 18, - zipObject.uncompressedLength, - true - ); - footer.view.setUint32(4, zipObject.crc.get(), true); - footer.view.setUint32(8, zipObject.compressedLength, true); - footer.view.setUint32(12, zipObject.uncompressedLength, true); - } - - ctrl.enqueue(footer.array); - offset += zipObject.compressedLength + 16; - next(); - }, - fileLike, - }); - - if (!activeZipObject) { - activeZipObject = zipObject; - processNextChunk(); - } - }, - close() { - if (closed) - throw new TypeError( - "Cannot close a readable stream that has already been requested to be closed" - ); - if (!activeZipObject) closeZip(); - closed = true; - }, - }; - - function closeZip() { - var length = 0; - var index = 0; - var indexFilename, file; - for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) { - file = files[filenames[indexFilename]]; - length += 46 + file.nameBuf.length + file.comment.length; - } - const data = getDataHelper(length + 22); - for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) { - file = files[filenames[indexFilename]]; - data.view.setUint32(index, 0x504b0102); - data.view.setUint16(index + 4, 0x1400); - data.array.set(file.header.array, index + 6); - data.view.setUint16(index + 32, file.comment.length, true); - if (file.directory) { - data.view.setUint8(index + 38, 0x10); - } - data.view.setUint32(index + 42, file.offset, true); - data.array.set(file.nameBuf, index + 46); - data.array.set(file.comment, index + 46 + file.nameBuf.length); - index += 46 + file.nameBuf.length + file.comment.length; - } - data.view.setUint32(index, 0x504b0506); - data.view.setUint16(index + 8, filenames.length, true); - data.view.setUint16(index + 10, filenames.length, true); - data.view.setUint32(index + 12, length, true); - data.view.setUint32(index + 16, offset, true); - ctrl.enqueue(data.array); - ctrl.close(); - } - - function processNextChunk() { - if (!activeZipObject) return; - if (activeZipObject.directory) - return activeZipObject.writeFooter(activeZipObject.writeHeader()); - if (activeZipObject.reader) return pump(activeZipObject); - if (activeZipObject.fileLike.stream) { - activeZipObject.crc = new Crc32(); - activeZipObject.reader = activeZipObject.fileLike.stream().getReader(); - activeZipObject.writeHeader(); - } else next(); - } - return new ReadableStream({ - start: (c) => { - ctrl = c; - underlyingSource.start && - Promise.resolve(underlyingSource.start(zipWriter)); - }, - pull() { - return ( - processNextChunk() || - (underlyingSource.pull && - Promise.resolve(underlyingSource.pull(zipWriter))) - ); - }, - }); -} - -export default createWriter; -// window.ZIP = createWriter; diff --git a/yarn.lock b/yarn.lock index 79bf491..9b9a0a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1454,6 +1454,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" @@ -2413,6 +2418,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immutable@^4.0.0: version "4.3.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" @@ -2447,7 +2457,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2618,6 +2628,11 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3154,6 +3169,21 @@ jsonfile@^6.0.1: object.assign "^4.1.4" object.values "^1.1.6" +jszip-utils@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jszip-utils/-/jszip-utils-0.1.0.tgz#8c04cdedcdb291e83f055f5b261b3a3188ceca0b" + integrity sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg== + +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -3179,6 +3209,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -3513,6 +3550,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -3649,6 +3691,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3730,6 +3777,19 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -3839,6 +3899,11 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -3912,6 +3977,11 @@ set-function-name@^2.0.0: functions-have-names "^1.2.3" has-property-descriptors "^1.0.0" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -4026,11 +4096,6 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -streamsaver@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/streamsaver/-/streamsaver-2.0.6.tgz#869d2347dd70191e0ac888d52296956a8cba2ed9" - integrity sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw== - string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -4099,6 +4164,13 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -4360,6 +4432,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + v8-to-istanbul@^9.0.1: version "9.2.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"