diff --git a/config/defaults.js b/config/defaults.js index b89a920a..2a8810a3 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -42,7 +42,6 @@ const defaultConfig = { api_root: "http://ws.audioscrobbler.com/2.0/", api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123 secret: "a5d2a36fdf64819290f6982481eaffa2", - suffixesToRemove: [' - Topic', 'VEVO'] // removes suffixes of the artist name, for better recognition }, discord: { enabled: false, diff --git a/package.json b/package.json index acddcb90..16c880a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "youtube-music", "productName": "YouTube Music", - "version": "1.12.0", + "version": "1.12.1", "description": "YouTube Music Desktop App - including custom plugins", "license": "MIT", "repository": "th-ch/youtube-music", @@ -42,6 +42,7 @@ "scripts": { "test": "jest", "start": "electron .", + "start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .", "icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated", "generate:package": "node utils/generate-package-json.js", "postinstall": "yarn run icon && yarn run plugins", @@ -69,6 +70,7 @@ "async-mutex": "^0.3.1", "browser-id3-writer": "^4.4.0", "custom-electron-prompt": "^1.1.0", + "chokidar": "^3.5.1", "custom-electron-titlebar": "^3.2.6", "discord-rpc": "^3.2.0", "electron-debug": "^3.2.0", diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 49b5eea5..ac32623d 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -7,6 +7,7 @@ const { dialog, ipcMain } = require("electron"); const getSongInfo = require("../../providers/song-info"); const { injectCSS, listenAction } = require("../utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); +const { getImage } = require("../../providers/song-info"); const sendError = (win, err) => { const dialogOpts = { @@ -41,23 +42,29 @@ function handle(win) { } }); - ipcMain.on("add-metadata", (event, filePath, songBuffer, currentMetadata) => { + ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => { let fileBuffer = songBuffer; const songMetadata = { ...metadata, ...currentMetadata }; + if (!songMetadata.image && songMetadata.imageSrc) { + songMetadata.image = await getImage(songMetadata.imageSrc); + } + try { - const coverBuffer = songMetadata.image.toPNG(); + const coverBuffer = songMetadata.image ? songMetadata.image.toPNG() : null; const writer = new ID3Writer(songBuffer); // Create the metadata tags writer .setFrame("TIT2", songMetadata.title) - .setFrame("TPE1", [songMetadata.artist]) - .setFrame("APIC", { + .setFrame("TPE1", [songMetadata.artist]); + if (coverBuffer) { + writer.setFrame("APIC", { type: 3, data: coverBuffer, description: "", }); + } writer.addTag(); fileBuffer = Buffer.from(writer.arrayBuffer); } catch (error) { diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index e4930f09..200a7e15 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -38,13 +38,18 @@ const baseUrl = defaultConfig.url; // contextBridge.exposeInMainWorld("downloader", { // download: () => { global.download = () => { + let metadata; let videoUrl = getSongMenu() .querySelector("ytmusic-menu-navigation-item-renderer") .querySelector("#navigation-endpoint") .getAttribute("href"); - videoUrl = !videoUrl - ? global.songInfo.url || window.location.href - : baseUrl + "/" + videoUrl; + if (videoUrl) { + videoUrl = baseUrl + "/" + videoUrl; + metadata = null; + } else { + metadata = global.songInfo; + videoUrl = metadata.url || window.location.href; + } downloadVideoToMP3( videoUrl, @@ -61,7 +66,7 @@ global.download = () => { }, reinit, pluginOptions, - global.songInfo + metadata ); }; // }); diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index b4800808..f5b99d73 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -5,6 +5,7 @@ const { URL } = require("url"); const { dialog, ipcMain } = require("electron"); const is = require("electron-is"); const ytpl = require("ytpl"); +const chokidar = require('chokidar'); const { setOptions } = require("../../config/plugins"); const getSongInfo = require("../../providers/song-info"); @@ -15,7 +16,7 @@ let downloadLabel = defaultMenuDownloadLabel; let metadataURL = undefined; let callbackIsRegistered = false; -module.exports = (win, options, refreshMenu) => { +module.exports = (win, options) => { if (!callbackIsRegistered) { const registerCallback = getSongInfo(win); registerCallback((info) => { @@ -35,7 +36,10 @@ module.exports = (win, options, refreshMenu) => { return; } - const playlist = await ytpl(playlistID); + console.log("trying to get playlist ID" +playlistID); + const playlist = await ytpl(playlistID, + { limit: options.playlistMaxItems || Infinity } + ); const playlistTitle = playlist.title; const folder = getFolder(options.downloadFolder); @@ -49,24 +53,40 @@ module.exports = (win, options, refreshMenu) => { } mkdirSync(playlistFolder, { recursive: true }); - ipcMain.on("downloader-feedback", (_, feedback) => { - downloadLabel = feedback; - refreshMenu(); + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Started Download", + message: `Downloading Playlist "${playlistTitle}"`, + detail: `(${playlist.items.length} songs)`, }); - downloadLabel = `Downloading "${playlistTitle}"`; - refreshMenu(); - if (is.dev()) { console.log( `Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)` ); } - playlist.items.slice(0, options.playlistMaxItems).forEach((song) => { + const steps = 1 / playlist.items.length; + let progress = 0; + + win.setProgressBar(2); // starts with indefinite bar + + let dirWatcher = chokidar.watch(playlistFolder); + dirWatcher.on('add', () => { + progress += steps; + if (progress >= 0.9999) { + win.setProgressBar(-1); // close progress bar + dirWatcher.close().then(() => dirWatcher = null); + } else { + win.setProgressBar(progress); + } + }); + + playlist.items.forEach((song) => { win.webContents.send( "downloader-download-playlist", - song, + song.url, playlistTitle, options ); diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js index 1a212aaf..814d9924 100644 --- a/plugins/downloader/youtube-dl.js +++ b/plugins/downloader/youtube-dl.js @@ -14,7 +14,8 @@ const ytdl = require("ytdl-core"); const { triggerAction, triggerActionSync } = require("../utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); -const { defaultMenuDownloadLabel, getFolder } = require("./utils"); +const { getFolder } = require("./utils"); +const { cleanupArtistName } = require("../../providers/song-info"); const { createFFmpeg } = FFmpeg; const ffmpeg = createFFmpeg({ @@ -24,7 +25,7 @@ const ffmpeg = createFFmpeg({ }); const ffmpegMutex = new Mutex(); -const downloadVideoToMP3 = ( +const downloadVideoToMP3 = async ( videoUrl, sendFeedback, sendError, @@ -35,6 +36,16 @@ const downloadVideoToMP3 = ( ) => { sendFeedback("Downloading…"); + if (metadata === null) { + const info = await ytdl.getInfo(videoUrl); + const thumbnails = info.videoDetails?.author?.thumbnails; + metadata = { + artist: info.videoDetails?.media?.artist || cleanupArtistName(info.videoDetails?.author?.name) || "", + title: info.videoDetails?.media?.song || info.videoDetails?.title || "", + imageSrc: thumbnails ? thumbnails[thumbnails.length - 1].url : "" + } + } + let videoName = "YouTube Music - Unknown title"; let videoReadableStream; try { @@ -135,6 +146,7 @@ const toMP3 = async ( ipcRenderer.send("add-metadata", filePath, fileBuffer, { artist: metadata.artist, title: metadata.title, + imageSrc: metadata.imageSrc }); ipcRenderer.once("add-metadata-done", reinit); } catch (e) { @@ -165,22 +177,16 @@ module.exports = { ipcRenderer.on( "downloader-download-playlist", - (_, songMetadata, playlistFolder, options) => { - const reinit = () => - ipcRenderer.send("downloader-feedback", defaultMenuDownloadLabel); - + (_, url, playlistFolder, options) => { downloadVideoToMP3( - songMetadata.url, - (feedback) => { - ipcRenderer.send("downloader-feedback", feedback); - }, + url, + () => {}, (error) => { triggerAction(CHANNEL, ACTIONS.ERROR, error); - reinit(); }, - reinit, + () => {}, options, - songMetadata, + null, playlistFolder ); } diff --git a/plugins/last-fm/back.js b/plugins/last-fm/back.js index a34a0a19..efa4f41e 100644 --- a/plugins/last-fm/back.js +++ b/plugins/last-fm/back.js @@ -5,21 +5,10 @@ const { setOptions } = require('../../config/plugins'); const getSongInfo = require('../../providers/song-info'); const defaultConfig = require('../../config/defaults'); -const cleanupArtistName = (config, artist) => { - // removes the suffixes of the artist name for more recognition by last.fm - const { suffixesToRemove } = config; - if (suffixesToRemove === undefined) return artist; - - for (suffix of suffixesToRemove) { - artist = artist.replace(suffix, ''); - } - return artist; -} - const createFormData = params => { // creates the body for in the post request const formData = new URLSearchParams(); - for (key in params) { + for (const key in params) { formData.append(key, params[key]); } return formData; @@ -28,7 +17,7 @@ const createQueryString = (params, api_sig) => { // creates a querystring const queryData = []; params.api_sig = api_sig; - for (key in params) { + for (const key in params) { queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`); } return '?'+queryData.join('&'); @@ -37,12 +26,12 @@ const createQueryString = (params, api_sig) => { const createApiSig = (params, secret) => { // this function creates the api signature, see: https://www.last.fm/api/authspec const keys = []; - for (key in params) { + for (const key in params) { keys.push(key); } keys.sort(); let sig = ''; - for (key of keys) { + for (const key of keys) { if (String(key) === 'format') continue sig += `${key}${params[key]}`; @@ -157,8 +146,6 @@ const lastfm = async (win, config) => { registerCallback( songInfo => { // set remove the old scrobble timer clearTimeout(scrobbleTimer); - // make the artist name a bit cleaner - songInfo.artist = cleanupArtistName(config, songInfo.artist); if (!songInfo.isPaused) { setNowPlaying(songInfo, config); // scrobble when the song is half way through, or has passed the 4 minute mark diff --git a/plugins/precise-volume/front.js b/plugins/precise-volume/front.js index 5b66b31f..f44aa643 100644 --- a/plugins/precise-volume/front.js +++ b/plugins/precise-volume/front.js @@ -192,12 +192,13 @@ function setupLocalArrowShortcuts(options) { } function callback(event) { - event.preventDefault(); switch (event.code) { case "ArrowUp": + event.preventDefault(); changeVolume(true, options); break; case "ArrowDown": + event.preventDefault(); changeVolume(false, options); break; } diff --git a/plugins/precise-volume/preload.js b/plugins/precise-volume/preload.js index bfd6a01c..edc5f20c 100644 --- a/plugins/precise-volume/preload.js +++ b/plugins/precise-volume/preload.js @@ -1,28 +1,33 @@ const { ipcRenderer } = require("electron"); +const is = require("electron-is"); + +let ignored = { + id: ["volume-slider", "expand-volume-slider"], + types: ["mousewheel", "keydown", "keyup"] +}; -// Override specific listeners of volume-slider by modifying Element.prototype function overrideAddEventListener() { - // Events to ignore - const nativeEvents = ["mousewheel", "keydown", "keyup"]; // Save native addEventListener Element.prototype._addEventListener = Element.prototype.addEventListener; // Override addEventListener to Ignore specific events in volume-slider Element.prototype.addEventListener = function (type, listener, useCapture = false) { - if (this.tagName === "TP-YT-PAPER-SLIDER") { // tagName of #volume-slider - for (const eventType of nativeEvents) { - if (eventType === type) { - return; - } - } - }//else - this._addEventListener(type, listener, useCapture); + if (!( + ignored.id.includes(this.id) && + ignored.types.includes(type) + )) { + this._addEventListener(type, listener, useCapture); + } else if (is.dev()) { + console.log(`Ignoring event: "${this.id}.${type}()"`); + } }; } module.exports = () => { overrideAddEventListener(); // Restore original function after did-finish-load to avoid keeping Element.prototype altered - ipcRenderer.once("restoreAddEventListener", () => { //called from Main to make sure page is completly loaded + ipcRenderer.once("restoreAddEventListener", () => { // Called from main to make sure page is completly loaded Element.prototype.addEventListener = Element.prototype._addEventListener; + Element.prototype._addEventListener = undefined; + ignored = undefined; }); }; diff --git a/plugins/utils.js b/plugins/utils.js index ed8c7c0a..b265692f 100644 --- a/plugins/utils.js +++ b/plugins/utils.js @@ -43,7 +43,7 @@ module.exports.fileExists = (path, callbackIfExists) => { }; module.exports.injectCSS = (webContents, filepath, cb = undefined) => { - webContents.once("did-finish-load", async () => { + webContents.on("did-finish-load", async () => { await webContents.insertCSS(fs.readFileSync(filepath, "utf8")); if (cb) { cb(); diff --git a/preload.js b/preload.js index 6cdaebdd..5a09c845 100644 --- a/preload.js +++ b/preload.js @@ -5,6 +5,8 @@ const { remote } = require("electron"); const config = require("./config"); const { fileExists } = require("./plugins/utils"); const setupFrontLogger = require("./providers/front-logger"); +const setupSongControl = require("./providers/song-controls-front"); +const setupSongInfo = require("./providers/song-info-front"); const plugins = config.plugins.getEnabled(); @@ -37,8 +39,10 @@ document.addEventListener("DOMContentLoaded", () => { }); // inject song-info provider - const songInfoProviderPath = path.join(__dirname, "providers", "song-info-front.js") - fileExists(songInfoProviderPath, require(songInfoProviderPath)); + setupSongInfo(); + + // inject song-control provider + setupSongControl(); // inject front logger setupFrontLogger(); diff --git a/providers/song-controls-front.js b/providers/song-controls-front.js new file mode 100644 index 00000000..1860e761 --- /dev/null +++ b/providers/song-controls-front.js @@ -0,0 +1,18 @@ +const { ipcRenderer } = require("electron"); + +let videoStream = document.querySelector(".video-stream"); +module.exports = () => { + ipcRenderer.on("playPause", () => { + if (!videoStream) { + videoStream = document.querySelector(".video-stream"); + } + + if (videoStream.paused) { + videoStream.play(); + } else { + videoStream.yns_pause ? + videoStream.yns_pause() : + videoStream.pause(); + } + }); +}; diff --git a/providers/song-controls.js b/providers/song-controls.js index 7f43df11..5d3ad953 100644 --- a/providers/song-controls.js +++ b/providers/song-controls.js @@ -12,7 +12,7 @@ module.exports = (win) => { // Playback previous: () => pressKey(win, "k"), next: () => pressKey(win, "j"), - playPause: () => pressKey(win, "space"), + playPause: () => win.webContents.send("playPause"), like: () => pressKey(win, "_"), dislike: () => pressKey(win, "+"), go10sBack: () => pressKey(win, "h"), diff --git a/providers/song-info-front.js b/providers/song-info-front.js index fbc98b99..fccc54a8 100644 --- a/providers/song-info-front.js +++ b/providers/song-info-front.js @@ -10,17 +10,16 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => { }); const injectListener = () => { - var oldXHR = window.XMLHttpRequest; + const oldXHR = window.XMLHttpRequest; function newXHR() { - var realXHR = new oldXHR(); + const realXHR = new oldXHR(); realXHR.addEventListener( "readystatechange", () => { - if (realXHR.readyState == 4 && realXHR.status == 200) { - if (realXHR.responseURL.includes("/player")) { + if (realXHR.readyState === 4 && realXHR.status === 200 + && realXHR.responseURL.includes("/player")) { // if the request contains the song info, send the response to ipcMain ipcRenderer.send("song-info-request", realXHR.responseText); - } } }, false diff --git a/providers/song-info.js b/providers/song-info.js index a7aa651d..15b51459 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -38,7 +38,7 @@ const getArtist = async (win) => { artistName.textContent; } ` - ) + ); } // Fill songInfo with empty values @@ -57,8 +57,8 @@ const songInfo = { const handleData = async (responseText, win) => { let data = JSON.parse(responseText); - songInfo.title = data?.videoDetails?.title; - songInfo.artist = await getArtist(win) || data?.videoDetails?.author; + songInfo.title = data.videoDetails?.media?.song || data?.videoDetails?.title; + songInfo.artist = data.videoDetails?.media?.artist || await getArtist(win) || cleanupArtistName(data?.videoDetails?.author); songInfo.views = data?.videoDetails?.viewCount; songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url; songInfo.songDuration = data?.videoDetails?.lengthSeconds; @@ -102,5 +102,20 @@ const registerProvider = (win) => { return registerCallback; }; +const suffixesToRemove = [' - Topic', 'VEVO']; +function cleanupArtistName(artist) { + if (!artist) { + return artist; + } + for (const suffix of suffixesToRemove) { + if (artist.endsWith(suffix)) { + return artist.slice(0, -suffix.length); + } + } + return artist; +} + module.exports = registerProvider; module.exports.getImage = getImage; +module.exports.cleanupArtistName = cleanupArtistName; + diff --git a/yarn.lock b/yarn.lock index 8ba6b788..f4957437 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1638,6 +1638,14 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + app-builder-bin@3.5.12: version "3.5.12" resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-3.5.12.tgz#bbe174972cc1f481f73d6d92ad47a8b4c7eb4530" @@ -1991,6 +1999,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + binaryextensions@^4.15.0: version "4.15.0" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-4.15.0.tgz#c63a502e0078ff1b0e9b00a9f74d3c2b0f8bd32e" @@ -2075,7 +2088,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2404,6 +2417,21 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +chokidar@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.3.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -4262,7 +4290,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.1.2: +fsevents@^2.1.2, fsevents@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -4375,6 +4403,13 @@ glob-parent@^5.0.0, glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" +glob-parent@~5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" @@ -4907,6 +4942,13 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5037,7 +5079,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -6586,7 +6628,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -7482,6 +7524,13 @@ readdir-glob@^1.0.0: dependencies: minimatch "^3.0.4" +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== + dependencies: + picomatch "^2.2.1" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -8676,9 +8725,9 @@ typescript@^4.1.5: integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== ua-parser-js@^0.7.21: - version "0.7.23" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b" - integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA== + version "0.7.28" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" + integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== unbzip2-stream@^1.3.3: version "1.4.3"