mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 02:51:46 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb5bc2ca1 | |||
| c79fdd9887 | |||
| d7b821727d | |||
| 21c45faf20 | |||
| 92cab89d17 | |||
| fa160b2e90 | |||
| 308ac38e6b | |||
| a62cafb601 | |||
| bf9e3b5f48 | |||
| 3c6b3aeff0 | |||
| 37181a7b5e | |||
| 0b363d6487 | |||
| e9398adac3 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -117,7 +117,7 @@ jobs:
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
id: ${{ steps.get_draft_release.outputs.id }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
20
.github/workflows/winget-cla.yml
vendored
Normal file
20
.github/workflows/winget-cla.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Submit CLA to Winget PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_url:
|
||||
description: "Specific PR URL"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment to PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Submit CLA to Windows Package Manager Community Repository Pull Request
|
||||
run: gh pr comment $PR_URL --body "@microsoft-github-policy-service agree"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
PR_URL: ${{ inputs.pr_url }}
|
||||
2
.github/workflows/winget-submission.yml
vendored
2
.github/workflows/winget-submission.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: th-ch.YouTubeMusic
|
||||
installers-regex: '^YouTube-Music-Setup-[\d\.]+\.exe$'
|
||||
installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'
|
||||
version: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
|
||||
22
changelog.md
22
changelog.md
@ -2,8 +2,30 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v2.1.0](https://github.com/th-ch/youtube-music/compare/v2.0.4...v2.1.0)
|
||||
|
||||
- feat(downloader): Added support for audio format auto-detection [`#1310`](https://github.com/th-ch/youtube-music/pull/1310)
|
||||
- feat(in-app-menu): enable in-app-menu by default (in Windows) [`#1311`](https://github.com/th-ch/youtube-music/pull/1311)
|
||||
- fix: winget publish [`#1307`](https://github.com/th-ch/youtube-music/pull/1307)
|
||||
- hotfix(downloader): fix invalid query selector (fix #1308) [`#1308`](https://github.com/th-ch/youtube-music/issues/1308)
|
||||
- chore(deps): bump dependencies [`3c6b3ae`](https://github.com/th-ch/youtube-music/commit/3c6b3aeff0aae32adb2f2ad9c091b0a9701d3c24)
|
||||
- chore(actions): create winget-cla.yml [`37181a7`](https://github.com/th-ch/youtube-music/commit/37181a7b5e2aa5bed6a36298eac3a66aac2762b8)
|
||||
- Update changelog for v2.0.4 [`e9398ad`](https://github.com/th-ch/youtube-music/commit/e9398adac34a8abb11801e32999a915a8be0ece6)
|
||||
|
||||
#### [v2.0.4](https://github.com/th-ch/youtube-music/compare/v2.0.3...v2.0.4)
|
||||
|
||||
> 12 October 2023
|
||||
|
||||
- hotfix(adblocker): fix `ipcRenderer.sendSync() with ...` [`#1301`](https://github.com/th-ch/youtube-music/pull/1301)
|
||||
- fix(downloader): Korean filename is broken on non-macOS devices [`#1297`](https://github.com/th-ch/youtube-music/pull/1297)
|
||||
- chore(deps): bump deps [`b6894dc`](https://github.com/th-ch/youtube-music/commit/b6894dca2974c63fa2945d3a4995665d11eb2a78)
|
||||
- fix: bump dependencies [`7aa970c`](https://github.com/th-ch/youtube-music/commit/7aa970cebc8e1407ff6937b402ba303e14c73efd)
|
||||
- fix(downloader): private playlist download [`1d5b299`](https://github.com/th-ch/youtube-music/commit/1d5b2997bd0c72c1c007c57b145509e4a8f77fef)
|
||||
|
||||
#### [v2.0.3](https://github.com/th-ch/youtube-music/compare/v2.0.2...v2.0.3)
|
||||
|
||||
> 10 October 2023
|
||||
|
||||
- feat(discord): add `Hide GitHub link Button` [`#1293`](https://github.com/th-ch/youtube-music/pull/1293)
|
||||
- feat(deps): bundle `youtubei.js` (temporary solution) [`#1292`](https://github.com/th-ch/youtube-music/pull/1292)
|
||||
- fix(mpris): fixed an issue where MPRIS information was incorrect [`#1291`](https://github.com/th-ch/youtube-music/pull/1291)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { blockers } from '../plugins/adblocker/blocker-types';
|
||||
|
||||
import { DefaultPresetList } from '../plugins/downloader/types';
|
||||
|
||||
export interface WindowSizeConfig {
|
||||
width: number;
|
||||
height: number;
|
||||
@ -111,14 +113,19 @@ const defaultConfig = {
|
||||
},
|
||||
'downloader': {
|
||||
enabled: false,
|
||||
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
|
||||
preset: 'mp3',
|
||||
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
||||
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined as number | undefined,
|
||||
},
|
||||
'exponential-volume': {},
|
||||
'in-app-menu': {},
|
||||
'in-app-menu': {
|
||||
/**
|
||||
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
|
||||
*/
|
||||
enabled: false,
|
||||
},
|
||||
'last-fm': {
|
||||
enabled: false,
|
||||
token: undefined as string | undefined, // Token used for authentication
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
import Store from 'electron-store';
|
||||
import Conf from 'conf';
|
||||
import is from 'electron-is';
|
||||
|
||||
import defaults from './defaults';
|
||||
|
||||
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
|
||||
|
||||
const getDefaults = () => {
|
||||
if (is.windows()) {
|
||||
defaults.plugins['in-app-menu'].enabled = true;
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
|
||||
if (!store.get(`plugins.${plugin}`)) {
|
||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||
@ -10,6 +20,26 @@ const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: k
|
||||
};
|
||||
|
||||
const migrations = {
|
||||
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
|
||||
const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
|
||||
if (originalPreset) {
|
||||
if (originalPreset !== 'opus') {
|
||||
store.set('plugins.downloader.selectedPreset', 'Custom');
|
||||
store.set('plugins.downloader.customPresetSetting', {
|
||||
extension: 'mp3',
|
||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
|
||||
} satisfies Preset);
|
||||
} else {
|
||||
store.set('plugins.downloader.selectedPreset', 'Source');
|
||||
store.set('plugins.downloader.customPresetSetting', {
|
||||
extension: null,
|
||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? [],
|
||||
} satisfies Preset);
|
||||
}
|
||||
store.delete('plugins.downloader.preset');
|
||||
store.delete('plugins.downloader.ffmpegArgs');
|
||||
}
|
||||
},
|
||||
'>=1.20.0'(store: Conf<Record<string, unknown>>) {
|
||||
setDefaultPluginOptions(store, 'visualizer');
|
||||
|
||||
@ -118,7 +148,7 @@ const migrations = {
|
||||
};
|
||||
|
||||
export default new Store({
|
||||
defaults,
|
||||
defaults: getDefaults(),
|
||||
clearInvalidConfig: false,
|
||||
migrations,
|
||||
});
|
||||
|
||||
6
menu.ts
6
menu.ts
@ -420,6 +420,12 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
{ role: 'quit' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
],
|
||||
}
|
||||
];
|
||||
};
|
||||
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
||||
|
||||
46
package-lock.json
generated
46
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"version": "2.0.4",
|
||||
"version": "2.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtube-music",
|
||||
"version": "2.0.4",
|
||||
"version": "2.1.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -42,7 +42,7 @@
|
||||
"ytpl": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.38.1",
|
||||
"@playwright/test": "1.39.0",
|
||||
"@rollup/plugin-commonjs": "25.0.5",
|
||||
"@rollup/plugin-image": "3.0.3",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
@ -65,10 +65,10 @@
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "8.0.0",
|
||||
"playwright": "1.38.1",
|
||||
"playwright": "1.39.0",
|
||||
"rollup": "4.0.2",
|
||||
"rollup-plugin-copy": "3.5.0",
|
||||
"rollup-plugin-import-css": "3.3.4",
|
||||
"rollup-plugin-import-css": "3.3.5",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
@ -269,9 +269,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz",
|
||||
"integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
|
||||
"integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@ -1163,12 +1163,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
|
||||
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz",
|
||||
"integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.38.1"
|
||||
"playwright": "1.39.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -8060,12 +8060,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
|
||||
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz",
|
||||
"integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.38.1"
|
||||
"playwright-core": "1.39.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -8078,9 +8078,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
|
||||
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz",
|
||||
"integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
@ -8574,9 +8574,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-import-css": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.3.4.tgz",
|
||||
"integrity": "sha512-w5p1Dd1CavAht/P82zB3WX2RVy7O47MlJGSmgrWXTBPAkWHTbOBh/nUPz94IczCD0HLxpuT4AhF24cix7CpZWA==",
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.3.5.tgz",
|
||||
"integrity": "sha512-wSfzveEzvUDlVevo70kmVD5Mk785UN55NG4C7VVnrmdE0qZ8apcVVFajyCPfFYSNxq5YkccOcrGUT2T/2HnEcQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.4"
|
||||
@ -8585,7 +8585,7 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.x.x || ^3.x.x"
|
||||
"rollup": "^2.x.x || ^3.x.x || ^4.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-string": {
|
||||
|
||||
10
package.json
10
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "2.0.4",
|
||||
"version": "2.1.1",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
@ -150,10 +150,10 @@
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "2.7.0",
|
||||
"@electron/universal": "1.4.2",
|
||||
"@babel/runtime": "7.23.1"
|
||||
"@babel/runtime": "7.23.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.38.1",
|
||||
"@playwright/test": "1.39.0",
|
||||
"@rollup/plugin-commonjs": "25.0.5",
|
||||
"@rollup/plugin-image": "3.0.3",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
@ -176,10 +176,10 @@
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "8.0.0",
|
||||
"playwright": "1.38.1",
|
||||
"playwright": "1.39.0",
|
||||
"rollup": "4.0.2",
|
||||
"rollup-plugin-copy": "3.5.0",
|
||||
"rollup-plugin-import-css": "3.3.4",
|
||||
"rollup-plugin-import-css": "3.3.5",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
|
||||
@ -8,12 +8,11 @@ import is from 'electron-is';
|
||||
import filenamify from 'filenamify';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { createFFmpeg } from '@ffmpeg.wasm/main';
|
||||
|
||||
import NodeID3, { TagConstants } from 'node-id3';
|
||||
|
||||
import { cropMaxWidth, getFolder, presets, sendFeedback as sendFeedback_, setBadge } from './utils';
|
||||
|
||||
import { cropMaxWidth, getFolder, sendFeedback as sendFeedback_, setBadge } from './utils';
|
||||
import config from './config';
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
@ -221,13 +220,32 @@ async function downloadSongUnsafe(
|
||||
);
|
||||
}
|
||||
|
||||
const preset = config.get('preset') ?? 'mp3';
|
||||
let presetSetting: { extension: string; ffmpegArgs: string[] } | null = null;
|
||||
if (preset === 'opus') {
|
||||
presetSetting = presets[preset];
|
||||
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
|
||||
let presetSetting: Preset;
|
||||
if (selectedPreset === 'Custom') {
|
||||
presetSetting = config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
||||
} else if (selectedPreset === 'Source') {
|
||||
presetSetting = DefaultPresetList['Source'];
|
||||
} else {
|
||||
presetSetting = DefaultPresetList['mp3 (256kbps)'];
|
||||
}
|
||||
|
||||
let filename = filenamify(`${name}.${presetSetting?.extension ?? 'mp3'}`, {
|
||||
const downloadOptions: FormatOptions = {
|
||||
type: 'audio', // Audio, video or video+audio
|
||||
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'any', // Media container format
|
||||
};
|
||||
|
||||
const format = info.chooseFormat(downloadOptions);
|
||||
|
||||
let targetFileExtension: string;
|
||||
if (!presetSetting?.extension) {
|
||||
targetFileExtension = YoutubeFormatList.find((it) => it.itag === format.itag)?.container ?? 'mp3';
|
||||
} else {
|
||||
targetFileExtension = presetSetting?.extension ?? 'mp3';
|
||||
}
|
||||
|
||||
let filename = filenamify(`${name}.${targetFileExtension}`, {
|
||||
replacement: '_',
|
||||
maxLength: 255,
|
||||
});
|
||||
@ -241,13 +259,6 @@ async function downloadSongUnsafe(
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadOptions: FormatOptions = {
|
||||
type: 'audio', // Audio, video or video+audio
|
||||
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'any', // Media container format
|
||||
};
|
||||
|
||||
const format = info.chooseFormat(downloadOptions);
|
||||
const stream = await info.download(downloadOptions);
|
||||
|
||||
console.info(
|
||||
@ -260,39 +271,20 @@ async function downloadSongUnsafe(
|
||||
mkdirSync(dir);
|
||||
}
|
||||
|
||||
const ffmpegArgs = config.get('ffmpegArgs');
|
||||
const fileBuffer = await iterableStreamToTargetFile(
|
||||
iterableStream,
|
||||
targetFileExtension,
|
||||
metadata,
|
||||
presetSetting?.ffmpegArgs ?? [],
|
||||
format.content_length ?? 0,
|
||||
sendFeedback,
|
||||
increasePlaylistProgress,
|
||||
);
|
||||
|
||||
if (presetSetting && presetSetting?.extension !== 'mp3') {
|
||||
const file = createWriteStream(filePath);
|
||||
let downloaded = 0;
|
||||
const total: number = format.content_length ?? 1;
|
||||
|
||||
for await (const chunk of iterableStream) {
|
||||
downloaded += chunk.length;
|
||||
const ratio = downloaded / total;
|
||||
const progress = Math.floor(ratio * 100);
|
||||
sendFeedback(`Download: ${progress}%`, ratio);
|
||||
increasePlaylistProgress(ratio);
|
||||
file.write(chunk);
|
||||
}
|
||||
|
||||
await ffmpegWriteTags(
|
||||
filePath,
|
||||
metadata,
|
||||
presetSetting.ffmpegArgs,
|
||||
ffmpegArgs,
|
||||
);
|
||||
sendFeedback(null, -1);
|
||||
} else {
|
||||
const fileBuffer = await iterableStreamToMP3(
|
||||
iterableStream,
|
||||
metadata,
|
||||
ffmpegArgs,
|
||||
format.content_length ?? 0,
|
||||
sendFeedback,
|
||||
increasePlaylistProgress,
|
||||
);
|
||||
if (fileBuffer) {
|
||||
if (fileBuffer) {
|
||||
if (targetFileExtension !== 'mp3') {
|
||||
createWriteStream(filePath).write(fileBuffer);
|
||||
} else {
|
||||
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
|
||||
if (buffer) {
|
||||
writeFileSync(filePath, buffer);
|
||||
@ -304,10 +296,11 @@ async function downloadSongUnsafe(
|
||||
console.info(`Done: "${filePath}"`);
|
||||
}
|
||||
|
||||
async function iterableStreamToMP3(
|
||||
async function iterableStreamToTargetFile(
|
||||
stream: AsyncGenerator<Uint8Array, void>,
|
||||
extension: string,
|
||||
metadata: CustomSongInfo,
|
||||
ffmpegArgs: string[],
|
||||
presetFfmpegArgs: string[],
|
||||
contentLength: number,
|
||||
sendFeedback: (str: string, value?: number) => void,
|
||||
increasePlaylistProgress: (value: number) => void = () => {
|
||||
@ -347,13 +340,14 @@ async function iterableStreamToMP3(
|
||||
increasePlaylistProgress(0.15 + (ratio * 0.85));
|
||||
});
|
||||
|
||||
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
|
||||
try {
|
||||
await ffmpeg.run(
|
||||
'-i',
|
||||
safeVideoName,
|
||||
...ffmpegArgs,
|
||||
...presetFfmpegArgs,
|
||||
...getFFmpegMetadataArgs(metadata),
|
||||
`${safeVideoName}.mp3`,
|
||||
safeVideoNameWithExtension,
|
||||
);
|
||||
} finally {
|
||||
ffmpeg.FS('unlink', safeVideoName);
|
||||
@ -362,9 +356,9 @@ async function iterableStreamToMP3(
|
||||
sendFeedback('Saving…');
|
||||
|
||||
try {
|
||||
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`);
|
||||
return ffmpeg.FS('readFile', safeVideoNameWithExtension);
|
||||
} finally {
|
||||
ffmpeg.FS('unlink', `${safeVideoName}.mp3`);
|
||||
ffmpeg.FS('unlink', safeVideoNameWithExtension);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
sendError(error as Error, safeVideoName);
|
||||
@ -466,11 +460,18 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
let playlistTitle = playlist.header?.title?.text ?? '';
|
||||
const isAlbum = playlistTitle?.startsWith('Album - ');
|
||||
if (isAlbum) {
|
||||
playlistTitle = playlistTitle.slice(8);
|
||||
}
|
||||
const normalPlaylistTitle = playlist.header?.title?.text;
|
||||
const playlistTitle = normalPlaylistTitle ??
|
||||
playlist
|
||||
.page
|
||||
.contents_memo
|
||||
?.get('MusicResponsiveListItemFlexColumn')
|
||||
?.at(2)
|
||||
?.as(YTNodes.MusicResponsiveListItemFlexColumn)
|
||||
?.title
|
||||
?.text ??
|
||||
'NO_TITLE';
|
||||
const isAlbum = !normalPlaylistTitle;
|
||||
|
||||
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
|
||||
if (!is.macOS()) {
|
||||
@ -544,29 +545,6 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ffmpegWriteTags(filePath: string, metadata: CustomSongInfo, presetFFmpegArgs: string[] = [], ffmpegArgs: string[] = []) {
|
||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||
|
||||
try {
|
||||
if (!ffmpeg.isLoaded()) {
|
||||
await ffmpeg.load();
|
||||
}
|
||||
|
||||
await ffmpeg.run(
|
||||
'-i',
|
||||
filePath,
|
||||
...getFFmpegMetadataArgs(metadata),
|
||||
...presetFFmpegArgs,
|
||||
...ffmpegArgs,
|
||||
filePath,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
sendError(error as Error);
|
||||
} finally {
|
||||
releaseFFmpegMutex();
|
||||
}
|
||||
}
|
||||
|
||||
function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
|
||||
if (!metadata) {
|
||||
return [];
|
||||
@ -601,8 +579,7 @@ const getMetadata = (info: TrackInfo): CustomSongInfo => ({
|
||||
videoId: info.basic_info.id!,
|
||||
title: cleanupName(info.basic_info.title!),
|
||||
artist: cleanupName(info.basic_info.author!),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||
album: (info.player_overlays?.browser_media_session as any)?.album?.text as string | undefined,
|
||||
album: info.player_overlays?.browser_media_session?.as(YTNodes.BrowserMediaSession).album?.text,
|
||||
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
|
||||
views: info.basic_info.view_count!,
|
||||
songDuration: info.basic_info.duration!,
|
||||
|
||||
@ -47,7 +47,7 @@ const menuObserver = new MutationObserver(() => {
|
||||
(global as any).download = () => {
|
||||
let videoUrl = getSongMenu()
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')
|
||||
?.getAttribute('href');
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { dialog } from 'electron';
|
||||
|
||||
import { downloadPlaylist } from './back';
|
||||
import { defaultMenuDownloadLabel, getFolder, presets } from './utils';
|
||||
import { defaultMenuDownloadLabel, getFolder } from './utils';
|
||||
import { DefaultPresetList } from './types';
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
@ -25,12 +26,12 @@ export default (): MenuTemplate => [
|
||||
},
|
||||
{
|
||||
label: 'Presets',
|
||||
submenu: Object.keys(presets).map((preset) => ({
|
||||
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||
label: preset,
|
||||
type: 'radio',
|
||||
checked: config.get('preset') === preset,
|
||||
checked: config.get('selectedPreset') === preset,
|
||||
click() {
|
||||
config.set('preset', preset);
|
||||
config.set('selectedPreset', preset);
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
||||
116
plugins/downloader/types.ts
Normal file
116
plugins/downloader/types.ts
Normal file
@ -0,0 +1,116 @@
|
||||
export interface Preset {
|
||||
extension?: string | null;
|
||||
ffmpegArgs: string[];
|
||||
}
|
||||
|
||||
// Presets for FFmpeg
|
||||
export const DefaultPresetList: Record<string, Preset> = {
|
||||
'mp3 (256kbps)': {
|
||||
extension: 'mp3',
|
||||
ffmpegArgs: ['-b:a', '256k'],
|
||||
},
|
||||
'Source': {
|
||||
extension: undefined,
|
||||
ffmpegArgs: ['-acodec', 'copy'],
|
||||
},
|
||||
'Custom': {
|
||||
extension: null,
|
||||
ffmpegArgs: [],
|
||||
}
|
||||
};
|
||||
|
||||
export interface YouTubeFormat {
|
||||
itag: number;
|
||||
container: string;
|
||||
content: string;
|
||||
resolution: string;
|
||||
bitrate: string;
|
||||
range: string;
|
||||
vrOr3D: string;
|
||||
}
|
||||
|
||||
// converted from https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2#file-youtube_format_code_itag_list-md
|
||||
export const YoutubeFormatList: YouTubeFormat[] = [
|
||||
{ itag: 5, container: 'flv', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 6, container: 'flv', content: 'audio/video', resolution: '270p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 17, container: '3gp', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 18, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 22, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 34, container: 'flv', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 35, container: 'flv', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 36, container: '3gp', content: 'audio/video', resolution: '180p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 37, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 38, container: 'mp4', content: 'audio/video', resolution: '3072p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 43, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 44, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 45, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 46, container: 'webm', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 82, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 83, container: 'mp4', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 84, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 85, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 91, container: 'hls', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 92, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 93, container: 'hls', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 94, container: 'hls', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 95, container: 'hls', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 96, container: 'hls', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 100, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 101, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 102, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 132, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 133, container: 'mp4', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 134, container: 'mp4', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 135, container: 'mp4', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 136, container: 'mp4', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 137, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 138, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 139, container: 'm4a', content: 'audio', resolution: '-', bitrate: '48k', range: '-', vrOr3D: '' },
|
||||
{ itag: 140, container: 'm4a', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
|
||||
{ itag: 141, container: 'm4a', content: 'audio', resolution: '-', bitrate: '256k', range: '-', vrOr3D: '' },
|
||||
{ itag: 151, container: 'hls', content: 'audio/video', resolution: '72p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 160, container: 'mp4', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 167, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 168, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 169, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 171, container: 'webm', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
|
||||
{ itag: 218, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 219, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 242, container: 'webm', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 243, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 244, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 245, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 246, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 247, container: 'webm', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 248, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 249, container: 'webm', content: 'audio', resolution: '-', bitrate: '50k', range: '-', vrOr3D: '' },
|
||||
{ itag: 250, container: 'webm', content: 'audio', resolution: '-', bitrate: '70k', range: '-', vrOr3D: '' },
|
||||
{ itag: 251, container: 'webm', content: 'audio', resolution: '-', bitrate: '160k', range: '-', vrOr3D: '' },
|
||||
{ itag: 264, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 266, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 271, container: 'webm', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 272, container: 'webm', content: 'video', resolution: '4320p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 278, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 298, container: 'mp4', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 299, container: 'mp4', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 302, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 303, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 308, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 313, container: 'webm', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 315, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 330, container: 'webm', content: 'video', resolution: '144p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 331, container: 'webm', content: 'video', resolution: '240p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 332, container: 'webm', content: 'video', resolution: '360p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 333, container: 'webm', content: 'video', resolution: '480p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 334, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 335, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 336, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 337, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 272, container: 'webm', content: 'video', resolution: '2880p/4320p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 399, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 400, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 401, container: 'mp4', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 402, container: 'mp4', content: 'video', resolution: '2880p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 571, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 702, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
];
|
||||
@ -10,7 +10,7 @@ export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
|
||||
|
||||
export const cropMaxWidth = (image: Electron.NativeImage) => {
|
||||
const imageSize = image.getSize();
|
||||
// Standart youtube artwork width with margins from both sides is 280 + 720 + 280
|
||||
// Standart YouTube artwork width with margins from both sides is 280 + 720 + 280
|
||||
if (imageSize.width === 1280 && imageSize.height === 720) {
|
||||
return image.crop({
|
||||
x: 280,
|
||||
@ -23,15 +23,6 @@ export const cropMaxWidth = (image: Electron.NativeImage) => {
|
||||
return image;
|
||||
};
|
||||
|
||||
// Presets for FFmpeg
|
||||
export const presets = {
|
||||
'None (defaults to mp3)': undefined,
|
||||
'opus': {
|
||||
extension: 'opus',
|
||||
ffmpegArgs: ['-acodec', 'libopus'],
|
||||
},
|
||||
};
|
||||
|
||||
export const setBadge = (n: number) => {
|
||||
if (is.linux() || is.macOS()) {
|
||||
app.setBadgeCount(n);
|
||||
|
||||
@ -61,5 +61,7 @@ export default (win: BrowserWindow) => {
|
||||
ipcMain.handle('window-close', () => win.close());
|
||||
ipcMain.handle('window-minimize', () => win.minimize());
|
||||
ipcMain.handle('window-maximize', () => win.maximize());
|
||||
win.on('maximize', () => win.webContents.send('window-maximize'));
|
||||
ipcMain.handle('window-unmaximize', () => win.unmaximize());
|
||||
win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
|
||||
};
|
||||
|
||||
@ -22,6 +22,7 @@ export default async () => {
|
||||
let hideMenu = config.get('options.hideMenu');
|
||||
const titleBar = document.createElement('title-bar');
|
||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||
let maximizeButton: HTMLButtonElement;
|
||||
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
|
||||
|
||||
logo.classList.add('title-bar-icon');
|
||||
@ -55,7 +56,7 @@ export default async () => {
|
||||
minimizeButton.appendChild(minimize);
|
||||
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
|
||||
|
||||
const maximizeButton = document.createElement('button');
|
||||
maximizeButton = document.createElement('button');
|
||||
if (await ipcRenderer.invoke('window-is-maximized')) {
|
||||
maximizeButton.classList.add('window-control');
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
@ -135,8 +136,14 @@ export default async () => {
|
||||
|
||||
document.title = 'Youtube Music';
|
||||
|
||||
ipcRenderer.on('refreshMenu', () => {
|
||||
updateMenu();
|
||||
ipcRenderer.on('refreshMenu', () => updateMenu());
|
||||
ipcRenderer.on('window-maximize', () => {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
});
|
||||
ipcRenderer.on('window-unmaximize', () => {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(maximize);
|
||||
});
|
||||
|
||||
if (isEnabled('picture-in-picture')) {
|
||||
|
||||
Reference in New Issue
Block a user