Compare commits

...

13 Commits

16 changed files with 314 additions and 135 deletions

View File

@ -117,7 +117,7 @@ jobs:
if: ${{ env.VERSION_HASH == '' }} if: ${{ env.VERSION_HASH == '' }}
uses: irongut/EditRelease@v1.2.0 uses: irongut/EditRelease@v1.2.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GH_TOKEN }}
id: ${{ steps.get_draft_release.outputs.id }} id: ${{ steps.get_draft_release.outputs.id }}
draft: false draft: false
prerelease: false prerelease: false

20
.github/workflows/winget-cla.yml vendored Normal file
View 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 }}

View File

@ -19,7 +19,7 @@ jobs:
uses: vedantmgoyal2009/winget-releaser@v2 uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: th-ch.YouTubeMusic 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 }} version: ${{ inputs.tag_name || github.event.release.tag_name }}
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }} release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
token: ${{ secrets.WINGET_ACC_TOKEN }} token: ${{ secrets.WINGET_ACC_TOKEN }}

View File

@ -2,8 +2,30 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. 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) #### [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(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) - 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) - fix(mpris): fixed an issue where MPRIS information was incorrect [`#1291`](https://github.com/th-ch/youtube-music/pull/1291)

View File

@ -1,5 +1,7 @@
import { blockers } from '../plugins/adblocker/blocker-types'; import { blockers } from '../plugins/adblocker/blocker-types';
import { DefaultPresetList } from '../plugins/downloader/types';
export interface WindowSizeConfig { export interface WindowSizeConfig {
width: number; width: number;
height: number; height: number;
@ -111,14 +113,19 @@ const defaultConfig = {
}, },
'downloader': { 'downloader': {
enabled: false, 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) downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
preset: 'mp3', selectedPreset: 'mp3 (256kbps)', // Selected preset
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
skipExisting: false, skipExisting: false,
playlistMaxItems: undefined as number | undefined, playlistMaxItems: undefined as number | undefined,
}, },
'exponential-volume': {}, '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': { 'last-fm': {
enabled: false, enabled: false,
token: undefined as string | undefined, // Token used for authentication token: undefined as string | undefined, // Token used for authentication

View File

@ -1,8 +1,18 @@
import Store from 'electron-store'; import Store from 'electron-store';
import Conf from 'conf'; import Conf from 'conf';
import is from 'electron-is';
import defaults from './defaults'; 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) => { const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
if (!store.get(`plugins.${plugin}`)) { if (!store.get(`plugins.${plugin}`)) {
store.set(`plugins.${plugin}`, defaults.plugins[plugin]); store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
@ -10,6 +20,26 @@ const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: k
}; };
const migrations = { 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>>) { '>=1.20.0'(store: Conf<Record<string, unknown>>) {
setDefaultPluginOptions(store, 'visualizer'); setDefaultPluginOptions(store, 'visualizer');
@ -118,7 +148,7 @@ const migrations = {
}; };
export default new Store({ export default new Store({
defaults, defaults: getDefaults(),
clearInvalidConfig: false, clearInvalidConfig: false,
migrations, migrations,
}); });

View File

@ -420,6 +420,12 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
{ role: 'quit' }, { role: 'quit' },
], ],
}, },
{
label: 'About',
submenu: [
{ role: 'about' },
],
}
]; ];
}; };
export const setApplicationMenu = (win: Electron.BrowserWindow) => { export const setApplicationMenu = (win: Electron.BrowserWindow) => {

46
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"version": "2.0.4", "version": "2.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "youtube-music", "name": "youtube-music",
"version": "2.0.4", "version": "2.1.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -42,7 +42,7 @@
"ytpl": "2.3.0" "ytpl": "2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.38.1", "@playwright/test": "1.39.0",
"@rollup/plugin-commonjs": "25.0.5", "@rollup/plugin-commonjs": "25.0.5",
"@rollup/plugin-image": "3.0.3", "@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.0.1", "@rollup/plugin-json": "6.0.1",
@ -65,10 +65,10 @@
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "5.0.1",
"node-gyp": "9.4.0", "node-gyp": "9.4.0",
"patch-package": "8.0.0", "patch-package": "8.0.0",
"playwright": "1.38.1", "playwright": "1.39.0",
"rollup": "4.0.2", "rollup": "4.0.2",
"rollup-plugin-copy": "3.5.0", "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", "rollup-plugin-string": "3.0.0",
"typescript": "5.2.2" "typescript": "5.2.2"
}, },
@ -269,9 +269,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.23.1", "version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
"integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -1163,12 +1163,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.38.1", "version": "1.39.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz",
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright": "1.38.1" "playwright": "1.39.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8060,12 +8060,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.38.1", "version": "1.39.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz",
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright-core": "1.38.1" "playwright-core": "1.39.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8078,9 +8078,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.38.1", "version": "1.39.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz",
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==",
"dev": true, "dev": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -8574,9 +8574,9 @@
} }
}, },
"node_modules/rollup-plugin-import-css": { "node_modules/rollup-plugin-import-css": {
"version": "3.3.4", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.3.4.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.3.5.tgz",
"integrity": "sha512-w5p1Dd1CavAht/P82zB3WX2RVy7O47MlJGSmgrWXTBPAkWHTbOBh/nUPz94IczCD0HLxpuT4AhF24cix7CpZWA==", "integrity": "sha512-wSfzveEzvUDlVevo70kmVD5Mk785UN55NG4C7VVnrmdE0qZ8apcVVFajyCPfFYSNxq5YkccOcrGUT2T/2HnEcQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.4" "@rollup/pluginutils": "^5.0.4"
@ -8585,7 +8585,7 @@
"node": ">=16" "node": ">=16"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.x.x || ^3.x.x" "rollup": "^2.x.x || ^3.x.x || ^4.x.x"
} }
}, },
"node_modules/rollup-plugin-string": { "node_modules/rollup-plugin-string": {

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "2.0.4", "version": "2.1.1",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
@ -150,10 +150,10 @@
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "2.7.0", "node-fetch": "2.7.0",
"@electron/universal": "1.4.2", "@electron/universal": "1.4.2",
"@babel/runtime": "7.23.1" "@babel/runtime": "7.23.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.38.1", "@playwright/test": "1.39.0",
"@rollup/plugin-commonjs": "25.0.5", "@rollup/plugin-commonjs": "25.0.5",
"@rollup/plugin-image": "3.0.3", "@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.0.1", "@rollup/plugin-json": "6.0.1",
@ -176,10 +176,10 @@
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "5.0.1",
"node-gyp": "9.4.0", "node-gyp": "9.4.0",
"patch-package": "8.0.0", "patch-package": "8.0.0",
"playwright": "1.38.1", "playwright": "1.39.0",
"rollup": "4.0.2", "rollup": "4.0.2",
"rollup-plugin-copy": "3.5.0", "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", "rollup-plugin-string": "3.0.0",
"typescript": "5.2.2" "typescript": "5.2.2"
}, },

