Compare commits

...

48 Commits

Author SHA1 Message Date
6288d0b171 Bump version to 2.2.0 2023-10-27 02:01:44 +09:00
4248d20e8e bump deps 2023-10-25 16:46:59 +09:00
0b413492ad feat(ambient-mode): add config for ambient-mode plugin (#1349) 2023-10-25 15:37:51 +09:00
dc73561c8a Update changelog for v2.1.3 2023-10-22 15:50:12 +00:00
949a2f6428 Bump version to 2.1.3 2023-10-23 00:35:10 +09:00
bceaa05197 fix(store): fix listenAlong statement 2023-10-23 00:29:59 +09:00
776cdac30d feat(discord): rename Listen Along to Play on YTM
resolve #1341
2023-10-23 00:27:43 +09:00
4333891cca chore(deps): bump deps 2023-10-23 00:19:42 +09:00
8a89bbccf7 fix: fixed bugs in downloader (#1342) 2023-10-23 00:19:01 +09:00
fa4c69d228 Update changelog for v2.1.2 2023-10-19 13:55:04 +00:00
c25def8901 Bump version to 2.1.2 2023-10-19 22:39:14 +09:00
284a59b721 fix: fix unresponsive (fix #1325) 2023-10-19 22:35:32 +09:00
5fcba8619a feat(in-app-menu): add an option to hide the window controls (#1335) 2023-10-19 22:34:18 +09:00
f3cd759276 fix: fixed an issue where the album name was missing (#1334) 2023-10-19 21:45:46 +09:00
9d3981e361 chore: Update build.yml 2023-10-19 21:33:54 +09:00
787326948b chore: making actions more efficient 2023-10-19 09:51:31 +09:00
779251933c chore(deps): update dependency electron to v27.0.1 (#1331) 2023-10-19 09:42:41 +09:00
1efe835c69 fix: fixed an issue where only the first 100 songs in a playlist were downloaded (#1329) 2023-10-19 05:40:05 +09:00
5702978227 chore(deps): update dependencies 2023-10-18 18:18:28 +09:00
fa3d742838 chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.8.0 2023-10-18 02:21:25 +09:00
c460cc2296 Updated readme plugins list (#1326) 2023-10-17 20:04:23 +09:00
4e4af5e830 fix(actions): fix if statement 2023-10-16 22:55:04 +09:00
9a4e98063b chore(actions): disable pnpm cache for macOS 2023-10-16 22:54:11 +09:00
8bfe04bb50 chore: update issue template
Thanks to Alipoodle for writing this question.
2023-10-16 22:42:02 +09:00
6774d54f5e chore(deps): update dependency rollup to v4.1.4 2023-10-16 16:41:36 +09:00
9705f8489d chore(deps): Bump @rollup/plugin-commonjs, pnpm version, Remove ytpl 2023-10-16 16:24:16 +09:00
a7229cbe14 Bump @rollup/plugin-commonjs, pnpm version 2023-10-16 03:08:04 +09:00
7577aba45e feat: use test:debug for CI 2023-10-16 02:08:25 +09:00
d78fbe476e hotfix: fix Cannot read properties of undefined (reading 'removeChild') 2023-10-16 01:09:24 +09:00
bfe4b2bba7 fix(actions): use GabrielBB/xvfb-action instead of coactions/setup-xvfb 2023-10-16 00:58:02 +09:00
7625a3aa52 QOL: Move source code under the src directory. (#1318) 2023-10-15 21:52:48 +09:00
30c8dcf730 fix: release action 2023-10-15 18:54:25 +09:00
00a3e8d35e chore(deps): Bump rollup, @xhayper/discord-rpc version 2023-10-15 18:35:57 +09:00
4d01cdfa6c fix(blocker): remove the app.isPackaged check (fix #1315) 2023-10-15 18:33:14 +09:00
f924b6c8e3 fix(actions): install pnpm before call setup-node 2023-10-15 18:28:09 +09:00
926d98174c fix: fix build actions 2023-10-15 18:22:19 +09:00
41b3972f54 chore(README): add pnpm install guide 2023-10-15 18:20:49 +09:00
467f29e363 feat: migrate from npm to pnpm (#1316) 2023-10-15 18:18:20 +09:00
9cc13c3757 Merge pull request #1317 from foonathan/fix-loop-status 2023-10-15 04:23:33 +09:00
f8ccb86156 Fix mpris player.loopStatus 2023-10-14 21:03:06 +02:00
b316aa2301 chore(deps): update dependency rollup to v4.1.0 2023-10-14 22:33:09 +09:00
5c49b28664 fix(discord): Discord RPC fails if a song's title is only one character (fix #1314) 2023-10-14 20:27:58 +09:00
dedf96afd3 Update changelog for v2.1.1 2023-10-14 05:49:56 +00:00
3bb5bc2ca1 Bump version to 2.1.1 2023-10-14 14:34:39 +09:00
c79fdd9887 fix: empty title playlist directory path 2023-10-14 14:03:47 +09:00
d7b821727d hotfix(downloader): can't get an album title (fix #1313) 2023-10-14 13:55:57 +09:00
TC
21c45faf20 Add "about" menu to show app version 2023-10-13 22:04:50 +02:00
92cab89d17 Update changelog for v2.1.0 2023-10-13 19:27:20 +00:00
172 changed files with 6479 additions and 10386 deletions

View File

@ -12,11 +12,13 @@ body:
required: true
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a bug report that matches the one I want to file, without success.
required: true
- label: I understand that **th-ch/youtube-music has NO affiliation with Google or YouTube**
required: true
- type: input
attributes:
label: YouTube Music (Application) Version
description: |
What version of YouTube Music Application are you using?
What version of the YouTube Music Application are you using?
Note: Please check if this issue is reproducible with the latest stable release.
placeholder: 2.0.0
@ -36,7 +38,7 @@ body:
- type: input
attributes:
label: Operating System Version
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
description: What operating system version are you using? On Windows, click the Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations:
required: true

View File

@ -20,59 +20,63 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Setup NodeJS
if: startsWith(matrix.os, 'macOS') != true
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Setup NodeJS for macOS
if: startsWith(matrix.os, 'macOS')
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
# Only rollup build without release if it is a fork
- name: Rollup Build
if: github.repository == 'th-ch/youtube-music' && github.event_name == 'pull_request'
run: |
pnpm build
# Build and release if it's the main repository and is not pull-request
- name: Build and release on Mac
if: startsWith(matrix.os, 'macOS') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pnpm release:mac
- name: Build and release on Linux
if: startsWith(matrix.os, 'ubuntu') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pnpm release:linux
- name: Build and release on Windows
if: startsWith(matrix.os, 'windows') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pnpm release:win
- name: Test
uses: GabrielBB/xvfb-action@v1
uses: coactions/setup-xvfb@v1
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
with:
run: npm run test
run: pnpm test:debug
# Build and release if it's the main repository
- name: Build and release on Mac
if: startsWith(matrix.os, 'macOS') && github.repository == 'th-ch/youtube-music'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm run release:mac
- name: Build and release on Linux
if: startsWith(matrix.os, 'ubuntu') && github.repository == 'th-ch/youtube-music'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm run release:linux
- name: Build and release on Windows
if: startsWith(matrix.os, 'windows') && github.repository == 'th-ch/youtube-music'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm run release:win
# Only build without release if it is a fork
- name: Build on Mac
if: startsWith(matrix.os, 'macOS') && github.repository != 'th-ch/youtube-music'
run: |
npm run build:mac
- name: Build on Linux
if: startsWith(matrix.os, 'ubuntu') && github.repository != 'th-ch/youtube-music'
run: |
npm run build:linux
- name: Build on Windows
if: startsWith(matrix.os, 'windows') && github.repository != 'th-ch/youtube-music'
run: |
npm run build:win
release:
runs-on: ubuntu-latest
@ -84,14 +88,27 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Setup NodeJS
if: startsWith(matrix.os, 'macOS') != true
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Setup NodeJS for macOS
if: startsWith(matrix.os, 'macOS')
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Get version
run: |
@ -132,7 +149,7 @@ jobs:
- name: Update changelog
if: ${{ env.VERSION_HASH == '' }}
run: |
npm run changelog
pnpm changelog
- name: Commit changelog
if: ${{ env.VERSION_HASH == '' }}

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true
}

View File

@ -2,8 +2,57 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v2.1.3](https://github.com/th-ch/youtube-music/compare/v2.1.2...v2.1.3)
- fix: fixed bugs in downloader [`#1342`](https://github.com/th-ch/youtube-music/pull/1342)
- feat(discord): rename `Listen Along` to `Play on YTM` [`#1341`](https://github.com/th-ch/youtube-music/issues/1341)
- chore(deps): bump deps [`4333891`](https://github.com/th-ch/youtube-music/commit/4333891ccabe42aedf756fd48618be715db13262)
- Update changelog for v2.1.2 [`fa4c69d`](https://github.com/th-ch/youtube-music/commit/fa4c69d228d4e06a7858e2b22fcdfa075a8ca766)
- fix(store): fix listenAlong statement [`bceaa05`](https://github.com/th-ch/youtube-music/commit/bceaa05197d47a4a4bbd22e767d1e4d6ec277514)
#### [v2.1.2](https://github.com/th-ch/youtube-music/compare/v2.1.1...v2.1.2)
> 19 October 2023
- feat(in-app-menu): add an option to hide the window controls [`#1335`](https://github.com/th-ch/youtube-music/pull/1335)
- fix: fixed an issue where the album name was missing [`#1334`](https://github.com/th-ch/youtube-music/pull/1334)
- chore(deps): update dependency electron to v27.0.1 [`#1331`](https://github.com/th-ch/youtube-music/pull/1331)
- fix: fixed an issue where only the first 100 songs in a playlist were downloaded [`#1329`](https://github.com/th-ch/youtube-music/pull/1329)
- Updated readme plugins list [`#1326`](https://github.com/th-ch/youtube-music/pull/1326)
- QOL: Move source code under the src directory. [`#1318`](https://github.com/th-ch/youtube-music/pull/1318)
- feat: migrate from `npm` to `pnpm` [`#1316`](https://github.com/th-ch/youtube-music/pull/1316)
- fix: fix unresponsive (fix #1325) [`#1325`](https://github.com/th-ch/youtube-music/issues/1325)
- fix(blocker): remove the `app.isPackaged` check (fix #1315) [`#1315`](https://github.com/th-ch/youtube-music/issues/1315)
- fix(discord): `Discord RPC fails if a song's title is only one character` (fix #1314) [`#1314`](https://github.com/th-ch/youtube-music/issues/1314)
- chore(deps): Bump @rollup/plugin-commonjs, pnpm version, Remove ytpl [`9705f84`](https://github.com/th-ch/youtube-music/commit/9705f8489d7bf262bfd8b15ab84c2d3485f10eae)
- chore(deps): Bump rollup, @xhayper/discord-rpc version [`00a3e8d`](https://github.com/th-ch/youtube-music/commit/00a3e8d35ec335e1913be19f30ae09dbe0b7acdd)
- chore(deps): update dependency rollup to v4.1.4 [`6774d54`](https://github.com/th-ch/youtube-music/commit/6774d54f5eca432edc2e11743d9d1b1c2fda9ac8)
#### [v2.1.1](https://github.com/th-ch/youtube-music/compare/v2.1.0...v2.1.1)
> 14 October 2023
- hotfix(downloader): can't get an album title (fix #1313) [`#1313`](https://github.com/th-ch/youtube-music/issues/1313)
- Update changelog for v2.1.0 [`92cab89`](https://github.com/th-ch/youtube-music/commit/92cab89d17175741e60e65ea61633e23ebdc1f45)
- Bump version to 2.1.1 [`3bb5bc2`](https://github.com/th-ch/youtube-music/commit/3bb5bc2ca1856f4e222ee1e01e865f1ab804fdba)
- Add "about" menu to show app version [`21c45fa`](https://github.com/th-ch/youtube-music/commit/21c45faf2043cf72a7c14d5cf6c8d848d0448528)
#### [v2.1.0](https://github.com/th-ch/youtube-music/compare/v2.0.4...v2.1.0)
> 14 October 2023
- 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)

10137
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
"version": "2.1.0",
"version": "2.2.0",
"description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/index.js",
"license": "MIT",
@ -87,39 +87,58 @@
}
},
"scripts": {
"test": "npm run build && playwright test",
"test:debug": "DEBUG=pw:browser* npm run build && playwright test",
"test": "playwright test",
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
"rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"build": "npm run rollup:preload && npm run rollup:main",
"start": "npm run build && electron ./dist/index.js",
"start:debug": "ELECTRON_ENABLE_LOGGING=1 npm run start",
"build": "yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main",
"start": "yarpm-pnpm run build && electron ./dist/index.js",
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start",
"postinstall": "patch-package",
"clean": "del-cli dist && del-cli pack",
"dist": "npm run clean && npm run build && electron-builder --win --mac --linux -p never",
"dist:linux": "npm run clean && npm run build && electron-builder --linux -p never",
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
"dist:win": "npm run clean && npm run build && electron-builder --win -p never",
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis-web:x64 -p never",
"dist": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win --mac --linux -p never",
"dist:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p never",
"dist:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:x64 -p never",
"dist:mac:arm64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:arm64 -p never",
"dist:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p never",
"dist:win:x64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win nsis-web:x64 -p never",
"lint": "eslint .",
"changelog": "auto-changelog",
"release:linux": "npm run clean && npm run build && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "npm run clean && npm run build && electron-builder --mac -p always",
"release:win": "npm run clean && npm run build && electron-builder --win -p always",
"release:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac -p always",
"release:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p always",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"engines": {
"node": ">=16.0.0"
},
"pnpm": {
"overrides": {
"rollup": "4.1.4",
"node-gyp": "9.4.0",
"xml2js": "0.6.2",
"node-fetch": "2.7.0",
"@electron/universal": "1.4.2",
"@babel/runtime": "7.23.2"
}
},
"overrides": {
"rollup": "4.1.4",
"node-gyp": "9.4.0",
"xml2js": "0.6.2",
"node-fetch": "2.7.0",
"@electron/universal": "1.4.2",
"@babel/runtime": "7.23.2"
},
"dependencies": {
"@cliqz/adblocker-electron": "1.26.8",
"@cliqz/adblocker-electron-preload": "1.26.8",
"@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0",
"@foobar404/wave": "2.0.4",
"@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4",
"@xhayper/discord-rpc": "1.0.23",
"@xhayper/discord-rpc": "1.0.24",
"async-mutex": "0.4.0",
"butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4",
@ -141,20 +160,12 @@
"simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
"vudio": "2.1.1",
"x11": "2.3.0",
"youtubei.js": "6.4.1",
"ytpl": "2.3.0"
},
"overrides": {
"rollup": "4.0.2",
"node-gyp": "9.4.0",
"xml2js": "0.6.2",
"node-fetch": "2.7.0",
"@electron/universal": "1.4.2",
"@babel/runtime": "7.23.2"
"youtubei.js": "6.4.1"
},
"devDependencies": {
"@milahu/patch-package": "6.4.14",
"@playwright/test": "1.39.0",
"@rollup/plugin-commonjs": "25.0.5",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.0.1",
"@rollup/plugin-node-resolve": "15.2.3",
@ -162,31 +173,34 @@
"@rollup/plugin-typescript": "11.1.5",
"@rollup/plugin-wasm": "6.2.2",
"@total-typescript/ts-reset": "0.5.1",
"@types/electron-localshortcut": "3.1.1",
"@types/howler": "2.2.9",
"@types/html-to-text": "9.0.2",
"@typescript-eslint/eslint-plugin": "6.7.5",
"@types/electron-localshortcut": "3.1.2",
"@types/howler": "2.2.10",
"@types/html-to-text": "9.0.3",
"@typescript-eslint/eslint-plugin": "6.9.0",
"auto-changelog": "2.4.0",
"builtin-modules": "^3.3.0",
"cross-env": "7.0.3",
"del-cli": "5.1.0",
"electron": "27.0.0",
"electron": "27.0.2",
"electron-builder": "24.6.4",
"electron-devtools-installer": "3.2.0",
"eslint": "8.51.0",
"eslint-plugin-import": "2.28.1",
"eslint": "8.52.0",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-prettier": "5.0.1",
"node-gyp": "9.4.0",
"patch-package": "8.0.0",
"playwright": "1.39.0",
"rollup": "4.0.2",
"rollup": "4.1.4",
"rollup-plugin-copy": "3.5.0",
"rollup-plugin-import-css": "3.3.5",
"rollup-plugin-string": "3.0.0",
"typescript": "5.2.2"
"typescript": "5.2.2",
"yarpm": "1.2.0"
},
"auto-changelog": {
"hideCredit": true,
"package": true,
"unreleased": true,
"output": "changelog.md"
}
},
"packageManager": "pnpm@8.9.2"
}

View File

@ -1,10 +0,0 @@
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
injectCSS(win.webContents, style);
};

View File

@ -1,7 +0,0 @@
#song-video canvas.html5-blur-canvas{
position: absolute;
left: 0;
top: 0;
filter: blur(100px);
}

5637
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
<div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest">
<img src="web/youtube-music.svg" width="400" height="100">
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
</a>
</div>
@ -79,6 +79,10 @@ winget install th-ch.YouTubeMusic
- **Ad Blocker**: Block all ads and tracking out of the box
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
- **Ambient Mode**: Applies a lighting effect by casting gentle colors from the video, into your screens background.
- **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
volume of the softest parts)
@ -111,6 +115,8 @@ winget install th-ch.YouTubeMusic
- [**Last.fm**](https://www.last.fm/): Scrobbles support
- **Lumia Stream**: Adds [Lumia Stream](https://lumiastream.com/) support
- **Lyrics Genius**: Adds lyrics support for most songs
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
@ -178,8 +184,8 @@ Some predefined themes are available in https://github.com/kerichdev/themes-for-
```bash
git clone https://github.com/th-ch/youtube-music
cd youtube-music
npm ci
npm run start
pnpm install --frozen-lockfile
pnpm start
```
## Build your own plugins
@ -266,12 +272,13 @@ export default () => {
## Build
1. Clone the repo
2. Run `npm i` to install dependencies
3. Run `npm run build:OS`
2. Follow [this guide](https://pnpm.io/installation) to install `pnpm`
3. Run `pnpm install --frozen-lockfile` to install dependencies
4. Run `pnpm build:OS`
- `npm run dist:win` - Windows
- `npm run dist:linux` - Linux
- `npm run dist:mac` - MacOS
- `pnpm dist:win` - Windows
- `pnpm dist:linux` - Linux
- `pnpm dist:mac` - MacOS
Builds the app for macOS, Linux, and Windows,
using [electron-builder](https://github.com/electron-userland/electron-builder).
@ -279,7 +286,7 @@ using [electron-builder](https://github.com/electron-userland/electron-builder).
## Tests
```bash
npm run test
pnpm test
```
Uses [Playwright](https://playwright.dev/) to test the app.

View File

@ -18,7 +18,7 @@ export default defineConfig({
nodeResolvePlugin({
browser: false,
preferBuiltins: true,
exportConditions: ['node', 'default', 'module', 'import'] ,
exportConditions: ['node', 'default', 'module', 'import'],
}),
commonjs({
ignoreDynamicRequires: true,
@ -34,7 +34,7 @@ export default defineConfig({
css(),
copy({
targets: [
{ src: 'error.html', dest: 'dist/' },
{ src: 'src/error.html', dest: 'dist/' },
{ src: 'assets', dest: 'dist/' },
],
}),
@ -47,18 +47,14 @@ export default defineConfig({
setTimeout(() => process.exit(0));
}
},
name: 'force-close'
name: 'force-close',
},
],
input: './index.ts',
input: './src/index.ts',
output: {
format: 'cjs',
name: '[name].js',
dir: './dist',
},
external: [
'electron',
'custom-electron-prompt',
...builtinModules,
],
external: ['electron', 'custom-electron-prompt', ...builtinModules],
});

View File

@ -41,18 +41,14 @@ export default defineConfig({
setTimeout(() => process.exit(0));
}
},
name: 'force-close'
name: 'force-close',
},
],
input: './preload.ts',
input: './src/preload.ts',
output: {
format: 'cjs',
name: '[name].js',
dir: './dist',
},
external: [
'electron',
'custom-electron-prompt',
...builtinModules,
],
external: ['electron', 'custom-electron-prompt', ...builtinModules],
});

View File

@ -76,12 +76,21 @@ const defaultConfig = {
'adblocker': {
enabled: true,
cache: true,
blocker: blockers.WithBlocklists as string,
blocker: blockers.InPlayer as string,
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
disableDefaultLists: false,
},
'album-color-theme': {},
'ambient-mode': {},
'ambient-mode': {
enabled: false,
quality: 50,
buffer: 30,
interpolationTime: 1500,
blur: 100,
size: 100,
opacity: 1,
fullscreen: false,
},
'audio-compressor': {},
'blur-nav-bar': {},
'bypass-age-restrictions': {},
@ -107,7 +116,7 @@ const defaultConfig = {
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
listenAlong: true, // Add a "listen along" button to rich presence
playOnYouTubeMusic: true, // Add a "Play on YouTube Music" button to rich presence
hideGitHubButton: false, // Disable the "View App On GitHub" button
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
},
@ -125,6 +134,7 @@ const defaultConfig = {
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
*/
enabled: false,
hideDOMWindowControls: false,
},
'last-fm': {
enabled: false,

View File

@ -22,8 +22,20 @@ export function isEnabled(plugin: string) {
return pluginConfig !== undefined && pluginConfig.enabled;
}
export function setOptions<T>(plugin: string, options: T) {
/**
* Set options for a plugin
* @param plugin Plugin name
* @param options Options to set
* @param exclude Options to exclude from the options object
*/
export function setOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) {
const plugins = store.get('plugins') as Record<string, T>;
// HACK: This is a workaround for preventing changed options from being overwritten
exclude.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(options, key)) {
delete options[key as keyof T];
}
});
store.set('plugins', {
...plugins,
[plugin]: {
@ -33,8 +45,8 @@ export function setOptions<T>(plugin: string, options: T) {
});
}
export function setMenuOptions<T>(plugin: string, options: T) {
setOptions(plugin, options);
export function setMenuOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) {
setOptions(plugin, options, exclude);
if (store.get('options.restartOnConfigChanges')) {
restart();
}
@ -45,11 +57,11 @@ export function getOptions<T>(plugin: string): T {
}
export function enable(plugin: string) {
setMenuOptions(plugin, { enabled: true });
setMenuOptions(plugin, { enabled: true }, []);
}
export function disable(plugin: string) {
setMenuOptions(plugin, { enabled: false });
setMenuOptions(plugin, { enabled: false }, []);
}
export default {

View File

@ -20,6 +20,13 @@ const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: k
};
const migrations = {
'>=2.1.3'(store: Conf<Record<string, unknown>>) {
const listenAlong = store.get('plugins.discord.listenAlong');
if (listenAlong !== undefined) {
store.set('plugins.discord.playOnYouTubeMusic', listenAlong);
store.delete('plugins.discord.listenAlong');
}
},
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
if (originalPreset) {

View File

@ -8,11 +8,13 @@ import { startingPages } from './providers/extracted-data';
import promptOptions from './providers/prompt-options';
import adblockerMenu from './plugins/adblocker/menu';
import ambientModeMenu from './plugins/ambient-mode/menu';
import captionsSelectorMenu from './plugins/captions-selector/menu';
import crossfadeMenu from './plugins/crossfade/menu';
import disableAutoplayMenu from './plugins/disable-autoplay/menu';
import discordMenu from './plugins/discord/menu';
import downloaderMenu from './plugins/downloader/menu';
import inAppMenuTitlebarMenu from './plugins/in-app-menu/menu';
import lyricsGeniusMenu from './plugins/lyrics-genius/menu';
import notificationsMenu from './plugins/notifications/menu';
import pictureInPictureMenu from './plugins/picture-in-picture/menu';
@ -31,11 +33,13 @@ const betaPlugins = ['crossfade', 'lumiastream'];
const pluginMenus = {
'adblocker': adblockerMenu,
'ambient-mode': ambientModeMenu,
'disable-autoplay': disableAutoplayMenu,
'captions-selector': captionsSelectorMenu,
'crossfade': crossfadeMenu,
'discord': discordMenu,
'downloader': downloaderMenu,
'in-app-menu': inAppMenuTitlebarMenu,
'lyrics-genius': lyricsGeniusMenu,
'notifications': notificationsMenu,
'picture-in-picture': pictureInPictureMenu,
@ -420,6 +424,12 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
{ role: 'quit' },
],
},
{
label: 'About',
submenu: [
{ role: 'about' },
],
}
];
};
export const setApplicationMenu = (win: Electron.BrowserWindow) => {

View File

@ -24,12 +24,7 @@ export const loadAdBlockerEngine = async (
disableDefaultLists: boolean | unknown[] = false,
) => {
// Only use cache if no additional blocklists are passed
let cacheDirectory: string;
if (app.isPackaged) {
cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
} else {
cacheDirectory = path.resolve(__dirname, 'adblock_cache');
}
const cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
if (!fs.existsSync(cacheDirectory)) {
fs.mkdirSync(cacheDirectory);
}

View File

@ -0,0 +1,14 @@
import { BrowserWindow } from 'electron';
import config from './config';
import style from './style.css';
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
config.subscribeAll((newConfig) => {
win.webContents.send('ambient-mode:config-change', newConfig);
});
injectCSS(win.webContents, style);
};

View File

@ -0,0 +1,4 @@
import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('ambient-mode');
export default config;

View File

@ -1,9 +1,15 @@
import { ipcRenderer } from 'electron';
import { ConfigType } from '../../config/dynamic';
export default (_: ConfigType<'ambient-mode'>) => {
const interpolationTime = 3000; // interpolation time (ms)
const framerate = 30; // frame
const qualityRatio = 50; // width size (pixel)
export default (config: ConfigType<'ambient-mode'>) => {
let interpolationTime = config.interpolationTime; // interpolation time (ms)
let buffer = config.buffer; // frame
let qualityRatio = config.quality; // width size (pixel)
let sizeRatio = config.size / 100; // size ratio (percent)
let blur = config.blur; // blur (pixel)
let opacity = config.opacity; // opacity (percent)
let isFullscreen = config.fullscreen; // fullscreen (boolean)
let unregister: (() => void) | null = null;
@ -37,7 +43,7 @@ export default (_: ConfigType<'ambient-mode'>) => {
context.globalAlpha = 1;
if (lastImageData) {
const frameOffset = (1 / framerate) * (1000 / interpolationTime);
const frameOffset = (1 / buffer) * (1000 / interpolationTime);
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
context.putImageData(lastImageData, 0, 0);
context.globalAlpha = frameOffset;
@ -61,8 +67,18 @@ export default (_: ConfigType<'ambient-mode'>) => {
blurCanvas.width = qualityRatio;
blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio);
blurCanvas.style.width = `${newWidth}px`;
blurCanvas.style.height = `${newHeight}px`;
blurCanvas.style.width = `${newWidth * sizeRatio}px`;
blurCanvas.style.height = `${newHeight * sizeRatio}px`;
if (isFullscreen) blurCanvas.classList.add('fullscreen');
else blurCanvas.classList.remove('fullscreen');
const leftOffset = newWidth * (sizeRatio - 1) / 2;
const topOffset = newHeight * (sizeRatio - 1) / 2;
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
blurCanvas.style.setProperty('--blur', `${blur}px`);
blurCanvas.style.setProperty('--opacity', `${opacity}`);
};
const observer = new MutationObserver((mutations) => {
@ -75,10 +91,22 @@ export default (_: ConfigType<'ambient-mode'>) => {
const resizeObserver = new ResizeObserver(() => {
applyVideoAttributes();
});
const onConfigSync = (_: Electron.IpcRendererEvent, newConfig: ConfigType<'ambient-mode'>) => {
if (typeof newConfig.interpolationTime === 'number') interpolationTime = newConfig.interpolationTime;
if (typeof newConfig.buffer === 'number') buffer = newConfig.buffer;
if (typeof newConfig.quality === 'number') qualityRatio = newConfig.quality;
if (typeof newConfig.size === 'number') sizeRatio = newConfig.size / 100;
if (typeof newConfig.blur === 'number') blur = newConfig.blur;
if (typeof newConfig.opacity === 'number') opacity = newConfig.opacity;
if (typeof newConfig.fullscreen === 'boolean') isFullscreen = newConfig.fullscreen;
applyVideoAttributes();
};
ipcRenderer.on('ambient-mode:config-change', onConfigSync);
/* hooking */
let canvasInterval: NodeJS.Timeout | null = null;
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate)));
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
applyVideoAttributes();
observer.observe(songVideo, { attributes: true });
resizeObserver.observe(songVideo);
@ -90,7 +118,7 @@ export default (_: ConfigType<'ambient-mode'>) => {
};
const onPlay = () => {
if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate)));
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
};
songVideo.addEventListener('pause', onPause);
songVideo.addEventListener('play', onPlay);
@ -107,6 +135,7 @@ export default (_: ConfigType<'ambient-mode'>) => {
observer.disconnect();
resizeObserver.disconnect();
ipcRenderer.off('ambient-mode:config-change', onConfigSync);
window.removeEventListener('resize', applyVideoAttributes);
wrapper.removeChild(blurCanvas);

View File

@ -0,0 +1,87 @@
import config from './config';
import { MenuTemplate } from '../../menu';
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
const sizeList = [100, 110, 125, 150, 175, 200, 300];
const bufferList = [1, 5, 10, 20, 30];
const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500];
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
export default (): MenuTemplate => [
{
label: 'Smoothness transition',
submenu: interpolationTimeList.map((interpolationTime) => ({
label: `During ${interpolationTime / 1000}s`,
type: 'radio',
checked: config.get('interpolationTime') === interpolationTime,
click() {
config.set('interpolationTime', interpolationTime);
},
})),
},
{
label: 'Quality',
submenu: qualityList.map((quality) => ({
label: `${quality} pixels`,
type: 'radio',
checked: config.get('quality') === quality,
click() {
config.set('quality', quality);
},
})),
},
{
label: 'Size',
submenu: sizeList.map((size) => ({
label: `${size}%`,
type: 'radio',
checked: config.get('size') === size,
click() {
config.set('size', size);
},
})),
},
{
label: 'Buffer',
submenu: bufferList.map((buffer) => ({
label: `${buffer}`,
type: 'radio',
checked: config.get('buffer') === buffer,
click() {
config.set('buffer', buffer);
},
})),
},
{
label: 'Opacity',
submenu: opacityList.map((opacity) => ({
label: `${opacity * 100}%`,
type: 'radio',
checked: config.get('opacity') === opacity,
click() {
config.set('opacity', opacity);
},
})),
},
{
label: 'Blur amount',
submenu: blurAmountList.map((blur) => ({
label: `${blur} pixels`,
type: 'radio',
checked: config.get('blur') === blur,
click() {
config.set('blur', blur);
},
})),
},
{
label: 'Using fullscreen',
type: 'checkbox',
checked: config.get('fullscreen'),
click(item) {
config.set('fullscreen', item.checked);
},
},
];

View File

@ -0,0 +1,26 @@
#song-video canvas.html5-blur-canvas {
filter: blur(var(--blur, 100px));
opacity: var(--opacity, 1);
pointer-events: none;
}
#song-video canvas.html5-blur-canvas:not(.fullscreen) {
position: absolute;
left: var(--left, 0px);
top: var(--top, 0px);
}
#song-video canvas.html5-blur-canvas.fullscreen {
position: fixed;
width: 100% !important;
height: 100% !important;
left: 0 !important;
top: 0 !important;
}
#song-video .html5-video-container > video {
top: 0 !important;
}

View File

@ -156,13 +156,21 @@ export default (
// Song information changed, so lets update the rich presence
// @see https://discord.com/developers/docs/topics/gateway#activity-object
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
if (songInfo.title.length < 2) {
songInfo.title += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
}
if (songInfo.artist.length < 2) {
songInfo.artist += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
}
const activityInfo: SetActivity = {
details: songInfo.title,
state: songInfo.artist,
largeImageKey: songInfo.imageSrc ?? '',
largeImageText: songInfo.album ?? '',
buttons: [
...(options.listenAlong ? [{ label: 'Listen Along', url: songInfo.url ?? '' }] : []),
...(options.playOnYouTubeMusic ? [{ label: 'Play on YouTube Music', url: songInfo.url ?? '' }] : []),
...(options.hideGitHubButton ? [] : [{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' }]),
],
};

View File

@ -47,11 +47,11 @@ export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMen
},
},
{
label: 'Listen Along',
label: 'Play on YouTube Music',
type: 'checkbox',
checked: options.listenAlong,
checked: options.playOnYouTubeMusic,
click(item: Electron.MenuItem) {
options.listenAlong = item.checked;
options.playOnYouTubeMusic = item.checked;
setMenuOptions('discord', options);
},
},

View File

@ -1,16 +1,32 @@
import { createWriteStream, existsSync, mkdirSync, writeFileSync, } from 'node:fs';
import {
createWriteStream,
existsSync,
mkdirSync,
writeFileSync,
} from 'node:fs';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
import { ClientType, Innertube, UniversalCache, Utils, YTNodes } from 'youtubei.js';
import {
ClientType,
Innertube,
UniversalCache,
Utils,
YTNodes,
} from 'youtubei.js';
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, 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';
@ -34,10 +50,8 @@ type CustomSongInfo = SongInfo & { trackId?: string };
const ffmpeg = createFFmpeg({
log: false,
logger() {
}, // Console.log,
progress() {
}, // Console.log,
logger() {}, // Console.log,
progress() {}, // Console.log,
});
const ffmpegMutex = new Mutex();
@ -65,9 +79,13 @@ const sendError = (error: Error, source?: string) => {
};
export const getCookieFromWindow = async (win: BrowserWindow) => {
return (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
it.name + '=' + it.value + ';'
).join('');
return (
await win.webContents.session.cookies.get({
url: 'https://music.youtube.com',
})
)
.map((it) => it.name + '=' + it.value)
.join(';');
};
export default async (win_: BrowserWindow) => {
@ -78,12 +96,13 @@ export default async (win_: BrowserWindow) => {
cache: new UniversalCache(false),
cookie: await getCookieFromWindow(win),
generate_session_locally: true,
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
const url =
typeof input === 'string' ?
new URL(input) :
input instanceof URL ?
input : new URL(input.url);
typeof input === 'string'
? new URL(input)
: input instanceof URL
? input
: new URL(input.url);
if (init?.body && !init.method) {
init.method = 'POST';
@ -95,7 +114,7 @@ export default async (win_: BrowserWindow) => {
);
return net.fetch(request, init);
}
}) as typeof fetch,
});
ipcMain.on('download-song', (_, url: string) => downloadSong(url));
ipcMain.on('video-src-changed', (_, data: GetPlayerResponse) => {
@ -110,15 +129,14 @@ export async function downloadSong(
url: string,
playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined,
increasePlaylistProgress: (value: number) => void = () => {
},
increasePlaylistProgress: (value: number) => void = () => {},
) {
let resolvedName;
try {
await downloadSongUnsafe(
false,
url,
(name: string) => resolvedName = name,
(name: string) => (resolvedName = name),
playlistFolder,
trackId,
increasePlaylistProgress,
@ -132,15 +150,14 @@ export async function downloadSongFromId(
id: string,
playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined,
increasePlaylistProgress: (value: number) => void = () => {
},
increasePlaylistProgress: (value: number) => void = () => {},
) {
let resolvedName;
try {
await downloadSongUnsafe(
true,
id,
(name: string) => resolvedName = name,
(name: string) => (resolvedName = name),
playlistFolder,
trackId,
increasePlaylistProgress,
@ -190,8 +207,8 @@ async function downloadSongUnsafe(
metadata.trackId = trackId;
const dir
= playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
const dir =
playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${
metadata.title
}`;
@ -214,7 +231,8 @@ async function downloadSongUnsafe(
}
if (playabilityStatus.status === 'UNPLAYABLE') {
const errorScreen = playabilityStatus.error_screen as PlayerErrorMessage | null;
const errorScreen =
playabilityStatus.error_screen as PlayerErrorMessage | null;
throw new Error(
`[${playabilityStatus.status}] ${errorScreen?.reason.text}: ${errorScreen?.subreason.text}`,
);
@ -223,7 +241,8 @@ async function downloadSongUnsafe(
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
let presetSetting: Preset;
if (selectedPreset === 'Custom') {
presetSetting = config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
presetSetting =
config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
} else if (selectedPreset === 'Source') {
presetSetting = DefaultPresetList['Source'];
} else {
@ -240,7 +259,9 @@ async function downloadSongUnsafe(
let targetFileExtension: string;
if (!presetSetting?.extension) {
targetFileExtension = YoutubeFormatList.find((it) => it.itag === format.itag)?.container ?? 'mp3';
targetFileExtension =
YoutubeFormatList.find((it) => it.itag === format.itag)?.container ??
'mp3';
} else {
targetFileExtension = presetSetting?.extension ?? 'mp3';
}
@ -285,7 +306,11 @@ async function downloadSongUnsafe(
if (targetFileExtension !== 'mp3') {
createWriteStream(filePath).write(fileBuffer);
} else {
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
const buffer = await writeID3(
Buffer.from(fileBuffer),
metadata,
sendFeedback,
);
if (buffer) {
writeFileSync(filePath, buffer);
}
@ -303,8 +328,7 @@ async function iterableStreamToTargetFile(
presetFfmpegArgs: string[],
contentLength: number,
sendFeedback: (str: string, value?: number) => void,
increasePlaylistProgress: (value: number) => void = () => {
},
increasePlaylistProgress: (value: number) => void = () => {},
) {
const chunks = [];
let downloaded = 0;
@ -372,7 +396,11 @@ const getCoverBuffer = cache(async (url: string) => {
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
});
async function writeID3(buffer: Buffer, metadata: CustomSongInfo, sendFeedback: (str: string, value?: number) => void) {
async function writeID3(
buffer: Buffer,
metadata: CustomSongInfo,
sendFeedback: (str: string, value?: number) => void,
) {
try {
sendFeedback('Writing ID3 tags...');
const tags: NodeID3.Tags = {};
@ -422,13 +450,12 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
try {
givenUrl = new URL(givenUrl ?? '');
} catch {
return;
givenUrl = new URL(win.webContents.getURL());
}
const playlistId
= getPlaylistID(givenUrl)
|| getPlaylistID(new URL(win.webContents.getURL()))
|| getPlaylistID(new URL(playingUrl));
const playlistId =
getPlaylistID(givenUrl) ||
getPlaylistID(new URL(playingUrl));
if (!playlistId) {
sendError(new Error('No playlist ID found'));
@ -440,11 +467,19 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
console.log(`trying to get playlist ID: '${playlistId}'`);
sendFeedback('Getting playlist info…');
let playlist: Playlist;
const items: YTNodes.MusicResponsiveListItem[] = [];
try {
playlist = await yt.music.getPlaylist(playlistId);
if (playlist?.items) {
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
}
} catch (error: unknown) {
sendError(
Error(`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(error)}`),
Error(
`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(
error,
)}`,
),
);
return;
}
@ -453,19 +488,29 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
sendError(new Error('Playlist is empty'));
}
const items = playlist.items!.as(YTNodes.MusicResponsiveListItem);
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;
while (playlist.has_continuation) {
playlist = await playlist.getContinuation();
if (playlist?.items) {
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
}
}
if (items.length === 1) {
sendFeedback('Playlist has only one item, downloading it directly');
await downloadSongFromId(items.at(0)!.id!);
return;
}
let playlistTitle = playlist.header?.title?.text ?? '';
const isAlbum = playlistTitle?.startsWith('Album - ');
if (isAlbum) {
playlistTitle = playlistTitle.slice(8);
}
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
if (!is.macOS()) {
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
@ -521,7 +566,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
increaseProgress,
).catch((error) =>
sendError(
new Error(`Error downloading "${song.author!.name} - ${song.title!}":\n ${error}`)
new Error(
`Error downloading "${
song.author!.name
} - ${song.title!}":\n ${error}`,
),
),
);
@ -554,9 +603,9 @@ function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
// Playlist radio modifier needs to be cut from playlist ID
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
const getPlaylistID = (aURL: URL) => {
const result
= aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
const getPlaylistID = (aURL?: URL): string | null | undefined => {
const result =
aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
return result.slice(INVALID_PLAYLIST_MODIFIER.length);
}
@ -565,16 +614,18 @@ const getPlaylistID = (aURL: URL) => {
};
const getVideoId = (url: URL | string): string | null => {
return (new URL(url)).searchParams.get('v');
return new URL(url).searchParams.get('v');
};
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,
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
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!,
});

View File

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B

View File

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

View File

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 546 B

View File

@ -19,6 +19,7 @@ const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
export default async () => {
const hideDOMWindowControls = config.get('plugins.in-app-menu.hideDOMWindowControls');
let hideMenu = config.get('options.hideMenu');
const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
@ -98,7 +99,7 @@ export default async () => {
titleBar.appendChild(windowControlsContainer);
};
if (isNotWindowsOrMacOS) await addWindowControls();
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
if (navBar) {
const observer = new MutationObserver((mutations) => {
@ -130,7 +131,7 @@ export default async () => {
menu.style.visibility = 'hidden';
}
});
if (isNotWindowsOrMacOS) await addWindowControls();
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
};
await updateMenu();
@ -138,12 +139,16 @@ export default async () => {
ipcRenderer.on('refreshMenu', () => updateMenu());
ipcRenderer.on('window-maximize', () => {
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(unmaximize);
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
});
ipcRenderer.on('window-unmaximize', () => {
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(maximize);
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
});
if (isEnabled('picture-in-picture')) {

View File

@ -0,0 +1,22 @@
import { BrowserWindow } from 'electron';
import is from 'electron-is';
import { setMenuOptions } from '../../config/plugins';
import type { MenuTemplate } from '../../menu';
import type { ConfigType } from '../../config/dynamic';
export default (_: BrowserWindow, config: ConfigType<'in-app-menu'>): MenuTemplate => [
...(is.linux() ? [
{
label: 'Hide DOM Window Controls',
type: 'checkbox',
checked: config.hideDOMWindowControls,
click(item) {
config.hideDOMWindowControls = item.checked;
setMenuOptions('in-app-menu', config);
}
}
] : []) satisfies Electron.MenuItemConstructorOptions[],
];

Some files were not shown because too many files have changed in this diff Show More