Compare commits
70 Commits
v2.0.4
...
legacy/htt
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e97699e6c | |||
| a810621762 | |||
| a41db79c35 | |||
| 87786d9aef | |||
| 22f5866050 | |||
| 04894fbcf5 | |||
| c17c624ba4 | |||
| bfe7249df8 | |||
| 13c570efe9 | |||
| b299846f0f | |||
| 59e9289d27 | |||
| 8dc29caa1b | |||
| 7fedf88654 | |||
| 5da0202425 | |||
| 6288d0b171 | |||
| 4248d20e8e | |||
| 0b413492ad | |||
| dc73561c8a | |||
| 949a2f6428 | |||
| bceaa05197 | |||
| 776cdac30d | |||
| 4333891cca | |||
| 8a89bbccf7 | |||
| fa4c69d228 | |||
| 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 |
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
|
||||
|
||||
111
.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
|
||||
uses: actions/setup-node@v3
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
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
|
||||
uses: actions/setup-node@v3
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get version
|
||||
run: |
|
||||
@ -117,7 +134,7 @@ jobs:
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
id: ${{ steps.get_draft_release.outputs.id }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
@ -132,7 +149,7 @@ jobs:
|
||||
- name: Update changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
run: |
|
||||
npm run changelog
|
||||
pnpm changelog
|
||||
|
||||
- name: Commit changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
|
||||
20
.github/workflows/winget-cla.yml
vendored
Normal file
@ -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
|
||||
with:
|
||||
identifier: th-ch.YouTubeMusic
|
||||
installers-regex: '^YouTube-Music-Setup-[\d\.]+\.exe$'
|
||||
installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'
|
||||
version: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
|
||||
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
68
changelog.md
@ -2,8 +2,76 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v2.2.0](https://github.com/th-ch/youtube-music/compare/v2.1.3...v2.2.0)
|
||||
|
||||
- feat(ambient-mode): add config for `ambient-mode` plugin [`#1349`](https://github.com/th-ch/youtube-music/pull/1349)
|
||||
- bump deps [`4248d20`](https://github.com/th-ch/youtube-music/commit/4248d20e8ef926ce7b1d07eb83743755a341d9f6)
|
||||
- Update changelog for v2.1.3 [`dc73561`](https://github.com/th-ch/youtube-music/commit/dc73561c8a8acfc8ba91aff2dc78e4267869f2fd)
|
||||
- Bump version to 2.2.0 [`6288d0b`](https://github.com/th-ch/youtube-music/commit/6288d0b171a65ea015922cdf3af6c7bd9a1f269b)
|
||||
|
||||
#### [v2.1.3](https://github.com/th-ch/youtube-music/compare/v2.1.2...v2.1.3)
|
||||
|
||||
> 23 October 2023
|
||||
|
||||
- fix: fixed bugs in downloader [`#1342`](https://github.com/th-ch/youtube-music/pull/1342)
|
||||
- feat(discord): rename `Listen Along` to `Play on YTM` [`#1341`](https://github.com/th-ch/youtube-music/issues/1341)
|
||||
- chore(deps): bump deps [`4333891`](https://github.com/th-ch/youtube-music/commit/4333891ccabe42aedf756fd48618be715db13262)
|
||||
- Update changelog for v2.1.2 [`fa4c69d`](https://github.com/th-ch/youtube-music/commit/fa4c69d228d4e06a7858e2b22fcdfa075a8ca766)
|
||||
- fix(store): fix listenAlong statement [`bceaa05`](https://github.com/th-ch/youtube-music/commit/bceaa05197d47a4a4bbd22e767d1e4d6ec277514)
|
||||
|
||||
#### [v2.1.2](https://github.com/th-ch/youtube-music/compare/v2.1.1...v2.1.2)
|
||||
|
||||
> 19 October 2023
|
||||
|
||||
- feat(in-app-menu): add an option to hide the window controls [`#1335`](https://github.com/th-ch/youtube-music/pull/1335)
|
||||
- fix: fixed an issue where the album name was missing [`#1334`](https://github.com/th-ch/youtube-music/pull/1334)
|
||||
- chore(deps): update dependency electron to v27.0.1 [`#1331`](https://github.com/th-ch/youtube-music/pull/1331)
|
||||
- fix: fixed an issue where only the first 100 songs in a playlist were downloaded [`#1329`](https://github.com/th-ch/youtube-music/pull/1329)
|
||||
- Updated readme plugins list [`#1326`](https://github.com/th-ch/youtube-music/pull/1326)
|
||||
- QOL: Move source code under the src directory. [`#1318`](https://github.com/th-ch/youtube-music/pull/1318)
|
||||
- feat: migrate from `npm` to `pnpm` [`#1316`](https://github.com/th-ch/youtube-music/pull/1316)
|
||||
- fix: fix unresponsive (fix #1325) [`#1325`](https://github.com/th-ch/youtube-music/issues/1325)
|
||||
- fix(blocker): remove the `app.isPackaged` check (fix #1315) [`#1315`](https://github.com/th-ch/youtube-music/issues/1315)
|
||||
- fix(discord): `Discord RPC fails if a song's title is only one character` (fix #1314) [`#1314`](https://github.com/th-ch/youtube-music/issues/1314)
|
||||
- chore(deps): Bump @rollup/plugin-commonjs, pnpm version, Remove ytpl [`9705f84`](https://github.com/th-ch/youtube-music/commit/9705f8489d7bf262bfd8b15ab84c2d3485f10eae)
|
||||
- chore(deps): Bump rollup, @xhayper/discord-rpc version [`00a3e8d`](https://github.com/th-ch/youtube-music/commit/00a3e8d35ec335e1913be19f30ae09dbe0b7acdd)
|
||||
- chore(deps): update dependency rollup to v4.1.4 [`6774d54`](https://github.com/th-ch/youtube-music/commit/6774d54f5eca432edc2e11743d9d1b1c2fda9ac8)
|
||||
|
||||
#### [v2.1.1](https://github.com/th-ch/youtube-music/compare/v2.1.0...v2.1.1)
|
||||
|
||||
> 14 October 2023
|
||||
|
||||
- hotfix(downloader): can't get an album title (fix #1313) [`#1313`](https://github.com/th-ch/youtube-music/issues/1313)
|
||||
- Update changelog for v2.1.0 [`92cab89`](https://github.com/th-ch/youtube-music/commit/92cab89d17175741e60e65ea61633e23ebdc1f45)
|
||||
- Bump version to 2.1.1 [`3bb5bc2`](https://github.com/th-ch/youtube-music/commit/3bb5bc2ca1856f4e222ee1e01e865f1ab804fdba)
|
||||
- Add "about" menu to show app version [`21c45fa`](https://github.com/th-ch/youtube-music/commit/21c45faf2043cf72a7c14d5cf6c8d848d0448528)
|
||||
|
||||
#### [v2.1.0](https://github.com/th-ch/youtube-music/compare/v2.0.4...v2.1.0)
|
||||
|
||||
> 14 October 2023
|
||||
|
||||
- feat(downloader): Added support for audio format auto-detection [`#1310`](https://github.com/th-ch/youtube-music/pull/1310)
|
||||
- feat(in-app-menu): enable in-app-menu by default (in Windows) [`#1311`](https://github.com/th-ch/youtube-music/pull/1311)
|
||||
- fix: winget publish [`#1307`](https://github.com/th-ch/youtube-music/pull/1307)
|
||||
- hotfix(downloader): fix invalid query selector (fix #1308) [`#1308`](https://github.com/th-ch/youtube-music/issues/1308)
|
||||
- chore(deps): bump dependencies [`3c6b3ae`](https://github.com/th-ch/youtube-music/commit/3c6b3aeff0aae32adb2f2ad9c091b0a9701d3c24)
|
||||
- chore(actions): create winget-cla.yml [`37181a7`](https://github.com/th-ch/youtube-music/commit/37181a7b5e2aa5bed6a36298eac3a66aac2762b8)
|
||||
- Update changelog for v2.0.4 [`e9398ad`](https://github.com/th-ch/youtube-music/commit/e9398adac34a8abb11801e32999a915a8be0ece6)
|
||||
|
||||
#### [v2.0.4](https://github.com/th-ch/youtube-music/compare/v2.0.3...v2.0.4)
|
||||
|
||||
> 12 October 2023
|
||||
|
||||
- hotfix(adblocker): fix `ipcRenderer.sendSync() with ...` [`#1301`](https://github.com/th-ch/youtube-music/pull/1301)
|
||||
- fix(downloader): Korean filename is broken on non-macOS devices [`#1297`](https://github.com/th-ch/youtube-music/pull/1297)
|
||||
- chore(deps): bump deps [`b6894dc`](https://github.com/th-ch/youtube-music/commit/b6894dca2974c63fa2945d3a4995665d11eb2a78)
|
||||
- fix: bump dependencies [`7aa970c`](https://github.com/th-ch/youtube-music/commit/7aa970cebc8e1407ff6937b402ba303e14c73efd)
|
||||
- fix(downloader): private playlist download [`1d5b299`](https://github.com/th-ch/youtube-music/commit/1d5b2997bd0c72c1c007c57b145509e4a8f77fef)
|
||||
|
||||
#### [v2.0.3](https://github.com/th-ch/youtube-music/compare/v2.0.2...v2.0.3)
|
||||
|
||||
> 10 October 2023
|
||||
|
||||
- feat(discord): add `Hide GitHub link Button` [`#1293`](https://github.com/th-ch/youtube-music/pull/1293)
|
||||
- feat(deps): bundle `youtubei.js` (temporary solution) [`#1292`](https://github.com/th-ch/youtube-music/pull/1292)
|
||||
- fix(mpris): fixed an issue where MPRIS information was incorrect [`#1291`](https://github.com/th-ch/youtube-music/pull/1291)
|
||||
|
||||
10137
package-lock.json
generated
103
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "2.0.4",
|
||||
"version": "2.2.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
@ -87,39 +87,57 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"postinstall": "patch-package",
|
||||
"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",
|
||||
"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",
|
||||
"changelog": "npx --yes auto-changelog",
|
||||
"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.2.0",
|
||||
"node-gyp": "10.0.1",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "1.4.4",
|
||||
"@babel/runtime": "7.23.2"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "4.2.0",
|
||||
"node-gyp": "10.0.1",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "1.4.4",
|
||||
"@babel/runtime": "7.23.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "1.26.8",
|
||||
"@cliqz/adblocker-electron": "1.26.10",
|
||||
"@cliqz/adblocker-electron-preload": "1.26.10",
|
||||
"@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 +159,11 @@
|
||||
"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.1"
|
||||
"youtubei.js": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.38.1",
|
||||
"@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-json": "6.0.1",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
@ -162,31 +171,33 @@
|
||||
"@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",
|
||||
"auto-changelog": "2.4.0",
|
||||
"@types/electron-localshortcut": "3.1.2",
|
||||
"@types/howler": "2.2.10",
|
||||
"@types/html-to-text": "9.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "5.1.0",
|
||||
"electron": "27.0.0",
|
||||
"electron": "27.0.3",
|
||||
"electron-builder": "24.6.4",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"eslint": "8.51.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint": "8.52.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "8.0.0",
|
||||
"playwright": "1.38.1",
|
||||
"rollup": "4.0.2",
|
||||
"node-gyp": "10.0.1",
|
||||
"playwright": "1.39.0",
|
||||
"rollup": "4.2.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",
|
||||
"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.10.2"
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
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,10 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
#song-video canvas.html5-blur-canvas{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
filter: blur(100px);
|
||||
}
|
||||
5506
pnpm-lock.yaml
generated
Normal file
32
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>
|
||||
|
||||
@ -38,7 +38,12 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
|
||||
|
||||
### MacOS
|
||||
|
||||
If you get an error "is damaged and can’t be opened." when launching the app, run the following in the Terminal:
|
||||
You can install the app using Homebrew:
|
||||
```bash
|
||||
brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb
|
||||
```
|
||||
|
||||
If you install the app manually and get an error "is damaged and can’t be opened." when launching the app, run the following in the Terminal:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
@ -79,6 +84,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 +120,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 +189,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 +277,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 +291,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],
|
||||
});
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { blockers } from '../plugins/adblocker/blocker-types';
|
||||
|
||||
import { DefaultPresetList } from '../plugins/downloader/types';
|
||||
|
||||
export interface WindowSizeConfig {
|
||||
width: number;
|
||||
height: number;
|
||||
@ -74,12 +76,21 @@ 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,
|
||||
},
|
||||
'album-color-theme': {},
|
||||
'ambient-mode': {},
|
||||
'ambient-mode': {
|
||||
enabled: false,
|
||||
quality: 50,
|
||||
buffer: 30,
|
||||
interpolationTime: 1500,
|
||||
blur: 100,
|
||||
size: 100,
|
||||
opacity: 1,
|
||||
fullscreen: false,
|
||||
},
|
||||
'audio-compressor': {},
|
||||
'blur-nav-bar': {},
|
||||
'bypass-age-restrictions': {},
|
||||
@ -105,20 +116,27 @@ const defaultConfig = {
|
||||
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
||||
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
|
||||
listenAlong: true, // Add a "listen along" button to rich presence
|
||||
playOnYouTubeMusic: true, // Add a "Play on YouTube Music" button to rich presence
|
||||
hideGitHubButton: false, // Disable the "View App On GitHub" button
|
||||
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
|
||||
},
|
||||
'downloader': {
|
||||
enabled: false,
|
||||
ffmpegArgs: ['-b:a', '256k'], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
|
||||
preset: 'mp3',
|
||||
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
||||
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined as number | undefined,
|
||||
},
|
||||
'http-api': {},
|
||||
'exponential-volume': {},
|
||||
'in-app-menu': {},
|
||||
'in-app-menu': {
|
||||
/**
|
||||
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
|
||||
*/
|
||||
enabled: false,
|
||||
hideDOMWindowControls: false,
|
||||
},
|
||||
'last-fm': {
|
||||
enabled: false,
|
||||
token: undefined as string | undefined, // Token used for authentication
|
||||
@ -22,8 +22,20 @@ export function isEnabled(plugin: string) {
|
||||
return pluginConfig !== undefined && pluginConfig.enabled;
|
||||
}
|
||||
|
||||
export function setOptions<T>(plugin: string, options: T) {
|
||||
/**
|
||||
* Set options for a plugin
|
||||
* @param plugin Plugin name
|
||||
* @param options Options to set
|
||||
* @param exclude Options to exclude from the options object
|
||||
*/
|
||||
export function setOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) {
|
||||
const plugins = store.get('plugins') as Record<string, T>;
|
||||
// HACK: This is a workaround for preventing changed options from being overwritten
|
||||
exclude.forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(options, key)) {
|
||||
delete options[key as keyof T];
|
||||
}
|
||||
});
|
||||
store.set('plugins', {
|
||||
...plugins,
|
||||
[plugin]: {
|
||||
@ -33,8 +45,8 @@ export function setOptions<T>(plugin: string, options: T) {
|
||||
});
|
||||
}
|
||||
|
||||
export function setMenuOptions<T>(plugin: string, options: T) {
|
||||
setOptions(plugin, options);
|
||||
export function setMenuOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) {
|
||||
setOptions(plugin, options, exclude);
|
||||
if (store.get('options.restartOnConfigChanges')) {
|
||||
restart();
|
||||
}
|
||||
@ -45,11 +57,11 @@ export function getOptions<T>(plugin: string): T {
|
||||
}
|
||||
|
||||
export function enable(plugin: string) {
|
||||
setMenuOptions(plugin, { enabled: true });
|
||||
setMenuOptions(plugin, { enabled: true }, []);
|
||||
}
|
||||
|
||||
export function disable(plugin: string) {
|
||||
setMenuOptions(plugin, { enabled: false });
|
||||
setMenuOptions(plugin, { enabled: false }, []);
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -1,8 +1,18 @@
|
||||
import Store from 'electron-store';
|
||||
import Conf from 'conf';
|
||||
import is from 'electron-is';
|
||||
|
||||
import defaults from './defaults';
|
||||
|
||||
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
|
||||
|
||||
const getDefaults = () => {
|
||||
if (is.windows()) {
|
||||
defaults.plugins['in-app-menu'].enabled = true;
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
|
||||
if (!store.get(`plugins.${plugin}`)) {
|
||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||
@ -10,6 +20,33 @@ const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: k
|
||||
};
|
||||
|
||||
const migrations = {
|
||||
'>=2.1.3'(store: Conf<Record<string, unknown>>) {
|
||||
const listenAlong = store.get('plugins.discord.listenAlong');
|
||||
if (listenAlong !== undefined) {
|
||||
store.set('plugins.discord.playOnYouTubeMusic', listenAlong);
|
||||
store.delete('plugins.discord.listenAlong');
|
||||
}
|
||||
},
|
||||
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
|
||||
const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
|
||||
if (originalPreset) {
|
||||
if (originalPreset !== 'opus') {
|
||||
store.set('plugins.downloader.selectedPreset', 'Custom');
|
||||
store.set('plugins.downloader.customPresetSetting', {
|
||||
extension: 'mp3',
|
||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
|
||||
} satisfies Preset);
|
||||
} else {
|
||||
store.set('plugins.downloader.selectedPreset', 'Source');
|
||||
store.set('plugins.downloader.customPresetSetting', {
|
||||
extension: null,
|
||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? [],
|
||||
} satisfies Preset);
|
||||
}
|
||||
store.delete('plugins.downloader.preset');
|
||||
store.delete('plugins.downloader.ffmpegArgs');
|
||||
}
|
||||
},
|
||||
'>=1.20.0'(store: Conf<Record<string, unknown>>) {
|
||||
setDefaultPluginOptions(store, 'visualizer');
|
||||
|
||||
@ -118,7 +155,7 @@ const migrations = {
|
||||
};
|
||||
|
||||
export default new Store({
|
||||
defaults,
|
||||
defaults: getDefaults(),
|
||||
clearInvalidConfig: false,
|
||||
migrations,
|
||||
});
|
||||
@ -24,6 +24,7 @@ import captionsSelector from './plugins/captions-selector/back';
|
||||
import crossfade from './plugins/crossfade/back';
|
||||
import discord from './plugins/discord/back';
|
||||
import downloader from './plugins/downloader/back';
|
||||
import httpApi from './plugins/http-api/back';
|
||||
import inAppMenu from './plugins/in-app-menu/back';
|
||||
import lastFm from './plugins/last-fm/back';
|
||||
import lumiaStream from './plugins/lumiastream/back';
|
||||
@ -109,6 +110,7 @@ const mainPlugins = {
|
||||
'crossfade': crossfade,
|
||||
'discord': discord,
|
||||
'downloader': downloader,
|
||||
'http-api': httpApi,
|
||||
'in-app-menu': inAppMenu,
|
||||
'last-fm': lastFm,
|
||||
'lumiastream': lumiaStream,
|
||||
@ -8,11 +8,13 @@ import { startingPages } from './providers/extracted-data';
|
||||
import promptOptions from './providers/prompt-options';
|
||||
|
||||
import adblockerMenu from './plugins/adblocker/menu';
|
||||
import ambientModeMenu from './plugins/ambient-mode/menu';
|
||||
import captionsSelectorMenu from './plugins/captions-selector/menu';
|
||||
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';
|
||||
@ -31,11 +33,13 @@ const betaPlugins = ['crossfade', 'lumiastream'];
|
||||
|
||||
const pluginMenus = {
|
||||
'adblocker': adblockerMenu,
|
||||
'ambient-mode': ambientModeMenu,
|
||||
'disable-autoplay': disableAutoplayMenu,
|
||||
'captions-selector': captionsSelectorMenu,
|
||||
'crossfade': crossfadeMenu,
|
||||
'discord': discordMenu,
|
||||
'downloader': downloaderMenu,
|
||||
'in-app-menu': inAppMenuTitlebarMenu,
|
||||
'lyrics-genius': lyricsGeniusMenu,
|
||||
'notifications': notificationsMenu,
|
||||
'picture-in-picture': pictureInPictureMenu,
|
||||
@ -420,6 +424,12 @@ export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
{ role: 'quit' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
],
|
||||
}
|
||||
];
|
||||
};
|
||||
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
||||
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);
|
||||
}
|
||||
14
src/plugins/ambient-mode/back.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import config from './config';
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
config.subscribeAll((newConfig) => {
|
||||
win.webContents.send('ambient-mode:config-change', newConfig);
|
||||
});
|
||||
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
4
src/plugins/ambient-mode/config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('ambient-mode');
|
||||
export default config;
|
||||
@ -1,9 +1,15 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (_: ConfigType<'ambient-mode'>) => {
|
||||
const interpolationTime = 3000; // interpolation time (ms)
|
||||
const framerate = 30; // frame
|
||||
const qualityRatio = 50; // width size (pixel)
|
||||
export default (config: ConfigType<'ambient-mode'>) => {
|
||||
let interpolationTime = config.interpolationTime; // interpolation time (ms)
|
||||
let buffer = config.buffer; // frame
|
||||
let qualityRatio = config.quality; // width size (pixel)
|
||||
let sizeRatio = config.size / 100; // size ratio (percent)
|
||||
let blur = config.blur; // blur (pixel)
|
||||
let opacity = config.opacity; // opacity (percent)
|
||||
let isFullscreen = config.fullscreen; // fullscreen (boolean)
|
||||
|
||||
let unregister: (() => void) | null = null;
|
||||
|
||||
@ -37,7 +43,7 @@ export default (_: ConfigType<'ambient-mode'>) => {
|
||||
|
||||
context.globalAlpha = 1;
|
||||
if (lastImageData) {
|
||||
const frameOffset = (1 / framerate) * (1000 / interpolationTime);
|
||||
const frameOffset = (1 / buffer) * (1000 / interpolationTime);
|
||||
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
|
||||
context.putImageData(lastImageData, 0, 0);
|
||||
context.globalAlpha = frameOffset;
|
||||
@ -61,8 +67,18 @@ export default (_: ConfigType<'ambient-mode'>) => {
|
||||
|
||||
blurCanvas.width = qualityRatio;
|
||||
blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio);
|
||||
blurCanvas.style.width = `${newWidth}px`;
|
||||
blurCanvas.style.height = `${newHeight}px`;
|
||||
blurCanvas.style.width = `${newWidth * sizeRatio}px`;
|
||||
blurCanvas.style.height = `${newHeight * sizeRatio}px`;
|
||||
|
||||
if (isFullscreen) blurCanvas.classList.add('fullscreen');
|
||||
else blurCanvas.classList.remove('fullscreen');
|
||||
|
||||
const leftOffset = newWidth * (sizeRatio - 1) / 2;
|
||||
const topOffset = newHeight * (sizeRatio - 1) / 2;
|
||||
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
|
||||
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
|
||||
blurCanvas.style.setProperty('--blur', `${blur}px`);
|
||||
blurCanvas.style.setProperty('--opacity', `${opacity}`);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
@ -75,10 +91,22 @@ export default (_: ConfigType<'ambient-mode'>) => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
applyVideoAttributes();
|
||||
});
|
||||
const onConfigSync = (_: Electron.IpcRendererEvent, newConfig: ConfigType<'ambient-mode'>) => {
|
||||
if (typeof newConfig.interpolationTime === 'number') interpolationTime = newConfig.interpolationTime;
|
||||
if (typeof newConfig.buffer === 'number') buffer = newConfig.buffer;
|
||||
if (typeof newConfig.quality === 'number') qualityRatio = newConfig.quality;
|
||||
if (typeof newConfig.size === 'number') sizeRatio = newConfig.size / 100;
|
||||
if (typeof newConfig.blur === 'number') blur = newConfig.blur;
|
||||
if (typeof newConfig.opacity === 'number') opacity = newConfig.opacity;
|
||||
if (typeof newConfig.fullscreen === 'boolean') isFullscreen = newConfig.fullscreen;
|
||||
|
||||
applyVideoAttributes();
|
||||
};
|
||||
ipcRenderer.on('ambient-mode:config-change', onConfigSync);
|
||||
|
||||
/* hooking */
|
||||
let canvasInterval: NodeJS.Timeout | null = null;
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate)));
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
|
||||
applyVideoAttributes();
|
||||
observer.observe(songVideo, { attributes: true });
|
||||
resizeObserver.observe(songVideo);
|
||||
@ -90,7 +118,7 @@ export default (_: ConfigType<'ambient-mode'>) => {
|
||||
};
|
||||
const onPlay = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate)));
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
|
||||
};
|
||||
songVideo.addEventListener('pause', onPause);
|
||||
songVideo.addEventListener('play', onPlay);
|
||||
@ -107,6 +135,7 @@ export default (_: ConfigType<'ambient-mode'>) => {
|
||||
|
||||
observer.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
ipcRenderer.off('ambient-mode:config-change', onConfigSync);
|
||||
window.removeEventListener('resize', applyVideoAttributes);
|
||||
|
||||
wrapper.removeChild(blurCanvas);
|
||||
87
src/plugins/ambient-mode/menu.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
|
||||
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
|
||||
const sizeList = [100, 110, 125, 150, 175, 200, 300];
|
||||
const bufferList = [1, 5, 10, 20, 30];
|
||||
const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500];
|
||||
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
|
||||
|
||||
export default (): MenuTemplate => [
|
||||
{
|
||||
label: 'Smoothness transition',
|
||||
submenu: interpolationTimeList.map((interpolationTime) => ({
|
||||
label: `During ${interpolationTime / 1000}s`,
|
||||
type: 'radio',
|
||||
checked: config.get('interpolationTime') === interpolationTime,
|
||||
click() {
|
||||
config.set('interpolationTime', interpolationTime);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Quality',
|
||||
submenu: qualityList.map((quality) => ({
|
||||
label: `${quality} pixels`,
|
||||
type: 'radio',
|
||||
checked: config.get('quality') === quality,
|
||||
click() {
|
||||
config.set('quality', quality);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
submenu: sizeList.map((size) => ({
|
||||
label: `${size}%`,
|
||||
type: 'radio',
|
||||
checked: config.get('size') === size,
|
||||
click() {
|
||||
config.set('size', size);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Buffer',
|
||||
submenu: bufferList.map((buffer) => ({
|
||||
label: `${buffer}`,
|
||||
type: 'radio',
|
||||
checked: config.get('buffer') === buffer,
|
||||
click() {
|
||||
config.set('buffer', buffer);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Opacity',
|
||||
submenu: opacityList.map((opacity) => ({
|
||||
label: `${opacity * 100}%`,
|
||||
type: 'radio',
|
||||
checked: config.get('opacity') === opacity,
|
||||
click() {
|
||||
config.set('opacity', opacity);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Blur amount',
|
||||
submenu: blurAmountList.map((blur) => ({
|
||||
label: `${blur} pixels`,
|
||||
type: 'radio',
|
||||
checked: config.get('blur') === blur,
|
||||
click() {
|
||||
config.set('blur', blur);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Using fullscreen',
|
||||
type: 'checkbox',
|
||||
checked: config.get('fullscreen'),
|
||||
click(item) {
|
||||
config.set('fullscreen', item.checked);
|
||||
},
|
||||
},
|
||||
];
|
||||
26
src/plugins/ambient-mode/style.css
Normal file
@ -0,0 +1,26 @@
|
||||
#song-video canvas.html5-blur-canvas {
|
||||
filter: blur(var(--blur, 100px));
|
||||
opacity: var(--opacity, 1);
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#song-video canvas.html5-blur-canvas:not(.fullscreen) {
|
||||
position: absolute;
|
||||
|
||||
left: var(--left, 0px);
|
||||
top: var(--top, 0px);
|
||||
}
|
||||
|
||||
#song-video canvas.html5-blur-canvas.fullscreen {
|
||||
position: fixed;
|
||||
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
#song-video .html5-video-container > video {
|
||||
top: 0 !important;
|
||||
}
|
||||
@ -156,13 +156,21 @@ export default (
|
||||
// Song information changed, so lets update the rich presence
|
||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
|
||||
if (songInfo.title.length < 2) {
|
||||
songInfo.title += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
if (songInfo.artist.length < 2) {
|
||||
songInfo.artist += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
|
||||
const activityInfo: SetActivity = {
|
||||
details: songInfo.title,
|
||||
state: songInfo.artist,
|
||||
largeImageKey: songInfo.imageSrc ?? '',
|
||||
largeImageText: songInfo.album ?? '',
|
||||
buttons: [
|
||||
...(options.listenAlong ? [{ label: 'Listen Along', url: songInfo.url ?? '' }] : []),
|
||||
...(options.playOnYouTubeMusic ? [{ label: 'Play on YouTube Music', url: songInfo.url ?? '' }] : []),
|
||||
...(options.hideGitHubButton ? [] : [{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' }]),
|
||||
],
|
||||
};
|
||||
@ -47,11 +47,11 @@ export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMen
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Listen Along',
|
||||
label: 'Play on YouTube Music',
|
||||
type: 'checkbox',
|
||||
checked: options.listenAlong,
|
||||
checked: options.playOnYouTubeMusic,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.listenAlong = item.checked;
|
||||
options.playOnYouTubeMusic = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
@ -1,19 +1,34 @@
|
||||
import { createWriteStream, existsSync, mkdirSync, writeFileSync, } from 'node:fs';
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { app, BrowserWindow, dialog, ipcMain, net } from 'electron';
|
||||
import { ClientType, Innertube, UniversalCache, Utils, 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, presets, sendFeedback as sendFeedback_, setBadge } from './utils';
|
||||
|
||||
import {
|
||||
cropMaxWidth,
|
||||
getFolder,
|
||||
sendFeedback as sendFeedback_,
|
||||
setBadge,
|
||||
} from './utils';
|
||||
import config from './config';
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from './types';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
@ -35,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();
|
||||
|
||||
@ -66,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) => {
|
||||
@ -79,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';
|
||||
@ -96,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) => {
|
||||
@ -111,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,
|
||||
@ -133,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,
|
||||
@ -191,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
|
||||
}`;
|
||||
@ -215,19 +231,42 @@ async function downloadSongUnsafe(
|
||||
}
|
||||
|
||||
if (playabilityStatus.status === 'UNPLAYABLE') {
|
||||
const errorScreen = playabilityStatus.error_screen as PlayerErrorMessage | null;
|
||||
const errorScreen =
|
||||
playabilityStatus.error_screen as PlayerErrorMessage | null;
|
||||
throw new Error(
|
||||
`[${playabilityStatus.status}] ${errorScreen?.reason.text}: ${errorScreen?.subreason.text}`,
|
||||
);
|
||||
}
|
||||
|
||||
const preset = config.get('preset') ?? 'mp3';
|
||||
let presetSetting: { extension: string; ffmpegArgs: string[] } | null = null;
|
||||
if (preset === 'opus') {
|
||||
presetSetting = presets[preset];
|
||||
const selectedPreset = config.get('selectedPreset') ?? 'mp3 (256kbps)';
|
||||
let presetSetting: Preset;
|
||||
if (selectedPreset === 'Custom') {
|
||||
presetSetting =
|
||||
config.get('customPresetSetting') ?? DefaultPresetList['Custom'];
|
||||
} else if (selectedPreset === 'Source') {
|
||||
presetSetting = DefaultPresetList['Source'];
|
||||
} else {
|
||||
presetSetting = DefaultPresetList['mp3 (256kbps)'];
|
||||
}
|
||||
|
||||
let filename = filenamify(`${name}.${presetSetting?.extension ?? 'mp3'}`, {
|
||||
const downloadOptions: FormatOptions = {
|
||||
type: 'audio', // Audio, video or video+audio
|
||||
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'any', // Media container format
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
@ -241,13 +280,6 @@ async function downloadSongUnsafe(
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadOptions: FormatOptions = {
|
||||
type: 'audio', // Audio, video or video+audio
|
||||
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'any', // Media container format
|
||||
};
|
||||
|
||||
const format = info.chooseFormat(downloadOptions);
|
||||
const stream = await info.download(downloadOptions);
|
||||
|
||||
console.info(
|
||||
@ -260,40 +292,25 @@ async function downloadSongUnsafe(
|
||||
mkdirSync(dir);
|
||||
}
|
||||
|
||||
const ffmpegArgs = config.get('ffmpegArgs');
|
||||
const fileBuffer = await iterableStreamToTargetFile(
|
||||
iterableStream,
|
||||
targetFileExtension,
|
||||
metadata,
|
||||
presetSetting?.ffmpegArgs ?? [],
|
||||
format.content_length ?? 0,
|
||||
sendFeedback,
|
||||
increasePlaylistProgress,
|
||||
);
|
||||
|
||||
if (presetSetting && presetSetting?.extension !== 'mp3') {
|
||||
const file = createWriteStream(filePath);
|
||||
let downloaded = 0;
|
||||
const total: number = format.content_length ?? 1;
|
||||
|
||||
for await (const chunk of iterableStream) {
|
||||
downloaded += chunk.length;
|
||||
const ratio = downloaded / total;
|
||||
const progress = Math.floor(ratio * 100);
|
||||
sendFeedback(`Download: ${progress}%`, ratio);
|
||||
increasePlaylistProgress(ratio);
|
||||
file.write(chunk);
|
||||
}
|
||||
|
||||
await ffmpegWriteTags(
|
||||
filePath,
|
||||
metadata,
|
||||
presetSetting.ffmpegArgs,
|
||||
ffmpegArgs,
|
||||
);
|
||||
sendFeedback(null, -1);
|
||||
} else {
|
||||
const fileBuffer = await iterableStreamToMP3(
|
||||
iterableStream,
|
||||
metadata,
|
||||
ffmpegArgs,
|
||||
format.content_length ?? 0,
|
||||
sendFeedback,
|
||||
increasePlaylistProgress,
|
||||
);
|
||||
if (fileBuffer) {
|
||||
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
|
||||
if (fileBuffer) {
|
||||
if (targetFileExtension !== 'mp3') {
|
||||
createWriteStream(filePath).write(fileBuffer);
|
||||
} else {
|
||||
const buffer = await writeID3(
|
||||
Buffer.from(fileBuffer),
|
||||
metadata,
|
||||
sendFeedback,
|
||||
);
|
||||
if (buffer) {
|
||||
writeFileSync(filePath, buffer);
|
||||
}
|
||||
@ -304,14 +321,14 @@ async function downloadSongUnsafe(
|
||||
console.info(`Done: "${filePath}"`);
|
||||
}
|
||||
|
||||
async function iterableStreamToMP3(
|
||||
async function iterableStreamToTargetFile(
|
||||
stream: AsyncGenerator<Uint8Array, void>,
|
||||
extension: string,
|
||||
metadata: CustomSongInfo,
|
||||
ffmpegArgs: string[],
|
||||
presetFfmpegArgs: string[],
|
||||
contentLength: number,
|
||||
sendFeedback: (str: string, value?: number) => void,
|
||||
increasePlaylistProgress: (value: number) => void = () => {
|
||||
},
|
||||
increasePlaylistProgress: (value: number) => void = () => {},
|
||||
) {
|
||||
const chunks = [];
|
||||
let downloaded = 0;
|
||||
@ -347,13 +364,14 @@ async function iterableStreamToMP3(
|
||||
increasePlaylistProgress(0.15 + (ratio * 0.85));
|
||||
});
|
||||
|
||||
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
|
||||
try {
|
||||
await ffmpeg.run(
|
||||
'-i',
|
||||
safeVideoName,
|
||||
...ffmpegArgs,
|
||||
...presetFfmpegArgs,
|
||||
...getFFmpegMetadataArgs(metadata),
|
||||
`${safeVideoName}.mp3`,
|
||||
safeVideoNameWithExtension,
|
||||
);
|
||||
} finally {
|
||||
ffmpeg.FS('unlink', safeVideoName);
|
||||
@ -362,9 +380,9 @@ async function iterableStreamToMP3(
|
||||
sendFeedback('Saving…');
|
||||
|
||||
try {
|
||||
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`);
|
||||
return ffmpeg.FS('readFile', safeVideoNameWithExtension);
|
||||
} finally {
|
||||
ffmpeg.FS('unlink', `${safeVideoName}.mp3`);
|
||||
ffmpeg.FS('unlink', safeVideoNameWithExtension);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
sendError(error as Error, safeVideoName);
|
||||
@ -378,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 = {};
|
||||
@ -428,13 +450,12 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
try {
|
||||
givenUrl = new URL(givenUrl ?? '');
|
||||
} catch {
|
||||
return;
|
||||
givenUrl = new URL(win.webContents.getURL());
|
||||
}
|
||||
|
||||
const playlistId
|
||||
= getPlaylistID(givenUrl)
|
||||
|| getPlaylistID(new URL(win.webContents.getURL()))
|
||||
|| getPlaylistID(new URL(playingUrl));
|
||||
const playlistId =
|
||||
getPlaylistID(givenUrl) ||
|
||||
getPlaylistID(new URL(playingUrl));
|
||||
|
||||
if (!playlistId) {
|
||||
sendError(new Error('No playlist ID found'));
|
||||
@ -446,11 +467,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;
|
||||
}
|
||||
@ -459,19 +488,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;
|
||||
}
|
||||
|
||||
let playlistTitle = playlist.header?.title?.text ?? '';
|
||||
const isAlbum = playlistTitle?.startsWith('Album - ');
|
||||
if (isAlbum) {
|
||||
playlistTitle = playlistTitle.slice(8);
|
||||
}
|
||||
|
||||
let safePlaylistTitle = filenamify(playlistTitle, { replacement: ' ' });
|
||||
if (!is.macOS()) {
|
||||
safePlaylistTitle = safePlaylistTitle.normalize('NFC');
|
||||
@ -527,7 +566,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}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -544,29 +587,6 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ffmpegWriteTags(filePath: string, metadata: CustomSongInfo, presetFFmpegArgs: string[] = [], ffmpegArgs: string[] = []) {
|
||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||
|
||||
try {
|
||||
if (!ffmpeg.isLoaded()) {
|
||||
await ffmpeg.load();
|
||||
}
|
||||
|
||||
await ffmpeg.run(
|
||||
'-i',
|
||||
filePath,
|
||||
...getFFmpegMetadataArgs(metadata),
|
||||
...presetFFmpegArgs,
|
||||
...ffmpegArgs,
|
||||
filePath,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
sendError(error as Error);
|
||||
} finally {
|
||||
releaseFFmpegMutex();
|
||||
}
|
||||
}
|
||||
|
||||
function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
|
||||
if (!metadata) {
|
||||
return [];
|
||||
@ -583,9 +603,9 @@ function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
|
||||
// Playlist radio modifier needs to be cut from playlist ID
|
||||
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
|
||||
|
||||
const getPlaylistID = (aURL: URL) => {
|
||||
const result
|
||||
= aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
|
||||
const getPlaylistID = (aURL?: URL): string | null | undefined => {
|
||||
const result =
|
||||
aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
|
||||
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
|
||||
return result.slice(INVALID_PLAYLIST_MODIFIER.length);
|
||||
}
|
||||
@ -594,16 +614,18 @@ const getPlaylistID = (aURL: URL) => {
|
||||
};
|
||||
|
||||
const getVideoId = (url: URL | string): string | null => {
|
||||
return (new URL(url)).searchParams.get('v');
|
||||
return new URL(url).searchParams.get('v');
|
||||
};
|
||||
|
||||
const getMetadata = (info: TrackInfo): CustomSongInfo => ({
|
||||
videoId: info.basic_info.id!,
|
||||
title: cleanupName(info.basic_info.title!),
|
||||
artist: cleanupName(info.basic_info.author!),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||
album: (info.player_overlays?.browser_media_session as any)?.album?.text as string | undefined,
|
||||
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
|
||||
album: info.player_overlays?.browser_media_session?.as(
|
||||
YTNodes.BrowserMediaSession,
|
||||
).album?.text,
|
||||
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))
|
||||
?.url,
|
||||
views: info.basic_info.view_count!,
|
||||
songDuration: info.basic_info.duration!,
|
||||
});
|
||||
@ -47,7 +47,7 @@ const menuObserver = new MutationObserver(() => {
|
||||
(global as any).download = () => {
|
||||
let videoUrl = getSongMenu()
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')
|
||||
?.getAttribute('href');
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
@ -1,7 +1,8 @@
|
||||
import { dialog } from 'electron';
|
||||
|
||||
import { downloadPlaylist } from './back';
|
||||
import { defaultMenuDownloadLabel, getFolder, presets } from './utils';
|
||||
import { defaultMenuDownloadLabel, getFolder } from './utils';
|
||||
import { DefaultPresetList } from './types';
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
@ -25,12 +26,12 @@ export default (): MenuTemplate => [
|
||||
},
|
||||
{
|
||||
label: 'Presets',
|
||||
submenu: Object.keys(presets).map((preset) => ({
|
||||
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||
label: preset,
|
||||
type: 'radio',
|
||||
checked: config.get('preset') === preset,
|
||||
checked: config.get('selectedPreset') === preset,
|
||||
click() {
|
||||
config.set('preset', preset);
|
||||
config.set('selectedPreset', preset);
|
||||
},
|
||||
})),
|
||||
},
|
||||
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) => {
|
||||
const imageSize = image.getSize();
|
||||
// Standart youtube artwork width with margins from both sides is 280 + 720 + 280
|
||||
// Standart YouTube artwork width with margins from both sides is 280 + 720 + 280
|
||||
if (imageSize.width === 1280 && imageSize.height === 720) {
|
||||
return image.crop({
|
||||
x: 280,
|
||||
@ -23,15 +23,6 @@ export const cropMaxWidth = (image: Electron.NativeImage) => {
|
||||
return image;
|
||||
};
|
||||
|
||||
// Presets for FFmpeg
|
||||
export const presets = {
|
||||
'None (defaults to mp3)': undefined,
|
||||
'opus': {
|
||||
extension: 'opus',
|
||||
ffmpegArgs: ['-acodec', 'libopus'],
|
||||
},
|
||||
};
|
||||
|
||||
export const setBadge = (n: number) => {
|
||||
if (is.linux() || is.macOS()) {
|
||||
app.setBadgeCount(n);
|
||||
112
src/plugins/http-api/back.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import http from 'node:http';
|
||||
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import is from 'electron-is';
|
||||
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
import { isEnabled } from '../../config/plugins';
|
||||
|
||||
const port = 9669; // Choose a port number
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
let songInfo: (SongInfo & { loopStatus?: string, volume?: number }) | undefined;
|
||||
|
||||
if (!is.linux() || (is.linux() && !isEnabled('shortcuts'))) {
|
||||
ipcMain.on('apiLoaded', () => {
|
||||
win.webContents.send('setupSeekedListener', 'webinterface');
|
||||
win.webContents.send('setupTimeChangedListener', 'webinterface');
|
||||
win.webContents.send('setupRepeatChangedListener', 'webinterface');
|
||||
win.webContents.send('setupVolumeChangedListener', 'webinterface');
|
||||
});
|
||||
}
|
||||
|
||||
// updations
|
||||
ipcMain.on('seeked', (_, t: number) => {
|
||||
if (songInfo) {
|
||||
songInfo.elapsedSeconds = t;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('timeChanged', (_, t: number) => {
|
||||
if (songInfo) {
|
||||
songInfo.elapsedSeconds = t;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('repeatChanged', (_, mode: string) => {
|
||||
if (songInfo) {
|
||||
songInfo.loopStatus = mode;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('volumeChanged', (_, newVolume: number) => {
|
||||
if(songInfo) {
|
||||
songInfo.volume = newVolume;
|
||||
}
|
||||
});
|
||||
|
||||
registerCallback((info) => songInfo = info as SongInfo & { loopStatus?: string, volume?: number });
|
||||
|
||||
const doAction = (action: URLSearchParams) => {
|
||||
//actions
|
||||
const songControls = getSongControls(win);
|
||||
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
|
||||
switch (action.get('action')) {
|
||||
case 'play':
|
||||
case 'pause':
|
||||
case 'playpause':
|
||||
if (songInfo) {
|
||||
songInfo.isPaused = !songInfo.isPaused;
|
||||
}
|
||||
playPause();
|
||||
break;
|
||||
case 'next':
|
||||
next();
|
||||
break;
|
||||
case 'previous':
|
||||
previous();
|
||||
break;
|
||||
case 'shuffle':
|
||||
shuffle();
|
||||
break;
|
||||
case 'looptoggle':
|
||||
songControls.switchRepeat(1);
|
||||
break;
|
||||
case 'volumeplus10':
|
||||
volumePlus10();
|
||||
break;
|
||||
case 'volumeminus10':
|
||||
volumeMinus10();
|
||||
break;
|
||||
case 'seek':
|
||||
console.log('seek', action.get('value'));
|
||||
win.webContents.send('seekTo', action.get('value'));
|
||||
break;
|
||||
default:
|
||||
throw Error(`web-interface: Requested action "${action.get('action')}" not found`);
|
||||
}
|
||||
};
|
||||
|
||||
const server = http.createServer((req,res) => {
|
||||
if (req?.url?.slice(0,4) === '/api') {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
if (url.searchParams.has('action')) {
|
||||
console.log('webinterface: received and trying action ' + url.searchParams.get('action'));
|
||||
try {
|
||||
doAction(url.searchParams);
|
||||
} catch (e) {
|
||||
res.statusCode = 404;
|
||||
res.write(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.setHeader('Content-Type','application/json');
|
||||
res.write(JSON.stringify(songInfo));
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
server.listen(port, () => {
|
||||
console.log(`web-interface API Server is running on port ${port}`);
|
||||
});
|
||||
};
|
||||
|
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-minimize', () => win.minimize());
|
||||
ipcMain.handle('window-maximize', () => win.maximize());
|
||||
win.on('maximize', () => win.webContents.send('window-maximize'));
|
||||
ipcMain.handle('window-unmaximize', () => win.unmaximize());
|
||||
win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
|
||||
};
|
||||
@ -19,9 +19,11 @@ const isMacOS = navigator.userAgent.includes('Macintosh');
|
||||
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
|
||||
|
||||
export default async () => {
|
||||
const hideDOMWindowControls = config.get('plugins.in-app-menu.hideDOMWindowControls');
|
||||
let hideMenu = config.get('options.hideMenu');
|
||||
const titleBar = document.createElement('title-bar');
|
||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||
let maximizeButton: HTMLButtonElement;
|
||||
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
|
||||
|
||||
logo.classList.add('title-bar-icon');
|
||||
@ -55,7 +57,7 @@ export default async () => {
|
||||
minimizeButton.appendChild(minimize);
|
||||
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
|
||||
|
||||
const maximizeButton = document.createElement('button');
|
||||
maximizeButton = document.createElement('button');
|
||||
if (await ipcRenderer.invoke('window-is-maximized')) {
|
||||
maximizeButton.classList.add('window-control');
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
@ -97,7 +99,7 @@ export default async () => {
|
||||
titleBar.appendChild(windowControlsContainer);
|
||||
};
|
||||
|
||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||
|
||||
if (navBar) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
@ -129,14 +131,24 @@ export default async () => {
|
||||
menu.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls();
|
||||
};
|
||||
await updateMenu();
|
||||
|
||||
document.title = 'Youtube Music';
|
||||
|
||||
ipcRenderer.on('refreshMenu', () => {
|
||||
updateMenu();
|
||||
ipcRenderer.on('refreshMenu', () => updateMenu());
|
||||
ipcRenderer.on('window-maximize', () => {
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
}
|
||||
});
|
||||
ipcRenderer.on('window-unmaximize', () => {
|
||||
if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
}
|
||||
});
|
||||
|
||||
if (isEnabled('picture-in-picture')) {
|
||||
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[],
|
||||
];
|
||||