View File

@ -8,12 +8,11 @@ import is from 'electron-is';
import filenamify from 'filenamify'; import filenamify from 'filenamify';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { createFFmpeg } from '@ffmpeg.wasm/main'; import { createFFmpeg } from '@ffmpeg.wasm/main';
import NodeID3, { TagConstants } from 'node-id3'; 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 config from './config';
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
import style from './style.css'; import style from './style.css';
@ -221,13 +220,32 @@ async function downloadSongUnsafe(
); );
} }
const preset = config.get('preset') ?? 'mp3'; const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
let presetSetting: { extension: string; ffmpegArgs: string[] } | null = null; let presetSetting: Preset;
if (preset === 'opus') { if (selectedPreset === 'Custom') {
presetSetting = presets[preset]; 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: '_', replacement: '_',
maxLength: 255, maxLength: 255,
}); });
@ -241,13 +259,6 @@ async function downloadSongUnsafe(
return; 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); const stream = await info.download(downloadOptions);
console.info( console.info(
@ -260,39 +271,20 @@ async function downloadSongUnsafe(
mkdirSync(dir); 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') { if (fileBuffer) {
const file = createWriteStream(filePath); if (targetFileExtension !== 'mp3') {
let downloaded = 0; createWriteStream(filePath).write(fileBuffer);
const total: number = format.content_length ?? 1; } else {
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) {
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback); const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
if (buffer) { if (buffer) {
writeFileSync(filePath, buffer); writeFileSync(filePath, buffer);
@ -304,10 +296,11 @@ async function downloadSongUnsafe(
console.info(`Done: "${filePath}"`); console.info(`Done: "${filePath}"`);
} }
async function iterableStreamToMP3( async function iterableStreamToTargetFile(
stream: AsyncGenerator<Uint8Array, void>, stream: AsyncGenerator<Uint8Array, void>,
extension: string,
metadata: CustomSongInfo, metadata: CustomSongInfo,
ffmpegArgs: string[], presetFfmpegArgs: string[],
contentLength: number, contentLength: number,
sendFeedback: (str: string, value?: number) => void, sendFeedback: (str: string, value?: number) => void,
increasePlaylistProgress: (value: number) => void = () => { increasePlaylistProgress: (value: number) => void = () => {
@ -347,13 +340,14 @@ async function iterableStreamToMP3(
increasePlaylistProgress(0.15 + (ratio * 0.85)); increasePlaylistProgress(0.15 + (ratio * 0.85));
}); });
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
try { try {
await ffmpeg.run( await ffmpeg.run(
'-i', '-i',
safeVideoName, safeVideoName,
...ffmpegArgs, ...presetFfmpegArgs,
...getFFmpegMetadataArgs(metadata), ...getFFmpegMetadataArgs(metadata),
`${safeVideoName}.mp3`, safeVideoNameWithExtension,
); );
} finally { } finally {
ffmpeg.FS('unlink', safeVideoName); ffmpeg.FS('unlink', safeVideoName);
@ -362,9 +356,9 @@ async function iterableStreamToMP3(
sendFeedback('Saving…'); sendFeedback('Saving…');
try { try {
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`); return ffmpeg.FS('readFile', safeVideoNameWithExtension);
} finally { } finally {
ffmpeg.FS('unlink', `${safeVideoName}.mp3`); ffmpeg.FS('unlink', safeVideoNameWithExtension);
} }
} catch (error: unknown) { } catch (error: unknown) {
sendError(error as Error, safeVideoName); sendError(error as Error, safeVideoName);
@ -466,11 +460,18 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
return; return;
} }
let playlistTitle = playlist.header?.title?.text ?? ''; const normalPlaylistTitle = playlist.header?.title?.text;
const isAlbum = playlistTitle?.startsWith('Album - '); const playlistTitle = normalPlaylistTitle ??
if (isAlbum) { playlist
playlistTitle = playlistTitle.slice(8); .page
} .contents_memo
?.get('MusicResponsiveListItemFlexColumn')
?.at(2)
?.as(YTNodes.MusicResponsiveListItemFlexColumn)
?.title
?.text ??
'NO_TITLE';
const isAlbum = !normalPlaylistTitle;
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' }); let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
if (!is.macOS()) { 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) { function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
if (!metadata) { if (!metadata) {
return []; return [];
@ -601,8 +579,7 @@ const getMetadata = (info: TrackInfo): CustomSongInfo => ({
videoId: info.basic_info.id!, videoId: info.basic_info.id!,
title: cleanupName(info.basic_info.title!), title: cleanupName(info.basic_info.title!),
artist: cleanupName(info.basic_info.author!), 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(YTNodes.BrowserMediaSession).album?.text,
album: (info.player_overlays?.browser_media_session as any)?.album?.text as string | undefined,
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url, imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
views: info.basic_info.view_count!, views: info.basic_info.view_count!,
songDuration: info.basic_info.duration!, songDuration: info.basic_info.duration!,

View File

@ -47,7 +47,7 @@ const menuObserver = new MutationObserver(() => {
(global as any).download = () => { (global as any).download = () => {
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// Selector of first button which is always "Start Radio" // 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'); ?.getAttribute('href');
if (videoUrl) { if (videoUrl) {
if (videoUrl.startsWith('watch?')) { if (videoUrl.startsWith('watch?')) {

View File

@ -1,7 +1,8 @@
import { dialog } from 'electron'; import { dialog } from 'electron';
import { downloadPlaylist } from './back'; import { downloadPlaylist } from './back';
import { defaultMenuDownloadLabel, getFolder, presets } from './utils'; import { defaultMenuDownloadLabel, getFolder } from './utils';
import { DefaultPresetList } from './types';
import config from './config'; import config from './config';
import { MenuTemplate } from '../../menu'; import { MenuTemplate } from '../../menu';
@ -25,12 +26,12 @@ export default (): MenuTemplate => [
}, },
{ {
label: 'Presets', label: 'Presets',
submenu: Object.keys(presets).map((preset) => ({ submenu: Object.keys(DefaultPresetList).map((preset) => ({
label: preset, label: preset,
type: 'radio', type: 'radio',
checked: config.get('preset') === preset, checked: config.get('selectedPreset') === preset,
click() { click() {
config.set('preset', preset); config.set('selectedPreset', preset);
}, },
})), })),
}, },

116
plugins/downloader/types.ts Normal file
View 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: '' },
];

View File

@ -10,7 +10,7 @@ export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
export const cropMaxWidth = (image: Electron.NativeImage) => { export const cropMaxWidth = (image: Electron.NativeImage) => {
const imageSize = image.getSize(); 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) { if (imageSize.width === 1280 && imageSize.height === 720) {
return image.crop({ return image.crop({
x: 280, x: 280,
@ -23,15 +23,6 @@ export const cropMaxWidth = (image: Electron.NativeImage) => {
return image; return image;
}; };
// Presets for FFmpeg
export const presets = {
'None (defaults to mp3)': undefined,
'opus': {
extension: 'opus',
ffmpegArgs: ['-acodec', 'libopus'],
},
};
export const setBadge = (n: number) => { export const setBadge = (n: number) => {
if (is.linux() || is.macOS()) { if (is.linux() || is.macOS()) {
app.setBadgeCount(n); app.setBadgeCount(n);

View File

@ -61,5 +61,7 @@ export default (win: BrowserWindow) => {
ipcMain.handle('window-close', () => win.close()); ipcMain.handle('window-close', () => win.close());
ipcMain.handle('window-minimize', () => win.minimize()); ipcMain.handle('window-minimize', () => win.minimize());
ipcMain.handle('window-maximize', () => win.maximize()); ipcMain.handle('window-maximize', () => win.maximize());
win.on('maximize', () => win.webContents.send('window-maximize'));
ipcMain.handle('window-unmaximize', () => win.unmaximize()); ipcMain.handle('window-unmaximize', () => win.unmaximize());
win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
}; };

View File

@ -22,6 +22,7 @@ export default async () => {
let hideMenu = config.get('options.hideMenu'); let hideMenu = config.get('options.hideMenu');
const titleBar = document.createElement('title-bar'); const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background'); const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
let maximizeButton: HTMLButtonElement;
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px'); if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
logo.classList.add('title-bar-icon'); logo.classList.add('title-bar-icon');
@ -55,7 +56,7 @@ export default async () => {
minimizeButton.appendChild(minimize); minimizeButton.appendChild(minimize);
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize'); minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
const maximizeButton = document.createElement('button'); maximizeButton = document.createElement('button');
if (await ipcRenderer.invoke('window-is-maximized')) { if (await ipcRenderer.invoke('window-is-maximized')) {
maximizeButton.classList.add('window-control'); maximizeButton.classList.add('window-control');
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
@ -135,8 +136,14 @@ export default async () => {
document.title = 'Youtube Music'; document.title = 'Youtube Music';
ipcRenderer.on('refreshMenu', () => { ipcRenderer.on('refreshMenu', () => updateMenu());
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')) { if (isEnabled('picture-in-picture')) {