Compare commits

...

82 Commits

Author SHA1 Message Date
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 6748 additions and 10195 deletions

View File

@ -12,11 +12,13 @@ body:
required: true 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. - 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 required: true
- label: I understand that **th-ch/youtube-music has NO affiliation with Google or YouTube**
required: true
- type: input - type: input
attributes: attributes:
label: YouTube Music (Application) Version label: YouTube Music (Application) Version
description: | 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. Note: Please check if this issue is reproducible with the latest stable release.
placeholder: 2.0.0 placeholder: 2.0.0
@ -36,7 +38,7 @@ body:
- type: input - type: input
attributes: attributes:
label: Operating System Version 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" placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations: validations:
required: true required: true

View File

@ -20,59 +20,63 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Setup NodeJS - 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 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies - 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 - name: Test
uses: GabrielBB/xvfb-action@v1 uses: coactions/setup-xvfb@v1
env: env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
with: 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: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -84,14 +88,27 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Setup NodeJS - 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 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
- name: Get version - name: Get version
run: | run: |
@ -117,7 +134,7 @@ jobs:
if: ${{ env.VERSION_HASH == '' }} if: ${{ env.VERSION_HASH == '' }}
uses: irongut/EditRelease@v1.2.0 uses: irongut/EditRelease@v1.2.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GH_TOKEN }}
id: ${{ steps.get_draft_release.outputs.id }} id: ${{ steps.get_draft_release.outputs.id }}
draft: false draft: false
prerelease: false prerelease: false
@ -132,7 +149,7 @@ jobs:
- name: Update changelog - name: Update changelog
if: ${{ env.VERSION_HASH == '' }} if: ${{ env.VERSION_HASH == '' }}
run: | run: |
npm run changelog pnpm changelog
- name: Commit changelog - name: Commit changelog
if: ${{ env.VERSION_HASH == '' }} 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 uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: th-ch.YouTubeMusic identifier: th-ch.YouTubeMusic
installers-regex: '^YouTube-Music-Setup-[\d\.]+\.exe$' installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'
version: ${{ inputs.tag_name || github.event.release.tag_name }} version: ${{ inputs.tag_name || github.event.release.tag_name }}
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }} release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
token: ${{ secrets.WINGET_ACC_TOKEN }} token: ${{ secrets.WINGET_ACC_TOKEN }}

5
.prettierrc Normal file
View File

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

View File

