fix: remove xo, migration to eslint

This commit is contained in:
JellyBrick
2023-08-29 17:22:38 +09:00
parent 31a7588cee
commit c722896a73
142 changed files with 17210 additions and 18409 deletions

View File

@ -1,7 +1,8 @@
root = true root = true
[*] [*]
indent_style = tab indent_style = space
indent_size = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
.eslintrc.js

56
.eslintrc.js Normal file
View File

@ -0,0 +1,56 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:import/recommended',
],
plugins: ['import'],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'arrow-parens': ['error', 'always'],
'object-curly-spacing': ['error', 'always'],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-default-export': 'off',
'import/no-duplicates': 'error',
'import/order': [
'error',
{
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
'newlines-between': 'always-and-inside-groups',
'alphabetize': {order: 'ignore', caseInsensitive: false}
}
],
'import/prefer-default-export': 'off',
'camelcase': ['error', {properties: 'never'}],
'class-methods-use-this': 'off',
'lines-around-comment': [
'error',
{
beforeBlockComment: false,
afterBlockComment: false,
beforeLineComment: false,
afterLineComment: false,
},
],
'max-len': 'off',
'no-mixed-operators': 'error',
'no-multi-spaces': ['error', {ignoreEOLComments: true}],
'no-tabs': 'error',
'no-void': 'error',
'no-empty': 'off',
'prefer-promise-reject-errors': 'off',
'quotes': ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: false,
}],
'quote-props': ['error', 'consistent'],
},
env: {
browser: true,
node: true,
es6: true,
},
ignorePatterns: ['dist', 'node_modules'],
};

View File

@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
os: [macos-latest, ubuntu-latest, windows-latest] os: [ macos-latest, ubuntu-latest, windows-latest ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -5,7 +5,7 @@
# Source repository: https://github.com/actions/dependency-review-action # Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: "Dependency Review" name: "Dependency Review"
on: [pull_request] on: [ pull_request ]
permissions: permissions:
contents: read contents: read

View File

@ -2,7 +2,7 @@ name: Submit to Windows Package Manager Community Repository
on: on:
release: release:
types: [released] types: [ released ]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag_name: tag_name:

View File

@ -23,22 +23,29 @@ All notable changes to this project will be documented in this file. Dates are d
- Allow downloading age restricted videos [`#1086`](https://github.com/th-ch/youtube-music/pull/1086) - Allow downloading age restricted videos [`#1086`](https://github.com/th-ch/youtube-music/pull/1086)
- add starting page option [`#1073`](https://github.com/th-ch/youtube-music/pull/1073) - add starting page option [`#1073`](https://github.com/th-ch/youtube-music/pull/1073)
- [downloader] plugin overhaul [`#1054`](https://github.com/th-ch/youtube-music/pull/1054) - [downloader] plugin overhaul [`#1054`](https://github.com/th-ch/youtube-music/pull/1054)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.25.2 to 1.26.0 [`#1070`](https://github.com/th-ch/youtube-music/pull/1070) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.25.2 to
- [in-app-menu] fix css style of the library of uploaded songs [`#1072`](https://github.com/th-ch/youtube-music/pull/1072) 1.26.0 [`#1070`](https://github.com/th-ch/youtube-music/pull/1070)
- [in-app-menu] fix css style of the library of uploaded
songs [`#1072`](https://github.com/th-ch/youtube-music/pull/1072)
- add option to hide the like buttons [`#1077`](https://github.com/th-ch/youtube-music/pull/1077) - add option to hide the like buttons [`#1077`](https://github.com/th-ch/youtube-music/pull/1077)
- Nitpick: Fix name casing in tray icon tooltip [`#1081`](https://github.com/th-ch/youtube-music/pull/1081) - Nitpick: Fix name casing in tray icon tooltip [`#1081`](https://github.com/th-ch/youtube-music/pull/1081)
- [lyrics-genius] Improved reliability of east asian language detection #1080 [`#1082`](https://github.com/th-ch/youtube-music/pull/1082) - [lyrics-genius] Improved reliability of east asian language detection
#1080 [`#1082`](https://github.com/th-ch/youtube-music/pull/1082)
- Add dynamic synced plugin config provider [`#1064`](https://github.com/th-ch/youtube-music/pull/1064) - Add dynamic synced plugin config provider [`#1064`](https://github.com/th-ch/youtube-music/pull/1064)
- [captions-selector] fix button showing when there aren't any captions available [`#1063`](https://github.com/th-ch/youtube-music/pull/1063) - [captions-selector] fix button showing when there aren't any captions
available [`#1063`](https://github.com/th-ch/youtube-music/pull/1063)
- [in-app-menu] fix items hidden by navbar in library [`#1067`](https://github.com/th-ch/youtube-music/pull/1067) - [in-app-menu] fix items hidden by navbar in library [`#1067`](https://github.com/th-ch/youtube-music/pull/1067)
- Fix Youtube Music logo is draggable [`#1061`](https://github.com/th-ch/youtube-music/pull/1061) - Fix Youtube Music logo is draggable [`#1061`](https://github.com/th-ch/youtube-music/pull/1061)
- fix build action failing on forks, and run it on pull requests [`#1069`](https://github.com/th-ch/youtube-music/pull/1069) - fix build action failing on forks, and run it on pull
requests [`#1069`](https://github.com/th-ch/youtube-music/pull/1069)
- try to fix songInfo time&album [`#1032`](https://github.com/th-ch/youtube-music/pull/1032) - try to fix songInfo time&album [`#1032`](https://github.com/th-ch/youtube-music/pull/1032)
- [lyrics] Romanization toggle for Genius plugin [`#1039`](https://github.com/th-ch/youtube-music/pull/1039) - [lyrics] Romanization toggle for Genius plugin [`#1039`](https://github.com/th-ch/youtube-music/pull/1039)
- [Snyk] Upgrade html-to-text from 9.0.3 to 9.0.4 [`#1056`](https://github.com/th-ch/youtube-music/pull/1056) - [Snyk] Upgrade html-to-text from 9.0.3 to 9.0.4 [`#1056`](https://github.com/th-ch/youtube-music/pull/1056)
- [in-app-menu] add toggle menu icon [`#988`](https://github.com/th-ch/youtube-music/pull/988) - [in-app-menu] add toggle menu icon [`#988`](https://github.com/th-ch/youtube-music/pull/988)
- Fix playback speed slider not showing and PiP button showing when it shouldn't [`#1048`](https://github.com/th-ch/youtube-music/pull/1048) - Fix playback speed slider not showing and PiP button showing when it
- [lyrics-genius] Fix lyrics not showing up or showing up when they shouldn't [`#1052`](https://github.com/th-ch/youtube-music/pull/1052) shouldn't [`#1048`](https://github.com/th-ch/youtube-music/pull/1048)
- [lyrics-genius] Fix lyrics not showing up or showing up when they
shouldn't [`#1052`](https://github.com/th-ch/youtube-music/pull/1052)
- [in-app-menu] disable nav-bar drag when menu is open [`#1055`](https://github.com/th-ch/youtube-music/pull/1055) - [in-app-menu] disable nav-bar drag when menu is open [`#1055`](https://github.com/th-ch/youtube-music/pull/1055)
- [Notifications] [Windows] Native interactive notifications [`#946`](https://github.com/th-ch/youtube-music/pull/946) - [Notifications] [Windows] Native interactive notifications [`#946`](https://github.com/th-ch/youtube-music/pull/946)
- automate winget releases [`#1049`](https://github.com/th-ch/youtube-music/pull/1049) - automate winget releases [`#1049`](https://github.com/th-ch/youtube-music/pull/1049)
@ -66,7 +73,8 @@ All notable changes to this project will be documented in this file. Dates are d
- fix SnoreToast implementation [`#941`](https://github.com/th-ch/youtube-music/pull/941) - fix SnoreToast implementation [`#941`](https://github.com/th-ch/youtube-music/pull/941)
- Bump json5 from 1.0.1 to 1.0.2 [`#942`](https://github.com/th-ch/youtube-music/pull/942) - Bump json5 from 1.0.1 to 1.0.2 [`#942`](https://github.com/th-ch/youtube-music/pull/942)
- [Snyk] Upgrade custom-electron-titlebar from 4.1.3 to 4.1.5 [`#969`](https://github.com/th-ch/youtube-music/pull/969) - [Snyk] Upgrade custom-electron-titlebar from 4.1.3 to 4.1.5 [`#969`](https://github.com/th-ch/youtube-music/pull/969)
- Fixed video-toggle aligning running before #main-panel exists [`#956`](https://github.com/th-ch/youtube-music/pull/956) - Fixed video-toggle aligning running before #main-panel
exists [`#956`](https://github.com/th-ch/youtube-music/pull/956)
- [New plugin] Music visualizers [`#953`](https://github.com/th-ch/youtube-music/pull/953) - [New plugin] Music visualizers [`#953`](https://github.com/th-ch/youtube-music/pull/953)
- fix PiP buttons not showing up [`#964`](https://github.com/th-ch/youtube-music/pull/964) - fix PiP buttons not showing up [`#964`](https://github.com/th-ch/youtube-music/pull/964)
- Use same audio context/source everywhere [`#951`](https://github.com/th-ch/youtube-music/pull/951) - Use same audio context/source everywhere [`#951`](https://github.com/th-ch/youtube-music/pull/951)
@ -78,7 +86,8 @@ All notable changes to this project will be documented in this file. Dates are d
- fix unescaped url params [`#1050`](https://github.com/th-ch/youtube-music/issues/1050) - fix unescaped url params [`#1050`](https://github.com/th-ch/youtube-music/issues/1050)
- fix playback speed selector [`#1045`](https://github.com/th-ch/youtube-music/issues/1045) - fix playback speed selector [`#1045`](https://github.com/th-ch/youtube-music/issues/1045)
- fix PiP button [`#959`](https://github.com/th-ch/youtube-music/issues/959) - fix PiP button [`#959`](https://github.com/th-ch/youtube-music/issues/959)
- fix security issues in deps [`9cde19d`](https://github.com/th-ch/youtube-music/commit/9cde19d906081fe1851f90fa44581b2b74c328e3) - fix security issues in
deps [`9cde19d`](https://github.com/th-ch/youtube-music/commit/9cde19d906081fe1851f90fa44581b2b74c328e3)
- rome lint [`325026e`](https://github.com/th-ch/youtube-music/commit/325026e3eae3daed33a6d66d1ef9f898d6805b28) - rome lint [`325026e`](https://github.com/th-ch/youtube-music/commit/325026e3eae3daed33a6d66d1ef9f898d6805b28)
- lint [`b652a01`](https://github.com/th-ch/youtube-music/commit/b652a011a5a08978db6660aeca6908c47a7cf07a) - lint [`b652a01`](https://github.com/th-ch/youtube-music/commit/b652a011a5a08978db6660aeca6908c47a7cf07a)
@ -91,25 +100,32 @@ All notable changes to this project will be documented in this file. Dates are d
- Load plugins as soon as the window is created [`#890`](https://github.com/th-ch/youtube-music/pull/890) - Load plugins as soon as the window is created [`#890`](https://github.com/th-ch/youtube-music/pull/890)
- Bump qs from 6.5.2 to 6.5.3 [`#913`](https://github.com/th-ch/youtube-music/pull/913) - Bump qs from 6.5.2 to 6.5.3 [`#913`](https://github.com/th-ch/youtube-music/pull/913)
- [Snyk] Upgrade custom-electron-titlebar from 4.1.1 to 4.1.2 [`#900`](https://github.com/th-ch/youtube-music/pull/900) - [Snyk] Upgrade custom-electron-titlebar from 4.1.1 to 4.1.2 [`#900`](https://github.com/th-ch/youtube-music/pull/900)
- Add option in skip-silences plugin to only skip at the beginning [`#931`](https://github.com/th-ch/youtube-music/pull/931) - Add option in skip-silences plugin to only skip at the
beginning [`#931`](https://github.com/th-ch/youtube-music/pull/931)
- Replace rimraf by del-cli [`#932`](https://github.com/th-ch/youtube-music/pull/932) - Replace rimraf by del-cli [`#932`](https://github.com/th-ch/youtube-music/pull/932)
- docs: Added winget install instructions [`#873`](https://github.com/th-ch/youtube-music/pull/873) - docs: Added winget install instructions [`#873`](https://github.com/th-ch/youtube-music/pull/873)
- [Snyk] Upgrade async-mutex from 0.3.2 to 0.4.0 [`#855`](https://github.com/th-ch/youtube-music/pull/855) - [Snyk] Upgrade async-mutex from 0.3.2 to 0.4.0 [`#855`](https://github.com/th-ch/youtube-music/pull/855)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.25.0 to 1.25.1 [`#856`](https://github.com/th-ch/youtube-music/pull/856) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.25.0 to
1.25.1 [`#856`](https://github.com/th-ch/youtube-music/pull/856)
- [Snyk] Upgrade custom-electron-titlebar from 4.1.0 to 4.1.1 [`#865`](https://github.com/th-ch/youtube-music/pull/865) - [Snyk] Upgrade custom-electron-titlebar from 4.1.0 to 4.1.1 [`#865`](https://github.com/th-ch/youtube-music/pull/865)
- [Snyk] Upgrade @ffmpeg/ffmpeg from 0.11.5 to 0.11.6 [`#876`](https://github.com/th-ch/youtube-music/pull/876) - [Snyk] Upgrade @ffmpeg/ffmpeg from 0.11.5 to 0.11.6 [`#876`](https://github.com/th-ch/youtube-music/pull/876)
- Discord Plugin RPC Fix [`#888`](https://github.com/th-ch/youtube-music/pull/888) - Discord Plugin RPC Fix [`#888`](https://github.com/th-ch/youtube-music/pull/888)
- Bump FFMpeg [`#854`](https://github.com/th-ch/youtube-music/pull/854) - Bump FFMpeg [`#854`](https://github.com/th-ch/youtube-music/pull/854)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.8 to 1.23.9 [`#823`](https://github.com/th-ch/youtube-music/pull/823) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.8 to
1.23.9 [`#823`](https://github.com/th-ch/youtube-music/pull/823)
- [Snyk] Upgrade electron-store from 8.0.2 to 8.1.0 [`#801`](https://github.com/th-ch/youtube-music/pull/801) - [Snyk] Upgrade electron-store from 8.0.2 to 8.1.0 [`#801`](https://github.com/th-ch/youtube-music/pull/801)
- proposal: Adding an option to hide duration before the song ends [`#802`](https://github.com/th-ch/youtube-music/pull/802) - proposal: Adding an option to hide duration before the song
ends [`#802`](https://github.com/th-ch/youtube-music/pull/802)
- [Snyk] Security upgrade node-fetch from 2.6.7 to 3.2.10 [`#790`](https://github.com/th-ch/youtube-music/pull/790) - [Snyk] Security upgrade node-fetch from 2.6.7 to 3.2.10 [`#790`](https://github.com/th-ch/youtube-music/pull/790)
- Update README.md with a new theme repo [`#807`](https://github.com/th-ch/youtube-music/pull/807) - Update README.md with a new theme repo [`#807`](https://github.com/th-ch/youtube-music/pull/807)
- Fix likes on touchbar (they were inverted) [`#822`](https://github.com/th-ch/youtube-music/pull/822) - Fix likes on touchbar (they were inverted) [`#822`](https://github.com/th-ch/youtube-music/pull/822)
- Add Scoop install directions for Windows 🪟 [`#839`](https://github.com/th-ch/youtube-music/pull/839) - Add Scoop install directions for Windows 🪟 [`#839`](https://github.com/th-ch/youtube-music/pull/839)
- Bump version and change release type when publishing a new version [`31ab27c`](https://github.com/th-ch/youtube-music/commit/31ab27c39ff6319116a6514d952eed1f02dd45fd) - Bump version and change release type when publishing a new
- Lock node-fetch to v2 for commonJS [`c9f610f`](https://github.com/th-ch/youtube-music/commit/c9f610f7fcfcce1317338364045ab0e1bf4249a4) version [`31ab27c`](https://github.com/th-ch/youtube-music/commit/31ab27c39ff6319116a6514d952eed1f02dd45fd)
- fix: upgrade @cliqz/adblocker-electron from 1.25.0 to 1.25.1 [`762ef4e`](https://github.com/th-ch/youtube-music/commit/762ef4eede29b53aae912b3b50a1ca53f6765c53) - Lock node-fetch to v2 for
commonJS [`c9f610f`](https://github.com/th-ch/youtube-music/commit/c9f610f7fcfcce1317338364045ab0e1bf4249a4)
- fix: upgrade @cliqz/adblocker-electron from 1.25.0 to
1.25.1 [`762ef4e`](https://github.com/th-ch/youtube-music/commit/762ef4eede29b53aae912b3b50a1ca53f6765c53)
#### [v1.18.0](https://github.com/th-ch/youtube-music/compare/v1.17.0...v1.18.0) #### [v1.18.0](https://github.com/th-ch/youtube-music/compare/v1.17.0...v1.18.0)
@ -122,7 +138,8 @@ All notable changes to this project will be documented in this file. Dates are d
- [Snyk] Upgrade electron-store from 8.0.1 to 8.0.2 [`#772`](https://github.com/th-ch/youtube-music/pull/772) - [Snyk] Upgrade electron-store from 8.0.1 to 8.0.2 [`#772`](https://github.com/th-ch/youtube-music/pull/772)
- Bump jpeg-js from 0.4.3 to 0.4.4 [`#756`](https://github.com/th-ch/youtube-music/pull/756) - Bump jpeg-js from 0.4.3 to 0.4.4 [`#756`](https://github.com/th-ch/youtube-music/pull/756)
- Support MPRIS loop and volume change [`#749`](https://github.com/th-ch/youtube-music/pull/749) - Support MPRIS loop and volume change [`#749`](https://github.com/th-ch/youtube-music/pull/749)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8 [`#742`](https://github.com/th-ch/youtube-music/pull/742) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.7 to
1.23.8 [`#742`](https://github.com/th-ch/youtube-music/pull/742)
- Use ; instead of space for play/pause. [`#745`](https://github.com/th-ch/youtube-music/pull/745) - Use ; instead of space for play/pause. [`#745`](https://github.com/th-ch/youtube-music/pull/745)
- Update readme.md [`#750`](https://github.com/th-ch/youtube-music/pull/750) - Update readme.md [`#750`](https://github.com/th-ch/youtube-music/pull/750)
- fix lyrics font size [`#753`](https://github.com/th-ch/youtube-music/pull/753) - fix lyrics font size [`#753`](https://github.com/th-ch/youtube-music/pull/753)
@ -132,7 +149,8 @@ All notable changes to this project will be documented in this file. Dates are d
- Picture in Picture v2 [`#685`](https://github.com/th-ch/youtube-music/pull/685) - Picture in Picture v2 [`#685`](https://github.com/th-ch/youtube-music/pull/685)
- Add MPRIS volume control [`#776`](https://github.com/th-ch/youtube-music/issues/776) - Add MPRIS volume control [`#776`](https://github.com/th-ch/youtube-music/issues/776)
- Remove jest [`bb6115f`](https://github.com/th-ch/youtube-music/commit/bb6115fec1a18a416edb365a442eb0b0ee330768) - Remove jest [`bb6115f`](https://github.com/th-ch/youtube-music/commit/bb6115fec1a18a416edb365a442eb0b0ee330768)
- migrate from remote to ipc [`5bd9768`](https://github.com/th-ch/youtube-music/commit/5bd97685b9e07c656e0b57a9e02819afc70af1b1) - migrate from remote to
ipc [`5bd9768`](https://github.com/th-ch/youtube-music/commit/5bd97685b9e07c656e0b57a9e02819afc70af1b1)
- v3 [`d23bfe9`](https://github.com/th-ch/youtube-music/commit/d23bfe936840b947ca101fd304464f65d36e88cc) - v3 [`d23bfe9`](https://github.com/th-ch/youtube-music/commit/d23bfe936840b947ca101fd304464f65d36e88cc)
#### [v1.17.0](https://github.com/th-ch/youtube-music/compare/v1.16.0...v1.17.0) #### [v1.17.0](https://github.com/th-ch/youtube-music/compare/v1.16.0...v1.17.0)
@ -141,7 +159,8 @@ All notable changes to this project will be documented in this file. Dates are d
- Bump ejs from 3.1.6 to 3.1.7 [`#712`](https://github.com/th-ch/youtube-music/pull/712) - Bump ejs from 3.1.6 to 3.1.7 [`#712`](https://github.com/th-ch/youtube-music/pull/712)
- fix injectCSS `did-finish-load` listener overload [`#693`](https://github.com/th-ch/youtube-music/pull/693) - fix injectCSS `did-finish-load` listener overload [`#693`](https://github.com/th-ch/youtube-music/pull/693)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.6 to 1.23.7 [`#689`](https://github.com/th-ch/youtube-music/pull/689) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.6 to
1.23.7 [`#689`](https://github.com/th-ch/youtube-music/pull/689)
- [Snyk] Upgrade custom-electron-prompt from 1.4.1 to 1.4.2 [`#686`](https://github.com/th-ch/youtube-music/pull/686) - [Snyk] Upgrade custom-electron-prompt from 1.4.1 to 1.4.2 [`#686`](https://github.com/th-ch/youtube-music/pull/686)
- [Snyk] Upgrade @electron/remote from 2.0.7 to 2.0.8 [`#684`](https://github.com/th-ch/youtube-music/pull/684) - [Snyk] Upgrade @electron/remote from 2.0.7 to 2.0.8 [`#684`](https://github.com/th-ch/youtube-music/pull/684)
- Improve plugin submenu ux [`#699`](https://github.com/th-ch/youtube-music/pull/699) - Improve plugin submenu ux [`#699`](https://github.com/th-ch/youtube-music/pull/699)
@ -154,21 +173,27 @@ All notable changes to this project will be documented in this file. Dates are d
- Add plugin to bypass age restrictions [`#682`](https://github.com/th-ch/youtube-music/pull/682) - Add plugin to bypass age restrictions [`#682`](https://github.com/th-ch/youtube-music/pull/682)
- Add "Picture in picture" plugin [`#674`](https://github.com/th-ch/youtube-music/pull/674) - Add "Picture in picture" plugin [`#674`](https://github.com/th-ch/youtube-music/pull/674)
- Set lyrics metadata from Genius [`#679`](https://github.com/th-ch/youtube-music/pull/679) - Set lyrics metadata from Genius [`#679`](https://github.com/th-ch/youtube-music/pull/679)
- MacOS: bring back the app in dock when using tray + app hidden [`#677`](https://github.com/th-ch/youtube-music/pull/677) - MacOS: bring back the app in dock when using tray + app
hidden [`#677`](https://github.com/th-ch/youtube-music/pull/677)
- [Snyk] Upgrade @electron/remote from 2.0.4 to 2.0.5 [`#644`](https://github.com/th-ch/youtube-music/pull/644) - [Snyk] Upgrade @electron/remote from 2.0.4 to 2.0.5 [`#644`](https://github.com/th-ch/youtube-music/pull/644)
- [Snyk] Upgrade ytpl from 2.2.3 to 2.3.0 [`#660`](https://github.com/th-ch/youtube-music/pull/660) - [Snyk] Upgrade ytpl from 2.2.3 to 2.3.0 [`#660`](https://github.com/th-ch/youtube-music/pull/660)
- [Snyk] Upgrade ytdl-core from 4.10.1 to 4.11.0 [`#659`](https://github.com/th-ch/youtube-music/pull/659) - [Snyk] Upgrade ytdl-core from 4.10.1 to 4.11.0 [`#659`](https://github.com/th-ch/youtube-music/pull/659)
- Bump plist from 3.0.2 to 3.0.5 [`#678`](https://github.com/th-ch/youtube-music/pull/678) - Bump plist from 3.0.2 to 3.0.5 [`#678`](https://github.com/th-ch/youtube-music/pull/678)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.4 to 1.23.5 [`#624`](https://github.com/th-ch/youtube-music/pull/624) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.4 to
1.23.5 [`#624`](https://github.com/th-ch/youtube-music/pull/624)
- [Precise-Volume] fix volumeHud position in miniplayer [`#645`](https://github.com/th-ch/youtube-music/pull/645) - [Precise-Volume] fix volumeHud position in miniplayer [`#645`](https://github.com/th-ch/youtube-music/pull/645)
- add always-on-top option [`#655`](https://github.com/th-ch/youtube-music/pull/655) - add always-on-top option [`#655`](https://github.com/th-ch/youtube-music/pull/655)
- [precise-volume] fix expand-volume-slider not updating its value [`#670`](https://github.com/th-ch/youtube-music/pull/670) - [precise-volume] fix expand-volume-slider not updating its
value [`#670`](https://github.com/th-ch/youtube-music/pull/670)
- Fix lyrics genius missing parts [`#671`](https://github.com/th-ch/youtube-music/pull/671) - Fix lyrics genius missing parts [`#671`](https://github.com/th-ch/youtube-music/pull/671)
- feat: option to force show like buttons [`#673`](https://github.com/th-ch/youtube-music/pull/673) - feat: option to force show like buttons [`#673`](https://github.com/th-ch/youtube-music/pull/673)
- fix custom titlebar in prompt options [`#619`](https://github.com/th-ch/youtube-music/pull/619) - fix custom titlebar in prompt options [`#619`](https://github.com/th-ch/youtube-music/pull/619)
- Process lyrics HTML in Genius util [`d0532d6`](https://github.com/th-ch/youtube-music/commit/d0532d691e56f955ef0b41f5fe2efe6295dddf9e) - Process lyrics HTML in Genius
- Create first version of picture in picture plugin [`d2265b5`](https://github.com/th-ch/youtube-music/commit/d2265b59d78143cf51fe4dc3d5dee9da66873cc1) util [`d0532d6`](https://github.com/th-ch/youtube-music/commit/d0532d691e56f955ef0b41f5fe2efe6295dddf9e)
- Bump electron-builder to fix Mac build script [`ae8365f`](https://github.com/th-ch/youtube-music/commit/ae8365f721eafda6c502d02eee86d098f2b9e2a1) - Create first version of picture in picture
plugin [`d2265b5`](https://github.com/th-ch/youtube-music/commit/d2265b59d78143cf51fe4dc3d5dee9da66873cc1)
- Bump electron-builder to fix Mac build
script [`ae8365f`](https://github.com/th-ch/youtube-music/commit/ae8365f721eafda6c502d02eee86d098f2b9e2a1)
#### [v1.16.0](https://github.com/th-ch/youtube-music/compare/v1.15.0...v1.16.0) #### [v1.16.0](https://github.com/th-ch/youtube-music/compare/v1.15.0...v1.16.0)
@ -177,7 +202,8 @@ All notable changes to this project will be documented in this file. Dates are d
- update in-app-menu [`#596`](https://github.com/th-ch/youtube-music/pull/596) - update in-app-menu [`#596`](https://github.com/th-ch/youtube-music/pull/596)
- Fix clientID [`#602`](https://github.com/th-ch/youtube-music/pull/602) - Fix clientID [`#602`](https://github.com/th-ch/youtube-music/pull/602)
- Add snoretoast custom compile script [`#600`](https://github.com/th-ch/youtube-music/pull/600) - Add snoretoast custom compile script [`#600`](https://github.com/th-ch/youtube-music/pull/600)
- fix interactive notifications icon + exclude platform specific plugins from build [`#591`](https://github.com/th-ch/youtube-music/pull/591) - fix interactive notifications icon + exclude platform specific plugins from
build [`#591`](https://github.com/th-ch/youtube-music/pull/591)
- Add album title to largeImage and change paused icon [`#587`](https://github.com/th-ch/youtube-music/pull/587) - Add album title to largeImage and change paused icon [`#587`](https://github.com/th-ch/youtube-music/pull/587)
- make useragent override optional [`#595`](https://github.com/th-ch/youtube-music/pull/595) - make useragent override optional [`#595`](https://github.com/th-ch/youtube-music/pull/595)
- get album name from DOM [`#588`](https://github.com/th-ch/youtube-music/pull/588) - get album name from DOM [`#588`](https://github.com/th-ch/youtube-music/pull/588)
@ -192,7 +218,8 @@ All notable changes to this project will be documented in this file. Dates are d
- fix precise-volume hud positioning [`#567`](https://github.com/th-ch/youtube-music/pull/567) - fix precise-volume hud positioning [`#567`](https://github.com/th-ch/youtube-music/pull/567)
- update electron and dependencies [`#565`](https://github.com/th-ch/youtube-music/pull/565) - update electron and dependencies [`#565`](https://github.com/th-ch/youtube-music/pull/565)
- filenamify playlist folder name [`#557`](https://github.com/th-ch/youtube-music/pull/557) - filenamify playlist folder name [`#557`](https://github.com/th-ch/youtube-music/pull/557)
- [Snyk] Security upgrade node-fetch from 2.6.6 to 2.6.7 (3.1.1 incompatible) [`#554`](https://github.com/th-ch/youtube-music/pull/554) - [Snyk] Security upgrade node-fetch from 2.6.6 to 2.6.7 (3.1.1
incompatible) [`#554`](https://github.com/th-ch/youtube-music/pull/554)
- fix app starting offscreen [`#548`](https://github.com/th-ch/youtube-music/pull/548) - fix app starting offscreen [`#548`](https://github.com/th-ch/youtube-music/pull/548)
- Release Mac arm64 [`#566`](https://github.com/th-ch/youtube-music/pull/566) - Release Mac arm64 [`#566`](https://github.com/th-ch/youtube-music/pull/566)
- Build command for Apple (m1) silicon macs [`#553`](https://github.com/th-ch/youtube-music/pull/553) - Build command for Apple (m1) silicon macs [`#553`](https://github.com/th-ch/youtube-music/pull/553)
@ -202,17 +229,24 @@ All notable changes to this project will be documented in this file. Dates are d
- allow downloading playlists from popup menu [`#549`](https://github.com/th-ch/youtube-music/pull/549) - allow downloading playlists from popup menu [`#549`](https://github.com/th-ch/youtube-music/pull/549)
- xesam:artist should be a list [`#539`](https://github.com/th-ch/youtube-music/pull/539) - xesam:artist should be a list [`#539`](https://github.com/th-ch/youtube-music/pull/539)
- fix notifications showing thumbnail of last song [`#537`](https://github.com/th-ch/youtube-music/pull/537) - fix notifications showing thumbnail of last song [`#537`](https://github.com/th-ch/youtube-music/pull/537)
- Fix https://github.com/th-ch/youtube-music/pull/578#issuecomment-1035517531 [`#578`](https://github.com/th-ch/youtube-music/pull/578) -
- Add automatic changelog [`1d9bfe8`](https://github.com/th-ch/youtube-music/commit/1d9bfe8ac8869cde648164979986964baa52c2f9)
- update electron to v17.0.0 [`fef7115`](https://github.com/th-ch/youtube-music/commit/fef711549fa9862f8ea23301edde747c5802e352) Fix https://github.com/th-ch/youtube-music/pull/578#issuecomment-1035517531 [`#578`](https://github.com/th-ch/youtube-music/pull/578)
- update dependencies [`8be07bc`](https://github.com/th-ch/youtube-music/commit/8be07bcb7ad8b727d97c36aa0760aed4e2fc481f)
- Add automatic
changelog [`1d9bfe8`](https://github.com/th-ch/youtube-music/commit/1d9bfe8ac8869cde648164979986964baa52c2f9)
- update electron to
v17.0.0 [`fef7115`](https://github.com/th-ch/youtube-music/commit/fef711549fa9862f8ea23301edde747c5802e352)
- update
dependencies [`8be07bc`](https://github.com/th-ch/youtube-music/commit/8be07bcb7ad8b727d97c36aa0760aed4e2fc481f)
#### [v1.15.0](https://github.com/th-ch/youtube-music/compare/v1.14.0...v1.15.0) #### [v1.15.0](https://github.com/th-ch/youtube-music/compare/v1.14.0...v1.15.0)
> 30 December 2021 > 30 December 2021
- Switch from spectron to playwright to fix tests [`#531`](https://github.com/th-ch/youtube-music/pull/531) - Switch from spectron to playwright to fix tests [`#531`](https://github.com/th-ch/youtube-music/pull/531)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.0 to 1.23.1 [`#529`](https://github.com/th-ch/youtube-music/pull/529) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.0 to
1.23.1 [`#529`](https://github.com/th-ch/youtube-music/pull/529)
- fix precise-volume options sync [`#525`](https://github.com/th-ch/youtube-music/pull/525) - fix precise-volume options sync [`#525`](https://github.com/th-ch/youtube-music/pull/525)
- Add album art/thumbnail to discord activity [`#524`](https://github.com/th-ch/youtube-music/pull/524) - Add album art/thumbnail to discord activity [`#524`](https://github.com/th-ch/youtube-music/pull/524)
- fix skip-silences plugin [`#521`](https://github.com/th-ch/youtube-music/pull/521) - fix skip-silences plugin [`#521`](https://github.com/th-ch/youtube-music/pull/521)
@ -222,19 +256,23 @@ All notable changes to this project will be documented in this file. Dates are d
- Add "Skip silences" plugin [`#519`](https://github.com/th-ch/youtube-music/pull/519) - Add "Skip silences" plugin [`#519`](https://github.com/th-ch/youtube-music/pull/519)
- Aligned lyric design [`#510`](https://github.com/th-ch/youtube-music/pull/510) - Aligned lyric design [`#510`](https://github.com/th-ch/youtube-music/pull/510)
- Fix mpris bugs - follows #480 [`#509`](https://github.com/th-ch/youtube-music/pull/509) - Fix mpris bugs - follows #480 [`#509`](https://github.com/th-ch/youtube-music/pull/509)
- Various small fixes (discord, video-toggle, precise-volume, playback-speed, shortcuts, lyrics) [`#476`](https://github.com/th-ch/youtube-music/pull/476) - Various small fixes (discord, video-toggle, precise-volume, playback-speed, shortcuts,
lyrics) [`#476`](https://github.com/th-ch/youtube-music/pull/476)
- Mpris + obs-tuna fixes [`#480`](https://github.com/th-ch/youtube-music/pull/480) - Mpris + obs-tuna fixes [`#480`](https://github.com/th-ch/youtube-music/pull/480)
- [Snyk] Upgrade node-fetch from 2.6.5 to 2.6.6 [`#498`](https://github.com/th-ch/youtube-music/pull/498) - [Snyk] Upgrade node-fetch from 2.6.5 to 2.6.6 [`#498`](https://github.com/th-ch/youtube-music/pull/498)
- fix interaction between blur navbar & in-app-menu [`#491`](https://github.com/th-ch/youtube-music/pull/491) - fix interaction between blur navbar & in-app-menu [`#491`](https://github.com/th-ch/youtube-music/pull/491)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.7 to 1.23.0 [`#475`](https://github.com/th-ch/youtube-music/pull/475) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.7 to
1.23.0 [`#475`](https://github.com/th-ch/youtube-music/pull/475)
- New Plugin: Exponential Volume [`#488`](https://github.com/th-ch/youtube-music/pull/488) - New Plugin: Exponential Volume [`#488`](https://github.com/th-ch/youtube-music/pull/488)
- [Snyk] Upgrade electron-updater from 4.6.0 to 4.6.1 [`#474`](https://github.com/th-ch/youtube-music/pull/474) - [Snyk] Upgrade electron-updater from 4.6.0 to 4.6.1 [`#474`](https://github.com/th-ch/youtube-music/pull/474)
- Fix loadeddata/metadata video events rarely not firing (+other small fixes) [`#477`](https://github.com/th-ch/youtube-music/pull/477) - Fix loadeddata/metadata video events rarely not firing (+other small
fixes) [`#477`](https://github.com/th-ch/youtube-music/pull/477)
- fix #490 [`#490`](https://github.com/th-ch/youtube-music/issues/490) - fix #490 [`#490`](https://github.com/th-ch/youtube-music/issues/490)
- fix #472 [`#472`](https://github.com/th-ch/youtube-music/issues/472) - fix #472 [`#472`](https://github.com/th-ch/youtube-music/issues/472)
- fix mpris [`ccfe743`](https://github.com/th-ch/youtube-music/commit/ccfe7434bf708ee58156c2952234a049706edfc2) - fix mpris [`ccfe743`](https://github.com/th-ch/youtube-music/commit/ccfe7434bf708ee58156c2952234a049706edfc2)
- lint [`4362101`](https://github.com/th-ch/youtube-music/commit/4362101c0a2ebb7f0536f615cecba8a55ac96702) - lint [`4362101`](https://github.com/th-ch/youtube-music/commit/4362101c0a2ebb7f0536f615cecba8a55ac96702)
- rework songInfo pause listener [`6726e26`](https://github.com/th-ch/youtube-music/commit/6726e2600b3ca3a8d68e3e1b95b50da211fa354d) - rework songInfo pause
listener [`6726e26`](https://github.com/th-ch/youtube-music/commit/6726e2600b3ca3a8d68e3e1b95b50da211fa354d)
#### [v1.14.0](https://github.com/th-ch/youtube-music/compare/v1.13.0...v1.14.0) #### [v1.14.0](https://github.com/th-ch/youtube-music/compare/v1.13.0...v1.14.0)
@ -255,50 +293,60 @@ All notable changes to this project will be documented in this file. Dates are d
- Discord plugin: Clean Up Export (follow-up #380) [`#440`](https://github.com/th-ch/youtube-music/pull/440) - Discord plugin: Clean Up Export (follow-up #380) [`#440`](https://github.com/th-ch/youtube-music/pull/440)
- remove upgrade button + makes images unselectable [`#434`](https://github.com/th-ch/youtube-music/pull/434) - remove upgrade button + makes images unselectable [`#434`](https://github.com/th-ch/youtube-music/pull/434)
- new auto confirm when paused [`#433`](https://github.com/th-ch/youtube-music/pull/433) - new auto confirm when paused [`#433`](https://github.com/th-ch/youtube-music/pull/433)
- fix: mpris instance not registering itself and media controls [`#431`](https://github.com/th-ch/youtube-music/pull/431) - fix: mpris instance not registering itself and media
controls [`#431`](https://github.com/th-ch/youtube-music/pull/431)
- Audio compressor plugin [`#288`](https://github.com/th-ch/youtube-music/pull/288) - Audio compressor plugin [`#288`](https://github.com/th-ch/youtube-music/pull/288)
- precise-volume plugin fixes & updates [`#275`](https://github.com/th-ch/youtube-music/pull/275) - precise-volume plugin fixes & updates [`#275`](https://github.com/th-ch/youtube-music/pull/275)
- Custom Prompt for changing options [`#243`](https://github.com/th-ch/youtube-music/pull/243) - Custom Prompt for changing options [`#243`](https://github.com/th-ch/youtube-music/pull/243)
- [Snyk] Upgrade async-mutex from 0.3.1 to 0.3.2 [`#412`](https://github.com/th-ch/youtube-music/pull/412) - [Snyk] Upgrade async-mutex from 0.3.1 to 0.3.2 [`#412`](https://github.com/th-ch/youtube-music/pull/412)
- build(deps): bump tmpl from 1.0.4 to 1.0.5 [`#414`](https://github.com/th-ch/youtube-music/pull/414) - build(deps): bump tmpl from 1.0.4 to 1.0.5 [`#414`](https://github.com/th-ch/youtube-music/pull/414)
- [Snyk] Upgrade node-fetch from 2.6.1 to 2.6.2 [`#416`](https://github.com/th-ch/youtube-music/pull/416) - [Snyk] Upgrade node-fetch from 2.6.1 to 2.6.2 [`#416`](https://github.com/th-ch/youtube-music/pull/416)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.5 to 1.22.6 [`#429`](https://github.com/th-ch/youtube-music/pull/429) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.5 to
1.22.6 [`#429`](https://github.com/th-ch/youtube-music/pull/429)
- build(deps-dev): bump electron from 12.0.8 to 12.1.0 [`#430`](https://github.com/th-ch/youtube-music/pull/430) - build(deps-dev): bump electron from 12.0.8 to 12.1.0 [`#430`](https://github.com/th-ch/youtube-music/pull/430)
- Fix discord clearActivity, menu, listen along option [`#380`](https://github.com/th-ch/youtube-music/pull/380) - Fix discord clearActivity, menu, listen along option [`#380`](https://github.com/th-ch/youtube-music/pull/380)
- Bump dev deps [`41a01ba`](https://github.com/th-ch/youtube-music/commit/41a01ba58a17056ba5143fdbd10d3bae11dd8d52) - Bump dev deps [`41a01ba`](https://github.com/th-ch/youtube-music/commit/41a01ba58a17056ba5143fdbd10d3bae11dd8d52)
- Discord add reconnecting functionality [`b5fd6b4`](https://github.com/th-ch/youtube-music/commit/b5fd6b4969a318b3738583e7f33eb2c0cf295237) - Discord add reconnecting
- add custom-electron-prompt [`e4eed2e`](https://github.com/th-ch/youtube-music/commit/e4eed2e51979378e62dab902e425218cae5108dc) functionality [`b5fd6b4`](https://github.com/th-ch/youtube-music/commit/b5fd6b4969a318b3738583e7f33eb2c0cf295237)
- add
custom-electron-prompt [`e4eed2e`](https://github.com/th-ch/youtube-music/commit/e4eed2e51979378e62dab902e425218cae5108dc)
#### [v1.13.0](https://github.com/th-ch/youtube-music/compare/v1.12.2...v1.13.0) #### [v1.13.0](https://github.com/th-ch/youtube-music/compare/v1.12.2...v1.13.0)
> 19 September 2021 > 19 September 2021
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.4 to 1.22.5 [`#406`](https://github.com/th-ch/youtube-music/pull/406) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.4 to
- Fix incorrect Google alert caused by changing user agent coresponding to current platform [`#384`](https://github.com/th-ch/youtube-music/pull/384) 1.22.5 [`#406`](https://github.com/th-ch/youtube-music/pull/406)
- Fix incorrect Google alert caused by changing user agent coresponding to current
platform [`#384`](https://github.com/th-ch/youtube-music/pull/384)
- [Snyk] Upgrade electron-updater from 4.4.3 to 4.4.6 [`#401`](https://github.com/th-ch/youtube-music/pull/401) - [Snyk] Upgrade electron-updater from 4.4.3 to 4.4.6 [`#401`](https://github.com/th-ch/youtube-music/pull/401)
- [Snyk] Upgrade electron-updater from 4.4.0 to 4.4.1 [`#370`](https://github.com/th-ch/youtube-music/pull/370) - [Snyk] Upgrade electron-updater from 4.4.0 to 4.4.1 [`#370`](https://github.com/th-ch/youtube-music/pull/370)
- Bump path-parse from 1.0.6 to 1.0.7 [`#375`](https://github.com/th-ch/youtube-music/pull/375) - Bump path-parse from 1.0.6 to 1.0.7 [`#375`](https://github.com/th-ch/youtube-music/pull/375)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.2 to 1.22.3 [`#385`](https://github.com/th-ch/youtube-music/pull/385) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.2 to
1.22.3 [`#385`](https://github.com/th-ch/youtube-music/pull/385)
- Bump jszip from 3.5.0 to 3.7.1 [`#388`](https://github.com/th-ch/youtube-music/pull/388) - Bump jszip from 3.5.0 to 3.7.1 [`#388`](https://github.com/th-ch/youtube-music/pull/388)
- List missing plugins [`#382`](https://github.com/th-ch/youtube-music/pull/382) - List missing plugins [`#382`](https://github.com/th-ch/youtube-music/pull/382)
- add tuna plugin for obs [`#397`](https://github.com/th-ch/youtube-music/pull/397) - add tuna plugin for obs [`#397`](https://github.com/th-ch/youtube-music/pull/397)
- Update menu buttons to new format [`#389`](https://github.com/th-ch/youtube-music/pull/389) - Update menu buttons to new format [`#389`](https://github.com/th-ch/youtube-music/pull/389)
- Plugin to fetch lyrics from Genius [`#387`](https://github.com/th-ch/youtube-music/pull/387) - Plugin to fetch lyrics from Genius [`#387`](https://github.com/th-ch/youtube-music/pull/387)
- Add mpris support with cherry picked commit from previous PR https://github.com/th-ch/youtube-music/pull/394 [`#395`](https://github.com/th-ch/youtube-music/pull/395) - Add mpris support with cherry picked commit from previous
PR https://github.com/th-ch/youtube-music/pull/394 [`#395`](https://github.com/th-ch/youtube-music/pull/395)
- Add "Listen Along" button, solve #353 [`#383`](https://github.com/th-ch/youtube-music/pull/383) - Add "Listen Along" button, solve #353 [`#383`](https://github.com/th-ch/youtube-music/pull/383)
- Bump node to v14 [`#386`](https://github.com/th-ch/youtube-music/pull/386) - Bump node to v14 [`#386`](https://github.com/th-ch/youtube-music/pull/386)
- [Snyk] Upgrade electron-updater from 4.3.9 to 4.3.10 [`#350`](https://github.com/th-ch/youtube-music/pull/350) - [Snyk] Upgrade electron-updater from 4.3.9 to 4.3.10 [`#350`](https://github.com/th-ch/youtube-music/pull/350)
- [Snyk] Upgrade chokidar from 3.5.1 to 3.5.2 [`#354`](https://github.com/th-ch/youtube-music/pull/354) - [Snyk] Upgrade chokidar from 3.5.1 to 3.5.2 [`#354`](https://github.com/th-ch/youtube-music/pull/354)
- Bump ytdl/ytpl [`c01506d`](https://github.com/th-ch/youtube-music/commit/c01506dc441bfc538471dc2c552c1a8a2800c611) - Bump ytdl/ytpl [`c01506d`](https://github.com/th-ch/youtube-music/commit/c01506dc441bfc538471dc2c552c1a8a2800c611)
- Add mpris support [`e255777`](https://github.com/th-ch/youtube-music/commit/e255777283c7b16611404cbfe260bfcca75a1e40) - Add mpris support [`e255777`](https://github.com/th-ch/youtube-music/commit/e255777283c7b16611404cbfe260bfcca75a1e40)
- Add Genius lyrics plugin [`acbe0ac`](https://github.com/th-ch/youtube-music/commit/acbe0ac25d568c25fedb514e0e96c66497b0f2d6) - Add Genius lyrics
plugin [`acbe0ac`](https://github.com/th-ch/youtube-music/commit/acbe0ac25d568c25fedb514e0e96c66497b0f2d6)
#### [v1.12.2](https://github.com/th-ch/youtube-music/compare/v1.12.1...v1.12.2) #### [v1.12.2](https://github.com/th-ch/youtube-music/compare/v1.12.1...v1.12.2)
> 1 July 2021 > 1 July 2021
- Fix downloader plugin [`#339`](https://github.com/th-ch/youtube-music/pull/339) - Fix downloader plugin [`#339`](https://github.com/th-ch/youtube-music/pull/339)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.0 to 1.22.1 [`#337`](https://github.com/th-ch/youtube-music/pull/337) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.22.0 to
1.22.1 [`#337`](https://github.com/th-ch/youtube-music/pull/337)
- Update and simplify in-app-menu [`#249`](https://github.com/th-ch/youtube-music/pull/249) - Update and simplify in-app-menu [`#249`](https://github.com/th-ch/youtube-music/pull/249)
- Bump hosted-git-info from 2.8.8 to 2.8.9 [`#331`](https://github.com/th-ch/youtube-music/pull/331) - Bump hosted-git-info from 2.8.8 to 2.8.9 [`#331`](https://github.com/th-ch/youtube-music/pull/331)
- Bump lodash from 4.17.20 to 4.17.21 [`#330`](https://github.com/th-ch/youtube-music/pull/330) - Bump lodash from 4.17.20 to 4.17.21 [`#330`](https://github.com/th-ch/youtube-music/pull/330)
@ -309,12 +357,16 @@ All notable changes to this project will be documented in this file. Dates are d
- [Snyk] Upgrade @ffmpeg/core from 0.9.0 to 0.10.0 [`#317`](https://github.com/th-ch/youtube-music/pull/317) - [Snyk] Upgrade @ffmpeg/core from 0.9.0 to 0.10.0 [`#317`](https://github.com/th-ch/youtube-music/pull/317)
- [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.8 to 0.10.0 [`#316`](https://github.com/th-ch/youtube-music/pull/316) - [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.8 to 0.10.0 [`#316`](https://github.com/th-ch/youtube-music/pull/316)
- [Snyk] Upgrade custom-electron-titlebar from 3.2.6 to 3.2.7 [`#311`](https://github.com/th-ch/youtube-music/pull/311) - [Snyk] Upgrade custom-electron-titlebar from 3.2.6 to 3.2.7 [`#311`](https://github.com/th-ch/youtube-music/pull/311)
- fix hidden webp thumbnail throwing MIME type error in downloader [`#318`](https://github.com/th-ch/youtube-music/pull/318) - fix hidden webp thumbnail throwing MIME type error in
downloader [`#318`](https://github.com/th-ch/youtube-music/pull/318)
- Add Sponsorblock plugin [`#308`](https://github.com/th-ch/youtube-music/pull/308) - Add Sponsorblock plugin [`#308`](https://github.com/th-ch/youtube-music/pull/308)
- [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.7 to 0.9.8 [`#305`](https://github.com/th-ch/youtube-music/pull/305) - [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.7 to 0.9.8 [`#305`](https://github.com/th-ch/youtube-music/pull/305)
- Bump dependencies to fix vulnerabilities [`496836b`](https://github.com/th-ch/youtube-music/commit/496836b33b116e06b8d1361ce1f47ab6c9138cae) - Bump dependencies to fix
- update refreshMenu() function [`33855f1`](https://github.com/th-ch/youtube-music/commit/33855f17dd80c099117a3d84bbd9b5021776771c) vulnerabilities [`496836b`](https://github.com/th-ch/youtube-music/commit/496836b33b116e06b8d1361ce1f47ab6c9138cae)
- Add SponsorBlock plugin [`ca64a77`](https://github.com/th-ch/youtube-music/commit/ca64a77ed0236fd9cfb4b40e450578a186638dc7) - update refreshMenu()
function [`33855f1`](https://github.com/th-ch/youtube-music/commit/33855f17dd80c099117a3d84bbd9b5021776771c)
- Add SponsorBlock
plugin [`ca64a77`](https://github.com/th-ch/youtube-music/commit/ca64a77ed0236fd9cfb4b40e450578a186638dc7)
#### [v1.12.1](https://github.com/th-ch/youtube-music/compare/v1.12.0...v1.12.1) #### [v1.12.1](https://github.com/th-ch/youtube-music/compare/v1.12.0...v1.12.1)
@ -322,13 +374,15 @@ All notable changes to this project will be documented in this file. Dates are d
- Bump ws from 7.4.3 to 7.4.6 [`#303`](https://github.com/th-ch/youtube-music/pull/303) - Bump ws from 7.4.3 to 7.4.6 [`#303`](https://github.com/th-ch/youtube-music/pull/303)
- Bump browserslist from 4.16.3 to 4.16.6 [`#301`](https://github.com/th-ch/youtube-music/pull/301) - Bump browserslist from 4.16.3 to 4.16.6 [`#301`](https://github.com/th-ch/youtube-music/pull/301)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.20.4 to 1.20.5 [`#300`](https://github.com/th-ch/youtube-music/pull/300) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.20.4 to
1.20.5 [`#300`](https://github.com/th-ch/youtube-music/pull/300)
- [Snyk] Upgrade ytdl-core from 4.5.0 to 4.7.0 [`#299`](https://github.com/th-ch/youtube-music/pull/299) - [Snyk] Upgrade ytdl-core from 4.5.0 to 4.7.0 [`#299`](https://github.com/th-ch/youtube-music/pull/299)
- [Snyk] Upgrade @ffmpeg/core from 0.8.5 to 0.9.0 [`#298`](https://github.com/th-ch/youtube-music/pull/298) - [Snyk] Upgrade @ffmpeg/core from 0.8.5 to 0.9.0 [`#298`](https://github.com/th-ch/youtube-music/pull/298)
- [Snyk] Upgrade filenamify from 4.2.0 to 4.3.0 [`#293`](https://github.com/th-ch/youtube-music/pull/293) - [Snyk] Upgrade filenamify from 4.2.0 to 4.3.0 [`#293`](https://github.com/th-ch/youtube-music/pull/293)
- [Snyk] Upgrade ytpl from 2.1.1 to 2.2.0 [`#285`](https://github.com/th-ch/youtube-music/pull/285) - [Snyk] Upgrade ytpl from 2.1.1 to 2.2.0 [`#285`](https://github.com/th-ch/youtube-music/pull/285)
- fix song-info callback duplication [`#269`](https://github.com/th-ch/youtube-music/pull/269) - fix song-info callback duplication [`#269`](https://github.com/th-ch/youtube-music/pull/269)
- fix notification showing appID instead of app name on windows [`#270`](https://github.com/th-ch/youtube-music/pull/270) - fix notification showing appID instead of app name on
windows [`#270`](https://github.com/th-ch/youtube-music/pull/270)
- Upgrade electron to v12 [`#273`](https://github.com/th-ch/youtube-music/pull/273) - Upgrade electron to v12 [`#273`](https://github.com/th-ch/youtube-music/pull/273)
- fix last-fm overwrite config on each start [`#267`](https://github.com/th-ch/youtube-music/pull/267) - fix last-fm overwrite config on each start [`#267`](https://github.com/th-ch/youtube-music/pull/267)
- Downloader tweaks + taskbar progress bar [`#265`](https://github.com/th-ch/youtube-music/pull/265) - Downloader tweaks + taskbar progress bar [`#265`](https://github.com/th-ch/youtube-music/pull/265)
@ -338,9 +392,12 @@ All notable changes to this project will be documented in this file. Dates are d
- Bump ua-parser-js from 0.7.23 to 0.7.28 [`#260`](https://github.com/th-ch/youtube-music/pull/260) - Bump ua-parser-js from 0.7.23 to 0.7.28 [`#260`](https://github.com/th-ch/youtube-music/pull/260)
- Fix precise volume listener override [`#253`](https://github.com/th-ch/youtube-music/pull/253) - Fix precise volume listener override [`#253`](https://github.com/th-ch/youtube-music/pull/253)
- fix css not inserting on reload [`#255`](https://github.com/th-ch/youtube-music/pull/255) - fix css not inserting on reload [`#255`](https://github.com/th-ch/youtube-music/pull/255)
- playlist download progressBar using `chokidar` [`53bf7c5`](https://github.com/th-ch/youtube-music/commit/53bf7c5068fdc14f5aa469d47b3174d27f40e05c) - playlist download progressBar
- download progress bar on taskbar [`a8ac2c3`](https://github.com/th-ch/youtube-music/commit/a8ac2c3af988f299be85010e7fea541096b7e261) using `chokidar` [`53bf7c5`](https://github.com/th-ch/youtube-music/commit/53bf7c5068fdc14f5aa469d47b3174d27f40e05c)
- fix: upgrade @cliqz/adblocker-electron from 1.20.4 to 1.20.5 [`c5f84b5`](https://github.com/th-ch/youtube-music/commit/c5f84b568b0c3480af1abc8ff111771e2170a50e) - download progress bar on
taskbar [`a8ac2c3`](https://github.com/th-ch/youtube-music/commit/a8ac2c3af988f299be85010e7fea541096b7e261)
- fix: upgrade @cliqz/adblocker-electron from 1.20.4 to
1.20.5 [`c5f84b5`](https://github.com/th-ch/youtube-music/commit/c5f84b568b0c3480af1abc8ff111771e2170a50e)
#### [v1.12.0](https://github.com/th-ch/youtube-music/compare/v1.11.0...v1.12.0) #### [v1.12.0](https://github.com/th-ch/youtube-music/compare/v1.11.0...v1.12.0)
@ -350,7 +407,8 @@ All notable changes to this project will be documented in this file. Dates are d
- Interactive notifications for windows [`#228`](https://github.com/th-ch/youtube-music/pull/228) - Interactive notifications for windows [`#228`](https://github.com/th-ch/youtube-music/pull/228)
- [Plugin] Precise volume control [`#236`](https://github.com/th-ch/youtube-music/pull/236) - [Plugin] Precise volume control [`#236`](https://github.com/th-ch/youtube-music/pull/236)
- [Snyk] Upgrade electron-store from 7.0.2 to 7.0.3 [`#244`](https://github.com/th-ch/youtube-music/pull/244) - [Snyk] Upgrade electron-store from 7.0.2 to 7.0.3 [`#244`](https://github.com/th-ch/youtube-music/pull/244)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.20.3 to 1.20.4 [`#233`](https://github.com/th-ch/youtube-music/pull/233) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.20.3 to
1.20.4 [`#233`](https://github.com/th-ch/youtube-music/pull/233)
- Dependencies update [`#231`](https://github.com/th-ch/youtube-music/pull/231) - Dependencies update [`#231`](https://github.com/th-ch/youtube-music/pull/231)
- Fix downloader metadata [`#245`](https://github.com/th-ch/youtube-music/pull/245) - Fix downloader metadata [`#245`](https://github.com/th-ch/youtube-music/pull/245)
- Last.fm support [`#196`](https://github.com/th-ch/youtube-music/pull/196) - Last.fm support [`#196`](https://github.com/th-ch/youtube-music/pull/196)
@ -365,40 +423,53 @@ All notable changes to this project will be documented in this file. Dates are d
- [Plugin] styled-bars [`#201`](https://github.com/th-ch/youtube-music/pull/201) - [Plugin] styled-bars [`#201`](https://github.com/th-ch/youtube-music/pull/201)
- Add configurable notification urgency [`#212`](https://github.com/th-ch/youtube-music/pull/212) - Add configurable notification urgency [`#212`](https://github.com/th-ch/youtube-music/pull/212)
- add Download Folder Chooser [`#207`](https://github.com/th-ch/youtube-music/pull/207) - add Download Folder Chooser [`#207`](https://github.com/th-ch/youtube-music/pull/207)
- Improved songinfo provider, by using the data from the '/player' request [`#194`](https://github.com/th-ch/youtube-music/pull/194) - Improved songinfo provider, by using the data from the '/player'
request [`#194`](https://github.com/th-ch/youtube-music/pull/194)
- Download plugin directory chooser [`#10`](https://github.com/th-ch/youtube-music/pull/10) - Download plugin directory chooser [`#10`](https://github.com/th-ch/youtube-music/pull/10)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.20.0 to 1.20.1 [`#180`](https://github.com/th-ch/youtube-music/pull/180) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.20.0 to
1.20.1 [`#180`](https://github.com/th-ch/youtube-music/pull/180)
- [Plugin] taskbar-mediacontrol (for Windows) [`#200`](https://github.com/th-ch/youtube-music/pull/200) - [Plugin] taskbar-mediacontrol (for Windows) [`#200`](https://github.com/th-ch/youtube-music/pull/200)
- merge source [`#3`](https://github.com/th-ch/youtube-music/pull/3) - merge source [`#3`](https://github.com/th-ch/youtube-music/pull/3)
- merge source [`#2`](https://github.com/th-ch/youtube-music/pull/2) - merge source [`#2`](https://github.com/th-ch/youtube-music/pull/2)
- Add playlist feature in downloader plugin + custom menus in plugin system [`#203`](https://github.com/th-ch/youtube-music/pull/203) - Add playlist feature in downloader plugin + custom menus in plugin
system [`#203`](https://github.com/th-ch/youtube-music/pull/203)
- Added Discord timeout [`#192`](https://github.com/th-ch/youtube-music/pull/192) - Added Discord timeout [`#192`](https://github.com/th-ch/youtube-music/pull/192)
- Override hide(),show(),isVisible from inside plugin [`6427b34`](https://github.com/th-ch/youtube-music/commit/6427b3406c8d84c5b7ecbe6a28158d5dc895c3c2) - Override hide(),show(),isVisible from inside
- added back original yarn.lock [`24fea5a`](https://github.com/th-ch/youtube-music/commit/24fea5a24afd4f547628549962d24756cca5e413) plugin [`6427b34`](https://github.com/th-ch/youtube-music/commit/6427b3406c8d84c5b7ecbe6a28158d5dc895c3c2)
- remove local prompt [`8dc486f`](https://github.com/th-ch/youtube-music/commit/8dc486f18fe02a218b149838dc7ab939ec1b698a) - added back original
yarn.lock [`24fea5a`](https://github.com/th-ch/youtube-music/commit/24fea5a24afd4f547628549962d24756cca5e413)
- remove local
prompt [`8dc486f`](https://github.com/th-ch/youtube-music/commit/8dc486f18fe02a218b149838dc7ab939ec1b698a)
#### [v1.11.0](https://github.com/th-ch/youtube-music/compare/v1.10.0...v1.11.0) #### [v1.11.0](https://github.com/th-ch/youtube-music/compare/v1.10.0...v1.11.0)
> 9 March 2021 > 9 March 2021
- [Snyk] Upgrade electron-store from 7.0.1 to 7.0.2 [`#178`](https://github.com/th-ch/youtube-music/pull/178) - [Snyk] Upgrade electron-store from 7.0.1 to 7.0.2 [`#178`](https://github.com/th-ch/youtube-music/pull/178)
- Added function to toggle resuming of last song when app starts [`#177`](https://github.com/th-ch/youtube-music/pull/177) - Added function to toggle resuming of last song when app
starts [`#177`](https://github.com/th-ch/youtube-music/pull/177)
- [Snyk] Upgrade discord-rpc from 3.1.4 to 3.2.0 [`#175`](https://github.com/th-ch/youtube-music/pull/175) - [Snyk] Upgrade discord-rpc from 3.1.4 to 3.2.0 [`#175`](https://github.com/th-ch/youtube-music/pull/175)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.19.0 to 1.20.0 [`#154`](https://github.com/th-ch/youtube-music/pull/154) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.19.0 to
- Added metadata to downloader plugin, and updated packages [`dd1bdae`](https://github.com/th-ch/youtube-music/commit/dd1bdae9478ef831ee2a00b29be04c65626933f8) 1.20.0 [`#154`](https://github.com/th-ch/youtube-music/pull/154)
- Fix download/speed menu item [`796a7aa`](https://github.com/th-ch/youtube-music/commit/796a7aaaf1ecaf80b2ef113137f2222499803e29) - Added metadata to downloader plugin, and updated
- fix: upgrade @cliqz/adblocker-electron from 1.19.0 to 1.20.0 [`538ab52`](https://github.com/th-ch/youtube-music/commit/538ab52abd46c2e3c6abb529c5137b5286d29670) packages [`dd1bdae`](https://github.com/th-ch/youtube-music/commit/dd1bdae9478ef831ee2a00b29be04c65626933f8)
- Fix download/speed menu
item [`796a7aa`](https://github.com/th-ch/youtube-music/commit/796a7aaaf1ecaf80b2ef113137f2222499803e29)
- fix: upgrade @cliqz/adblocker-electron from 1.19.0 to
1.20.0 [`538ab52`](https://github.com/th-ch/youtube-music/commit/538ab52abd46c2e3c6abb529c5137b5286d29670)
#### [v1.10.0](https://github.com/th-ch/youtube-music/compare/v1.9.0...v1.10.0) #### [v1.10.0](https://github.com/th-ch/youtube-music/compare/v1.9.0...v1.10.0)
> 7 February 2021 > 7 February 2021
- [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.6 to 0.9.7 [`#146`](https://github.com/th-ch/youtube-music/pull/146) - [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.6 to 0.9.7 [`#146`](https://github.com/th-ch/youtube-music/pull/146)
- Reuse the same notification, instead of creating a new one each time the song changes. [`#144`](https://github.com/th-ch/youtube-music/pull/144) - Reuse the same notification, instead of creating a new one each time the song
changes. [`#144`](https://github.com/th-ch/youtube-music/pull/144)
- [Snyk] Upgrade ytdl-core from 4.2.1 to 4.3.0 [`#136`](https://github.com/th-ch/youtube-music/pull/136) - [Snyk] Upgrade ytdl-core from 4.2.1 to 4.3.0 [`#136`](https://github.com/th-ch/youtube-music/pull/136)
- bring the new commits to this fork [`#1`](https://github.com/th-ch/youtube-music/pull/1) - bring the new commits to this fork [`#1`](https://github.com/th-ch/youtube-music/pull/1)
- GH page [`3bcf409`](https://github.com/th-ch/youtube-music/commit/3bcf409f2b1629333714b187c606891cedb12512) - GH page [`3bcf409`](https://github.com/th-ch/youtube-music/commit/3bcf409f2b1629333714b187c606891cedb12512)
- Add plugin to control playback speed like in YouTube (from 0.25 to 2) [`f7f3185`](https://github.com/th-ch/youtube-music/commit/f7f31850d3d9879002dc47326e4f6ec9a52c25a1) - Add plugin to control playback speed like in YouTube (from 0.25 to
2) [`f7f3185`](https://github.com/th-ch/youtube-music/commit/f7f31850d3d9879002dc47326e4f6ec9a52c25a1)
- Update back.js [`1fdf241`](https://github.com/th-ch/youtube-music/commit/1fdf2416ad414035104bfb51b8450d82e566cb13) - Update back.js [`1fdf241`](https://github.com/th-ch/youtube-music/commit/1fdf2416ad414035104bfb51b8450d82e566cb13)
#### [v1.9.0](https://github.com/th-ch/youtube-music/compare/v1.8.2...v1.9.0) #### [v1.9.0](https://github.com/th-ch/youtube-music/compare/v1.8.2...v1.9.0)
@ -407,35 +478,47 @@ All notable changes to this project will be documented in this file. Dates are d
- [Snyk] Upgrade electron-debug from 3.1.0 to 3.2.0 [`#121`](https://github.com/th-ch/youtube-music/pull/121) - [Snyk] Upgrade electron-debug from 3.1.0 to 3.2.0 [`#121`](https://github.com/th-ch/youtube-music/pull/121)
- Refactor providers [`#125`](https://github.com/th-ch/youtube-music/pull/125) - Refactor providers [`#125`](https://github.com/th-ch/youtube-music/pull/125)
- Added Discord rich presence and added extra properties to songInfo provider [`#124`](https://github.com/th-ch/youtube-music/pull/124) - Added Discord rich presence and added extra properties to songInfo
provider [`#124`](https://github.com/th-ch/youtube-music/pull/124)
- Fix plugins with context isolation [`#127`](https://github.com/th-ch/youtube-music/pull/127) - Fix plugins with context isolation [`#127`](https://github.com/th-ch/youtube-music/pull/127)
- Windows portable exe [`#126`](https://github.com/th-ch/youtube-music/pull/126) - Windows portable exe [`#126`](https://github.com/th-ch/youtube-music/pull/126)
- Split providers in 2 [`0743034`](https://github.com/th-ch/youtube-music/commit/0743034de0443e889ec11d7ea83727ff4fb96599) - Split providers in
- Added Discord rich presence and added extra properties to songinfo provider [`a8ce87f`](https://github.com/th-ch/youtube-music/commit/a8ce87f2ccb4f0fdbd36676883e6a0497bebc263) 2 [`0743034`](https://github.com/th-ch/youtube-music/commit/0743034de0443e889ec11d7ea83727ff4fb96599)
- Update discord plugin for new provider + wait for ready [`aec542e`](https://github.com/th-ch/youtube-music/commit/aec542e95e2837f54bf19de675f311444789ea4e) - Added Discord rich presence and added extra properties to songinfo
provider [`a8ce87f`](https://github.com/th-ch/youtube-music/commit/a8ce87f2ccb4f0fdbd36676883e6a0497bebc263)
- Update discord plugin for new provider + wait for
ready [`aec542e`](https://github.com/th-ch/youtube-music/commit/aec542e95e2837f54bf19de675f311444789ea4e)
#### [v1.8.2](https://github.com/th-ch/youtube-music/compare/v1.8.1...v1.8.2) #### [v1.8.2](https://github.com/th-ch/youtube-music/compare/v1.8.1...v1.8.2)
> 12 January 2021 > 12 January 2021
- Downloader plugin - custom audio format [`#118`](https://github.com/th-ch/youtube-music/pull/118) - Downloader plugin - custom audio format [`#118`](https://github.com/th-ch/youtube-music/pull/118)
- Globalized the song info and song controls, and updated Touch Bar for it. [`#102`](https://github.com/th-ch/youtube-music/pull/102) - Globalized the song info and song controls, and updated Touch Bar for
it. [`#102`](https://github.com/th-ch/youtube-music/pull/102)
- Bump electron to v11 [`#120`](https://github.com/th-ch/youtube-music/pull/120) - Bump electron to v11 [`#120`](https://github.com/th-ch/youtube-music/pull/120)
- Globalized the songinfo and song controls, and changed the pause/play button. [`9be3e1a`](https://github.com/th-ch/youtube-music/commit/9be3e1afe91f0aa3419040bba65e7b3b83b469c6) - Globalized the songinfo and song controls, and changed the pause/play
- Simplifies the notification plugin to use the globalized song info [`5bffdbd`](https://github.com/th-ch/youtube-music/commit/5bffdbd6285a6816749c467d6e912d14748f9959) button. [`9be3e1a`](https://github.com/th-ch/youtube-music/commit/9be3e1afe91f0aa3419040bba65e7b3b83b469c6)
- Loads providers before plugins [`3a5d9bd`](https://github.com/th-ch/youtube-music/commit/3a5d9bd973bdd67e77f8a7687c1430245a9490bd) - Simplifies the notification plugin to use the globalized song
info [`5bffdbd`](https://github.com/th-ch/youtube-music/commit/5bffdbd6285a6816749c467d6e912d14748f9959)
- Loads providers before
plugins [`3a5d9bd`](https://github.com/th-ch/youtube-music/commit/3a5d9bd973bdd67e77f8a7687c1430245a9490bd)
#### [v1.8.1](https://github.com/th-ch/youtube-music/compare/v1.8.0...v1.8.1) #### [v1.8.1](https://github.com/th-ch/youtube-music/compare/v1.8.0...v1.8.1)
> 8 January 2021 > 8 January 2021
- [Snyk] Upgrade electron-updater from 4.3.5 to 4.3.6 [`#116`](https://github.com/th-ch/youtube-music/pull/116) - [Snyk] Upgrade electron-updater from 4.3.5 to 4.3.6 [`#116`](https://github.com/th-ch/youtube-music/pull/116)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.18.8 to 1.19.0 [`#117`](https://github.com/th-ch/youtube-music/pull/117) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.18.8 to
1.19.0 [`#117`](https://github.com/th-ch/youtube-music/pull/117)
- [Snyk] Upgrade ytdl-core from 4.1.1 to 4.1.2 [`#109`](https://github.com/th-ch/youtube-music/pull/109) - [Snyk] Upgrade ytdl-core from 4.1.1 to 4.1.2 [`#109`](https://github.com/th-ch/youtube-music/pull/109)
- Bump node-notifier from 8.0.0 to 8.0.1 [`#104`](https://github.com/th-ch/youtube-music/pull/104) - Bump node-notifier from 8.0.0 to 8.0.1 [`#104`](https://github.com/th-ch/youtube-music/pull/104)
- fix: upgrade electron-updater from 4.3.5 to 4.3.6 [`0bf77e5`](https://github.com/th-ch/youtube-music/commit/0bf77e592a87eb8a5222cf2c1588488a51044422) - fix: upgrade electron-updater from 4.3.5 to
- fix: upgrade @cliqz/adblocker-electron from 1.18.8 to 1.19.0 [`5c0cc08`](https://github.com/th-ch/youtube-music/commit/5c0cc08d80d60c46e8b27343c6fc302f64fe89e2) 4.3.6 [`0bf77e5`](https://github.com/th-ch/youtube-music/commit/0bf77e592a87eb8a5222cf2c1588488a51044422)
- fix: upgrade ytdl-core from 4.1.1 to 4.1.2 [`e2cc262`](https://github.com/th-ch/youtube-music/commit/e2cc2628aea653739f878ec2cd2e72e2e70018a1) - fix: upgrade @cliqz/adblocker-electron from 1.18.8 to
1.19.0 [`5c0cc08`](https://github.com/th-ch/youtube-music/commit/5c0cc08d80d60c46e8b27343c6fc302f64fe89e2)
- fix: upgrade ytdl-core from 4.1.1 to
4.1.2 [`e2cc262`](https://github.com/th-ch/youtube-music/commit/e2cc2628aea653739f878ec2cd2e72e2e70018a1)
#### [v1.8.0](https://github.com/th-ch/youtube-music/compare/v1.7.5...v1.8.0) #### [v1.8.0](https://github.com/th-ch/youtube-music/compare/v1.7.5...v1.8.0)
@ -446,9 +529,12 @@ All notable changes to this project will be documented in this file. Dates are d
- [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.5 to 0.9.6 [`#100`](https://github.com/th-ch/youtube-music/pull/100) - [Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.5 to 0.9.6 [`#100`](https://github.com/th-ch/youtube-music/pull/100)
- [Readme] Web folder for readme assets + new SVG animation [`#96`](https://github.com/th-ch/youtube-music/pull/96) - [Readme] Web folder for readme assets + new SVG animation [`#96`](https://github.com/th-ch/youtube-music/pull/96)
- Add new Linux targets (deb, freebsd, rpm) [`#94`](https://github.com/th-ch/youtube-music/pull/94) - Add new Linux targets (deb, freebsd, rpm) [`#94`](https://github.com/th-ch/youtube-music/pull/94)
- Web folder for readme assets + new svg animation [`01fc965`](https://github.com/th-ch/youtube-music/commit/01fc9651705f457da63615ff774f00957f783d3d) - Web folder for readme assets + new svg
- touchbar plugin - fixed code style [`7473677`](https://github.com/th-ch/youtube-music/commit/7473677477071ca5e7b18bda3193e345d7fd549f) animation [`01fc965`](https://github.com/th-ch/youtube-music/commit/01fc9651705f457da63615ff774f00957f783d3d)
- added initial touchbar support [`c3e2c13`](https://github.com/th-ch/youtube-music/commit/c3e2c1380810d156d9d6863fffc804242171bec0) - touchbar plugin - fixed code
style [`7473677`](https://github.com/th-ch/youtube-music/commit/7473677477071ca5e7b18bda3193e345d7fd549f)
- added initial touchbar
support [`c3e2c13`](https://github.com/th-ch/youtube-music/commit/c3e2c1380810d156d9d6863fffc804242171bec0)
#### [v1.7.5](https://github.com/th-ch/youtube-music/compare/v1.7.4...v1.7.5) #### [v1.7.5](https://github.com/th-ch/youtube-music/compare/v1.7.4...v1.7.5)
@ -456,9 +542,12 @@ All notable changes to this project will be documented in this file. Dates are d
- Bump ini from 1.3.5 to 1.3.7 [`#92`](https://github.com/th-ch/youtube-music/pull/92) - Bump ini from 1.3.5 to 1.3.7 [`#92`](https://github.com/th-ch/youtube-music/pull/92)
- Fix adblocking [`#90`](https://github.com/th-ch/youtube-music/pull/90) - Fix adblocking [`#90`](https://github.com/th-ch/youtube-music/pull/90)
- Bump adblocker dependency [`49497d0`](https://github.com/th-ch/youtube-music/commit/49497d0efb28ee0be5b16d0f1c3660efafcd289c) - Bump adblocker
- Fix adblocker preloading to inject scripts/styles [`66c5ce4`](https://github.com/th-ch/youtube-music/commit/66c5ce46caa85a7ae4ceb3d63a9e168827015c71) dependency [`49497d0`](https://github.com/th-ch/youtube-music/commit/49497d0efb28ee0be5b16d0f1c3660efafcd289c)
- Add uBlock Origin filters to default sources [`79c7959`](https://github.com/th-ch/youtube-music/commit/79c795927a3be96456a2f45159285c64166a29b8) - Fix adblocker preloading to inject
scripts/styles [`66c5ce4`](https://github.com/th-ch/youtube-music/commit/66c5ce46caa85a7ae4ceb3d63a9e168827015c71)
- Add uBlock Origin filters to default
sources [`79c7959`](https://github.com/th-ch/youtube-music/commit/79c795927a3be96456a2f45159285c64166a29b8)
#### [v1.7.4](https://github.com/th-ch/youtube-music/compare/v1.7.3...v1.7.4) #### [v1.7.4](https://github.com/th-ch/youtube-music/compare/v1.7.3...v1.7.4)
@ -468,32 +557,41 @@ All notable changes to this project will be documented in this file. Dates are d
> 8 December 2020 > 8 December 2020
- Adblocker: add option to disable default lists [`22c7f70`](https://github.com/th-ch/youtube-music/commit/22c7f70c938566a9db9c4d46a57224cfdee43df0) - Adblocker: add option to disable default
lists [`22c7f70`](https://github.com/th-ch/youtube-music/commit/22c7f70c938566a9db9c4d46a57224cfdee43df0)
#### [v1.7.2](https://github.com/th-ch/youtube-music/compare/v1.7.1...v1.7.2) #### [v1.7.2](https://github.com/th-ch/youtube-music/compare/v1.7.1...v1.7.2)
> 6 December 2020 > 6 December 2020
- Add AUR badge + beautify badges [`#82`](https://github.com/th-ch/youtube-music/pull/82) - Add AUR badge + beautify badges [`#82`](https://github.com/th-ch/youtube-music/pull/82)
- Bugfix: only use cache with no additional blocklists [`467171a`](https://github.com/th-ch/youtube-music/commit/467171a17e648331d63f166c2da2f3134e95b37f) - Bugfix: only use cache with no additional
- Add AUR tag + beautify tags [`d212206`](https://github.com/th-ch/youtube-music/commit/d21220693b9ffa26e05fe1963376b636b40b9952) blocklists [`467171a`](https://github.com/th-ch/youtube-music/commit/467171a17e648331d63f166c2da2f3134e95b37f)
- Readme: add youtube-music logo to badges [`3022fac`](https://github.com/th-ch/youtube-music/commit/3022facbead40ccd81629c37b870ab33ce7fa106) - Add AUR tag + beautify
tags [`d212206`](https://github.com/th-ch/youtube-music/commit/d21220693b9ffa26e05fe1963376b636b40b9952)
- Readme: add youtube-music logo to
badges [`3022fac`](https://github.com/th-ch/youtube-music/commit/3022facbead40ccd81629c37b870ab33ce7fa106)
#### [v1.7.1](https://github.com/th-ch/youtube-music/compare/v1.7.0...v1.7.1) #### [v1.7.1](https://github.com/th-ch/youtube-music/compare/v1.7.0...v1.7.1)
> 3 December 2020 > 3 December 2020
- Option to restart the app on config changes [`fd97576`](https://github.com/th-ch/youtube-music/commit/fd97576611ae80b959ffe7984e88ddc8d28a1ffc) - Option to restart the app on config
- Bump version to 1.7.1 [`e07cac2`](https://github.com/th-ch/youtube-music/commit/e07cac240691b1c9d6909e457824616182374c3a) changes [`fd97576`](https://github.com/th-ch/youtube-music/commit/fd97576611ae80b959ffe7984e88ddc8d28a1ffc)
- Bump version to
1.7.1 [`e07cac2`](https://github.com/th-ch/youtube-music/commit/e07cac240691b1c9d6909e457824616182374c3a)
#### [v1.7.0](https://github.com/th-ch/youtube-music/compare/v1.6.5...v1.7.0) #### [v1.7.0](https://github.com/th-ch/youtube-music/compare/v1.6.5...v1.7.0)
> 3 December 2020 > 3 December 2020
- Refactor config, custom plugin options [`#79`](https://github.com/th-ch/youtube-music/pull/79) - Refactor config, custom plugin options [`#79`](https://github.com/th-ch/youtube-music/pull/79)
- Refactor config for simpler use and advanced options in plugins [`8ab2da0`](https://github.com/th-ch/youtube-music/commit/8ab2da0482b6211b6b6d43423ec06daed48dac4f) - Refactor config for simpler use and advanced options in
- Allow editing config (advanced) [`f4fe5c2`](https://github.com/th-ch/youtube-music/commit/f4fe5c2a58e1ad555c321f27c00d2d78184fc687) plugins [`8ab2da0`](https://github.com/th-ch/youtube-music/commit/8ab2da0482b6211b6b6d43423ec06daed48dac4f)
- Adblocker - advanced options (caching or not, additional lists) [`b94d0d4`](https://github.com/th-ch/youtube-music/commit/b94d0d4e8bd3a92bbb5e012a63fa782baa774be7) - Allow editing config (
advanced) [`f4fe5c2`](https://github.com/th-ch/youtube-music/commit/f4fe5c2a58e1ad555c321f27c00d2d78184fc687)
- Adblocker - advanced options (caching or not, additional
lists) [`b94d0d4`](https://github.com/th-ch/youtube-music/commit/b94d0d4e8bd3a92bbb5e012a63fa782baa774be7)
#### [v1.6.5](https://github.com/th-ch/youtube-music/compare/v1.6.4...v1.6.5) #### [v1.6.5](https://github.com/th-ch/youtube-music/compare/v1.6.4...v1.6.5)
@ -504,9 +602,12 @@ All notable changes to this project will be documented in this file. Dates are d
- Reflect Arch Linux package name change [`#70`](https://github.com/th-ch/youtube-music/pull/70) - Reflect Arch Linux package name change [`#70`](https://github.com/th-ch/youtube-music/pull/70)
- Option to hide menu [`#67`](https://github.com/th-ch/youtube-music/pull/67) - Option to hide menu [`#67`](https://github.com/th-ch/youtube-music/pull/67)
- Add Arch Linux installation instructions [`#68`](https://github.com/th-ch/youtube-music/pull/68) - Add Arch Linux installation instructions [`#68`](https://github.com/th-ch/youtube-music/pull/68)
- Update ytdl-core to 4.1.1 [`33a11ef`](https://github.com/th-ch/youtube-music/commit/33a11efe9acad234e41ad9044ae9e67fd573b7f4) - Update ytdl-core to
- Autoupdate modal: add download/disable updates buttons [`ae5b85d`](https://github.com/th-ch/youtube-music/commit/ae5b85d8d748659f2e23d417560026f24ab8ce9c) 4.1.1 [`33a11ef`](https://github.com/th-ch/youtube-music/commit/33a11efe9acad234e41ad9044ae9e67fd573b7f4)
- Option to hide menu (win/linux) [`4bac3ac`](https://github.com/th-ch/youtube-music/commit/4bac3ace186c5be2cb9409d2b703f960bd662145) - Autoupdate modal: add download/disable updates
buttons [`ae5b85d`](https://github.com/th-ch/youtube-music/commit/ae5b85d8d748659f2e23d417560026f24ab8ce9c)
- Option to hide menu (
win/linux) [`4bac3ac`](https://github.com/th-ch/youtube-music/commit/4bac3ace186c5be2cb9409d2b703f960bd662145)
#### [v1.6.4](https://github.com/th-ch/youtube-music/compare/v1.6.3...v1.6.4) #### [v1.6.4](https://github.com/th-ch/youtube-music/compare/v1.6.3...v1.6.4)
@ -519,9 +620,12 @@ All notable changes to this project will be documented in this file. Dates are d
- Improve CI [`#64`](https://github.com/th-ch/youtube-music/pull/64) - Improve CI [`#64`](https://github.com/th-ch/youtube-music/pull/64)
- Ensure menu is visible on all platforms [`#63`](https://github.com/th-ch/youtube-music/pull/63) - Ensure menu is visible on all platforms [`#63`](https://github.com/th-ch/youtube-music/pull/63)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.18.3 to 1.18.4 [`#62`](https://github.com/th-ch/youtube-music/pull/62) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.18.3 to 1.18.4 [`#62`](https://github.com/th-ch/youtube-music/pull/62)
- fix: upgrade @cliqz/adblocker-electron from 1.18.3 to 1.18.4 [`2b243f6`](https://github.com/th-ch/youtube-music/commit/2b243f6dcb00d3b6f27fd066c093e7b16bb384e2) - fix: upgrade @cliqz/adblocker-electron from 1.18.3 to
- CI: cache yarn directory [`0fd4933`](https://github.com/th-ch/youtube-music/commit/0fd49330d3218ec5f1bc62b72ace28e79d02bc93) 1.18.4 [`2b243f6`](https://github.com/th-ch/youtube-music/commit/2b243f6dcb00d3b6f27fd066c093e7b16bb384e2)
- Run CI on every push/PR [`cf4827d`](https://github.com/th-ch/youtube-music/commit/cf4827d780fee510a27eecf42453b0505c52bcf9) - CI: cache yarn
directory [`0fd4933`](https://github.com/th-ch/youtube-music/commit/0fd49330d3218ec5f1bc62b72ace28e79d02bc93)
- Run CI on every
push/PR [`cf4827d`](https://github.com/th-ch/youtube-music/commit/cf4827d780fee510a27eecf42453b0505c52bcf9)
#### [v1.6.2](https://github.com/th-ch/youtube-music/compare/v1.6.0...v1.6.2) #### [v1.6.2](https://github.com/th-ch/youtube-music/compare/v1.6.0...v1.6.2)
@ -530,18 +634,24 @@ All notable changes to this project will be documented in this file. Dates are d
- Add github action to build/release [`#60`](https://github.com/th-ch/youtube-music/pull/60) - Add github action to build/release [`#60`](https://github.com/th-ch/youtube-music/pull/60)
- Bump to node 12 [`#59`](https://github.com/th-ch/youtube-music/pull/59) - Bump to node 12 [`#59`](https://github.com/th-ch/youtube-music/pull/59)
- Bump to node 12 [`#59`](https://github.com/th-ch/youtube-music/pull/59) - Bump to node 12 [`#59`](https://github.com/th-ch/youtube-music/pull/59)
- Add downloader (video -> mp3) plugin (in music menu) [`e197087`](https://github.com/th-ch/youtube-music/commit/e197087a5027af1ca71ecde7bbdf6351137555b9) - Add downloader (video -> mp3) plugin (in music
- Delete AppVeyor/Travis CI integration [`941dd90`](https://github.com/th-ch/youtube-music/commit/941dd90d77a5c46ed5505918374693fcd892af1f) menu) [`e197087`](https://github.com/th-ch/youtube-music/commit/e197087a5027af1ca71ecde7bbdf6351137555b9)
- GH action to build/release [`fc4754a`](https://github.com/th-ch/youtube-music/commit/fc4754a1709e6eb70d662f89eafd360aa4a77aa2) - Delete AppVeyor/Travis CI
integration [`941dd90`](https://github.com/th-ch/youtube-music/commit/941dd90d77a5c46ed5505918374693fcd892af1f)
- GH action to
build/release [`fc4754a`](https://github.com/th-ch/youtube-music/commit/fc4754a1709e6eb70d662f89eafd360aa4a77aa2)
#### [v1.6.0](https://github.com/th-ch/youtube-music/compare/v1.5.0...v1.6.0) #### [v1.6.0](https://github.com/th-ch/youtube-music/compare/v1.5.0...v1.6.0)
> 11 November 2020 > 11 November 2020
- [Snyk] Upgrade electron-store from 6.0.0 to 6.0.1 [`#54`](https://github.com/th-ch/youtube-music/pull/54) - [Snyk] Upgrade electron-store from 6.0.0 to 6.0.1 [`#54`](https://github.com/th-ch/youtube-music/pull/54)
- Add notifications plugin (notify of song on play event) [`bcff6e5`](https://github.com/th-ch/youtube-music/commit/bcff6e51348645395549c206717225fb16a29cda) - Add notifications plugin (notify of song on play
- Plugins/event handlers in each window [`9bc81da`](https://github.com/th-ch/youtube-music/commit/9bc81da6f2c7f5f35769489e179851bdd80a7da8) event) [`bcff6e5`](https://github.com/th-ch/youtube-music/commit/bcff6e51348645395549c206717225fb16a29cda)
- Option to toggle devtools [`3e97e93`](https://github.com/th-ch/youtube-music/commit/3e97e9307cf0991adc5584a603c292b03bc6202d) - Plugins/event handlers in each
window [`9bc81da`](https://github.com/th-ch/youtube-music/commit/9bc81da6f2c7f5f35769489e179851bdd80a7da8)
- Option to toggle
devtools [`3e97e93`](https://github.com/th-ch/youtube-music/commit/3e97e9307cf0991adc5584a603c292b03bc6202d)
#### [v1.5.0](https://github.com/th-ch/youtube-music/compare/v1.4.0...v1.5.0) #### [v1.5.0](https://github.com/th-ch/youtube-music/compare/v1.4.0...v1.5.0)
@ -555,8 +665,10 @@ All notable changes to this project will be documented in this file. Dates are d
- Bump lodash from 4.17.15 to 4.17.19 [`#34`](https://github.com/th-ch/youtube-music/pull/34) - Bump lodash from 4.17.15 to 4.17.19 [`#34`](https://github.com/th-ch/youtube-music/pull/34)
- Option to start at login [`#32`](https://github.com/th-ch/youtube-music/pull/32) - Option to start at login [`#32`](https://github.com/th-ch/youtube-music/pull/32)
- Bump dependencies [`97dce5a`](https://github.com/th-ch/youtube-music/commit/97dce5ad41ba7ff7a12d4e57a6a0acfeccd666d8) - Bump dependencies [`97dce5a`](https://github.com/th-ch/youtube-music/commit/97dce5ad41ba7ff7a12d4e57a6a0acfeccd666d8)
- Bump electron to v10 (+ remove devtron, bump spectron) [`5f0dcbb`](https://github.com/th-ch/youtube-music/commit/5f0dcbb3fc9b2912bba690db232184d32c599150) - Bump electron to v10 (+ remove devtron, bump
- Navigation plugin: fix arrow style [`8d74a0a`](https://github.com/th-ch/youtube-music/commit/8d74a0a9b52c5b5a04b0986e5fbec9b47a35823e) spectron) [`5f0dcbb`](https://github.com/th-ch/youtube-music/commit/5f0dcbb3fc9b2912bba690db232184d32c599150)
- Navigation plugin: fix arrow
style [`8d74a0a`](https://github.com/th-ch/youtube-music/commit/8d74a0a9b52c5b5a04b0986e5fbec9b47a35823e)
#### [v1.4.0](https://github.com/th-ch/youtube-music/compare/v1.3.3...v1.4.0) #### [v1.4.0](https://github.com/th-ch/youtube-music/compare/v1.3.3...v1.4.0)
@ -570,25 +682,33 @@ All notable changes to this project will be documented in this file. Dates are d
- [Snyk] Upgrade electron-updater from 4.3.0 to 4.3.1 [`#26`](https://github.com/th-ch/youtube-music/pull/26) - [Snyk] Upgrade electron-updater from 4.3.0 to 4.3.1 [`#26`](https://github.com/th-ch/youtube-music/pull/26)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2 [`#25`](https://github.com/th-ch/youtube-music/pull/25) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2 [`#25`](https://github.com/th-ch/youtube-music/pull/25)
- [Tests] Add integration tests [`#24`](https://github.com/th-ch/youtube-music/pull/24) - [Tests] Add integration tests [`#24`](https://github.com/th-ch/youtube-music/pull/24)
- Add jest, spectron and getPort util for tests [`736a706`](https://github.com/th-ch/youtube-music/commit/736a70680108620cdecab2da9dd48e10354c713e) - Add jest, spectron and getPort util for
- fix: upgrade electron-updater from 4.3.1 to 4.3.2 [`8c94510`](https://github.com/th-ch/youtube-music/commit/8c945100e24187885dbbe5bb7830b1da11e4eaa2) tests [`736a706`](https://github.com/th-ch/youtube-music/commit/736a70680108620cdecab2da9dd48e10354c713e)
- Add jest config and test environment to launch app [`bce5b7d`](https://github.com/th-ch/youtube-music/commit/bce5b7d8ebd96886d462a3c999d72e6c69b6f807) - fix: upgrade electron-updater from 4.3.1 to
4.3.2 [`8c94510`](https://github.com/th-ch/youtube-music/commit/8c945100e24187885dbbe5bb7830b1da11e4eaa2)
- Add jest config and test environment to launch
app [`bce5b7d`](https://github.com/th-ch/youtube-music/commit/bce5b7d8ebd96886d462a3c999d72e6c69b6f807)
#### [v1.3.3](https://github.com/th-ch/youtube-music/compare/v1.3.2...v1.3.3) #### [v1.3.3](https://github.com/th-ch/youtube-music/compare/v1.3.2...v1.3.3)
> 29 April 2020 > 29 April 2020
- Move tray click callback in setUpTray [`4824dda`](https://github.com/th-ch/youtube-music/commit/4824dda5d52565deb5cd6ef4b51d2d742677a154) - Move tray click callback in
- Bump version to 1.3.3 [`37cac19`](https://github.com/th-ch/youtube-music/commit/37cac19d9ccae59b89a68b995eaf7e08c7d24d11) setUpTray [`4824dda`](https://github.com/th-ch/youtube-music/commit/4824dda5d52565deb5cd6ef4b51d2d742677a154)
- Bump version to
1.3.3 [`37cac19`](https://github.com/th-ch/youtube-music/commit/37cac19d9ccae59b89a68b995eaf7e08c7d24d11)
#### [v1.3.2](https://github.com/th-ch/youtube-music/compare/v1.3.1...v1.3.2) #### [v1.3.2](https://github.com/th-ch/youtube-music/compare/v1.3.1...v1.3.2)
> 26 April 2020 > 26 April 2020
- [Snyk] Upgrade electron-updater from 4.2.5 to 4.3.0 [`#22`](https://github.com/th-ch/youtube-music/pull/22) - [Snyk] Upgrade electron-updater from 4.2.5 to 4.3.0 [`#22`](https://github.com/th-ch/youtube-music/pull/22)
- fix: upgrade electron-updater from 4.2.5 to 4.3.0 [`9821300`](https://github.com/th-ch/youtube-music/commit/98213005d09d00bf013d2217809736bdc334ede6) - fix: upgrade electron-updater from 4.2.5 to
- Hide the app (no quit) on close if tray enabled [`430687f`](https://github.com/th-ch/youtube-music/commit/430687f4d6d301aaeaeeaa11ae34d971ac3280df) 4.3.0 [`9821300`](https://github.com/th-ch/youtube-music/commit/98213005d09d00bf013d2217809736bdc334ede6)
- Show/hide window when clicking on tray [`058371a`](https://github.com/th-ch/youtube-music/commit/058371ace8fbd3d9f126454fdc7dbff86df05506) - Hide the app (no quit) on close if tray
enabled [`430687f`](https://github.com/th-ch/youtube-music/commit/430687f4d6d301aaeaeeaa11ae34d971ac3280df)
- Show/hide window when clicking on
tray [`058371a`](https://github.com/th-ch/youtube-music/commit/058371ace8fbd3d9f126454fdc7dbff86df05506)
#### [v1.3.1](https://github.com/th-ch/youtube-music/compare/v1.2.0...v1.3.1) #### [v1.3.1](https://github.com/th-ch/youtube-music/compare/v1.2.0...v1.3.1)
@ -598,8 +718,10 @@ All notable changes to this project will be documented in this file. Dates are d
- Upgrade outdated dependencies [`#20`](https://github.com/th-ch/youtube-music/pull/20) - Upgrade outdated dependencies [`#20`](https://github.com/th-ch/youtube-music/pull/20)
- [Plugins] Migrate ad blocker [`#19`](https://github.com/th-ch/youtube-music/pull/19) - [Plugins] Migrate ad blocker [`#19`](https://github.com/th-ch/youtube-music/pull/19)
- Upgrade xo [`297de08`](https://github.com/th-ch/youtube-music/commit/297de08278c2704b3baf65c455bba72f72acc06f) - Upgrade xo [`297de08`](https://github.com/th-ch/youtube-music/commit/297de08278c2704b3baf65c455bba72f72acc06f)
- Bump electron-builder (needed after electron upgrade) [`3d9e59d`](https://github.com/th-ch/youtube-music/commit/3d9e59dc90e0e994e20af55af9134477e68907a5) - Bump electron-builder (needed after electron
- Migrate from adblock-rs to cliqz [`422c3fc`](https://github.com/th-ch/youtube-music/commit/422c3fc28d83da309a80447dcd5064a4346580e8) upgrade) [`3d9e59d`](https://github.com/th-ch/youtube-music/commit/3d9e59dc90e0e994e20af55af9134477e68907a5)
- Migrate from adblock-rs to
cliqz [`422c3fc`](https://github.com/th-ch/youtube-music/commit/422c3fc28d83da309a80447dcd5064a4346580e8)
#### [v1.2.0](https://github.com/th-ch/youtube-music/compare/v1.1.6...v1.2.0) #### [v1.2.0](https://github.com/th-ch/youtube-music/compare/v1.1.6...v1.2.0)
@ -610,9 +732,12 @@ All notable changes to this project will be documented in this file. Dates are d
- [Snyk] Upgrade electron-debug from 2.1.0 to 2.2.0 [`#15`](https://github.com/th-ch/youtube-music/pull/15) - [Snyk] Upgrade electron-debug from 2.1.0 to 2.2.0 [`#15`](https://github.com/th-ch/youtube-music/pull/15)
- Fix vulnerability [`#16`](https://github.com/th-ch/youtube-music/pull/16) - Fix vulnerability [`#16`](https://github.com/th-ch/youtube-music/pull/16)
- Plugin: autoconfirm when paused [`#11`](https://github.com/th-ch/youtube-music/pull/11) - Plugin: autoconfirm when paused [`#11`](https://github.com/th-ch/youtube-music/pull/11)
- Migrate to yarn to install packages without package.json (but keep npm rebuild) [`9371a48`](https://github.com/th-ch/youtube-music/commit/9371a4827e2312258a4f692c18f964155d57ceb8) - Migrate to yarn to install packages without package.json (but keep npm
- Bump electron-store to fix a vulnerability [`7050dfc`](https://github.com/th-ch/youtube-music/commit/7050dfca5c6a545dabc334690572d7f88b37e027) rebuild) [`9371a48`](https://github.com/th-ch/youtube-music/commit/9371a4827e2312258a4f692c18f964155d57ceb8)
- Bump electron updater [`f25bb59`](https://github.com/th-ch/youtube-music/commit/f25bb59065d84cde202b5192688847c528c6ef61) - Bump electron-store to fix a
vulnerability [`7050dfc`](https://github.com/th-ch/youtube-music/commit/7050dfca5c6a545dabc334690572d7f88b37e027)
- Bump electron
updater [`f25bb59`](https://github.com/th-ch/youtube-music/commit/f25bb59065d84cde202b5192688847c528c6ef61)
#### [v1.1.6](https://github.com/th-ch/youtube-music/compare/v1.1.5...v1.1.6) #### [v1.1.6](https://github.com/th-ch/youtube-music/compare/v1.1.5...v1.1.6)
@ -623,59 +748,78 @@ All notable changes to this project will be documented in this file. Dates are d
- Bump lodash from 4.17.11 to 4.17.14 [`#5`](https://github.com/th-ch/youtube-music/pull/5) - Bump lodash from 4.17.11 to 4.17.14 [`#5`](https://github.com/th-ch/youtube-music/pull/5)
- npm audit fix [`1a72129`](https://github.com/th-ch/youtube-music/commit/1a72129108935cbe732621d93b877e90d11a4195) - npm audit fix [`1a72129`](https://github.com/th-ch/youtube-music/commit/1a72129108935cbe732621d93b877e90d11a4195)
- Fix Google login [`746b5f1`](https://github.com/th-ch/youtube-music/commit/746b5f13bb08c614df290e69946cfd116a550521) - Fix Google login [`746b5f1`](https://github.com/th-ch/youtube-music/commit/746b5f13bb08c614df290e69946cfd116a550521)
- Bump version to 1.1.6 [`6fd10ea`](https://github.com/th-ch/youtube-music/commit/6fd10ea4a0f63e9a46e7307d811977f4e0f3213f) - Bump version to
1.1.6 [`6fd10ea`](https://github.com/th-ch/youtube-music/commit/6fd10ea4a0f63e9a46e7307d811977f4e0f3213f)
#### [v1.1.5](https://github.com/th-ch/youtube-music/compare/v1.1.4...v1.1.5) #### [v1.1.5](https://github.com/th-ch/youtube-music/compare/v1.1.4...v1.1.5)
> 6 July 2019 > 6 July 2019
- Fix navigation plugin [`b10a1bb`](https://github.com/th-ch/youtube-music/commit/b10a1bb32dbea187422a43487527c379a9ddbb26) - Fix navigation
- Bump version to 1.1.5 [`07c4a42`](https://github.com/th-ch/youtube-music/commit/07c4a429c15f22b173629618518abb97d9ec0100) plugin [`b10a1bb`](https://github.com/th-ch/youtube-music/commit/b10a1bb32dbea187422a43487527c379a9ddbb26)
- Bump version to
1.1.5 [`07c4a42`](https://github.com/th-ch/youtube-music/commit/07c4a429c15f22b173629618518abb97d9ec0100)
#### [v1.1.4](https://github.com/th-ch/youtube-music/compare/v1.1.3...v1.1.4) #### [v1.1.4](https://github.com/th-ch/youtube-music/compare/v1.1.3...v1.1.4)
> 8 June 2019 > 8 June 2019
- isDev -> is package [`a85325f`](https://github.com/th-ch/youtube-music/commit/a85325f33dbd40517b6029e500569fc1640af2ef) - isDev -> is
- Add titlebar/frame only on MacOS [`b1c4cc9`](https://github.com/th-ch/youtube-music/commit/b1c4cc9c45cc48413118aec8ce54767b1983a3e7) package [`a85325f`](https://github.com/th-ch/youtube-music/commit/a85325f33dbd40517b6029e500569fc1640af2ef)
- Bump version to 1.1.4 [`0420f2e`](https://github.com/th-ch/youtube-music/commit/0420f2e49e295cede0db22dbb1f35ffafd6318ed) - Add titlebar/frame only on
MacOS [`b1c4cc9`](https://github.com/th-ch/youtube-music/commit/b1c4cc9c45cc48413118aec8ce54767b1983a3e7)
- Bump version to
1.1.4 [`0420f2e`](https://github.com/th-ch/youtube-music/commit/0420f2e49e295cede0db22dbb1f35ffafd6318ed)
#### [v1.1.3](https://github.com/th-ch/youtube-music/compare/v1.1.2...v1.1.3) #### [v1.1.3](https://github.com/th-ch/youtube-music/compare/v1.1.2...v1.1.3)
> 2 June 2019 > 2 June 2019
- Bump fstream from 1.0.11 to 1.0.12 [`#3`](https://github.com/th-ch/youtube-music/pull/3) - Bump fstream from 1.0.11 to 1.0.12 [`#3`](https://github.com/th-ch/youtube-music/pull/3)
- Version 1.1.3 + npm audit fix [`147ac48`](https://github.com/th-ch/youtube-music/commit/147ac48de6540c836e835fefe47e66e55dbdc9bc) - Version 1.1.3 + npm audit
- Fix case for {en/dis}ablePlugin [`e86d63d`](https://github.com/th-ch/youtube-music/commit/e86d63da8cb083b89c2a26e6514a5b0df8868b13) fix [`147ac48`](https://github.com/th-ch/youtube-music/commit/147ac48de6540c836e835fefe47e66e55dbdc9bc)
- Remove outdated download links [`ec58b5c`](https://github.com/th-ch/youtube-music/commit/ec58b5cbedda8d6f881f0e81f185a1707dbe5fab) - Fix case for
{en/dis}ablePlugin [`e86d63d`](https://github.com/th-ch/youtube-music/commit/e86d63da8cb083b89c2a26e6514a5b0df8868b13)
- Remove outdated download
links [`ec58b5c`](https://github.com/th-ch/youtube-music/commit/ec58b5cbedda8d6f881f0e81f185a1707dbe5fab)
#### [v1.1.2](https://github.com/th-ch/youtube-music/compare/v1.1.1...v1.1.2) #### [v1.1.2](https://github.com/th-ch/youtube-music/compare/v1.1.1...v1.1.2)
> 1 May 2019 > 1 May 2019
- Display error/retry in case of failure [`5a1d7fb`](https://github.com/th-ch/youtube-music/commit/5a1d7fbf230fcd840a3ea654f31602fb5f504852) - Display error/retry in case of
- Bump version to 1.1.2 [`eac2c5c`](https://github.com/th-ch/youtube-music/commit/eac2c5cf14d0a348704f7fbf0ff0bdce02758670) failure [`5a1d7fb`](https://github.com/th-ch/youtube-music/commit/5a1d7fbf230fcd840a3ea654f31602fb5f504852)
- Bump version to
1.1.2 [`eac2c5c`](https://github.com/th-ch/youtube-music/commit/eac2c5cf14d0a348704f7fbf0ff0bdce02758670)
#### [v1.1.1](https://github.com/th-ch/youtube-music/compare/v1.1.0...v1.1.1) #### [v1.1.1](https://github.com/th-ch/youtube-music/compare/v1.1.0...v1.1.1)
> 28 April 2019 > 28 April 2019
- Update package lock [`2d3f77d`](https://github.com/th-ch/youtube-music/commit/2d3f77d96211460bb81a73c8c62b9e5407a7cf30) - Update package
lock [`2d3f77d`](https://github.com/th-ch/youtube-music/commit/2d3f77d96211460bb81a73c8c62b9e5407a7cf30)
- Add travis config [`5279a45`](https://github.com/th-ch/youtube-music/commit/5279a45f3537170006ba04cd5d59ac8b879d78a5) - Add travis config [`5279a45`](https://github.com/th-ch/youtube-music/commit/5279a45f3537170006ba04cd5d59ac8b879d78a5)
- Add Appveyor config [`abc2bb8`](https://github.com/th-ch/youtube-music/commit/abc2bb8a4f749704f2daf376c0d392030f030caf) - Add Appveyor
config [`abc2bb8`](https://github.com/th-ch/youtube-music/commit/abc2bb8a4f749704f2daf376c0d392030f030caf)
#### [v1.1.0](https://github.com/th-ch/youtube-music/compare/v1.0.0...v1.1.0) #### [v1.1.0](https://github.com/th-ch/youtube-music/compare/v1.0.0...v1.1.0)
> 19 April 2019 > 19 April 2019
- Build script + check for updates [`b3c24a5`](https://github.com/th-ch/youtube-music/commit/b3c24a521281c352c37d649e8334b581b2a1de4f) - Build script + check for
- Add download section in readme [`828e8d4`](https://github.com/th-ch/youtube-music/commit/828e8d472ca3d76dea71d95a85f8fa726404b8e7) updates [`b3c24a5`](https://github.com/th-ch/youtube-music/commit/b3c24a521281c352c37d649e8334b581b2a1de4f)
- Add release/licence badge in readme [`9d343bf`](https://github.com/th-ch/youtube-music/commit/9d343bf779f2fa830302cc84c484bf4a93a25f36) - Add download section in
readme [`828e8d4`](https://github.com/th-ch/youtube-music/commit/828e8d472ca3d76dea71d95a85f8fa726404b8e7)
- Add release/licence badge in
readme [`9d343bf`](https://github.com/th-ch/youtube-music/commit/9d343bf779f2fa830302cc84c484bf4a93a25f36)
#### v1.0.0 #### v1.0.0
> 19 April 2019 > 19 April 2019
- Initial commit - app + 4 plugins [`8787b5c`](https://github.com/th-ch/youtube-music/commit/8787b5c175d02b52de65f2c559b411d999fa51e4) - Initial commit - app + 4
- Fix screenshot shadow + compress image [`c5c128f`](https://github.com/th-ch/youtube-music/commit/c5c128fa0f77c69e9bf12f6ca551315b37c51e84) plugins [`8787b5c`](https://github.com/th-ch/youtube-music/commit/8787b5c175d02b52de65f2c559b411d999fa51e4)
- Missing quote in readme [`4b446ac`](https://github.com/th-ch/youtube-music/commit/4b446ac7c816c660cf369f3b8b6e420f766ee35f) - Fix screenshot shadow + compress
image [`c5c128f`](https://github.com/th-ch/youtube-music/commit/c5c128fa0f77c69e9bf12f6ca551315b37c51e84)
- Missing quote in
readme [`4b446ac`](https://github.com/th-ch/youtube-music/commit/4b446ac7c816c660cf369f3b8b6e420f766ee35f)

View File

@ -1,184 +1,184 @@
const defaultConfig = { const defaultConfig = {
"window-size": { 'window-size': {
width: 1100, width: 1100,
height: 550, height: 550,
}, },
url: "https://music.youtube.com", 'url': 'https://music.youtube.com',
options: { 'options': {
tray: false, tray: false,
appVisible: true, appVisible: true,
autoUpdates: true, autoUpdates: true,
hideMenu: false, hideMenu: false,
startAtLogin: false, startAtLogin: false,
disableHardwareAcceleration: false, disableHardwareAcceleration: false,
restartOnConfigChanges: false, restartOnConfigChanges: false,
trayClickPlayPause: false, trayClickPlayPause: false,
autoResetAppCache: false, autoResetAppCache: false,
resumeOnStart: true, resumeOnStart: true,
proxy: "", proxy: '',
startingPage: "", startingPage: '',
}, },
plugins: { 'plugins': {
// Enabled plugins // Enabled plugins
navigation: { 'navigation': {
enabled: true, enabled: true,
}, },
adblocker: { 'adblocker': {
enabled: true, enabled: true,
cache: true, cache: true,
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt" additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
}, },
// Disabled plugins // Disabled plugins
shortcuts: { 'shortcuts': {
enabled: false, enabled: false,
overrideMediaKeys: false, overrideMediaKeys: false,
}, },
downloader: { 'downloader': {
enabled: false, enabled: false,
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s ffmpegArgs: [], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined, // Custom download folder (absolute path) downloadFolder: undefined, // Custom download folder (absolute path)
preset: "mp3", preset: 'mp3',
}, },
"last-fm": { 'last-fm': {
enabled: false, enabled: false,
api_root: "http://ws.audioscrobbler.com/2.0/", api_root: 'http://ws.audioscrobbler.com/2.0/',
api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123 api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
secret: "a5d2a36fdf64819290f6982481eaffa2", secret: 'a5d2a36fdf64819290f6982481eaffa2',
}, },
discord: { 'discord': {
enabled: false, enabled: false,
autoReconnect: true, // if enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect 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 activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000, // 10 minutes activityTimoutTime: 10 * 60 * 1000, // 10 minutes
listenAlong: true, // add a "listen along" button to rich presence listenAlong: true, // Add a "listen along" button to rich presence
hideDurationLeft: false, // hides the start and end time of the song to rich presence hideDurationLeft: false, // Hides the start and end time of the song to rich presence
}, },
notifications: { 'notifications': {
enabled: false, enabled: false,
unpauseNotification: false, unpauseNotification: false,
urgency: "normal", //has effect only on Linux urgency: 'normal', // Has effect only on Linux
// the following has effect only on Windows // the following has effect only on Windows
interactive: true, interactive: true,
toastStyle: 1, // see plugins/notifications/utils for more info toastStyle: 1, // See plugins/notifications/utils for more info
refreshOnPlayPause: false, refreshOnPlayPause: false,
trayControls: true, trayControls: true,
hideButtonText: false hideButtonText: false,
}, },
"precise-volume": { 'precise-volume': {
enabled: false, enabled: false,
steps: 1, //percentage of volume to change steps: 1, // Percentage of volume to change
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: { globalShortcuts: {
volumeUp: "", volumeUp: '',
volumeDown: "" volumeDown: '',
}, },
savedVolume: undefined //plugin save volume between session here savedVolume: undefined, // Plugin save volume between session here
}, },
sponsorblock: { 'sponsorblock': {
enabled: false, enabled: false,
apiURL: "https://sponsor.ajay.app", apiURL: 'https://sponsor.ajay.app',
categories: [ categories: [
"sponsor", 'sponsor',
"intro", 'intro',
"outro", 'outro',
"interaction", 'interaction',
"selfpromo", 'selfpromo',
"music_offtopic", 'music_offtopic',
], ],
}, },
"video-toggle": { 'video-toggle': {
enabled: false, enabled: false,
mode: "custom", mode: 'custom',
forceHide: false, forceHide: false,
}, },
"picture-in-picture": { 'picture-in-picture': {
"enabled": false, enabled: false,
"alwaysOnTop": true, alwaysOnTop: true,
"savePosition": true, savePosition: true,
"saveSize": false, saveSize: false,
"hotkey": "P" hotkey: 'P',
}, },
"captions-selector": { 'captions-selector': {
enabled: false, enabled: false,
disableCaptions: false disableCaptions: false,
}, },
"skip-silences": { 'skip-silences': {
onlySkipBeginning: false, onlySkipBeginning: false,
}, },
"crossfade": { 'crossfade': {
enabled: false, enabled: false,
fadeInDuration: 1500, // ms fadeInDuration: 1500, // Ms
fadeOutDuration: 5000, // ms fadeOutDuration: 5000, // Ms
secondsBeforeEnd: 10, // s secondsBeforeEnd: 10, // S
fadeScaling: "linear", // 'linear', 'logarithmic' or a positive number in dB fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
}, },
visualizer: { 'visualizer': {
enabled: false, enabled: false,
type: "butterchurn", type: 'butterchurn',
// Config per visualizer // Config per visualizer
butterchurn: { butterchurn: {
preset: "martin [shadow harlequins shape code] - fata morgana", preset: 'martin [shadow harlequins shape code] - fata morgana',
renderingFrequencyInMs: 500, renderingFrequencyInMs: 500,
blendTimeInSeconds: 2.7, blendTimeInSeconds: 2.7,
}, },
vudio: { vudio: {
effect: "lighting", effect: 'lighting',
accuracy: 128, accuracy: 128,
lighting: { lighting: {
maxHeight: 160, maxHeight: 160,
maxSize: 12, maxSize: 12,
lineWidth: 1, lineWidth: 1,
color: "#49f3f7", color: '#49f3f7',
shadowBlur: 2, shadowBlur: 2,
shadowColor: "rgba(244,244,244,.5)", shadowColor: 'rgba(244,244,244,.5)',
fadeSide: true, fadeSide: true,
prettify: false, prettify: false,
horizontalAlign: "center", horizontalAlign: 'center',
verticalAlign: "middle", verticalAlign: 'middle',
dottify: true, dottify: true,
}, },
}, },
wave: { wave: {
animations: [ animations: [
{ {
type: "Cubes", type: 'Cubes',
config: { config: {
bottom: true, bottom: true,
count: 30, count: 30,
cubeHeight: 5, cubeHeight: 5,
fillColor: { gradient: ["#FAD961", "#F76B1C"] }, fillColor: { gradient: ['#FAD961', '#F76B1C'] },
lineColor: "rgba(0,0,0,0)", lineColor: 'rgba(0,0,0,0)',
radius: 20, radius: 20,
}, },
}, },
{ {
type: "Cubes", type: 'Cubes',
config: { config: {
top: true, top: true,
count: 12, count: 12,
cubeHeight: 5, cubeHeight: 5,
fillColor: { gradient: ["#FAD961", "#F76B1C"] }, fillColor: { gradient: ['#FAD961', '#F76B1C'] },
lineColor: "rgba(0,0,0,0)", lineColor: 'rgba(0,0,0,0)',
radius: 10, radius: 10,
}, },
}, },
{ {
type: "Circles", type: 'Circles',
config: { config: {
lineColor: { lineColor: {
gradient: ["#FAD961", "#FAD961", "#F76B1C"], gradient: ['#FAD961', '#FAD961', '#F76B1C'],
rotate: 90, rotate: 90,
}, },
lineWidth: 4, lineWidth: 4,
diameter: 20, diameter: 20,
count: 10, count: 10,
frequencyBand: "base", frequencyBand: 'base',
}, },
}, },
], ],
}, },
}, },
}, },
}; };
module.exports = defaultConfig; module.exports = defaultConfig;

View File

@ -1,32 +1,33 @@
const { ipcRenderer, ipcMain } = require("electron"); const { ipcRenderer, ipcMain } = require('electron');
const defaultConfig = require("./defaults"); const defaultConfig = require('./defaults');
const { getOptions, setOptions, setMenuOptions } = require("./plugins"); const { getOptions, setOptions, setMenuOptions } = require('./plugins');
const { sendToFront } = require("../providers/app-controls");
const { sendToFront } = require('../providers/app-controls');
const activePlugins = {}; const activePlugins = {};
/** /**
* [!IMPORTANT!] * [!IMPORTANT!]
* The method is **sync** in the main process and **async** in the renderer process. * The method is **sync** in the main process and **async** in the renderer process.
*/ */
module.exports.getActivePlugins = module.exports.getActivePlugins
process.type === "renderer" = process.type === 'renderer'
? async () => ipcRenderer.invoke("get-active-plugins") ? async () => ipcRenderer.invoke('get-active-plugins')
: () => activePlugins; : () => activePlugins;
if (process.type === "browser") { if (process.type === 'browser') {
ipcMain.handle("get-active-plugins", this.getActivePlugins); ipcMain.handle('get-active-plugins', this.getActivePlugins);
} }
/** /**
* [!IMPORTANT!] * [!IMPORTANT!]
* The method is **sync** in the main process and **async** in the renderer process. * The method is **sync** in the main process and **async** in the renderer process.
*/ */
module.exports.isActive = module.exports.isActive
process.type === "renderer" = process.type === 'renderer'
? async (plugin) => ? async (plugin) =>
plugin in (await ipcRenderer.invoke("get-active-plugins")) plugin in (await ipcRenderer.invoke('get-active-plugins'))
: (plugin) => plugin in activePlugins; : (plugin) => plugin in activePlugins;
/** /**
* This class is used to create a dynamic synced config for plugins. * This class is used to create a dynamic synced config for plugins.
@ -47,159 +48,167 @@ module.exports.isActive =
* *
* module.exports = (win, options) => { * module.exports = (win, options) => {
* const config = new PluginConfig("plugin-name", { * const config = new PluginConfig("plugin-name", {
* enableFront: true, * enableFront: true,
* initialOptions: options, * initialOptions: options,
* }); * });
* setupMyPlugin(win, config); * setupMyPlugin(win, config);
* }; * };
*/ */
module.exports.PluginConfig = class PluginConfig { module.exports.PluginConfig = class PluginConfig {
#name; #name;
#config; #config;
#defaultConfig; #defaultConfig;
#enableFront; #enableFront;
#subscribers = {}; #subscribers = {};
#allSubscribers = []; #allSubscribers = [];
constructor(name, { enableFront = false, initialOptions = undefined } = {}) { constructor(name, { enableFront = false, initialOptions = undefined } = {}) {
const pluginDefaultConfig = defaultConfig.plugins[name] || {}; const pluginDefaultConfig = defaultConfig.plugins[name] || {};
const pluginConfig = initialOptions || getOptions(name) || {}; const pluginConfig = initialOptions || getOptions(name) || {};
this.#name = name; this.#name = name;
this.#enableFront = enableFront; this.#enableFront = enableFront;
this.#defaultConfig = pluginDefaultConfig; this.#defaultConfig = pluginDefaultConfig;
this.#config = { ...pluginDefaultConfig, ...pluginConfig }; this.#config = { ...pluginDefaultConfig, ...pluginConfig };
if (this.#enableFront) { if (this.#enableFront) {
this.#setupFront(); this.#setupFront();
} }
activePlugins[name] = this; activePlugins[name] = this;
} }
get = (option) => { get = (option) => this.#config[option];
return this.#config[option];
};
set = (option, value) => { set = (option, value) => {
this.#config[option] = value; this.#config[option] = value;
this.#onChange(option); this.#onChange(option);
this.#save(); this.#save();
}; };
toggle = (option) => { toggle = (option) => {
this.#config[option] = !this.#config[option]; this.#config[option] = !this.#config[option];
this.#onChange(option); this.#onChange(option);
this.#save(); this.#save();
}; };
getAll = () => { getAll = () => ({ ...this.#config });
return { ...this.#config };
};
setAll = (options) => { setAll = (options) => {
if (!options || typeof options !== "object") if (!options || typeof options !== 'object') {
throw new Error("Options must be an object."); throw new Error('Options must be an object.');
}
let changed = false; let changed = false;
for (const [key, val] of Object.entries(options)) { for (const [key, value] of Object.entries(options)) {
if (this.#config[key] !== val) { if (this.#config[key] !== value) {
this.#config[key] = val; this.#config[key] = value;
this.#onChange(key, false); this.#onChange(key, false);
changed = true; changed = true;
} }
} }
if (changed) this.#allSubscribers.forEach((fn) => fn(this.#config));
this.#save();
};
getDefaultConfig = () => { if (changed) {
return this.#defaultConfig; for (const fn of this.#allSubscribers) {
}; fn(this.#config);
}
}
/** this.#save();
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true` };
*
* Used for options that require a restart to take effect.
*/
setAndMaybeRestart = (option, value) => {
this.#config[option] = value;
setMenuOptions(this.#name, this.#config);
this.#onChange(option);
};
subscribe = (valueName, fn) => { getDefaultConfig = () => this.#defaultConfig;
this.#subscribers[valueName] = fn;
};
subscribeAll = (fn) => { /**
this.#allSubscribers.push(fn); * Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
}; *
* Used for options that require a restart to take effect.
*/
setAndMaybeRestart = (option, value) => {
this.#config[option] = value;
setMenuOptions(this.#name, this.#config);
this.#onChange(option);
};
/** Called only from back */ subscribe = (valueName, fn) => {
#save() { this.#subscribers[valueName] = fn;
setOptions(this.#name, this.#config); };
}
#onChange(valueName, single = true) { subscribeAll = (fn) => {
this.#subscribers[valueName]?.(this.#config[valueName]); this.#allSubscribers.push(fn);
if (single) this.#allSubscribers.forEach((fn) => fn(this.#config)); };
}
#setupFront() { /** Called only from back */
const ignoredMethods = ["subscribe", "subscribeAll"]; #save() {
setOptions(this.#name, this.#config);
}
if (process.type === "renderer") { #onChange(valueName, single = true) {
for (const [fnName, fn] of Object.entries(this)) { this.#subscribers[valueName]?.(this.#config[valueName]);
if (typeof fn !== "function" || fn.name in ignoredMethods) return; if (single) {
this[fnName] = async (...args) => { for (const fn of this.#allSubscribers) {
return await ipcRenderer.invoke( fn(this.#config);
`${this.#name}-config-${fnName}`, }
...args, }
); }
};
this.subscribe = (valueName, fn) => { #setupFront() {
if (valueName in this.#subscribers) { const ignoredMethods = ['subscribe', 'subscribeAll'];
console.error(`Already subscribed to ${valueName}`);
}
this.#subscribers[valueName] = fn;
ipcRenderer.on(
`${this.#name}-config-changed-${valueName}`,
(_, value) => {
fn(value);
},
);
ipcRenderer.send(`${this.#name}-config-subscribe`, valueName);
};
this.subscribeAll = (fn) => { if (process.type === 'renderer') {
ipcRenderer.on(`${this.#name}-config-changed`, (_, value) => { for (const [fnName, fn] of Object.entries(this)) {
fn(value); if (typeof fn !== 'function' || fn.name in ignoredMethods) {
}); return;
ipcRenderer.send(`${this.#name}-config-subscribe-all`); }
};
}
} else if (process.type === "browser") {
for (const [fnName, fn] of Object.entries(this)) {
if (typeof fn !== "function" || fn.name in ignoredMethods) return;
ipcMain.handle(`${this.#name}-config-${fnName}`, (_, ...args) => {
return fn(...args);
});
}
ipcMain.on(`${this.#name}-config-subscribe`, (_, valueName) => { this[fnName] = async (...args) => await ipcRenderer.invoke(
this.subscribe(valueName, (value) => { `${this.#name}-config-${fnName}`,
sendToFront(`${this.#name}-config-changed-${valueName}`, value); ...args,
}); );
});
ipcMain.on(`${this.#name}-config-subscribe-all`, () => { this.subscribe = (valueName, fn) => {
this.subscribeAll((value) => { if (valueName in this.#subscribers) {
sendToFront(`${this.#name}-config-changed`, value); console.error(`Already subscribed to ${valueName}`);
}); }
});
} this.#subscribers[valueName] = fn;
} ipcRenderer.on(
`${this.#name}-config-changed-${valueName}`,
(_, value) => {
fn(value);
},
);
ipcRenderer.send(`${this.#name}-config-subscribe`, valueName);
};
this.subscribeAll = (fn) => {
ipcRenderer.on(`${this.#name}-config-changed`, (_, value) => {
fn(value);
});
ipcRenderer.send(`${this.#name}-config-subscribe-all`);
};
}
} else if (process.type === 'browser') {
for (const [fnName, fn] of Object.entries(this)) {
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
return;
}
ipcMain.handle(`${this.#name}-config-${fnName}`, (_, ...args) => fn(...args));
}
ipcMain.on(`${this.#name}-config-subscribe`, (_, valueName) => {
this.subscribe(valueName, (value) => {
sendToFront(`${this.#name}-config-changed-${valueName}`, value);
});
});
ipcMain.on(`${this.#name}-config-subscribe-all`, () => {
this.subscribeAll((value) => {
sendToFront(`${this.#name}-config-changed`, value);
});
});
}
}
}; };

View File

@ -1,30 +1,31 @@
const defaultConfig = require("./defaults"); const defaultConfig = require('./defaults');
const plugins = require("./plugins"); const plugins = require('./plugins');
const store = require("./store"); const store = require('./store');
const { restart } = require("../providers/app-controls");
const { restart } = require('../providers/app-controls');
const set = (key, value) => { const set = (key, value) => {
store.set(key, value); store.set(key, value);
}; };
function setMenuOption(key, value) { function setMenuOption(key, value) {
set(key, value); set(key, value);
if (store.get("options.restartOnConfigChanges")) restart(); if (store.get('options.restartOnConfigChanges')) {
restart();
}
} }
const get = (key) => { const get = (key) => store.get(key);
return store.get(key);
};
module.exports = { module.exports = {
defaultConfig, defaultConfig,
get, get,
set, set,
setMenuOption, setMenuOption,
edit: () => store.openInEditor(), edit: () => store.openInEditor(),
watch: (cb) => { watch(cb) {
store.onDidChange("options", cb); store.onDidChange('options', cb);
store.onDidChange("plugins", cb); store.onDidChange('plugins', cb);
}, },
plugins, plugins,
}; };

View File

@ -1,53 +1,56 @@
const store = require("./store"); const store = require('./store');
const { restart } = require("../providers/app-controls");
const { restart } = require('../providers/app-controls');
function getEnabled() { function getEnabled() {
const plugins = store.get("plugins"); const plugins = store.get('plugins');
const enabledPlugins = Object.entries(plugins).filter(([plugin, options]) => const enabledPlugins = Object.entries(plugins).filter(([plugin, options]) =>
isEnabled(plugin) isEnabled(plugin),
); );
return enabledPlugins; return enabledPlugins;
} }
function isEnabled(plugin) { function isEnabled(plugin) {
const pluginConfig = store.get("plugins")[plugin]; const pluginConfig = store.get('plugins')[plugin];
return pluginConfig !== undefined && pluginConfig.enabled; return pluginConfig !== undefined && pluginConfig.enabled;
} }
function setOptions(plugin, options) { function setOptions(plugin, options) {
const plugins = store.get("plugins"); const plugins = store.get('plugins');
store.set("plugins", { store.set('plugins', {
...plugins, ...plugins,
[plugin]: { [plugin]: {
...plugins[plugin], ...plugins[plugin],
...options, ...options,
}, },
}); });
} }
function setMenuOptions(plugin, options) { function setMenuOptions(plugin, options) {
setOptions(plugin, options); setOptions(plugin, options);
if (store.get("options.restartOnConfigChanges")) restart(); if (store.get('options.restartOnConfigChanges')) {
restart();
}
} }
function getOptions(plugin) { function getOptions(plugin) {
return store.get("plugins")[plugin]; return store.get('plugins')[plugin];
} }
function enable(plugin) { function enable(plugin) {
setMenuOptions(plugin, { enabled: true }); setMenuOptions(plugin, { enabled: true });
} }
function disable(plugin) { function disable(plugin) {
setMenuOptions(plugin, { enabled: false }); setMenuOptions(plugin, { enabled: false });
} }
module.exports = { module.exports = {
isEnabled, isEnabled,
getEnabled, getEnabled,
enable, enable,
disable, disable,
setOptions, setOptions,
setMenuOptions, setMenuOptions,
getOptions, getOptions,
}; };

View File

@ -1,112 +1,113 @@
const Store = require("electron-store"); const Store = require('electron-store');
const defaults = require("./defaults"); const defaults = require('./defaults');
const setDefaultPluginOptions = (store, plugin) => { const setDefaultPluginOptions = (store, plugin) => {
if (!store.get(`plugins.${plugin}`)) { if (!store.get(`plugins.${plugin}`)) {
store.set(`plugins.${plugin}`, defaults.plugins[plugin]); store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
} }
} };
const migrations = { const migrations = {
">=1.20.0": (store) => { '>=1.20.0'(store) {
setDefaultPluginOptions(store, "visualizer"); setDefaultPluginOptions(store, 'visualizer');
if (store.get("plugins.notifications.toastStyle") === undefined) { if (store.get('plugins.notifications.toastStyle') === undefined) {
const pluginOptions = store.get("plugins.notifications") || {}; const pluginOptions = store.get('plugins.notifications') || {};
store.set("plugins.notifications", { store.set('plugins.notifications', {
...defaults.plugins.notifications, ...defaults.plugins.notifications,
...pluginOptions, ...pluginOptions,
}); });
} }
if (store.get("options.ForceShowLikeButtons")) { if (store.get('options.ForceShowLikeButtons')) {
store.delete("options.ForceShowLikeButtons"); store.delete('options.ForceShowLikeButtons');
store.set("options.likeButtons", 'force'); store.set('options.likeButtons', 'force');
} }
}, },
">=1.17.0": (store) => { '>=1.17.0'(store) {
setDefaultPluginOptions(store, "picture-in-picture"); setDefaultPluginOptions(store, 'picture-in-picture');
if (store.get("plugins.video-toggle.mode") === undefined) { if (store.get('plugins.video-toggle.mode') === undefined) {
store.set("plugins.video-toggle.mode", "custom"); store.set('plugins.video-toggle.mode', 'custom');
} }
}, },
">=1.14.0": (store) => { '>=1.14.0'(store) {
if ( if (
typeof store.get("plugins.precise-volume.globalShortcuts") !== "object" typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object'
) { ) {
store.set("plugins.precise-volume.globalShortcuts", {}); store.set('plugins.precise-volume.globalShortcuts', {});
} }
if (store.get("plugins.hide-video-player.enabled")) { if (store.get('plugins.hide-video-player.enabled')) {
store.delete("plugins.hide-video-player"); store.delete('plugins.hide-video-player');
store.set("plugins.video-toggle.enabled", true); store.set('plugins.video-toggle.enabled', true);
} }
}, },
">=1.13.0": (store) => { '>=1.13.0'(store) {
if (store.get("plugins.discord.listenAlong") === undefined) { if (store.get('plugins.discord.listenAlong') === undefined) {
store.set("plugins.discord.listenAlong", true); store.set('plugins.discord.listenAlong', true);
} }
}, },
">=1.12.0": (store) => { '>=1.12.0'(store) {
const options = store.get("plugins.shortcuts"); const options = store.get('plugins.shortcuts');
let updated = false; let updated = false;
for (const optionType of ["global", "local"]) { for (const optionType of ['global', 'local']) {
if (Array.isArray(options[optionType])) { if (Array.isArray(options[optionType])) {
const updatedOptions = {}; const updatedOptions = {};
for (const optionObject of options[optionType]) { for (const optionObject of options[optionType]) {
if (optionObject.action && optionObject.shortcut) { if (optionObject.action && optionObject.shortcut) {
updatedOptions[optionObject.action] = optionObject.shortcut; updatedOptions[optionObject.action] = optionObject.shortcut;
} }
} }
options[optionType] = updatedOptions; options[optionType] = updatedOptions;
updated = true; updated = true;
} }
} }
if (updated) { if (updated) {
store.set("plugins.shortcuts", options); store.set('plugins.shortcuts', options);
} }
}, },
">=1.11.0": (store) => { '>=1.11.0'(store) {
if (store.get("options.resumeOnStart") === undefined) { if (store.get('options.resumeOnStart') === undefined) {
store.set("options.resumeOnStart", true); store.set('options.resumeOnStart', true);
} }
}, },
">=1.7.0": (store) => { '>=1.7.0'(store) {
const enabledPlugins = store.get("plugins"); const enabledPlugins = store.get('plugins');
if (!Array.isArray(enabledPlugins)) { if (!Array.isArray(enabledPlugins)) {
console.warn("Plugins are not in array format, cannot migrate"); console.warn('Plugins are not in array format, cannot migrate');
return; return;
} }
// Include custom options // Include custom options
const plugins = { const plugins = {
adblocker: { adblocker: {
enabled: true, enabled: true,
cache: true, cache: true,
additionalBlockLists: [], additionalBlockLists: [],
}, },
downloader: { downloader: {
enabled: false, enabled: false,
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s ffmpegArgs: [], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined, // Custom download folder (absolute path) downloadFolder: undefined, // Custom download folder (absolute path)
}, },
}; };
enabledPlugins.forEach((enabledPlugin) => { for (const enabledPlugin of enabledPlugins) {
plugins[enabledPlugin] = { plugins[enabledPlugin] = {
...plugins[enabledPlugin], ...plugins[enabledPlugin],
enabled: true, enabled: true,
}; };
}); }
store.set("plugins", plugins);
}, store.set('plugins', plugins);
},
}; };
module.exports = new Store({ module.exports = new Store({
defaults, defaults,
clearInvalidConfig: false, clearInvalidConfig: false,
migrations, migrations,
}); });

View File

@ -1 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"><g transform="translate(183.604 196.396)" stroke="#fff" stroke-width="2.23"><path style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;block-progression:tb;marker:none" d="M-116.99 106.245l31.82 31.82 236.31-236.31-31.82-31.82z" color="#000" font-weight="400" font-family="Sans" overflow="visible" fill="#fff" stroke="none"/><circle r="171.304" cy="4" cx="16" fill="none" stroke-width="44.6"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
<g transform="translate(183.604 196.396)" stroke="#fff" stroke-width="2.23">
<path
style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;block-progression:tb;marker:none"
d="M-116.99 106.245l31.82 31.82 236.31-236.31-31.82-31.82z" color="#000" font-weight="400"
font-family="Sans" overflow="visible" fill="#fff" stroke="none"/>
<circle r="171.304" cy="4" cx="16" fill="none" stroke-width="44.6"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 588 B

View File

@ -1 +1,23 @@
<svg width="1440" height="347" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#606483" stop-opacity="0" offset="0%"/><stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b"><stop stop-color="#0B0D19" offset="0%"/><stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path d="M177.486 208.219c78.18 89.285 218.65-81.067 218.65-119.337 0-38.27-86.408-69.295-193-69.295-106.59 0-193 31.024-193 69.295 0 38.27 89.17 30.051 167.35 119.337z" transform="rotate(6 -140.175 3980.948)" fill="url(#a)"/><path d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z" fill="url(#a)" transform="rotate(24 321.92 -247.724)"/><path d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z" fill="url(#b)" transform="rotate(24 338.741 -285.505)"/></g></svg> <svg width="1440" height="347" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a">
<stop stop-color="#606483" stop-opacity="0" offset="0%"/>
<stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/>
</linearGradient>
<linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b">
<stop stop-color="#0B0D19" offset="0%"/>
<stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path
d="M177.486 208.219c78.18 89.285 218.65-81.067 218.65-119.337 0-38.27-86.408-69.295-193-69.295-106.59 0-193 31.024-193 69.295 0 38.27 89.17 30.051 167.35 119.337z"
transform="rotate(6 -140.175 3980.948)" fill="url(#a)"/>
<path
d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z"
fill="url(#a)" transform="rotate(24 321.92 -247.724)"/>
<path
d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z"
fill="url(#b)" transform="rotate(24 338.741 -285.505)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +1,32 @@
<svg width="1440" height="318" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="38.706%" y1="-187.115%" x2="18.675%" y2="110.984%" id="a"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#c3352e" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="c"><stop stop-color="#606483" stop-opacity="0" offset="0%"/><stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="d"><stop stop-color="#0B0D19" stop-opacity=".32" offset="0%"/><stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/></linearGradient><filter id="b"><feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/><feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/><feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/><feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M88.494 90c67.04 7.177 161.094-24.753 224.996-90H.2c25.3 48.079 42.361 85.083 88.294 90z" transform="translate(1051)" fill="url(#a)" filter="url(#b)"/><path d="M250.464 367.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z" fill="url(#c)" transform="rotate(143 810.285 354.367)"/><path d="M373.408 256.178c88.026 32.429 156-25.04 156-55.929 0-30.888-69.843-55.929-156-55.929-86.156 0-156 25.04-156 55.93 0 30.888 67.975 23.5 156 55.928z" fill="url(#d)" transform="rotate(136 905.21 332.676)"/></g></svg> <svg width="1440" height="318" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="38.706%" y1="-187.115%" x2="18.675%" y2="110.984%" id="a">
<stop stop-color="#FFF" stop-opacity="0" offset="0%"/>
<stop stop-color="#c3352e" offset="100%"/>
</linearGradient>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="c">
<stop stop-color="#606483" stop-opacity="0" offset="0%"/>
<stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/>
</linearGradient>
<linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="d">
<stop stop-color="#0B0D19" stop-opacity=".32" offset="0%"/>
<stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/>
</linearGradient>
<filter id="b">
<feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/>
<feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/>
<feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/>
<feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<path d="M88.494 90c67.04 7.177 161.094-24.753 224.996-90H.2c25.3 48.079 42.361 85.083 88.294 90z"
transform="translate(1051)" fill="url(#a)" filter="url(#b)"/>
<path
d="M250.464 367.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z"
fill="url(#c)" transform="rotate(143 810.285 354.367)"/>
<path
d="M373.408 256.178c88.026 32.429 156-25.04 156-55.929 0-30.888-69.843-55.929-156-55.929-86.156 0-156 25.04-156 55.93 0 30.888 67.975 23.5 156 55.928z"
fill="url(#d)" transform="rotate(136 905.21 332.676)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1 +1,5 @@
<svg width="96" height="48" xmlns="http://www.w3.org/2000/svg"><text y="35" x="48" fill="#fff" stroke-width="0" font-size="36" font-family="Monospace" text-anchor="middle" stroke="#fff">&lt;/&gt;</text></svg> <svg width="96" height="48" xmlns="http://www.w3.org/2000/svg">
<text y="35" x="48" fill="#fff" stroke-width="0" font-size="36" font-family="Monospace" text-anchor="middle"
stroke="#fff">&lt;/&gt;
</text>
</svg>

Before

Width:  |  Height:  |  Size: 208 B

After

Width:  |  Height:  |  Size: 224 B

View File

@ -1 +1,8 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="style-scope yt-icon" style="width:100%;height:100%" pointer-events="none" display="block" fill="#fff"><g class="style-scope yt-icon"><path d="M25.462 19.105v6.848H4.515v-6.848H.489v8.861c0 1.111.9 2.012 2.016 2.012h24.967c1.115 0 2.016-.9 2.016-2.012v-8.861h-4.026zM14.62 18.426l-5.764-6.965s-.877-.828.074-.828h3.248V9.217.494S12.049 0 12.793 0h4.572c.536 0 .524.416.524.416V10.424h2.998c1.154 0 .285.867.285.867s-4.904 6.51-5.588 7.193c-.492.495-.964-.058-.964-.058z" class="style-scope yt-icon"/></g></svg> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="style-scope yt-icon" style="width:100%;height:100%"
pointer-events="none" display="block" fill="#fff">
<g class="style-scope yt-icon">
<path
d="M25.462 19.105v6.848H4.515v-6.848H.489v8.861c0 1.111.9 2.012 2.016 2.012h24.967c1.115 0 2.016-.9 2.016-2.012v-8.861h-4.026zM14.62 18.426l-5.764-6.965s-.877-.828.074-.828h3.248V9.217.494S12.049 0 12.793 0h4.572c.536 0 .524.416.524.416V10.424h2.998c1.154 0 .285.867.285.867s-4.904 6.51-5.588 7.193c-.492.495-.964-.058-.964-.058z"
class="style-scope yt-icon"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 576 B

After

Width:  |  Height:  |  Size: 634 B

View File

@ -1 +1,35 @@
<svg width="1440" height="582" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#606483" stop-opacity="0" offset="0%"/><stop stop-color="#363636" stop-opacity=".72" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b"><stop stop-color="#363636" offset="0%"/><stop stop-color="#363636" stop-opacity="0" offset="100%"/></linearGradient><radialGradient cx="33.3%" cy="43.394%" fx="33.3%" fy="43.394%" r="57.93%" gradientTransform="matrix(.24796 -.96592 .92535 .25883 -.151 .643)" id="c"><stop stop-color="#c3352e" stop-opacity="0" offset="0%"/><stop stop-color="#c3352e" stop-opacity=".64" offset="51.712%"/><stop stop-color="#c3352e" stop-opacity=".24" offset="100%"/></radialGradient><filter id="d"><feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/><feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/><feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/><feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z" fill="url(#a)" transform="rotate(24 -272.272 -82.087)"/><path d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z" fill="url(#b)" transform="rotate(24 -255.451 -119.868)"/><path d="M103.064 315.218c128.156 12.998 192.38 157.059 218.627 106.632 26.247-50.427-44.059-106.456 60.397-202.707 104.457-96.252-143.2-285.785-172.392-122.551C180.503 259.825-25.091 302.22 103.064 315.218z" transform="translate(1176 -33)" fill="url(#c)" filter="url(#d)"/></g></svg> <svg width="1440" height="582" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a">
<stop stop-color="#606483" stop-opacity="0" offset="0%"/>
<stop stop-color="#363636" stop-opacity=".72" offset="100%"/>
</linearGradient>
<linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b">
<stop stop-color="#363636" offset="0%"/>
<stop stop-color="#363636" stop-opacity="0" offset="100%"/>
</linearGradient>
<radialGradient cx="33.3%" cy="43.394%" fx="33.3%" fy="43.394%" r="57.93%"
gradientTransform="matrix(.24796 -.96592 .92535 .25883 -.151 .643)" id="c">
<stop stop-color="#c3352e" stop-opacity="0" offset="0%"/>
<stop stop-color="#c3352e" stop-opacity=".64" offset="51.712%"/>
<stop stop-color="#c3352e" stop-opacity=".24" offset="100%"/>
</radialGradient>
<filter id="d">
<feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/>
<feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/>
<feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/>
<feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<path
d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z"
fill="url(#a)" transform="rotate(24 -272.272 -82.087)"/>
<path
d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z"
fill="url(#b)" transform="rotate(24 -255.451 -119.868)"/>
<path
d="M103.064 315.218c128.156 12.998 192.38 157.059 218.627 106.632 26.247-50.427-44.059-106.456 60.397-202.707 104.457-96.252-143.2-285.785-172.392-122.551C180.503 259.825-25.091 302.22 103.064 315.218z"
transform="translate(1176 -33)" fill="url(#c)" filter="url(#d)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" fill="#fff"><path d="M45.563 29.174l-22-15A1 1 0 0022 15v30a.999.999 0 001.563.826l22-15a1 1 0 000-1.652zM24 43.107V16.893L43.225 30 24 43.107z"/><path d="M30 0C13.458 0 0 13.458 0 30s13.458 30 30 30 30-13.458 30-30S46.542 0 30 0zm0 58C14.561 58 2 45.439 2 30S14.561 2 30 2s28 12.561 28 28-12.561 28-28 28z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" fill="#fff">
<path
d="M45.563 29.174l-22-15A1 1 0 0022 15v30a.999.999 0 001.563.826l22-15a1 1 0 000-1.652zM24 43.107V16.893L43.225 30 24 43.107z"/>
<path
d="M30 0C13.458 0 0 13.458 0 30s13.458 30 30 30 30-13.458 30-30S46.542 0 30 0zm0 58C14.561 58 2 45.439 2 30S14.561 2 30 2s28 12.561 28 28-12.561 28-28 28z"/>
</svg>

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 391 B

View File

@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176" width="32" height="32"><circle fill="red" cx="88" cy="88" r="88"/><path fill="#FFF" d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.9-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/><path fill="#FFF" d="M72 111l39-24-39-22z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176" width="32" height="32">
<circle fill="red" cx="88" cy="88" r="88"/>
<path fill="#FFF"
d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.9-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/>
<path fill="#FFF" d="M72 111l39-24-39-22z"/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 360 B

View File

@ -1,138 +1,137 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>YouTube Music Desktop App (Unofficial)</title> <title>YouTube Music Desktop App (Unofficial)</title>
<link <link
rel="icon" href="./favicon/favicon.ico"
href="./favicon/favicon.ico" rel="icon"
sizes="16x16" sizes="16x16"
type="image/x-icon" type="image/x-icon"
/> />
<link <link
rel="icon" href="./favicon/favicon_32.png"
href="./favicon/favicon_32.png" rel="icon"
sizes="32x32" sizes="32x32"
type="image/png" type="image/png"
/> />
<link <link
rel="icon" href="./favicon/favicon_48.png"
href="./favicon/favicon_48.png" rel="icon"
sizes="48x48" sizes="48x48"
type="image/png" type="image/png"
/> />
<link <link
rel="icon" href="./favicon/favicon_96.png"
href="./favicon/favicon_96.png" rel="icon"
sizes="96x96" sizes="96x96"
type="image/png" type="image/png"
/> />
<link <link
rel="icon" href="./favicon/favicon_144.png"
href="./favicon/favicon_144.png" rel="icon"
sizes="144x144" sizes="144x144"
type="image/png" type="image/png"
/> />
<meta name="theme-color" content="#131313" /> <meta content="#131313" name="theme-color"/>
<meta <meta
name="description" content="YouTube Music Unofficial Desktop App with built-in ad blocker and downloader"
content="YouTube Music Unofficial Desktop App with built-in ad blocker and downloader" name="description"
/> />
<meta <meta
property="og:site_name" content="YouTube&nbsp;Music&nbsp;Desktop&nbsp;App"
content="YouTube&nbsp;Music&nbsp;Desktop&nbsp;App" property="og:site_name"
/> />
<meta <meta
property="og:url" class="meta-url"
class="meta-url" content="https://th-ch.github.io/youtube-music"
content="https://th-ch.github.io/youtube-music" property="og:url"
/> />
<meta property="og:type" content="website" /> <meta content="website" property="og:type"/>
<meta <meta
name="twitter:url" class="meta-url"
class="meta-url" content="https://th-ch.github.io/youtube-music"
content="https://th-ch.github.io/youtube-music" name="twitter:url"
/> />
<link href="./style/fonts.css" rel="stylesheet" /> <link href="./style/fonts.css" rel="stylesheet"/>
<link rel="stylesheet" href="./style/style.css" /> <link href="./style/style.css" rel="stylesheet"/>
<script src="https://unpkg.com/scrollreveal"></script> <script src="https://unpkg.com/scrollreveal"></script>
</head> </head>
<body class="has-animations vsc-initialized" style="height: 100%;"> <body class="has-animations vsc-initialized" style="height: 100%;">
<div class="body-wrap boxed-container"> <div class="body-wrap boxed-container">
<header class="site-header text-light"> <header class="site-header text-light">
<div class="container"> <div class="container">
<div class="site-header-inner"> <div class="site-header-inner">
<div class="brand header-brand"> <div class="brand header-brand">
<h1 class="m-0"> <h1 class="m-0">
<a href="https://github.com/th-ch/youtube-music"> <a href="https://github.com/th-ch/youtube-music">
<img <img
class="header-logo-image" alt="YouTube Music"
src="./img/youtube-music.svg" class="header-logo-image"
alt="YouTube Music" src="./img/youtube-music.svg"
/> />
</a> </a>
</h1> </h1>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<main> <main>
<section class="hero text-center text-light"> <section class="hero text-center text-light">
<div class="hero-bg"></div> <div class="hero-bg"></div>
<div class="hero-particles-container"> <div class="hero-particles-container">
<canvas id="hero-particles"></canvas> <canvas id="hero-particles"></canvas>
</div> </div>
<div class="container-sm"> <div class="container-sm">
<div class="hero-inner"> <div class="hero-inner">
<div class="hero-copy"> <div class="hero-copy">
<h1 class="hero-title mt-0"> <h1 class="hero-title mt-0">
Custom YouTube Music Desktop App Custom YouTube Music Desktop App
</h1> </h1>
<p class="hero-paragraph"> <p class="hero-paragraph">
Open source, cross-platform, unofficial YouTube Music Desktop Open source, cross-platform, unofficial YouTube Music Desktop
App with built-in <strong>ad blocker</strong> and App with built-in <strong>ad blocker</strong> and
<strong>downloader</strong> <strong>downloader</strong>
</p> </p>
<div class="hero-cta"> <div class="hero-cta">
<a <a
class="button button-primary button-wide-mobile" class="button button-primary button-wide-mobile"
href="https://github.com/th-ch/youtube-music/releases/latest" href="https://github.com/th-ch/youtube-music/releases/latest"
>Download</a >Download</a
> >
</div> </div>
</div> </div>
<div class="mockup-container"> <div class="mockup-container">
<div class="mockup-bg"> <div class="mockup-bg">
<img <img
src="./img/youtube-music.png" alt="YouTube Music"
id="mockup-header-img" id="mockup-header-img"
alt="YouTube Music" src="./img/youtube-music.png"
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section class="features-extended section"> <section class="features-extended section">
<div class="features-extended-inner section-inner"> <div class="features-extended-inner section-inner">
<div class="features-extended-wrap"> <div class="features-extended-wrap">
<div class="container"> <div class="container">
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup" alt="Adblocker"
src="./img/adblock.svg" class="device-mockup"
width="100px" data-sr-id="0"
alt="Adblocker" src="./img/adblock.svg"
data-sr-id="0" style="
style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -157,12 +156,13 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
/> width="100px"
</div> />
<div </div>
class="feature-extended-body" <div
data-sr-id="5" class="feature-extended-body"
style=" data-sr-id="5"
style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -187,19 +187,19 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
> >
<h3 class="mt-0 mb-16">Built-in adblocker</h3> <h3 class="mt-0 mb-16">Built-in adblocker</h3>
<p class="m-0">Block all ads and tracking out of the box</p> <p class="m-0">Block all ads and tracking out of the box</p>
</div> </div>
</div> </div>
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup" alt="Downloader"
src="./img/download.svg" class="device-mockup"
alt="Downloader" data-sr-id="2"
data-sr-id="2" src="./img/download.svg"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -224,12 +224,12 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
/> />
</div> </div>
<div <div
class="feature-extended-body" class="feature-extended-body"
data-sr-id="6" data-sr-id="6"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -254,22 +254,22 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
> >
<h3 class="mt-0 mb-16">Built-in downloader</h3> <h3 class="mt-0 mb-16">Built-in downloader</h3>
<p class="m-0"> <p class="m-0">
Download (like youtube-dl) to custom formats (mp3, opus, Download (like youtube-dl) to custom formats (mp3, opus,
etc) directly from the interface etc) directly from the interface
</p> </p>
</div> </div>
</div> </div>
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup" alt="Plugins"
src="./img/plugins.svg" class="device-mockup"
alt="Plugins" data-sr-id="3"
data-sr-id="3" src="./img/plugins.svg"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -294,12 +294,12 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
/> />
</div> </div>
<div <div
class="feature-extended-body" class="feature-extended-body"
data-sr-id="7" data-sr-id="7"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -324,24 +324,24 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
> >
<h3 class="mt-0 mb-16">Many other plugins in one click</h3> <h3 class="mt-0 mb-16">Many other plugins in one click</h3>
<p class="m-0"> <p class="m-0">
Enhance your user experience with media keys, integrations Enhance your user experience with media keys, integrations
(Discord), cosmetic filters, notifications, TouchBar, (Discord), cosmetic filters, notifications, TouchBar,
auto-unpause and many more! Every plugin can be enabled or auto-unpause and many more! Every plugin can be enabled or
disabled in one click. disabled in one click.
</p> </p>
</div> </div>
</div> </div>
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup" alt="Code"
src="./img/code.svg" class="device-mockup"
alt="Code" data-sr-id="4"
data-sr-id="4" src="./img/code.svg"
style=" style="
visibility: visible; visibility: visible;
width: 200%; width: 200%;
opacity: 1; opacity: 1;
@ -367,12 +367,12 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
/> />
</div> </div>
<div <div
class="feature-extended-body" class="feature-extended-body"
data-sr-id="8" data-sr-id="8"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transform: matrix3d( transform: matrix3d(
@ -397,94 +397,94 @@
cubic-bezier(0.215, 0.61, 0.355, 1) 0s, cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s; transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
" "
> >
<h3 class="mt-0 mb-16">Open source & Cross platform</h3> <h3 class="mt-0 mb-16">Open source & Cross platform</h3>
<p class="m-0"> <p class="m-0">
Available for Windows (installer and portable), Mac and Available for Windows (installer and portable), Mac and
Linux (AppImage, deb, etc) Linux (AppImage, deb, etc)
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="main-particles-container"> <div class="main-particles-container">
<canvas id="main-particles"></canvas> <canvas id="main-particles"></canvas>
</div> </div>
</section> </section>
</main> </main>
<footer class="site-footer"> <footer class="site-footer">
<div class="footer-particles-container"> <div class="footer-particles-container">
<canvas id="footer-particles"></canvas> <canvas id="footer-particles"></canvas>
</div> </div>
<div class="site-footer-top"> <div class="site-footer-top">
<section class="cta section text-light"> <section class="cta section text-light">
<div class="container-sm"> <div class="container-sm">
<div class="cta-inner section-inner"> <div class="cta-inner section-inner">
<div class="cta-header text-center"> <div class="cta-header text-center">
<h2 class="section-title mt-0">Download and/or contribute</h2> <h2 class="section-title mt-0">Download and/or contribute</h2>
<p class="section-paragraph">Pull requests welcome!</p> <p class="section-paragraph">Pull requests welcome!</p>
<div class="cta-cta"> <div class="cta-cta">
<a <a
class="button button-primary button-wide-mobile" class="button button-primary button-wide-mobile"
href="https://github.com/th-ch/youtube-music" href="https://github.com/th-ch/youtube-music"
>Go to code</a >Go to code</a
> >
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<div class="site-footer-bottom"> <div class="site-footer-bottom">
<div class="container"> <div class="container">
<div class="site-footer-inner"> <div class="site-footer-inner">
<div class="brand footer-brand"> <div class="brand footer-brand">
<a href="https://github.com/th-ch/youtube-music"> <a href="https://github.com/th-ch/youtube-music">
<img src="./img/youtube-music.svg" alt="YouTube Music logo" /> <img alt="YouTube Music logo" src="./img/youtube-music.svg"/>
</a> </a>
</div> </div>
<ul class="footer-links list-reset"> <ul class="footer-links list-reset">
<li> <li>
<a href="https://github.com/th-ch/youtube-music">Main page</a> <a href="https://github.com/th-ch/youtube-music">Main page</a>
</li> </li>
<li> <li>
<a href="https://github.com/th-ch/youtube-music/issues" <a href="https://github.com/th-ch/youtube-music/issues"
>Issues</a >Issues</a
> >
</li> </li>
<li> <li>
<a href="https://github.com/th-ch/youtube-music/pulls" <a href="https://github.com/th-ch/youtube-music/pulls"
>Pull requests</a >Pull requests</a
> >
</li> </li>
</ul> </ul>
<ul class="footer-social-links list-reset"> <ul class="footer-social-links list-reset">
<li> <li>
<a href="https://github.com/th-ch/youtube-music"> <a href="https://github.com/th-ch/youtube-music">
<span class="screen-reader-text">GitHub</span> <span class="screen-reader-text">GitHub</span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" height="16"
width="16" viewBox="0 0 1792 1792"
height="16" width="16"
viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5q0 251-146.5 451.5t-378.5 277.5q-27 5-40-7t-13-30q0-3 .5-76.5t.5-134.5q0-97-52-142 57-6 102.5-18t94-39 81-66.5 53-105 20.5-150.5q0-119-79-206 37-91-8-204-28-9-81 11t-92 44l-38 24q-93-26-192-26t-192 26q-16-11-42.5-27t-83.5-38.5-85-13.5q-45 113-8 204-79 87-79 206 0 85 20.5 150t52.5 105 80.5 67 94 39 102.5 18q-39 36-49 103-21 10-45 15t-57 5-65.5-21.5-55.5-62.5q-19-32-48.5-52t-49.5-24l-20-3q-21 0-29 4.5t-5 11.5 9 14 13 12l7 5q22 10 43.5 38t31.5 51l10 23q13 38 44 61.5t67 30 69.5 7 55.5-3.5l23-4q0 38 .5 88.5t.5 54.5q0 18-13 30t-40 7q-232-77-378.5-277.5t-146.5-451.5q0-209 103-385.5t279.5-279.5 385.5-103zm-477 1103q3-7-7-12-10-3-13 2-3 7 7 12 9 6 13-2zm31 34q7-5-2-16-10-9-16-3-7 5 2 16 10 10 16 3zm30 45q9-7 0-19-8-13-17-6-9 5 0 18t17 7zm42 42q8-8-4-19-12-12-20-3-9 8 4 19 12 12 20 3zm57 25q3-11-13-16-15-4-19 7t13 15q15 6 19-6zm63 5q0-13-17-11-16 0-16 11 0 13 17 11 16 0 16-11zm58-10q-2-11-18-9-16 3-14 15t18 8 14-14z" d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5q0 251-146.5 451.5t-378.5 277.5q-27 5-40-7t-13-30q0-3 .5-76.5t.5-134.5q0-97-52-142 57-6 102.5-18t94-39 81-66.5 53-105 20.5-150.5q0-119-79-206 37-91-8-204-28-9-81 11t-92 44l-38 24q-93-26-192-26t-192 26q-16-11-42.5-27t-83.5-38.5-85-13.5q-45 113-8 204-79 87-79 206 0 85 20.5 150t52.5 105 80.5 67 94 39 102.5 18q-39 36-49 103-21 10-45 15t-57 5-65.5-21.5-55.5-62.5q-19-32-48.5-52t-49.5-24l-20-3q-21 0-29 4.5t-5 11.5 9 14 13 12l7 5q22 10 43.5 38t31.5 51l10 23q13 38 44 61.5t67 30 69.5 7 55.5-3.5l23-4q0 38 .5 88.5t.5 54.5q0 18-13 30t-40 7q-232-77-378.5-277.5t-146.5-451.5q0-209 103-385.5t279.5-279.5 385.5-103zm-477 1103q3-7-7-12-10-3-13 2-3 7 7 12 9 6 13-2zm31 34q7-5-2-16-10-9-16-3-7 5 2 16 10 10 16 3zm30 45q9-7 0-19-8-13-17-6-9 5 0 18t17 7zm42 42q8-8-4-19-12-12-20-3-9 8 4 19 12 12 20 3zm57 25q3-11-13-16-15-4-19 7t13 15q15 6 19-6zm63 5q0-13-17-11-16 0-16 11 0 13 17 11 16 0 16-11zm58-10q-2-11-18-9-16 3-14 15t18 8 14-14z"
fill="#fff" fill="#fff"
/> />
</svg> </svg>
</a> </a>
</li> </li>
</ul> </ul>
<div class="footer-copyright">© 2021 th-ch</div> <div class="footer-copyright">© 2021 th-ch</div>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>
</div> </div>
<script src="./js/main.js"></script> <script src="./js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,45 +1,45 @@
// Constants // Constants
const element = document.documentElement, const element = document.documentElement;
body = document.body, const { body } = document;
revealOnScroll = (window.sr = ScrollReveal({ mobile: false })); const revealOnScroll = (window.sr = ScrollReveal({ mobile: false }));
// Load animations // Load animations
element.classList.remove("no-js"); element.classList.remove('no-js');
element.classList.add("js"); element.classList.add('js');
window.addEventListener("load", function () { window.addEventListener('load', () => {
body.classList.add("is-loaded"); body.classList.add('is-loaded');
}); });
if (body.classList.contains("has-animations")) { if (body.classList.contains('has-animations')) {
window.addEventListener("load", function () { window.addEventListener('load', () => {
revealOnScroll.reveal(".feature-extended .device-mockup", { revealOnScroll.reveal('.feature-extended .device-mockup', {
duration: 600, duration: 600,
distance: "100px", distance: '100px',
easing: "cubic-bezier(0.215, 0.61, 0.355, 1)", easing: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
origin: "bottom", origin: 'bottom',
viewFactor: 0.6, viewFactor: 0.6,
}); });
revealOnScroll.reveal(".feature-extended .feature-extended-body", { revealOnScroll.reveal('.feature-extended .feature-extended-body', {
duration: 600, duration: 600,
distance: "40px", distance: '40px',
easing: "cubic-bezier(0.215, 0.61, 0.355, 1)", easing: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
origin: "top", origin: 'top',
viewFactor: 0.6, viewFactor: 0.6,
}); });
}); });
} }
// Bubble canvas // Bubble canvas
let bubbleCanvas = function (t) { const bubbleCanvas = function (t) {
let e = this; const e = this;
e.parentNode = t; e.parentNode = t;
e.setCanvasSize(); e.setCanvasSize();
window.addEventListener("resize", function () { window.addEventListener('resize', () => {
e.setCanvasSize(); e.setCanvasSize();
}); });
e.mouseX = 0; e.mouseX = 0;
e.mouseY = 0; e.mouseY = 0;
window.addEventListener("mousemove", function (t) { window.addEventListener('mousemove', (t) => {
(e.mouseX = t.clientX), (e.mouseY = t.clientY); (e.mouseX = t.clientX), (e.mouseY = t.clientY);
}); });
e.randomise(); e.randomise();
@ -55,15 +55,15 @@ bubbleCanvas.prototype.generateDecimalBetween = function (start, end) {
}; };
bubbleCanvas.prototype.update = function () { bubbleCanvas.prototype.update = function () {
let t = this; const t = this;
t.translateX = t.translateX - t.movementX; t.translateX -= t.movementX;
t.translateY = t.translateY - t.movementY; t.translateY -= t.movementY;
t.posX += (t.mouseX / (t.staticity / t.magnetism) - t.posX) / t.smoothFactor; t.posX += (t.mouseX / (t.staticity / t.magnetism) - t.posX) / t.smoothFactor;
t.posY += (t.mouseY / (t.staticity / t.magnetism) - t.posY) / t.smoothFactor; t.posY += (t.mouseY / (t.staticity / t.magnetism) - t.posY) / t.smoothFactor;
if ( if (
t.translateY + t.posY < 0 || t.translateY + t.posY < 0
t.translateX + t.posX < 0 || || t.translateX + t.posX < 0
t.translateX + t.posX > t.canvasWidth || t.translateX + t.posX > t.canvasWidth
) { ) {
t.randomise(); t.randomise();
t.translateY = t.canvasHeight; t.translateY = t.canvasHeight;
@ -71,7 +71,7 @@ bubbleCanvas.prototype.update = function () {
}; };
bubbleCanvas.prototype.randomise = function () { bubbleCanvas.prototype.randomise = function () {
this.colors = ["195,53,46", "172,54,46"]; this.colors = ['195,53,46', '172,54,46'];
this.velocity = 20; this.velocity = 20;
this.smoothFactor = 50; this.smoothFactor = 50;
@ -88,17 +88,17 @@ bubbleCanvas.prototype.randomise = function () {
this.translateY = this.generateDecimalBetween(0, this.canvasHeight); this.translateY = this.generateDecimalBetween(0, this.canvasHeight);
}; };
let drawBubbleCanvas = function (t) { const drawBubbleCanvas = function (t) {
this.canvas = document.getElementById(t); this.canvas = document.getElementById(t);
this.ctx = this.canvas.getContext("2d"); this.ctx = this.canvas.getContext('2d');
this.dpr = window.devicePixelRatio; this.dpr = window.devicePixelRatio;
}; };
drawBubbleCanvas.prototype.start = function (bubbleDensity) { drawBubbleCanvas.prototype.start = function (bubbleDensity) {
let t = this; const t = this;
t.bubbleDensity = bubbleDensity; t.bubbleDensity = bubbleDensity;
t.setCanvasSize(); t.setCanvasSize();
window.addEventListener("resize", function () { window.addEventListener('resize', () => {
t.setCanvasSize(); t.setCanvasSize();
}); });
t.bubblesList = []; t.bubblesList = [];
@ -114,23 +114,24 @@ drawBubbleCanvas.prototype.setCanvasSize = function () {
this.hdpi = this.h * this.dpr; this.hdpi = this.h * this.dpr;
this.canvas.width = this.wdpi; this.canvas.width = this.wdpi;
this.canvas.height = this.hdpi; this.canvas.height = this.hdpi;
this.canvas.style.width = this.w + "px"; this.canvas.style.width = this.w + 'px';
this.canvas.style.height = this.h + "px"; this.canvas.style.height = this.h + 'px';
this.ctx.scale(this.dpr, this.dpr); this.ctx.scale(this.dpr, this.dpr);
}; };
drawBubbleCanvas.prototype.animate = function () { drawBubbleCanvas.prototype.animate = function () {
let t = this; const t = this;
t.ctx.clearRect(0, 0, t.canvas.clientWidth, t.canvas.clientHeight); t.ctx.clearRect(0, 0, t.canvas.clientWidth, t.canvas.clientHeight);
t.bubblesList.forEach(function (e) { for (const e of t.bubblesList) {
e.update(); e.update();
t.ctx.translate(e.translateX, e.translateY); t.ctx.translate(e.translateX, e.translateY);
t.ctx.beginPath(); t.ctx.beginPath();
t.ctx.arc(e.posX, e.posY, e.size, 0, 2 * Math.PI); t.ctx.arc(e.posX, e.posY, e.size, 0, 2 * Math.PI);
t.ctx.fillStyle = "rgba(" + e.color + "," + e.alpha + ")"; t.ctx.fillStyle = 'rgba(' + e.color + ',' + e.alpha + ')';
t.ctx.fill(); t.ctx.fill();
t.ctx.setTransform(t.dpr, 0, 0, t.dpr, 0, 0); t.ctx.setTransform(t.dpr, 0, 0, t.dpr, 0, 0);
}); }
requestAnimationFrame(this.animate.bind(this)); requestAnimationFrame(this.animate.bind(this));
}; };
@ -139,15 +140,16 @@ drawBubbleCanvas.prototype.addBubble = function (t) {
}; };
drawBubbleCanvas.prototype.generateBubbles = function () { drawBubbleCanvas.prototype.generateBubbles = function () {
let t = this; const t = this;
for (let e = 0; e < t.bubbleDensity; e++) for (let e = 0; e < t.bubbleDensity; e++) {
t.addBubble(new bubbleCanvas(t.canvas.parentNode)); t.addBubble(new bubbleCanvas(t.canvas.parentNode));
}
}; };
// Night sky with stars canvas // Night sky with stars canvas
let starCanvas = function (t) { const starCanvas = function (t) {
this.canvas = document.getElementById(t); this.canvas = document.getElementById(t);
this.ctx = this.canvas.getContext("2d"); this.ctx = this.canvas.getContext('2d');
this.dpr = window.devicePixelRatio; this.dpr = window.devicePixelRatio;
}; };
@ -156,17 +158,17 @@ starCanvas.prototype.start = function () {
let h; let h;
const setCanvasExtents = () => { const setCanvasExtents = () => {
w = this.canvas.parentNode.clientWidth; w = this.canvas.parentNode.clientWidth;
h = this.canvas.parentNode.clientHeight; h = this.canvas.parentNode.clientHeight;
this.canvas.width = w; this.canvas.width = w;
this.canvas.height = h; this.canvas.height = h;
}; };
setCanvasExtents(); setCanvasExtents();
window.onresize = () => { window.addEventListener('resize', () => {
setCanvasExtents(); setCanvasExtents();
}; });
const makeStars = (count) => { const makeStars = (count) => {
const out = []; const out = [];
@ -178,19 +180,20 @@ starCanvas.prototype.start = function () {
}; };
out.push(s); out.push(s);
} }
return out; return out;
}; };
let stars = makeStars(10000); const stars = makeStars(10_000);
const clear = () => { const clear = () => {
this.ctx.fillStyle = "#212121"; this.ctx.fillStyle = '#212121';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}; };
const putPixel = (x, y, brightness) => { const putPixel = (x, y, brightness) => {
const intensity = brightness * 255; const intensity = brightness * 255;
const rgb = "rgb(" + intensity + "," + intensity + "," + intensity + ")"; const rgb = 'rgb(' + intensity + ',' + intensity + ',' + intensity + ')';
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.arc(x, y, 0.9, 0, 2 * Math.PI); this.ctx.arc(x, y, 0.9, 0, 2 * Math.PI);
this.ctx.fillStyle = rgb; this.ctx.fillStyle = rgb;
@ -199,7 +202,7 @@ starCanvas.prototype.start = function () {
const moveStars = (distance) => { const moveStars = (distance) => {
const count = stars.length; const count = stars.length;
for (var i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const s = stars[i]; const s = stars[i];
s.z -= distance; s.z -= distance;
while (s.z <= 1) { while (s.z <= 1) {
@ -208,15 +211,15 @@ starCanvas.prototype.start = function () {
} }
}; };
let prevTime; let previousTime;
const init = (time) => { const init = (time) => {
prevTime = time; previousTime = time;
requestAnimationFrame(tick); requestAnimationFrame(tick);
}; };
const tick = (time) => { const tick = (time) => {
let elapsed = time - prevTime; const elapsed = time - previousTime;
prevTime = time; previousTime = time;
moveStars(elapsed * 0.1); moveStars(elapsed * 0.1);
@ -226,7 +229,7 @@ starCanvas.prototype.start = function () {
const cy = h / 2; const cy = h / 2;
const count = stars.length; const count = stars.length;
for (var i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const star = stars[i]; const star = stars[i];
const x = cx + star.x / (star.z * 0.001); const x = cx + star.x / (star.z * 0.001);
@ -236,7 +239,7 @@ starCanvas.prototype.start = function () {
continue; continue;
} }
const d = star.z / 1000.0; const d = star.z / 1000;
const b = 1 - d * d; const b = 1 - d * d;
putPixel(x, y, b); putPixel(x, y, b);
@ -249,12 +252,12 @@ starCanvas.prototype.start = function () {
}; };
// Start canvas animations // Start canvas animations
window.addEventListener("load", function () { window.addEventListener('load', () => {
// Stars // Stars
const headCanvas = new starCanvas("hero-particles"); const headCanvas = new starCanvas('hero-particles');
// Bubbles // Bubbles
const footerCanvas = new drawBubbleCanvas("footer-particles"); const footerCanvas = new drawBubbleCanvas('footer-particles');
const mainCanvas = new drawBubbleCanvas("main-particles"); const mainCanvas = new drawBubbleCanvas('main-particles');
headCanvas.start(); headCanvas.start();
footerCanvas.start(30); footerCanvas.start(30);

View File

@ -6,6 +6,7 @@
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H0TbFhsqMA6aw.woff2) format('woff2'); src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H0TbFhsqMA6aw.woff2) format('woff2');
unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F; unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;
} }
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Heebo'; font-family: 'Heebo';
@ -14,6 +15,7 @@
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H2TbFhsqMA.woff2) format('woff2'); src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H2TbFhsqMA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
/* hebrew */ /* hebrew */
@font-face { @font-face {
font-family: 'Heebo'; font-family: 'Heebo';
@ -22,6 +24,7 @@
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H0TbFhsqMA6aw.woff2) format('woff2'); src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H0TbFhsqMA6aw.woff2) format('woff2');
unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F; unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;
} }
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Heebo'; font-family: 'Heebo';
@ -30,6 +33,7 @@
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H2TbFhsqMA.woff2) format('woff2'); src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H2TbFhsqMA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
/* latin-ext */ /* latin-ext */
@font-face { @font-face {
font-family: 'Oxygen'; font-family: 'Oxygen';
@ -38,6 +42,7 @@
src: url(https://fonts.gstatic.com/s/oxygen/v10/2sDcZG1Wl4LcnbuCNWgzZmW5Kb8VZBHR.woff2) format('woff2'); src: url(https://fonts.gstatic.com/s/oxygen/v10/2sDcZG1Wl4LcnbuCNWgzZmW5Kb8VZBHR.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Oxygen'; font-family: 'Oxygen';

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,50 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<title>Cannot load YouTube Music</title> <title>Cannot load YouTube Music</title>
<style> <style>
body { body {
background: #000; background: #000;
} }
.container { .container {
margin: 0; margin: 0;
font-family: Roboto, Arial, sans-serif; font-family: Roboto, Arial, sans-serif;
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-right: -50%; margin-right: -50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
text-align: center; text-align: center;
} }
.button { .button {
background: #065fd4; background: #065fd4;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: white; color: white;
font: inherit; font: inherit;
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
border-radius: 2px; border-radius: 2px;
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-align: center; text-align: center;
padding: 8px 22px; padding: 8px 22px;
display: inline-block; display: inline-block;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<p>Cannot load YouTube Music… Internet disconnected?</p> <p>Cannot load YouTube Music… Internet disconnected?</p>
<a href="#" class="button" onclick="reload()">Retry</a> <a class="button" href="#" onclick="reload()">Retry</a>
</div> </div>
</body> </body>
</html> </html>

888
index.js
View File

@ -1,516 +1,558 @@
"use strict"; 'use strict';
const path = require("path"); const path = require('node:path');
const electron = require("electron"); const electron = require('electron');
const enhanceWebRequest = require("electron-better-web-request").default; const enhanceWebRequest = require('electron-better-web-request').default;
const is = require("electron-is"); const is = require('electron-is');
const unhandled = require("electron-unhandled"); const unhandled = require('electron-unhandled');
const { autoUpdater } = require("electron-updater"); const { autoUpdater } = require('electron-updater');
const config = require("./config"); const config = require('./config');
const { setApplicationMenu } = require("./menu"); const { setApplicationMenu } = require('./menu');
const { fileExists, injectCSS } = require("./plugins/utils"); const { fileExists, injectCSS } = require('./plugins/utils');
const { isTesting } = require("./utils/testing"); const { isTesting } = require('./utils/testing');
const { setUpTray } = require("./tray"); const { setUpTray } = require('./tray');
const { setupSongInfo } = require("./providers/song-info"); const { setupSongInfo } = require('./providers/song-info');
const { setupAppControls, restart } = require("./providers/app-controls"); const { setupAppControls, restart } = require('./providers/app-controls');
const { APP_PROTOCOL, setupProtocolHandler, handleProtocol } = require("./providers/protocol-handler"); const { APP_PROTOCOL, setupProtocolHandler, handleProtocol } = require('./providers/protocol-handler');
// Catch errors and log them // Catch errors and log them
unhandled({ unhandled({
logger: console.error, logger: console.error,
showDialog: false, showDialog: false,
}); });
// Disable Node options if the env var is set // Disable Node options if the env var is set
process.env.NODE_OPTIONS = ""; process.env.NODE_OPTIONS = '';
const app = electron.app; const { app } = electron;
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow; let mainWindow;
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.exit(); if (!gotTheLock) {
app.exit();
}
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer'); // Required for downloader
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397 app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
if (config.get("options.disableHardwareAcceleration")) { if (config.get('options.disableHardwareAcceleration')) {
if (is.dev()) { if (is.dev()) {
console.log("Disabling hardware acceleration"); console.log('Disabling hardware acceleration');
} }
app.disableHardwareAcceleration();
app.disableHardwareAcceleration();
} }
if (is.linux() && config.plugins.isEnabled("shortcuts")) { if (is.linux() && config.plugins.isEnabled('shortcuts')) {
//stops chromium from launching it's own mpris service // Stops chromium from launching it's own mpris service
app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
} }
if (config.get("options.proxy")) { if (config.get('options.proxy')) {
app.commandLine.appendSwitch("proxy-server", config.get("options.proxy")); app.commandLine.appendSwitch('proxy-server', config.get('options.proxy'));
} }
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
require("electron-debug")({ require('electron-debug')({
showDevTools: false //disable automatic devTools on new window showDevTools: false, // Disable automatic devTools on new window
}); });
let icon = "assets/youtube-music.png"; let icon = 'assets/youtube-music.png';
if (process.platform == "win32") { if (process.platform == 'win32') {
icon = "assets/generated/icon.ico"; icon = 'assets/generated/icon.ico';
} else if (process.platform == "darwin") { } else if (process.platform == 'darwin') {
icon = "assets/generated/icon.icns"; icon = 'assets/generated/icon.icns';
} }
function onClosed() { function onClosed() {
// Dereference the window // Dereference the window
// For multiple windows store them in an array // For multiple windows store them in an array
mainWindow = null; mainWindow = null;
} }
/** @param {Electron.BrowserWindow} win */ /** @param {Electron.BrowserWindow} win */
function loadPlugins(win) { function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css")); injectCSS(win.webContents, path.join(__dirname, 'youtube-music.css'));
// Load user CSS // Load user CSS
const themes = config.get("options.themes"); const themes = config.get('options.themes');
if (Array.isArray(themes)) { if (Array.isArray(themes)) {
themes.forEach((cssFile) => { for (const cssFile of themes) {
fileExists( fileExists(
cssFile, cssFile,
() => { () => {
injectCSS(win.webContents, cssFile); injectCSS(win.webContents, cssFile);
}, },
() => { () => {
console.warn(`CSS file "${cssFile}" does not exist, ignoring`); console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
} },
); );
}); }
} }
win.webContents.once("did-finish-load", () => { win.webContents.once('did-finish-load', () => {
if (is.dev()) { if (is.dev()) {
console.log("did finish load"); console.log('did finish load');
win.webContents.openDevTools(); win.webContents.openDevTools();
} }
}); });
config.plugins.getEnabled().forEach(([plugin, options]) => { for (const [plugin, options] of config.plugins.getEnabled()) {
console.log("Loaded plugin - " + plugin); console.log('Loaded plugin - ' + plugin);
const pluginPath = path.join(__dirname, "plugins", plugin, "back.js"); const pluginPath = path.join(__dirname, 'plugins', plugin, 'back.js');
fileExists(pluginPath, () => { fileExists(pluginPath, () => {
const handle = require(pluginPath); const handle = require(pluginPath);
handle(win, options); handle(win, options);
}); });
}); }
} }
function createMainWindow() { function createMainWindow() {
const windowSize = config.get("window-size"); const windowSize = config.get('window-size');
const windowMaximized = config.get("window-maximized"); const windowMaximized = config.get('window-maximized');
const windowPosition = config.get("window-position"); const windowPosition = config.get('window-position');
const useInlineMenu = config.plugins.isEnabled("in-app-menu"); const useInlineMenu = config.plugins.isEnabled('in-app-menu');
const win = new electron.BrowserWindow({ const win = new electron.BrowserWindow({
icon: icon, icon,
width: windowSize.width, width: windowSize.width,
height: windowSize.height, height: windowSize.height,
backgroundColor: "#000", backgroundColor: '#000',
show: false, show: false,
webPreferences: { webPreferences: {
// TODO: re-enable contextIsolation once it can work with ffmepg.wasm // TODO: re-enable contextIsolation once it can work with ffmepg.wasm
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126 // Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
contextIsolation: false, contextIsolation: false,
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, 'preload.js'),
nodeIntegrationInSubFrames: true, nodeIntegrationInSubFrames: true,
affinity: "main-window", // main window, and addition windows should work in one process affinity: 'main-window', // Main window, and addition windows should work in one process
...(!isTesting() ...(isTesting()
? { ? undefined
// Sandbox is only enabled in tests for now : {
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts // Sandbox is only enabled in tests for now
sandbox: false, // See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
} sandbox: false,
: undefined), }),
}, },
frame: !is.macOS() && !useInlineMenu, frame: !is.macOS() && !useInlineMenu,
titleBarStyle: useInlineMenu titleBarStyle: useInlineMenu
? "hidden" ? 'hidden'
: is.macOS() : (is.macOS()
? "hiddenInset" ? 'hiddenInset'
: "default", : 'default'),
autoHideMenuBar: config.get("options.hideMenu"), autoHideMenuBar: config.get('options.hideMenu'),
}); });
loadPlugins(win); loadPlugins(win);
if (windowPosition) { if (windowPosition) {
const { x, y } = windowPosition; const { x, y } = windowPosition;
const winSize = win.getSize(); const winSize = win.getSize();
const displaySize = const displaySize
electron.screen.getDisplayNearestPoint(windowPosition).bounds; = electron.screen.getDisplayNearestPoint(windowPosition).bounds;
if ( if (
x + winSize[0] < displaySize.x - 8 || x + winSize[0] < displaySize.x - 8
x - winSize[0] > displaySize.x + displaySize.width || || x - winSize[0] > displaySize.x + displaySize.width
y < displaySize.y - 8 || || y < displaySize.y - 8
y > displaySize.y + displaySize.height || y > displaySize.y + displaySize.height
) { ) {
//Window is offscreen // Window is offscreen
if (is.dev()) { if (is.dev()) {
console.log( console.log(
`Window tried to render offscreen, windowSize=${winSize}, displaySize=${displaySize}, position=${windowPosition}` `Window tried to render offscreen, windowSize=${winSize}, displaySize=${displaySize}, position=${windowPosition}`,
); );
} }
} else { } else {
win.setPosition(x, y); win.setPosition(x, y);
} }
} }
if (windowMaximized) {
win.maximize();
}
if(config.get("options.alwaysOnTop")){ if (windowMaximized) {
win.setAlwaysOnTop(true); win.maximize();
} }
const urlToLoad = config.get("options.resumeOnStart") if (config.get('options.alwaysOnTop')) {
? config.get("url") win.setAlwaysOnTop(true);
: config.defaultConfig.url; }
win.webContents.loadURL(urlToLoad);
win.on("closed", onClosed);
const setPiPOptions = config.plugins.isEnabled("picture-in-picture") const urlToLoad = config.get('options.resumeOnStart')
? (key, value) => require("./plugins/picture-in-picture/back").setOptions({ [key]: value }) ? config.get('url')
: () => {}; : config.defaultConfig.url;
win.webContents.loadURL(urlToLoad);
win.on('closed', onClosed);
win.on("move", () => { const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
if (win.isMaximized()) return; ? (key, value) => require('./plugins/picture-in-picture/back').setOptions({ [key]: value })
let position = win.getPosition(); : () => {
const isPiPEnabled = };
config.plugins.isEnabled("picture-in-picture") &&
config.plugins.getOptions("picture-in-picture")["isInPiP"];
if (!isPiPEnabled) {
lateSave("window-position", { x: position[0], y: position[1] });
} else if(config.plugins.getOptions("picture-in-picture")["savePosition"]) {
lateSave("pip-position", position, setPiPOptions);
}
});
let winWasMaximized; win.on('move', () => {
if (win.isMaximized()) {
return;
}
win.on("resize", () => { const position = win.getPosition();
const windowSize = win.getSize(); const isPiPEnabled
const isMaximized = win.isMaximized(); = config.plugins.isEnabled('picture-in-picture')
&& config.plugins.getOptions('picture-in-picture').isInPiP;
if (!isPiPEnabled) {
lateSave('window-position', { x: position[0], y: position[1] });
} else if (config.plugins.getOptions('picture-in-picture').savePosition) {
lateSave('pip-position', position, setPiPOptions);
}
});
const isPiPEnabled = let winWasMaximized;
config.plugins.isEnabled("picture-in-picture") &&
config.plugins.getOptions("picture-in-picture")["isInPiP"];
if (!isPiPEnabled && winWasMaximized !== isMaximized) { win.on('resize', () => {
winWasMaximized = isMaximized; const windowSize = win.getSize();
config.set("window-maximized", isMaximized); const isMaximized = win.isMaximized();
}
if (isMaximized) return;
if (!isPiPEnabled) { const isPiPEnabled
lateSave("window-size", { = config.plugins.isEnabled('picture-in-picture')
width: windowSize[0], && config.plugins.getOptions('picture-in-picture').isInPiP;
height: windowSize[1],
});
} else if(config.plugins.getOptions("picture-in-picture")["saveSize"]) {
lateSave("pip-size", windowSize, setPiPOptions);
}
});
let savedTimeouts = {}; if (!isPiPEnabled && winWasMaximized !== isMaximized) {
function lateSave(key, value, fn = config.set) { winWasMaximized = isMaximized;
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]); config.set('window-maximized', isMaximized);
}
savedTimeouts[key] = setTimeout(() => { if (isMaximized) {
fn(key, value); return;
savedTimeouts[key] = undefined; }
}, 600);
}
win.webContents.on("render-process-gone", (event, webContents, details) => { if (!isPiPEnabled) {
showUnresponsiveDialog(win, details); lateSave('window-size', {
}); width: windowSize[0],
height: windowSize[1],
});
} else if (config.plugins.getOptions('picture-in-picture').saveSize) {
lateSave('pip-size', windowSize, setPiPOptions);
}
});
win.once("ready-to-show", () => { const savedTimeouts = {};
if (config.get("options.appVisible")) {
win.show();
}
});
removeContentSecurityPolicy(); function lateSave(key, value, fn = config.set) {
if (savedTimeouts[key]) {
clearTimeout(savedTimeouts[key]);
}
return win; savedTimeouts[key] = setTimeout(() => {
fn(key, value);
savedTimeouts[key] = undefined;
}, 600);
}
win.webContents.on('render-process-gone', (event, webContents, details) => {
showUnresponsiveDialog(win, details);
});
win.once('ready-to-show', () => {
if (config.get('options.appVisible')) {
win.show();
}
});
removeContentSecurityPolicy();
return win;
} }
app.once("browser-window-created", (event, win) => { app.once('browser-window-created', (event, win) => {
if (config.get("options.overrideUserAgent")) { if (config.get('options.overrideUserAgent')) {
// User agents are from https://developers.whatismybrowser.com/useragents/explore/ // User agents are from https://developers.whatismybrowser.com/useragents/explore/
const originalUserAgent = win.webContents.userAgent; const originalUserAgent = win.webContents.userAgent;
const userAgents = { const userAgents = {
mac: "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0", mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0',
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", windows: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
linux: "Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0", linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0',
} };
const updatedUserAgent = const updatedUserAgent
is.macOS() ? userAgents.mac : = is.macOS() ? userAgents.mac
is.windows() ? userAgents.windows : : (is.windows() ? userAgents.windows
userAgents.linux; : userAgents.linux);
win.webContents.userAgent = updatedUserAgent; win.webContents.userAgent = updatedUserAgent;
app.userAgentFallback = updatedUserAgent; app.userAgentFallback = updatedUserAgent;
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => { win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
// this will only happen if login failed, and "retry" was pressed // This will only happen if login failed, and "retry" was pressed
if (win.webContents.getURL().startsWith("https://accounts.google.com") && details.url.startsWith("https://accounts.google.com")) { if (win.webContents.getURL().startsWith('https://accounts.google.com') && details.url.startsWith('https://accounts.google.com')) {
details.requestHeaders["User-Agent"] = originalUserAgent; details.requestHeaders['User-Agent'] = originalUserAgent;
} }
cb({ requestHeaders: details.requestHeaders });
});
}
setupSongInfo(win); cb({ requestHeaders: details.requestHeaders });
setupAppControls(); });
}
win.webContents.on("did-fail-load", ( setupSongInfo(win);
_event, setupAppControls();
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
) => {
const log = JSON.stringify({
error: "did-fail-load",
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
}, null, "\t");
if (is.dev()) {
console.log(log);
}
if( !(config.plugins.isEnabled("in-app-menu") && errorCode === -3)) { // -3 is a false positive with in-app-menu
win.webContents.send("log", log);
win.webContents.loadFile(path.join(__dirname, "error.html"));
}
});
win.webContents.on("will-prevent-unload", (event) => { win.webContents.on('did-fail-load', (
event.preventDefault(); _event,
}); errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
) => {
const log = JSON.stringify({
error: 'did-fail-load',
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
}, null, '\t');
if (is.dev()) {
console.log(log);
}
if (!(config.plugins.isEnabled('in-app-menu') && errorCode === -3)) { // -3 is a false positive with in-app-menu
win.webContents.send('log', log);
win.webContents.loadFile(path.join(__dirname, 'error.html'));
}
});
win.webContents.on('will-prevent-unload', (event) => {
event.preventDefault();
});
}); });
app.on("window-all-closed", () => { app.on('window-all-closed', () => {
if (process.platform !== "darwin") { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
// Unregister all shortcuts. // Unregister all shortcuts.
electron.globalShortcut.unregisterAll(); electron.globalShortcut.unregisterAll();
}); });
app.on("activate", () => { app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the // On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (mainWindow === null) { if (mainWindow === null) {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
} else if (!mainWindow.isVisible()) { } else if (!mainWindow.isVisible()) {
mainWindow.show(); mainWindow.show();
} }
}); });
app.on("ready", () => { app.on('ready', () => {
if (config.get("options.autoResetAppCache")) { if (config.get('options.autoResetAppCache')) {
// Clear cache after 20s // Clear cache after 20s
const clearCacheTimeout = setTimeout(() => { const clearCacheTimeout = setTimeout(() => {
if (is.dev()) { if (is.dev()) {
console.log("Clearing app cache."); console.log('Clearing app cache.');
} }
electron.session.defaultSession.clearCache();
clearTimeout(clearCacheTimeout);
}, 20000);
}
// Register appID on windows electron.session.defaultSession.clearCache();
if (is.windows()) { clearTimeout(clearCacheTimeout);
const appID = "com.github.th-ch.youtube-music"; }, 20_000);
app.setAppUserModelId(appID); }
const appLocation = process.execPath;
const appData = app.getPath("appData");
// check shortcut validity if not in dev mode / running portable app
if (!is.dev() && !appLocation.startsWith(path.join(appData, "..", "Local", "Temp"))) {
const shortcutPath = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "YouTube Music.lnk");
try { // check if shortcut is registered and valid
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
if (
shortcutDetails.target !== appLocation ||
shortcutDetails.appUserModelId !== appID
) {
throw "needUpdate";
}
} catch (error) { // if not valid -> Register shortcut
electron.shell.writeShortcutLink(
shortcutPath,
error === "needUpdate" ? "update" : "create",
{
target: appLocation,
cwd: path.dirname(appLocation),
description: "YouTube Music Desktop App - including custom plugins",
appUserModelId: appID,
}
);
}
}
}
mainWindow = createMainWindow(); // Register appID on windows
setApplicationMenu(mainWindow); if (is.windows()) {
setUpTray(app, mainWindow); const appID = 'com.github.th-ch.youtube-music';
app.setAppUserModelId(appID);
const appLocation = process.execPath;
const appData = app.getPath('appData');
// Check shortcut validity if not in dev mode / running portable app
if (!is.dev() && !appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))) {
const shortcutPath = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'YouTube Music.lnk');
try { // Check if shortcut is registered and valid
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
if (
shortcutDetails.target !== appLocation
|| shortcutDetails.appUserModelId !== appID
) {
throw 'needUpdate';
}
} catch (error) { // If not valid -> Register shortcut
electron.shell.writeShortcutLink(
shortcutPath,
error === 'needUpdate' ? 'update' : 'create',
{
target: appLocation,
cwd: path.dirname(appLocation),
description: 'YouTube Music Desktop App - including custom plugins',
appUserModelId: appID,
},
);
}
}
}
setupProtocolHandler(mainWindow); mainWindow = createMainWindow();
setApplicationMenu(mainWindow);
setUpTray(app, mainWindow);
app.on('second-instance', (_event, commandLine, _workingDirectory) => { setupProtocolHandler(mainWindow);
const uri = `${APP_PROTOCOL}://`;
const protocolArgv = commandLine.find(arg => arg.startsWith(uri));
if (protocolArgv) {
const lastIndex = protocolArgv.endsWith("/") ? -1 : undefined;
const command = protocolArgv.slice(uri.length, lastIndex);
if (is.dev()) console.debug(`Received command over protocol: "${command}"`);
handleProtocol(command);
return;
}
if (!mainWindow) return;
if (mainWindow.isMinimized()) mainWindow.restore();
if (!mainWindow.isVisible()) mainWindow.show();
mainWindow.focus();
});
// Autostart at login app.on('second-instance', (_event, commandLine, _workingDirectory) => {
app.setLoginItemSettings({ const uri = `${APP_PROTOCOL}://`;
openAtLogin: config.get("options.startAtLogin"), const protocolArgv = commandLine.find((arg) => arg.startsWith(uri));
}); if (protocolArgv) {
const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
const command = protocolArgv.slice(uri.length, lastIndex);
if (is.dev()) {
console.debug(`Received command over protocol: "${command}"`);
}
if (!is.dev() && config.get("options.autoUpdates")) { handleProtocol(command);
const updateTimeout = setTimeout(() => { return;
autoUpdater.checkForUpdatesAndNotify(); }
clearTimeout(updateTimeout);
}, 2000);
autoUpdater.on("update-available", () => {
const downloadLink =
"https://github.com/th-ch/youtube-music/releases/latest";
const dialogOpts = {
type: "info",
buttons: ["OK", "Download", "Disable updates"],
title: "Application Update",
message: "A new version is available",
detail: `A new version is available and can be downloaded at ${downloadLink}`,
};
electron.dialog.showMessageBox(dialogOpts).then((dialogOutput) => {
switch (dialogOutput.response) {
// Download
case 1:
electron.shell.openExternal(downloadLink);
break;
// Disable updates
case 2:
config.set("options.autoUpdates", false);
break;
default:
break;
}
});
});
}
if (config.get("options.hideMenu") && !config.get("options.hideMenuWarned")) { if (!mainWindow) {
electron.dialog.showMessageBox(mainWindow, { return;
type: 'info', title: 'Hide Menu Enabled', }
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
});
config.set("options.hideMenuWarned", true);
}
// Optimized for Mac OS X if (mainWindow.isMinimized()) {
if (is.macOS() && !config.get("options.appVisible")) { mainWindow.restore();
app.dock.hide(); }
}
let forceQuit = false; if (!mainWindow.isVisible()) {
app.on("before-quit", () => { mainWindow.show();
forceQuit = true; }
});
if (is.macOS() || config.get("options.tray")) { mainWindow.focus();
mainWindow.on("close", (event) => { });
// Hide the window instead of quitting (quit is available in tray options)
if (!forceQuit) { // Autostart at login
event.preventDefault(); app.setLoginItemSettings({
mainWindow.hide(); openAtLogin: config.get('options.startAtLogin'),
} });
});
} if (!is.dev() && config.get('options.autoUpdates')) {
const updateTimeout = setTimeout(() => {
autoUpdater.checkForUpdatesAndNotify();
clearTimeout(updateTimeout);
}, 2000);
autoUpdater.on('update-available', () => {
const downloadLink
= 'https://github.com/th-ch/youtube-music/releases/latest';
const dialogOptions = {
type: 'info',
buttons: ['OK', 'Download', 'Disable updates'],
title: 'Application Update',
message: 'A new version is available',
detail: `A new version is available and can be downloaded at ${downloadLink}`,
};
electron.dialog.showMessageBox(dialogOptions).then((dialogOutput) => {
switch (dialogOutput.response) {
// Download
case 1: {
electron.shell.openExternal(downloadLink);
break;
}
// Disable updates
case 2: {
config.set('options.autoUpdates', false);
break;
}
default: {
break;
}
}
});
});
}
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
electron.dialog.showMessageBox(mainWindow, {
type: 'info', title: 'Hide Menu Enabled',
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
});
config.set('options.hideMenuWarned', true);
}
// Optimized for Mac OS X
if (is.macOS() && !config.get('options.appVisible')) {
app.dock.hide();
}
let forceQuit = false;
app.on('before-quit', () => {
forceQuit = true;
});
if (is.macOS() || config.get('options.tray')) {
mainWindow.on('close', (event) => {
// Hide the window instead of quitting (quit is available in tray options)
if (!forceQuit) {
event.preventDefault();
mainWindow.hide();
}
});
}
}); });
function showUnresponsiveDialog(win, details) { function showUnresponsiveDialog(win, details) {
if (!!details) { if (details) {
console.log("Unresponsive Error!\n"+JSON.stringify(details, null, "\t")) console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
} }
electron.dialog.showMessageBox(win, {
type: "error", electron.dialog.showMessageBox(win, {
title: "Window Unresponsive", type: 'error',
message: "The Application is Unresponsive", title: 'Window Unresponsive',
details: "We are sorry for the inconvenience! please choose what to do:", message: 'The Application is Unresponsive',
buttons: ["Wait", "Relaunch", "Quit"], details: 'We are sorry for the inconvenience! please choose what to do:',
cancelId: 0 buttons: ['Wait', 'Relaunch', 'Quit'],
}).then( result => { cancelId: 0,
switch (result.response) { }).then((result) => {
case 1: restart(); break; switch (result.response) {
case 2: app.quit(); break; case 1: {
} restart();
}); break;
}
case 2: {
app.quit();
break;
}
}
});
} }
function removeContentSecurityPolicy( function removeContentSecurityPolicy(
session = electron.session.defaultSession session = electron.session.defaultSession,
) { ) {
// Allows defining multiple "onHeadersReceived" listeners // Allows defining multiple "onHeadersReceived" listeners
// by enhancing the session. // by enhancing the session.
// Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener // Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener
enhanceWebRequest(session); enhanceWebRequest(session);
// Custom listener to tweak the content security policy // Custom listener to tweak the content security policy
session.webRequest.onHeadersReceived(function (details, callback) { session.webRequest.onHeadersReceived((details, callback) => {
details.responseHeaders ??= {} details.responseHeaders ??= {};
// Remove the content security policy // Remove the content security policy
delete details.responseHeaders["content-security-policy-report-only"]; delete details.responseHeaders['content-security-policy-report-only'];
delete details.responseHeaders["content-security-policy"]; delete details.responseHeaders['content-security-policy'];
callback({ cancel: false, responseHeaders: details.responseHeaders }); callback({ cancel: false, responseHeaders: details.responseHeaders });
}); });
// When multiple listeners are defined, apply them all // When multiple listeners are defined, apply them all
session.webRequest.setResolver("onHeadersReceived", (listeners) => { session.webRequest.setResolver('onHeadersReceived', (listeners) => {
const response = listeners.reduce( const response = listeners.reduce(
async (accumulator, listener) => { async (accumulator, listener) => {
if (accumulator.cancel) { if (accumulator.cancel) {
return accumulator; return accumulator;
} }
const result = await listener.apply(); const result = await listener.apply();
return { ...accumulator, ...result }; return { ...accumulator, ...result };
}, },
{ cancel: false } { cancel: false },
); );
return response; return response;
}); });
} }

845
menu.js
View File

@ -1,437 +1,442 @@
const { existsSync } = require("fs"); const { existsSync } = require('node:fs');
const path = require("path"); const path = require('node:path');
const { app, clipboard, Menu, dialog } = require("electron"); const { app, clipboard, Menu, dialog } = require('electron');
const is = require("electron-is"); const is = require('electron-is');
const { restart } = require("./providers/app-controls"); const prompt = require('custom-electron-prompt');
const { getAllPlugins } = require("./plugins/utils"); const { restart } = require('./providers/app-controls');
const config = require("./config"); const { getAllPlugins } = require('./plugins/utils');
const { startingPages } = require("./providers/extracted-data"); const config = require('./config');
const { startingPages } = require('./providers/extracted-data');
const promptOptions = require('./providers/prompt-options');
const prompt = require("custom-electron-prompt"); // True only if in-app-menu was loaded on launch
const promptOptions = require("./providers/prompt-options"); const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
// true only if in-app-menu was loaded on launch const pluginEnabledMenu = (plugin, label = '', hasSubmenu = false, refreshMenu = undefined) => ({
const inAppMenuActive = config.plugins.isEnabled("in-app-menu"); label: label || plugin,
type: 'checkbox',
checked: config.plugins.isEnabled(plugin),
click(item) {
if (item.checked) {
config.plugins.enable(plugin);
} else {
config.plugins.disable(plugin);
}
const pluginEnabledMenu = (plugin, label = "", hasSubmenu = false, refreshMenu = undefined) => ({ if (hasSubmenu) {
label: label || plugin, refreshMenu();
type: "checkbox", }
checked: config.plugins.isEnabled(plugin), },
click: (item) => {
if (item.checked) {
config.plugins.enable(plugin);
} else {
config.plugins.disable(plugin);
}
if (hasSubmenu) {
refreshMenu();
}
},
}); });
const mainMenuTemplate = (win) => { const mainMenuTemplate = (win) => {
const refreshMenu = () => { const refreshMenu = () => {
this.setApplicationMenu(win); this.setApplicationMenu(win);
if (inAppMenuActive) { if (inAppMenuActive) {
win.webContents.send("refreshMenu"); win.webContents.send('refreshMenu');
} }
} };
return [
{ return [
label: "Plugins", {
submenu: [ label: 'Plugins',
...getAllPlugins().map((plugin) => { submenu:
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js") getAllPlugins().map((plugin) => {
if (existsSync(pluginPath)) { const pluginPath = path.join(__dirname, 'plugins', plugin, 'menu.js');
let pluginLabel = plugin; if (existsSync(pluginPath)) {
if (pluginLabel === "crossfade") { let pluginLabel = plugin;
pluginLabel = "crossfade [beta]"; if (pluginLabel === 'crossfade') {
} pluginLabel = 'crossfade [beta]';
if (!config.plugins.isEnabled(plugin)) { }
return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu);
} if (!config.plugins.isEnabled(plugin)) {
const getPluginMenu = require(pluginPath); return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu);
return { }
label: pluginLabel,
submenu: [ const getPluginMenu = require(pluginPath);
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu), return {
{ type: "separator" }, label: pluginLabel,
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu), submenu: [
], pluginEnabledMenu(plugin, 'Enabled', true, refreshMenu),
}; { type: 'separator' },
} ...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
return pluginEnabledMenu(plugin); ],
}), };
], }
},
{ return pluginEnabledMenu(plugin);
label: "Options", })
submenu: [ ,
{ },
label: "Auto-update", {
type: "checkbox", label: 'Options',
checked: config.get("options.autoUpdates"), submenu: [
click: (item) => { {
config.setMenuOption("options.autoUpdates", item.checked); label: 'Auto-update',
}, type: 'checkbox',
}, checked: config.get('options.autoUpdates'),
{ click(item) {
label: "Resume last song when app starts", config.setMenuOption('options.autoUpdates', item.checked);
type: "checkbox", },
checked: config.get("options.resumeOnStart"), },
click: (item) => { {
config.setMenuOption("options.resumeOnStart", item.checked); label: 'Resume last song when app starts',
}, type: 'checkbox',
}, checked: config.get('options.resumeOnStart'),
{ click(item) {
label: 'Starting page', config.setMenuOption('options.resumeOnStart', item.checked);
submenu: Object.keys(startingPages).map((name) => ({ },
label: name, },
type: 'radio', {
checked: config.get('options.startingPage') === name, label: 'Starting page',
click: () => { submenu: Object.keys(startingPages).map((name) => ({
config.set('options.startingPage', name); label: name,
}, type: 'radio',
})) checked: config.get('options.startingPage') === name,
}, click() {
{ config.set('options.startingPage', name);
label: "Visual Tweaks", },
submenu: [ })),
{ },
label: "Remove upgrade button", {
type: "checkbox", label: 'Visual Tweaks',
checked: config.get("options.removeUpgradeButton"), submenu: [
click: (item) => { {
config.setMenuOption("options.removeUpgradeButton", item.checked); label: 'Remove upgrade button',
}, type: 'checkbox',
}, checked: config.get('options.removeUpgradeButton'),
{ click(item) {
label: "Like buttons", config.setMenuOption('options.removeUpgradeButton', item.checked);
submenu: [ },
{ },
label: "Default", {
type: "radio", label: 'Like buttons',
checked: !config.get("options.likeButtons"), submenu: [
click: () => { {
config.set("options.likeButtons", ''); label: 'Default',
}, type: 'radio',
}, checked: !config.get('options.likeButtons'),
{ click() {
label: "Force show", config.set('options.likeButtons', '');
type: "radio", },
checked: config.get("options.likeButtons") === 'force', },
click: () => { {
config.set("options.likeButtons", 'force'); label: 'Force show',
} type: 'radio',
}, checked: config.get('options.likeButtons') === 'force',
{ click() {
label: "Hide", config.set('options.likeButtons', 'force');
type: "radio", },
checked: config.get("options.likeButtons") === 'hide', },
click: () => { {
config.set("options.likeButtons", 'hide'); label: 'Hide',
} type: 'radio',
}, checked: config.get('options.likeButtons') === 'hide',
], click() {
}, config.set('options.likeButtons', 'hide');
{ },
label: "Theme", },
submenu: [ ],
{ },
label: "No theme", {
type: "radio", label: 'Theme',
checked: !config.get("options.themes"), // todo rename "themes" submenu: [
click: () => { {
config.set("options.themes", []); label: 'No theme',
}, type: 'radio',
}, checked: !config.get('options.themes'), // Todo rename "themes"
{ type: "separator" }, click() {
{ config.set('options.themes', []);
label: "Import custom CSS file", },
type: "radio", },
checked: false, { type: 'separator' },
click: async () => { {
const { filePaths } = await dialog.showOpenDialog({ label: 'Import custom CSS file',
filters: [{ name: "CSS Files", extensions: ["css"] }], type: 'radio',
properties: ["openFile", "multiSelections"], checked: false,
}); async click() {
if (filePaths) { const { filePaths } = await dialog.showOpenDialog({
config.set("options.themes", filePaths); filters: [{ name: 'CSS Files', extensions: ['css'] }],
} properties: ['openFile', 'multiSelections'],
}, });
}, if (filePaths) {
], config.set('options.themes', filePaths);
}, }
], },
}, },
{ ],
label: "Single instance lock", },
type: "checkbox", ],
checked: true, },
click: (item) => { {
if (!item.checked && app.hasSingleInstanceLock()) label: 'Single instance lock',
app.releaseSingleInstanceLock(); type: 'checkbox',
else if (item.checked && !app.hasSingleInstanceLock()) checked: true,
app.requestSingleInstanceLock(); click(item) {
}, if (!item.checked && app.hasSingleInstanceLock()) {
}, app.releaseSingleInstanceLock();
{ } else if (item.checked && !app.hasSingleInstanceLock()) {
label: "Always on top", app.requestSingleInstanceLock();
type: "checkbox", }
checked: config.get("options.alwaysOnTop"), },
click: (item) => { },
config.setMenuOption("options.alwaysOnTop", item.checked); {
win.setAlwaysOnTop(item.checked); label: 'Always on top',
}, type: 'checkbox',
}, checked: config.get('options.alwaysOnTop'),
...(is.windows() || is.linux() click(item) {
? [ config.setMenuOption('options.alwaysOnTop', item.checked);
{ win.setAlwaysOnTop(item.checked);
label: "Hide menu", },
type: "checkbox", },
checked: config.get("options.hideMenu"), ...(is.windows() || is.linux()
click: (item) => { ? [
config.setMenuOption("options.hideMenu", item.checked); {
if (item.checked && !config.get("options.hideMenuWarned")) { label: 'Hide menu',
dialog.showMessageBox(win, { type: 'checkbox',
type: 'info', title: 'Hide Menu Enabled', checked: config.get('options.hideMenu'),
message: "Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)" click(item) {
}); config.setMenuOption('options.hideMenu', item.checked);
} if (item.checked && !config.get('options.hideMenuWarned')) {
}, dialog.showMessageBox(win, {
}, type: 'info', title: 'Hide Menu Enabled',
] message: 'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
: []), });
...(is.windows() || is.macOS() }
? // Only works on Win/Mac },
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows },
[ ]
{ : []),
label: "Start at login", ...(is.windows() || is.macOS()
type: "checkbox", ? // Only works on Win/Mac
checked: config.get("options.startAtLogin"), // https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
click: (item) => { [
config.setMenuOption("options.startAtLogin", item.checked); {
}, label: 'Start at login',
}, type: 'checkbox',
] checked: config.get('options.startAtLogin'),
: []), click(item) {
{ config.setMenuOption('options.startAtLogin', item.checked);
label: "Tray", },
submenu: [ },
{ ]
label: "Disabled", : []),
type: "radio", {
checked: !config.get("options.tray"), label: 'Tray',
click: () => { submenu: [
config.setMenuOption("options.tray", false); {
config.setMenuOption("options.appVisible", true); label: 'Disabled',
}, type: 'radio',
}, checked: !config.get('options.tray'),
{ click() {
label: "Enabled + app visible", config.setMenuOption('options.tray', false);
type: "radio", config.setMenuOption('options.appVisible', true);
checked: },
config.get("options.tray") && config.get("options.appVisible"), },
click: () => { {
config.setMenuOption("options.tray", true); label: 'Enabled + app visible',
config.setMenuOption("options.appVisible", true); type: 'radio',
}, checked:
}, config.get('options.tray') && config.get('options.appVisible'),
{ click() {
label: "Enabled + app hidden", config.setMenuOption('options.tray', true);
type: "radio", config.setMenuOption('options.appVisible', true);
checked: },
config.get("options.tray") && !config.get("options.appVisible"), },
click: () => { {
config.setMenuOption("options.tray", true); label: 'Enabled + app hidden',
config.setMenuOption("options.appVisible", false); type: 'radio',
}, checked:
}, config.get('options.tray') && !config.get('options.appVisible'),
{ type: "separator" }, click() {
{ config.setMenuOption('options.tray', true);
label: "Play/Pause on click", config.setMenuOption('options.appVisible', false);
type: "checkbox", },
checked: config.get("options.trayClickPlayPause"), },
click: (item) => { { type: 'separator' },
config.setMenuOption("options.trayClickPlayPause", item.checked); {
}, label: 'Play/Pause on click',
}, type: 'checkbox',
], checked: config.get('options.trayClickPlayPause'),
}, click(item) {
{ type: "separator" }, config.setMenuOption('options.trayClickPlayPause', item.checked);
{ },
label: "Advanced options", },
submenu: [ ],
{ },
label: "Proxy", { type: 'separator' },
type: "checkbox", {
checked: !!config.get("options.proxy"), label: 'Advanced options',
click: (item) => { submenu: [
setProxy(item, win); {
}, label: 'Proxy',
}, type: 'checkbox',
{ checked: Boolean(config.get('options.proxy')),
label: "Override useragent", click(item) {
type: "checkbox", setProxy(item, win);
checked: config.get("options.overrideUserAgent"), },
click: (item) => { },
config.setMenuOption("options.overrideUserAgent", item.checked); {
} label: 'Override useragent',
}, type: 'checkbox',
{ checked: config.get('options.overrideUserAgent'),
label: "Disable hardware acceleration", click(item) {
type: "checkbox", config.setMenuOption('options.overrideUserAgent', item.checked);
checked: config.get("options.disableHardwareAcceleration"), },
click: (item) => { },
config.setMenuOption("options.disableHardwareAcceleration", item.checked); {
}, label: 'Disable hardware acceleration',
}, type: 'checkbox',
{ checked: config.get('options.disableHardwareAcceleration'),
label: "Restart on config changes", click(item) {
type: "checkbox", config.setMenuOption('options.disableHardwareAcceleration', item.checked);
checked: config.get("options.restartOnConfigChanges"), },
click: (item) => { },
config.setMenuOption("options.restartOnConfigChanges", item.checked); {
}, label: 'Restart on config changes',
}, type: 'checkbox',
{ checked: config.get('options.restartOnConfigChanges'),
label: "Reset App cache when app starts", click(item) {
type: "checkbox", config.setMenuOption('options.restartOnConfigChanges', item.checked);
checked: config.get("options.autoResetAppCache"), },
click: (item) => { },
config.setMenuOption("options.autoResetAppCache", item.checked); {
}, label: 'Reset App cache when app starts',
}, type: 'checkbox',
{ type: "separator" }, checked: config.get('options.autoResetAppCache'),
is.macOS() ? click(item) {
{ config.setMenuOption('options.autoResetAppCache', item.checked);
label: "Toggle DevTools", },
// Cannot use "toggleDevTools" role in MacOS },
click: () => { { type: 'separator' },
const { webContents } = win; is.macOS()
if (webContents.isDevToolsOpened()) { ? {
webContents.closeDevTools(); label: 'Toggle DevTools',
} else { // Cannot use "toggleDevTools" role in MacOS
const devToolsOptions = {}; click() {
webContents.openDevTools(devToolsOptions); const { webContents } = win;
} if (webContents.isDevToolsOpened()) {
}, webContents.closeDevTools();
} : } else {
{ role: "toggleDevTools" }, const devToolsOptions = {};
{ webContents.openDevTools(devToolsOptions);
label: "Edit config.json", }
click: () => { },
config.edit(); }
}, : { role: 'toggleDevTools' },
}, {
] label: 'Edit config.json',
}, click() {
], config.edit();
}, },
{ },
label: "View", ],
submenu: [ },
{ role: "reload" }, ],
{ role: "forceReload" }, },
{ type: "separator" }, {
{ role: "zoomIn" }, label: 'View',
{ role: "zoomOut" }, submenu: [
{ role: "resetZoom" }, { role: 'reload' },
{ type: "separator" }, { role: 'forceReload' },
{ role: "togglefullscreen" }, { type: 'separator' },
], { role: 'zoomIn' },
}, { role: 'zoomOut' },
{ { role: 'resetZoom' },
label: "Navigation", { type: 'separator' },
submenu: [ { role: 'togglefullscreen' },
{ ],
label: "Go back", },
click: () => { {
if (win.webContents.canGoBack()) { label: 'Navigation',
win.webContents.goBack(); submenu: [
} {
}, label: 'Go back',
}, click() {
{ if (win.webContents.canGoBack()) {
label: "Go forward", win.webContents.goBack();
click: () => { }
if (win.webContents.canGoForward()) { },
win.webContents.goForward(); },
} {
}, label: 'Go forward',
}, click() {
{ if (win.webContents.canGoForward()) {
label: "Copy current URL", win.webContents.goForward();
click: () => { }
const currentURL = win.webContents.getURL(); },
clipboard.writeText(currentURL); },
}, {
}, label: 'Copy current URL',
{ click() {
label: "Restart App", const currentURL = win.webContents.getURL();
click: restart clipboard.writeText(currentURL);
}, },
{ role: "quit" }, },
], {
}, label: 'Restart App',
]; click: restart,
} },
{ role: 'quit' },
],
},
];
};
module.exports.mainMenuTemplate = mainMenuTemplate; module.exports.mainMenuTemplate = mainMenuTemplate;
module.exports.setApplicationMenu = (win) => { module.exports.setApplicationMenu = (win) => {
const menuTemplate = [...mainMenuTemplate(win)]; const menuTemplate = [...mainMenuTemplate(win)];
if (process.platform === "darwin") { if (process.platform === 'darwin') {
const name = app.name; const { name } = app;
menuTemplate.unshift({ menuTemplate.unshift({
label: name, label: name,
submenu: [ submenu: [
{ role: "about" }, { role: 'about' },
{ type: "separator" }, { type: 'separator' },
{ role: "hide" }, { role: 'hide' },
{ role: "hideothers" }, { role: 'hideothers' },
{ role: "unhide" }, { role: 'unhide' },
{ type: "separator" }, { type: 'separator' },
{ {
label: "Select All", label: 'Select All',
accelerator: "CmdOrCtrl+A", accelerator: 'CmdOrCtrl+A',
selector: "selectAll:", selector: 'selectAll:',
}, },
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
{ type: "separator" }, { type: 'separator' },
{ role: "minimize" }, { role: 'minimize' },
{ role: "close" }, { role: 'close' },
{ role: "quit" }, { role: 'quit' },
], ],
}); });
} }
const menu = Menu.buildFromTemplate(menuTemplate); const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
}; };
async function setProxy(item, win) { async function setProxy(item, win) {
const output = await prompt({ const output = await prompt({
title: 'Set Proxy', title: 'Set Proxy',
label: 'Enter Proxy Address: (leave empty to disable)', label: 'Enter Proxy Address: (leave empty to disable)',
value: config.get("options.proxy"), value: config.get('options.proxy'),
type: 'input', type: 'input',
inputAttrs: { inputAttrs: {
type: 'url', type: 'url',
placeholder: "Example: 'socks5://127.0.0.1:9999" placeholder: "Example: 'socks5://127.0.0.1:9999",
}, },
width: 450, width: 450,
...promptOptions() ...promptOptions(),
}, win); }, win);
if (typeof output === "string") { if (typeof output === 'string') {
config.setMenuOption("options.proxy", output); config.setMenuOption('options.proxy', output);
item.checked = output !== ""; item.checked = output !== '';
} else { //user pressed cancel } else { // User pressed cancel
item.checked = !item.checked; //reset checkbox item.checked = !item.checked; // Reset checkbox
} }
} }

20078
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,175 +1,161 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "1.20.0", "version": "1.20.0",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"license": "MIT", "license": "MIT",
"repository": "th-ch/youtube-music", "repository": "th-ch/youtube-music",
"author": { "author": {
"name": "th-ch", "name": "th-ch",
"email": "th-ch@users.noreply.github.com", "email": "th-ch@users.noreply.github.com",
"url": "https://github.com/th-ch/youtube-music" "url": "https://github.com/th-ch/youtube-music"
}, },
"build": { "build": {
"appId": "com.github.th-ch.youtube-music", "appId": "com.github.th-ch.youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"mac": { "mac": {
"identity": null, "identity": null,
"files": [ "files": [
"!plugins/taskbar-mediacontrol${/*}" "!plugins/taskbar-mediacontrol${/*}"
], ],
"target": [ "target": [
{ {
"target": "dmg", "target": "dmg",
"arch": [ "arch": [
"x64", "x64",
"arm64" "arm64"
] ]
} }
], ],
"icon": "assets/generated/icons/mac/icon.icns" "icon": "assets/generated/icons/mac/icon.icns"
}, },
"win": { "win": {
"icon": "assets/generated/icons/win/icon.ico", "icon": "assets/generated/icons/win/icon.ico",
"files": [ "files": [
"!plugins/touchbar${/*}" "!plugins/touchbar${/*}"
], ],
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis",
"arch": [ "arch": [
"x64", "x64",
"arm64" "arm64"
] ]
}, },
{ {
"target": "portable", "target": "portable",
"arch": [ "arch": [
"x64", "x64",
"arm64" "arm64"
] ]
} }
] ]
}, },
"nsis": { "nsis": {
"runAfterFinish": false "runAfterFinish": false
}, },
"linux": { "linux": {
"icon": "assets/generated/icons/png", "icon": "assets/generated/icons/png",
"files": [ "files": [
"!plugins/{touchbar,taskbar-mediacontrol}${/*}" "!plugins/{touchbar,taskbar-mediacontrol}${/*}"
], ],
"category": "AudioVideo", "category": "AudioVideo",
"target": [ "target": [
"AppImage", "AppImage",
"snap", "snap",
"freebsd", "freebsd",
"deb", "deb",
"rpm" "rpm"
] ]
}, },
"snap": { "snap": {
"slots": [ "slots": [
{ {
"mpris": { "mpris": {
"interface": "mpris" "interface": "mpris"
} }
} }
] ]
} }
}, },
"scripts": { "scripts": {
"test": "playwright test", "test": "playwright test",
"test:debug": "DEBUG=pw:browser* playwright test", "test:debug": "DEBUG=pw:browser* playwright test",
"start": "electron .", "start": "electron .",
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .", "start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
"generate:package": "node utils/generate-package-json.js", "generate:package": "node utils/generate-package-json.js",
"postinstall": "npm run plugins", "postinstall": "npm run plugins",
"clean": "del-cli dist", "clean": "del-cli dist",
"build": "npm run clean && electron-builder --win --mac --linux -p never", "build": "npm run clean && electron-builder --win --mac --linux -p never",
"build:linux": "npm run clean && electron-builder --linux -p never", "build:linux": "npm run clean && electron-builder --linux -p never",
"build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never", "build:mac": "npm run clean && electron-builder --mac dmg:x64 -p never",
"build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never", "build:mac:arm64": "npm run clean && electron-builder --mac dmg:arm64 -p never",
"build:win": "npm run clean && electron-builder --win -p never", "build:win": "npm run clean && electron-builder --win -p never",
"lint": "xo", "lint": "xo",
"changelog": "auto-changelog", "changelog": "auto-changelog",
"plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions", "plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions",
"plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js", "plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
"plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass", "plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass",
"release:linux": "npm run clean && electron-builder --linux -p always -c.snap.publish=github", "release:linux": "npm run clean && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "npm run clean && electron-builder --mac -p always", "release:mac": "npm run clean && electron-builder --mac -p always",
"release:win": "npm run clean && electron-builder --win -p always" "release:win": "npm run clean && electron-builder --win -p always"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
}, },
"dependencies": { "dependencies": {
"@cliqz/adblocker-electron": "1.26.6", "@cliqz/adblocker-electron": "1.26.6",
"@ffmpeg/core": "0.12.2", "@ffmpeg/core": "0.12.2",
"@ffmpeg/ffmpeg": "0.12.5", "@ffmpeg/ffmpeg": "0.12.5",
"@foobar404/wave": "2.0.4", "@foobar404/wave": "2.0.4",
"@xhayper/discord-rpc": "1.0.22", "@xhayper/discord-rpc": "1.0.22",
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"browser-id3-writer": "5.0.0", "browser-id3-writer": "5.0.0",
"butterchurn": "2.6.7", "butterchurn": "2.6.7",
"butterchurn-presets": "2.4.7", "butterchurn-presets": "2.4.7",
"custom-electron-prompt": "1.5.7", "custom-electron-prompt": "1.5.7",
"custom-electron-titlebar": "4.2.7", "custom-electron-titlebar": "4.2.7",
"electron": "26.1.0", "electron": "26.1.0",
"electron-better-web-request": "1.0.1", "electron-better-web-request": "1.0.1",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-is": "3.0.0", "electron-is": "3.0.0",
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-unhandled": "4.0.1", "electron-unhandled": "4.0.1",
"electron-updater": "6.1.1", "electron-updater": "6.1.1",
"filenamify": "4.3.0", "filenamify": "4.3.0",
"howler": "2.2.3", "howler": "2.2.3",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"md5": "2.3.0", "md5": "2.3.0",
"mpris-service": "2.1.2", "mpris-service": "2.1.2",
"node-fetch": "2.7.0", "node-fetch": "2.7.0",
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9", "simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
"vudio": "2.1.1", "vudio": "2.1.1",
"youtubei.js": "4.3.0", "youtubei.js": "4.3.0",
"ytpl": "2.3.0" "ytpl": "2.3.0"
}, },
"overrides": { "overrides": {
"xml2js": "0.6.2", "xml2js": "0.6.2",
"@electron/universal": "1.4.1" "@electron/universal": "1.4.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.37.1", "@playwright/test": "1.37.1",
"auto-changelog": "2.4.0", "auto-changelog": "2.4.0",
"del-cli": "5.0.1", "del-cli": "5.0.1",
"electron-builder": "24.6.3", "electron-builder": "24.6.3",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"node-gyp": "9.4.0", "eslint": "8.48.0",
"playwright": "1.37.1", "eslint-plugin-import": "2.28.1",
"xo": "0.56.0" "eslint-plugin-prettier": "5.0.0",
}, "node-gyp": "9.4.0",
"auto-changelog": { "playwright": "1.37.1"
"hideCredit": true, },
"package": true, "auto-changelog": {
"unreleased": true, "hideCredit": true,
"output": "changelog.md" "package": true,
}, "unreleased": true,
"xo": { "output": "changelog.md"
"envs": [ }
"node",
"browser"
],
"rules": {
"quotes": [
"error",
"double",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
]
}
}
} }

View File

@ -1,13 +1,13 @@
const { loadAdBlockerEngine } = require("./blocker"); const { loadAdBlockerEngine } = require('./blocker');
const config = require("./config"); const config = require('./config');
module.exports = async (win, options) => { module.exports = async (win, options) => {
if (await config.shouldUseBlocklists()) { if (await config.shouldUseBlocklists()) {
loadAdBlockerEngine( loadAdBlockerEngine(
win.webContents.session, win.webContents.session,
options.cache, options.cache,
options.additionalBlockLists, options.additionalBlockLists,
options.disableDefaultLists, options.disableDefaultLists,
); );
} }
}; };

View File

@ -1,63 +1,63 @@
const { promises } = require("fs"); // used for caching const { promises } = require('node:fs'); // Used for caching
const path = require("path"); const path = require('node:path');
const { ElectronBlocker } = require("@cliqz/adblocker-electron"); const { ElectronBlocker } = require('@cliqz/adblocker-electron');
const fetch = require("node-fetch"); const fetch = require('node-fetch');
const SOURCES = [ const SOURCES = [
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt", 'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
// uBlock Origin // UBlock Origin
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt", 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt',
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2020.txt", 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2020.txt',
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2021.txt", 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2021.txt',
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2022.txt", 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2022.txt',
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2023.txt", 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2023.txt',
// Fanboy Annoyances // Fanboy Annoyances
"https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt", 'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
]; ];
const loadAdBlockerEngine = ( const loadAdBlockerEngine = (
session = undefined, session = undefined,
cache = true, cache = true,
additionalBlockLists = [], additionalBlockLists = [],
disableDefaultLists = false disableDefaultLists = false,
) => { ) => {
// Only use cache if no additional blocklists are passed // Only use cache if no additional blocklists are passed
const cachingOptions = const cachingOptions
cache && additionalBlockLists.length === 0 = cache && additionalBlockLists.length === 0
? { ? {
path: path.resolve(__dirname, "ad-blocker-engine.bin"), path: path.resolve(__dirname, 'ad-blocker-engine.bin'),
read: promises.readFile, read: promises.readFile,
write: promises.writeFile, write: promises.writeFile,
} }
: undefined; : undefined;
const lists = [ const lists = [
...(disableDefaultLists ? [] : SOURCES), ...(disableDefaultLists ? [] : SOURCES),
...additionalBlockLists, ...additionalBlockLists,
]; ];
ElectronBlocker.fromLists( ElectronBlocker.fromLists(
fetch, fetch,
lists, lists,
{ {
// when generating the engine for caching, do not load network filters // When generating the engine for caching, do not load network filters
// So that enhancing the session works as expected // So that enhancing the session works as expected
// Allowing to define multiple webRequest listeners // Allowing to define multiple webRequest listeners
loadNetworkFilters: session !== undefined, loadNetworkFilters: session !== undefined,
}, },
cachingOptions cachingOptions,
) )
.then((blocker) => { .then((blocker) => {
if (session) { if (session) {
blocker.enableBlockingInSession(session); blocker.enableBlockingInSession(session);
} else { } else {
console.log("Successfully generated adBlocker engine."); console.log('Successfully generated adBlocker engine.');
} }
}) })
.catch((err) => console.log("Error loading adBlocker engine", err)); .catch((error) => console.log('Error loading adBlocker engine', error));
}; };
module.exports = { loadAdBlockerEngine }; module.exports = { loadAdBlockerEngine };
if (require.main === module) { if (require.main === module) {
loadAdBlockerEngine(); // Generate the engine without enabling it loadAdBlockerEngine(); // Generate the engine without enabling it
} }

View File

@ -1,13 +1,13 @@
const { PluginConfig } = require("../../config/dynamic"); const { PluginConfig } = require('../../config/dynamic');
const config = new PluginConfig("adblocker", { enableFront: true }); const config = new PluginConfig('adblocker', { enableFront: true });
const blockers = { const blockers = {
WithBlocklists: "With blocklists", WithBlocklists: 'With blocklists',
InPlayer: "In player", InPlayer: 'In player',
}; };
const shouldUseBlocklists = async () => const shouldUseBlocklists = async () =>
(await config.get("blocker")) !== blockers.InPlayer; (await config.get('blocker')) !== blockers.InPlayer;
module.exports = { shouldUseBlocklists, blockers, ...config }; module.exports = { shouldUseBlocklists, blockers, ...config };

View File

@ -7,283 +7,426 @@
*/ */
{ {
let pruner = function (o) { const pruner = function (o) {
delete o.playerAds; delete o.playerAds;
delete o.adPlacements; delete o.adPlacements;
// //
if (o.playerResponse) { if (o.playerResponse) {
delete o.playerResponse.playerAds; delete o.playerResponse.playerAds;
delete o.playerResponse.adPlacements; delete o.playerResponse.adPlacements;
} }
//
return o;
};
JSON.parse = new Proxy(JSON.parse, { //
apply: function () { return o;
return pruner(Reflect.apply(...arguments)); };
},
});
Response.prototype.json = new Proxy(Response.prototype.json, { JSON.parse = new Proxy(JSON.parse, {
apply: function () { apply() {
return Reflect.apply(...arguments).then((o) => pruner(o)); return pruner(Reflect.apply(...arguments));
}, },
}); });
Response.prototype.json = new Proxy(Response.prototype.json, {
apply() {
return Reflect.apply(...arguments).then((o) => pruner(o));
},
});
} }
(function () { (function () {
let cValue = "undefined"; let cValue = 'undefined';
const chain = "playerResponse.adPlacements"; const chain = 'playerResponse.adPlacements';
const thisScript = document.currentScript; const thisScript = document.currentScript;
// //
if (cValue === "null") cValue = null; switch (cValue) {
else if (cValue === "''") cValue = ""; case 'null': {
else if (cValue === "true") cValue = true; cValue = null;
else if (cValue === "false") cValue = false; break;
else if (cValue === "undefined") cValue = undefined; }
else if (cValue === "noopFunc") cValue = function () {};
else if (cValue === "trueFunc")
cValue = function () {
return true;
};
else if (cValue === "falseFunc")
cValue = function () {
return false;
};
else if (/^\d+$/.test(cValue)) {
cValue = parseFloat(cValue);
//
if (isNaN(cValue)) return;
if (Math.abs(cValue) > 0x7fff) return;
} else {
return;
}
//
let aborted = false;
const mustAbort = function (v) {
if (aborted) return true;
aborted =
v !== undefined &&
v !== null &&
cValue !== undefined &&
cValue !== null &&
typeof v !== typeof cValue;
return aborted;
};
/* case "''": {
Support multiple trappers for the same property: cValue = '';
https://github.com/uBlockOrigin/uBlock-issues/issues/156 break;
}
case 'true': {
cValue = true;
break;
}
case 'false': {
cValue = false;
break;
}
case 'undefined': {
cValue = undefined;
break;
}
case 'noopFunc': {
cValue = function () {
};
break;
}
case 'trueFunc': {
cValue = function () {
return true;
};
break;
}
case 'falseFunc': {
cValue = function () {
return false;
};
break;
}
default: {
if (/^\d+$/.test(cValue)) {
cValue = Number.parseFloat(cValue);
//
if (isNaN(cValue)) {
return;
}
if (Math.abs(cValue) > 0x7F_FF) {
return;
}
} else {
return;
}
}
}
//
let aborted = false;
const mustAbort = function (v) {
if (aborted) {
return true;
}
aborted
= v !== undefined
&& v !== null
&& cValue !== undefined
&& cValue !== null
&& typeof v !== typeof cValue;
return aborted;
};
/*
Support multiple trappers for the same property:
https://github.com/uBlockOrigin/uBlock-issues/issues/156
*/ */
const trapProp = function (owner, prop, configurable, handler) { const trapProp = function (owner, prop, configurable, handler) {
if (handler.init(owner[prop]) === false) { if (handler.init(owner[prop]) === false) {
return; return;
} }
//
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter;
if (odesc instanceof Object) {
if (odesc.configurable === false) return;
if (odesc.get instanceof Function) prevGetter = odesc.get;
if (odesc.set instanceof Function) prevSetter = odesc.set;
}
//
Object.defineProperty(owner, prop, {
configurable,
get() {
if (prevGetter !== undefined) {
prevGetter();
}
//
return handler.getter();
},
set(a) {
if (prevSetter !== undefined) {
prevSetter(a);
}
//
handler.setter(a);
},
});
};
const trapChain = function (owner, chain) { //
const pos = chain.indexOf("."); const odesc = Object.getOwnPropertyDescriptor(owner, prop);
if (pos === -1) { let previousGetter;
trapProp(owner, chain, false, { let previousSetter;
v: undefined, if (odesc instanceof Object) {
getter: function () { if (odesc.configurable === false) {
return document.currentScript === thisScript ? this.v : cValue; return;
}, }
setter: function (a) {
if (mustAbort(a) === false) return; if (odesc.get instanceof Function) {
cValue = a; previousGetter = odesc.get;
}, }
init: function (v) {
if (mustAbort(v)) return false; if (odesc.set instanceof Function) {
// previousSetter = odesc.set;
this.v = v; }
return true; }
},
}); //
// Object.defineProperty(owner, prop, {
return; configurable,
} get() {
// if (previousGetter !== undefined) {
const prop = chain.slice(0, pos); previousGetter();
const v = owner[prop]; }
//
chain = chain.slice(pos + 1); //
if (v instanceof Object || (typeof v === "object" && v !== null)) { return handler.getter();
trapChain(v, chain); },
return; set(a) {
} if (previousSetter !== undefined) {
// previousSetter(a);
trapProp(owner, prop, true, { }
v: undefined,
getter: function () { //
return this.v; handler.setter(a);
}, },
setter: function (a) { });
this.v = a; };
if (a instanceof Object) trapChain(a, chain);
}, const trapChain = function (owner, chain) {
init: function (v) { const pos = chain.indexOf('.');
this.v = v; if (pos === -1) {
return true; trapProp(owner, chain, false, {
}, v: undefined,
}); getter() {
}; return document.currentScript === thisScript ? this.v : cValue;
// },
trapChain(window, chain); setter(a) {
if (mustAbort(a) === false) {
return;
}
cValue = a;
},
init(v) {
if (mustAbort(v)) {
return false;
}
//
this.v = v;
return true;
},
});
//
return;
}
//
const prop = chain.slice(0, pos);
const v = owner[prop];
//
chain = chain.slice(pos + 1);
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
trapChain(v, chain);
return;
}
//
trapProp(owner, prop, true, {
v: undefined,
getter() {
return this.v;
},
setter(a) {
this.v = a;
if (a instanceof Object) {
trapChain(a, chain);
}
},
init(v) {
this.v = v;
return true;
},
});
};
//
trapChain(window, chain);
})(); })();
(function () { (function () {
let cValue = "undefined"; let cValue = 'undefined';
const thisScript = document.currentScript; const thisScript = document.currentScript;
const chain = "ytInitialPlayerResponse.adPlacements"; const chain = 'ytInitialPlayerResponse.adPlacements';
// //
if (cValue === "null") cValue = null; switch (cValue) {
else if (cValue === "''") cValue = ""; case 'null': {
else if (cValue === "true") cValue = true; cValue = null;
else if (cValue === "false") cValue = false; break;
else if (cValue === "undefined") cValue = undefined; }
else if (cValue === "noopFunc") cValue = function () {};
else if (cValue === "trueFunc")
cValue = function () {
return true;
};
else if (cValue === "falseFunc")
cValue = function () {
return false;
};
else if (/^\d+$/.test(cValue)) {
cValue = parseFloat(cValue);
//
if (isNaN(cValue)) return;
if (Math.abs(cValue) > 0x7fff) return;
} else {
return;
}
//
let aborted = false;
const mustAbort = function (v) {
if (aborted) return true;
aborted =
v !== undefined &&
v !== null &&
cValue !== undefined &&
cValue !== null &&
typeof v !== typeof cValue;
return aborted;
};
/* case "''": {
Support multiple trappers for the same property: cValue = '';
https://github.com/uBlockOrigin/uBlock-issues/issues/156 break;
}
case 'true': {
cValue = true;
break;
}
case 'false': {
cValue = false;
break;
}
case 'undefined': {
cValue = undefined;
break;
}
case 'noopFunc': {
cValue = function () {
};
break;
}
case 'trueFunc': {
cValue = function () {
return true;
};
break;
}
case 'falseFunc': {
cValue = function () {
return false;
};
break;
}
default: {
if (/^\d+$/.test(cValue)) {
cValue = Number.parseFloat(cValue);
//
if (isNaN(cValue)) {
return;
}
if (Math.abs(cValue) > 0x7F_FF) {
return;
}
} else {
return;
}
}
}
//
let aborted = false;
const mustAbort = function (v) {
if (aborted) {
return true;
}
aborted
= v !== undefined
&& v !== null
&& cValue !== undefined
&& cValue !== null
&& typeof v !== typeof cValue;
return aborted;
};
/*
Support multiple trappers for the same property:
https://github.com/uBlockOrigin/uBlock-issues/issues/156
*/ */
const trapProp = function (owner, prop, configurable, handler) { const trapProp = function (owner, prop, configurable, handler) {
if (handler.init(owner[prop]) === false) { if (handler.init(owner[prop]) === false) {
return; return;
} }
//
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter;
if (odesc instanceof Object) {
if (odesc.configurable === false) return;
if (odesc.get instanceof Function) prevGetter = odesc.get;
if (odesc.set instanceof Function) prevSetter = odesc.set;
}
//
Object.defineProperty(owner, prop, {
configurable,
get() {
if (prevGetter !== undefined) {
prevGetter();
}
//
return handler.getter();
},
set(a) {
if (prevSetter !== undefined) {
prevSetter(a);
}
//
handler.setter(a);
},
});
};
const trapChain = function (owner, chain) { //
const pos = chain.indexOf("."); const odesc = Object.getOwnPropertyDescriptor(owner, prop);
if (pos === -1) { let previousGetter;
trapProp(owner, chain, false, { let previousSetter;
v: undefined, if (odesc instanceof Object) {
getter: function () { if (odesc.configurable === false) {
return document.currentScript === thisScript ? this.v : cValue; return;
}, }
setter: function (a) {
if (mustAbort(a) === false) return; if (odesc.get instanceof Function) {
cValue = a; previousGetter = odesc.get;
}, }
init: function (v) {
if (mustAbort(v)) return false; if (odesc.set instanceof Function) {
// previousSetter = odesc.set;
this.v = v; }
return true; }
},
}); //
// Object.defineProperty(owner, prop, {
return; configurable,
} get() {
// if (previousGetter !== undefined) {
const prop = chain.slice(0, pos); previousGetter();
const v = owner[prop]; }
//
chain = chain.slice(pos + 1); //
if (v instanceof Object || (typeof v === "object" && v !== null)) { return handler.getter();
trapChain(v, chain); },
return; set(a) {
} if (previousSetter !== undefined) {
// previousSetter(a);
trapProp(owner, prop, true, { }
v: undefined,
getter: function () { //
return this.v; handler.setter(a);
}, },
setter: function (a) { });
this.v = a; };
if (a instanceof Object) trapChain(a, chain);
}, const trapChain = function (owner, chain) {
init: function (v) { const pos = chain.indexOf('.');
this.v = v; if (pos === -1) {
return true; trapProp(owner, chain, false, {
}, v: undefined,
}); getter() {
}; return document.currentScript === thisScript ? this.v : cValue;
// },
trapChain(window, chain); setter(a) {
if (mustAbort(a) === false) {
return;
}
cValue = a;
},
init(v) {
if (mustAbort(v)) {
return false;
}
//
this.v = v;
return true;
},
});
//
return;
}
//
const prop = chain.slice(0, pos);
const v = owner[prop];
//
chain = chain.slice(pos + 1);
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
trapChain(v, chain);
return;
}
//
trapProp(owner, prop, true, {
v: undefined,
getter() {
return this.v;
},
setter(a) {
this.v = a;
if (a instanceof Object) {
trapChain(a, chain);
}
},
init(v) {
this.v = v;
return true;
},
});
};
//
trapChain(window, chain);
})(); })();

View File

@ -1,15 +1,15 @@
const config = require("./config"); const config = require('./config');
module.exports = () => [ module.exports = () => [
{ {
label: "Blocker", label: 'Blocker',
submenu: Object.values(config.blockers).map((blocker) => ({ submenu: Object.values(config.blockers).map((blocker) => ({
label: blocker, label: blocker,
type: "radio", type: 'radio',
checked: (config.get("blocker") || config.blockers.WithBlocklists) === blocker, checked: (config.get('blocker') || config.blockers.WithBlocklists) === blocker,
click: () => { click() {
config.set("blocker", blocker); config.set('blocker', blocker);
}, },
})), })),
}, },
]; ];

View File

@ -1,10 +1,10 @@
const config = require("./config"); const config = require('./config');
module.exports = async () => { module.exports = async () => {
if (await config.shouldUseBlocklists()) { if (await config.shouldUseBlocklists()) {
// Preload adblocker to inject scripts/styles // Preload adblocker to inject scripts/styles
require("@cliqz/adblocker-electron-preload"); require('@cliqz/adblocker-electron-preload');
} else if ((await config.get("blocker")) === config.blockers.InPlayer) { } else if ((await config.get('blocker')) === config.blockers.InPlayer) {
require("./inject"); require('./inject');
} }
}; };

View File

@ -1,19 +1,19 @@
const applyCompressor = (e) => { const applyCompressor = (e) => {
const audioContext = e.detail.audioContext; const { audioContext } = e.detail;
const compressor = audioContext.createDynamicsCompressor(); const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50; compressor.threshold.value = -50;
compressor.ratio.value = 12; compressor.ratio.value = 12;
compressor.knee.value = 40; compressor.knee.value = 40;
compressor.attack.value = 0; compressor.attack.value = 0;
compressor.release.value = 0.25; compressor.release.value = 0.25;
e.detail.audioSource.connect(compressor); e.detail.audioSource.connect(compressor);
compressor.connect(audioContext.destination); compressor.connect(audioContext.destination);
}; };
module.exports = () => module.exports = () =>
document.addEventListener("audioCanPlay", applyCompressor, { document.addEventListener('audioCanPlay', applyCompressor, {
once: true, // Only create the audio compressor once, not on each video once: true, // Only create the audio compressor once, not on each video
passive: true, passive: true,
}); });

View File

@ -1,6 +1,7 @@
const path = require("path"); const path = require('node:path');
const { injectCSS } = require("../utils");
module.exports = win => { const { injectCSS } = require('../utils');
injectCSS(win.webContents, path.join(__dirname, "style.css"));
module.exports = (win) => {
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
}; };

View File

@ -1,10 +1,10 @@
#nav-bar-background, #nav-bar-background,
#header.ytmusic-item-section-renderer, #header.ytmusic-item-section-renderer,
ytmusic-tabs { ytmusic-tabs {
background: rgba(0, 0, 0, 0.3) !important; background: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(8px) !important; backdrop-filter: blur(8px) !important;
} }
#nav-bar-divider { #nav-bar-divider {
display: none !important; display: none !important;
} }

View File

@ -1,4 +1,4 @@
module.exports = () => { module.exports = () => {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
require("simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js"); require('simple-youtube-age-restriction-bypass/dist/Simple-YouTube-Age-Restriction-Bypass.user.js');
}; };

View File

@ -1,21 +1,19 @@
const { ipcMain } = require("electron"); const { ipcMain } = require('electron');
const prompt = require('custom-electron-prompt');
const prompt = require("custom-electron-prompt"); const promptOptions = require('../../providers/prompt-options');
const promptOptions = require("../../providers/prompt-options");
module.exports = (win) => { module.exports = (win) => {
ipcMain.handle("captionsSelector", async (_, captionLabels, currentIndex) => { ipcMain.handle('captionsSelector', async (_, captionLabels, currentIndex) => await prompt(
return await prompt( {
{ title: 'Choose Caption',
title: "Choose Caption", label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
label: `Current Caption: ${captionLabels[currentIndex] || "None"}`, type: 'select',
type: "select", value: currentIndex,
value: currentIndex, selectOptions: captionLabels,
selectOptions: captionLabels, resizable: true,
resizable: true, ...promptOptions(),
...promptOptions(), },
}, win,
win ));
);
});
}; };

View File

@ -1,3 +1,4 @@
const { PluginConfig } = require("../../config/dynamic"); const { PluginConfig } = require('../../config/dynamic');
const config = new PluginConfig("captions-selector", { enableFront: true });
const config = new PluginConfig('captions-selector', { enableFront: true });
module.exports = { ...config }; module.exports = { ...config };

View File

@ -1,77 +1,83 @@
const { ElementFromFile, templatePath } = require("../utils"); const { ipcRenderer } = require('electron');
const { ipcRenderer } = require("electron");
const configProvider = require('./config');
const { ElementFromFile, templatePath } = require('../utils');
const configProvider = require("./config");
let config; let config;
function $(selector) { return document.querySelector(selector); } function $(selector) {
return document.querySelector(selector);
}
const captionsSettingsButton = ElementFromFile( const captionsSettingsButton = ElementFromFile(
templatePath(__dirname, "captions-settings-template.html") templatePath(__dirname, 'captions-settings-template.html'),
); );
module.exports = async () => { module.exports = async () => {
config = await configProvider.getAll(); config = await configProvider.getAll();
configProvider.subscribeAll((newConfig) => { configProvider.subscribeAll((newConfig) => {
config = newConfig; config = newConfig;
}); });
document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true }); document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true });
} };
function setup(api) { function setup(api) {
$(".right-controls-buttons").append(captionsSettingsButton); $('.right-controls-buttons').append(captionsSettingsButton);
let captionTrackList = api.getOption("captions", "tracklist"); let captionTrackList = api.getOption('captions', 'tracklist');
$("video").addEventListener("srcChanged", async () => { $('video').addEventListener('srcChanged', async () => {
if (config.disableCaptions) { if (config.disableCaptions) {
setTimeout(() => api.unloadModule("captions"), 100); setTimeout(() => api.unloadModule('captions'), 100);
captionsSettingsButton.style.display = "none"; captionsSettingsButton.style.display = 'none';
return; return;
}
api.loadModule("captions");
setTimeout(async () => {
captionTrackList = api.getOption("captions", "tracklist");
if (config.autoload && config.lastCaptionsCode) {
api.setOption("captions", "track", {
languageCode: config.lastCaptionsCode,
});
}
captionsSettingsButton.style.display = captionTrackList?.length
? "inline-block"
: "none";
}, 250);
});
captionsSettingsButton.onclick = async () => {
if (captionTrackList?.length) {
const currentCaptionTrack = api.getOption("captions", "track");
let currentIndex = !currentCaptionTrack ?
null :
captionTrackList.indexOf(captionTrackList.find(track => track.languageCode === currentCaptionTrack.languageCode));
const captionLabels = [
...captionTrackList.map(track => track.displayName),
'None'
];
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex)
if (currentIndex === null) return;
const newCaptions = captionTrackList[currentIndex];
configProvider.set('lastCaptionsCode', newCaptions?.languageCode);
if (newCaptions) {
api.setOption("captions", "track", { languageCode: newCaptions.languageCode });
} else {
api.setOption("captions", "track", {});
}
setTimeout(() => api.playVideo());
}
} }
api.loadModule('captions');
setTimeout(async () => {
captionTrackList = api.getOption('captions', 'tracklist');
if (config.autoload && config.lastCaptionsCode) {
api.setOption('captions', 'track', {
languageCode: config.lastCaptionsCode,
});
}
captionsSettingsButton.style.display = captionTrackList?.length
? 'inline-block'
: 'none';
}, 250);
});
captionsSettingsButton.addEventListener('click', async () => {
if (captionTrackList?.length) {
const currentCaptionTrack = api.getOption('captions', 'track');
let currentIndex = currentCaptionTrack
? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode))
: null;
const captionLabels = [
...captionTrackList.map((track) => track.displayName),
'None',
];
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex);
if (currentIndex === null) {
return;
}
const newCaptions = captionTrackList[currentIndex];
configProvider.set('lastCaptionsCode', newCaptions?.languageCode);
if (newCaptions) {
api.setOption('captions', 'track', { languageCode: newCaptions.languageCode });
} else {
api.setOption('captions', 'track', {});
}
setTimeout(() => api.playVideo());
}
});
} }

View File

@ -1,20 +1,20 @@
const config = require("./config"); const config = require('./config');
module.exports = () => [ module.exports = () => [
{ {
label: "Automatically select last used caption", label: 'Automatically select last used caption',
type: "checkbox", type: 'checkbox',
checked: config.get("autoload"), checked: config.get('autoload'),
click: (item) => { click(item) {
config.set('autoload', item.checked); config.set('autoload', item.checked);
}
}, },
{ },
label: "No captions by default", {
type: "checkbox", label: 'No captions by default',
checked: config.get("disabledCaptions"), type: 'checkbox',
click: (item) => { checked: config.get('disabledCaptions'),
config.set('disableCaptions', item.checked); click(item) {
}, config.set('disableCaptions', item.checked);
} },
},
]; ];

View File

@ -1,13 +1,17 @@
<tp-yt-paper-icon-button class="player-captions-button style-scope ytmusic-player" icon="yt-icons:subtitles" <tp-yt-paper-icon-button aria-disabled="false" aria-label="Open captions selector"
title="Open captions selector" aria-label="Open captions selector" role="button" tabindex="0" aria-disabled="false"> class="player-captions-button style-scope ytmusic-player" icon="yt-icons:subtitles"
<tp-yt-iron-icon id="icon" class="style-scope tp-yt-paper-icon-button"><svg viewBox="0 0 24 24" role="button" tabindex="0"
preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" title="Open captions selector">
style="pointer-events: none; display: block; width: 100%; height: 100%;"> <tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
<g class="style-scope yt-icon"> <svg class="style-scope yt-icon"
<path focusable="false" preserveAspectRatio="xMidYMid meet"
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z" style="pointer-events: none; display: block; width: 100%; height: 100%;"
class="style-scope tp-yt-iron-icon"></path> viewBox="0 0 24 24">
</g> <g class="style-scope yt-icon">
</svg> <path
</tp-yt-iron-icon> class="style-scope tp-yt-iron-icon"
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"></path>
</g>
</svg>
</tp-yt-iron-icon>
</tp-yt-paper-icon-button> </tp-yt-paper-icon-button>

View File

@ -1,10 +1,10 @@
module.exports = () => { module.exports = () => {
const compactSidebar = document.querySelector("#mini-guide"); const compactSidebar = document.querySelector('#mini-guide');
const isCompactSidebarDisabled = const isCompactSidebarDisabled
compactSidebar === null || = compactSidebar === null
window.getComputedStyle(compactSidebar).display === "none"; || window.getComputedStyle(compactSidebar).display === 'none';
if (isCompactSidebarDisabled) { if (isCompactSidebarDisabled) {
document.querySelector("#button").click(); document.querySelector('#button').click();
} }
}; };

View File

@ -1,15 +1,15 @@
const { ipcMain } = require("electron"); const { ipcMain } = require('electron');
const { Innertube } = require("youtubei.js"); const { Innertube } = require('youtubei.js');
require("./config"); require('./config');
module.exports = async () => { module.exports = async () => {
const yt = await Innertube.create(); const yt = await Innertube.create();
ipcMain.handle("audio-url", async (_, videoID) => { ipcMain.handle('audio-url', async (_, videoID) => {
const info = await yt.getBasicInfo(videoID); const info = await yt.getBasicInfo(videoID);
const url = info.streaming_data?.formats[0].decipher(yt.session.player); const url = info.streaming_data?.formats[0].decipher(yt.session.player);
return url; return url;
}); });
}; };

View File

@ -1,3 +1,4 @@
const { PluginConfig } = require("../../config/dynamic"); const { PluginConfig } = require('../../config/dynamic');
const config = new PluginConfig("crossfade", { enableFront: true });
const config = new PluginConfig('crossfade', { enableFront: true });
module.exports = { ...config }; module.exports = { ...config };

View File

@ -16,345 +16,346 @@
*/ */
(function (root) { (function (root) {
"use strict"; 'use strict';
// internal utility: check if value is a valid volume level and throw if not // Internal utility: check if value is a valid volume level and throw if not
let validateVolumeLevel = (value) => { const validateVolumeLevel = (value) => {
// number between 0 and 1? // Number between 0 and 1?
if (!Number.isNaN(value) && value >= 0 && value <= 1) { if (!Number.isNaN(value) && value >= 0 && value <= 1) {
// yup, that's fine // Yup, that's fine
return;
} else {
// abort and throw an exception
throw new TypeError("Number between 0 and 1 expected as volume!");
}
};
// main class } else {
class VolumeFader { // Abort and throw an exception
/** throw new TypeError('Number between 0 and 1 expected as volume!');
* VolumeFader Constructor }
* };
* @param media {HTMLMediaElement} - audio or video element to be controlled
* @param options {Object} - an object with optional settings
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
*
* options:
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
*/
constructor(media, options) {
// passed media element of correct type?
if (media instanceof HTMLMediaElement) {
// save reference to media element
this.media = media;
} else {
// abort and throw an exception
throw new TypeError("Media element expected!");
}
// make sure options is an object // Main class
options = options || {}; class VolumeFader {
/**
* VolumeFader Constructor
*
* @param media {HTMLMediaElement} - audio or video element to be controlled
* @param options {Object} - an object with optional settings
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
*
* options:
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
*/
constructor(media, options) {
// Passed media element of correct type?
if (media instanceof HTMLMediaElement) {
// Save reference to media element
this.media = media;
} else {
// Abort and throw an exception
throw new TypeError('Media element expected!');
}
// log function passed? // Make sure options is an object
if (typeof options.logger == "function") { options = options || {};
// set log function to the one specified
this.logger = options.logger;
} else {
// set log function explicitly to false
this.logger = false;
}
// linear volume fading? // Log function passed?
if (options.fadeScaling == "linear") { if (typeof options.logger === 'function') {
// pass levels unchanged // Set log function to the one specified
this.scale = { this.logger = options.logger;
internalToVolume: (level) => level, } else {
volumeToInternal: (level) => level, // Set log function explicitly to false
}; this.logger = false;
}
// log setting // Linear volume fading?
this.logger && this.logger("Using linear fading."); if (options.fadeScaling == 'linear') {
} // Pass levels unchanged
// no linear, but logarithmic fading… this.scale = {
else { internalToVolume: (level) => level,
let dynamicRange; volumeToInternal: (level) => level,
};
// default dynamic range? // Log setting
if ( this.logger && this.logger('Using linear fading.');
options.fadeScaling === undefined || }
options.fadeScaling == "logarithmic" // No linear, but logarithmic fading…
) { else {
// set default of 60 dB let dynamicRange;
dynamicRange = 3;
}
// custom dynamic range?
else if (
!Number.isNaN(options.fadeScaling) &&
options.fadeScaling > 0
) {
// turn amplitude dB into a multiple of 10 power dB
dynamicRange = options.fadeScaling / 2 / 10;
}
// unsupported value
else {
// abort and throw exception
throw new TypeError(
"Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!"
);
}
// use exponential/logarithmic scaler for expansion/compression // Default dynamic range?
this.scale = { if (
internalToVolume: (level) => options.fadeScaling === undefined
this.exponentialScaler(level, dynamicRange), || options.fadeScaling == 'logarithmic'
volumeToInternal: (level) => ) {
this.logarithmicScaler(level, dynamicRange), // Set default of 60 dB
}; dynamicRange = 3;
}
// Custom dynamic range?
else if (
!Number.isNaN(options.fadeScaling)
&& options.fadeScaling > 0
) {
// Turn amplitude dB into a multiple of 10 power dB
dynamicRange = options.fadeScaling / 2 / 10;
}
// Unsupported value
else {
// Abort and throw exception
throw new TypeError(
"Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!",
);
}
// log setting if not default // Use exponential/logarithmic scaler for expansion/compression
options.fadeScaling && this.scale = {
this.logger && internalToVolume: (level) =>
this.logger( this.exponentialScaler(level, dynamicRange),
"Using logarithmic fading with " + volumeToInternal: (level) =>
String(10 * dynamicRange) + this.logarithmicScaler(level, dynamicRange),
" dB dynamic range." };
);
}
// set initial volume? // Log setting if not default
if (options.initialVolume !== undefined) { options.fadeScaling
// validate volume level and throw if invalid && this.logger
validateVolumeLevel(options.initialVolume); && this.logger(
'Using logarithmic fading with '
+ String(10 * dynamicRange)
+ ' dB dynamic range.',
);
}
// set initial volume // Set initial volume?
this.media.volume = options.initialVolume; if (options.initialVolume !== undefined) {
// Validate volume level and throw if invalid
validateVolumeLevel(options.initialVolume);
// log setting // Set initial volume
this.logger && this.media.volume = options.initialVolume;
this.logger(
"Set initial volume to " + String(this.media.volume) + "."
);
}
// fade duration given? // Log setting
if (options.fadeDuration !== undefined) { this.logger
// try to set given fade duration (will log if successful and throw if not) && this.logger(
this.setFadeDuration(options.fadeDuration); 'Set initial volume to ' + String(this.media.volume) + '.',
} else { );
// set default fade duration (1000 ms) }
this.fadeDuration = 1000;
}
// indicate that fader is not active yet // Fade duration given?
this.active = false; if (options.fadeDuration === undefined) {
// Set default fade duration (1000 ms)
this.fadeDuration = 1000;
} else {
// Try to set given fade duration (will log if successful and throw if not)
this.setFadeDuration(options.fadeDuration);
}
// initialization done // Indicate that fader is not active yet
this.logger && this.logger("Initialized for", this.media); this.active = false;
}
/** // Initialization done
* Re(start) the update cycle. this.logger && this.logger('Initialized for', this.media);
* (this.active must be truthy for volume updates to take effect) }
*
* @return {Object} VolumeFader instance for chaining
*/
start() {
// set fader to be active
this.active = true;
// start by running the update method /**
this.updateVolume(); * Re(start) the update cycle.
* (this.active must be truthy for volume updates to take effect)
*
* @return {Object} VolumeFader instance for chaining
*/
start() {
// Set fader to be active
this.active = true;
// return instance for chaining // Start by running the update method
return this; this.updateVolume();
}
/** // Return instance for chaining
* Stop the update cycle. return this;
* (interrupting any fade) }
*
* @return {Object} VolumeFader instance for chaining
*/
stop() {
// set fader to be inactive
this.active = false;
// return instance for chaining /**
return this; * Stop the update cycle.
} * (interrupting any fade)
*
* @return {Object} VolumeFader instance for chaining
*/
stop() {
// Set fader to be inactive
this.active = false;
/** // Return instance for chaining
* Set fade duration. return this;
* (used for future calls to fadeTo) }
*
* @param {Number} fadeDuration - fading length in milliseconds
* @throws {TypeError} if fadeDuration is not a number greater than zero
* @return {Object} VolumeFader instance for chaining
*/
setFadeDuration(fadeDuration) {
// if duration is a valid number > 0…
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
// set fade duration
this.fadeDuration = fadeDuration;
// log setting /**
this.logger && * Set fade duration.
this.logger("Set fade duration to " + String(fadeDuration) + " ms."); * (used for future calls to fadeTo)
} else { *
// abort and throw an exception * @param {Number} fadeDuration - fading length in milliseconds
throw new TypeError("Positive number expected as fade duration!"); * @throws {TypeError} if fadeDuration is not a number greater than zero
} * @return {Object} VolumeFader instance for chaining
*/
setFadeDuration(fadeDuration) {
// If duration is a valid number > 0…
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
// Set fade duration
this.fadeDuration = fadeDuration;
// return instance for chaining // Log setting
return this; this.logger
} && this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
} else {
// Abort and throw an exception
throw new TypeError('Positive number expected as fade duration!');
}
/** // Return instance for chaining
* Define a new fade and start fading. return this;
* }
* @param {Number} targetVolume - level to fade to in the range 0…1
* @param {Function} callback - (optional) function to be called when fade is complete
* @throws {TypeError} if targetVolume is not in the range 0…1
* @return {Object} VolumeFader instance for chaining
*/
fadeTo(targetVolume, callback) {
// validate volume and throw if invalid
validateVolumeLevel(targetVolume);
// define new fade /**
this.fade = { * Define a new fade and start fading.
// volume start and end point on internal fading scale *
volume: { * @param {Number} targetVolume - level to fade to in the range 0…1
start: this.scale.volumeToInternal(this.media.volume), * @param {Function} callback - (optional) function to be called when fade is complete
end: this.scale.volumeToInternal(targetVolume), * @throws {TypeError} if targetVolume is not in the range 0…1
}, * @return {Object} VolumeFader instance for chaining
// time start and end point */
time: { fadeTo(targetVolume, callback) {
start: Date.now(), // Validate volume and throw if invalid
end: Date.now() + this.fadeDuration, validateVolumeLevel(targetVolume);
},
// optional callback function
callback: callback,
};
// start fading // Define new fade
this.start(); this.fade = {
// Volume start and end point on internal fading scale
volume: {
start: this.scale.volumeToInternal(this.media.volume),
end: this.scale.volumeToInternal(targetVolume),
},
// Time start and end point
time: {
start: Date.now(),
end: Date.now() + this.fadeDuration,
},
// Optional callback function
callback,
};
// log new fade // Start fading
this.logger && this.logger("New fade started:", this.fade); this.start();
// return instance for chaining // Log new fade
return this; this.logger && this.logger('New fade started:', this.fade);
}
// convenience shorthand methods for common fades // Return instance for chaining
fadeIn(callback) { return this;
this.fadeTo(1, callback); }
}
fadeOut(callback) {
this.fadeTo(0, callback);
}
/** // Convenience shorthand methods for common fades
* Internal: Update media volume. fadeIn(callback) {
* (calls itself through requestAnimationFrame) this.fadeTo(1, callback);
* }
* @param {Number} targetVolume - linear level to fade to (0…1)
* @param {Function} callback - (optional) function to be called when fade is complete
*/
updateVolume() {
// fader active and fade available to process?
if (this.active && this.fade) {
// get current time
let now = Date.now();
// time left for fading? fadeOut(callback) {
if (now < this.fade.time.end) { this.fadeTo(0, callback);
// compute current fade progress }
let progress =
(now - this.fade.time.start) /
(this.fade.time.end - this.fade.time.start);
// compute current level on internal scale /**
let level = * Internal: Update media volume.
progress * (this.fade.volume.end - this.fade.volume.start) + * (calls itself through requestAnimationFrame)
this.fade.volume.start; *
* @param {Number} targetVolume - linear level to fade to (0…1)
* @param {Function} callback - (optional) function to be called when fade is complete
*/
updateVolume() {
// Fader active and fade available to process?
if (this.active && this.fade) {
// Get current time
const now = Date.now();
// map fade level to volume level and apply it to media element // Time left for fading?
this.media.volume = this.scale.internalToVolume(level); if (now < this.fade.time.end) {
// Compute current fade progress
const progress
= (now - this.fade.time.start)
/ (this.fade.time.end - this.fade.time.start);
// schedule next update // Compute current level on internal scale
root.requestAnimationFrame(this.updateVolume.bind(this)); const level
} else { = progress * (this.fade.volume.end - this.fade.volume.start)
// log end of fade + this.fade.volume.start;
this.logger &&
this.logger(
"Fade to " + String(this.fade.volume.end) + " complete."
);
// time is up, jump to target volume // Map fade level to volume level and apply it to media element
this.media.volume = this.scale.internalToVolume(this.fade.volume.end); this.media.volume = this.scale.internalToVolume(level);
// set fader to be inactive // Schedule next update
this.active = false; root.requestAnimationFrame(this.updateVolume.bind(this));
} else {
// Log end of fade
this.logger
&& this.logger(
'Fade to ' + String(this.fade.volume.end) + ' complete.',
);
// done, call back (if callable) // Time is up, jump to target volume
typeof this.fade.callback == "function" && this.fade.callback(); this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
// clear fade // Set fader to be inactive
this.fade = undefined; this.active = false;
}
}
}
/** // Done, call back (if callable)
* Internal: Exponential scaler with dynamic range limit. typeof this.fade.callback === 'function' && this.fade.callback();
*
* @param {Number} input - logarithmic input level to be expanded (float, 0…1)
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
* @return {Number} - expanded level (float, 0…1)
*/
exponentialScaler(input, dynamicRange) {
// special case: make zero (or any falsy input) return zero
if (input == 0) {
// since the dynamic range is limited,
// allow a zero to produce a plain zero instead of a small faction
// (audio would not be recognized as silent otherwise)
return 0;
} else {
// scale 0…1 to minus something × 10 dB
input = (input - 1) * dynamicRange;
// compute power of 10 // Clear fade
return Math.pow(10, input); this.fade = undefined;
} }
} }
}
/** /**
* Internal: Logarithmic scaler with dynamic range limit. * Internal: Exponential scaler with dynamic range limit.
* *
* @param {Number} input - exponential input level to be compressed (float, 0…1) * @param {Number} input - logarithmic input level to be expanded (float, 0…1)
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞) * @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
* @return {Number} - compressed level (float, 0…1) * @return {Number} - expanded level (float, 0…1)
*/ */
logarithmicScaler(input, dynamicRange) { exponentialScaler(input, dynamicRange) {
// special case: make zero (or any falsy input) return zero // Special case: make zero (or any falsy input) return zero
if (input == 0) { if (input == 0) {
// logarithm of zero would be -∞, which would map to zero anyway // Since the dynamic range is limited,
return 0; // allow a zero to produce a plain zero instead of a small faction
} else { // (audio would not be recognized as silent otherwise)
// compute base-10 logarithm return 0;
input = Math.log10(input); }
// scale minus something × 10 dB to 0…1 (clipping at 0) // Scale 0…1 to minus something × 10 dB
return Math.max(1 + input / dynamicRange, 0); input = (input - 1) * dynamicRange;
}
}
}
// export class to root scope // Compute power of 10
root.VolumeFader = VolumeFader; return 10 ** input;
}
/**
* Internal: Logarithmic scaler with dynamic range limit.
*
* @param {Number} input - exponential input level to be compressed (float, 0…1)
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
* @return {Number} - compressed level (float, 0…1)
*/
logarithmicScaler(input, dynamicRange) {
// Special case: make zero (or any falsy input) return zero
if (input == 0) {
// Logarithm of zero would be -∞, which would map to zero anyway
return 0;
}
// Compute base-10 logarithm
input = Math.log10(input);
// Scale minus something × 10 dB to 0…1 (clipping at 0)
return Math.max(1 + input / dynamicRange, 0);
}
}
// Export class to root scope
root.VolumeFader = VolumeFader;
})(window); })(window);

View File

@ -1,158 +1,160 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const { Howl } = require("howler"); const { Howl } = require('howler');
// Extracted from https://github.com/bitfasching/VolumeFader // Extracted from https://github.com/bitfasching/VolumeFader
require("./fader"); require('./fader');
let transitionAudio; // Howler audio used to fade out the current music let transitionAudio; // Howler audio used to fade out the current music
let firstVideo = true; let firstVideo = true;
let waitForTransition; let waitForTransition;
const defaultConfig = require("../../config/defaults").plugins.crossfade; const defaultConfig = require('../../config/defaults').plugins.crossfade;
const configProvider = require('./config');
const configProvider = require("./config");
let config; let config;
const configGetNum = (key) => Number(config[key]) || defaultConfig[key]; const configGetNumber = (key) => Number(config[key]) || defaultConfig[key];
const getStreamURL = async (videoID) => { const getStreamURL = async (videoID) => {
const url = await ipcRenderer.invoke("audio-url", videoID); const url = await ipcRenderer.invoke('audio-url', videoID);
return url; return url;
}; };
const getVideoIDFromURL = (url) => { const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
return new URLSearchParams(url.split("?")?.at(-1)).get("v");
};
const isReadyToCrossfade = () => { const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded';
return transitionAudio && transitionAudio.state() === "loaded";
};
const watchVideoIDChanges = (cb) => { const watchVideoIDChanges = (cb) => {
navigation.addEventListener("navigate", (event) => { navigation.addEventListener('navigate', (event) => {
const currentVideoID = getVideoIDFromURL( const currentVideoID = getVideoIDFromURL(
event.currentTarget.currentEntry.url, event.currentTarget.currentEntry.url,
); );
const nextVideoID = getVideoIDFromURL(event.destination.url); const nextVideoID = getVideoIDFromURL(event.destination.url);
if ( if (
nextVideoID && nextVideoID
currentVideoID && && currentVideoID
(firstVideo || nextVideoID !== currentVideoID) && (firstVideo || nextVideoID !== currentVideoID)
) { ) {
if (isReadyToCrossfade()) { if (isReadyToCrossfade()) {
crossfade(() => { crossfade(() => {
cb(nextVideoID); cb(nextVideoID);
}); });
} else { } else {
cb(nextVideoID); cb(nextVideoID);
firstVideo = false; firstVideo = false;
} }
} }
}); });
}; };
const createAudioForCrossfade = async (url) => { const createAudioForCrossfade = async (url) => {
if (transitionAudio) { if (transitionAudio) {
transitionAudio.unload(); transitionAudio.unload();
} }
transitionAudio = new Howl({
src: url, transitionAudio = new Howl({
html5: true, src: url,
volume: 0, html5: true,
}); volume: 0,
await syncVideoWithTransitionAudio(); });
await syncVideoWithTransitionAudio();
}; };
const syncVideoWithTransitionAudio = async () => { const syncVideoWithTransitionAudio = async () => {
const video = document.querySelector("video"); const video = document.querySelector('video');
const videoFader = new VolumeFader(video, { const videoFader = new VolumeFader(video, {
fadeScaling: configGetNum("fadeScaling"), fadeScaling: configGetNumber('fadeScaling'),
fadeDuration: configGetNum("fadeInDuration"), fadeDuration: configGetNumber('fadeInDuration'),
}); });
await transitionAudio.play(); await transitionAudio.play();
await transitionAudio.seek(video.currentTime); await transitionAudio.seek(video.currentTime);
video.onseeking = () => { video.addEventListener('seeking', () => {
transitionAudio.seek(video.currentTime); transitionAudio.seek(video.currentTime);
}; });
video.onpause = () => {
transitionAudio.pause();
};
video.onplay = async () => {
await transitionAudio.play();
await transitionAudio.seek(video.currentTime);
// Fade in video.addEventListener('pause', () => {
const videoVolume = video.volume; transitionAudio.pause();
video.volume = 0; });
videoFader.fadeTo(videoVolume);
};
// Exit just before the end for the transition video.addEventListener('play', async () => {
const transitionBeforeEnd = () => { await transitionAudio.play();
if ( await transitionAudio.seek(video.currentTime);
video.currentTime >= video.duration - configGetNum("secondsBeforeEnd") &&
isReadyToCrossfade()
) {
video.removeEventListener("timeupdate", transitionBeforeEnd);
// Go to next video - XXX: does not support "repeat 1" mode // Fade in
document.querySelector(".next-button").click(); const videoVolume = video.volume;
} video.volume = 0;
}; videoFader.fadeTo(videoVolume);
video.ontimeupdate = transitionBeforeEnd; });
// Exit just before the end for the transition
const transitionBeforeEnd = () => {
if (
video.currentTime >= video.duration - configGetNumber('secondsBeforeEnd')
&& isReadyToCrossfade()
) {
video.removeEventListener('timeupdate', transitionBeforeEnd);
// Go to next video - XXX: does not support "repeat 1" mode
document.querySelector('.next-button').click();
}
};
video.addEventListener('timeupdate', transitionBeforeEnd);
}; };
const onApiLoaded = () => { const onApiLoaded = () => {
watchVideoIDChanges(async (videoID) => { watchVideoIDChanges(async (videoID) => {
await waitForTransition; await waitForTransition;
const url = await getStreamURL(videoID); const url = await getStreamURL(videoID);
if (!url) { if (!url) {
return; return;
} }
await createAudioForCrossfade(url);
}); await createAudioForCrossfade(url);
});
}; };
const crossfade = async (cb) => { const crossfade = async (cb) => {
if (!isReadyToCrossfade()) { if (!isReadyToCrossfade()) {
cb(); cb();
return; return;
} }
let resolveTransition; let resolveTransition;
waitForTransition = new Promise(function (resolve, reject) { waitForTransition = new Promise((resolve, reject) => {
resolveTransition = resolve; resolveTransition = resolve;
}); });
const video = document.querySelector("video"); const video = document.querySelector('video');
const fader = new VolumeFader(transitionAudio._sounds[0]._node, { const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
initialVolume: video.volume, initialVolume: video.volume,
fadeScaling: configGetNum("fadeScaling"), fadeScaling: configGetNumber('fadeScaling'),
fadeDuration: configGetNum("fadeOutDuration"), fadeDuration: configGetNumber('fadeOutDuration'),
}); });
// Fade out the music // Fade out the music
video.volume = 0; video.volume = 0;
fader.fadeOut(() => { fader.fadeOut(() => {
resolveTransition(); resolveTransition();
cb(); cb();
}); });
}; };
module.exports = async () => { module.exports = async () => {
config = await configProvider.getAll(); config = await configProvider.getAll();
configProvider.subscribeAll((newConfig) => { configProvider.subscribeAll((newConfig) => {
config = newConfig; config = newConfig;
}); });
document.addEventListener("apiLoaded", onApiLoaded, { document.addEventListener('apiLoaded', onApiLoaded, {
once: true, once: true,
passive: true, passive: true,
}); });
}; };

View File

@ -1,72 +1,79 @@
const config = require("./config"); const config = require('./config');
const defaultOptions = require("../../config/defaults").plugins.crossfade;
const prompt = require("custom-electron-prompt"); const defaultOptions = require('../../config/defaults').plugins.crossfade;
const promptOptions = require("../../providers/prompt-options");
const prompt = require('custom-electron-prompt');
const promptOptions = require('../../providers/prompt-options');
module.exports = (win) => [ module.exports = (win) => [
{ {
label: "Advanced", label: 'Advanced',
click: async () => { async click() {
const newOptions = await promptCrossfadeValues(win, config.getAll()); const newOptions = await promptCrossfadeValues(win, config.getAll());
if (newOptions) config.setAll(newOptions); if (newOptions) {
}, config.setAll(newOptions);
}, }
},
},
]; ];
async function promptCrossfadeValues(win, options) { async function promptCrossfadeValues(win, options) {
const res = await prompt( const res = await prompt(
{ {
title: "Crossfade Options", title: 'Crossfade Options',
type: "multiInput", type: 'multiInput',
multiInputOptions: [ multiInputOptions: [
{ {
label: "Fade in duration (ms)", label: 'Fade in duration (ms)',
value: options.fadeInDuration || defaultOptions.fadeInDuration, value: options.fadeInDuration || defaultOptions.fadeInDuration,
inputAttrs: { inputAttrs: {
type: "number", type: 'number',
required: true, required: true,
min: 0, min: 0,
step: 100, step: 100,
}, },
}, },
{ {
label: "Fade out duration (ms)", label: 'Fade out duration (ms)',
value: options.fadeOutDuration || defaultOptions.fadeOutDuration, value: options.fadeOutDuration || defaultOptions.fadeOutDuration,
inputAttrs: { inputAttrs: {
type: "number", type: 'number',
required: true, required: true,
min: 0, min: 0,
step: 100, step: 100,
}, },
}, },
{ {
label: "Crossfade x seconds before end", label: 'Crossfade x seconds before end',
value: value:
options.secondsBeforeEnd || defaultOptions.secondsBeforeEnd, options.secondsBeforeEnd || defaultOptions.secondsBeforeEnd,
inputAttrs: { inputAttrs: {
type: "number", type: 'number',
required: true, required: true,
min: 0, min: 0,
}, },
}, },
{ {
label: "Fade scaling", label: 'Fade scaling',
selectOptions: { linear: "Linear", logarithmic: "Logarithmic" }, selectOptions: { linear: 'Linear', logarithmic: 'Logarithmic' },
value: options.fadeScaling || defaultOptions.fadeScaling, value: options.fadeScaling || defaultOptions.fadeScaling,
}, },
], ],
resizable: true, resizable: true,
height: 360, height: 360,
...promptOptions(), ...promptOptions(),
}, },
win, win,
).catch(console.error); ).catch(console.error);
if (!res) return undefined; if (!res) {
return { return undefined;
fadeInDuration: Number(res[0]), }
fadeOutDuration: Number(res[1]),
secondsBeforeEnd: Number(res[2]), return {
fadeScaling: res[3], fadeInDuration: Number(res[0]),
}; fadeOutDuration: Number(res[1]),
secondsBeforeEnd: Number(res[2]),
fadeScaling: res[3],
};
} }

View File

@ -1,14 +1,14 @@
module.exports = () => { module.exports = () => {
document.addEventListener('apiLoaded', apiEvent => { document.addEventListener('apiLoaded', (apiEvent) => {
apiEvent.detail.addEventListener('videodatachange', name => { apiEvent.detail.addEventListener('videodatachange', (name) => {
if (name === 'dataloaded') { if (name === 'dataloaded') {
apiEvent.detail.pauseVideo(); apiEvent.detail.pauseVideo();
document.querySelector('video').ontimeupdate = e => { document.querySelector('video').addEventListener('timeupdate', (e) => {
e.target.pause(); e.target.pause();
} });
} else { } else {
document.querySelector('video').ontimeupdate = null; document.querySelector('video').ontimeupdate = null;
} }
}) });
}, { once: true, passive: true }) }, { once: true, passive: true });
}; };

View File

@ -1,12 +1,12 @@
"use strict"; 'use strict';
const Discord = require("@xhayper/discord-rpc"); const { dialog, app } = require('electron');
const { dev } = require("electron-is"); const Discord = require('@xhayper/discord-rpc');
const { dialog, app } = require("electron"); const { dev } = require('electron-is');
const registerCallback = require("../../providers/song-info"); const registerCallback = require('../../providers/song-info');
// Application ID registered by @Zo-Bro-23 // Application ID registered by @Zo-Bro-23
const clientId = "1043858434585526382"; const clientId = '1043858434585526382';
/** /**
* @typedef {Object} Info * @typedef {Object} Info
@ -19,12 +19,12 @@ const clientId = "1043858434585526382";
* @type {Info} * @type {Info}
*/ */
const info = { const info = {
rpc: new Discord.Client({ rpc: new Discord.Client({
clientId clientId,
}), }),
ready: false, ready: false,
autoReconnect: true, autoReconnect: true,
lastSongInfo: null, lastSongInfo: null,
}; };
/** /**
@ -33,59 +33,87 @@ const info = {
const refreshCallbacks = []; const refreshCallbacks = [];
const resetInfo = () => { const resetInfo = () => {
info.ready = false; info.ready = false;
clearTimeout(clearActivity); clearTimeout(clearActivity);
if (dev()) console.log("discord disconnected"); if (dev()) {
refreshCallbacks.forEach(cb => cb()); console.log('discord disconnected');
}
for (const cb of refreshCallbacks) {
cb();
}
}; };
info.rpc.on("connected", () => { info.rpc.on('connected', () => {
if (dev()) console.log("discord connected"); if (dev()) {
refreshCallbacks.forEach(cb => cb()); console.log('discord connected');
}
for (const cb of refreshCallbacks) {
cb();
}
}); });
info.rpc.on("ready", () => { info.rpc.on('ready', () => {
info.ready = true; info.ready = true;
if (info.lastSongInfo) updateActivity(info.lastSongInfo) if (info.lastSongInfo) {
updateActivity(info.lastSongInfo);
}
}); });
info.rpc.on("disconnected", () => { info.rpc.on('disconnected', () => {
resetInfo(); resetInfo();
if (info.autoReconnect) { if (info.autoReconnect) {
connectTimeout(); connectTimeout();
} }
}); });
const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => { const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => {
if (!info.autoReconnect || info.rpc.isConnected) return; if (!info.autoReconnect || info.rpc.isConnected) {
info.rpc.login().then(resolve).catch(reject); return;
}
info.rpc.login().then(resolve).catch(reject);
}, 5000)); }, 5000));
const connectRecursive = () => { const connectRecursive = () => {
if (!info.autoReconnect || info.rpc.isConnected) return; if (!info.autoReconnect || info.rpc.isConnected) {
connectTimeout().catch(connectRecursive); return;
} }
connectTimeout().catch(connectRecursive);
};
let window; let window;
const connect = (showErr = false) => { const connect = (showError = false) => {
if (info.rpc.isConnected) { if (info.rpc.isConnected) {
if (dev()) if (dev()) {
console.log('Attempted to connect with active connection'); console.log('Attempted to connect with active connection');
return; }
}
info.ready = false; return;
}
// Startup the rpc client info.ready = false;
info.rpc.login({ clientId }).catch(err => {
resetInfo(); // Startup the rpc client
if (dev()) console.error(err); info.rpc.login({ clientId }).catch((error) => {
if (info.autoReconnect) { resetInfo();
connectRecursive(); if (dev()) {
} console.error(error);
else if (showErr) dialog.showMessageBox(window, { title: 'Connection failed', message: err.message || String(err), type: 'error' }); }
});
if (info.autoReconnect) {
connectRecursive();
} else if (showError) {
dialog.showMessageBox(window, {
title: 'Connection failed',
message: error.message || String(error),
type: 'error',
});
}
});
}; };
let clearActivity; let clearActivity;
@ -95,75 +123,80 @@ let clearActivity;
let updateActivity; let updateActivity;
module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTime, listenAlong, hideDurationLeft }) => { module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTime, listenAlong, hideDurationLeft }) => {
info.autoReconnect = autoReconnect; info.autoReconnect = autoReconnect;
window = win; window = win;
// We get multiple events // We get multiple events
// Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1) // Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
// Skip time: PAUSE(N), PLAY(N) // Skip time: PAUSE(N), PLAY(N)
updateActivity = songInfo => { updateActivity = (songInfo) => {
if (songInfo.title.length === 0 && songInfo.artist.length === 0) { if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
return; return;
} }
info.lastSongInfo = songInfo;
// stop the clear activity timout info.lastSongInfo = songInfo;
clearTimeout(clearActivity);
// stop early if discord connection is not ready // Stop the clear activity timout
// do this after clearTimeout to avoid unexpected clears clearTimeout(clearActivity);
if (!info.rpc || !info.ready) {
return;
}
// clear directly if timeout is 0 // Stop early if discord connection is not ready
if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) { // do this after clearTimeout to avoid unexpected clears
info.rpc.user?.clearActivity().catch(console.error); if (!info.rpc || !info.ready) {
return; return;
} }
// Song information changed, so lets update the rich presence // Clear directly if timeout is 0
// @see https://discord.com/developers/docs/topics/gateway#activity-object if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) {
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530 info.rpc.user?.clearActivity().catch(console.error);
const activityInfo = { return;
details: songInfo.title, }
state: songInfo.artist,
largeImageKey: songInfo.imageSrc,
largeImageText: songInfo.album,
buttons: listenAlong ? [
{ label: "Listen Along", url: songInfo.url },
] : undefined,
};
if (songInfo.isPaused) { // Song information changed, so lets update the rich presence
// Add a paused icon to show that the song is paused // @see https://discord.com/developers/docs/topics/gateway#activity-object
activityInfo.smallImageKey = "paused"; // not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
activityInfo.smallImageText = "Paused"; const activityInfo = {
// Set start the timer so the activity gets cleared after a while if enabled details: songInfo.title,
if (activityTimoutEnabled) state: songInfo.artist,
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), activityTimoutTime ?? 10000); largeImageKey: songInfo.imageSrc,
} else if (!hideDurationLeft) { largeImageText: songInfo.album,
// Add the start and end time of the song buttons: listenAlong ? [
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000; { label: 'Listen Along', url: songInfo.url },
activityInfo.startTimestamp = songStartTime; ] : undefined,
activityInfo.endTimestamp = };
songStartTime + songInfo.songDuration * 1000;
}
info.rpc.user?.setActivity(activityInfo).catch(console.error); if (songInfo.isPaused) {
}; // Add a paused icon to show that the song is paused
activityInfo.smallImageKey = 'paused';
activityInfo.smallImageText = 'Paused';
// Set start the timer so the activity gets cleared after a while if enabled
if (activityTimoutEnabled) {
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), activityTimoutTime ?? 10_000);
}
} else if (!hideDurationLeft) {
// Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp
= songStartTime + songInfo.songDuration * 1000;
}
// If the page is ready, register the callback info.rpc.user?.setActivity(activityInfo).catch(console.error);
win.once("ready-to-show", () => { };
registerCallback(updateActivity);
connect(); // If the page is ready, register the callback
}); win.once('ready-to-show', () => {
app.on('window-all-closed', module.exports.clear) registerCallback(updateActivity);
connect();
});
app.on('window-all-closed', module.exports.clear);
}; };
module.exports.clear = () => { module.exports.clear = () => {
if (info.rpc) info.rpc.user?.clearActivity(); if (info.rpc) {
clearTimeout(clearActivity); info.rpc.user?.clearActivity();
}
clearTimeout(clearActivity);
}; };
module.exports.connect = connect; module.exports.connect = connect;

View File

@ -1,84 +1,84 @@
const prompt = require("custom-electron-prompt"); const prompt = require('custom-electron-prompt');
const { setMenuOptions } = require("../../config/plugins"); const { clear, connect, registerRefresh, isConnected } = require('./back');
const promptOptions = require("../../providers/prompt-options");
const { clear, connect, registerRefresh, isConnected } = require("./back");
const { singleton } = require("../../providers/decorators") const { setMenuOptions } = require('../../config/plugins');
const promptOptions = require('../../providers/prompt-options');
const { singleton } = require('../../providers/decorators');
const registerRefreshOnce = singleton((refreshMenu) => { const registerRefreshOnce = singleton((refreshMenu) => {
registerRefresh(refreshMenu); registerRefresh(refreshMenu);
}); });
module.exports = (win, options, refreshMenu) => { module.exports = (win, options, refreshMenu) => {
registerRefreshOnce(refreshMenu); registerRefreshOnce(refreshMenu);
return [ return [
{ {
label: isConnected() ? "Connected" : "Reconnect", label: isConnected() ? 'Connected' : 'Reconnect',
enabled: !isConnected(), enabled: !isConnected(),
click: connect, click: connect,
}, },
{ {
label: "Auto reconnect", label: 'Auto reconnect',
type: "checkbox", type: 'checkbox',
checked: options.autoReconnect, checked: options.autoReconnect,
click: (item) => { click(item) {
options.autoReconnect = item.checked; options.autoReconnect = item.checked;
setMenuOptions('discord', options); setMenuOptions('discord', options);
}, },
}, },
{ {
label: "Clear activity", label: 'Clear activity',
click: clear, click: clear,
}, },
{ {
label: "Clear activity after timeout", label: 'Clear activity after timeout',
type: "checkbox", type: 'checkbox',
checked: options.activityTimoutEnabled, checked: options.activityTimoutEnabled,
click: (item) => { click(item) {
options.activityTimoutEnabled = item.checked; options.activityTimoutEnabled = item.checked;
setMenuOptions('discord', options); setMenuOptions('discord', options);
}, },
}, },
{ {
label: "Listen Along", label: 'Listen Along',
type: "checkbox", type: 'checkbox',
checked: options.listenAlong, checked: options.listenAlong,
click: (item) => { click(item) {
options.listenAlong = item.checked; options.listenAlong = item.checked;
setMenuOptions('discord', options); setMenuOptions('discord', options);
}, },
}, },
{ {
label: "Hide duration left", label: 'Hide duration left',
type: "checkbox", type: 'checkbox',
checked: options.hideDurationLeft, checked: options.hideDurationLeft,
click: (item) => { click(item) {
options.hideDurationLeft = item.checked; options.hideDurationLeft = item.checked;
setMenuOptions('discord', options); setMenuOptions('discord', options);
} },
}, },
{ {
label: "Set inactivity timeout", label: 'Set inactivity timeout',
click: () => setInactivityTimeout(win, options), click: () => setInactivityTimeout(win, options),
}, },
]; ];
}; };
async function setInactivityTimeout(win, options) { async function setInactivityTimeout(win, options) {
let output = await prompt({ const output = await prompt({
title: 'Set Inactivity Timeout', title: 'Set Inactivity Timeout',
label: 'Enter inactivity timeout in seconds:', label: 'Enter inactivity timeout in seconds:',
value: Math.round((options.activityTimoutTime ?? 0) / 1e3), value: Math.round((options.activityTimoutTime ?? 0) / 1e3),
type: "counter", type: 'counter',
counterOptions: { minimum: 0, multiFire: true }, counterOptions: { minimum: 0, multiFire: true },
width: 450, width: 450,
...promptOptions() ...promptOptions(),
}, win) }, win);
if (output) { if (output) {
options.activityTimoutTime = Math.round(output * 1e3); options.activityTimoutTime = Math.round(output * 1e3);
setMenuOptions("discord", options); setMenuOptions('discord', options);
} }
} }

View File

@ -3,14 +3,26 @@ const {
mkdirSync, mkdirSync,
createWriteStream, createWriteStream,
writeFileSync, writeFileSync,
} = require('fs'); } = require('node:fs');
const { join } = require('path'); const { join } = require('node:path');
const { randomBytes } = require('node:crypto');
const { ipcMain, app, dialog } = require('electron');
const is = require('electron-is');
const { Innertube, UniversalCache, Utils, ClientType } = require('youtubei.js');
const ytpl = require('ytpl'); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
const filenamify = require('filenamify');
const ID3Writer = require('browser-id3-writer');
const { Mutex } = require('async-mutex');
const ffmpeg = require('@ffmpeg/ffmpeg').createFFmpeg({
log: false,
logger() {
}, // Console.log,
progress() {
}, // Console.log,
});
const { fetchFromGenius } = require('../lyrics-genius/back');
const { isEnabled } = require('../../config/plugins');
const { getImage, cleanupName } = require('../../providers/song-info');
const { injectCSS } = require('../utils');
const { cache } = require("../../providers/decorators")
const { const {
presets, presets,
cropMaxWidth, cropMaxWidth,
@ -19,20 +31,12 @@ const {
sendFeedback: sendFeedback_, sendFeedback: sendFeedback_,
} = require('./utils'); } = require('./utils');
const { ipcMain, app, dialog } = require('electron'); const { fetchFromGenius } = require('../lyrics-genius/back');
const is = require('electron-is'); const { isEnabled } = require('../../config/plugins');
const { Innertube, UniversalCache, Utils, ClientType } = require('youtubei.js'); const { getImage, cleanupName } = require('../../providers/song-info');
const ytpl = require('ytpl'); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid const { injectCSS } = require('../utils');
const { cache } = require('../../providers/decorators');
const filenamify = require('filenamify');
const ID3Writer = require('browser-id3-writer');
const { randomBytes } = require('crypto');
const Mutex = require('async-mutex').Mutex;
const ffmpeg = require('@ffmpeg/ffmpeg').createFFmpeg({
log: false,
logger: () => {}, // console.log,
progress: () => {}, // console.log,
});
const ffmpegMutex = new Mutex(); const ffmpegMutex = new Mutex();
const config = require('./config'); const config = require('./config');
@ -40,12 +44,12 @@ const config = require('./config');
/** @type {Innertube} */ /** @type {Innertube} */
let yt; let yt;
let win; let win;
let playingUrl = undefined; let playingUrl;
const sendError = (error, source) => { const sendError = (error, source) => {
win.setProgressBar(-1); // close progress bar win.setProgressBar(-1); // Close progress bar
setBadge(0); // close badge setBadge(0); // Close badge
sendFeedback_(win); // reset feedback sendFeedback_(win); // Reset feedback
const songNameMessage = source ? `\nin ${source}` : ''; const songNameMessage = source ? `\nin ${source}` : '';
const cause = error.cause ? `\n\n${error.cause.toString()}` : ''; const cause = error.cause ? `\n\n${error.cause.toString()}` : '';
@ -71,8 +75,8 @@ module.exports = async (win_) => {
}); });
ipcMain.on('download-song', (_, url) => downloadSong(url)); ipcMain.on('download-song', (_, url) => downloadSong(url));
ipcMain.on('video-src-changed', async (_, data) => { ipcMain.on('video-src-changed', async (_, data) => {
playingUrl = playingUrl
JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical;
}); });
ipcMain.on('download-playlist-request', async (_event, url) => ipcMain.on('download-playlist-request', async (_event, url) =>
downloadPlaylist(url), downloadPlaylist(url),
@ -86,13 +90,14 @@ async function downloadSong(
url, url,
playlistFolder = undefined, playlistFolder = undefined,
trackId = undefined, trackId = undefined,
increasePlaylistProgress = () => {}, increasePlaylistProgress = () => {
},
) { ) {
let resolvedName = undefined; let resolvedName;
try { try {
await downloadSongUnsafe( await downloadSongUnsafe(
url, url,
name=>resolvedName=name, (name) => resolvedName = name,
playlistFolder, playlistFolder,
trackId, trackId,
increasePlaylistProgress, increasePlaylistProgress,
@ -107,7 +112,8 @@ async function downloadSongUnsafe(
setName, setName,
playlistFolder = undefined, playlistFolder = undefined,
trackId = undefined, trackId = undefined,
increasePlaylistProgress = () => {}, increasePlaylistProgress = () => {
},
) { ) {
const sendFeedback = (message, progress) => { const sendFeedback = (message, progress) => {
if (!playlistFolder) { if (!playlistFolder) {
@ -128,42 +134,45 @@ async function downloadSongUnsafe(
} }
const metadata = getMetadata(info); const metadata = getMetadata(info);
if (metadata.album === 'N/A') metadata.album = ''; if (metadata.album === 'N/A') {
metadata.album = '';
}
metadata.trackId = trackId; metadata.trackId = trackId;
const dir = const dir
playlistFolder || config.get('downloadFolder') || app.getPath('downloads'); = playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${ const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${
metadata.title metadata.title
}`; }`;
setName(name); setName(name);
let playabilityStatus = info.playability_status; let playabilityStatus = info.playability_status;
let bypassedResult = null; let bypassedResult = null;
if (playabilityStatus.status === "LOGIN_REQUIRED") { if (playabilityStatus.status === 'LOGIN_REQUIRED') {
// try to bypass the age restriction // Try to bypass the age restriction
bypassedResult = await getAndroidTvInfo(id); bypassedResult = await getAndroidTvInfo(id);
playabilityStatus = bypassedResult.playability_status; playabilityStatus = bypassedResult.playability_status;
if (playabilityStatus.status === "LOGIN_REQUIRED") { if (playabilityStatus.status === 'LOGIN_REQUIRED') {
throw new Error( throw new Error(
`[${playabilityStatus.status}] ${playabilityStatus.reason}`, `[${playabilityStatus.status}] ${playabilityStatus.reason}`,
); );
} }
info = bypassedResult; info = bypassedResult;
} }
if (playabilityStatus.status === "UNPLAYABLE") { if (playabilityStatus.status === 'UNPLAYABLE') {
/** /**
* @typedef {import('youtubei.js/dist/src/parser/classes/PlayerErrorMessage').default} PlayerErrorMessage * @typedef {import('youtubei.js/dist/src/parser/classes/PlayerErrorMessage').default} PlayerErrorMessage
* @type {PlayerErrorMessage} * @type {PlayerErrorMessage}
*/ */
const errorScreen = playabilityStatus.error_screen; const errorScreen = playabilityStatus.error_screen;
throw new Error( throw new Error(
`[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`, `[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`,
); );
} }
const extension = presets[config.get('preset')]?.extension || 'mp3'; const extension = presets[config.get('preset')]?.extension || 'mp3';
@ -179,9 +188,9 @@ async function downloadSongUnsafe(
} }
const download_options = { const download_options = {
type: 'audio', // audio, video or video+audio type: 'audio', // Audio, video or video+audio
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
format: 'any', // media container format format: 'any', // Media container format
}; };
const format = info.chooseFormat(download_options); const format = info.chooseFormat(download_options);
@ -197,16 +206,7 @@ async function downloadSongUnsafe(
mkdirSync(dir); mkdirSync(dir);
} }
if (!presets[config.get('preset')]) { if (presets[config.get('preset')]) {
const fileBuffer = await iterableStreamToMP3(
iterableStream,
metadata,
format.content_length,
sendFeedback,
increasePlaylistProgress,
);
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
} else {
const file = createWriteStream(filePath); const file = createWriteStream(filePath);
let downloaded = 0; let downloaded = 0;
const total = format.content_length; const total = format.content_length;
@ -219,12 +219,22 @@ async function downloadSongUnsafe(
increasePlaylistProgress(ratio); increasePlaylistProgress(ratio);
file.write(chunk); file.write(chunk);
} }
await ffmpegWriteTags( await ffmpegWriteTags(
filePath, filePath,
metadata, metadata,
presets[config.get('preset')]?.ffmpegArgs, presets[config.get('preset')]?.ffmpegArgs,
); );
sendFeedback(null, -1); sendFeedback(null, -1);
} else {
const fileBuffer = await iterableStreamToMP3(
iterableStream,
metadata,
format.content_length,
sendFeedback,
increasePlaylistProgress,
);
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
} }
sendFeedback(null, -1); sendFeedback(null, -1);
@ -236,7 +246,8 @@ async function iterableStreamToMP3(
metadata, metadata,
content_length, content_length,
sendFeedback, sendFeedback,
increasePlaylistProgress = () => {}, increasePlaylistProgress = () => {
},
) { ) {
const chunks = []; const chunks = [];
let downloaded = 0; let downloaded = 0;
@ -251,7 +262,8 @@ async function iterableStreamToMP3(
// This is a very rough estimate, trying to make the progress bar look nice // This is a very rough estimate, trying to make the progress bar look nice
increasePlaylistProgress(ratio * 0.15); increasePlaylistProgress(ratio * 0.15);
} }
sendFeedback('Loading…', 2); // indefinite progress bar after download
sendFeedback('Loading…', 2); // Indefinite progress bar after download
const buffer = Buffer.concat(chunks); const buffer = Buffer.concat(chunks);
const safeVideoName = randomBytes(32).toString('hex'); const safeVideoName = randomBytes(32).toString('hex');
@ -282,8 +294,8 @@ async function iterableStreamToMP3(
sendFeedback('Saving…'); sendFeedback('Saving…');
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`); return ffmpeg.FS('readFile', `${safeVideoName}.mp3`);
} catch (e) { } catch (error) {
sendError(e, safeVideoName); sendError(error, safeVideoName);
} finally { } finally {
releaseFFmpegMutex(); releaseFFmpegMutex();
} }
@ -307,6 +319,7 @@ async function writeID3(buffer, metadata, sendFeedback) {
if (metadata.album) { if (metadata.album) {
writer.setFrame('TALB', metadata.album); writer.setFrame('TALB', metadata.album);
} }
if (coverBuffer) { if (coverBuffer) {
writer.setFrame('APIC', { writer.setFrame('APIC', {
type: 3, type: 3,
@ -314,22 +327,25 @@ async function writeID3(buffer, metadata, sendFeedback) {
description: '', description: '',
}); });
} }
if (isEnabled('lyrics-genius')) { if (isEnabled('lyrics-genius')) {
const lyrics = await fetchFromGenius(metadata); const lyrics = await fetchFromGenius(metadata);
if (lyrics) { if (lyrics) {
writer.setFrame('USLT', { writer.setFrame('USLT', {
description: '', description: '',
lyrics: lyrics, lyrics,
}); });
} }
} }
if (metadata.trackId) { if (metadata.trackId) {
writer.setFrame('TRCK', metadata.trackId); writer.setFrame('TRCK', metadata.trackId);
} }
writer.addTag(); writer.addTag();
return Buffer.from(writer.arrayBuffer); return Buffer.from(writer.arrayBuffer);
} catch (e) { } catch (error) {
sendError(e, `${metadata.artist} - ${metadata.title}`); sendError(error, `${metadata.artist} - ${metadata.title}`);
} }
} }
@ -339,10 +355,11 @@ async function downloadPlaylist(givenUrl) {
} catch { } catch {
givenUrl = undefined; givenUrl = undefined;
} }
const playlistId =
getPlaylistID(givenUrl) || const playlistId
getPlaylistID(new URL(win.webContents.getURL())) || = getPlaylistID(givenUrl)
getPlaylistID(new URL(playingUrl)); || getPlaylistID(new URL(win.webContents.getURL()))
|| getPlaylistID(new URL(playingUrl));
if (!playlistId) { if (!playlistId) {
sendError(new Error('No playlist ID found')); sendError(new Error('No playlist ID found'));
@ -356,24 +373,30 @@ async function downloadPlaylist(givenUrl) {
let playlist; let playlist;
try { try {
playlist = await ytpl(playlistId, { playlist = await ytpl(playlistId, {
limit: config.get('playlistMaxItems') || Infinity, limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY,
}); });
} catch (e) { } catch (error) {
sendError( sendError(
`Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${e}`, `Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${error}`,
); );
return; return;
} }
if (playlist.items.length === 0) sendError(new Error('Playlist is empty'));
if (playlist.items.length === 0) {
sendError(new Error('Playlist is empty'));
}
if (playlist.items.length === 1) { if (playlist.items.length === 1) {
sendFeedback('Playlist has only one item, downloading it directly'); sendFeedback('Playlist has only one item, downloading it directly');
await downloadSong(playlist.items[0].url); await downloadSong(playlist.items[0].url);
return; return;
} }
const isAlbum = playlist.title.startsWith('Album - '); const isAlbum = playlist.title.startsWith('Album - ');
if (isAlbum) { if (isAlbum) {
playlist.title = playlist.title.slice(8); playlist.title = playlist.title.slice(8);
} }
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
const folder = getFolder(config.get('downloadFolder')); const folder = getFolder(config.get('downloadFolder'));
@ -401,7 +424,7 @@ async function downloadPlaylist(givenUrl) {
); );
} }
win.setProgressBar(2); // starts with indefinite bar win.setProgressBar(2); // Starts with indefinite bar
setBadge(playlist.items.length); setBadge(playlist.items.length);
@ -424,9 +447,9 @@ async function downloadPlaylist(givenUrl) {
playlistFolder, playlistFolder,
trackId, trackId,
increaseProgress, increaseProgress,
).catch((e) => ).catch((error) =>
sendError( sendError(
`Error downloading "${song.author.name} - ${song.title}":\n ${e}`, `Error downloading "${song.author.name} - ${song.title}":\n ${error}`,
), ),
); );
@ -434,12 +457,12 @@ async function downloadPlaylist(givenUrl) {
setBadge(playlist.items.length - counter); setBadge(playlist.items.length - counter);
counter++; counter++;
} }
} catch (e) { } catch (error) {
sendError(e); sendError(error);
} finally { } finally {
win.setProgressBar(-1); // close progress bar win.setProgressBar(-1); // Close progress bar
setBadge(0); // close badge counter setBadge(0); // Close badge counter
sendFeedback(); // clear feedback sendFeedback(); // Clear feedback
} }
} }
@ -458,8 +481,8 @@ async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) {
...ffmpegArgs, ...ffmpegArgs,
filePath, filePath,
); );
} catch (e) { } catch (error) {
sendError(e); sendError(error);
} finally { } finally {
releaseFFmpegMutex(); releaseFFmpegMutex();
} }
@ -482,11 +505,12 @@ function getFFmpegMetadataArgs(metadata) {
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
const getPlaylistID = (aURL) => { const getPlaylistID = (aURL) => {
const result = const result
aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist'); = aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
return result.slice(INVALID_PLAYLIST_MODIFIER.length); return result.slice(INVALID_PLAYLIST_MODIFIER.length);
} }
return result; return result;
}; };
@ -494,6 +518,7 @@ const getVideoId = (url) => {
if (typeof url === 'string') { if (typeof url === 'string') {
url = new URL(url); url = new URL(url);
} }
return url.searchParams.get('v'); return url.searchParams.get('v');
}; };
@ -513,7 +538,7 @@ const getAndroidTvInfo = async (id) => {
retrieve_player: true, retrieve_player: true,
}); });
const info = await innertube.getBasicInfo(id, 'TV_EMBEDDED'); const info = await innertube.getBasicInfo(id, 'TV_EMBEDDED');
// getInfo 404s with the bypass, so we use getBasicInfo instead // GetInfo 404s with the bypass, so we use getBasicInfo instead
// that's fine as we only need the streaming data // that's fine as we only need the streaming data
return info; return info;
} };

View File

@ -1,3 +1,4 @@
const { PluginConfig } = require('../../config/dynamic'); const { PluginConfig } = require('../../config/dynamic');
const config = new PluginConfig('downloader'); const config = new PluginConfig('downloader');
module.exports = { ...config }; module.exports = { ...config };

View File

@ -1,69 +1,81 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const { defaultConfig } = require("../../config"); const { defaultConfig } = require('../../config');
const { getSongMenu } = require("../../providers/dom-elements"); const { getSongMenu } = require('../../providers/dom-elements');
const { ElementFromFile, templatePath } = require("../utils"); const { ElementFromFile, templatePath } = require('../utils');
let menu = null; let menu = null;
let progress = null; let progress = null;
const downloadButton = ElementFromFile( const downloadButton = ElementFromFile(
templatePath(__dirname, "download.html") templatePath(__dirname, 'download.html'),
); );
let doneFirstLoad = false; let doneFirstLoad = false;
const menuObserver = new MutationObserver(() => { const menuObserver = new MutationObserver(() => {
if (!menu) { if (!menu) {
menu = getSongMenu(); menu = getSongMenu();
if (!menu) return; if (!menu) {
} return;
if (menu.contains(downloadButton)) return; }
const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; }
if (!menuUrl?.includes('watch?') && doneFirstLoad) return;
menu.prepend(downloadButton); if (menu.contains(downloadButton)) {
progress = document.querySelector("#ytmcustom-download"); return;
}
if (doneFirstLoad) return; const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
setTimeout(() => doneFirstLoad ||= true, 500); if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return;
}
menu.prepend(downloadButton);
progress = document.querySelector('#ytmcustom-download');
if (doneFirstLoad) {
return;
}
setTimeout(() => doneFirstLoad ||= true, 500);
}); });
// TODO: re-enable once contextIsolation is set to true // TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld("downloader", { // contextBridge.exposeInMainWorld("downloader", {
// download: () => { // download: () => {
global.download = () => { global.download = () => {
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// selector of first button which is always "Start Radio" // Selector of first button which is always "Start Radio"
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint') ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
?.getAttribute("href"); ?.getAttribute('href');
if (videoUrl) { if (videoUrl) {
if (videoUrl.startsWith('watch?')) { if (videoUrl.startsWith('watch?')) {
videoUrl = defaultConfig.url + "/" + videoUrl; videoUrl = defaultConfig.url + '/' + videoUrl;
} }
if (videoUrl.includes('?playlist=')) {
ipcRenderer.send('download-playlist-request', videoUrl);
return;
}
} else {
videoUrl = global.songInfo.url || window.location.href;
}
ipcRenderer.send('download-song', videoUrl); if (videoUrl.includes('?playlist=')) {
ipcRenderer.send('download-playlist-request', videoUrl);
return;
}
} else {
videoUrl = global.songInfo.url || window.location.href;
}
ipcRenderer.send('download-song', videoUrl);
}; };
module.exports = () => { module.exports = () => {
document.addEventListener('apiLoaded', () => { document.addEventListener('apiLoaded', () => {
menuObserver.observe(document.querySelector('ytmusic-popup-container'), { menuObserver.observe(document.querySelector('ytmusic-popup-container'), {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
}, { once: true, passive: true }) }, { once: true, passive: true });
ipcRenderer.on('downloader-feedback', (_, feedback) => { ipcRenderer.on('downloader-feedback', (_, feedback) => {
if (!progress) { if (progress) {
console.warn("Cannot update progress"); progress.innerHTML = feedback || 'Download';
} else { } else {
progress.innerHTML = feedback || "Download"; console.warn('Cannot update progress');
} }
}); });
}; };

View File

@ -1,45 +1,43 @@
const { dialog } = require("electron"); const { dialog } = require('electron');
const { downloadPlaylist } = require("./back"); const { downloadPlaylist } = require('./back');
const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); const { defaultMenuDownloadLabel, getFolder, presets } = require('./utils');
const config = require("./config"); const config = require('./config');
module.exports = () => { module.exports = () => [
return [ {
{ label: defaultMenuDownloadLabel,
label: defaultMenuDownloadLabel, click: () => downloadPlaylist(),
click: () => downloadPlaylist(), },
}, {
{ label: 'Choose download folder',
label: "Choose download folder", click() {
click: () => { const result = dialog.showOpenDialogSync({
const result = dialog.showOpenDialogSync({ properties: ['openDirectory', 'createDirectory'],
properties: ["openDirectory", "createDirectory"], defaultPath: getFolder(config.get('downloadFolder')),
defaultPath: getFolder(config.get("downloadFolder")), });
}); if (result) {
if (result) { config.set('downloadFolder', result[0]);
config.set("downloadFolder", result[0]); } // Else = user pressed cancel
} // else = user pressed cancel },
}, },
}, {
{ label: 'Presets',
label: "Presets", submenu: Object.keys(presets).map((preset) => ({
submenu: Object.keys(presets).map((preset) => ({ label: preset,
label: preset, type: 'radio',
type: "radio", checked: config.get('preset') === preset,
checked: config.get("preset") === preset, click() {
click: () => { config.set('preset', preset);
config.set("preset", preset); },
}, })),
})), },
}, {
{ label: 'Skip existing files',
label: "Skip existing files", type: 'checkbox',
type: "checkbox", checked: config.get('skipExisting'),
checked: config.get("skipExisting"), click(item) {
click: (item) => { config.set('skipExisting', item.checked);
config.set("skipExisting", item.checked); },
}, },
}, ];
];
};

View File

@ -1,21 +1,21 @@
.menu-item { .menu-item {
display: var(--ytmusic-menu-item_-_display); display: var(--ytmusic-menu-item_-_display);
height: var(--ytmusic-menu-item_-_height); height: var(--ytmusic-menu-item_-_height);
align-items: var(--ytmusic-menu-item_-_align-items); align-items: var(--ytmusic-menu-item_-_align-items);
padding: var(--ytmusic-menu-item_-_padding); padding: var(--ytmusic-menu-item_-_padding);
cursor: pointer; cursor: pointer;
} }
.menu-item > .yt-simple-endpoint:hover { .menu-item > .yt-simple-endpoint:hover {
background-color: var(--ytmusic-menu-item-hover-background-color); background-color: var(--ytmusic-menu-item-hover-background-color);
} }
.menu-icon { .menu-icon {
flex: var(--ytmusic-menu-item-icon_-_flex); flex: var(--ytmusic-menu-item-icon_-_flex);
margin: var(--ytmusic-menu-item-icon_-_margin); margin: var(--ytmusic-menu-item-icon_-_margin);
fill: var(--ytmusic-menu-item-icon_-_fill); fill: var(--ytmusic-menu-item-icon_-_fill);
stroke: var(--iron-icon-stroke-color, none); stroke: var(--iron-icon-stroke-color, none);
width: var(--iron-icon-width, 24px); width: var(--iron-icon-width, 24px);
height: var(--iron-icon-height, 24px); height: var(--iron-icon-height, 24px);
animation: var(--iron-icon_-_animation); animation: var(--iron-icon_-_animation);
} }

View File

@ -1,45 +1,45 @@
<div <div
class="style-scope menu-item ytmusic-menu-popup-renderer" aria-disabled="false"
role="option" aria-selected="false"
tabindex="-1" class="style-scope menu-item ytmusic-menu-popup-renderer"
aria-disabled="false" onclick="download()"
aria-selected="false" role="option"
onclick="download()" tabindex="-1"
> >
<div <div
id="navigation-endpoint" class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer" id="navigation-endpoint"
tabindex="-1" tabindex="-1"
> >
<div <div
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer" class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
> >
<svg <svg
viewBox="0 0 24 24" class="style-scope yt-icon"
preserveAspectRatio="xMidYMid meet" focusable="false"
focusable="false" preserveAspectRatio="xMidYMid meet"
class="style-scope yt-icon" style="pointer-events: none; display: block; width: 100%; height: 100%"
style="pointer-events: none; display: block; width: 100%; height: 100%" viewBox="0 0 24 24"
> >
<g class="style-scope yt-icon"> <g class="style-scope yt-icon">
<path <path
d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z" class="style-scope yt-icon"
class="style-scope yt-icon" d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z"
fill="#aaaaaa" fill="#aaaaaa"
/> />
<path <path
d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z" class="style-scope yt-icon"
class="style-scope yt-icon" d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z"
fill="#aaaaaa" fill="#aaaaaa"
/> />
</g> </g>
</svg> </svg>
</div> </div>
<div <div
class="text style-scope ytmusic-menu-navigation-item-renderer" class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-download" id="ytmcustom-download"
> >
Download Download
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,38 +1,39 @@
const { app } = require("electron"); const { app } = require('electron');
const is = require('electron-is'); const is = require('electron-is');
module.exports.getFolder = customFolder => customFolder || app.getPath("downloads"); module.exports.getFolder = (customFolder) => customFolder || app.getPath('downloads');
module.exports.defaultMenuDownloadLabel = "Download playlist"; module.exports.defaultMenuDownloadLabel = 'Download playlist';
module.exports.sendFeedback = (win, message) => { module.exports.sendFeedback = (win, message) => {
win.webContents.send("downloader-feedback", message); win.webContents.send('downloader-feedback', message);
}; };
module.exports.cropMaxWidth = (image) => { module.exports.cropMaxWidth = (image) => {
const imageSize = image.getSize(); const imageSize = image.getSize();
// standart youtube artwork width with margins from both sides is 280 + 720 + 280 // Standart youtube artwork width with margins from both sides is 280 + 720 + 280
if (imageSize.width === 1280 && imageSize.height === 720) { if (imageSize.width === 1280 && imageSize.height === 720) {
return image.crop({ return image.crop({
x: 280, x: 280,
y: 0, y: 0,
width: 720, width: 720,
height: 720 height: 720,
}); });
} }
return image;
} return image;
};
// Presets for FFmpeg // Presets for FFmpeg
module.exports.presets = { module.exports.presets = {
"None (defaults to mp3)": undefined, 'None (defaults to mp3)': undefined,
opus: { 'opus': {
extension: "opus", extension: 'opus',
ffmpegArgs: ["-acodec", "libopus"], ffmpegArgs: ['-acodec', 'libopus'],
}, },
}; };
module.exports.setBadge = n => { module.exports.setBadge = (n) => {
if (is.linux() || is.macOS()) { if (is.linux() || is.macOS()) {
app.setBadgeCount(n); app.setBadgeCount(n);
} }
} };

View File

@ -2,46 +2,46 @@
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/ // https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
const exponentialVolume = () => { const exponentialVolume = () => {
// manipulation exponent, higher value = lower volume // Manipulation exponent, higher value = lower volume
// 3 is the value used by pulseaudio, which Barteks2x figured out this gist here: https://gist.github.com/Barteks2x/a4e189a36a10c159bb1644ffca21c02a // 3 is the value used by pulseaudio, which Barteks2x figured out this gist here: https://gist.github.com/Barteks2x/a4e189a36a10c159bb1644ffca21c02a
// 0.05 (or 5%) is the lowest you can select in the UI which with an exponent of 3 becomes 0.000125 or 0.0125% // 0.05 (or 5%) is the lowest you can select in the UI which with an exponent of 3 becomes 0.000125 or 0.0125%
const EXPONENT = 3; const EXPONENT = 3;
const storedOriginalVolumes = new WeakMap(); const storedOriginalVolumes = new WeakMap();
const { get, set } = Object.getOwnPropertyDescriptor( const { get, set } = Object.getOwnPropertyDescriptor(
HTMLMediaElement.prototype, HTMLMediaElement.prototype,
"volume" 'volume',
); );
Object.defineProperty(HTMLMediaElement.prototype, "volume", { Object.defineProperty(HTMLMediaElement.prototype, 'volume', {
get() { get() {
const lowVolume = get.call(this); const lowVolume = get.call(this);
const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT); const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT);
// The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values. // The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values.
// To avoid this, I'll store the unmodified volume to return it when read here. // To avoid this, I'll store the unmodified volume to return it when read here.
// This mostly solves the issue, but the initial read has no stored value and the volume can also change though external influences. // This mostly solves the issue, but the initial read has no stored value and the volume can also change though external influences.
// To avoid ill effects, I check if the stored volume is somewhere in the same range as the calculated volume. // To avoid ill effects, I check if the stored volume is somewhere in the same range as the calculated volume.
const storedOriginalVolume = storedOriginalVolumes.get(this); const storedOriginalVolume = storedOriginalVolumes.get(this);
const storedDeviation = Math.abs( const storedDeviation = Math.abs(
storedOriginalVolume - calculatedOriginalVolume storedOriginalVolume - calculatedOriginalVolume,
); );
const originalVolume = const originalVolume
storedDeviation < 0.01 = storedDeviation < 0.01
? storedOriginalVolume ? storedOriginalVolume
: calculatedOriginalVolume; : calculatedOriginalVolume;
return originalVolume; return originalVolume;
}, },
set(originalVolume) { set(originalVolume) {
const lowVolume = originalVolume ** EXPONENT; const lowVolume = originalVolume ** EXPONENT;
storedOriginalVolumes.set(this, originalVolume); storedOriginalVolumes.set(this, originalVolume);
set.call(this, lowVolume); set.call(this, lowVolume);
}, },
}); });
}; };
module.exports = () => module.exports = () =>
document.addEventListener("apiLoaded", exponentialVolume, { document.addEventListener('apiLoaded', exponentialVolume, {
once: true, once: true,
passive: true, passive: true,
}); });

View File

@ -1,23 +1,23 @@
const path = require("path"); const path = require('node:path');
const electronLocalshortcut = require("electron-localshortcut");
const { injectCSS } = require("../utils");
const electronLocalshortcut = require('electron-localshortcut');
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main'); const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
const { injectCSS } = require('../utils');
setupTitlebar(); setupTitlebar();
//tracks menu visibility // Tracks menu visibility
module.exports = (win) => { module.exports = (win) => {
// css for custom scrollbar + disable drag area(was causing bugs) // Css for custom scrollbar + disable drag area(was causing bugs)
injectCSS(win.webContents, path.join(__dirname, "style.css")); injectCSS(win.webContents, path.join(__dirname, 'style.css'));
win.once("ready-to-show", () => { win.once('ready-to-show', () => {
attachTitlebarToWindow(win); attachTitlebarToWindow(win);
electronLocalshortcut.register(win, "`", () => { electronLocalshortcut.register(win, '`', () => {
win.webContents.send("toggleMenu"); win.webContents.send('toggleMenu');
}); });
}); });
}; };

View File

@ -1,74 +1,78 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const config = require("../../config"); const { Titlebar, Color } = require('custom-electron-titlebar');
const { Titlebar, Color } = require("custom-electron-titlebar");
const { isEnabled } = require("../../config/plugins"); const config = require('../../config');
function $(selector) { return document.querySelector(selector); } const { isEnabled } = require('../../config/plugins');
function $(selector) {
return document.querySelector(selector);
}
module.exports = (options) => { module.exports = (options) => {
let visible = () => !!$('.cet-menubar').firstChild; const visible = () => Boolean($('.cet-menubar').firstChild);
const bar = new Titlebar({ const bar = new Titlebar({
icon: "https://cdn-icons-png.flaticon.com/512/5358/5358672.png", icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
backgroundColor: Color.fromHex("#050505"), backgroundColor: Color.fromHex('#050505'),
itemBackgroundColor: Color.fromHex("#1d1d1d"), itemBackgroundColor: Color.fromHex('#1d1d1d'),
svgColor: Color.WHITE, svgColor: Color.WHITE,
menu: config.get("options.hideMenu") ? null : undefined menu: config.get('options.hideMenu') ? null : undefined,
}); });
bar.updateTitle(" "); bar.updateTitle(' ');
document.title = "Youtube Music"; document.title = 'Youtube Music';
const toggleMenu = () => { const toggleMenu = () => {
if (visible()) { if (visible()) {
bar.updateMenu(null); bar.updateMenu(null);
} else { } else {
bar.refreshMenu(); bar.refreshMenu();
} }
}; };
$('.cet-window-icon').addEventListener('click', toggleMenu); $('.cet-window-icon').addEventListener('click', toggleMenu);
ipcRenderer.on("toggleMenu", toggleMenu); ipcRenderer.on('toggleMenu', toggleMenu);
ipcRenderer.on("refreshMenu", () => { ipcRenderer.on('refreshMenu', () => {
if (visible()) { if (visible()) {
bar.refreshMenu(); bar.refreshMenu();
} }
}); });
if (isEnabled("picture-in-picture")) { if (isEnabled('picture-in-picture')) {
ipcRenderer.on("pip-toggle", (_, pipEnabled) => { ipcRenderer.on('pip-toggle', (_, pipEnabled) => {
bar.refreshMenu(); bar.refreshMenu();
}); });
} }
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
document.addEventListener('apiLoaded', () => { document.addEventListener('apiLoaded', () => {
setNavbarMargin(); setNavbarMargin();
const playPageObserver = new MutationObserver(setNavbarMargin); const playPageObserver = new MutationObserver(setNavbarMargin);
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] }) playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] });
setupSearchOpenObserver(); setupSearchOpenObserver();
setupMenuOpenObserver(); setupMenuOpenObserver();
}, { once: true, passive: true }) }, { once: true, passive: true });
}; };
function setupSearchOpenObserver() { function setupSearchOpenObserver() {
const searchOpenObserver = new MutationObserver(mutations => { const searchOpenObserver = new MutationObserver((mutations) => {
$('#nav-bar-background').style.webkitAppRegion = $('#nav-bar-background').style.webkitAppRegion
mutations[0].target.opened ? 'no-drag' : 'drag'; = mutations[0].target.opened ? 'no-drag' : 'drag';
}); });
searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ["opened"] }) searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ['opened'] });
} }
function setupMenuOpenObserver() { function setupMenuOpenObserver() {
const menuOpenObserver = new MutationObserver(mutations => { const menuOpenObserver = new MutationObserver((mutations) => {
$('#nav-bar-background').style.webkitAppRegion = $('#nav-bar-background').style.webkitAppRegion
Array.from($('.cet-menubar').childNodes).some(c => c.classList.contains('open')) ? = [...$('.cet-menubar').childNodes].some((c) => c.classList.contains('open'))
'no-drag' : 'drag'; ? 'no-drag' : 'drag';
}); });
menuOpenObserver.observe($('.cet-menubar'), { subtree: true, attributeFilter: ["class"] }) menuOpenObserver.observe($('.cet-menubar'), { subtree: true, attributeFilter: ['class'] });
} }
function setNavbarMargin() { function setNavbarMargin() {
$('#nav-bar-background').style.right = $('#nav-bar-background').style.right
$('ytmusic-app-layout').playerPageOpen_ ? = $('ytmusic-app-layout').playerPageOpen_
'0px' : ? '0px'
'12px'; : '12px';
} }

View File

@ -1,105 +1,107 @@
/* increase font size for menu and menuItems */ /* increase font size for menu and menuItems */
.titlebar, .titlebar,
.menubar-menu-container .action-label { .menubar-menu-container .action-label {
font-size: 14px !important; font-size: 14px !important;
} }
/* fixes nav-bar-background opacity bug, reposition it, and allows clicking scrollbar through it */ /* fixes nav-bar-background opacity bug, reposition it, and allows clicking scrollbar through it */
#nav-bar-background { #nav-bar-background {
opacity: 1 !important; opacity: 1 !important;
pointer-events: none !important; pointer-events: none !important;
top: 30px !important; top: 30px !important;
height: 75px !important; height: 75px !important;
} }
/* fix top gap between nav-bar and browse-page */ /* fix top gap between nav-bar and browse-page */
#browse-page { #browse-page {
padding-top: 0 !important; padding-top: 0 !important;
} }
/* fix navbar hiding library items */ /* fix navbar hiding library items */
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"], ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"],
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] { ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] {
top: 50px; top: 50px;
position: relative; position: relative;
} }
/* remove window dragging for nav bar (conflict with titlebar drag) */ /* remove window dragging for nav bar (conflict with titlebar drag) */
ytmusic-nav-bar, ytmusic-nav-bar,
.tab-titleiron-icon, .tab-titleiron-icon,
ytmusic-pivot-bar-item-renderer { ytmusic-pivot-bar-item-renderer {
-webkit-app-region: unset !important; -webkit-app-region: unset !important;
} }
/* move up item selection renderers */ /* move up item selection renderers */
ytmusic-item-section-renderer.stuck #header.ytmusic-item-section-renderer, ytmusic-item-section-renderer.stuck #header.ytmusic-item-section-renderer,
ytmusic-tabs.stuck { ytmusic-tabs.stuck {
top: calc(var(--ytmusic-nav-bar-height) - 15px) !important; top: calc(var(--ytmusic-nav-bar-height) - 15px) !important;
} }
/* fix weird positioning in search screen*/ /* fix weird positioning in search screen*/
ytmusic-header-renderer.ytmusic-search-page { ytmusic-header-renderer.ytmusic-search-page {
position: unset !important; position: unset !important;
} }
/* Move navBar downwards */ /* Move navBar downwards */
ytmusic-nav-bar[slot="nav-bar"] { ytmusic-nav-bar[slot="nav-bar"] {
top: 17px !important; top: 17px !important;
} }
/* fix page progress bar position*/ /* fix page progress bar position*/
yt-page-navigation-progress, yt-page-navigation-progress,
#progress.yt-page-navigation-progress { #progress.yt-page-navigation-progress {
top: 30px !important; top: 30px !important;
} }
/* custom scrollbar */ /* custom scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; width: 12px;
background-color: #030303; background-color: #030303;
border-radius: 100px; border-radius: 100px;
-moz-border-radius: 100px; -moz-border-radius: 100px;
-webkit-border-radius: 100px; -webkit-border-radius: 100px;
} }
/* hover effect for both scrollbar area, and scrollbar 'thumb' */ /* hover effect for both scrollbar area, and scrollbar 'thumb' */
::-webkit-scrollbar:hover { ::-webkit-scrollbar:hover {
background-color: rgba(15, 15, 15, 0.699); background-color: rgba(15, 15, 15, 0.699);
} }
/* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */ /* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */
::-webkit-scrollbar-thumb:vertical { ::-webkit-scrollbar-thumb:vertical {
border: 2px solid rgba(0, 0, 0, 0); border: 2px solid rgba(0, 0, 0, 0);
background: #3a3a3a; background: #3a3a3a;
background-clip: padding-box; background-clip: padding-box;
border-radius: 100px; border-radius: 100px;
-moz-border-radius: 100px; -moz-border-radius: 100px;
-webkit-border-radius: 100px; -webkit-border-radius: 100px;
} }
::-webkit-scrollbar-thumb:vertical:active { ::-webkit-scrollbar-thumb:vertical:active {
background: #4d4c4c; /* some darker color when you click it */ background: #4d4c4c; /* some darker color when you click it */
border-radius: 100px; border-radius: 100px;
-moz-border-radius: 100px; -moz-border-radius: 100px;
-webkit-border-radius: 100px; -webkit-border-radius: 100px;
} }
.cet-menubar-menu-container .cet-action-item { .cet-menubar-menu-container .cet-action-item {
background-color: inherit background-color: inherit
} }
/** hideMenu toggler **/ /** hideMenu toggler **/
.cet-window-icon { .cet-window-icon {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.cet-window-icon img { .cet-window-icon img {
-webkit-user-drag: none; -webkit-user-drag: none;
filter: invert(50%); filter: invert(50%);
} }
/** make navbar draggable **/ /** make navbar draggable **/
#nav-bar-background { #nav-bar-background {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
ytmusic-nav-bar input, ytmusic-nav-bar input,
@ -107,5 +109,5 @@ ytmusic-nav-bar span,
ytmusic-nav-bar [role="button"], ytmusic-nav-bar [role="button"],
ytmusic-nav-bar yt-icon, ytmusic-nav-bar yt-icon,
tp-yt-iron-dropdown { tp-yt-iron-dropdown {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }

View File

@ -1,161 +1,171 @@
const { shell } = require('electron');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const md5 = require('md5'); const md5 = require('md5');
const { shell } = require('electron');
const { setOptions } = require('../../config/plugins'); const { setOptions } = require('../../config/plugins');
const registerCallback = require('../../providers/song-info'); const registerCallback = require('../../providers/song-info');
const defaultConfig = require('../../config/defaults'); const defaultConfig = require('../../config/defaults');
const createFormData = params => { const createFormData = (parameters) => {
// creates the body for in the post request // Creates the body for in the post request
const formData = new URLSearchParams(); const formData = new URLSearchParams();
for (const key in params) { for (const key in parameters) {
formData.append(key, params[key]); formData.append(key, parameters[key]);
} }
return formData;
}
const createQueryString = (params, api_sig) => {
// creates a querystring
const queryData = [];
params.api_sig = api_sig;
for (const key in params) {
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
}
return '?'+queryData.join('&');
}
const createApiSig = (params, secret) => { return formData;
// this function creates the api signature, see: https://www.last.fm/api/authspec };
const keys = [];
for (const key in params) { const createQueryString = (parameters, api_sig) => {
keys.push(key); // Creates a querystring
} const queryData = [];
keys.sort(); parameters.api_sig = api_sig;
let sig = ''; for (const key in parameters) {
for (const key of keys) { queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`);
if (String(key) === 'format') }
continue
sig += `${key}${params[key]}`; return '?' + queryData.join('&');
} };
sig += secret;
sig = md5(sig); const createApiSig = (parameters, secret) => {
return sig; // This function creates the api signature, see: https://www.last.fm/api/authspec
} const keys = [];
for (const key in parameters) {
keys.push(key);
}
keys.sort();
let sig = '';
for (const key of keys) {
if (String(key) === 'format') {
continue;
}
sig += `${key}${parameters[key]}`;
}
sig += secret;
sig = md5(sig);
return sig;
};
const createToken = async ({ api_key, api_root, secret }) => { const createToken = async ({ api_key, api_root, secret }) => {
// creates and stores the auth token // Creates and stores the auth token
const data = { const data = {
method: 'auth.gettoken', method: 'auth.gettoken',
api_key: api_key, api_key,
format: 'json' format: 'json',
}; };
const api_sig = createApiSig(data, secret); const api_sig = createApiSig(data, secret);
let response = await fetch(`${api_root}${createQueryString(data, api_sig)}`); let response = await fetch(`${api_root}${createQueryString(data, api_sig)}`);
response = await response.json(); response = await response.json();
return response?.token; return response?.token;
} };
const authenticate = async config => { const authenticate = async (config) => {
// asks the user for authentication // Asks the user for authentication
config.token = await createToken(config); config.token = await createToken(config);
setOptions('last-fm', config); setOptions('last-fm', config);
shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`); shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
return config; return config;
} };
const getAndSetSessionKey = async config => { const getAndSetSessionKey = async (config) => {
// get and store the session key // Get and store the session key
const data = { const data = {
api_key: config.api_key, api_key: config.api_key,
format: 'json', format: 'json',
method: 'auth.getsession', method: 'auth.getsession',
token: config.token, token: config.token,
}; };
const api_sig = createApiSig(data, config.secret); const api_sig = createApiSig(data, config.secret);
let res = await fetch(`${config.api_root}${createQueryString(data, api_sig)}`); let res = await fetch(`${config.api_root}${createQueryString(data, api_sig)}`);
res = await res.json(); res = await res.json();
if (res.error) if (res.error) {
await authenticate(config); await authenticate(config);
config.session_key = res?.session?.key; }
setOptions('last-fm', config);
return config; config.session_key = res?.session?.key;
} setOptions('last-fm', config);
return config;
};
const postSongDataToAPI = async (songInfo, config, data) => { const postSongDataToAPI = async (songInfo, config, data) => {
// this sends a post request to the api, and adds the common data // This sends a post request to the api, and adds the common data
if (!config.session_key) if (!config.session_key) {
await getAndSetSessionKey(config); await getAndSetSessionKey(config);
}
const postData = { const postData = {
track: songInfo.title, track: songInfo.title,
duration: songInfo.songDuration, duration: songInfo.songDuration,
artist: songInfo.artist, artist: songInfo.artist,
...(songInfo.album ? { album: songInfo.album } : undefined), // will be undefined if current song is a video ...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
api_key: config.api_key, api_key: config.api_key,
sk: config.session_key, sk: config.session_key,
format: 'json', format: 'json',
...data, ...data,
}; };
postData.api_sig = createApiSig(postData, config.secret); postData.api_sig = createApiSig(postData, config.secret);
fetch('https://ws.audioscrobbler.com/2.0/', {method: 'POST', body: createFormData(postData)}) fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: createFormData(postData) })
.catch(res => { .catch((error) => {
if (res.response.data.error == 9) { if (error.response.data.error == 9) {
// session key is invalid, so remove it from the config and reauthenticate // Session key is invalid, so remove it from the config and reauthenticate
config.session_key = undefined; config.session_key = undefined;
setOptions('last-fm', config); setOptions('last-fm', config);
authenticate(config); authenticate(config);
} }
}); });
} };
const addScrobble = (songInfo, config) => { const addScrobble = (songInfo, config) => {
// this adds one scrobbled song to last.fm // This adds one scrobbled song to last.fm
const data = { const data = {
method: 'track.scrobble', method: 'track.scrobble',
timestamp: ~~((Date.now() - songInfo.elapsedSeconds) / 1000), timestamp: Math.trunc((Date.now() - songInfo.elapsedSeconds) / 1000),
}; };
postSongDataToAPI(songInfo, config, data); postSongDataToAPI(songInfo, config, data);
} };
const setNowPlaying = (songInfo, config) => { const setNowPlaying = (songInfo, config) => {
// this sets the now playing status in last.fm // This sets the now playing status in last.fm
const data = { const data = {
method: 'track.updateNowPlaying', method: 'track.updateNowPlaying',
}; };
postSongDataToAPI(songInfo, config, data); postSongDataToAPI(songInfo, config, data);
} };
// This will store the timeout that will trigger addScrobble
// this will store the timeout that will trigger addScrobble let scrobbleTimer;
let scrobbleTimer = undefined;
const lastfm = async (_win, config) => { const lastfm = async (_win, config) => {
if (!config.api_root) { if (!config.api_root) {
// settings are not present, creating them with the default values // Settings are not present, creating them with the default values
config = defaultConfig.plugins['last-fm']; config = defaultConfig.plugins['last-fm'];
config.enabled = true; config.enabled = true;
setOptions('last-fm', config); setOptions('last-fm', config);
} }
if (!config.session_key) { if (!config.session_key) {
// not authenticated // Not authenticated
config = await getAndSetSessionKey(config); config = await getAndSetSessionKey(config);
} }
registerCallback( songInfo => { registerCallback((songInfo) => {
// set remove the old scrobble timer // Set remove the old scrobble timer
clearTimeout(scrobbleTimer); clearTimeout(scrobbleTimer);
if (!songInfo.isPaused) { if (!songInfo.isPaused) {
setNowPlaying(songInfo, config); setNowPlaying(songInfo, config);
// scrobble when the song is half way through, or has passed the 4 minute mark // Scrobble when the song is half way through, or has passed the 4 minute mark
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60); const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
if (scrobbleTime > songInfo.elapsedSeconds) { if (scrobbleTime > songInfo.elapsedSeconds) {
// scrobble still needs to happen // Scrobble still needs to happen
const timeToWait = (scrobbleTime - songInfo.elapsedSeconds) * 1000; const timeToWait = (scrobbleTime - songInfo.elapsedSeconds) * 1000;
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config); scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
} }
} }
}); });
} };
module.exports = lastfm; module.exports = lastfm;

View File

@ -1,117 +1,123 @@
const { join } = require("path"); const { join } = require('node:path');
const { ipcMain } = require("electron"); const { ipcMain } = require('electron');
const is = require("electron-is"); const is = require('electron-is');
const { convert } = require("html-to-text"); const { convert } = require('html-to-text');
const fetch = require("node-fetch"); const fetch = require('node-fetch');
const { cleanupName } = require("../../providers/song-info"); const { cleanupName } = require('../../providers/song-info');
const { injectCSS } = require("../utils"); const { injectCSS } = require('../utils');
let eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
let revRomanized = false; const eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
let revRomanized = false;
module.exports = async (win, options) => { module.exports = async (win, options) => {
if(options.romanizedLyrics) { if (options.romanizedLyrics) {
revRomanized = true; revRomanized = true;
} }
injectCSS(win.webContents, join(__dirname, "style.css"));
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => { injectCSS(win.webContents, join(__dirname, 'style.css'));
const metadata = JSON.parse(extractedSongInfo);
event.returnValue = await fetchFromGenius(metadata); ipcMain.on('search-genius-lyrics', async (event, extractedSongInfo) => {
}); const metadata = JSON.parse(extractedSongInfo);
event.returnValue = await fetchFromGenius(metadata);
});
}; };
const toggleRomanized = () => { const toggleRomanized = () => {
revRomanized = !revRomanized; revRomanized = !revRomanized;
}; };
const fetchFromGenius = async (metadata) => { const fetchFromGenius = async (metadata) => {
const songTitle = `${cleanupName(metadata.title)}`; const songTitle = `${cleanupName(metadata.title)}`;
const songArtist = `${cleanupName(metadata.artist)}`; const songArtist = `${cleanupName(metadata.artist)}`;
let lyrics; let lyrics;
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal /* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
Genius Lyrics behavior is observed. Genius Lyrics behavior is observed.
*/ */
let hasAsianChars = false; let hasAsianChars = false;
if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) { if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) {
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`); lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
hasAsianChars = true; hasAsianChars = true;
} else { } else {
lyrics = await getLyricsList(`${songArtist} ${songTitle}`); lyrics = await getLyricsList(`${songArtist} ${songTitle}`);
} }
/* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check /* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check
for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics. for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics.
*/ */
if(revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) { if (revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) {
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`); lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
} }
return lyrics;
return lyrics;
}; };
/** /**
* Fetches a JSON of songs which is then parsed and passed into getLyrics to get the lyrical content of the first song * Fetches a JSON of songs which is then parsed and passed into getLyrics to get the lyrical content of the first song
* @param {*} queryString * @param {*} queryString
* @returns The lyrics of the first song found using the Genius-Lyrics API * @returns The lyrics of the first song found using the Genius-Lyrics API
*/ */
const getLyricsList = async (queryString) => { const getLyricsList = async (queryString) => {
let response = await fetch( const response = await fetch(
`https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}` `https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`,
); );
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
/* Fetch the first URL with the api, giving a collection of song results. /* Fetch the first URL with the api, giving a collection of song results.
Pick the first song, parsing the json given by the API. Pick the first song, parsing the json given by the API.
*/ */
const info = await response.json(); const info = await response.json();
let url = ""; let url = '';
try { try {
url = info.response.sections.filter((section) => section.type === "song")[0] url = info.response.sections.find((section) => section.type === 'song')
.hits[0].result.url; .hits[0].result.url;
} catch { } catch {
return null; return null;
} }
let lyrics = await getLyrics(url);
return lyrics; const lyrics = await getLyrics(url);
} return lyrics;
};
/** /**
* *
* @param {*} url * @param {*} url
* @returns The lyrics of the song URL provided, null if none * @returns The lyrics of the song URL provided, null if none
*/ */
const getLyrics = async (url) => { const getLyrics = async (url) => {
response = await fetch(url); response = await fetch(url);
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
if (is.dev()) {
console.log("Fetching lyrics from Genius:", url); if (is.dev()) {
} console.log('Fetching lyrics from Genius:', url);
const html = await response.text(); }
const lyrics = convert(html, {
baseElements: { const html = await response.text();
selectors: ['[class^="Lyrics__Container"]', ".lyrics"], const lyrics = convert(html, {
}, baseElements: {
selectors: [ selectors: ['[class^="Lyrics__Container"]', '.lyrics'],
{ },
selector: "a", selectors: [
format: "linkFormatter", {
}, selector: 'a',
], format: 'linkFormatter',
formatters: { },
// Remove links by keeping only the content ],
linkFormatter: (elem, walk, builder) => { formatters: {
walk(elem.children, builder); // Remove links by keeping only the content
}, linkFormatter(element, walk, builder) {
}, walk(element.children, builder);
}); },
return lyrics; },
});
return lyrics;
}; };
module.exports.toggleRomanized = toggleRomanized; module.exports.toggleRomanized = toggleRomanized;
module.exports.fetchFromGenius = fetchFromGenius; module.exports.fetchFromGenius = fetchFromGenius;

View File

@ -1,94 +1,95 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const is = require("electron-is"); const is = require('electron-is');
module.exports = () => { module.exports = () => {
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => setTimeout(() => { ipcRenderer.on('update-song-info', (_, extractedSongInfo) => setTimeout(() => {
const tabList = document.querySelectorAll("tp-yt-paper-tab"); const tabList = document.querySelectorAll('tp-yt-paper-tab');
const tabs = { const tabs = {
upNext: tabList[0], upNext: tabList[0],
lyrics: tabList[1], lyrics: tabList[1],
discover: tabList[2], discover: tabList[2],
} };
// Check if disabled // Check if disabled
if (!tabs.lyrics?.hasAttribute("disabled")) { if (!tabs.lyrics?.hasAttribute('disabled')) {
return; return;
} }
let hasLyrics = true; let hasLyrics = true;
const lyrics = ipcRenderer.sendSync( const lyrics = ipcRenderer.sendSync(
"search-genius-lyrics", 'search-genius-lyrics',
extractedSongInfo extractedSongInfo,
); );
if (!lyrics) { if (!lyrics) {
// Delete previous lyrics if tab is open and couldn't get new lyrics // Delete previous lyrics if tab is open and couldn't get new lyrics
checkLyricsContainer(() => { checkLyricsContainer(() => {
hasLyrics = false; hasLyrics = false;
setTabsOnclick(undefined); setTabsOnclick(undefined);
}); });
return; return;
} }
if (is.dev()) { if (is.dev()) {
console.log("Fetched lyrics from Genius"); console.log('Fetched lyrics from Genius');
} }
enableLyricsTab(); enableLyricsTab();
setTabsOnclick(enableLyricsTab); setTabsOnclick(enableLyricsTab);
checkLyricsContainer(); checkLyricsContainer();
tabs.lyrics.onclick = () => { tabs.lyrics.addEventListener('click', () => {
const tabContainer = document.querySelector("ytmusic-tab-renderer"); const tabContainer = document.querySelector('ytmusic-tab-renderer');
const observer = new MutationObserver((_, observer) => { const observer = new MutationObserver((_, observer) => {
checkLyricsContainer(() => observer.disconnect()); checkLyricsContainer(() => observer.disconnect());
}); });
observer.observe(tabContainer, { observer.observe(tabContainer, {
attributes: true, attributes: true,
childList: true, childList: true,
subtree: true, subtree: true,
}); });
}; });
function checkLyricsContainer(callback = () => {}) { function checkLyricsContainer(callback = () => {
const lyricsContainer = document.querySelector( }) {
'[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer' const lyricsContainer = document.querySelector(
); '[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer',
if (lyricsContainer) { );
callback(); if (lyricsContainer) {
setLyrics(lyricsContainer) callback();
} setLyrics(lyricsContainer);
} }
}
function setLyrics(lyricsContainer) { function setLyrics(lyricsContainer) {
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics"> lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
${ ${
hasLyrics hasLyrics
? lyrics.replace(/(?:\r\n|\r|\n)/g, "<br/>") ? lyrics.replaceAll(/\r\n|\r|\n/g, '<br/>')
: "Could not retrieve lyrics from genius" : 'Could not retrieve lyrics from genius'
} }
</div> </div>
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`; <yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
if (hasLyrics) { if (hasLyrics) {
lyricsContainer.querySelector('.footer').textContent = 'Source: Genius'; lyricsContainer.querySelector('.footer').textContent = 'Source: Genius';
enableLyricsTab(); enableLyricsTab();
} }
} }
function setTabsOnclick(callback) { function setTabsOnclick(callback) {
for (const tab of [tabs.upNext, tabs.discover]) { for (const tab of [tabs.upNext, tabs.discover]) {
if (tab) { if (tab) {
tab.onclick = callback; tab.addEventListener('click', callback);
} }
} }
} }
function enableLyricsTab() { function enableLyricsTab() {
tabs.lyrics.removeAttribute("disabled"); tabs.lyrics.removeAttribute('disabled');
tabs.lyrics.removeAttribute("aria-disabled"); tabs.lyrics.removeAttribute('aria-disabled');
} }
}, 500)); }, 500));
}; };

View File

@ -1,17 +1,16 @@
const { setOptions } = require("../../config/plugins"); const { toggleRomanized } = require('./back');
const { toggleRomanized } = require("./back");
module.exports = (win, options, refreshMenu) => { const { setOptions } = require('../../config/plugins');
return [
{ module.exports = (win, options, refreshMenu) => [
label: "Romanized Lyrics", {
type: "checkbox", label: 'Romanized Lyrics',
checked: options.romanizedLyrics, type: 'checkbox',
click: (item) => { checked: options.romanizedLyrics,
options.romanizedLyrics = item.checked; click(item) {
setOptions('lyrics-genius', options); options.romanizedLyrics = item.checked;
toggleRomanized(); setOptions('lyrics-genius', options);
}, toggleRomanized();
}, },
]; },
}; ];

View File

@ -1,12 +1,12 @@
/* Disable links in Genius lyrics */ /* Disable links in Genius lyrics */
.genius-lyrics a { .genius-lyrics a {
color: var(--ytmusic-text-primary); color: var(--ytmusic-text-primary);
display: inline-block; display: inline-block;
pointer-events: none; pointer-events: none;
text-decoration: none; text-decoration: none;
} }
.description { .description {
font-size: clamp(1.4rem, 1.1vmax, 3rem) !important; font-size: clamp(1.4rem, 1.1vmax, 3rem) !important;
text-align: center !important; text-align: center !important;
} }

View File

@ -1,24 +1,24 @@
const { triggerAction } = require("../utils"); const { triggerAction } = require('../utils');
const CHANNEL = "navigation"; const CHANNEL = 'navigation';
const ACTIONS = { const ACTIONS = {
NEXT: "next", NEXT: 'next',
BACK: "back", BACK: 'back',
}; };
function goToNextPage() { function goToNextPage() {
triggerAction(CHANNEL, ACTIONS.NEXT); triggerAction(CHANNEL, ACTIONS.NEXT);
} }
function goToPreviousPage() { function goToPreviousPage() {
triggerAction(CHANNEL, ACTIONS.BACK); triggerAction(CHANNEL, ACTIONS.BACK);
} }
module.exports = { module.exports = {
CHANNEL: CHANNEL, CHANNEL,
ACTIONS: ACTIONS, ACTIONS,
actions: { actions: {
goToNextPage: goToNextPage, goToNextPage,
goToPreviousPage: goToPreviousPage, goToPreviousPage,
}, },
}; };

View File

@ -1,29 +1,37 @@
const path = require("path"); const path = require('node:path');
const { injectCSS, listenAction } = require("../utils"); const { ACTIONS, CHANNEL } = require('./actions.js');
const { ACTIONS, CHANNEL } = require("./actions.js");
const { injectCSS, listenAction } = require('../utils');
function handle(win) { function handle(win) {
injectCSS(win.webContents, path.join(__dirname, "style.css"), () => { injectCSS(win.webContents, path.join(__dirname, 'style.css'), () => {
win.webContents.send("navigation-css-ready"); win.webContents.send('navigation-css-ready');
}); });
listenAction(CHANNEL, (event, action) => { listenAction(CHANNEL, (event, action) => {
switch (action) { switch (action) {
case ACTIONS.NEXT: case ACTIONS.NEXT: {
if (win.webContents.canGoForward()) { if (win.webContents.canGoForward()) {
win.webContents.goForward(); win.webContents.goForward();
} }
break;
case ACTIONS.BACK: break;
if (win.webContents.canGoBack()) { }
win.webContents.goBack();
} case ACTIONS.BACK: {
break; if (win.webContents.canGoBack()) {
default: win.webContents.goBack();
console.log("Unknown action: " + action); }
}
}); break;
}
default: {
console.log('Unknown action: ' + action);
}
}
});
} }
module.exports = handle; module.exports = handle;

View File

@ -1,19 +1,19 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const { ElementFromFile, templatePath } = require("../utils"); const { ElementFromFile, templatePath } = require('../utils');
function run() { function run() {
ipcRenderer.on("navigation-css-ready", () => { ipcRenderer.on('navigation-css-ready', () => {
const forwardButton = ElementFromFile( const forwardButton = ElementFromFile(
templatePath(__dirname, "forward.html") templatePath(__dirname, 'forward.html'),
); );
const backButton = ElementFromFile(templatePath(__dirname, "back.html")); const backButton = ElementFromFile(templatePath(__dirname, 'back.html'));
const menu = document.querySelector("#right-content"); const menu = document.querySelector('#right-content');
if (menu) { if (menu) {
menu.prepend(backButton, forwardButton); menu.prepend(backButton, forwardButton);
} }
}); });
} }
module.exports = run; module.exports = run;

View File

@ -1,35 +1,35 @@
.navigation-item { .navigation-item {
font-family: Roboto, Noto Naskh Arabic UI, Arial, sans-serif; font-family: Roboto, Noto Naskh Arabic UI, Arial, sans-serif;
font-size: 20px; font-size: 20px;
line-height: var(--ytmusic-title-1_-_line-height); line-height: var(--ytmusic-title-1_-_line-height);
font-weight: 500; font-weight: 500;
--yt-endpoint-color: #fff; --yt-endpoint-color: #fff;
--yt-endpoint-hover-color: #fff; --yt-endpoint-hover-color: #fff;
--yt-endpoint-visited-color: #fff; --yt-endpoint-visited-color: #fff;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
cursor: pointer; cursor: pointer;
margin: 0 var(--ytd-rich-grid-item-margin); margin: 0 var(--ytd-rich-grid-item-margin);
} }
.navigation-item:hover { .navigation-item:hover {
color: #fff; color: #fff;
} }
.navigation-icon { .navigation-icon {
display: inline-flex; display: inline-flex;
-ms-flex-align: center; -ms-flex-align: center;
-webkit-align-items: center; -webkit-align-items: center;
align-items: center; align-items: center;
-ms-flex-pack: center; -ms-flex-pack: center;
-webkit-justify-content: center; -webkit-justify-content: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
vertical-align: middle; vertical-align: middle;
fill: var(--iron-icon-fill-color, currentcolor); fill: var(--iron-icon-fill-color, currentcolor);
stroke: none; stroke: none;
width: var(--iron-icon-width, 24px); width: var(--iron-icon-width, 24px);
height: var(--iron-icon-height, 24px); height: var(--iron-icon-height, 24px);
animation: var(--iron-icon_-_animation); animation: var(--iron-icon_-_animation);
} }

View File

@ -1,33 +1,33 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
tab-id="FEmusic_back" onclick="goToPreviousPage()"
role="tab" role="tab"
onclick="goToPreviousPage()" tab-id="FEmusic_back"
> >
<div <div
class="search-icon style-scope ytmusic-search-box" aria-disabled="false"
role="button" class="search-icon style-scope ytmusic-search-box"
tabindex="0" role="button"
aria-disabled="false" tabindex="0"
title="Go to previous page" title="Go to previous page"
> >
<div <div
id="icon" class="tab-icon style-scope paper-icon-button navigation-icon"
class="tab-icon style-scope paper-icon-button navigation-icon" id="icon"
> >
<svg <svg
viewBox="0 0 492 492" class="style-scope iron-icon"
preserveAspectRatio="xMidYMid meet" focusable="false"
focusable="false" preserveAspectRatio="xMidYMid meet"
class="style-scope iron-icon" style="pointer-events: none; display: block; width: 100%; height: 100%"
style="pointer-events: none; display: block; width: 100%; height: 100%" viewBox="0 0 492 492"
> >
<g class="style-scope iron-icon"> <g class="style-scope iron-icon">
<path <path
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z" d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
></path> ></path>
</g> </g>
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,35 +1,35 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
tab-id="FEmusic_next" onclick="goToNextPage()"
role="tab" role="tab"
onclick="goToNextPage()" tab-id="FEmusic_next"
> >
<div <div
class="search-icon style-scope ytmusic-search-box" aria-disabled="false"
role="button" class="search-icon style-scope ytmusic-search-box"
tabindex="0" role="button"
aria-disabled="false" tabindex="0"
title="Go to next page" title="Go to next page"
> >
<div <div
id="icon" class="tab-icon style-scope paper-icon-button navigation-icon"
class="tab-icon style-scope paper-icon-button navigation-icon" id="icon"
> >
<svg <svg
viewBox="0 0 492 492" class="style-scope iron-icon"
preserveAspectRatio="xMidYMid meet" focusable="false"
focusable="false" preserveAspectRatio="xMidYMid meet"
class="style-scope iron-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;"
style="pointer-events: none; display: block; width: 100%; height: 100%;" viewBox="0 0 492 492"
> >
<g class="style-scope iron-icon"> <g class="style-scope iron-icon">
<path <path
d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1 d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1
l183.9,183.9L109.3,430c-5.1,5.1-7.9,11.8-7.9,19c0,7.2,2.8,14,7.9,19l16.1,16.1c5.1,5.1,11.8,7.9,19,7.9s14-2.8,19-7.9L382.7,265 l183.9,183.9L109.3,430c-5.1,5.1-7.9,11.8-7.9,19c0,7.2,2.8,14,7.9,19l16.1,16.1c5.1,5.1,11.8,7.9,19,7.9s14-2.8,19-7.9L382.7,265
c5.1-5.1,7.9-11.9,7.8-19.1C390.5,238.7,387.8,231.9,382.7,226.8z" c5.1-5.1,7.9-11.9,7.8-19.1C390.5,238.7,387.8,231.9,382.7,226.8z"
></path> ></path>
</g> </g>
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
const { injectCSS } = require("../utils"); const path = require('node:path');
const path = require("path");
module.exports = win => { const { injectCSS } = require('../utils');
injectCSS(win.webContents, path.join(__dirname, "style.css"));
module.exports = (win) => {
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
}; };

View File

@ -1,37 +1,37 @@
function removeLoginElements() { function removeLoginElements() {
const elementsToRemove = [ const elementsToRemove = [
".sign-in-link.ytmusic-nav-bar", '.sign-in-link.ytmusic-nav-bar',
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]', '.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]',
]; ];
elementsToRemove.forEach((selector) => { for (const selector of elementsToRemove) {
const node = document.querySelector(selector); const node = document.querySelector(selector);
if (node) { if (node) {
node.remove(); node.remove();
} }
}); }
// Remove the library button // Remove the library button
const libraryIconPath = const libraryIconPath
"M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z"; = 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
menuEntries = document.querySelectorAll( menuEntries = document.querySelectorAll(
"#items ytmusic-guide-entry-renderer" '#items ytmusic-guide-entry-renderer',
); );
menuEntries.forEach((item) => { for (const item of menuEntries) {
const icon = item.querySelector("path"); const icon = item.querySelector('path');
if (icon) { if (icon) {
observer.disconnect(); observer.disconnect();
if (icon.getAttribute("d") === libraryIconPath) { if (icon.getAttribute('d') === libraryIconPath) {
item.remove(); item.remove();
} }
} }
}); }
}); });
observer.observe(document.documentElement, { observer.observe(document.documentElement, {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
} }
module.exports = removeLoginElements; module.exports = removeLoginElements;

View File

@ -2,5 +2,5 @@
ytmusic-guide-signin-promo-renderer, ytmusic-guide-signin-promo-renderer,
a[href="/music_premium"], a[href="/music_premium"],
.sign-in-link { .sign-in-link {
display: none !important; display: none !important;
} }

View File

@ -1,46 +1,49 @@
const { Notification } = require("electron"); const { Notification } = require('electron');
const is = require("electron-is"); const is = require('electron-is');
const registerCallback = require("../../providers/song-info");
const { notificationImage } = require("./utils"); const { notificationImage } = require('./utils');
const config = require("./config"); const config = require('./config');
const registerCallback = require('../../providers/song-info');
const notify = (info) => { const notify = (info) => {
// Fill the notification with content
const notification = {
title: info.title || 'Playing',
body: info.artist,
icon: notificationImage(info),
silent: true,
urgency: config.get('urgency'),
};
// Fill the notification with content // Send the notification
const notification = { const currentNotification = new Notification(notification);
title: info.title || "Playing", currentNotification.show();
body: info.artist,
icon: notificationImage(info),
silent: true,
urgency: config.get('urgency'),
};
// Send the notification return currentNotification;
const currentNotification = new Notification(notification);
currentNotification.show()
return currentNotification;
}; };
const setup = () => { const setup = () => {
let oldNotification; let oldNotification;
let currentUrl; let currentUrl;
registerCallback(songInfo => { registerCallback((songInfo) => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) { if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) {
// Close the old notification // Close the old notification
oldNotification?.close(); oldNotification?.close();
currentUrl = songInfo.url; currentUrl = songInfo.url;
// This fixes a weird bug that would cause the notification to be updated instead of showing // This fixes a weird bug that would cause the notification to be updated instead of showing
setTimeout(() => { oldNotification = notify(songInfo) }, 10); setTimeout(() => {
} oldNotification = notify(songInfo);
}); }, 10);
} }
});
};
/** @param {Electron.BrowserWindow} win */ /** @param {Electron.BrowserWindow} win */
module.exports = (win, options) => { module.exports = (win, options) => {
// Register the callback for new song information // Register the callback for new song information
is.windows() && options.interactive ? is.windows() && options.interactive
require("./interactive")(win) : ? require('./interactive')(win)
setup(); : setup();
}; };

View File

@ -1,5 +1,5 @@
const { PluginConfig } = require("../../config/dynamic"); const { PluginConfig } = require('../../config/dynamic');
const config = new PluginConfig("notifications"); const config = new PluginConfig('notifications');
module.exports = { ...config }; module.exports = { ...config };

View File

@ -1,151 +1,170 @@
const { notificationImage, icons, save_temp_icons, secondsToMinutes, ToastStyles } = require("./utils"); const path = require('node:path');
const { Notification, app, ipcMain } = require('electron');
const { notificationImage, icons, save_temp_icons, secondsToMinutes, ToastStyles } = require('./utils');
const config = require('./config');
const getSongControls = require('../../providers/song-controls'); const getSongControls = require('../../providers/song-controls');
const registerCallback = require("../../providers/song-info"); const registerCallback = require('../../providers/song-info');
const { changeProtocolHandler } = require("../../providers/protocol-handler"); const { changeProtocolHandler } = require('../../providers/protocol-handler');
const { setTrayOnClick, setTrayOnDoubleClick } = require("../../tray"); const { setTrayOnClick, setTrayOnDoubleClick } = require('../../tray');
const { Notification, app, ipcMain } = require("electron");
const path = require('path');
const config = require("./config");
let songControls; let songControls;
let savedNotification; let savedNotification;
/** @param {Electron.BrowserWindow} win */ /** @param {Electron.BrowserWindow} win */
module.exports = (win) => { module.exports = (win) => {
songControls = getSongControls(win); songControls = getSongControls(win);
let currentSeconds = 0; let currentSeconds = 0;
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener')); ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
ipcMain.on('timeChanged', (_, t) => currentSeconds = t); ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
if (app.isPackaged) save_temp_icons(); if (app.isPackaged) {
save_temp_icons();
}
let savedSongInfo; let savedSongInfo;
let lastUrl; let lastUrl;
// Register songInfoCallback // Register songInfoCallback
registerCallback(songInfo => { registerCallback((songInfo) => {
if (!songInfo.artist && !songInfo.title) return; if (!songInfo.artist && !songInfo.title) {
savedSongInfo = { ...songInfo }; return;
if (!songInfo.isPaused &&
(songInfo.url !== lastUrl || config.get("unpauseNotification"))
) {
lastUrl = songInfo.url
sendNotification(songInfo);
}
});
if (config.get("trayControls")) {
setTrayOnClick(() => {
if (savedNotification) {
savedNotification.close();
savedNotification = undefined;
} else if (savedSongInfo) {
sendNotification({
...savedSongInfo,
elapsedSeconds: currentSeconds
})
}
});
setTrayOnDoubleClick(() => {
if (win.isVisible()) {
win.hide();
} else win.show();
})
} }
savedSongInfo = { ...songInfo };
if (!songInfo.isPaused
&& (songInfo.url !== lastUrl || config.get('unpauseNotification'))
) {
lastUrl = songInfo.url;
sendNotification(songInfo);
}
});
app.once("before-quit", () => { if (config.get('trayControls')) {
savedNotification?.close(); setTrayOnClick(() => {
if (savedNotification) {
savedNotification.close();
savedNotification = undefined;
} else if (savedSongInfo) {
sendNotification({
...savedSongInfo,
elapsedSeconds: currentSeconds,
});
}
}); });
setTrayOnDoubleClick(() => {
if (win.isVisible()) {
win.hide();
} else {
win.show();
}
});
}
changeProtocolHandler( app.once('before-quit', () => {
(cmd) => { savedNotification?.close();
if (Object.keys(songControls).includes(cmd)) { });
songControls[cmd]();
if (config.get("refreshOnPlayPause") && ( changeProtocolHandler(
cmd === 'pause' || (cmd) => {
(cmd === 'play' && !config.get("unpauseNotification")) if (Object.keys(songControls).includes(cmd)) {
) songControls[cmd]();
) { if (config.get('refreshOnPlayPause') && (
setImmediate(() => cmd === 'pause'
sendNotification({ || (cmd === 'play' && !config.get('unpauseNotification'))
...savedSongInfo, )
isPaused: cmd === 'pause', ) {
elapsedSeconds: currentSeconds setImmediate(() =>
}) sendNotification({
); ...savedSongInfo,
} isPaused: cmd === 'pause',
} elapsedSeconds: currentSeconds,
}),
);
} }
) }
} },
);
};
function sendNotification(songInfo) { function sendNotification(songInfo) {
const iconSrc = notificationImage(songInfo); const iconSrc = notificationImage(songInfo);
savedNotification?.close(); savedNotification?.close();
savedNotification = new Notification({ savedNotification = new Notification({
title: songInfo.title || "Playing", title: songInfo.title || 'Playing',
body: songInfo.artist, body: songInfo.artist,
icon: iconSrc, icon: iconSrc,
silent: true, silent: true,
// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root // https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype // https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
toastXml: get_xml(songInfo, iconSrc), toastXml: get_xml(songInfo, iconSrc),
}); });
savedNotification.on("close", (_) => { savedNotification.on('close', (_) => {
savedNotification = undefined; savedNotification = undefined;
}); });
savedNotification.show(); savedNotification.show();
} }
const get_xml = (songInfo, iconSrc) => { const get_xml = (songInfo, iconSrc) => {
switch (config.get("toastStyle")) { switch (config.get('toastStyle')) {
default: default:
case ToastStyles.logo: case ToastStyles.logo:
case ToastStyles.legacy: case ToastStyles.legacy: {
return xml_logo(songInfo, iconSrc); return xml_logo(songInfo, iconSrc);
case ToastStyles.banner_top_custom: }
return xml_banner_top_custom(songInfo, iconSrc);
case ToastStyles.hero:
return xml_hero(songInfo, iconSrc);
case ToastStyles.banner_bottom:
return xml_banner_bottom(songInfo, iconSrc);
case ToastStyles.banner_centered_bottom:
return xml_banner_centered_bottom(songInfo, iconSrc);
case ToastStyles.banner_centered_top:
return xml_banner_centered_top(songInfo, iconSrc);
};
}
const iconLocation = app.isPackaged ? case ToastStyles.banner_top_custom: {
path.resolve(app.getPath("userData"), 'icons') : return xml_banner_top_custom(songInfo, iconSrc);
path.resolve(__dirname, '..', '..', 'assets/media-icons-black'); }
case ToastStyles.hero: {
return xml_hero(songInfo, iconSrc);
}
case ToastStyles.banner_bottom: {
return xml_banner_bottom(songInfo, iconSrc);
}
case ToastStyles.banner_centered_bottom: {
return xml_banner_centered_bottom(songInfo, iconSrc);
}
case ToastStyles.banner_centered_top: {
return xml_banner_centered_top(songInfo, iconSrc);
}
}
};
const iconLocation = app.isPackaged
? path.resolve(app.getPath('userData'), 'icons')
: path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
const display = (kind) => { const display = (kind) => {
if (config.get("toastStyle") === ToastStyles.legacy) { if (config.get('toastStyle') === ToastStyles.legacy) {
return `content="${icons[kind]}"`; return `content="${icons[kind]}"`;
} else { }
return `\
content="${config.get("hideButtonText") ? "" : kind.charAt(0).toUpperCase() + kind.slice(1)}"\ return `\
content="${config.get('hideButtonText') ? '' : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}" imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"
`; `;
} };
}
const getButton = (kind) => const getButton = (kind) =>
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`; `<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
const getButtons = (isPaused) => `\ const getButtons = (isPaused) => `\
<actions> <actions>
@ -173,7 +192,6 @@ const xml_image = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\
<text id="2">${artist}</text>\ <text id="2">${artist}</text>\
`, isPaused); `, isPaused);
const xml_logo = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="appLogoOverride"'); const xml_logo = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="appLogoOverride"');
const xml_hero = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="hero"'); const xml_hero = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="hero"');
@ -194,8 +212,8 @@ const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\
const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\ const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\
<subgroup hint-textStacking="bottom"> <subgroup hint-textStacking="bottom">
${album ? ${album
`<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''} ? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds)} / ${secondsToMinutes(songDuration)}</text> <text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds)} / ${secondsToMinutes(songDuration)}</text>
</subgroup>\ </subgroup>\
`; `;
@ -223,13 +241,17 @@ const xml_banner_centered_top = ({ title, artist, isPaused }, imgSrc) => toast(`
`, isPaused); `, isPaused);
const titleFontPicker = (title) => { const titleFontPicker = (title) => {
if (title.length <= 13) { if (title.length <= 13) {
return 'Header'; return 'Header';
} else if (title.length <= 22) { }
return 'Subheader';
} else if (title.length <= 26) { if (title.length <= 22) {
return 'Title'; return 'Subheader';
} else { }
return 'Subtitle';
} if (title.length <= 26) {
} return 'Title';
}
return 'Subtitle';
};

View File

@ -1,80 +1,81 @@
const { urgencyLevels, ToastStyles, snakeToCamel } = require("./utils"); const is = require('electron-is');
const is = require("electron-is");
const config = require("./config"); const { urgencyLevels, ToastStyles, snakeToCamel } = require('./utils');
const config = require('./config');
module.exports = (_win, options) => [ module.exports = (_win, options) => [
...(is.linux() ...(is.linux()
? [ ? [
{ {
label: "Notification Priority", label: 'Notification Priority',
submenu: urgencyLevels.map((level) => ({ submenu: urgencyLevels.map((level) => ({
label: level.name, label: level.name,
type: "radio", type: 'radio',
checked: options.urgency === level.value, checked: options.urgency === level.value,
click: () => config.set("urgency", level.value), click: () => config.set('urgency', level.value),
})), })),
}, },
] ]
: []), : []),
...(is.windows() ...(is.windows()
? [ ? [
{ {
label: "Interactive Notifications", label: 'Interactive Notifications',
type: "checkbox", type: 'checkbox',
checked: options.interactive, checked: options.interactive,
// doesn't update until restart // Doesn't update until restart
click: (item) => config.setAndMaybeRestart("interactive", item.checked), click: (item) => config.setAndMaybeRestart('interactive', item.checked),
}, },
{ {
// submenu with settings for interactive notifications (name shouldn't be too long) // Submenu with settings for interactive notifications (name shouldn't be too long)
label: "Interactive Settings", label: 'Interactive Settings',
submenu: [ submenu: [
{ {
label: "Open/Close on tray click", label: 'Open/Close on tray click',
type: "checkbox", type: 'checkbox',
checked: options.trayControls, checked: options.trayControls,
click: (item) => config.set("trayControls", item.checked), click: (item) => config.set('trayControls', item.checked),
}, },
{ {
label: "Hide Button Text", label: 'Hide Button Text',
type: "checkbox", type: 'checkbox',
checked: options.hideButtonText, checked: options.hideButtonText,
click: (item) => config.set("hideButtonText", item.checked), click: (item) => config.set('hideButtonText', item.checked),
}, },
{ {
label: "Refresh on Play/Pause", label: 'Refresh on Play/Pause',
type: "checkbox", type: 'checkbox',
checked: options.refreshOnPlayPause, checked: options.refreshOnPlayPause,
click: (item) => config.set("refreshOnPlayPause", item.checked), click: (item) => config.set('refreshOnPlayPause', item.checked),
} },
] ],
}, },
{ {
label: "Style", label: 'Style',
submenu: getToastStyleMenuItems(options) submenu: getToastStyleMenuItems(options),
}, },
] ]
: []), : []),
{ {
label: "Show notification on unpause", label: 'Show notification on unpause',
type: "checkbox", type: 'checkbox',
checked: options.unpauseNotification, checked: options.unpauseNotification,
click: (item) => config.set("unpauseNotification", item.checked), click: (item) => config.set('unpauseNotification', item.checked),
}, },
]; ];
function getToastStyleMenuItems(options) { function getToastStyleMenuItems(options) {
const arr = new Array(Object.keys(ToastStyles).length); const array = Array.from({ length: Object.keys(ToastStyles).length });
// ToastStyles index starts from 1 // ToastStyles index starts from 1
for (const [name, index] of Object.entries(ToastStyles)) { for (const [name, index] of Object.entries(ToastStyles)) {
arr[index - 1] = { array[index - 1] = {
label: snakeToCamel(name), label: snakeToCamel(name),
type: "radio", type: 'radio',
checked: options.toastStyle === index, checked: options.toastStyle === index,
click: () => config.set("toastStyle", index), click: () => config.set('toastStyle', index),
}; };
} }
return arr; return array;
} }

View File

@ -1,93 +1,107 @@
const path = require("path"); const path = require('node:path');
const { app } = require("electron");
const fs = require("fs");
const config = require("./config");
const icon = "assets/youtube-music.png"; const fs = require('node:fs');
const userData = app.getPath("userData");
const tempIcon = path.join(userData, "tempIcon.png");
const tempBanner = path.join(userData, "tempBanner.png");
const { cache } = require("../../providers/decorators") const { app } = require('electron');
const config = require('./config');
const icon = 'assets/youtube-music.png';
const userData = app.getPath('userData');
const temporaryIcon = path.join(userData, 'tempIcon.png');
const temporaryBanner = path.join(userData, 'tempBanner.png');
const { cache } = require('../../providers/decorators');
module.exports.ToastStyles = { module.exports.ToastStyles = {
logo: 1, logo: 1,
banner_centered_top: 2, banner_centered_top: 2,
hero: 3, hero: 3,
banner_top_custom: 4, banner_top_custom: 4,
banner_centered_bottom: 5, banner_centered_bottom: 5,
banner_bottom: 6, banner_bottom: 6,
legacy: 7 legacy: 7,
} };
module.exports.icons = { module.exports.icons = {
play: "\u{1405}", // ᐅ play: '\u{1405}', // ᐅ
pause: "\u{2016}", // ‖ pause: '\u{2016}', // ‖
next: "\u{1433}", // next: '\u{1433}', //
previous: "\u{1438}" // previous: '\u{1438}', //
} };
module.exports.urgencyLevels = [ module.exports.urgencyLevels = [
{ name: "Low", value: "low" }, { name: 'Low', value: 'low' },
{ name: "Normal", value: "normal" }, { name: 'Normal', value: 'normal' },
{ name: "High", value: "critical" }, { name: 'High', value: 'critical' },
]; ];
const nativeImageToLogo = cache((nativeImage) => { const nativeImageToLogo = cache((nativeImage) => {
const tempImage = nativeImage.resize({ height: 256 }); const temporaryImage = nativeImage.resize({ height: 256 });
const margin = Math.max(tempImage.getSize().width - 256, 0); const margin = Math.max(temporaryImage.getSize().width - 256, 0);
return tempImage.crop({ return temporaryImage.crop({
x: Math.round(margin / 2), x: Math.round(margin / 2),
y: 0, y: 0,
width: 256, width: 256,
height: 256, height: 256,
}); });
}); });
module.exports.notificationImage = (songInfo) => { module.exports.notificationImage = (songInfo) => {
if (!songInfo.image) return icon; if (!songInfo.image) {
if (!config.get("interactive")) return nativeImageToLogo(songInfo.image); return icon;
}
switch (config.get("toastStyle")) { if (!config.get('interactive')) {
case module.exports.ToastStyles.logo: return nativeImageToLogo(songInfo.image);
case module.exports.ToastStyles.legacy: }
return this.saveImage(nativeImageToLogo(songInfo.image), tempIcon);
default: switch (config.get('toastStyle')) {
return this.saveImage(songInfo.image, tempBanner); case module.exports.ToastStyles.logo:
}; case module.exports.ToastStyles.legacy: {
return this.saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
}
default: {
return this.saveImage(songInfo.image, temporaryBanner);
}
}
}; };
module.exports.saveImage = cache((img, save_path) => { module.exports.saveImage = cache((img, save_path) => {
try { try {
fs.writeFileSync(save_path, img.toPNG()); fs.writeFileSync(save_path, img.toPNG());
} catch (err) { } catch (error) {
console.log(`Error writing song icon to disk:\n${err.toString()}`) console.log(`Error writing song icon to disk:\n${error.toString()}`);
return icon; return icon;
} }
return save_path;
return save_path;
}); });
module.exports.save_temp_icons = () => { module.exports.save_temp_icons = () => {
for (const kind of Object.keys(module.exports.icons)) { for (const kind of Object.keys(module.exports.icons)) {
const destinationPath = path.join(userData, 'icons', `${kind}.png`); const destinationPath = path.join(userData, 'icons', `${kind}.png`);
if (fs.existsSync(destinationPath)) continue; if (fs.existsSync(destinationPath)) {
const iconPath = path.resolve(__dirname, "../../assets/media-icons-black", `${kind}.png`); continue;
fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); }
fs.copyFile(iconPath, destinationPath, () => { });
} const iconPath = path.resolve(__dirname, '../../assets/media-icons-black', `${kind}.png`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFile(iconPath, destinationPath, () => {
});
}
}; };
module.exports.snakeToCamel = (str) => { module.exports.snakeToCamel = (string_) => string_.replaceAll(/([-_][a-z]|^[a-z])/g, (group) =>
return str.replace(/([-_][a-z]|^[a-z])/g, (group) => group.toUpperCase()
group.toUpperCase() .replace('-', ' ')
.replace('-', ' ') .replace('_', ' '),
.replace('_', ' ') );
);
}
module.exports.secondsToMinutes = (seconds) => { module.exports.secondsToMinutes = (seconds) => {
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const secondsLeft = seconds % 60; const secondsLeft = seconds % 60;
return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`; return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;
} };

View File

@ -1,10 +1,10 @@
const path = require("path"); const path = require('node:path');
const { app, ipcMain } = require("electron"); const { app, ipcMain } = require('electron');
const electronLocalshortcut = require("electron-localshortcut"); const electronLocalshortcut = require('electron-localshortcut');
const { setOptions } = require("../../config/plugins"); const { setOptions } = require('../../config/plugins');
const { injectCSS } = require("../utils"); const { injectCSS } = require('../utils');
let isInPiP = false; let isInPiP = false;
let originalPosition; let originalPosition;
@ -15,83 +15,93 @@ let originalMaximized;
let win; let win;
let options; let options;
const pipPosition = () => (options.savePosition && options["pip-position"]) || [10, 10]; const pipPosition = () => (options.savePosition && options['pip-position']) || [10, 10];
const pipSize = () => (options.saveSize && options["pip-size"]) || [450, 275]; const pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
const setLocalOptions = (_options) => { const setLocalOptions = (_options) => {
options = { ...options, ..._options }; options = { ...options, ..._options };
setOptions("picture-in-picture", _options); setOptions('picture-in-picture', _options);
} };
const togglePiP = async () => { const togglePiP = async () => {
isInPiP = !isInPiP; isInPiP = !isInPiP;
setLocalOptions({ isInPiP }); setLocalOptions({ isInPiP });
if (isInPiP) { if (isInPiP) {
originalFullScreen = win.isFullScreen(); originalFullScreen = win.isFullScreen();
if (originalFullScreen) win.setFullScreen(false); if (originalFullScreen) {
originalMaximized = win.isMaximized(); win.setFullScreen(false);
if (originalMaximized) win.unmaximize(); }
originalPosition = win.getPosition();
originalSize = win.getSize();
win.webContents.on("before-input-event", blockShortcutsInPiP); originalMaximized = win.isMaximized();
if (originalMaximized) {
win.unmaximize();
}
win.setMaximizable(false); originalPosition = win.getPosition();
win.setFullScreenable(false); originalSize = win.getSize();
win.webContents.send("pip-toggle", true); win.webContents.on('before-input-event', blockShortcutsInPiP);
app.dock?.hide(); win.setMaximizable(false);
win.setVisibleOnAllWorkspaces(true, { win.setFullScreenable(false);
visibleOnFullScreen: true,
});
app.dock?.show();
if (options.alwaysOnTop) {
win.setAlwaysOnTop(true, "screen-saver", 1);
}
} else {
win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
win.setMaximizable(true);
win.setFullScreenable(true);
win.webContents.send("pip-toggle", false); win.webContents.send('pip-toggle', true);
win.setVisibleOnAllWorkspaces(false); app.dock?.hide();
win.setAlwaysOnTop(false); win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
});
app.dock?.show();
if (options.alwaysOnTop) {
win.setAlwaysOnTop(true, 'screen-saver', 1);
}
} else {
win.webContents.removeListener('before-input-event', blockShortcutsInPiP);
win.setMaximizable(true);
win.setFullScreenable(true);
if (originalFullScreen) win.setFullScreen(true); win.webContents.send('pip-toggle', false);
if (originalMaximized) win.maximize();
}
const [x, y] = isInPiP ? pipPosition() : originalPosition; win.setVisibleOnAllWorkspaces(false);
const [w, h] = isInPiP ? pipSize() : originalSize; win.setAlwaysOnTop(false);
win.setPosition(x, y);
win.setSize(w, h);
win.setWindowButtonVisibility?.(!isInPiP); if (originalFullScreen) {
win.setFullScreen(true);
}
if (originalMaximized) {
win.maximize();
}
}
const [x, y] = isInPiP ? pipPosition() : originalPosition;
const [w, h] = isInPiP ? pipSize() : originalSize;
win.setPosition(x, y);
win.setSize(w, h);
win.setWindowButtonVisibility?.(!isInPiP);
}; };
const blockShortcutsInPiP = (event, input) => { const blockShortcutsInPiP = (event, input) => {
const key = input.key.toLowerCase(); const key = input.key.toLowerCase();
if (key === "f") { if (key === 'f') {
event.preventDefault(); event.preventDefault();
} else if (key === 'escape') { } else if (key === 'escape') {
togglePiP(); togglePiP();
event.preventDefault(); event.preventDefault();
}; }
}; };
module.exports = (_win, _options) => { module.exports = (_win, _options) => {
options ??= _options; options ??= _options;
win ??= _win; win ??= _win;
setLocalOptions({ isInPiP }); setLocalOptions({ isInPiP });
injectCSS(win.webContents, path.join(__dirname, "style.css")); injectCSS(win.webContents, path.join(__dirname, 'style.css'));
ipcMain.on("picture-in-picture", async () => { ipcMain.on('picture-in-picture', async () => {
await togglePiP(); await togglePiP();
}); });
}; };
module.exports.setOptions = setLocalOptions; module.exports.setOptions = setLocalOptions;

View File

@ -1,140 +1,156 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const { toKeyEvent } = require('keyboardevent-from-electron-accelerator');
const keyEventAreEqual = require('keyboardevents-areequal');
const { toKeyEvent } = require("keyboardevent-from-electron-accelerator"); const { getSongMenu } = require('../../providers/dom-elements');
const keyEventAreEqual = require("keyboardevents-areequal"); const { ElementFromFile, templatePath } = require('../utils');
const { getSongMenu } = require("../../providers/dom-elements"); function $(selector) {
const { ElementFromFile, templatePath } = require("../utils"); return document.querySelector(selector);
}
function $(selector) { return document.querySelector(selector); }
let useNativePiP = false; let useNativePiP = false;
let menu = null; let menu = null;
const pipButton = ElementFromFile( const pipButton = ElementFromFile(
templatePath(__dirname, "picture-in-picture.html") templatePath(__dirname, 'picture-in-picture.html'),
); );
// will also clone // Will also clone
function replaceButton(query, button) { function replaceButton(query, button) {
const svg = button.querySelector("#icon svg").cloneNode(true); const svg = button.querySelector('#icon svg').cloneNode(true);
button.replaceWith(button.cloneNode(true)); button.replaceWith(button.cloneNode(true));
button.remove(); button.remove();
const newButton = $(query); const newButton = $(query);
newButton.querySelector("#icon").appendChild(svg); newButton.querySelector('#icon').append(svg);
return newButton; return newButton;
} }
function cloneButton(query) { function cloneButton(query) {
replaceButton(query, $(query)); replaceButton(query, $(query));
return $(query); return $(query);
} }
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
if (!menu) { if (!menu) {
menu = getSongMenu(); menu = getSongMenu();
if (!menu) return; if (!menu) {
} return;
if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) return; }
const menuUrl = $( }
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
)?.href;
if (menuUrl && !menuUrl.includes("watch?")) return;
menu.prepend(pipButton); if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) {
return;
}
const menuUrl = $(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
)?.href;
if (menuUrl && !menuUrl.includes('watch?')) {
return;
}
menu.prepend(pipButton);
}); });
global.togglePictureInPicture = async () => { global.togglePictureInPicture = async () => {
if (useNativePiP) { if (useNativePiP) {
const isInPiP = document.pictureInPictureElement !== null; const isInPiP = document.pictureInPictureElement !== null;
const video = $("video"); const video = $('video');
const togglePiP = () => const togglePiP = () =>
isInPiP isInPiP
? document.exitPictureInPicture.call(document) ? document.exitPictureInPicture.call(document)
: video.requestPictureInPicture.call(video); : video.requestPictureInPicture.call(video);
try { try {
await togglePiP(); await togglePiP();
$("#icon").click(); // Close the menu $('#icon').click(); // Close the menu
return true; return true;
} catch {} } catch {
} }
}
ipcRenderer.send("picture-in-picture"); ipcRenderer.send('picture-in-picture');
return false; return false;
}; };
const listenForToggle = () => { const listenForToggle = () => {
const originalExitButton = $(".exit-fullscreen-button"); const originalExitButton = $('.exit-fullscreen-button');
const appLayout = $("ytmusic-app-layout"); const appLayout = $('ytmusic-app-layout');
const expandMenu = $('#expanding-menu'); const expandMenu = $('#expanding-menu');
const middleControls = $('.middle-controls'); const middleControls = $('.middle-controls');
const playerPage = $("ytmusic-player-page"); const playerPage = $('ytmusic-player-page');
const togglePlayerPageButton = $(".toggle-player-page-button"); const togglePlayerPageButton = $('.toggle-player-page-button');
const fullScreenButton = $(".fullscreen-button"); const fullScreenButton = $('.fullscreen-button');
const player = $('#player'); const player = $('#player');
const onPlayerDblClick = player.onDoubleClick_; const onPlayerDblClick = player.onDoubleClick_;
const titlebar = $(".cet-titlebar"); const titlebar = $('.cet-titlebar');
ipcRenderer.on("pip-toggle", (_, isPip) => { ipcRenderer.on('pip-toggle', (_, isPip) => {
if (isPip) { if (isPip) {
replaceButton(".exit-fullscreen-button", originalExitButton).onclick = replaceButton('.exit-fullscreen-button', originalExitButton).addEventListener('click', () => togglePictureInPicture());
() => togglePictureInPicture(); player.onDoubleClick_ = () => {
player.onDoubleClick_ = () => {}; };
expandMenu.onmouseleave = () => middleControls.click();
if (!playerPage.playerPageOpen_) { expandMenu.addEventListener('mouseleave', () => middleControls.click());
togglePlayerPageButton.click(); if (!playerPage.playerPageOpen_) {
} togglePlayerPageButton.click();
fullScreenButton.click(); }
appLayout.classList.add("pip");
if (titlebar) titlebar.style.display = "none"; fullScreenButton.click();
} else { appLayout.classList.add('pip');
$(".exit-fullscreen-button").replaceWith(originalExitButton); if (titlebar) {
player.onDoubleClick_ = onPlayerDblClick; titlebar.style.display = 'none';
expandMenu.onmouseleave = undefined; }
originalExitButton.click(); } else {
appLayout.classList.remove("pip"); $('.exit-fullscreen-button').replaceWith(originalExitButton);
if (titlebar) titlebar.style.display = "flex"; player.onDoubleClick_ = onPlayerDblClick;
} expandMenu.onmouseleave = undefined;
}); originalExitButton.click();
} appLayout.classList.remove('pip');
if (titlebar) {
titlebar.style.display = 'flex';
}
}
});
};
function observeMenu(options) { function observeMenu(options) {
useNativePiP = options.useNativePiP; useNativePiP = options.useNativePiP;
document.addEventListener( document.addEventListener(
"apiLoaded", 'apiLoaded',
() => { () => {
listenForToggle(); listenForToggle();
cloneButton(".player-minimize-button").onclick = async () => { cloneButton('.player-minimize-button').addEventListener('click', async () => {
await global.togglePictureInPicture(); await global.togglePictureInPicture();
setTimeout(() => $("#player").click()); setTimeout(() => $('#player').click());
}; });
// allows easily closing the menu by programmatically clicking outside of it // Allows easily closing the menu by programmatically clicking outside of it
$("#expanding-menu").removeAttribute("no-cancel-on-outside-click"); $('#expanding-menu').removeAttribute('no-cancel-on-outside-click');
// TODO: think about wether an additional button in songMenu is needed // TODO: think about wether an additional button in songMenu is needed
observer.observe($("ytmusic-popup-container"), { observer.observe($('ytmusic-popup-container'), {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
}, },
{ once: true, passive: true } { once: true, passive: true },
); );
} }
module.exports = (options) => { module.exports = (options) => {
observeMenu(options); observeMenu(options);
if (options.hotkey) { if (options.hotkey) {
const hotkeyEvent = toKeyEvent(options.hotkey); const hotkeyEvent = toKeyEvent(options.hotkey);
window.addEventListener("keydown", (event) => { window.addEventListener('keydown', (event) => {
if ( if (
keyEventAreEqual(event, hotkeyEvent) && keyEventAreEqual(event, hotkeyEvent)
!$("ytmusic-search-box").opened && !$('ytmusic-search-box').opened
) { ) {
togglePictureInPicture(); togglePictureInPicture();
} }
}); });
} }
}; };

View File

@ -1,68 +1,69 @@
const prompt = require("custom-electron-prompt"); const prompt = require('custom-electron-prompt');
const promptOptions = require("../../providers/prompt-options"); const { setOptions } = require('./back.js');
const { setOptions } = require("./back.js");
const promptOptions = require('../../providers/prompt-options');
module.exports = (win, options) => [ module.exports = (win, options) => [
{ {
label: "Always on top", label: 'Always on top',
type: "checkbox", type: 'checkbox',
checked: options.alwaysOnTop, checked: options.alwaysOnTop,
click: (item) => { click(item) {
setOptions({ alwaysOnTop: item.checked }); setOptions({ alwaysOnTop: item.checked });
win.setAlwaysOnTop(item.checked); win.setAlwaysOnTop(item.checked);
},
}, },
{ },
label: "Save window position", {
type: "checkbox", label: 'Save window position',
checked: options.savePosition, type: 'checkbox',
click: (item) => { checked: options.savePosition,
setOptions({ savePosition: item.checked }); click(item) {
}, setOptions({ savePosition: item.checked });
}, },
{ },
label: "Save window size", {
type: "checkbox", label: 'Save window size',
checked: options.saveSize, type: 'checkbox',
click: (item) => { checked: options.saveSize,
setOptions({ saveSize: item.checked }); click(item) {
}, setOptions({ saveSize: item.checked });
}, },
{ },
label: "Hotkey", {
type: "checkbox", label: 'Hotkey',
checked: options.hotkey, type: 'checkbox',
click: async (item) => { checked: options.hotkey,
const output = await prompt({ async click(item) {
title: "Picture in Picture Hotkey", const output = await prompt({
label: "Choose a hotkey for toggling Picture in Picture", title: 'Picture in Picture Hotkey',
type: "keybind", label: 'Choose a hotkey for toggling Picture in Picture',
keybindOptions: [{ type: 'keybind',
value: "hotkey", keybindOptions: [{
label: "Hotkey", value: 'hotkey',
default: options.hotkey label: 'Hotkey',
}], default: options.hotkey,
...promptOptions() }],
}, win) ...promptOptions(),
}, win);
if (output) { if (output) {
const { value, accelerator } = output[0]; const { value, accelerator } = output[0];
setOptions({ [value]: accelerator }); setOptions({ [value]: accelerator });
item.checked = !!accelerator; item.checked = Boolean(accelerator);
} else { } else {
// Reset checkbox if prompt was canceled // Reset checkbox if prompt was canceled
item.checked = !item.checked; item.checked = !item.checked;
} }
},
}, },
{ },
label: "Use native PiP", {
type: "checkbox", label: 'Use native PiP',
checked: options.useNativePiP, type: 'checkbox',
click: (item) => { checked: options.useNativePiP,
setOptions({ useNativePiP: item.checked }); click(item) {
}, setOptions({ useNativePiP: item.checked });
} },
},
]; ];

View File

@ -3,41 +3,41 @@ ytmusic-app-layout.pip ytmusic-player-bar svg,
ytmusic-app-layout.pip ytmusic-player-bar .time-info, ytmusic-app-layout.pip ytmusic-player-bar .time-info,
ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string, ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string,
ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string { ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string {
filter: drop-shadow(2px 4px 6px black); filter: drop-shadow(2px 4px 6px black);
color: white !important; color: white !important;
fill: white !important; fill: white !important;
} }
/* improve the style of the player bar expanding menu */ /* improve the style of the player bar expanding menu */
ytmusic-app-layout.pip ytmusic-player-expanding-menu { ytmusic-app-layout.pip ytmusic-player-expanding-menu {
border-radius: 30px; border-radius: 30px;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px) brightness(20%); backdrop-filter: blur(5px) brightness(20%);
} }
/* fix volumeHud position when both in-app-menu and PiP are active */ /* fix volumeHud position when both in-app-menu and PiP are active */
.cet-container ytmusic-app-layout.pip #volumeHud { .cet-container ytmusic-app-layout.pip #volumeHud {
top: 22px !important; top: 22px !important;
} }
/* make player-bar not draggable if in-app-menu is enabled */ /* make player-bar not draggable if in-app-menu is enabled */
.cet-container ytmusic-app-layout.pip ytmusic-player-bar { .cet-container ytmusic-app-layout.pip ytmusic-player-bar {
-webkit-app-region: no-drag !important; -webkit-app-region: no-drag !important;
} }
/* make player draggable if in-app-menu is enabled */ /* make player draggable if in-app-menu is enabled */
.cet-container ytmusic-app-layout.pip #player { .cet-container ytmusic-app-layout.pip #player {
-webkit-app-region: drag !important; -webkit-app-region: drag !important;
} }
/* remove info, thumbnail and menu from player-bar */ /* remove info, thumbnail and menu from player-bar */
ytmusic-app-layout.pip ytmusic-player-bar .content-info-wrapper, ytmusic-app-layout.pip ytmusic-player-bar .content-info-wrapper,
ytmusic-app-layout.pip ytmusic-player-bar .thumbnail-image-wrapper, ytmusic-app-layout.pip ytmusic-player-bar .thumbnail-image-wrapper,
ytmusic-app-layout.pip ytmusic-player-bar ytmusic-menu-renderer { ytmusic-app-layout.pip ytmusic-player-bar ytmusic-menu-renderer {
display: none !important; display: none !important;
} }
/* disable the video-toggle button when in PiP mode */ /* disable the video-toggle button when in PiP mode */
ytmusic-app-layout.pip .video-switch-button { ytmusic-app-layout.pip .video-switch-button {
display: none !important; display: none !important;
} }

View File

@ -1,51 +1,50 @@
<div <div
class="style-scope menu-item ytmusic-menu-popup-renderer" aria-disabled="false"
role="option" aria-selected="false"
tabindex="-1" class="style-scope menu-item ytmusic-menu-popup-renderer"
aria-disabled="false" onclick="togglePictureInPicture()"
aria-selected="false" role="option"
onclick="togglePictureInPicture()" tabindex="-1"
> >
<div <div
id="navigation-endpoint" class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer" id="navigation-endpoint"
tabindex="-1" tabindex="-1"
> >
<div <div
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer" class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
> >
<svg <svg
version="1.1" id="Layer_1"
id="Layer_1" style="enable-background: new 0 0 512 512"
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"
x="0px" x="0px"
y="0px" xml:space="preserve"
viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"
style="enable-background: new 0 0 512 512" y="0px"
xml:space="preserve" >
>
<style type="text/css"> <style type="text/css">
.st0 { .st0 {
fill: #aaaaaa; fill: #aaaaaa;
} }
</style> </style>
<g id="XMLID_6_"> <g id="XMLID_6_">
<path <path
id="XMLID_11_" class="st0"
class="st0" d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4 c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
v326.8H464.8z" v326.8H464.8z"
/> id="XMLID_11_"
/>
</g> </g>
</svg> </svg>
</div> </div>
<div <div
class="text style-scope ytmusic-menu-navigation-item-renderer" class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-pip" id="ytmcustom-pip"
> >
Picture in picture Picture in picture
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,12 +1,14 @@
const { getSongMenu } = require("../../providers/dom-elements"); const { getSongMenu } = require('../../providers/dom-elements');
const { ElementFromFile, templatePath } = require("../utils"); const { ElementFromFile, templatePath } = require('../utils');
const { singleton } = require("../../providers/decorators") const { singleton } = require('../../providers/decorators');
function $(selector) { return document.querySelector(selector); } function $(selector) {
return document.querySelector(selector);
}
const slider = ElementFromFile(templatePath(__dirname, "slider.html")); const slider = ElementFromFile(templatePath(__dirname, 'slider.html'));
const roundToTwo = n => Math.round(n * 1e2) / 1e2; const roundToTwo = (n) => Math.round(n * 1e2) / 1e2;
const MIN_PLAYBACK_SPEED = 0.07; const MIN_PLAYBACK_SPEED = 0.07;
const MAX_PLAYBACK_SPEED = 16; const MAX_PLAYBACK_SPEED = 16;
@ -14,77 +16,79 @@ const MAX_PLAYBACK_SPEED = 16;
let playbackSpeed = 1; let playbackSpeed = 1;
const updatePlayBackSpeed = () => { const updatePlayBackSpeed = () => {
$('video').playbackRate = playbackSpeed; $('video').playbackRate = playbackSpeed;
const playbackSpeedElement = $("#playback-speed-value"); const playbackSpeedElement = $('#playback-speed-value');
if (playbackSpeedElement) { if (playbackSpeedElement) {
playbackSpeedElement.innerHTML = playbackSpeed; playbackSpeedElement.innerHTML = playbackSpeed;
} }
}; };
let menu; let menu;
const setupSliderListener = singleton(() => { const setupSliderListener = singleton(() => {
$('#playback-speed-slider').addEventListener('immediate-value-changed', e => { $('#playback-speed-slider').addEventListener('immediate-value-changed', (e) => {
playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED; playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED;
if (isNaN(playbackSpeed)) { if (isNaN(playbackSpeed)) {
playbackSpeed = 1; playbackSpeed = 1;
} }
updatePlayBackSpeed();
}) updatePlayBackSpeed();
});
}); });
const observePopupContainer = () => { const observePopupContainer = () => {
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
if (!menu) { if (!menu) {
menu = getSongMenu(); menu = getSongMenu();
} }
if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) { if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) {
menu.prepend(slider); menu.prepend(slider);
setupSliderListener(); setupSliderListener();
} }
}); });
observer.observe($('ytmusic-popup-container'), { observer.observe($('ytmusic-popup-container'), {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
}; };
const observeVideo = () => { const observeVideo = () => {
$('video').addEventListener('ratechange', forcePlaybackRate) $('video').addEventListener('ratechange', forcePlaybackRate);
$('video').addEventListener('srcChanged', forcePlaybackRate) $('video').addEventListener('srcChanged', forcePlaybackRate);
} };
const setupWheelListener = () => { const setupWheelListener = () => {
slider.addEventListener('wheel', e => { slider.addEventListener('wheel', (e) => {
e.preventDefault(); e.preventDefault();
if (isNaN(playbackSpeed)) { if (isNaN(playbackSpeed)) {
playbackSpeed = 1; playbackSpeed = 1;
} }
// e.deltaY < 0 means wheel-up
playbackSpeed = roundToTwo(e.deltaY < 0 ?
Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED) :
Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED)
);
updatePlayBackSpeed(); // E.deltaY < 0 means wheel-up
// update slider position playbackSpeed = roundToTwo(e.deltaY < 0
$('#playback-speed-slider').value = playbackSpeed; ? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
}) : Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
} );
updatePlayBackSpeed();
// Update slider position
$('#playback-speed-slider').value = playbackSpeed;
});
};
function forcePlaybackRate(e) { function forcePlaybackRate(e) {
if (e.target.playbackRate !== playbackSpeed) { if (e.target.playbackRate !== playbackSpeed) {
e.target.playbackRate = playbackSpeed e.target.playbackRate = playbackSpeed;
} }
} }
module.exports = () => { module.exports = () => {
document.addEventListener('apiLoaded', () => { document.addEventListener('apiLoaded', () => {
observePopupContainer(); observePopupContainer();
observeVideo(); observeVideo();
setupWheelListener(); setupWheelListener();
}, { once: true, passive: true }) }, { once: true, passive: true });
}; };

View File

@ -1,88 +1,93 @@
<div <div
class="style-scope menu-item ytmusic-menu-popup-renderer" aria-disabled="false"
role="option" aria-selected="false"
tabindex="-1" class="style-scope menu-item ytmusic-menu-popup-renderer"
aria-disabled="false" role="option"
aria-selected="false" tabindex="-1"
> >
<div <div
id="navigation-endpoint" class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer" id="navigation-endpoint"
tabindex="-1" tabindex="-1"
> >
<tp-yt-paper-slider <tp-yt-paper-slider
id="playback-speed-slider" aria-disabled="false"
class="volume-slider style-scope ytmusic-player-bar on-hover" aria-label="Playback speed"
style="display: inherit !important" aria-valuemax="2"
max="2" aria-valuemin="0"
min="0" aria-valuenow="1"
step="0.125" class="volume-slider style-scope ytmusic-player-bar on-hover"
dir="ltr" dir="ltr"
title="Playback speed" id="playback-speed-slider"
aria-label="Playback speed" max="2"
role="slider" min="0"
tabindex="0" role="slider"
aria-valuemin="0" step="0.125"
aria-valuemax="2" style="display: inherit !important"
aria-valuenow="1" tabindex="0"
aria-disabled="false" title="Playback speed"
value="1" value="1"
><!--css-build:shady--> ><!--css-build:shady-->
<div id="sliderContainer" class="style-scope tp-yt-paper-slider"> <div class="style-scope tp-yt-paper-slider" id="sliderContainer">
<div class="bar-container style-scope tp-yt-paper-slider"> <div class="bar-container style-scope tp-yt-paper-slider">
<tp-yt-paper-progress <tp-yt-paper-progress
id="sliderBar" aria-disabled="false"
aria-hidden="true" aria-hidden="true"
class="style-scope tp-yt-paper-slider" aria-valuemax="2"
role="progressbar" aria-valuemin="0"
value="1" aria-valuenow="1"
aria-valuenow="1" class="style-scope tp-yt-paper-slider"
aria-valuemin="0" id="sliderBar"
aria-valuemax="2" role="progressbar"
aria-disabled="false" style="touch-action: none"
style="touch-action: none" value="1"
><!--css-build:shady--> ><!--css-build:shady-->
<div <div
id="progressContainer" class="style-scope tp-yt-paper-progress"
class="style-scope tp-yt-paper-progress" id="progressContainer"
> >
<div <div
id="secondaryProgress" class="style-scope tp-yt-paper-progress"
class="style-scope tp-yt-paper-progress" hidden="true"
hidden="true" id="secondaryProgress"
style="transform: scaleX(0)" style="transform: scaleX(0)"
></div> ></div>
<div <div
id="primaryProgress" class="style-scope tp-yt-paper-progress"
class="style-scope tp-yt-paper-progress" id="primaryProgress"
style="transform: scaleX(0.5)" style="transform: scaleX(0.5)"
></div> ></div>
</div> </div>
</tp-yt-paper-progress> </tp-yt-paper-progress>
</div> </div>
<dom-if class="style-scope tp-yt-paper-slider" <dom-if class="style-scope tp-yt-paper-slider"
><template is="dom-if"></template >
></dom-if> <template is="dom-if"></template
<div >
id="sliderKnob" </dom-if>
class="slider-knob style-scope tp-yt-paper-slider" <div
style="left: 50%; touch-action: none" class="slider-knob style-scope tp-yt-paper-slider"
> id="sliderKnob"
<div style="left: 50%; touch-action: none"
class="slider-knob-inner style-scope tp-yt-paper-slider" >
value="1" <div
></div> class="slider-knob-inner style-scope tp-yt-paper-slider"
</div> value="1"
</div> ></div>
<dom-if class="style-scope tp-yt-paper-slider" </div>
><template is="dom-if"></template></dom-if </div>
></tp-yt-paper-slider> <dom-if class="style-scope tp-yt-paper-slider"
<div >
class="text style-scope ytmusic-menu-navigation-item-renderer" <template is="dom-if"></template>
id="ytmcustom-playback-speed" </dom-if
> >
Speed (<span id="playback-speed-value">1</span>) </tp-yt-paper-slider>
</div> <div
</div> class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-playback-speed"
>
Speed (<span id="playback-speed-value">1</span>)
</div>
</div>
</div> </div>

View File

@ -1,5 +1,6 @@
const { injectCSS } = require("../utils"); const { injectCSS } = require('../utils');
const path = require("path");
const path = require('node:path');
/* /*
This is used to determine if plugin is actually active This is used to determine if plugin is actually active
@ -10,15 +11,16 @@ let enabled = false;
const { globalShortcut } = require('electron'); const { globalShortcut } = require('electron');
module.exports = (win, options) => { module.exports = (win, options) => {
enabled = true; enabled = true;
injectCSS(win.webContents, path.join(__dirname, "volume-hud.css")); injectCSS(win.webContents, path.join(__dirname, 'volume-hud.css'));
if (options.globalShortcuts?.volumeUp) { if (options.globalShortcuts?.volumeUp) {
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true)); globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
} }
if (options.globalShortcuts?.volumeDown) {
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false)); if (options.globalShortcuts?.volumeDown) {
} globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
} }
};
module.exports.enabled = () => enabled; module.exports.enabled = () => enabled;

View File

@ -1,232 +1,245 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins"); const { setOptions, setMenuOptions, isEnabled } = require('../../config/plugins');
function $(selector) { return document.querySelector(selector); } function $(selector) {
return document.querySelector(selector);
}
const { debounce } = require("../../providers/decorators"); const { debounce } = require('../../providers/decorators');
let api, options; let api;
let options;
module.exports = (_options) => { module.exports = (_options) => {
options = _options; options = _options;
document.addEventListener('apiLoaded', e => { document.addEventListener('apiLoaded', (e) => {
api = e.detail; api = e.detail;
ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease)); ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease));
ipcRenderer.on('setVolume', (_, value) => setVolume(value)); ipcRenderer.on('setVolume', (_, value) => setVolume(value));
firstRun(); firstRun();
}, { once: true, passive: true }) }, { once: true, passive: true });
}; };
//without this function it would rewrite config 20 time when volume change by 20 // Without this function it would rewrite config 20 time when volume change by 20
const writeOptions = debounce(() => { const writeOptions = debounce(() => {
setOptions("precise-volume", options); setOptions('precise-volume', options);
}, 1000); }, 1000);
module.exports.moveVolumeHud = debounce((showVideo) => { module.exports.moveVolumeHud = debounce((showVideo) => {
const volumeHud = $("#volumeHud"); const volumeHud = $('#volumeHud');
if (!volumeHud) return; if (!volumeHud) {
volumeHud.style.top = showVideo return;
? `${($("ytmusic-player").clientHeight - $("video").clientHeight) / 2}px` }
: 0;
volumeHud.style.top = showVideo
? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px`
: 0;
}, 250); }, 250);
const hideVolumeHud = debounce((volumeHud) => { const hideVolumeHud = debounce((volumeHud) => {
volumeHud.style.opacity = 0; volumeHud.style.opacity = 0;
}, 2000); }, 2000);
const hideVolumeSlider = debounce((slider) => { const hideVolumeSlider = debounce((slider) => {
slider.classList.remove("on-hover"); slider.classList.remove('on-hover');
}, 2500); }, 2500);
/** Restore saved volume and setup tooltip */ /** Restore saved volume and setup tooltip */
function firstRun() { function firstRun() {
if (typeof options.savedVolume === "number") { if (typeof options.savedVolume === 'number') {
// Set saved volume as tooltip // Set saved volume as tooltip
setTooltip(options.savedVolume); setTooltip(options.savedVolume);
if (api.getVolume() !== options.savedVolume) { if (api.getVolume() !== options.savedVolume) {
api.setVolume(options.savedVolume); api.setVolume(options.savedVolume);
} }
} }
setupPlaybar(); setupPlaybar();
setupLocalArrowShortcuts(); setupLocalArrowShortcuts();
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none"; const noVid = $('#main-panel')?.computedStyleMap().get('display').value === 'none';
injectVolumeHud(noVid); injectVolumeHud(noVid);
if (!noVid) { if (!noVid) {
setupVideoPlayerOnwheel(); setupVideoPlayerOnwheel();
if (!isEnabled('video-toggle')) { if (!isEnabled('video-toggle')) {
//video-toggle handles hud positioning on its own // Video-toggle handles hud positioning on its own
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV'; const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
$("video").addEventListener("srcChanged", () => moveVolumeHud(videoMode())); $('video').addEventListener('srcChanged', () => moveVolumeHud(videoMode()));
} }
} }
// Change options from renderer to keep sync // Change options from renderer to keep sync
ipcRenderer.on("setOptions", (_event, newOptions = {}) => { ipcRenderer.on('setOptions', (_event, newOptions = {}) => {
Object.assign(options, newOptions) Object.assign(options, newOptions);
setMenuOptions("precise-volume", options); setMenuOptions('precise-volume', options);
}); });
} }
function injectVolumeHud(noVid) { function injectVolumeHud(noVid) {
if (noVid) { if (noVid) {
const position = "top: 18px; right: 60px;"; const position = 'top: 18px; right: 60px;';
const mainStyle = "font-size: xx-large;"; const mainStyle = 'font-size: xx-large;';
$(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend", $('.center-content.ytmusic-nav-bar').insertAdjacentHTML('beforeend',
`<span id="volumeHud" style="${position + mainStyle}"></span>`) `<span id="volumeHud" style="${position + mainStyle}"></span>`);
} else { } else {
const position = `top: 10px; left: 10px;`; const position = 'top: 10px; left: 10px;';
const mainStyle = "font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;"; const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;';
$("#song-video").insertAdjacentHTML('afterend', $('#song-video').insertAdjacentHTML('afterend',
`<span id="volumeHud" style="${position + mainStyle}"></span>`) `<span id="volumeHud" style="${position + mainStyle}"></span>`);
} }
} }
function showVolumeHud(volume) { function showVolumeHud(volume) {
const volumeHud = $("#volumeHud"); const volumeHud = $('#volumeHud');
if (!volumeHud) return; if (!volumeHud) {
return;
}
volumeHud.textContent = `${volume}%`; volumeHud.textContent = `${volume}%`;
volumeHud.style.opacity = 1; volumeHud.style.opacity = 1;
hideVolumeHud(volumeHud); hideVolumeHud(volumeHud);
} }
/** Add onwheel event to video player */ /** Add onwheel event to video player */
function setupVideoPlayerOnwheel() { function setupVideoPlayerOnwheel() {
$("#main-panel").addEventListener("wheel", event => { $('#main-panel').addEventListener('wheel', (event) => {
event.preventDefault(); event.preventDefault();
// Event.deltaY < 0 means wheel-up // Event.deltaY < 0 means wheel-up
changeVolume(event.deltaY < 0); changeVolume(event.deltaY < 0);
}); });
} }
function saveVolume(volume) { function saveVolume(volume) {
options.savedVolume = volume; options.savedVolume = volume;
writeOptions(); writeOptions();
} }
/** Add onwheel event to play bar and also track if play bar is hovered*/ /** Add onwheel event to play bar and also track if play bar is hovered */
function setupPlaybar() { function setupPlaybar() {
const playerbar = $("ytmusic-player-bar"); const playerbar = $('ytmusic-player-bar');
playerbar.addEventListener("wheel", event => { playerbar.addEventListener('wheel', (event) => {
event.preventDefault(); event.preventDefault();
// Event.deltaY < 0 means wheel-up // Event.deltaY < 0 means wheel-up
changeVolume(event.deltaY < 0); changeVolume(event.deltaY < 0);
}); });
// Keep track of mouse position for showVolumeSlider() // Keep track of mouse position for showVolumeSlider()
playerbar.addEventListener("mouseenter", () => { playerbar.addEventListener('mouseenter', () => {
playerbar.classList.add("on-hover"); playerbar.classList.add('on-hover');
}); });
playerbar.addEventListener("mouseleave", () => { playerbar.addEventListener('mouseleave', () => {
playerbar.classList.remove("on-hover"); playerbar.classList.remove('on-hover');
}); });
setupSliderObserver(); setupSliderObserver();
} }
/** Save volume + Update the volume tooltip when volume-slider is manually changed */ /** Save volume + Update the volume tooltip when volume-slider is manually changed */
function setupSliderObserver() { function setupSliderObserver() {
const sliderObserver = new MutationObserver(mutations => { const sliderObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) { for (const mutation of mutations) {
// This checks that volume-slider was manually set // This checks that volume-slider was manually set
if (mutation.oldValue !== mutation.target.value && if (mutation.oldValue !== mutation.target.value
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) { && (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
// Diff>4 means it was manually set // Diff>4 means it was manually set
setTooltip(mutation.target.value); setTooltip(mutation.target.value);
saveVolume(mutation.target.value); saveVolume(mutation.target.value);
} }
} }
}); });
// Observing only changes in 'value' of volume-slider // Observing only changes in 'value' of volume-slider
sliderObserver.observe($("#volume-slider"), { sliderObserver.observe($('#volume-slider'), {
attributeFilter: ["value"], attributeFilter: ['value'],
attributeOldValue: true attributeOldValue: true,
}); });
} }
function setVolume(value) { function setVolume(value) {
api.setVolume(value); api.setVolume(value);
// Save the new volume // Save the new volume
saveVolume(value); saveVolume(value);
// change slider position (important) // Change slider position (important)
updateVolumeSlider(); updateVolumeSlider();
// Change tooltips to new value // Change tooltips to new value
setTooltip(value); setTooltip(value);
// Show volume slider // Show volume slider
showVolumeSlider(); showVolumeSlider();
// Show volume HUD // Show volume HUD
showVolumeHud(value); showVolumeHud(value);
} }
/** if (toIncrease = false) then volume decrease */ /** If (toIncrease = false) then volume decrease */
function changeVolume(toIncrease) { function changeVolume(toIncrease) {
// Apply volume change if valid // Apply volume change if valid
const steps = Number(options.steps || 1); const steps = Number(options.steps || 1);
setVolume(toIncrease ? setVolume(toIncrease
Math.min(api.getVolume() + steps, 100) : ? Math.min(api.getVolume() + steps, 100)
Math.max(api.getVolume() - steps, 0)); : Math.max(api.getVolume() - steps, 0));
} }
function updateVolumeSlider() { function updateVolumeSlider() {
// Slider value automatically rounds to multiples of 5 // Slider value automatically rounds to multiples of 5
for (const slider of ["#volume-slider", "#expand-volume-slider"]) { for (const slider of ['#volume-slider', '#expand-volume-slider']) {
$(slider).value = $(slider).value
options.savedVolume > 0 && options.savedVolume < 5 = options.savedVolume > 0 && options.savedVolume < 5
? 5 ? 5
: options.savedVolume; : options.savedVolume;
} }
} }
function showVolumeSlider() { function showVolumeSlider() {
const slider = $("#volume-slider"); const slider = $('#volume-slider');
// This class display the volume slider if not in minimized mode // This class display the volume slider if not in minimized mode
slider.classList.add("on-hover"); slider.classList.add('on-hover');
hideVolumeSlider(slider); hideVolumeSlider(slider);
} }
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small) // Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
const tooltipTargets = [ const tooltipTargets = [
"#volume-slider", '#volume-slider',
"tp-yt-paper-icon-button.volume", 'tp-yt-paper-icon-button.volume',
"#expand-volume-slider", '#expand-volume-slider',
"#expand-volume" '#expand-volume',
]; ];
function setTooltip(volume) { function setTooltip(volume) {
for (target of tooltipTargets) { for (target of tooltipTargets) {
$(target).title = `${volume}%`; $(target).title = `${volume}%`;
} }
} }
function setupLocalArrowShortcuts() { function setupLocalArrowShortcuts() {
if (options.arrowsShortcut) { if (options.arrowsShortcut) {
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if ($('ytmusic-search-box').opened) return; if ($('ytmusic-search-box').opened) {
switch (event.code) { return;
case "ArrowUp": }
event.preventDefault();
changeVolume(true); switch (event.code) {
break; case 'ArrowUp': {
case "ArrowDown": event.preventDefault();
event.preventDefault(); changeVolume(true);
changeVolume(false); break;
break; }
}
}); case 'ArrowDown': {
} event.preventDefault();
changeVolume(false);
break;
}
}
});
}
} }

View File

@ -1,82 +1,86 @@
const { enabled } = require("./back"); const prompt = require('custom-electron-prompt');
const { setMenuOptions } = require("../../config/plugins");
const prompt = require("custom-electron-prompt"); const { enabled } = require('./back');
const promptOptions = require("../../providers/prompt-options");
const { setMenuOptions } = require('../../config/plugins');
const promptOptions = require('../../providers/prompt-options');
function changeOptions(changedOptions, options, win) { function changeOptions(changedOptions, options, win) {
for (option in changedOptions) { for (option in changedOptions) {
options[option] = changedOptions[option]; options[option] = changedOptions[option];
} }
// Dynamically change setting if plugin is enabled
if (enabled()) { // Dynamically change setting if plugin is enabled
win.webContents.send("setOptions", changedOptions); if (enabled()) {
} else { // Fallback to usual method if disabled win.webContents.send('setOptions', changedOptions);
setMenuOptions("precise-volume", options); } else { // Fallback to usual method if disabled
} setMenuOptions('precise-volume', options);
}
} }
module.exports = (win, options) => [ module.exports = (win, options) => [
{ {
label: "Local Arrowkeys Controls", label: 'Local Arrowkeys Controls',
type: "checkbox", type: 'checkbox',
checked: !!options.arrowsShortcut, checked: Boolean(options.arrowsShortcut),
click: item => { click(item) {
changeOptions({ arrowsShortcut: item.checked }, options, win); changeOptions({ arrowsShortcut: item.checked }, options, win);
} },
}, },
{ {
label: "Global Hotkeys", label: 'Global Hotkeys',
type: "checkbox", type: 'checkbox',
checked: !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown, checked: Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown),
click: item => promptGlobalShortcuts(win, options, item) click: (item) => promptGlobalShortcuts(win, options, item),
}, },
{ {
label: "Set Custom Volume Steps", label: 'Set Custom Volume Steps',
click: () => promptVolumeSteps(win, options) click: () => promptVolumeSteps(win, options),
} },
]; ];
// Helper function for globalShortcuts prompt // Helper function for globalShortcuts prompt
const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ || undefined }; }; const kb = (label_, value_, default_) => ({ value: value_, label: label_, default: default_ || undefined });
async function promptVolumeSteps(win, options) { async function promptVolumeSteps(win, options) {
const output = await prompt({ const output = await prompt({
title: "Volume Steps", title: 'Volume Steps',
label: "Choose Volume Increase/Decrease Steps", label: 'Choose Volume Increase/Decrease Steps',
value: options.steps || 1, value: options.steps || 1,
type: "counter", type: 'counter',
counterOptions: { minimum: 0, maximum: 100, multiFire: true }, counterOptions: { minimum: 0, maximum: 100, multiFire: true },
width: 380, width: 380,
...promptOptions() ...promptOptions(),
}, win) }, win);
if (output || output === 0) { // 0 is somewhat valid if (output || output === 0) { // 0 is somewhat valid
changeOptions({ steps: output}, options, win); changeOptions({ steps: output }, options, win);
} }
} }
async function promptGlobalShortcuts(win, options, item) { async function promptGlobalShortcuts(win, options, item) {
const output = await prompt({ const output = await prompt({
title: "Global Volume Keybinds", title: 'Global Volume Keybinds',
label: "Choose Global Volume Keybinds:", label: 'Choose Global Volume Keybinds:',
type: "keybind", type: 'keybind',
keybindOptions: [ keybindOptions: [
kb("Increase Volume", "volumeUp", options.globalShortcuts?.volumeUp), kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
kb("Decrease Volume", "volumeDown", options.globalShortcuts?.volumeDown) kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
], ],
...promptOptions() ...promptOptions(),
}, win) }, win);
if (output) { if (output) {
let newGlobalShortcuts = {}; const newGlobalShortcuts = {};
for (const { value, accelerator } of output) { for (const { value, accelerator } of output) {
newGlobalShortcuts[value] = accelerator; newGlobalShortcuts[value] = accelerator;
} }
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
item.checked = !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown; changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
} else {
// Reset checkbox if prompt was canceled item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
item.checked = !item.checked; } else {
} // Reset checkbox if prompt was canceled
item.checked = !item.checked;
}
} }

View File

@ -1,32 +1,32 @@
const is = require("electron-is"); const is = require('electron-is');
let ignored = { let ignored = {
id: ["volume-slider", "expand-volume-slider"], id: ['volume-slider', 'expand-volume-slider'],
types: ["mousewheel", "keydown", "keyup"] types: ['mousewheel', 'keydown', 'keyup'],
}; };
function overrideAddEventListener() { function overrideAddEventListener() {
// Save native addEventListener // Save native addEventListener
Element.prototype._addEventListener = Element.prototype.addEventListener; Element.prototype._addEventListener = Element.prototype.addEventListener;
// Override addEventListener to Ignore specific events in volume-slider // Override addEventListener to Ignore specific events in volume-slider
Element.prototype.addEventListener = function (type, listener, useCapture = false) { Element.prototype.addEventListener = function (type, listener, useCapture = false) {
if (!( if (!(
ignored.id.includes(this.id) && ignored.id.includes(this.id)
ignored.types.includes(type) && ignored.types.includes(type)
)) { )) {
this._addEventListener(type, listener, useCapture); this._addEventListener(type, listener, useCapture);
} else if (is.dev()) { } else if (is.dev()) {
console.log(`Ignoring event: "${this.id}.${type}()"`); console.log(`Ignoring event: "${this.id}.${type}()"`);
} }
}; };
} }
module.exports = () => { module.exports = () => {
overrideAddEventListener(); overrideAddEventListener();
// Restore original function after finished loading to avoid keeping Element.prototype altered // Restore original function after finished loading to avoid keeping Element.prototype altered
window.addEventListener('load', () => { window.addEventListener('load', () => {
Element.prototype.addEventListener = Element.prototype._addEventListener; Element.prototype.addEventListener = Element.prototype._addEventListener;
Element.prototype._addEventListener = undefined; Element.prototype._addEventListener = undefined;
ignored = undefined; ignored = undefined;
}, { once: true }); }, { once: true });
}; };

View File

@ -1,11 +1,11 @@
#volumeHud { #volumeHud {
z-index: 999; z-index: 999;
position: absolute; position: absolute;
transition: opacity 0.6s; transition: opacity 0.6s;
pointer-events: none; pointer-events: none;
padding: 10px; padding: 10px;
} }
ytmusic-player[player-ui-state_="MINIPLAYER"] #volumeHud { ytmusic-player[player-ui-state_="MINIPLAYER"] #volumeHud {
top: 0 !important; top: 0 !important;
} }

View File

@ -1,15 +1,13 @@
const { ipcMain, dialog } = require("electron"); const { ipcMain, dialog } = require('electron');
module.exports = () => { module.exports = () => {
ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => { ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => await dialog.showMessageBox({
return await dialog.showMessageBox({ type: 'question',
type: "question", buttons: qualityLabels,
buttons: qualityLabels, defaultId: currentIndex,
defaultId: currentIndex, title: 'Choose Video Quality',
title: "Choose Video Quality", message: 'Choose Video Quality:',
message: "Choose Video Quality:", detail: `Current Quality: ${qualityLabels[currentIndex]}`,
detail: `Current Quality: ${qualityLabels[currentIndex]}`, cancelId: -1,
cancelId: -1 }));
})
})
}; };

View File

@ -1,34 +1,39 @@
const { ElementFromFile, templatePath } = require("../utils"); const { ipcRenderer } = require('electron');
const { ipcRenderer } = require("electron");
function $(selector) { return document.querySelector(selector); } const { ElementFromFile, templatePath } = require('../utils');
function $(selector) {
return document.querySelector(selector);
}
const qualitySettingsButton = ElementFromFile( const qualitySettingsButton = ElementFromFile(
templatePath(__dirname, "qualitySettingsTemplate.html") templatePath(__dirname, 'qualitySettingsTemplate.html'),
); );
module.exports = () => { module.exports = () => {
document.addEventListener('apiLoaded', setup, { once: true, passive: true }); document.addEventListener('apiLoaded', setup, { once: true, passive: true });
} };
function setup(event) { function setup(event) {
const api = event.detail; const api = event.detail;
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton); $('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
qualitySettingsButton.onclick = function chooseQuality() { qualitySettingsButton.addEventListener('click', function chooseQuality() {
setTimeout(() => $('#player').click()); setTimeout(() => $('#player').click());
const qualityLevels = api.getAvailableQualityLevels(); const qualityLevels = api.getAvailableQualityLevels();
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality()); const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then(promise => { ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then((promise) => {
if (promise.response === -1) return; if (promise.response === -1) {
const newQuality = qualityLevels[promise.response]; return;
api.setPlaybackQualityRange(newQuality); }
api.setPlaybackQuality(newQuality)
}); const newQuality = qualityLevels[promise.response];
} api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality);
});
});
} }

View File

@ -1,13 +1,16 @@
<tp-yt-paper-icon-button class="player-quality-button style-scope ytmusic-player" icon="yt-icons:settings" <tp-yt-paper-icon-button aria-disabled="false" aria-label="Open player quality changer"
title="Open player quality changer" aria-label="Open player quality changer" role="button" tabindex="0" aria-disabled="false"> class="player-quality-button style-scope ytmusic-player" icon="yt-icons:settings" role="button"
<tp-yt-iron-icon id="icon" class="style-scope tp-yt-paper-icon-button"><svg viewBox="0 0 24 24" tabindex="0" title="Open player quality changer">
preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" <tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
style="pointer-events: none; display: block; width: 100%; height: 100%;"> <svg class="style-scope yt-icon"
<g class="style-scope yt-icon"> focusable="false" preserveAspectRatio="xMidYMid meet"
<path style="pointer-events: none; display: block; width: 100%; height: 100%;"
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" viewBox="0 0 24 24">
class="style-scope yt-icon"></path> <g class="style-scope yt-icon">
</g> <path
</svg> class="style-scope yt-icon"
</tp-yt-iron-icon> d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"></path>
</tp-yt-paper-icon-button> </g>
</svg>
</tp-yt-iron-icon>
</tp-yt-paper-icon-button>

View File

@ -1,62 +1,66 @@
const { globalShortcut } = require("electron"); const { globalShortcut } = require('electron');
const is = require("electron-is"); const is = require('electron-is');
const electronLocalshortcut = require("electron-localshortcut"); const electronLocalshortcut = require('electron-localshortcut');
const getSongControls = require("../../providers/song-controls");
const registerMPRIS = require("./mpris"); const registerMPRIS = require('./mpris');
const getSongControls = require('../../providers/song-controls');
function _registerGlobalShortcut(webContents, shortcut, action) { function _registerGlobalShortcut(webContents, shortcut, action) {
globalShortcut.register(shortcut, () => { globalShortcut.register(shortcut, () => {
action(webContents); action(webContents);
}); });
} }
function _registerLocalShortcut(win, shortcut, action) { function _registerLocalShortcut(win, shortcut, action) {
electronLocalshortcut.register(win, shortcut, () => { electronLocalshortcut.register(win, shortcut, () => {
action(win.webContents); action(win.webContents);
}); });
} }
function registerShortcuts(win, options) { function registerShortcuts(win, options) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { playPause, next, previous, search } = songControls; const { playPause, next, previous, search } = songControls;
if (options.overrideMediaKeys) { if (options.overrideMediaKeys) {
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause); _registerGlobalShortcut(win.webContents, 'MediaPlayPause', playPause);
_registerGlobalShortcut(win.webContents, "MediaNextTrack", next); _registerGlobalShortcut(win.webContents, 'MediaNextTrack', next);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous); _registerGlobalShortcut(win.webContents, 'MediaPreviousTrack', previous);
} }
_registerLocalShortcut(win, "CommandOrControl+F", search); _registerLocalShortcut(win, 'CommandOrControl+F', search);
_registerLocalShortcut(win, "CommandOrControl+L", search); _registerLocalShortcut(win, 'CommandOrControl+L', search);
if (is.linux()) registerMPRIS(win); if (is.linux()) {
registerMPRIS(win);
}
const { global, local } = options; const { global, local } = options;
const shortcutOptions = { global, local }; const shortcutOptions = { global, local };
for (const optionType in shortcutOptions) { for (const optionType in shortcutOptions) {
registerAllShortcuts(shortcutOptions[optionType], optionType); registerAllShortcuts(shortcutOptions[optionType], optionType);
} }
function registerAllShortcuts(container, type) { function registerAllShortcuts(container, type) {
for (const action in container) { for (const action in container) {
if (!container[action]) { if (!container[action]) {
continue; // Action accelerator is empty continue; // Action accelerator is empty
} }
console.debug(`Registering ${type} shortcut`, container[action], ":", action); console.debug(`Registering ${type} shortcut`, container[action], ':', action);
if (!songControls[action]) { if (!songControls[action]) {
console.warn("Invalid action", action); console.warn('Invalid action', action);
continue; continue;
} }
if (type === "global") { if (type === 'global') {
_registerGlobalShortcut(win.webContents, container[action], songControls[action]); _registerGlobalShortcut(win.webContents, container[action], songControls[action]);
} else { // type === "local" } else { // Type === "local"
_registerLocalShortcut(win, local[action], songControls[action]); _registerLocalShortcut(win, local[action], songControls[action]);
} }
} }
} }
} }
module.exports = registerShortcuts; module.exports = registerShortcuts;

View File

@ -1,53 +1,56 @@
const { setMenuOptions } = require("../../config/plugins"); const prompt = require('custom-electron-prompt');
const prompt = require("custom-electron-prompt");
const promptOptions = require("../../providers/prompt-options"); const { setMenuOptions } = require('../../config/plugins');
const promptOptions = require('../../providers/prompt-options');
module.exports = (win, options) => [ module.exports = (win, options) => [
{ {
label: "Set Global Song Controls", label: 'Set Global Song Controls',
click: () => promptKeybind(options, win) click: () => promptKeybind(options, win),
}, },
{ {
label: "Override MediaKeys", label: 'Override MediaKeys',
type: "checkbox", type: 'checkbox',
checked: options.overrideMediaKeys, checked: options.overrideMediaKeys,
click: item => setOption(options, "overrideMediaKeys", item.checked) click: (item) => setOption(options, 'overrideMediaKeys', item.checked),
} },
]; ];
function setOption(options, key = null, newValue = null) { function setOption(options, key = null, newValue = null) {
if (key && newValue !== null) { if (key && newValue !== null) {
options[key] = newValue; options[key] = newValue;
} }
setMenuOptions("shortcuts", options); setMenuOptions('shortcuts', options);
} }
// Helper function for keybind prompt // Helper function for keybind prompt
const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ }; }; const kb = (label_, value_, default_) => ({ value: value_, label: label_, default: default_ });
async function promptKeybind(options, win) { async function promptKeybind(options, win) {
const output = await prompt({ const output = await prompt({
title: "Global Keybinds", title: 'Global Keybinds',
label: "Choose Global Keybinds for Songs Control:", label: 'Choose Global Keybinds for Songs Control:',
type: "keybind", type: 'keybind',
keybindOptions: [ // If default=undefined then no default is used keybindOptions: [ // If default=undefined then no default is used
kb("Previous", "previous", options.global?.previous), kb('Previous', 'previous', options.global?.previous),
kb("Play / Pause", "playPause", options.global?.playPause), kb('Play / Pause', 'playPause', options.global?.playPause),
kb("Next", "next", options.global?.next) kb('Next', 'next', options.global?.next),
], ],
height: 270, height: 270,
...promptOptions() ...promptOptions(),
}, win); }, win);
if (output) { if (output) {
if (!options.global) { if (!options.global) {
options.global = {}; options.global = {};
} }
for (const { value, accelerator } of output) {
options.global[value] = accelerator; for (const { value, accelerator } of output) {
} options.global[value] = accelerator;
setOption(options); }
}
// else -> pressed cancel setOption(options);
}
// Else -> pressed cancel
} }

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