Compare commits

...

88 Commits

Author SHA1 Message Date
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
fa160b2e90 Bump version to 2.1.0 2023-10-14 04:09:52 +09:00
308ac38e6b feat(downloader): Added support for audio format auto-detection (#1310) 2023-10-14 03:42:10 +09:00
a62cafb601 feat(in-app-menu): enable in-app-menu by default (in Windows) (#1311) 2023-10-14 03:07:06 +09:00
bf9e3b5f48 hotfix(downloader): fix invalid query selector (fix #1308) 2023-10-13 22:06:27 +09:00
3c6b3aeff0 chore(deps): bump dependencies 2023-10-12 13:39:14 +09:00
37181a7b5e chore(actions): create winget-cla.yml 2023-10-12 12:51:17 +09:00
0b363d6487 fix: winget publish (#1307)
* chore(actions): Update build.yml

* fix: installer regex
2023-10-12 08:01:10 +09:00
e9398adac3 Update changelog for v2.0.4 2023-10-11 16:02:44 +00:00
6901713036 Bump version to 2.0.4 2023-10-12 00:46:04 +09:00
1d5b2997bd fix(downloader): private playlist download 2023-10-12 00:41:58 +09:00
572a023aaa fix: fixed an issue with the initial launch in certain regions, such as South Korea 2023-10-11 23:09:05 +09:00
9187f1e240 Revert "fix: set default adblocker as InPlayer"
This reverts commit 85228fd7d2.
2023-10-11 22:47:56 +09:00
df13d7d0f3 Merge pull request #1304 from th-ch/fix/deps 2023-10-11 22:37:16 +09:00
85228fd7d2 fix: set default adblocker as InPlayer
Fixed an issue with the initial launch in certain regions, such as South Korea.
2023-10-11 22:12:54 +09:00
17ba071057 fix: crash before window loaded 2023-10-11 21:59:03 +09:00
d7df4d7d10 fix: fix It Just Works
Fixed an issue that caused inconsistent execution results.
2023-10-11 19:28:01 +09:00
7aa970cebc fix: bump dependencies 2023-10-11 18:24:11 +09:00
f08f003cf4 Merge pull request #1301 from th-ch/fix/1300
hotfix(adblocker): fix `ipcRenderer.sendSync() with ...`
2023-10-11 08:53:22 +09:00
9f99eded9e chore(readme): update build instruction 2023-10-11 08:48:36 +09:00
c512f13009 hotfix(adblocker): fix ipcRenderer.sendSync() with ...
This issue is caused by the renderer's adblocker being loaded before the main process's adblocker.
2023-10-11 02:01:44 +09:00
b475f780ff Merge pull request #1296 from Lucasamiel0406/master 2023-10-11 00:23:11 +09:00
2294102006 Merge pull request #1297 from nnnlog/master
fix(downloader): Korean filename is broken on non-macOS devices
2023-10-10 16:48:43 +09:00
d69a07d025 fix(downloader): normalize filename depending on OS 2023-10-10 16:05:09 +09:00
4f4995c20c fix: typo in readme.md 2023-10-10 15:54:55 +09:00
b6894dca29 chore(deps): bump deps 2023-10-10 14:10:33 +09:00
73f14e581d Fix Library removed for Premium users
As by now, the code removes the last child of the YT's buttons sidebar. It's good for non-premium users but affects premium users, as it removes the "Library" button.

This small fix targets the 4th child (usually the Upgrade button location) instead of last child.

A bad move/practice, but does its job and remove the Upgrade button while not removing the Library one.
2023-10-09 20:56:08 -03:00
2f2e64af4a Update changelog for v2.0.3 2023-10-09 16:06:41 +00:00
5710307ddc Bump version to 2.0.3 2023-10-10 00:51:44 +09:00
52ba2dc9ff remove: migration scripts 2023-10-10 00:51:16 +09:00
926b9fb5e6 feat: add migration script 2023-10-10 00:42:26 +09:00
a6c9b3381a fix(discord): apply hideGitHubButton 2023-10-10 00:42:10 +09:00
5dc13a4698 feat(discord): add Hide GitHub link Button (#1293) 2023-10-10 00:15:15 +09:00
a69085c591 fix: chore(deps): update dependency @jellybrick/mpris-service to 2.1.4 (fix #971) 2023-10-09 21:55:23 +09:00
a22f7fed21 feat(deps): bundle youtubei.js (temporary solution) (#1292) 2023-10-09 21:30:53 +09:00
8b7045fb1b chore(deps): update dependency @rollup/plugin-node-resolve to v15.2.3 2023-10-09 20:16:40 +09:00
efd1b92514 feat(defaults): change the default value of blocker back to WithBlocklists 2023-10-09 19:52:02 +09:00
969f6d7bba chore(deps): Bump @cliqz/adblocker-electron to 1.26.8 (fix #1269) 2023-10-09 19:50:27 +09:00
4f7c92d6a0 feat(plugins/utils): mediaIcons as const 2023-10-09 19:49:40 +09:00
24d4a50574 fix(mpris): fixed an issue where MPRIS information was incorrect (#1291) 2023-10-09 19:36:17 +09:00
7693a3ba4a fix(discord): fixed an issue where timeChanged was not being applied to Discord activities (#1290) 2023-10-09 19:36:06 +09:00
7ca4dc5c85 Fix: typo in README (#1286) 2023-10-08 23:54:58 +09:00
21ff09b605 Merge pull request #1283 from th-ch/fix/missing-taskbar-mediacontrol-icons 2023-10-08 19:57:50 +09:00
fbf4b3b8b5 fix: missing icons taskbar-mediacontrol 2023-10-08 19:41:39 +09:00
5812eb0147 Update changelog for v2.0.2 2023-10-08 08:51:28 +00:00
174 changed files with 6803 additions and 10208 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@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
# 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: |
@ -117,7 +134,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
@ -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 == '' }}

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
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 }}

5
.prettierrc Normal file
View File

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

View File

@ -2,8 +2,81 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v2.1.2](https://github.com/th-ch/youtube-music/compare/v2.1.1...v2.1.2)
- 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)
- 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)
- fix(discord): fixed an issue where `timeChanged` was not being applied to Discord activities [`#1290`](https://github.com/th-ch/youtube-music/pull/1290)
- Fix: typo in README [`#1286`](https://github.com/th-ch/youtube-music/pull/1286)
- fix: chore(deps): update dependency @jellybrick/mpris-service to 2.1.4 (fix #971) [`#971`](https://github.com/th-ch/youtube-music/issues/971)
- chore(deps): Bump `@cliqz/adblocker-electron` to 1.26.8 (fix #1269) [`#1269`](https://github.com/th-ch/youtube-music/issues/1269)
- fix: missing icons taskbar-mediacontrol [`fbf4b3b`](https://github.com/th-ch/youtube-music/commit/fbf4b3b8b5e39c61975e67efc990c45f62de76d8)
- remove: migration scripts [`52ba2dc`](https://github.com/th-ch/youtube-music/commit/52ba2dc9ffd8e235251d1279686f55e33b3fa3bb)
- feat: add migration script [`926b9fb`](https://github.com/th-ch/youtube-music/commit/926b9fb5e6db69b69935ec5d7be9a76a84e54ceb)
#### [v2.0.2](https://github.com/th-ch/youtube-music/compare/v2.0.1...v2.0.2)
> 8 October 2023
- fix: discord-rpc [`#1278`](https://github.com/th-ch/youtube-music/pull/1278)
- Bump version to 2.0.2 [`b5dbfaf`](https://github.com/th-ch/youtube-music/commit/b5dbfaf68691a546d72f5c1818fd3a44802eb0fa)
- Merge pull request #1272 from th-ch/feat/resolves-1265 [`6b7fd5b`](https://github.com/th-ch/youtube-music/commit/6b7fd5ba630888de08004105179c059c6d93e028)
- Merge pull request #1279 from th-ch/fix/1274 [`73a049a`](https://github.com/th-ch/youtube-music/commit/73a049a7bc5161f0d53c252cf510f1e2a6f6eeb3)
#### [v2.0.1](https://github.com/th-ch/youtube-music/compare/v2.0.0...v2.0.1)
> 8 October 2023
- Update changelog for v2.0.0 [`2d69dfd`](https://github.com/th-ch/youtube-music/commit/2d69dfd333c3223ecc7de13a0abc98fd99aa3a2b)
- hotfix: hotfix for #1267 [`c002263`](https://github.com/th-ch/youtube-music/commit/c002263c3bdd51890b8ffb431283afb60405d8fe)
- Bump version to 2.0.1 [`a1f025e`](https://github.com/th-ch/youtube-music/commit/a1f025e23c599fe5eb63b32ea38ee81200d232d6)

9671
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.0.2",
"version": "2.1.3",
"description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/index.js",
"license": "MIT",
@ -20,24 +20,7 @@
"license",
"!node_modules",
"node_modules/custom-electron-prompt/**",
"node_modules/youtubei.js/**",
"node_modules/undici/**",
"node_modules/@fastify/busboy/**",
"node_modules/jintr/**",
"node_modules/acorn/**",
"node_modules/tslib/**",
"node_modules/semver/**",
"node_modules/lru-cache/**",
"node_modules/detect-libc/**",
"node_modules/color/**",
"node_modules/color-convert/**",
"node_modules/color-string/**",
"node_modules/color-name/**",
"node_modules/simple-swizzle/**",
"node_modules/is-arrayish/**",
"node_modules/@cliqz/adblocker-electron-preload/**",
"node_modules/@cliqz/adblocker-content/**",
"node_modules/@cliqz/adblocker-extended-selectors/**",
"node_modules/@ffmpeg.wasm/core-mt/**",
"!node_modules/**/*.map",
"!node_modules/**/*.ts"
@ -104,46 +87,63 @@
}
},
"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",
"generate:package": "node utils/generate-package-json.js",
"postinstall": "npm run plugins && npm run clean",
"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",
"plugins": "npm run plugin:bypass-age-restrictions",
"plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass",
"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.7",
"@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",
"@xhayper/discord-rpc": "1.0.23",
"@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4",
"@xhayper/discord-rpc": "1.0.24",
"async-mutex": "0.4.0",
"butterchurn": "2.6.7",
"butterchurn-presets": "2.4.7",
"butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4",
"conf": "10.2.0",
"custom-electron-prompt": "1.5.7",
"electron-better-web-request": "1.0.1",
"electron-debug": "3.2.0",
"electron-is": "3.0.0",
"electron-localshortcut": "3.2.1",
@ -156,57 +156,51 @@
"html-to-text": "9.0.5",
"keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2",
"mpris-service": "2.1.2",
"node-id3": "0.2.6",
"simple-youtube-age-restriction-bypass": "git+https://github.com/MiepHD/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.5",
"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",
"dbus-next": "0.10.2",
"node-fetch": "2.7.0",
"@electron/universal": "1.4.2",
"electron": "27.0.0-beta.9"
"youtubei.js": "6.4.1"
},
"devDependencies": {
"@playwright/test": "1.38.1",
"@rollup/plugin-commonjs": "25.0.5",
"@milahu/patch-package": "6.4.14",
"@playwright/test": "1.39.0",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.0.1",
"@rollup/plugin-node-resolve": "15.2.2",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-terser": "0.4.4",
"@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.4",
"@types/electron-localshortcut": "3.1.2",
"@types/howler": "2.2.10",
"@types/html-to-text": "9.0.3",
"@typescript-eslint/eslint-plugin": "6.8.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-beta.9",
"electron": "27.0.2",
"electron-builder": "24.6.4",
"electron-devtools-installer": "3.2.0",
"eslint": "8.51.0",
"eslint": "8.52.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-prettier": "5.0.1",
"node-gyp": "9.4.0",
"playwright": "1.38.1",
"rollup": "4.0.2",
"playwright": "1.39.0",
"rollup": "4.1.4",
"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"
"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

@ -0,0 +1,38 @@
diff --git a/node_modules/youtubei.js/bundle/node.cjs b/node_modules/youtubei.js/bundle/node.cjs
index 7e3072e..bf5be6a 100644
--- a/node_modules/youtubei.js/bundle/node.cjs
+++ b/node_modules/youtubei.js/bundle/node.cjs
@@ -16969,7 +16969,13 @@ var _Cache_createCache;
var meta_url = import_meta.url;
var is_cjs = !meta_url;
var __dirname__ = is_cjs ? __dirname : import_path.default.dirname((0, import_url.fileURLToPath)(meta_url));
-var package_json = JSON.parse((0, import_fs.readFileSync)(import_path.default.resolve(__dirname__, is_cjs ? "../package.json" : "../../package.json"), "utf-8"));
+var package_json = {
+ homepage: "https://github.com/LuanRT/YouTube.js#readme",
+ version: "6.4.1",
+ bugs: {
+ url: "https://github.com/LuanRT/YouTube.js/issues"
+ }
+};
var repo_url = (_a3 = package_json.homepage) === null || _a3 === void 0 ? void 0 : _a3.split("#")[0];
var Cache = class {
constructor(persistent = false, persistent_directory) {
diff --git a/node_modules/youtubei.js/dist/src/platform/node.js b/node_modules/youtubei.js/dist/src/platform/node.js
index 0e1b2ca..17b437c 100644
--- a/node_modules/youtubei.js/dist/src/platform/node.js
+++ b/node_modules/youtubei.js/dist/src/platform/node.js
@@ -16,7 +16,13 @@ import evaluate from './jsruntime/jinter.js';
const meta_url = import.meta.url;
const is_cjs = !meta_url;
const __dirname__ = is_cjs ? __dirname : path.dirname(fileURLToPath(meta_url));
-const package_json = JSON.parse(readFileSync(path.resolve(__dirname__, is_cjs ? '../package.json' : '../../package.json'), 'utf-8'));
+const package_json = {
+ homepage: "https://github.com/LuanRT/YouTube.js#readme",
+ version: "6.4.1",
+ bugs: {
+ url: "https://github.com/LuanRT/YouTube.js/issues"
+ }
+};
const repo_url = (_a = package_json.homepage) === null || _a === void 0 ? void 0 : _a.split('#')[0];
class Cache {
constructor(persistent = false, persistent_directory) {

View File

@ -1,4 +0,0 @@
export default () => {
const path = '@cliqz/adblocker-electron-preload'; // prevent require hoisting
require(path);
};

View File

@ -1,4 +0,0 @@
export default () => {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
require('simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js');
};

5624
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)
@ -104,14 +108,15 @@ winget install th-ch.YouTubeMusic
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
select lower volumes.
- **In-App Menu
**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
- **In-App Menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
> (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem
accessing the menu after enabling this plugin and hide-menu option)
- [**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
@ -179,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
npm run start
pnpm install --frozen-lockfile
pnpm start
```
## Build your own plugins
@ -267,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 build:win` - Windows
- `npm run build:linux` - Linux
- `npm run build: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).
@ -280,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,19 +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',
'youtubei.js', // https://github.com/LuanRT/YouTube.js/pull/509
...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

@ -1,5 +1,7 @@
import { blockers } from '../plugins/adblocker/blocker-types';
import { DefaultPresetList } from '../plugins/downloader/types';
export interface WindowSizeConfig {
width: number;
height: number;
@ -105,19 +107,26 @@ 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
},
'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,
hideDOMWindowControls: false,
},
'last-fm': {
enabled: false,
token: undefined as string | undefined, // Token used for authentication

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

@ -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,33 @@ 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) {
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 +155,7 @@ const migrations = {
};
export default new Store({
defaults,
defaults: getDefaults(),
clearInvalidConfig: false,
migrations,
});

View File

@ -1,16 +1,14 @@
import path from 'node:path';
import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron';
import enhanceWebRequest from 'electron-better-web-request';
import enhanceWebRequest, { BetterSession } from '@jellybrick/electron-better-web-request';
import is from 'electron-is';
import unhandled from 'electron-unhandled';
import { autoUpdater } from 'electron-updater';
import electronDebug from 'electron-debug';
import { BetterWebRequest } from 'electron-better-web-request/lib/electron-better-web-request';
import config from './config';
import { setApplicationMenu } from './menu';
import { refreshMenu, setApplicationMenu } from './menu';
import { fileExists, injectCSS, injectCSSAsFile } from './plugins/utils';
import { isTesting } from './utils/testing';
import { setUpTray } from './tray';
@ -144,7 +142,7 @@ if (is.windows()) {
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
function loadPlugins(win: BrowserWindow) {
async function loadPlugins(win: BrowserWindow) {
injectCSS(win.webContents, youtubeMusicCSS);
// Load user CSS
const themes: string[] = config.get('options.themes');
@ -175,7 +173,7 @@ function loadPlugins(win: BrowserWindow) {
console.log('Loaded plugin - ' + plugin);
const handler = mainPlugins[plugin as keyof typeof mainPlugins];
if (handler) {
handler(win, options as never);
await handler(win, options as never);
}
}
} catch (e) {
@ -184,7 +182,7 @@ function loadPlugins(win: BrowserWindow) {
}
}
function createMainWindow() {
async function createMainWindow() {
const windowSize = config.get('window-size');
const windowMaximized = config.get('window-maximized');
const windowPosition: Electron.Point = config.get('window-position');
@ -223,7 +221,7 @@ function createMainWindow() {
: 'default'),
autoHideMenuBar: config.get('options.hideMenu'),
});
loadPlugins(win);
await loadPlugins(win);
if (windowPosition) {
const { x: windowX, y: windowY } = windowPosition;
@ -258,7 +256,6 @@ function createMainWindow() {
const urlToLoad = config.get('options.resumeOnStart')
? config.get('url')
: config.defaultConfig.url;
win.webContents.loadURL(urlToLoad);
win.on('closed', onClosed);
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
@ -338,6 +335,8 @@ function createMainWindow() {
removeContentSecurityPolicy();
win.webContents.loadURL(urlToLoad);
return win;
}
@ -394,7 +393,7 @@ app.once('browser-window-created', (event, win) => {
console.log(log);
}
if (!(config.plugins.isEnabled('in-app-menu') && errorCode === -3)) { // -3 is a false positive with in-app-menu
if (errorCode !== -3) { // -3 is a false positive
win.webContents.send('log', log);
win.webContents.loadFile(path.join(__dirname, 'error.html'));
}
@ -414,17 +413,17 @@ app.on('window-all-closed', () => {
globalShortcut.unregisterAll();
});
app.on('activate', () => {
app.on('activate', async () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
mainWindow = createMainWindow();
mainWindow = await createMainWindow();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
}
});
app.on('ready', () => {
app.on('ready', async () => {
if (config.get('options.autoResetAppCache')) {
// Clear cache after 20s
const clearCacheTimeout = setTimeout(() => {
@ -469,8 +468,9 @@ app.on('ready', () => {
}
}
mainWindow = createMainWindow();
mainWindow = await createMainWindow();
setApplicationMenu(mainWindow);
refreshMenu(mainWindow);
setUpTray(app, mainWindow);
setupProtocolHandler(mainWindow);
@ -602,8 +602,6 @@ function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProc
});
}
// HACK: electron-better-web-request's typing is wrong
type BetterSession = Omit<Electron.Session, 'webRequest'> & { webRequest: BetterWebRequest & Electron.WebRequest };
function removeContentSecurityPolicy(
betterSession: BetterSession = session.defaultSession as BetterSession,
) {
@ -623,11 +621,10 @@ function removeContentSecurityPolicy(
callback({ cancel: false, responseHeaders: details.responseHeaders });
});
type ResolverListener = { apply: () => Promise<Record<string, unknown>>; context: unknown };
// When multiple listeners are defined, apply them all
betterSession.webRequest.setResolver('onHeadersReceived', async (listeners: ResolverListener[]) => {
return listeners.reduce<Promise<Record<string, unknown>>>(
async (accumulator: Promise<Record<string, unknown>>, listener: ResolverListener) => {
betterSession.webRequest.setResolver('onHeadersReceived', async (listeners) => {
return listeners.reduce(
async (accumulator, listener) => {
const acc = await accumulator;
if (acc.cancel) {
return acc;

View File

@ -13,6 +13,7 @@ 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';
@ -36,6 +37,7 @@ const pluginMenus = {
'crossfade': crossfadeMenu,
'discord': discordMenu,
'downloader': downloaderMenu,
'in-app-menu': inAppMenuTitlebarMenu,
'lyrics-genius': lyricsGeniusMenu,
'notifications': notificationsMenu,
'picture-in-picture': pictureInPictureMenu,
@ -62,13 +64,15 @@ const pluginEnabledMenu = (plugin: string, label = '', hasSubmenu = false, refre
},
});
export const refreshMenu = (win: BrowserWindow) => {
setApplicationMenu(win);
if (inAppMenuActive) {
win.webContents.send('refreshMenu');
}
};
export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
const refreshMenu = () => {
setApplicationMenu(win);
if (inAppMenuActive) {
win.webContents.send('refreshMenu');
}
};
const innerRefreshMenu = () => refreshMenu(win);
return [
{
@ -84,15 +88,15 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
const getPluginMenu = pluginMenus[pluginName as keyof typeof pluginMenus];
if (!config.plugins.isEnabled(pluginName)) {
return pluginEnabledMenu(pluginName, pluginLabel, true, refreshMenu);
return pluginEnabledMenu(pluginName, pluginLabel, true, innerRefreshMenu);
}
return {
label: pluginLabel,
submenu: [
pluginEnabledMenu(pluginName, 'Enabled', true, refreshMenu),
pluginEnabledMenu(pluginName, 'Enabled', true, innerRefreshMenu),
{ type: 'separator' },
...getPluginMenu(win, config.plugins.getOptions(pluginName), refreshMenu),
...getPluginMenu(win, config.plugins.getOptions(pluginName), innerRefreshMenu),
],
} satisfies Electron.MenuItemConstructorOptions;
}
@ -418,6 +422,12 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
{ role: 'quit' },
],
},
{
label: 'About',
submenu: [
{ role: 'about' },
],
}
];
};
export const setApplicationMenu = (win: Electron.BrowserWindow) => {

View File

@ -8,8 +8,8 @@ import type { ConfigType } from '../../config/dynamic';
type AdBlockOptions = ConfigType<'adblocker'>;
export default async (win: BrowserWindow, options: AdBlockOptions) => {
if (await shouldUseBlocklists()) {
loadAdBlockerEngine(
if (shouldUseBlocklists()) {
await loadAdBlockerEngine(
win.webContents.session,
options.cache,
options.additionalBlockLists,

View File

@ -3,7 +3,7 @@ import path from 'node:path';
import fs, { promises } from 'node:fs';
import { ElectronBlocker } from '@cliqz/adblocker-electron';
import { app } from 'electron';
import { app, net } from 'electron';
const SOURCES = [
'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
@ -17,19 +17,14 @@ const SOURCES = [
'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
];
export const loadAdBlockerEngine = (
export const loadAdBlockerEngine = async (
session: Electron.Session | undefined = undefined,
cache = true,
additionalBlockLists = [],
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);
}
@ -49,25 +44,24 @@ export const loadAdBlockerEngine = (
...additionalBlockLists,
];
ElectronBlocker.fromLists(
fetch,
lists,
{
// When generating the engine for caching, do not load network filters
// So that enhancing the session works as expected
// Allowing to define multiple webRequest listeners
loadNetworkFilters: session !== undefined,
},
cachingOptions,
)
.then((blocker) => {
if (session) {
blocker.enableBlockingInSession(session);
} else {
console.log('Successfully generated adBlocker engine.');
}
})
.catch((error) => console.log('Error loading adBlocker engine', error));
try {
const blocker = await ElectronBlocker.fromLists(
(url: string) => net.fetch(url),
lists,
{
// When generating the engine for caching, do not load network filters
// So that enhancing the session works as expected
// Allowing to define multiple webRequest listeners
loadNetworkFilters: session !== undefined,
},
cachingOptions,
);
if (session) {
blocker.enableBlockingInSession(session);
}
} catch (error) {
console.log('Error loading adBlocker engine', error);
}
};
export default { loadAdBlockerEngine };

View File

@ -7,7 +7,7 @@ import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('adblocker', { enableFront: true });
export const shouldUseBlocklists = async () => await config.get('blocker') !== blockers.InPlayer;
export const shouldUseBlocklists = () => config.get('blocker') !== blockers.InPlayer;
export default Object.assign(config, {
shouldUseBlocklists,

View File

@ -0,0 +1,3 @@
export default async () => {
await import('@cliqz/adblocker-electron-preload');
};

View File

@ -1,15 +1,15 @@
import config from './config';
import config, { shouldUseBlocklists } from './config';
import inject from './inject';
import injectCliqzPreload from './inject-cliqz-preload';
import { blockers } from './blocker-types';
export default async () => {
if (await config.shouldUseBlocklists()) {
if (shouldUseBlocklists()) {
// Preload adblocker to inject scripts/styles
injectCliqzPreload();
await injectCliqzPreload();
// eslint-disable-next-line @typescript-eslint/await-thenable
} else if ((await config.get('blocker')) === blockers.InPlayer) {
} else if ((config.get('blocker')) === blockers.InPlayer) {
inject();
}
};

View File

@ -0,0 +1,4 @@
export default async () => {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
await import('simple-youtube-age-restriction-bypass');
};

View File

@ -0,0 +1,4 @@
declare module 'simple-youtube-age-restriction-bypass' {
const nothing: never;
export default nothing;
}

View File

@ -1,4 +1,4 @@
import { app, dialog } from 'electron';
import { app, dialog, ipcMain } from 'electron';
import { Client as DiscordClient } from '@xhayper/discord-rpc';
import { dev } from 'electron-is';
@ -156,14 +156,22 @@ 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 ?? '' }] : []),
{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' },
...(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' }]),
],
};
@ -188,8 +196,22 @@ export default (
// If the page is ready, register the callback
win.once('ready-to-show', () => {
registerCallback(updateActivity);
let lastSongInfo: SongInfo;
registerCallback((songInfo) => {
lastSongInfo = songInfo;
updateActivity(songInfo);
});
connect();
let lastSent = Date.now();
ipcMain.on('timeChanged', (_, t: number) => {
const currentTime = Date.now();
// if lastSent is more than 5 seconds ago, send the new time
if (currentTime - lastSent > 5000) {
lastSent = currentTime;
lastSongInfo.elapsedSeconds = t;
updateActivity(lastSongInfo);
}
});
});
app.on('window-all-closed', clear);
};

View File

@ -47,11 +47,20 @@ 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);
},
},
{
label: 'Hide GitHub link Button',
type: 'checkbox',
checked: options.hideGitHubButton,
click(item: Electron.MenuItem) {
options.hideGitHubButton = item.checked;
setMenuOptions('discord', options);
},
},

View File

@ -1,28 +1,34 @@
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 } from 'youtubei.js';
import {
ClientType,
Innertube,
UniversalCache,
Utils,
YTNodes,
} from 'youtubei.js';
import is from 'electron-is';
import ytpl from 'ytpl';
// REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
import filenamify from 'filenamify';
import { Mutex } from 'async-mutex';
import { createFFmpeg } from '@ffmpeg.wasm/main';
import NodeID3, { TagConstants } from 'node-id3';
import PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
import { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
import TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
import { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
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';
@ -32,17 +38,20 @@ import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
import { injectCSS } from '../utils';
import { cache } from '../../providers/decorators';
import type { GetPlayerResponse } from '../../types/get-player-response';
import type { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
import type PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
import type { Playlist } from 'youtubei.js/dist/src/parser/ytmusic';
import type { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
import type TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
import type { GetPlayerResponse } from '../../types/get-player-response';
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();
@ -69,23 +78,31 @@ 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(';');
};
export default async (win_: BrowserWindow) => {
win = win_;
injectCSS(win.webContents, style);
const cookie = (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
it.name + '=' + it.value + ';'
).join('');
yt = await Innertube.create({
cache: new UniversalCache(false),
cookie,
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';
@ -97,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) => {
@ -112,14 +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,
@ -129,8 +146,30 @@ export async function downloadSong(
}
}
export async function downloadSongFromId(
id: string,
playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined,
increasePlaylistProgress: (value: number) => void = () => {},
) {
let resolvedName;
try {
await downloadSongUnsafe(
true,
id,
(name: string) => (resolvedName = name),
playlistFolder,
trackId,
increasePlaylistProgress,
);
} catch (error: unknown) {
sendError(error as Error, resolvedName || id);
}
}
async function downloadSongUnsafe(
url: string,
isId: boolean,
idOrUrl: string,
setName: (name: string) => void,
playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined,
@ -147,8 +186,13 @@ async function downloadSongUnsafe(
sendFeedback('Downloading...', 2);
const id = getVideoId(url);
if (typeof id !== 'string') throw new Error('Video not found');
let id: string | null;
if (isId) {
id = idOrUrl;
} else {
id = getVideoId(idOrUrl);
if (typeof id !== 'string') throw new Error('Video not found');
}
let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
@ -163,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
}`;
@ -187,27 +231,22 @@ 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}`,
);
}
const preset = config.get('preset') ?? 'mp3';
let presetSetting: { extension: string; ffmpegArgs: string[] } | null = null;
if (preset === 'opus') {
presetSetting = presets[preset];
}
const filename = filenamify(`${name}.${presetSetting?.extension ?? 'mp3'}`, {
replacement: '_',
maxLength: 255,
});
const filePath = join(dir, filename);
if (config.get('skipExisting') && existsSync(filePath)) {
sendFeedback(null, -1);
return;
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)'];
}
const downloadOptions: FormatOptions = {
@ -217,6 +256,30 @@ async function downloadSongUnsafe(
};
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,
});
if (!is.macOS()) {
filename = filename.normalize('NFC');
}
const filePath = join(dir, filename);
if (config.get('skipExisting') && existsSync(filePath)) {
sendFeedback(null, -1);
return;
}
const stream = await info.download(downloadOptions);
console.info(
@ -229,40 +292,25 @@ 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) {
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
if (fileBuffer) {
if (targetFileExtension !== 'mp3') {
createWriteStream(filePath).write(fileBuffer);
} else {
const buffer = await writeID3(
Buffer.from(fileBuffer),
metadata,
sendFeedback,
);
if (buffer) {
writeFileSync(filePath, buffer);
}
@ -273,14 +321,14 @@ 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 = () => {
},
increasePlaylistProgress: (value: number) => void = () => {},
) {
const chunks = [];
let downloaded = 0;
@ -316,13 +364,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);
@ -331,9 +380,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);
@ -347,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 = {};
@ -397,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'));
@ -414,35 +466,56 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
console.log(`trying to get playlist ID: '${playlistId}'`);
sendFeedback('Getting playlist info…');
let playlist: ytpl.Result;
let playlist: Playlist;
const items: YTNodes.MusicResponsiveListItem[] = [];
try {
playlist = await ytpl(playlistId, {
limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY,
});
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;
}
if (playlist.items.length === 0) {
if (!playlist || !playlist.items || playlist.items.length === 0) {
sendError(new Error('Playlist is empty'));
}
if (playlist.items.length === 1) {
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 downloadSong(playlist.items[0].url);
await downloadSongFromId(items.at(0)!.id!);
return;
}
const isAlbum = playlist.title.startsWith('Album - ');
if (isAlbum) {
playlist.title = playlist.title.slice(8);
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
if (!is.macOS()) {
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
}
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
const folder = getFolder(config.get('downloadFolder') ?? '');
const playlistFolder = join(folder, safePlaylistTitle);
if (existsSync(playlistFolder)) {
@ -458,47 +531,51 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
type: 'info',
buttons: ['OK'],
title: 'Started Download',
message: `Downloading Playlist "${playlist.title}"`,
detail: `(${playlist.items.length} songs)`,
message: `Downloading Playlist "${playlistTitle}"`,
detail: `(${items.length} songs)`,
});
if (is.dev()) {
console.log(
`Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`,
`Downloading playlist "${playlistTitle}" - ${items.length} songs (${playlistId})`,
);
}
win.setProgressBar(2); // Starts with indefinite bar
setBadge(playlist.items.length);
setBadge(items.length);
let counter = 1;
const progressStep = 1 / playlist.items.length;
const progressStep = 1 / items.length;
const increaseProgress = (itemPercentage: number) => {
const currentProgress = (counter - 1) / (playlist.items.length ?? 1);
const currentProgress = (counter - 1) / (items.length ?? 1);
const newProgress = currentProgress + (progressStep * itemPercentage);
win.setProgressBar(newProgress);
};
try {
for (const song of playlist.items) {
sendFeedback(`Downloading ${counter}/${playlist.items.length}...`);
for (const song of items) {
sendFeedback(`Downloading ${counter}/${items.length}...`);
const trackId = isAlbum ? counter : undefined;
await downloadSong(
song.url,
await downloadSongFromId(
song.id!,
playlistFolder,
trackId?.toString(),
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}`,
),
),
);
win.setProgressBar(counter / playlist.items.length);
setBadge(playlist.items.length - counter);
win.setProgressBar(counter / items.length);
setBadge(items.length - counter);
counter++;
}
} catch (error: unknown) {
@ -510,29 +587,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 [];
@ -549,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);
}
@ -560,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

@ -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?')) {

View File

@ -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);
},
})),
},

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) => {
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);

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

@ -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'));
};

View File

@ -19,9 +19,11 @@ 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');
let maximizeButton: HTMLButtonElement;
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
logo.classList.add('title-bar-icon');
@ -55,7 +57,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);
@ -97,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) => {
@ -129,14 +131,24 @@ export default async () => {
menu.style.visibility = 'hidden';
}
});
if (isNotWindowsOrMacOS) await addWindowControls();
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
};
await updateMenu();
document.title = 'Youtube Music';
ipcRenderer.on('refreshMenu', () => {
updateMenu();
ipcRenderer.on('refreshMenu', () => updateMenu());
ipcRenderer.on('window-maximize', () => {
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
maximizeButton.removeChild(maximizeButton.firstChild);
maximizeButton.appendChild(unmaximize);
}
});
ipcRenderer.on('window-unmaximize', () => {
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[],
];

View File

@ -2,14 +2,14 @@ import path from 'node:path';
import { app, BrowserWindow, ipcMain, Notification } from 'electron';
import { icons, notificationImage, saveTempIcon, secondsToMinutes, ToastStyles } from './utils';
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
import config from './config';
import getSongControls from '../../providers/song-controls';
import registerCallback, { SongInfo } from '../../providers/song-info';
import { changeProtocolHandler } from '../../providers/protocol-handler';
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
import { getMediaIconLocation } from '../utils';
import { getMediaIconLocation, mediaIcons, saveMediaIcon } from '../utils';
let songControls: ReturnType<typeof getSongControls>;
let savedNotification: Notification | undefined;
@ -23,7 +23,7 @@ export default (win: BrowserWindow) => {
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
if (app.isPackaged) {
saveTempIcon();
saveMediaIcon();
}
let savedSongInfo: SongInfo;
@ -152,9 +152,9 @@ const getXml = (songInfo: SongInfo, iconSrc: string) => {
}
}
};
const display = (kind: keyof typeof icons) => {
const display = (kind: keyof typeof mediaIcons) => {
if (config.get('toastStyle') === ToastStyles.legacy) {
return `content="${icons[kind]}"`;
return `content="${mediaIcons[kind]}"`;
}
return `\
@ -163,7 +163,7 @@ const display = (kind: keyof typeof icons) => {
`;
};
const getButton = (kind: keyof typeof icons) =>
const getButton = (kind: keyof typeof mediaIcons) =>
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
const getButtons = (isPaused: boolean) => `\

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