@ -2,8 +2,63 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v2.1.1](https://github.com/th-ch/youtube-music/compare/v2.1.0...v2.1.1)
- 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) #### [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) - 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) - 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) - 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", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "2.0.2", "version": "2.1.2",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
@ -20,24 +20,7 @@
"license", "license",
"!node_modules", "!node_modules",
"node_modules/custom-electron-prompt/**", "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-electron-preload/**",
"node_modules/@cliqz/adblocker-content/**",
"node_modules/@cliqz/adblocker-extended-selectors/**",
"node_modules/@ffmpeg.wasm/core-mt/**", "node_modules/@ffmpeg.wasm/core-mt/**",
"!node_modules/**/*.map", "!node_modules/**/*.map",
"!node_modules/**/*.ts" "!node_modules/**/*.ts"
@ -104,46 +87,63 @@
} }
}, },
"scripts": { "scripts": {
"test": "npm run build && playwright test", "test": "playwright test",
"test:debug": "DEBUG=pw:browser* npm run build && 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: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", "rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"build": "npm run rollup:preload && npm run rollup:main", "build": "yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main",
"start": "npm run build && electron ./dist/index.js", "start": "yarpm-pnpm run build && electron ./dist/index.js",
"start:debug": "ELECTRON_ENABLE_LOGGING=1 npm run start", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start",
"generate:package": "node utils/generate-package-json.js", "postinstall": "patch-package",
"postinstall": "npm run plugins && npm run clean",
"clean": "del-cli dist && del-cli pack", "clean": "del-cli dist && del-cli pack",
"dist": "npm run clean && npm run build && electron-builder --win --mac --linux -p never", "dist": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win --mac --linux -p never",
"dist:linux": "npm run clean && npm run build && electron-builder --linux -p never", "dist:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p never",
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never", "dist:mac": "yarpm-pnpm run clean && yarpm-pnpm 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:mac:arm64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:arm64 -p never",
"dist:win": "npm run clean && npm run build && electron-builder --win -p never", "dist:win": "yarpm-pnpm run clean && yarpm-pnpm 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:win:x64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win nsis-web:x64 -p never",
"lint": "eslint .", "lint": "eslint .",
"changelog": "auto-changelog", "changelog": "auto-changelog",
"plugins": "npm run plugin:bypass-age-restrictions", "release:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p always -c.snap.publish=github",
"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:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac -p always",
"release:linux": "npm run clean && npm run build && electron-builder --linux -p always -c.snap.publish=github", "release:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p always",
"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",
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "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": { "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/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0", "@ffmpeg.wasm/main": "0.12.0",
"@foobar404/wave": "2.0.4", "@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", "async-mutex": "0.4.0",
"butterchurn": "2.6.7", "butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "2.4.7", "butterchurn-presets": "3.0.0-beta.4",
"conf": "10.2.0", "conf": "10.2.0",
"custom-electron-prompt": "1.5.7", "custom-electron-prompt": "1.5.7",
"electron-better-web-request": "1.0.1",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-is": "3.0.0", "electron-is": "3.0.0",
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
@ -156,57 +156,51 @@
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"mpris-service": "2.1.2",
"node-id3": "0.2.6", "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", "vudio": "2.1.1",
"x11": "2.3.0", "x11": "2.3.0",
"youtubei.js": "6.4.1", "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"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.38.1", "@milahu/patch-package": "6.4.14",
"@rollup/plugin-commonjs": "25.0.5", "@playwright/test": "1.39.0",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-image": "3.0.3", "@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.0.1", "@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-terser": "0.4.4",
"@rollup/plugin-typescript": "11.1.5", "@rollup/plugin-typescript": "11.1.5",
"@rollup/plugin-wasm": "6.2.2", "@rollup/plugin-wasm": "6.2.2",
"@total-typescript/ts-reset": "0.5.1", "@total-typescript/ts-reset": "0.5.1",
"@types/electron-localshortcut": "3.1.1", "@types/electron-localshortcut": "3.1.2",
"@types/howler": "2.2.9", "@types/howler": "2.2.10",
"@types/html-to-text": "9.0.2", "@types/html-to-text": "9.0.3",
"@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/eslint-plugin": "6.8.0",
"auto-changelog": "2.4.0", "auto-changelog": "2.4.0",
"builtin-modules": "^3.3.0",
"cross-env": "7.0.3",
"del-cli": "5.1.0", "del-cli": "5.1.0",
"electron": "27.0.0-beta.9", "electron": "27.0.1",
"electron-builder": "24.6.4", "electron-builder": "24.6.4",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"eslint": "8.51.0", "eslint": "8.51.0",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.28.1",
"eslint-plugin-prettier": "5.0.0", "eslint-plugin-prettier": "5.0.1",
"node-gyp": "9.4.0", "node-gyp": "9.4.0",
"playwright": "1.38.1", "playwright": "1.39.0",
"rollup": "4.0.2", "rollup": "4.1.4",
"rollup-plugin-copy": "3.5.0", "rollup-plugin-copy": "3.5.0",
"rollup-plugin-import-css": "3.3.4", "rollup-plugin-import-css": "3.3.5",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"typescript": "5.2.2" "typescript": "5.2.2",
"yarpm": "1.2.0"
}, },
"auto-changelog": { "auto-changelog": {
"hideCredit": true, "hideCredit": true,
"package": true, "package": true,
"unreleased": true, "unreleased": true,
"output": "changelog.md" "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');
};

5618
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"> <div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest"> <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> </a>
</div> </div>
@ -79,6 +79,10 @@ winget install th-ch.YouTubeMusic
- **Ad Blocker**: Block all ads and tracking out of the box - **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 - **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
volume of the softest parts) 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 slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
select lower volumes. select lower volumes.
- **In-App Menu - **In-App Menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
**: [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 > (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) accessing the menu after enabling this plugin and hide-menu option)
- [**Last.fm**](https://www.last.fm/): Scrobbles support - [**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 - **Lyrics Genius**: Adds lyrics support for most songs
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser - **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 ```bash
git clone https://github.com/th-ch/youtube-music git clone https://github.com/th-ch/youtube-music
cd youtube-music cd youtube-music
npm pnpm install --frozen-lockfile
npm run start pnpm start
``` ```
## Build your own plugins ## Build your own plugins
@ -267,12 +272,13 @@ export default () => {
## Build ## Build
1. Clone the repo 1. Clone the repo
2. Run `npm i` to install dependencies 2. Follow [this guide](https://pnpm.io/installation) to install `pnpm`
3. Run `npm run build:OS` 3. Run `pnpm install --frozen-lockfile` to install dependencies
4. Run `pnpm build:OS`
- `npm run build:win` - Windows - `pnpm dist:win` - Windows
- `npm run build:linux` - Linux - `pnpm dist:linux` - Linux
- `npm run build:mac` - MacOS - `pnpm dist:mac` - MacOS
Builds the app for macOS, Linux, and Windows, Builds the app for macOS, Linux, and Windows,
using [electron-builder](https://github.com/electron-userland/electron-builder). 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 ## Tests
```bash ```bash
npm run test pnpm test
``` ```
Uses [Playwright](https://playwright.dev/) to test the app. Uses [Playwright](https://playwright.dev/) to test the app.

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { blockers } from '../plugins/adblocker/blocker-types'; import { blockers } from '../plugins/adblocker/blocker-types';
import { DefaultPresetList } from '../plugins/downloader/types';
export interface WindowSizeConfig { export interface WindowSizeConfig {
width: number; width: number;
height: number; height: number;
@ -106,18 +108,25 @@ const defaultConfig = {
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000, // 10 minutes activityTimoutTime: 10 * 60 * 1000, // 10 minutes
listenAlong: true, // Add a "listen along" button to rich presence listenAlong: true, // Add a "listen along" 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 hideDurationLeft: false, // Hides the start and end time of the song to rich presence
}, },
'downloader': { 'downloader': {
enabled: false, enabled: false,
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path) downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
preset: 'mp3', selectedPreset: 'mp3 (256kbps)', // Selected preset
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
skipExisting: false, skipExisting: false,
playlistMaxItems: undefined as number | undefined, playlistMaxItems: undefined as number | undefined,
}, },
'exponential-volume': {}, 'exponential-volume': {},
'in-app-menu': {}, 'in-app-menu': {
/**
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
*/
enabled: false,
hideDOMWindowControls: false,
},
'last-fm': { 'last-fm': {
enabled: false, enabled: false,
token: undefined as string | undefined, // Token used for authentication token: undefined as string | undefined, // Token used for authentication

View File

@ -1,8 +1,18 @@
import Store from 'electron-store'; import Store from 'electron-store';
import Conf from 'conf'; import Conf from 'conf';
import is from 'electron-is';
import defaults from './defaults'; import defaults from './defaults';
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
const getDefaults = () => {
if (is.windows()) {
defaults.plugins['in-app-menu'].enabled = true;
}
return defaults;
};
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => { const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
if (!store.get(`plugins.${plugin}`)) { if (!store.get(`plugins.${plugin}`)) {
store.set(`plugins.${plugin}`, defaults.plugins[plugin]); store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
@ -10,6 +20,26 @@ const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: k
}; };
const migrations = { const migrations = {
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
if (originalPreset) {
if (originalPreset !== 'opus') {
store.set('plugins.downloader.selectedPreset', 'Custom');
store.set('plugins.downloader.customPresetSetting', {
extension: 'mp3',
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
} satisfies Preset);
} else {
store.set('plugins.downloader.selectedPreset', 'Source');
store.set('plugins.downloader.customPresetSetting', {
extension: null,
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? [],
} satisfies Preset);
}
store.delete('plugins.downloader.preset');
store.delete('plugins.downloader.ffmpegArgs');
}
},
'>=1.20.0'(store: Conf<Record<string, unknown>>) { '>=1.20.0'(store: Conf<Record<string, unknown>>) {
setDefaultPluginOptions(store, 'visualizer'); setDefaultPluginOptions(store, 'visualizer');
@ -118,7 +148,7 @@ const migrations = {
}; };
export default new Store({ export default new Store({
defaults, defaults: getDefaults(),
clearInvalidConfig: false, clearInvalidConfig: false,
migrations, migrations,
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('adblocker', { enableFront: true }); 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, { export default Object.assign(config, {
shouldUseBlocklists, 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 inject from './inject';
import injectCliqzPreload from './inject-cliqz-preload'; import injectCliqzPreload from './inject-cliqz-preload';
import { blockers } from './blocker-types'; import { blockers } from './blocker-types';
export default async () => { export default async () => {
if (await config.shouldUseBlocklists()) { if (shouldUseBlocklists()) {
// Preload adblocker to inject scripts/styles // Preload adblocker to inject scripts/styles
injectCliqzPreload(); await injectCliqzPreload();
// eslint-disable-next-line @typescript-eslint/await-thenable // eslint-disable-next-line @typescript-eslint/await-thenable
} else if ((await config.get('blocker')) === blockers.InPlayer) { } else if ((config.get('blocker')) === blockers.InPlayer) {
inject(); 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 { Client as DiscordClient } from '@xhayper/discord-rpc';
import { dev } from 'electron-is'; import { dev } from 'electron-is';
@ -156,6 +156,14 @@ export default (
// Song information changed, so lets update the rich presence // Song information changed, so lets update the rich presence
// @see https://discord.com/developers/docs/topics/gateway#activity-object // @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 // 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 = { const activityInfo: SetActivity = {
details: songInfo.title, details: songInfo.title,
state: songInfo.artist, state: songInfo.artist,
@ -163,7 +171,7 @@ export default (
largeImageText: songInfo.album ?? '', largeImageText: songInfo.album ?? '',
buttons: [ buttons: [
...(options.listenAlong ? [{ label: 'Listen Along', url: songInfo.url ?? '' }] : []), ...(options.listenAlong ? [{ label: 'Listen Along', url: songInfo.url ?? '' }] : []),
{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' }, ...(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 // If the page is ready, register the callback
win.once('ready-to-show', () => { win.once('ready-to-show', () => {
registerCallback(updateActivity); let lastSongInfo: SongInfo;
registerCallback((songInfo) => {
lastSongInfo = songInfo;
updateActivity(songInfo);
});
connect(); 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); app.on('window-all-closed', clear);
}; };

View File

@ -55,6 +55,15 @@ export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMen
setMenuOptions('discord', options); setMenuOptions('discord', options);
}, },
}, },
{
label: 'Hide GitHub link Button',
type: 'checkbox',
checked: options.hideGitHubButton,
click(item: Electron.MenuItem) {
options.hideGitHubButton = item.checked;
setMenuOptions('discord', options);
},
},
{ {
label: 'Hide duration left', label: 'Hide duration left',
type: 'checkbox', type: 'checkbox',

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 { join } from 'node:path';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron'; 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 is from 'electron-is';
import ytpl from 'ytpl';
// REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
import filenamify from 'filenamify'; import filenamify from 'filenamify';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { createFFmpeg } from '@ffmpeg.wasm/main'; import { createFFmpeg } from '@ffmpeg.wasm/main';
import NodeID3, { TagConstants } from 'node-id3'; import NodeID3, { TagConstants } from 'node-id3';
import PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage'; import {
import { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils'; cropMaxWidth,
getFolder,
import TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo'; sendFeedback as sendFeedback_,
setBadge,
import { VideoInfo } from 'youtubei.js/dist/src/parser/youtube'; } from './utils';
import { cropMaxWidth, getFolder, presets, sendFeedback as sendFeedback_, setBadge } from './utils';
import config from './config'; import config from './config';
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
import style from './style.css'; import style from './style.css';
@ -32,17 +38,20 @@ import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
import { injectCSS } from '../utils'; import { injectCSS } from '../utils';
import { cache } from '../../providers/decorators'; 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 }; type CustomSongInfo = SongInfo & { trackId?: string };
const ffmpeg = createFFmpeg({ const ffmpeg = createFFmpeg({
log: false, log: false,
logger() { logger() {}, // Console.log,
}, // Console.log, progress() {}, // Console.log,
progress() {
}, // Console.log,
}); });
const ffmpegMutex = new Mutex(); 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) => { export default async (win_: BrowserWindow) => {
win = win_; win = win_;
injectCSS(win.webContents, style); 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({ yt = await Innertube.create({
cache: new UniversalCache(false), cache: new UniversalCache(false),
cookie, cookie: await getCookieFromWindow(win),
generate_session_locally: true, generate_session_locally: true,
fetch: async (input: RequestInfo | URL, init?: RequestInit) => { fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = const url =
typeof input === 'string' ? typeof input === 'string'
new URL(input) : ? new URL(input)
input instanceof URL ? : input instanceof URL
input : new URL(input.url); ? input
: new URL(input.url);
if (init?.body && !init.method) { if (init?.body && !init.method) {
init.method = 'POST'; init.method = 'POST';
@ -97,7 +114,7 @@ export default async (win_: BrowserWindow) => {
); );
return net.fetch(request, init); return net.fetch(request, init);
} }) as typeof fetch,
}); });
ipcMain.on('download-song', (_, url: string) => downloadSong(url)); ipcMain.on('download-song', (_, url: string) => downloadSong(url));
ipcMain.on('video-src-changed', (_, data: GetPlayerResponse) => { ipcMain.on('video-src-changed', (_, data: GetPlayerResponse) => {
@ -112,14 +129,14 @@ export async function downloadSong(
url: string, url: string,
playlistFolder: string | undefined = undefined, playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined, trackId: string | undefined = undefined,
increasePlaylistProgress: (value: number) => void = () => { increasePlaylistProgress: (value: number) => void = () => {},
},
) { ) {
let resolvedName; let resolvedName;
try { try {
await downloadSongUnsafe( await downloadSongUnsafe(
false,
url, url,
(name: string) => resolvedName = name, (name: string) => (resolvedName = name),
playlistFolder, playlistFolder,
trackId, trackId,
increasePlaylistProgress, 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( async function downloadSongUnsafe(
url: string, isId: boolean,
idOrUrl: string,
setName: (name: string) => void, setName: (name: string) => void,
playlistFolder: string | undefined = undefined, playlistFolder: string | undefined = undefined,
trackId: string | undefined = undefined, trackId: string | undefined = undefined,
@ -147,8 +186,13 @@ async function downloadSongUnsafe(
sendFeedback('Downloading...', 2); sendFeedback('Downloading...', 2);
const id = getVideoId(url); let id: string | null;
if (typeof id !== 'string') throw new Error('Video not found'); 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); let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
@ -163,8 +207,8 @@ async function downloadSongUnsafe(
metadata.trackId = trackId; metadata.trackId = trackId;
const dir const dir =
= playlistFolder || config.get('downloadFolder') || app.getPath('downloads'); playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${ const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${
metadata.title metadata.title
}`; }`;
@ -187,27 +231,22 @@ async function downloadSongUnsafe(
} }
if (playabilityStatus.status === 'UNPLAYABLE') { if (playabilityStatus.status === 'UNPLAYABLE') {
const errorScreen = playabilityStatus.error_screen as PlayerErrorMessage | null; const errorScreen =
playabilityStatus.error_screen as PlayerErrorMessage | null;
throw new Error( throw new Error(
`[${playabilityStatus.status}] ${errorScreen?.reason.text}: ${errorScreen?.subreason.text}`, `[${playabilityStatus.status}] ${errorScreen?.reason.text}: ${errorScreen?.subreason.text}`,
); );
} }
const preset = config.get('preset') ?? 'mp3'; const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
let presetSetting: { extension: string; ffmpegArgs: string[] } | null = null; let presetSetting: Preset;
if (preset === 'opus') { if (selectedPreset === 'Custom') {
presetSetting = presets[preset]; presetSetting =
} config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
} else if (selectedPreset === 'Source') {
const filename = filenamify(`${name}.${presetSetting?.extension ?? 'mp3'}`, { presetSetting = DefaultPresetList['Source'];
replacement: '_', } else {
maxLength: 255, presetSetting = DefaultPresetList['mp3 (256kbps)'];
});
const filePath = join(dir, filename);
if (config.get('skipExisting') && existsSync(filePath)) {
sendFeedback(null, -1);
return;
} }
const downloadOptions: FormatOptions = { const downloadOptions: FormatOptions = {
@ -217,6 +256,30 @@ async function downloadSongUnsafe(
}; };
const format = info.chooseFormat(downloadOptions); 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); const stream = await info.download(downloadOptions);
console.info( console.info(
@ -229,40 +292,25 @@ async function downloadSongUnsafe(
mkdirSync(dir); mkdirSync(dir);
} }
const ffmpegArgs = config.get('ffmpegArgs'); const fileBuffer = await iterableStreamToTargetFile(
iterableStream,
targetFileExtension,
metadata,
presetSetting?.ffmpegArgs ?? [],
format.content_length ?? 0,
sendFeedback,
increasePlaylistProgress,
);
if (presetSetting && presetSetting?.extension !== 'mp3') { if (fileBuffer) {
const file = createWriteStream(filePath); if (targetFileExtension !== 'mp3') {
let downloaded = 0; createWriteStream(filePath).write(fileBuffer);
const total: number = format.content_length ?? 1; } else {
const buffer = await writeID3(
for await (const chunk of iterableStream) { Buffer.from(fileBuffer),
downloaded += chunk.length; metadata,
const ratio = downloaded / total; sendFeedback,
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 (buffer) { if (buffer) {
writeFileSync(filePath, buffer); writeFileSync(filePath, buffer);
} }
@ -273,14 +321,14 @@ async function downloadSongUnsafe(
console.info(`Done: "${filePath}"`); console.info(`Done: "${filePath}"`);
} }
async function iterableStreamToMP3( async function iterableStreamToTargetFile(
stream: AsyncGenerator<Uint8Array, void>, stream: AsyncGenerator<Uint8Array, void>,
extension: string,
metadata: CustomSongInfo, metadata: CustomSongInfo,
ffmpegArgs: string[], presetFfmpegArgs: string[],
contentLength: number, contentLength: number,
sendFeedback: (str: string, value?: number) => void, sendFeedback: (str: string, value?: number) => void,
increasePlaylistProgress: (value: number) => void = () => { increasePlaylistProgress: (value: number) => void = () => {},
},
) { ) {
const chunks = []; const chunks = [];
let downloaded = 0; let downloaded = 0;
@ -316,13 +364,14 @@ async function iterableStreamToMP3(
increasePlaylistProgress(0.15 + (ratio * 0.85)); increasePlaylistProgress(0.15 + (ratio * 0.85));
}); });
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
try { try {
await ffmpeg.run( await ffmpeg.run(
'-i', '-i',
safeVideoName, safeVideoName,
...ffmpegArgs, ...presetFfmpegArgs,
...getFFmpegMetadataArgs(metadata), ...getFFmpegMetadataArgs(metadata),
`${safeVideoName}.mp3`, safeVideoNameWithExtension,
); );
} finally { } finally {
ffmpeg.FS('unlink', safeVideoName); ffmpeg.FS('unlink', safeVideoName);
@ -331,9 +380,9 @@ async function iterableStreamToMP3(
sendFeedback('Saving…'); sendFeedback('Saving…');
try { try {
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`); return ffmpeg.FS('readFile', safeVideoNameWithExtension);
} finally { } finally {
ffmpeg.FS('unlink', `${safeVideoName}.mp3`); ffmpeg.FS('unlink', safeVideoNameWithExtension);
} }
} catch (error: unknown) { } catch (error: unknown) {
sendError(error as Error, safeVideoName); sendError(error as Error, safeVideoName);
@ -347,7 +396,11 @@ const getCoverBuffer = cache(async (url: string) => {
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null; 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 { try {
sendFeedback('Writing ID3 tags...'); sendFeedback('Writing ID3 tags...');
const tags: NodeID3.Tags = {}; const tags: NodeID3.Tags = {};
@ -400,10 +453,10 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
return; return;
} }
const playlistId const playlistId =
= getPlaylistID(givenUrl) getPlaylistID(givenUrl) ||
|| getPlaylistID(new URL(win.webContents.getURL())) getPlaylistID(new URL(win.webContents.getURL())) ||
|| getPlaylistID(new URL(playingUrl)); getPlaylistID(new URL(playingUrl));
if (!playlistId) { if (!playlistId) {
sendError(new Error('No playlist ID found')); sendError(new Error('No playlist ID found'));
@ -414,35 +467,56 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
console.log(`trying to get playlist ID: '${playlistId}'`); console.log(`trying to get playlist ID: '${playlistId}'`);
sendFeedback('Getting playlist info…'); sendFeedback('Getting playlist info…');
let playlist: ytpl.Result; let playlist: Playlist;
const items: YTNodes.MusicResponsiveListItem[] = [];
try { try {
playlist = await ytpl(playlistId, { playlist = await yt.music.getPlaylist(playlistId);
limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY, if (playlist?.items) {
}); items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
}
} catch (error: unknown) { } catch (error: unknown) {
sendError( 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; return;
} }
if (playlist.items.length === 0) { if (!playlist || !playlist.items || playlist.items.length === 0) {
sendError(new Error('Playlist is empty')); 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'); sendFeedback('Playlist has only one item, downloading it directly');
await downloadSong(playlist.items[0].url); await downloadSongFromId(items.at(0)!.id!);
return; return;
} }
const isAlbum = playlist.title.startsWith('Album - '); let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
if (isAlbum) { if (!is.macOS()) {
playlist.title = playlist.title.slice(8); safePlaylistTitle = safePlaylistTitle.normalize('NFC');
} }
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
const folder = getFolder(config.get('downloadFolder') ?? ''); const folder = getFolder(config.get('downloadFolder') ?? '');
const playlistFolder = join(folder, safePlaylistTitle); const playlistFolder = join(folder, safePlaylistTitle);
if (existsSync(playlistFolder)) { if (existsSync(playlistFolder)) {
@ -458,47 +532,51 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
type: 'info', type: 'info',
buttons: ['OK'], buttons: ['OK'],
title: 'Started Download', title: 'Started Download',
message: `Downloading Playlist "${playlist.title}"`, message: `Downloading Playlist "${playlistTitle}"`,
detail: `(${playlist.items.length} songs)`, detail: `(${items.length} songs)`,
}); });
if (is.dev()) { if (is.dev()) {
console.log( 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 win.setProgressBar(2); // Starts with indefinite bar
setBadge(playlist.items.length); setBadge(items.length);
let counter = 1; let counter = 1;
const progressStep = 1 / playlist.items.length; const progressStep = 1 / items.length;
const increaseProgress = (itemPercentage: number) => { const increaseProgress = (itemPercentage: number) => {
const currentProgress = (counter - 1) / (playlist.items.length ?? 1); const currentProgress = (counter - 1) / (items.length ?? 1);
const newProgress = currentProgress + (progressStep * itemPercentage); const newProgress = currentProgress + (progressStep * itemPercentage);
win.setProgressBar(newProgress); win.setProgressBar(newProgress);
}; };
try { try {
for (const song of playlist.items) { for (const song of items) {
sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); sendFeedback(`Downloading ${counter}/${items.length}...`);
const trackId = isAlbum ? counter : undefined; const trackId = isAlbum ? counter : undefined;
await downloadSong( await downloadSongFromId(
song.url, song.id!,
playlistFolder, playlistFolder,
trackId?.toString(), trackId?.toString(),
increaseProgress, increaseProgress,
).catch((error) => ).catch((error) =>
sendError( 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); win.setProgressBar(counter / items.length);
setBadge(playlist.items.length - counter); setBadge(items.length - counter);
counter++; counter++;
} }
} catch (error: unknown) { } catch (error: unknown) {
@ -510,29 +588,6 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
} }
} }
async function ffmpegWriteTags(filePath: string, metadata: CustomSongInfo, presetFFmpegArgs: string[] = [], ffmpegArgs: string[] = []) {
const releaseFFmpegMutex = await ffmpegMutex.acquire();
try {
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
await ffmpeg.run(
'-i',
filePath,
...getFFmpegMetadataArgs(metadata),
...presetFFmpegArgs,
...ffmpegArgs,
filePath,
);
} catch (error: unknown) {
sendError(error as Error);
} finally {
releaseFFmpegMutex();
}
}
function getFFmpegMetadataArgs(metadata: CustomSongInfo) { function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
if (!metadata) { if (!metadata) {
return []; return [];
@ -550,8 +605,8 @@ function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
const getPlaylistID = (aURL: URL) => { const getPlaylistID = (aURL: URL) => {
const result const result =
= aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist'); aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
return result.slice(INVALID_PLAYLIST_MODIFIER.length); return result.slice(INVALID_PLAYLIST_MODIFIER.length);
} }
@ -560,16 +615,18 @@ const getPlaylistID = (aURL: URL) => {
}; };
const getVideoId = (url: URL | string): string | null => { 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 => ({ const getMetadata = (info: TrackInfo): CustomSongInfo => ({
videoId: info.basic_info.id!, videoId: info.basic_info.id!,
title: cleanupName(info.basic_info.title!), title: cleanupName(info.basic_info.title!),
artist: cleanupName(info.basic_info.author!), artist: cleanupName(info.basic_info.author!),
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any album: info.player_overlays?.browser_media_session?.as(
album: (info.player_overlays?.browser_media_session as any)?.album?.text as string | undefined, YTNodes.BrowserMediaSession,
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url, ).album?.text,
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))
?.url,
views: info.basic_info.view_count!, views: info.basic_info.view_count!,
songDuration: info.basic_info.duration!, songDuration: info.basic_info.duration!,
}); });

View File

@ -47,7 +47,7 @@ const menuObserver = new MutationObserver(() => {
(global as any).download = () => { (global as any).download = () => {
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// Selector of first button which is always "Start Radio" // Selector of first button which is always "Start Radio"
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint') ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')
?.getAttribute('href'); ?.getAttribute('href');
if (videoUrl) { if (videoUrl) {
if (videoUrl.startsWith('watch?')) { if (videoUrl.startsWith('watch?')) {

View File

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

View File

@ -0,0 +1,116 @@
export interface Preset {
extension?: string | null;
ffmpegArgs: string[];
}
// Presets for FFmpeg
export const DefaultPresetList: Record<string, Preset> = {
'mp3 (256kbps)': {
extension: 'mp3',
ffmpegArgs: ['-b:a', '256k'],
},
'Source': {
extension: undefined,
ffmpegArgs: ['-acodec', 'copy'],
},
'Custom': {
extension: null,
ffmpegArgs: [],
}
};
export interface YouTubeFormat {
itag: number;
container: string;
content: string;
resolution: string;
bitrate: string;
range: string;
vrOr3D: string;
}
// converted from https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2#file-youtube_format_code_itag_list-md
export const YoutubeFormatList: YouTubeFormat[] = [
{ itag: 5, container: 'flv', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 6, container: 'flv', content: 'audio/video', resolution: '270p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 17, container: '3gp', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 18, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 22, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 34, container: 'flv', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 35, container: 'flv', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 36, container: '3gp', content: 'audio/video', resolution: '180p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 37, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 38, container: 'mp4', content: 'audio/video', resolution: '3072p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 43, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 44, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 45, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 46, container: 'webm', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 82, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 83, container: 'mp4', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 84, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 85, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 91, container: 'hls', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 92, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 93, container: 'hls', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 94, container: 'hls', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 95, container: 'hls', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 96, container: 'hls', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
{ itag: 100, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 101, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 102, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
{ itag: 132, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 133, container: 'mp4', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 134, container: 'mp4', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 135, container: 'mp4', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 136, container: 'mp4', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 137, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 138, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 139, container: 'm4a', content: 'audio', resolution: '-', bitrate: '48k', range: '-', vrOr3D: '' },
{ itag: 140, container: 'm4a', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
{ itag: 141, container: 'm4a', content: 'audio', resolution: '-', bitrate: '256k', range: '-', vrOr3D: '' },
{ itag: 151, container: 'hls', content: 'audio/video', resolution: '72p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 160, container: 'mp4', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 167, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 168, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 169, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 171, container: 'webm', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
{ itag: 218, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 219, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 242, container: 'webm', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 243, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 244, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 245, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 246, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 247, container: 'webm', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 248, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 249, container: 'webm', content: 'audio', resolution: '-', bitrate: '50k', range: '-', vrOr3D: '' },
{ itag: 250, container: 'webm', content: 'audio', resolution: '-', bitrate: '70k', range: '-', vrOr3D: '' },
{ itag: 251, container: 'webm', content: 'audio', resolution: '-', bitrate: '160k', range: '-', vrOr3D: '' },
{ itag: 264, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 266, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 271, container: 'webm', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 272, container: 'webm', content: 'video', resolution: '4320p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 278, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 298, container: 'mp4', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 299, container: 'mp4', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 302, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 303, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 308, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 313, container: 'webm', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 315, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 330, container: 'webm', content: 'video', resolution: '144p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 331, container: 'webm', content: 'video', resolution: '240p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 332, container: 'webm', content: 'video', resolution: '360p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 333, container: 'webm', content: 'video', resolution: '480p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 334, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 335, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 336, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 337, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
{ itag: 272, container: 'webm', content: 'video', resolution: '2880p/4320p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 399, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 400, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 401, container: 'mp4', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 402, container: 'mp4', content: 'video', resolution: '2880p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 571, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
{ itag: 702, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
];

View File

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

View File

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-close', () => win.close());
ipcMain.handle('window-minimize', () => win.minimize()); ipcMain.handle('window-minimize', () => win.minimize());
ipcMain.handle('window-maximize', () => win.maximize()); ipcMain.handle('window-maximize', () => win.maximize());
win.on('maximize', () => win.webContents.send('window-maximize'));
ipcMain.handle('window-unmaximize', () => win.unmaximize()); ipcMain.handle('window-unmaximize', () => win.unmaximize());
win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
}; };

View File

@ -19,9 +19,11 @@ const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS; const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
export default async () => { export default async () => {
const hideDOMWindowControls = config.get('plugins.in-app-menu.hideDOMWindowControls');
let hideMenu = config.get('options.hideMenu'); let hideMenu = config.get('options.hideMenu');
const titleBar = document.createElement('title-bar'); const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background'); const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
let maximizeButton: HTMLButtonElement;
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px'); if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
logo.classList.add('title-bar-icon'); logo.classList.add('title-bar-icon');
@ -55,7 +57,7 @@ export default async () => {
minimizeButton.appendChild(minimize); minimizeButton.appendChild(minimize);
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize'); minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
const maximizeButton = document.createElement('button'); maximizeButton = document.createElement('button');
if (await ipcRenderer.invoke('window-is-maximized')) { if (await ipcRenderer.invoke('window-is-maximized')) {
maximizeButton.classList.add('window-control'); maximizeButton.classList.add('window-control');
maximizeButton.appendChild(unmaximize); maximizeButton.appendChild(unmaximize);
@ -97,7 +99,7 @@ export default async () => {
titleBar.appendChild(windowControlsContainer); titleBar.appendChild(windowControlsContainer);
}; };
if (isNotWindowsOrMacOS) await addWindowControls(); if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
if (navBar) { if (navBar) {
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
@ -129,14 +131,24 @@ export default async () => {
menu.style.visibility = 'hidden'; menu.style.visibility = 'hidden';
} }
}); });
if (isNotWindowsOrMacOS) await addWindowControls(); if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
}; };
await updateMenu(); await updateMenu();
document.title = 'Youtube Music'; document.title = 'Youtube Music';
ipcRenderer.on('refreshMenu', () => { ipcRenderer.on('refreshMenu', () => updateMenu());
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')) { 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 { 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 config from './config';
import getSongControls from '../../providers/song-controls'; import getSongControls from '../../providers/song-controls';
import registerCallback, { SongInfo } from '../../providers/song-info'; import registerCallback, { SongInfo } from '../../providers/song-info';
import { changeProtocolHandler } from '../../providers/protocol-handler'; import { changeProtocolHandler } from '../../providers/protocol-handler';
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray'; import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
import { getMediaIconLocation } from '../utils'; import { getMediaIconLocation, mediaIcons, saveMediaIcon } from '../utils';
let songControls: ReturnType<typeof getSongControls>; let songControls: ReturnType<typeof getSongControls>;
let savedNotification: Notification | undefined; let savedNotification: Notification | undefined;
@ -23,7 +23,7 @@ export default (win: BrowserWindow) => {
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t); ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
if (app.isPackaged) { if (app.isPackaged) {
saveTempIcon(); saveMediaIcon();
} }
let savedSongInfo: SongInfo; 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) { if (config.get('toastStyle') === ToastStyles.legacy) {
return `content="${icons[kind]}"`; return `content="${mediaIcons[kind]}"`;
} }
return `\ 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}"/>`; `<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
const getButtons = (isPaused: boolean) => `\ const getButtons = (isPaused: boolean) => `\

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