Compare commits
33 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 |
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
|
||||||
|
|||||||
105
.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: |
|
||||||
@ -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 == '' }}
|
||||||
|
|||||||
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
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)
|
#### [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(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)
|
- 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)
|
- fix: winget publish [`#1307`](https://github.com/th-ch/youtube-music/pull/1307)
|
||||||
|
|||||||
10137
package-lock.json
generated
86
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "2.1.1",
|
"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",
|
||||||
@ -87,39 +87,58 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"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",
|
||||||
"release:linux": "npm run clean && npm run build && electron-builder --linux -p always -c.snap.publish=github",
|
"release:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "npm run clean && npm run build && electron-builder --mac -p always",
|
"release:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac -p always",
|
||||||
"release:win": "npm run clean && npm run build && electron-builder --win -p always",
|
"release:win": "yarpm-pnpm run clean && yarpm-pnpm 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.8",
|
"@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",
|
||||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||||
"@jellybrick/mpris-service": "2.1.4",
|
"@jellybrick/mpris-service": "2.1.4",
|
||||||
"@xhayper/discord-rpc": "1.0.23",
|
"@xhayper/discord-rpc": "1.0.24",
|
||||||
"async-mutex": "0.4.0",
|
"async-mutex": "0.4.0",
|
||||||
"butterchurn": "3.0.0-beta.4",
|
"butterchurn": "3.0.0-beta.4",
|
||||||
"butterchurn-presets": "3.0.0-beta.4",
|
"butterchurn-presets": "3.0.0-beta.4",
|
||||||
@ -141,20 +160,12 @@
|
|||||||
"simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
|
"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",
|
|
||||||
"node-fetch": "2.7.0",
|
|
||||||
"@electron/universal": "1.4.2",
|
|
||||||
"@babel/runtime": "7.23.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@milahu/patch-package": "6.4.14",
|
||||||
"@playwright/test": "1.39.0",
|
"@playwright/test": "1.39.0",
|
||||||
"@rollup/plugin-commonjs": "25.0.5",
|
"@rollup/plugin-commonjs": "25.0.7",
|
||||||
"@rollup/plugin-image": "3.0.3",
|
"@rollup/plugin-image": "3.0.3",
|
||||||
"@rollup/plugin-json": "6.0.1",
|
"@rollup/plugin-json": "6.0.1",
|
||||||
"@rollup/plugin-node-resolve": "15.2.3",
|
"@rollup/plugin-node-resolve": "15.2.3",
|
||||||
@ -162,31 +173,34 @@
|
|||||||
"@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.5",
|
"@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",
|
"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.1",
|
"eslint-plugin-prettier": "5.0.1",
|
||||||
"node-gyp": "9.4.0",
|
"node-gyp": "9.4.0",
|
||||||
"patch-package": "8.0.0",
|
|
||||||
"playwright": "1.39.0",
|
"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.5",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
5618
pnpm-lock.yaml
generated
Normal file
25
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)
|
||||||
|
|
||||||
@ -111,6 +115,8 @@ winget install th-ch.YouTubeMusic
|
|||||||
|
|
||||||
- [**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
|
||||||
@ -178,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 ci
|
pnpm install --frozen-lockfile
|
||||||
npm run start
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build your own plugins
|
## Build your own plugins
|
||||||
@ -266,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 dist:win` - Windows
|
- `pnpm dist:win` - Windows
|
||||||
- `npm run dist:linux` - Linux
|
- `pnpm dist:linux` - Linux
|
||||||
- `npm run dist: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).
|
||||||
@ -279,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,18 +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',
|
|
||||||
...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,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -76,7 +76,7 @@ const defaultConfig = {
|
|||||||
'adblocker': {
|
'adblocker': {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
cache: true,
|
cache: true,
|
||||||
blocker: blockers.WithBlocklists as string,
|
blocker: blockers.InPlayer as string,
|
||||||
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
||||||
disableDefaultLists: false,
|
disableDefaultLists: false,
|
||||||
},
|
},
|
||||||
@ -125,6 +125,7 @@ const defaultConfig = {
|
|||||||
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
|
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
|
||||||
*/
|
*/
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
hideDOMWindowControls: false,
|
||||||
},
|
},
|
||||||
'last-fm': {
|
'last-fm': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -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,
|
||||||
0
navigation.d.ts → src/navigation.d.ts
vendored
@ -24,12 +24,7 @@ export const loadAdBlockerEngine = async (
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
@ -1,16 +1,32 @@
|
|||||||
import { createWriteStream, existsSync, mkdirSync, writeFileSync, } from 'node:fs';
|
import {
|
||||||
|
createWriteStream,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { 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, YTNodes } from 'youtubei.js';
|
import {
|
||||||
|
ClientType,
|
||||||
|
Innertube,
|
||||||
|
UniversalCache,
|
||||||
|
Utils,
|
||||||
|
YTNodes,
|
||||||
|
} from 'youtubei.js';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import filenamify from 'filenamify';
|
import filenamify from 'filenamify';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
import { createFFmpeg } from '@ffmpeg.wasm/main';
|
import { createFFmpeg } from '@ffmpeg.wasm/main';
|
||||||
import NodeID3, { TagConstants } from 'node-id3';
|
import NodeID3, { TagConstants } from 'node-id3';
|
||||||
|
|
||||||
import { cropMaxWidth, getFolder, sendFeedback as sendFeedback_, setBadge } from './utils';
|
import {
|
||||||
|
cropMaxWidth,
|
||||||
|
getFolder,
|
||||||
|
sendFeedback as sendFeedback_,
|
||||||
|
setBadge,
|
||||||
|
} from './utils';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
|
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
|
||||||
|
|
||||||
@ -34,10 +50,8 @@ 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();
|
||||||
|
|
||||||
@ -65,9 +79,13 @@ const sendError = (error: Error, source?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCookieFromWindow = async (win: BrowserWindow) => {
|
export const getCookieFromWindow = async (win: BrowserWindow) => {
|
||||||
return (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
|
return (
|
||||||
it.name + '=' + it.value + ';'
|
await win.webContents.session.cookies.get({
|
||||||
).join('');
|
url: 'https://music.youtube.com',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.map((it) => it.name + '=' + it.value + ';')
|
||||||
|
.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (win_: BrowserWindow) => {
|
export default async (win_: BrowserWindow) => {
|
||||||
@ -78,12 +96,13 @@ export default async (win_: BrowserWindow) => {
|
|||||||
cache: new UniversalCache(false),
|
cache: new UniversalCache(false),
|
||||||
cookie: await getCookieFromWindow(win),
|
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';
|
||||||
@ -95,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) => {
|
||||||
@ -110,15 +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,
|
false,
|
||||||
url,
|
url,
|
||||||
(name: string) => resolvedName = name,
|
(name: string) => (resolvedName = name),
|
||||||
playlistFolder,
|
playlistFolder,
|
||||||
trackId,
|
trackId,
|
||||||
increasePlaylistProgress,
|
increasePlaylistProgress,
|
||||||
@ -132,15 +150,14 @@ export async function downloadSongFromId(
|
|||||||
id: string,
|
id: 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(
|
||||||
true,
|
true,
|
||||||
id,
|
id,
|
||||||
(name: string) => resolvedName = name,
|
(name: string) => (resolvedName = name),
|
||||||
playlistFolder,
|
playlistFolder,
|
||||||
trackId,
|
trackId,
|
||||||
increasePlaylistProgress,
|
increasePlaylistProgress,
|
||||||
@ -190,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
|
||||||
}`;
|
}`;
|
||||||
@ -214,7 +231,8 @@ 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}`,
|
||||||
);
|
);
|
||||||
@ -223,7 +241,8 @@ async function downloadSongUnsafe(
|
|||||||
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
|
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
|
||||||
let presetSetting: Preset;
|
let presetSetting: Preset;
|
||||||
if (selectedPreset === 'Custom') {
|
if (selectedPreset === 'Custom') {
|
||||||
presetSetting = config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
presetSetting =
|
||||||
|
config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
||||||
} else if (selectedPreset === 'Source') {
|
} else if (selectedPreset === 'Source') {
|
||||||
presetSetting = DefaultPresetList['Source'];
|
presetSetting = DefaultPresetList['Source'];
|
||||||
} else {
|
} else {
|
||||||
@ -240,7 +259,9 @@ async function downloadSongUnsafe(
|
|||||||
|
|
||||||
let targetFileExtension: string;
|
let targetFileExtension: string;
|
||||||
if (!presetSetting?.extension) {
|
if (!presetSetting?.extension) {
|
||||||
targetFileExtension = YoutubeFormatList.find((it) => it.itag === format.itag)?.container ?? 'mp3';
|
targetFileExtension =
|
||||||
|
YoutubeFormatList.find((it) => it.itag === format.itag)?.container ??
|
||||||
|
'mp3';
|
||||||
} else {
|
} else {
|
||||||
targetFileExtension = presetSetting?.extension ?? 'mp3';
|
targetFileExtension = presetSetting?.extension ?? 'mp3';
|
||||||
}
|
}
|
||||||
@ -285,7 +306,11 @@ async function downloadSongUnsafe(
|
|||||||
if (targetFileExtension !== 'mp3') {
|
if (targetFileExtension !== 'mp3') {
|
||||||
createWriteStream(filePath).write(fileBuffer);
|
createWriteStream(filePath).write(fileBuffer);
|
||||||
} else {
|
} else {
|
||||||
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
|
const buffer = await writeID3(
|
||||||
|
Buffer.from(fileBuffer),
|
||||||
|
metadata,
|
||||||
|
sendFeedback,
|
||||||
|
);
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
writeFileSync(filePath, buffer);
|
writeFileSync(filePath, buffer);
|
||||||
}
|
}
|
||||||
@ -303,8 +328,7 @@ async function iterableStreamToTargetFile(
|
|||||||
presetFfmpegArgs: 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;
|
||||||
@ -372,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 = {};
|
||||||
@ -425,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'));
|
||||||
@ -440,11 +468,19 @@ 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: Playlist;
|
let playlist: Playlist;
|
||||||
|
const items: YTNodes.MusicResponsiveListItem[] = [];
|
||||||
try {
|
try {
|
||||||
playlist = await yt.music.getPlaylist(playlistId);
|
playlist = await yt.music.getPlaylist(playlistId);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@ -453,26 +489,29 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
sendError(new Error('Playlist is empty'));
|
sendError(new Error('Playlist is empty'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = playlist.items!.as(YTNodes.MusicResponsiveListItem);
|
const normalPlaylistTitle = playlist.header?.title?.text;
|
||||||
|
const playlistTitle =
|
||||||
|
normalPlaylistTitle ??
|
||||||
|
playlist.page.contents_memo
|
||||||
|
?.get('MusicResponsiveListItemFlexColumn')
|
||||||
|
?.at(2)
|
||||||
|
?.as(YTNodes.MusicResponsiveListItemFlexColumn)?.title?.text ??
|
||||||
|
'NO_TITLE';
|
||||||
|
const isAlbum = !normalPlaylistTitle;
|
||||||
|
|
||||||
|
while (playlist.has_continuation) {
|
||||||
|
playlist = await playlist.getContinuation();
|
||||||
|
if (playlist?.items) {
|
||||||
|
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (items.length === 1) {
|
if (items.length === 1) {
|
||||||
sendFeedback('Playlist has only one item, downloading it directly');
|
sendFeedback('Playlist has only one item, downloading it directly');
|
||||||
await downloadSongFromId(items.at(0)!.id!);
|
await downloadSongFromId(items.at(0)!.id!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
|
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
|
||||||
if (!is.macOS()) {
|
if (!is.macOS()) {
|
||||||
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
|
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
|
||||||
@ -528,7 +567,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
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}`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -562,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);
|
||||||
}
|
}
|
||||||
@ -572,15 +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!),
|
||||||
album: info.player_overlays?.browser_media_session?.as(YTNodes.BrowserMediaSession).album?.text,
|
album: info.player_overlays?.browser_media_session?.as(
|
||||||
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
|
YTNodes.BrowserMediaSession,
|
||||||
|
).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!,
|
||||||
});
|
});
|
||||||
|
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 |
@ -19,6 +19,7 @@ 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');
|
||||||
@ -98,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) => {
|
||||||
@ -130,7 +131,7 @@ export default async () => {
|
|||||||
menu.style.visibility = 'hidden';
|
menu.style.visibility = 'hidden';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||||
};
|
};
|
||||||
await updateMenu();
|
await updateMenu();
|
||||||
|
|
||||||
@ -138,12 +139,16 @@ export default async () => {
|
|||||||
|
|
||||||
ipcRenderer.on('refreshMenu', () => updateMenu());
|
ipcRenderer.on('refreshMenu', () => updateMenu());
|
||||||
ipcRenderer.on('window-maximize', () => {
|
ipcRenderer.on('window-maximize', () => {
|
||||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||||
maximizeButton.appendChild(unmaximize);
|
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||||
|
maximizeButton.appendChild(unmaximize);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ipcRenderer.on('window-unmaximize', () => {
|
ipcRenderer.on('window-unmaximize', () => {
|
||||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||||
maximizeButton.appendChild(maximize);
|
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[],
|
||||||
|
];
|
||||||