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
|
||||
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- label: I understand that **th-ch/youtube-music has NO affiliation with Google or YouTube**
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: YouTube Music (Application) Version
|
||||
description: |
|
||||
What version of YouTube Music Application are you using?
|
||||
What version of the YouTube Music Application are you using?
|
||||
|
||||
Note: Please check if this issue is reproducible with the latest stable release.
|
||||
placeholder: 2.0.0
|
||||
@ -36,7 +38,7 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
description: What operating system version are you using? On Windows, click the Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
105
.github/workflows/build.yml
vendored
@ -20,59 +20,63 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Only rollup build without release if it is a fork
|
||||
- name: Rollup Build
|
||||
if: github.repository == 'th-ch/youtube-music' && github.event_name == 'pull_request'
|
||||
run: |
|
||||
pnpm build
|
||||
|
||||
# Build and release if it's the main repository and is not pull-request
|
||||
- name: Build and release on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pnpm release:mac
|
||||
|
||||
- name: Build and release on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pnpm release:linux
|
||||
|
||||
- name: Build and release on Windows
|
||||
if: startsWith(matrix.os, 'windows') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pnpm release:win
|
||||
|
||||
- name: Test
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
uses: coactions/setup-xvfb@v1
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
with:
|
||||
run: npm run test
|
||||
run: pnpm test:debug
|
||||
|
||||
# Build and release if it's the main repository
|
||||
- name: Build and release on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:mac
|
||||
|
||||
- name: Build and release on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:linux
|
||||
|
||||
- name: Build and release on Windows
|
||||
if: startsWith(matrix.os, 'windows') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:win
|
||||
|
||||
# Only build without release if it is a fork
|
||||
- name: Build on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && github.repository != 'th-ch/youtube-music'
|
||||
run: |
|
||||
npm run build:mac
|
||||
|
||||
- name: Build on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && github.repository != 'th-ch/youtube-music'
|
||||
run: |
|
||||
npm run build:linux
|
||||
|
||||
- name: Build on Windows
|
||||
if: startsWith(matrix.os, 'windows') && github.repository != 'th-ch/youtube-music'
|
||||
run: |
|
||||
npm run build:win
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@ -84,14 +88,27 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get version
|
||||
run: |
|
||||
@ -132,7 +149,7 @@ jobs:
|
||||
- name: Update changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
run: |
|
||||
npm run changelog
|
||||
pnpm changelog
|
||||
|
||||
- name: Commit changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
|
||||
5
.prettierrc
Normal file
@ -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.
|
||||
|
||||
#### [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)
|
||||
|
||||
10137
package-lock.json
generated
86
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
@ -87,39 +87,58 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run build && playwright test",
|
||||
"test:debug": "DEBUG=pw:browser* npm run build && playwright test",
|
||||
"test": "playwright test",
|
||||
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||
"rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
|
||||
"rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
|
||||
"build": "npm run rollup:preload && npm run rollup:main",
|
||||
"start": "npm run build && electron ./dist/index.js",
|
||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 npm run start",
|
||||
"build": "yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main",
|
||||
"start": "yarpm-pnpm run build && electron ./dist/index.js",
|
||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start",
|
||||
"postinstall": "patch-package",
|
||||
"clean": "del-cli dist && del-cli pack",
|
||||
"dist": "npm run clean && npm run build && electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "npm run clean && npm run build && electron-builder --linux -p never",
|
||||
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "npm run clean && npm run build && electron-builder --win -p never",
|
||||
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis-web:x64 -p never",
|
||||
"dist": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p never",
|
||||
"dist:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p never",
|
||||
"dist:win:x64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win nsis-web:x64 -p never",
|
||||
"lint": "eslint .",
|
||||
"changelog": "auto-changelog",
|
||||
"release:linux": "npm run clean && npm run build && electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "npm run clean && npm run build && electron-builder --mac -p always",
|
||||
"release:win": "npm run clean && npm run build && electron-builder --win -p always",
|
||||
"release:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac -p always",
|
||||
"release:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p always",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"rollup": "4.1.4",
|
||||
"node-gyp": "9.4.0",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "2.7.0",
|
||||
"@electron/universal": "1.4.2",
|
||||
"@babel/runtime": "7.23.2"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "4.1.4",
|
||||
"node-gyp": "9.4.0",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "2.7.0",
|
||||
"@electron/universal": "1.4.2",
|
||||
"@babel/runtime": "7.23.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "1.26.8",
|
||||
"@cliqz/adblocker-electron-preload": "1.26.8",
|
||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||
"@ffmpeg.wasm/main": "0.12.0",
|
||||
"@foobar404/wave": "2.0.4",
|
||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||
"@jellybrick/mpris-service": "2.1.4",
|
||||
"@xhayper/discord-rpc": "1.0.23",
|
||||
"@xhayper/discord-rpc": "1.0.24",
|
||||
"async-mutex": "0.4.0",
|
||||
"butterchurn": "3.0.0-beta.4",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
@ -141,20 +160,12 @@
|
||||
"simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "6.4.1",
|
||||
"ytpl": "2.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "4.0.2",
|
||||
"node-gyp": "9.4.0",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "2.7.0",
|
||||
"@electron/universal": "1.4.2",
|
||||
"@babel/runtime": "7.23.2"
|
||||
"youtubei.js": "6.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@milahu/patch-package": "6.4.14",
|
||||
"@playwright/test": "1.39.0",
|
||||
"@rollup/plugin-commonjs": "25.0.5",
|
||||
"@rollup/plugin-commonjs": "25.0.7",
|
||||
"@rollup/plugin-image": "3.0.3",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
@ -162,31 +173,34 @@
|
||||
"@rollup/plugin-typescript": "11.1.5",
|
||||
"@rollup/plugin-wasm": "6.2.2",
|
||||
"@total-typescript/ts-reset": "0.5.1",
|
||||
"@types/electron-localshortcut": "3.1.1",
|
||||
"@types/howler": "2.2.9",
|
||||
"@types/html-to-text": "9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.5",
|
||||
"@types/electron-localshortcut": "3.1.2",
|
||||
"@types/howler": "2.2.10",
|
||||
"@types/html-to-text": "9.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||
"auto-changelog": "2.4.0",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "5.1.0",
|
||||
"electron": "27.0.0",
|
||||
"electron": "27.0.1",
|
||||
"electron-builder": "24.6.4",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"eslint": "8.51.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "8.0.0",
|
||||
"playwright": "1.39.0",
|
||||
"rollup": "4.0.2",
|
||||
"rollup": "4.1.4",
|
||||
"rollup-plugin-copy": "3.5.0",
|
||||
"rollup-plugin-import-css": "3.3.5",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.2.2",
|
||||
"yarpm": "1.2.0"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"hideCredit": true,
|
||||
"package": true,
|
||||
"unreleased": true,
|
||||
"output": "changelog.md"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@8.9.2"
|
||||
}
|
||||
|
||||
5618
pnpm-lock.yaml
generated
Normal file
25
readme.md
@ -16,7 +16,7 @@
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="web/youtube-music.svg" width="400" height="100">
|
||||
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -79,6 +79,10 @@ winget install th-ch.YouTubeMusic
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
|
||||
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
|
||||
|
||||
- **Ambient Mode**: Applies a lighting effect by casting gentle colors from the video, into your screen’s background.
|
||||
|
||||
- **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
|
||||
volume of the softest parts)
|
||||
|
||||
@ -111,6 +115,8 @@ winget install th-ch.YouTubeMusic
|
||||
|
||||
- [**Last.fm**](https://www.last.fm/): Scrobbles support
|
||||
|
||||
- **Lumia Stream**: Adds [Lumia Stream](https://lumiastream.com/) support
|
||||
|
||||
- **Lyrics Genius**: Adds lyrics support for most songs
|
||||
|
||||
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||
@ -178,8 +184,8 @@ Some predefined themes are available in https://github.com/kerichdev/themes-for-
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
npm ci
|
||||
npm run start
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Build your own plugins
|
||||
@ -266,12 +272,13 @@ export default () => {
|
||||
## Build
|
||||
|
||||
1. Clone the repo
|
||||
2. Run `npm i` to install dependencies
|
||||
3. Run `npm run build:OS`
|
||||
2. Follow [this guide](https://pnpm.io/installation) to install `pnpm`
|
||||
3. Run `pnpm install --frozen-lockfile` to install dependencies
|
||||
4. Run `pnpm build:OS`
|
||||
|
||||
- `npm run dist:win` - Windows
|
||||
- `npm run dist:linux` - Linux
|
||||
- `npm run dist:mac` - MacOS
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux
|
||||
- `pnpm dist:mac` - MacOS
|
||||
|
||||
Builds the app for macOS, Linux, and Windows,
|
||||
using [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
@ -279,7 +286,7 @@ using [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Uses [Playwright](https://playwright.dev/) to test the app.
|
||||
|
||||
@ -18,7 +18,7 @@ export default defineConfig({
|
||||
nodeResolvePlugin({
|
||||
browser: false,
|
||||
preferBuiltins: true,
|
||||
exportConditions: ['node', 'default', 'module', 'import'] ,
|
||||
exportConditions: ['node', 'default', 'module', 'import'],
|
||||
}),
|
||||
commonjs({
|
||||
ignoreDynamicRequires: true,
|
||||
@ -34,7 +34,7 @@ export default defineConfig({
|
||||
css(),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'error.html', dest: 'dist/' },
|
||||
{ src: 'src/error.html', dest: 'dist/' },
|
||||
{ src: 'assets', dest: 'dist/' },
|
||||
],
|
||||
}),
|
||||
@ -47,18 +47,14 @@ export default defineConfig({
|
||||
setTimeout(() => process.exit(0));
|
||||
}
|
||||
},
|
||||
name: 'force-close'
|
||||
name: 'force-close',
|
||||
},
|
||||
],
|
||||
input: './index.ts',
|
||||
input: './src/index.ts',
|
||||
output: {
|
||||
format: 'cjs',
|
||||
name: '[name].js',
|
||||
dir: './dist',
|
||||
},
|
||||
external: [
|
||||
'electron',
|
||||
'custom-electron-prompt',
|
||||
...builtinModules,
|
||||
],
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
});
|
||||
|
||||
@ -41,18 +41,14 @@ export default defineConfig({
|
||||
setTimeout(() => process.exit(0));
|
||||
}
|
||||
},
|
||||
name: 'force-close'
|
||||
name: 'force-close',
|
||||
},
|
||||
],
|
||||
input: './preload.ts',
|
||||
input: './src/preload.ts',
|
||||
output: {
|
||||
format: 'cjs',
|
||||
name: '[name].js',
|
||||
dir: './dist',
|
||||
},
|
||||
external: [
|
||||
'electron',
|
||||
'custom-electron-prompt',
|
||||
...builtinModules,
|
||||
],
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
});
|
||||
|
||||
@ -76,7 +76,7 @@ const defaultConfig = {
|
||||
'adblocker': {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
blocker: blockers.WithBlocklists as string,
|
||||
blocker: blockers.InPlayer as string,
|
||||
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
||||
disableDefaultLists: false,
|
||||
},
|
||||
@ -125,6 +125,7 @@ const defaultConfig = {
|
||||
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
|
||||
*/
|
||||
enabled: false,
|
||||
hideDOMWindowControls: false,
|
||||
},
|
||||
'last-fm': {
|
||||
enabled: false,
|
||||
@ -13,6 +13,7 @@ import crossfadeMenu from './plugins/crossfade/menu';
|
||||
import disableAutoplayMenu from './plugins/disable-autoplay/menu';
|
||||
import discordMenu from './plugins/discord/menu';
|
||||
import downloaderMenu from './plugins/downloader/menu';
|
||||
import inAppMenuTitlebarMenu from './plugins/in-app-menu/menu';
|
||||
import lyricsGeniusMenu from './plugins/lyrics-genius/menu';
|
||||
import notificationsMenu from './plugins/notifications/menu';
|
||||
import pictureInPictureMenu from './plugins/picture-in-picture/menu';
|
||||
@ -36,6 +37,7 @@ const pluginMenus = {
|
||||
'crossfade': crossfadeMenu,
|
||||
'discord': discordMenu,
|
||||
'downloader': downloaderMenu,
|
||||
'in-app-menu': inAppMenuTitlebarMenu,
|
||||
'lyrics-genius': lyricsGeniusMenu,
|
||||
'notifications': notificationsMenu,
|
||||
'picture-in-picture': pictureInPictureMenu,
|
||||
0
navigation.d.ts → src/navigation.d.ts
vendored
@ -24,12 +24,7 @@ export const loadAdBlockerEngine = async (
|
||||
disableDefaultLists: boolean | unknown[] = false,
|
||||
) => {
|
||||
// Only use cache if no additional blocklists are passed
|
||||
let cacheDirectory: string;
|
||||
if (app.isPackaged) {
|
||||
cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
|
||||
} else {
|
||||
cacheDirectory = path.resolve(__dirname, 'adblock_cache');
|
||||
}
|
||||
const cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
|
||||
if (!fs.existsSync(cacheDirectory)) {
|
||||
fs.mkdirSync(cacheDirectory);
|
||||
}
|
||||
@ -156,6 +156,14 @@ export default (
|
||||
// Song information changed, so lets update the rich presence
|
||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
|
||||
if (songInfo.title.length < 2) {
|
||||
songInfo.title += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
if (songInfo.artist.length < 2) {
|
||||
songInfo.artist += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
|
||||
const activityInfo: SetActivity = {
|
||||
details: songInfo.title,
|
||||
state: songInfo.artist,
|
||||
@ -1,16 +1,32 @@
|
||||
import { createWriteStream, existsSync, mkdirSync, writeFileSync, } from 'node:fs';
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
|
||||
import { ClientType, Innertube, UniversalCache, Utils, YTNodes } from 'youtubei.js';
|
||||
import {
|
||||
ClientType,
|
||||
Innertube,
|
||||
UniversalCache,
|
||||
Utils,
|
||||
YTNodes,
|
||||
} from 'youtubei.js';
|
||||
import is from 'electron-is';
|
||||
import filenamify from 'filenamify';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { createFFmpeg } from '@ffmpeg.wasm/main';
|
||||
import NodeID3, { TagConstants } from 'node-id3';
|
||||
|
||||
import { cropMaxWidth, getFolder, sendFeedback as sendFeedback_, setBadge } from './utils';
|
||||
import {
|
||||
cropMaxWidth,
|
||||
getFolder,
|
||||
sendFeedback as sendFeedback_,
|
||||
setBadge,
|
||||
} from './utils';
|
||||
import config from './config';
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
|
||||
|
||||
@ -34,10 +50,8 @@ type CustomSongInfo = SongInfo & { trackId?: string };
|
||||
|
||||
const ffmpeg = createFFmpeg({
|
||||
log: false,
|
||||
logger() {
|
||||
}, // Console.log,
|
||||
progress() {
|
||||
}, // Console.log,
|
||||
logger() {}, // Console.log,
|
||||
progress() {}, // Console.log,
|
||||
});
|
||||
const ffmpegMutex = new Mutex();
|
||||
|
||||
@ -65,9 +79,13 @@ const sendError = (error: Error, source?: string) => {
|
||||
};
|
||||
|
||||
export const getCookieFromWindow = async (win: BrowserWindow) => {
|
||||
return (await win.webContents.session.cookies.get({ url: 'https://music.youtube.com' })).map((it) =>
|
||||
it.name + '=' + it.value + ';'
|
||||
).join('');
|
||||
return (
|
||||
await win.webContents.session.cookies.get({
|
||||
url: 'https://music.youtube.com',
|
||||
})
|
||||
)
|
||||
.map((it) => it.name + '=' + it.value + ';')
|
||||
.join('');
|
||||
};
|
||||
|
||||
export default async (win_: BrowserWindow) => {
|
||||
@ -78,12 +96,13 @@ export default async (win_: BrowserWindow) => {
|
||||
cache: new UniversalCache(false),
|
||||
cookie: await getCookieFromWindow(win),
|
||||
generate_session_locally: true,
|
||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url =
|
||||
typeof input === 'string' ?
|
||||
new URL(input) :
|
||||
input instanceof URL ?
|
||||
input : new URL(input.url);
|
||||
typeof input === 'string'
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL(input.url);
|
||||
|
||||
if (init?.body && !init.method) {
|
||||
init.method = 'POST';
|
||||
@ -95,7 +114,7 @@ export default async (win_: BrowserWindow) => {
|
||||
);
|
||||
|
||||
return net.fetch(request, init);
|
||||
}
|
||||
}) as typeof fetch,
|
||||
});
|
||||
ipcMain.on('download-song', (_, url: string) => downloadSong(url));
|
||||
ipcMain.on('video-src-changed', (_, data: GetPlayerResponse) => {
|
||||
@ -110,15 +129,14 @@ export async function downloadSong(
|
||||
url: string,
|
||||
playlistFolder: string | undefined = undefined,
|
||||
trackId: string | undefined = undefined,
|
||||
increasePlaylistProgress: (value: number) => void = () => {
|
||||
},
|
||||
increasePlaylistProgress: (value: number) => void = () => {},
|
||||
) {
|
||||
let resolvedName;
|
||||
try {
|
||||
await downloadSongUnsafe(
|
||||
false,
|
||||
url,
|
||||
(name: string) => resolvedName = name,
|
||||
(name: string) => (resolvedName = name),
|
||||
playlistFolder,
|
||||
trackId,
|
||||
increasePlaylistProgress,
|
||||
@ -132,15 +150,14 @@ export async function downloadSongFromId(
|
||||
id: string,
|
||||
playlistFolder: string | undefined = undefined,
|
||||
trackId: string | undefined = undefined,
|
||||
increasePlaylistProgress: (value: number) => void = () => {
|
||||
},
|
||||
increasePlaylistProgress: (value: number) => void = () => {},
|
||||
) {
|
||||
let resolvedName;
|
||||
try {
|
||||
await downloadSongUnsafe(
|
||||
true,
|
||||
id,
|
||||
(name: string) => resolvedName = name,
|
||||
(name: string) => (resolvedName = name),
|
||||
playlistFolder,
|
||||
trackId,
|
||||
increasePlaylistProgress,
|
||||
@ -190,8 +207,8 @@ async function downloadSongUnsafe(
|
||||
|
||||
metadata.trackId = trackId;
|
||||
|
||||
const dir
|
||||
= playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
|
||||
const dir =
|
||||
playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
|
||||
const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${
|
||||
metadata.title
|
||||
}`;
|
||||
@ -214,7 +231,8 @@ async function downloadSongUnsafe(
|
||||
}
|
||||
|
||||
if (playabilityStatus.status === 'UNPLAYABLE') {
|
||||
const errorScreen = playabilityStatus.error_screen as PlayerErrorMessage | null;
|
||||
const errorScreen =
|
||||
playabilityStatus.error_screen as PlayerErrorMessage | null;
|
||||
throw new Error(
|
||||
`[${playabilityStatus.status}] ${errorScreen?.reason.text}: ${errorScreen?.subreason.text}`,
|
||||
);
|
||||
@ -223,7 +241,8 @@ async function downloadSongUnsafe(
|
||||
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
|
||||
let presetSetting: Preset;
|
||||
if (selectedPreset === 'Custom') {
|
||||
presetSetting = config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
||||
presetSetting =
|
||||
config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
||||
} else if (selectedPreset === 'Source') {
|
||||
presetSetting = DefaultPresetList['Source'];
|
||||
} else {
|
||||
@ -240,7 +259,9 @@ async function downloadSongUnsafe(
|
||||
|
||||
let targetFileExtension: string;
|
||||
if (!presetSetting?.extension) {
|
||||
targetFileExtension = YoutubeFormatList.find((it) => it.itag === format.itag)?.container ?? 'mp3';
|
||||
targetFileExtension =
|
||||
YoutubeFormatList.find((it) => it.itag === format.itag)?.container ??
|
||||
'mp3';
|
||||
} else {
|
||||
targetFileExtension = presetSetting?.extension ?? 'mp3';
|
||||
}
|
||||
@ -285,7 +306,11 @@ async function downloadSongUnsafe(
|
||||
if (targetFileExtension !== 'mp3') {
|
||||
createWriteStream(filePath).write(fileBuffer);
|
||||
} else {
|
||||
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
|
||||
const buffer = await writeID3(
|
||||
Buffer.from(fileBuffer),
|
||||
metadata,
|
||||
sendFeedback,
|
||||
);
|
||||
if (buffer) {
|
||||
writeFileSync(filePath, buffer);
|
||||
}
|
||||
@ -303,8 +328,7 @@ async function iterableStreamToTargetFile(
|
||||
presetFfmpegArgs: string[],
|
||||
contentLength: number,
|
||||
sendFeedback: (str: string, value?: number) => void,
|
||||
increasePlaylistProgress: (value: number) => void = () => {
|
||||
},
|
||||
increasePlaylistProgress: (value: number) => void = () => {},
|
||||
) {
|
||||
const chunks = [];
|
||||
let downloaded = 0;
|
||||
@ -372,7 +396,11 @@ const getCoverBuffer = cache(async (url: string) => {
|
||||
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
|
||||
});
|
||||
|
||||
async function writeID3(buffer: Buffer, metadata: CustomSongInfo, sendFeedback: (str: string, value?: number) => void) {
|
||||
async function writeID3(
|
||||
buffer: Buffer,
|
||||
metadata: CustomSongInfo,
|
||||
sendFeedback: (str: string, value?: number) => void,
|
||||
) {
|
||||
try {
|
||||
sendFeedback('Writing ID3 tags...');
|
||||
const tags: NodeID3.Tags = {};
|
||||
@ -425,10 +453,10 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playlistId
|
||||
= getPlaylistID(givenUrl)
|
||||
|| getPlaylistID(new URL(win.webContents.getURL()))
|
||||
|| getPlaylistID(new URL(playingUrl));
|
||||
const playlistId =
|
||||
getPlaylistID(givenUrl) ||
|
||||
getPlaylistID(new URL(win.webContents.getURL())) ||
|
||||
getPlaylistID(new URL(playingUrl));
|
||||
|
||||
if (!playlistId) {
|
||||
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}'`);
|
||||
sendFeedback('Getting playlist info…');
|
||||
let playlist: Playlist;
|
||||
const items: YTNodes.MusicResponsiveListItem[] = [];
|
||||
try {
|
||||
playlist = await yt.music.getPlaylist(playlistId);
|
||||
if (playlist?.items) {
|
||||
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
sendError(
|
||||
Error(`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(error)}`),
|
||||
Error(
|
||||
`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(
|
||||
error,
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -453,26 +489,29 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
sendError(new Error('Playlist is empty'));
|
||||
}
|
||||
|
||||
const items = playlist.items!.as(YTNodes.MusicResponsiveListItem);
|
||||
const normalPlaylistTitle = playlist.header?.title?.text;
|
||||
const playlistTitle =
|
||||
normalPlaylistTitle ??
|
||||
playlist.page.contents_memo
|
||||
?.get('MusicResponsiveListItemFlexColumn')
|
||||
?.at(2)
|
||||
?.as(YTNodes.MusicResponsiveListItemFlexColumn)?.title?.text ??
|
||||
'NO_TITLE';
|
||||
const isAlbum = !normalPlaylistTitle;
|
||||
|
||||
while (playlist.has_continuation) {
|
||||
playlist = await playlist.getContinuation();
|
||||
if (playlist?.items) {
|
||||
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 1) {
|
||||
sendFeedback('Playlist has only one item, downloading it directly');
|
||||
await downloadSongFromId(items.at(0)!.id!);
|
||||
return;
|
||||
}
|
||||
|
||||
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: ' ' });
|
||||
if (!is.macOS()) {
|
||||
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
|
||||
@ -528,7 +567,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
increaseProgress,
|
||||
).catch((error) =>
|
||||
sendError(
|
||||
new Error(`Error downloading "${song.author!.name} - ${song.title!}":\n ${error}`)
|
||||
new Error(
|
||||
`Error downloading "${
|
||||
song.author!.name
|
||||
} - ${song.title!}":\n ${error}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -562,8 +605,8 @@ function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
|
||||
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
|
||||
|
||||
const getPlaylistID = (aURL: URL) => {
|
||||
const result
|
||||
= aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
|
||||
const result =
|
||||
aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
|
||||
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
|
||||
return result.slice(INVALID_PLAYLIST_MODIFIER.length);
|
||||
}
|
||||
@ -572,15 +615,18 @@ const getPlaylistID = (aURL: URL) => {
|
||||
};
|
||||
|
||||
const getVideoId = (url: URL | string): string | null => {
|
||||
return (new URL(url)).searchParams.get('v');
|
||||
return new URL(url).searchParams.get('v');
|
||||
};
|
||||
|
||||
const getMetadata = (info: TrackInfo): CustomSongInfo => ({
|
||||
videoId: info.basic_info.id!,
|
||||
title: cleanupName(info.basic_info.title!),
|
||||
artist: cleanupName(info.basic_info.author!),
|
||||
album: info.player_overlays?.browser_media_session?.as(YTNodes.BrowserMediaSession).album?.text,
|
||||
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
|
||||
album: info.player_overlays?.browser_media_session?.as(
|
||||
YTNodes.BrowserMediaSession,
|
||||
).album?.text,
|
||||
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))
|
||||
?.url,
|
||||
views: info.basic_info.view_count!,
|
||||
songDuration: info.basic_info.duration!,
|
||||
});
|
||||
|
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;
|
||||
|
||||
export default async () => {
|
||||
const hideDOMWindowControls = config.get('plugins.in-app-menu.hideDOMWindowControls');
|
||||
let hideMenu = config.get('options.hideMenu');
|
||||
const titleBar = document.createElement('title-bar');
|
||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||
@ -98,7 +99,7 @@ export default async () => {
|
||||
titleBar.appendChild(windowControlsContainer);
|
||||
};
|
||||
|
||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||
|
||||
if (navBar) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
@ -130,7 +131,7 @@ export default async () => {
|
||||
menu.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||
};
|
||||
await updateMenu();
|
||||
|
||||
@ -138,12 +139,16 @@ export default async () => {
|
||||
|
||||
ipcRenderer.on('refreshMenu', () => updateMenu());
|
||||
ipcRenderer.on('window-maximize', () => {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
}
|
||||
});
|
||||
ipcRenderer.on('window-unmaximize', () => {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(maximize);
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
}
|
||||
});
|
||||
|
||||
if (isEnabled('picture-in-picture')) {
|
||||
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[],
|
||||
];
|
||||