diff --git a/download-third-party-apps.js b/download-third-party-apps.js index 2c37a02..7bd9124 100644 --- a/download-third-party-apps.js +++ b/download-third-party-apps.js @@ -1,5 +1,4 @@ import process from 'process' -import util from 'util' import fs from 'fs' import fsPromises from 'fs/promises' import path from 'path' @@ -98,6 +97,22 @@ async function main() { 'https://raw.githubusercontent.com/facebookresearch/demucs/main/demucs/remote/htdemucs_ft.yaml', path.join('anyos-extra-files', 'Models', 'htdemucs_ft.yaml'), ], + [ + 'https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/955717e8-8726e21a.th', + path.join('anyos-extra-files', 'Models', '955717e8-8726e21a.th'), + ], + [ + 'https://raw.githubusercontent.com/facebookresearch/demucs/main/demucs/remote/htdemucs.yaml', + path.join('anyos-extra-files', 'Models', 'htdemucs.yaml'), + ], + [ + 'https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/5c90dfd2-34c22ccb.th', + path.join('anyos-extra-files', 'Models', '5c90dfd2-34c22ccb.th'), + ], + [ + 'https://raw.githubusercontent.com/facebookresearch/demucs/main/demucs/remote/htdemucs_6s.yaml', + path.join('anyos-extra-files', 'Models', 'htdemucs_6s.yaml'), + ], [ `https://github.com/stemrollerapp/demucs-cxfreeze/releases/download/release-26a2baeb0058444b3cf87028d9df721d37c78dfb/demucs-cxfreeze-${winOrMac}${cudaSuffix}.${demucsZipOr7z}`, path.join( @@ -176,7 +191,7 @@ async function main() { `${winOrMac}-extra-files`, 'ThirdPartyApps', 'ffmpeg', - 'ffmpeg-7.0.1-essentials_build' + 'ffmpeg-7.0.2-essentials_build' ) ) } else if (process.platform === 'darwin') { diff --git a/main-src/main.cjs b/main-src/main.cjs index 9eb501b..83e6ec1 100644 --- a/main-src/main.cjs +++ b/main-src/main.cjs @@ -128,10 +128,22 @@ async function handleGetOutputPath() { return processQueue.getOutputPath() } +async function handleGetModelName() { + return processQueue.getModelName() +} + async function handleGetLocalFileOutputToContainingDir() { return processQueue.getLocalFileOutputToContainingDir() } +async function handleGetPrefixStemFilenameWithSongName() { + return processQueue.getPrefixStemFilenameWithSongName() +} + +async function handleGetPreserveOriginalAudio() { + return processQueue.getPreserveOriginalAudio() +} + async function handleBrowseOutputPath() { const response = await dialog.showOpenDialog(mainWindow, { title: 'Select folder to store output stems', @@ -144,10 +156,22 @@ async function handleBrowseOutputPath() { return null } +async function handleSetModelName(event, name) { + return processQueue.setModelName(name) +} + async function handleSetLocalFileOutputToContainingDir(event, value) { return processQueue.setLocalFileOutputToContainingDir(value) } +async function handleSetPrefixStemFilenameWithSongName(event, value) { + return processQueue.setPrefixStemFilenameWithSongName(value) +} + +async function handleSetPreserveOriginalAudio(event, value) { + return processQueue.setPreserveOriginalAudio(value) +} + async function handleGetOutputFormat() { return processQueue.getOutputFormat() } @@ -284,9 +308,15 @@ function main() { ipcMain.handle('openChat', handleOpenChat) ipcMain.handle('disableDonatePopup', handleDisableDonatePopup) ipcMain.handle('getOutputPath', handleGetOutputPath) + ipcMain.handle('getModelName', handleGetModelName) ipcMain.handle('getLocalFileOutputToContainingDir', handleGetLocalFileOutputToContainingDir) + ipcMain.handle('getPrefixStemFilenameWithSongName', handleGetPrefixStemFilenameWithSongName) + ipcMain.handle('getPreserveOriginalAudio', handleGetPreserveOriginalAudio) ipcMain.handle('browseOutputPath', handleBrowseOutputPath) + ipcMain.handle('setModelName', handleSetModelName) ipcMain.handle('setLocalFileOutputToContainingDir', handleSetLocalFileOutputToContainingDir) + ipcMain.handle('setPrefixStemFilenameWithSongName', handleSetPrefixStemFilenameWithSongName) + ipcMain.handle('setPreserveOriginalAudio', handleSetPreserveOriginalAudio) ipcMain.handle('getOutputFormat', handleGetOutputFormat) ipcMain.handle('setOutputFormat', handleSetOutputFormat) ipcMain.handle('getPyTorchBackend', handleGetPyTorchBackend) diff --git a/main-src/preload.cjs b/main-src/preload.cjs index 3e19605..d7f94a3 100644 --- a/main-src/preload.cjs +++ b/main-src/preload.cjs @@ -28,13 +28,29 @@ contextBridge.exposeInMainWorld('disableDonatePopup', () => ipcRenderer.invoke('disableDonatePopup') ) contextBridge.exposeInMainWorld('getOutputPath', () => ipcRenderer.invoke('getOutputPath')) +contextBridge.exposeInMainWorld('getModelName', () => ipcRenderer.invoke('getModelName')) contextBridge.exposeInMainWorld('getLocalFileOutputToContainingDir', () => ipcRenderer.invoke('getLocalFileOutputToContainingDir') ) +contextBridge.exposeInMainWorld('getPrefixStemFilenameWithSongName', () => + ipcRenderer.invoke('getPrefixStemFilenameWithSongName') +) +contextBridge.exposeInMainWorld('getPreserveOriginalAudio', () => + ipcRenderer.invoke('getPreserveOriginalAudio') +) contextBridge.exposeInMainWorld('browseOutputPath', () => ipcRenderer.invoke('browseOutputPath')) +contextBridge.exposeInMainWorld('setModelName', (value) => + ipcRenderer.invoke('setModelName', value) +) contextBridge.exposeInMainWorld('setLocalFileOutputToContainingDir', (value) => ipcRenderer.invoke('setLocalFileOutputToContainingDir', value) ) +contextBridge.exposeInMainWorld('setPrefixStemFilenameWithSongName', (value) => + ipcRenderer.invoke('setPrefixStemFilenameWithSongName', value) +) +contextBridge.exposeInMainWorld('setPreserveOriginalAudio', (value) => + ipcRenderer.invoke('setPreserveOriginalAudio', value) +) contextBridge.exposeInMainWorld('getOutputFormat', () => ipcRenderer.invoke('getOutputFormat')) contextBridge.exposeInMainWorld('setOutputFormat', (outputFormat) => ipcRenderer.invoke('setOutputFormat', outputFormat) diff --git a/main-src/processQueue.cjs b/main-src/processQueue.cjs index e1628dc..83a5d51 100644 --- a/main-src/processQueue.cjs +++ b/main-src/processQueue.cjs @@ -14,7 +14,7 @@ let statusUpdateCallback = null, let curItems = [], curChildProcess = null, curYtdlAbortController = null -let curProgressStemIdx = 0 +let curProgressFtStemIdx = null function getPathToThirdPartyApps() { if (process.env.NODE_ENV === 'dev') { @@ -72,7 +72,6 @@ if (PATH_TO_THIRD_PARTY_APPS) { CHILD_PROCESS_ENV.PATH = PATH_TO_DEMUCS + (process.platform === 'win32' ? ';' : ':') + PATH_TO_FFMPEG } -const DEMUCS_MODEL_NAME = 'htdemucs_ft' const TMP_PREFIX = 'StemRoller-' function getJobCount() { @@ -98,39 +97,46 @@ function killCurChildProcess() { } curChildProcess = null } +} - curProgressStemIdx = 0 +function updateProgressRaw(videoId, progress) { + let mainWindow = BrowserWindow.getAllWindows()[0] + if (!mainWindow) { + return + } + mainWindow.webContents.send('videoStatusUpdate', { + videoId, + status: { + step: 'processing', + progress, + }, + }) } -function updateProgress(videoId, data) { +function updateDemucsProgress(videoId, data) { // Check if the output contains the progress update const progressMatch = data.toString().match(/\r\s+\d+%\|/) if (progressMatch) { - const progress = parseInt(progressMatch) - if (progress === 0) { - ++curProgressStemIdx + let progress = parseInt(progressMatch) + if (isNaN(progress)) { + return } - // Find the renderer window and send the update - let mainWindow = BrowserWindow.getAllWindows()[0] - if (!isNaN(progress) && mainWindow) { - mainWindow.webContents.send('videoStatusUpdate', { - videoId, - status: { - step: 'processing', - progress, - stemIdx: curProgressStemIdx, - }, - }) + if (curProgressFtStemIdx !== null && progress === 0) { + ++curProgressFtStemIdx } + progress /= 100 + updateProgressRaw( + videoId, + (curProgressFtStemIdx === null ? progress : (curProgressFtStemIdx - 1) / 4 + progress / 4) * + 0.95 + ) } } -function spawnAndWait(videoId, cwd, command, args) { +function spawnAndWait(videoId, cwd, command, args, isDemucs, isHybrid) { return new Promise((resolve, reject) => { killCurChildProcess() - curProgressStemIdx = 0 - CHILD_PROCESS_ENV.LANG = `${(app.getSystemLocale() || 'en-US').replace('-', '_')}.UTF-8` // Set here instead of when CHILD_PROCESS_ENV defined, because app must be ready before we can read the system locale curChildProcess = childProcess.spawn(command, args, { cwd, @@ -143,8 +149,10 @@ function spawnAndWait(videoId, cwd, command, args) { curChildProcess.stderr.on('data', (data) => { console.log(`child stderr:\n${data}`) - // For some reason the progress displays in stderr instead of stdout - updateProgress(videoId, data) + if (isDemucs) { + // For some reason the progress displays in stderr instead of stdout + updateDemucsProgress(videoId, data) + } }) curChildProcess.on('error', (error) => { @@ -188,6 +196,19 @@ async function findDemucsOutputDir(basePath) { throw new Error('Unable to find Demucs output directory') } +async function listDemucsOutputFiles(basePath) { + const files = [] + const entries = await fs.readdir(basePath, { + withFileTypes: true, + }) + for (const entry of entries) { + if (entry.isFile()) { + files.push(path.join(basePath, entry.name)) + } + } + return files +} + async function ensureFileExists(path) { try { await fs.access(path) @@ -197,17 +218,51 @@ async function ensureFileExists(path) { } } -async function ensureDemucsPathsExist(paths) { - for (const i in paths) { - if (!ensureFileExists(paths[i])) { - console.trace(`File "${paths[i]}" does not exist`) - return false +async function convertDemucsFiles({ videoId, tmpDir, filesList, filetype, compressionArgs }) { + console.log(`Converting files to format "${filetype}"`) + + const result = [] + for (const oldFilename of filesList) { + const parsedOldFilename = path.parse(oldFilename) + const newFilename = `${path.join(parsedOldFilename.dir, parsedOldFilename.name)}.${filetype}` + + await spawnAndWait( + videoId, + tmpDir, + FFMPEG_EXE_NAME, + ['-i', oldFilename, ...compressionArgs, newFilename], + false + ) + + const success = await ensureFileExists(newFilename) + if (!success) { + throw new Error(`Unable to access converted file "${newFilename}" - ffmpeg probably failed`) } + + result.push(newFilename) + } + + return result +} + +function getFfmpegCompressionArguments(filetype) { + if (filetype === 'mp3') { + return ['-q:a', '0'] + } else if (filetype === 'flac') { + return ['-compression_level', '5'] + } else if (filetype === 'wav') { + return [] } - return true + throw new Error(`Unrecognized filetype: ${filetype}`) } async function _processVideo(video, tmpDir) { + const demucsModelName = module.exports.getModelName() + const demucsStemsFiletype = module.exports.getOutputFormat() + const compressionArgs = getFfmpegCompressionArguments(demucsStemsFiletype) + const needsPrefix = module.exports.getPrefixStemFilenameWithSongName() + const needsOriginal = module.exports.getPreserveOriginalAudio() + const beginTime = Date.now() console.log(`BEGIN downloading/processing video "${video.videoId}" - "${video.title}"`) setVideoStatusAndPath(video.videoId, { step: 'downloading' }, null) @@ -226,20 +281,25 @@ async function _processVideo(video, tmpDir) { throw new Error(`Invalid mediaSource: ${video.mediaSource}`) } + const outputFolderName = + video.mediaSource === 'youtube' + ? sanitizeFilename(`${video.title}-${video.videoId}`) + : sanitizeFilename(video.title) + const outputFilenamesPrefix = needsPrefix ? `${outputFolderName} - ` : '' + setVideoStatusAndPath( video.videoId, { step: 'processing', progress: 0, - stemIdx: 0, }, null ) const jobCount = getJobCount() console.log( - `Splitting video "${video.videoId}"; ${jobCount} jobs using model "${DEMUCS_MODEL_NAME}"...` + `Splitting video "${video.videoId}"; ${jobCount} jobs using model "${demucsModelName}"...` ) - const demucsExeArgs = [mediaPath, '-n', DEMUCS_MODEL_NAME, '-j', jobCount] + const demucsExeArgs = [mediaPath, '-n', demucsModelName, '-j', jobCount] if (module.exports.getPyTorchBackend() === 'cpu') { console.log('Running with "-d cpu" to force CPU instead of CUDA') demucsExeArgs.push('-d', 'cpu') @@ -249,54 +309,86 @@ async function _processVideo(video, tmpDir) { demucsExeArgs.push('-d', 'mps') } - const demucsStemsFiletype = module.exports.getOutputFormat() - if (demucsStemsFiletype === 'mp3') { - demucsExeArgs.push('--mp3') - } if (PATH_TO_MODELS) { demucsExeArgs.push('--repo', PATH_TO_MODELS) } - await spawnAndWait(video.videoId, tmpDir, DEMUCS_EXE_NAME, demucsExeArgs) - - const demucsBasePath = await findDemucsOutputDir( - path.join(tmpDir, 'separated', DEMUCS_MODEL_NAME) - ) - const demucsPaths = { - bass: path.join(demucsBasePath, 'bass.' + demucsStemsFiletype), - drums: path.join(demucsBasePath, 'drums.' + demucsStemsFiletype), - other: path.join(demucsBasePath, 'other.' + demucsStemsFiletype), - vocals: path.join(demucsBasePath, 'vocals.' + demucsStemsFiletype), + if (demucsModelName.indexOf('_ft') >= 0) { + curProgressFtStemIdx = 0 + } else { + curProgressFtStemIdx = null } - - const demucsSuccess = await ensureDemucsPathsExist(demucsPaths) - if (!demucsSuccess) { - throw new Error('Unable to access output stems - Demucs probably failed') + await spawnAndWait(video.videoId, tmpDir, DEMUCS_EXE_NAME, demucsExeArgs, true) + curProgressFtStemIdx = null + updateProgressRaw(video.videoId, 0.95) + + const demucsBasePath = await findDemucsOutputDir(path.join(tmpDir, 'separated', demucsModelName)) + const demucsWavFilesList = await listDemucsOutputFiles(demucsBasePath) + if (demucsWavFilesList.length === 0) { + throw new Error('No .wav output stems written - Demucs probably failed') } + const demucsConvertedFilesList = + demucsStemsFiletype === 'wav' + ? demucsWavFilesList + : await convertDemucsFiles({ + videoId: video.videoId, + tmpDir, + filesList: demucsWavFilesList, + filetype: demucsStemsFiletype, + compressionArgs, + }) + updateProgressRaw(video.videoId, 0.97) - const instrumentalPath = path.join(tmpDir, 'instrumental.' + demucsStemsFiletype) + const instrumentalPath = path.join(tmpDir, `instrumental.${demucsStemsFiletype}`) console.log(`Mixing down instrumental stems to "${instrumentalPath}"`) - await spawnAndWait(video.videoId, tmpDir, FFMPEG_EXE_NAME, [ - '-i', - demucsPaths.bass, - '-i', - demucsPaths.drums, - '-i', - demucsPaths.other, - '-filter_complex', - 'amix=inputs=3:normalize=0', - instrumentalPath, - ]) + const ffmpegInstrumentalSourceFiles = [] + for (const filename of demucsWavFilesList) { + const baseName = path.parse(filename).name + if (baseName === 'vocals') { + continue + } + ffmpegInstrumentalSourceFiles.push('-i') + ffmpegInstrumentalSourceFiles.push(filename) + } + await spawnAndWait( + video.videoId, + tmpDir, + FFMPEG_EXE_NAME, + [ + ...ffmpegInstrumentalSourceFiles, + ...compressionArgs, + '-filter_complex', + `amix=inputs=${ffmpegInstrumentalSourceFiles.length / 2}:normalize=0`, + instrumentalPath, + ], + false + ) const instrumentalSuccess = await ensureFileExists(instrumentalPath) if (!instrumentalSuccess) { throw new Error( `Unable to access instrumental file "${instrumentalPath}" - ffmpeg probably failed` ) } + updateProgressRaw(video.videoId, 0.98) + + let originalOutPath = null + if (needsOriginal) { + originalOutPath = path.join(tmpDir, `original.${demucsStemsFiletype}`) + await spawnAndWait( + video.videoId, + tmpDir, + FFMPEG_EXE_NAME, + ['-i', mediaPath, ...compressionArgs, originalOutPath], + false + ) + const originalSuccess = await ensureFileExists(originalOutPath) + if (!originalSuccess) { + throw new Error( + `Unable to access instrumental file "${originalOutPath}" - ffmpeg probably failed` + ) + } + } + updateProgressRaw(video.videoId, 0.99) - const outputFolderName = - video.mediaSource === 'youtube' - ? sanitizeFilename(`${video.title}-${video.videoId}`) - : sanitizeFilename(video.title) const outputBasePathContainingFolder = video.mediaSource === 'local' && module.exports.getLocalFileOutputToContainingDir() ? path.dirname(mediaPath) @@ -304,18 +396,24 @@ async function _processVideo(video, tmpDir) { const outputBasePath = path.join(outputBasePathContainingFolder, outputFolderName) await fs.mkdir(outputBasePath, { recursive: true }) console.log(`Copying all stems to "${outputBasePath}"`) - const outputPaths = { - bass: path.join(outputBasePath, 'bass.' + demucsStemsFiletype), - drums: path.join(outputBasePath, 'drums.' + demucsStemsFiletype), - other: path.join(outputBasePath, 'other.' + demucsStemsFiletype), - vocals: path.join(outputBasePath, 'vocals.' + demucsStemsFiletype), - instrumental: path.join(outputBasePath, 'instrumental.' + demucsStemsFiletype), + for (const filename of demucsConvertedFilesList) { + const baseName = path.parse(filename).name + const outputPath = path.join( + outputBasePath, + `${outputFilenamesPrefix}${baseName}.${demucsStemsFiletype}` + ) + await fs.copyFile(filename, outputPath) } - - for (const i in demucsPaths) { - await fs.copyFile(demucsPaths[i], outputPaths[i]) + await fs.copyFile( + instrumentalPath, + path.join(outputBasePath, `${outputFilenamesPrefix}instrumental.${demucsStemsFiletype}`) + ) + if (needsOriginal) { + await fs.copyFile( + originalOutPath, + path.join(outputBasePath, `${outputFilenamesPrefix}original.${demucsStemsFiletype}`) + ) } - await fs.copyFile(instrumentalPath, outputPaths.instrumental) const elapsedSeconds = (Date.now() - beginTime) * 0.001 console.log( @@ -350,6 +448,8 @@ async function processVideo(video) { setVideoStatusAndPath(video.videoId, { step: 'error' }, null) } } finally { + curProgressFtStemIdx = null + try { await fs.rm(tmpDir, { recursive: true, @@ -472,18 +572,48 @@ module.exports.getOutputPath = () => { return path.join(os.homedir(), 'Music', 'StemRoller') } +module.exports.getModelName = () => { + if (electronStore) { + const modelName = electronStore.get('modelName') + if (modelName) { + return modelName + } + } + return 'htdemucs' +} + module.exports.getLocalFileOutputToContainingDir = () => { return electronStore.get('localFileOutputToContainingDir') || false } +module.exports.getPrefixStemFilenameWithSongName = () => { + return electronStore.get('prefixStemFilenameWithSongName') || false +} + +module.exports.getPreserveOriginalAudio = () => { + return electronStore.get('preserveOriginalAudio') || false +} + module.exports.setOutputPath = (outputPath) => { electronStore.set('outputPath', outputPath) } +module.exports.setModelName = (name) => { + electronStore.set('modelName', name) +} + module.exports.setLocalFileOutputToContainingDir = (value) => { electronStore.set('localFileOutputToContainingDir', value) } +module.exports.setPrefixStemFilenameWithSongName = (value) => { + electronStore.set('prefixStemFilenameWithSongName', value) +} + +module.exports.setPreserveOriginalAudio = (value) => { + electronStore.set('preserveOriginalAudio', value) +} + module.exports.getOutputFormat = () => { if (electronStore) { const outputFormat = electronStore.get('outputFormat') diff --git a/package-lock.json b/package-lock.json index e7ec1e8..c85da82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "stemroller", - "version": "2.0.7", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { @@ -9,7 +9,7 @@ "version": "2.0.7", "license": "Unlicense", "dependencies": { - "@distube/ytdl-core": "^4.13.5", + "@distube/ytdl-core": "^4.13.7", "compare-versions": "^4.1.3", "electron-fetch": "^1.7.4", "electron-serve": "^1.1.0", @@ -27,7 +27,7 @@ "concurrently": "^7.2.2", "cross-env": "^7.0.3", "dotenv": "^16.0.1", - "electron": "^21.0.0", + "electron": "^31.3.1", "electron-builder": "^23.1.0", "electron-connect": "^0.6.3", "electron-packager": "^15.5.1", @@ -66,9 +66,9 @@ } }, "node_modules/@distube/ytdl-core": { - "version": "4.13.5", - "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.13.5.tgz", - "integrity": "sha512-g+4UJIR/auAJbia7iB0aWvaJDbs22P53NySWa47b1NT4xMTDJYguxHFArPrvRkcJrb/AgKjv/XoSZGghpL0CJA==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.13.7.tgz", + "integrity": "sha512-xpovwZVPwQ0R4Mrwt/fhh1rqmBjiZ5qj30ck5kz+mQJ5XPzW9dUiTDz89qCxO3YvgbCAqaC5BNNnWiFC2uCV5Q==", "dependencies": { "http-cookie-agent": "^6.0.5", "m3u8stream": "^0.8.6", @@ -197,14 +197,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "engines": { - "node": ">=14" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", @@ -442,6 +434,18 @@ "node": ">= 10" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -471,6 +475,21 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -485,10 +504,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.48", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.48.tgz", - "integrity": "sha512-Z9r9UWlNeNkYnxybm+1fc0jxUNjZqRekTAr1pG0qdXe9apT9yCiqk1c4VvKQJsFpnchU4+fLl25MabSLA2wxIw==", - "dev": true + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/plist": { "version": "3.0.2", @@ -507,6 +529,15 @@ "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", "dev": true }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/sass": { "version": "1.43.1", "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", @@ -1338,6 +1369,15 @@ "node": ">= 10.0.0" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, "node_modules/cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -2276,21 +2316,21 @@ } }, "node_modules/electron": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-21.0.0.tgz", - "integrity": "sha512-7HGxgaH0goYsq5m23rbLuKNwxOP4wS/JTNVTYt4n+a4sPkxI97Fcngh55pHaIvvMO3jKZ9yzll7L/D1dHwMdLA==", + "version": "31.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.3.1.tgz", + "integrity": "sha512-9fiuWlRhBfygtcT+auRd/WdBK/f8LZZcrpx0RjpXhH2DPTP/PfnkC4JB1PW55qCbGbh4wAgkYbf4ExIag8oGCA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@electron/get": "^1.14.1", - "@types/node": "^16.11.26", + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" }, "engines": { - "node": ">= 10.17.0" + "node": ">= 12.20.55" } }, "node_modules/electron-builder": { @@ -2836,6 +2876,187 @@ "node": ">= 10.0.0" } }, + "node_modules/electron/node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/electron/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/electron/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron/node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron/node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron/node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/electron/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/electron/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/electron/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron/node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4379,6 +4600,19 @@ "node": ">= 6" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -6203,6 +6437,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7138,16 +7378,19 @@ "dev": true }, "node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.5.tgz", + "integrity": "sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg==", "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -7500,16 +7743,16 @@ } }, "@distube/ytdl-core": { - "version": "4.13.5", - "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.13.5.tgz", - "integrity": "sha512-g+4UJIR/auAJbia7iB0aWvaJDbs22P53NySWa47b1NT4xMTDJYguxHFArPrvRkcJrb/AgKjv/XoSZGghpL0CJA==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.13.7.tgz", + "integrity": "sha512-xpovwZVPwQ0R4Mrwt/fhh1rqmBjiZ5qj30ck5kz+mQJ5XPzW9dUiTDz89qCxO3YvgbCAqaC5BNNnWiFC2uCV5Q==", "requires": { "http-cookie-agent": "^6.0.5", "m3u8stream": "^0.8.6", "miniget": "^4.2.3", "sax": "^1.4.1", "tough-cookie": "^4.1.4", - "undici": "5.28.4" + "undici": "^6.19.2" } }, "@electron/get": { @@ -7599,11 +7842,6 @@ "strip-json-comments": "^3.1.1" } }, - "@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==" - }, "@humanwhocodes/config-array": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", @@ -7767,6 +8005,18 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -7796,6 +8046,21 @@ "@types/node": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -7810,10 +8075,13 @@ "dev": true }, "@types/node": { - "version": "16.11.48", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.48.tgz", - "integrity": "sha512-Z9r9UWlNeNkYnxybm+1fc0jxUNjZqRekTAr1pG0qdXe9apT9yCiqk1c4VvKQJsFpnchU4+fLl25MabSLA2wxIw==", - "dev": true + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } }, "@types/plist": { "version": "3.0.2", @@ -7832,6 +8100,15 @@ "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", "dev": true }, + "@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/sass": { "version": "1.43.1", "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", @@ -8483,6 +8760,12 @@ "sax": "^1.2.4" } }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -9169,14 +9452,144 @@ } }, "electron": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-21.0.0.tgz", - "integrity": "sha512-7HGxgaH0goYsq5m23rbLuKNwxOP4wS/JTNVTYt4n+a4sPkxI97Fcngh55pHaIvvMO3jKZ9yzll7L/D1dHwMdLA==", + "version": "31.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.3.1.tgz", + "integrity": "sha512-9fiuWlRhBfygtcT+auRd/WdBK/f8LZZcrpx0RjpXhH2DPTP/PfnkC4JB1PW55qCbGbh4wAgkYbf4ExIag8oGCA==", "dev": true, "requires": { - "@electron/get": "^1.14.1", - "@types/node": "^16.11.26", + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" + }, + "dependencies": { + "@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "global-agent": "^3.0.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + } + }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true + }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + } } }, "electron-builder": { @@ -10723,6 +11136,16 @@ "debug": "4" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -12055,6 +12478,12 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -12751,12 +13180,15 @@ "dev": true }, "undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", - "requires": { - "@fastify/busboy": "^2.0.0" - } + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.5.tgz", + "integrity": "sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg==" + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "universalify": { "version": "0.1.2", diff --git a/package.json b/package.json index 6f82862..b002a16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stemroller", - "version": "2.0.7", + "version": "2.1.0", "private": true, "type": "module", "description": "Isolate vocals, drums, bass, and other instrumental stems from any song", @@ -33,7 +33,7 @@ "concurrently": "^7.2.2", "cross-env": "^7.0.3", "dotenv": "^16.0.1", - "electron": "^21.0.0", + "electron": "^31.3.1", "electron-builder": "^23.1.0", "electron-connect": "^0.6.3", "electron-packager": "^15.5.1", @@ -54,7 +54,7 @@ "tailwindcss": "^3.1.4" }, "dependencies": { - "@distube/ytdl-core": "^4.13.5", + "@distube/ytdl-core": "^4.13.7", "compare-versions": "^4.1.3", "electron-fetch": "^1.7.4", "electron-serve": "^1.1.0", @@ -63,8 +63,5 @@ "tree-kill": "^1.2.2", "xxhash-wasm": "^1.0.1", "yt-search": "^2.10.4" - }, - "overrides": { - "undici": "5.28.4" } } diff --git a/renderer-src/components/PreferencesPopup.svelte b/renderer-src/components/PreferencesPopup.svelte index 80306da..b2cd743 100644 --- a/renderer-src/components/PreferencesPopup.svelte +++ b/renderer-src/components/PreferencesPopup.svelte @@ -9,8 +9,11 @@ let pyTorchBackend = null let outputPath = null + let modelName = null let outputFormat = null let localFileOutputToContainingDir = null + let prefixStemFilenameWithSongName = null + let preserveOriginalAudio = null async function handleBrowseStems() { const newOutputPath = await window.browseOutputPath() @@ -22,20 +25,32 @@ onMount(async () => { pyTorchBackend = await window.getPyTorchBackend() outputPath = await window.getOutputPath() + modelName = await window.getModelName() outputFormat = await window.getOutputFormat() localFileOutputToContainingDir = await window.getLocalFileOutputToContainingDir() + prefixStemFilenameWithSongName = await window.getPrefixStemFilenameWithSongName() + preserveOriginalAudio = await window.getPreserveOriginalAudio() }) $: { if (pyTorchBackend) { window.setPyTorchBackend(pyTorchBackend) } + if (modelName) { + window.setModelName(modelName) + } if (outputFormat) { window.setOutputFormat(outputFormat) } if (localFileOutputToContainingDir !== null) { window.setLocalFileOutputToContainingDir(localFileOutputToContainingDir) } + if (prefixStemFilenameWithSongName !== null) { + window.setPrefixStemFilenameWithSongName(prefixStemFilenameWithSongName) + } + if (preserveOriginalAudio !== null) { + window.setPreserveOriginalAudio(preserveOriginalAudio) + } } @@ -80,6 +95,32 @@ > +
+ + + +
+ +
+ + + +
+
Stems output format
+
Demucs model
+ + +
Backend

Try "Always use CPU" if splitting fails on your device.

diff --git a/renderer-src/components/ProcessQueueCard.svelte b/renderer-src/components/ProcessQueueCard.svelte index 275529c..d54d9cb 100644 --- a/renderer-src/components/ProcessQueueCard.svelte +++ b/renderer-src/components/ProcessQueueCard.svelte @@ -8,8 +8,11 @@ import ExternalLinkIcon from '$icons/outline/ExternalLinkIcon.svelte' import XCircleIcon from '$icons/solid/XCircleIcon.svelte' - export let video = null, onSplitClicked = null, onCancelClicked = null - let hovered = false, cancelHovered = false + export let video = null, + onSplitClicked = null, + onCancelClicked = null + let hovered = false, + cancelHovered = false function handleCancelClicked(event) { event.stopPropagation() @@ -23,10 +26,12 @@ } } - let status = null, path = null, onClick = null + let status = null, + path = null, + onClick = null $: { - window.getVideoStatus(video.videoId).then((newStatus) => status = newStatus) - window.getVideoPath(video.videoId).then((newPath) => path = newPath) + window.getVideoStatus(video.videoId).then((newStatus) => (status = newStatus)) + window.getVideoPath(video.videoId).then((newPath) => (path = newPath)) } $: { window.setVideoStatusUpdateHandler(video.videoId, 'ProcessQueueCard', (message) => { @@ -52,7 +57,13 @@ {#if status !== null} - diff --git a/renderer-src/components/ResultCard.svelte b/renderer-src/components/ResultCard.svelte index 0ed3dce..52bc2b4 100644 --- a/renderer-src/components/ResultCard.svelte +++ b/renderer-src/components/ResultCard.svelte @@ -7,7 +7,8 @@ import ExternalLinkIcon from '$icons/solid/ExternalLinkIcon.svelte' import LoadingSpinnerIcon from '$icons/animated/LoadingSpinnerIcon.svelte' - export let video = null, onSplitClicked = null + export let video = null, + onSplitClicked = null async function handleOpenStemsClicked() { const result = await window.openStemsPath(video) @@ -16,10 +17,11 @@ } } - let status = null, path = null + let status = null, + path = null $: { - window.getVideoStatus(video.videoId).then((newStatus) => status = newStatus) - window.getVideoPath(video.videoId).then((newPath) => path = newPath) + window.getVideoStatus(video.videoId).then((newStatus) => (status = newStatus)) + window.getVideoPath(video.videoId).then((newPath) => (path = newPath)) } $: { window.setVideoStatusUpdateHandler(video.videoId, 'ResultCard', (message) => { @@ -33,18 +35,26 @@ }) -
+
- +
{video.timestamp}
{video.title}
-
{video.author.name}
+
+ {video.author.name} +
{#if status !== null && status.step === 'processing'} -