Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c25def8901 | |||
| 284a59b721 | |||
| 5fcba8619a | |||
| f3cd759276 | |||
| 9d3981e361 | |||
| 787326948b | |||
| 779251933c | |||
| 1efe835c69 | |||
| 5702978227 | |||
| fa3d742838 | |||
| c460cc2296 | |||
| 4e4af5e830 | |||
| 9a4e98063b | |||
| 8bfe04bb50 | |||
| 6774d54f5e | |||
| 9705f8489d | |||
| a7229cbe14 | |||
| 7577aba45e | |||
| d78fbe476e | |||
| bfe4b2bba7 | |||
| 7625a3aa52 | |||
| 30c8dcf730 | |||
| 00a3e8d35e | |||
| 4d01cdfa6c | |||
| f924b6c8e3 | |||
| 926d98174c | |||
| 41b3972f54 | |||
| 467f29e363 | |||
| 9cc13c3757 | |||
| f8ccb86156 | |||
| b316aa2301 | |||
| 5c49b28664 | |||
| dedf96afd3 | |||
| 3bb5bc2ca1 | |||
| c79fdd9887 | |||
| d7b821727d | |||
| 21c45faf20 | |||
| 92cab89d17 | |||
| fa160b2e90 | |||
| 308ac38e6b | |||
| a62cafb601 | |||
| bf9e3b5f48 | |||
| 3c6b3aeff0 | |||
| 37181a7b5e | |||
| 0b363d6487 | |||
| e9398adac3 | |||
| 6901713036 | |||
| 1d5b2997bd | |||
| 572a023aaa | |||
| 9187f1e240 | |||
| df13d7d0f3 | |||
| 85228fd7d2 | |||
| 17ba071057 | |||
| d7df4d7d10 | |||
| 7aa970cebc | |||
| f08f003cf4 | |||
| 9f99eded9e | |||
| c512f13009 | |||
| b475f780ff | |||
| 2294102006 | |||
| d69a07d025 | |||
| 4f4995c20c | |||
| b6894dca29 | |||
| 73f14e581d | |||
| 2f2e64af4a | |||
| 5710307ddc | |||
| 52ba2dc9ff | |||
| 926b9fb5e6 | |||
| a6c9b3381a | |||
| 5dc13a4698 | |||
| a69085c591 | |||
| a22f7fed21 | |||
| 8b7045fb1b | |||
| efd1b92514 | |||
| 969f6d7bba | |||
| 4f7c92d6a0 | |||
| 24d4a50574 | |||
| 7693a3ba4a | |||
| 7ca4dc5c85 | |||
| 21ff09b605 | |||
| fbf4b3b8b5 | |||
| 5812eb0147 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
||||||
|
|||||||
107
.github/workflows/build.yml
vendored
@ -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
@ -0,0 +1,20 @@
|
|||||||
|
name: Submit CLA to Winget PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pr_url:
|
||||||
|
description: "Specific PR URL"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
name: Comment to PR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Submit CLA to Windows Package Manager Community Repository Pull Request
|
||||||
|
run: gh pr comment $PR_URL --body "@microsoft-github-policy-service agree"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||||
|
PR_URL: ${{ inputs.pr_url }}
|
||||||
2
.github/workflows/winget-submission.yml
vendored
@ -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
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
55
changelog.md
@ -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
130
package.json
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
38
patches/youtubei.js+6.4.1.patch
Normal 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) {
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export default () => {
|
|
||||||
const path = '@cliqz/adblocker-electron-preload'; // prevent require hoisting
|
|
||||||
require(path);
|
|
||||||
};
|
|
||||||
@ -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
28
readme.md
@ -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 screen’s 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.
|
||||||
|
|||||||
@ -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,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
@ -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;
|
||||||
@ -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) => {
|
||||||
0
navigation.d.ts → src/navigation.d.ts
vendored
@ -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,
|
||||||
@ -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 };
|
||||||
@ -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,
|
||||||
3
src/plugins/adblocker/inject-cliqz-preload.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default async () => {
|
||||||
|
await import('@cliqz/adblocker-electron-preload');
|
||||||
|
};
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
4
src/plugins/bypass-age-restrictions/front.ts
Normal 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');
|
||||||
|
};
|
||||||
4
src/plugins/bypass-age-restrictions/simple-youtube-age-restriction-bypass.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module 'simple-youtube-age-restriction-bypass' {
|
||||||
|
const nothing: never;
|
||||||
|
export default nothing;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
};
|
};
|
||||||
@ -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',
|
||||||
@ -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!,
|
||||||
});
|
});
|
||||||
@ -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?')) {
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
|
||||||
import { downloadPlaylist } from './back';
|
import { downloadPlaylist } from './back';
|
||||||
import { defaultMenuDownloadLabel, getFolder, presets } from './utils';
|
import { defaultMenuDownloadLabel, getFolder } from './utils';
|
||||||
|
import { DefaultPresetList } from './types';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
import { MenuTemplate } from '../../menu';
|
import { MenuTemplate } from '../../menu';
|
||||||
@ -25,12 +26,12 @@ export default (): MenuTemplate => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Presets',
|
label: 'Presets',
|
||||||
submenu: Object.keys(presets).map((preset) => ({
|
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||||
label: preset,
|
label: preset,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.get('preset') === preset,
|
checked: config.get('selectedPreset') === preset,
|
||||||
click() {
|
click() {
|
||||||
config.set('preset', preset);
|
config.set('selectedPreset', preset);
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
116
src/plugins/downloader/types.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
export interface Preset {
|
||||||
|
extension?: string | null;
|
||||||
|
ffmpegArgs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presets for FFmpeg
|
||||||
|
export const DefaultPresetList: Record<string, Preset> = {
|
||||||
|
'mp3 (256kbps)': {
|
||||||
|
extension: 'mp3',
|
||||||
|
ffmpegArgs: ['-b:a', '256k'],
|
||||||
|
},
|
||||||
|
'Source': {
|
||||||
|
extension: undefined,
|
||||||
|
ffmpegArgs: ['-acodec', 'copy'],
|
||||||
|
},
|
||||||
|
'Custom': {
|
||||||
|
extension: null,
|
||||||
|
ffmpegArgs: [],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface YouTubeFormat {
|
||||||
|
itag: number;
|
||||||
|
container: string;
|
||||||
|
content: string;
|
||||||
|
resolution: string;
|
||||||
|
bitrate: string;
|
||||||
|
range: string;
|
||||||
|
vrOr3D: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// converted from https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2#file-youtube_format_code_itag_list-md
|
||||||
|
export const YoutubeFormatList: YouTubeFormat[] = [
|
||||||
|
{ itag: 5, container: 'flv', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 6, container: 'flv', content: 'audio/video', resolution: '270p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 17, container: '3gp', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 18, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 22, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 34, container: 'flv', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 35, container: 'flv', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 36, container: '3gp', content: 'audio/video', resolution: '180p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 37, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 38, container: 'mp4', content: 'audio/video', resolution: '3072p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 43, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 44, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 45, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 46, container: 'webm', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 82, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 83, container: 'mp4', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 84, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 85, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 91, container: 'hls', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 92, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 93, container: 'hls', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 94, container: 'hls', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 95, container: 'hls', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 96, container: 'hls', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||||
|
{ itag: 100, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 101, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 102, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||||
|
{ itag: 132, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 133, container: 'mp4', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 134, container: 'mp4', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 135, container: 'mp4', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 136, container: 'mp4', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 137, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 138, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 139, container: 'm4a', content: 'audio', resolution: '-', bitrate: '48k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 140, container: 'm4a', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 141, container: 'm4a', content: 'audio', resolution: '-', bitrate: '256k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 151, container: 'hls', content: 'audio/video', resolution: '72p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 160, container: 'mp4', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 167, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 168, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 169, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 171, container: 'webm', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 218, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 219, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 242, container: 'webm', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 243, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 244, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 245, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 246, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 247, container: 'webm', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 248, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 249, container: 'webm', content: 'audio', resolution: '-', bitrate: '50k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 250, container: 'webm', content: 'audio', resolution: '-', bitrate: '70k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 251, container: 'webm', content: 'audio', resolution: '-', bitrate: '160k', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 264, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 266, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 271, container: 'webm', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 272, container: 'webm', content: 'video', resolution: '4320p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 278, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 298, container: 'mp4', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 299, container: 'mp4', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 302, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 303, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 308, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 313, container: 'webm', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 315, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 330, container: 'webm', content: 'video', resolution: '144p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 331, container: 'webm', content: 'video', resolution: '240p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 332, container: 'webm', content: 'video', resolution: '360p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 333, container: 'webm', content: 'video', resolution: '480p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 334, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 335, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 336, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 337, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||||
|
{ itag: 272, container: 'webm', content: 'video', resolution: '2880p/4320p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 399, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 400, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 401, container: 'mp4', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 402, container: 'mp4', content: 'video', resolution: '2880p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 571, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
{ itag: 702, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||||
|
];
|
||||||
@ -10,7 +10,7 @@ export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
|
|||||||
|
|
||||||
export const cropMaxWidth = (image: Electron.NativeImage) => {
|
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);
|
||||||
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 392 B |
|
Before Width: | Height: | Size: 252 B After Width: | Height: | Size: 252 B |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
|
Before Width: | Height: | Size: 174 B After Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 546 B |
@ -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'));
|
||||||
};
|
};
|
||||||
@ -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')) {
|
||||||
22
src/plugins/in-app-menu/menu.ts
Normal 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[],
|
||||||
|
];
|
||||||
@ -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) => `\
|
||||||