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,10 +1,10 @@
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,
@ -15,164 +15,164 @@ const defaultConfig = {
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,
"alwaysOnTop": true,
"savePosition": true,
"saveSize": false,
"hotkey": "P"
},
"captions-selector": {
enabled: false, enabled: false,
disableCaptions: false alwaysOnTop: true,
savePosition: true,
saveSize: false,
hotkey: 'P',
}, },
"skip-silences": { 'captions-selector': {
enabled: false,
disableCaptions: false,
},
'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',
}, },
}, },
], ],

View File

@ -1,31 +1,32 @@
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;
/** /**
@ -78,9 +79,7 @@ module.exports.PluginConfig = class PluginConfig {
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;
@ -94,29 +93,32 @@ module.exports.PluginConfig = class PluginConfig {
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));
if (changed) {
for (const fn of this.#allSubscribers) {
fn(this.#config);
}
}
this.#save(); this.#save();
}; };
getDefaultConfig = () => { getDefaultConfig = () => this.#defaultConfig;
return this.#defaultConfig;
};
/** /**
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true` * Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
@ -144,26 +146,32 @@ module.exports.PluginConfig = class PluginConfig {
#onChange(valueName, single = true) { #onChange(valueName, single = true) {
this.#subscribers[valueName]?.(this.#config[valueName]); this.#subscribers[valueName]?.(this.#config[valueName]);
if (single) this.#allSubscribers.forEach((fn) => fn(this.#config)); if (single) {
for (const fn of this.#allSubscribers) {
fn(this.#config);
}
}
} }
#setupFront() { #setupFront() {
const ignoredMethods = ["subscribe", "subscribeAll"]; const ignoredMethods = ['subscribe', 'subscribeAll'];
if (process.type === "renderer") { if (process.type === 'renderer') {
for (const [fnName, fn] of Object.entries(this)) { for (const [fnName, fn] of Object.entries(this)) {
if (typeof fn !== "function" || fn.name in ignoredMethods) return; if (typeof fn !== 'function' || fn.name in ignoredMethods) {
this[fnName] = async (...args) => { return;
return await ipcRenderer.invoke( }
this[fnName] = async (...args) => await ipcRenderer.invoke(
`${this.#name}-config-${fnName}`, `${this.#name}-config-${fnName}`,
...args, ...args,
); );
};
this.subscribe = (valueName, fn) => { this.subscribe = (valueName, fn) => {
if (valueName in this.#subscribers) { if (valueName in this.#subscribers) {
console.error(`Already subscribed to ${valueName}`); console.error(`Already subscribed to ${valueName}`);
} }
this.#subscribers[valueName] = fn; this.#subscribers[valueName] = fn;
ipcRenderer.on( ipcRenderer.on(
`${this.#name}-config-changed-${valueName}`, `${this.#name}-config-changed-${valueName}`,
@ -181,12 +189,13 @@ module.exports.PluginConfig = class PluginConfig {
ipcRenderer.send(`${this.#name}-config-subscribe-all`); ipcRenderer.send(`${this.#name}-config-subscribe-all`);
}; };
} }
} else if (process.type === "browser") { } else if (process.type === 'browser') {
for (const [fnName, fn] of Object.entries(this)) { for (const [fnName, fn] of Object.entries(this)) {
if (typeof fn !== "function" || fn.name in ignoredMethods) return; if (typeof fn !== 'function' || fn.name in ignoredMethods) {
ipcMain.handle(`${this.#name}-config-${fnName}`, (_, ...args) => { return;
return fn(...args); }
});
ipcMain.handle(`${this.#name}-config-${fnName}`, (_, ...args) => fn(...args));
} }
ipcMain.on(`${this.#name}-config-subscribe`, (_, valueName) => { ipcMain.on(`${this.#name}-config-subscribe`, (_, valueName) => {

View File

@ -1,7 +1,8 @@
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);
@ -9,12 +10,12 @@ const 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,
@ -22,9 +23,9 @@ module.exports = {
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,22 +1,23 @@
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],
@ -27,11 +28,13 @@ function setOptions(plugin, 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) {

View File

@ -1,58 +1,58 @@
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]) {
@ -67,18 +67,18 @@ const migrations = {
} }
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;
} }
@ -91,17 +91,18 @@ const migrations = {
}, },
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);
}, },
}; };

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,69 +1,69 @@
<!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
class="meta-url"
content="https://th-ch.github.io/youtube-music"
property="og:url" property="og:url"
class="meta-url"
content="https://th-ch.github.io/youtube-music"
/> />
<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">
@ -71,9 +71,9 @@
<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
alt="YouTube Music"
class="header-logo-image" class="header-logo-image"
src="./img/youtube-music.svg" src="./img/youtube-music.svg"
alt="YouTube Music"
/> />
</a> </a>
</h1> </h1>
@ -110,9 +110,9 @@
<div class="mockup-container"> <div class="mockup-container">
<div class="mockup-bg"> <div class="mockup-bg">
<img <img
src="./img/youtube-music.png"
id="mockup-header-img"
alt="YouTube Music" alt="YouTube Music"
id="mockup-header-img"
src="./img/youtube-music.png"
/> />
</div> </div>
</div> </div>
@ -127,11 +127,10 @@
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup"
src="./img/adblock.svg"
width="100px"
alt="Adblocker" alt="Adblocker"
class="device-mockup"
data-sr-id="0" data-sr-id="0"
src="./img/adblock.svg"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
@ -157,6 +156,7 @@
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 <div
@ -195,10 +195,10 @@
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup"
src="./img/download.svg"
alt="Downloader" alt="Downloader"
class="device-mockup"
data-sr-id="2" data-sr-id="2"
src="./img/download.svg"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
@ -265,10 +265,10 @@
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup"
src="./img/plugins.svg"
alt="Plugins" alt="Plugins"
class="device-mockup"
data-sr-id="3" data-sr-id="3"
src="./img/plugins.svg"
style=" style="
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
@ -337,10 +337,10 @@
<div class="feature-extended"> <div class="feature-extended">
<div class="feature-extended-image"> <div class="feature-extended-image">
<img <img
class="device-mockup"
src="./img/code.svg"
alt="Code" alt="Code"
class="device-mockup"
data-sr-id="4" data-sr-id="4"
src="./img/code.svg"
style=" style="
visibility: visible; visibility: visible;
width: 200%; width: 200%;
@ -442,7 +442,7 @@
<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">
@ -465,10 +465,10 @@
<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"
width="16"
height="16" height="16"
viewBox="0 0 1792 1792" viewBox="0 0 1792 1792"
width="16"
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"
@ -483,8 +483,8 @@
</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;
}; };
@ -164,9 +166,9 @@ starCanvas.prototype.start = function () {
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,7 +1,7 @@
<!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 {
@ -39,12 +39,12 @@
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>

414
index.js
View File

@ -1,20 +1,20 @@
"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({
@ -23,45 +23,47 @@ unhandled({
}); });
// 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() {
@ -72,11 +74,11 @@ function onClosed() {
/** @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,
() => { () => {
@ -84,146 +86,157 @@ function loadPlugins(win) {
}, },
() => { () => {
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 // Sandbox is only enabled in tests for now
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts // See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
sandbox: false, 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) { if (windowMaximized) {
win.maximize(); win.maximize();
} }
if(config.get("options.alwaysOnTop")){ if (config.get('options.alwaysOnTop')) {
win.setAlwaysOnTop(true); win.setAlwaysOnTop(true);
} }
const urlToLoad = config.get("options.resumeOnStart") const urlToLoad = config.get('options.resumeOnStart')
? config.get("url") ? config.get('url')
: config.defaultConfig.url; : config.defaultConfig.url;
win.webContents.loadURL(urlToLoad); win.webContents.loadURL(urlToLoad);
win.on("closed", onClosed); win.on('closed', onClosed);
const setPiPOptions = config.plugins.isEnabled("picture-in-picture") const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
? (key, value) => require("./plugins/picture-in-picture/back").setOptions({ [key]: value }) ? (key, value) => require('./plugins/picture-in-picture/back').setOptions({ [key]: value })
: () => {}; : () => {
};
win.on("move", () => { win.on('move', () => {
if (win.isMaximized()) return; if (win.isMaximized()) {
let position = win.getPosition(); return;
const isPiPEnabled = }
config.plugins.isEnabled("picture-in-picture") &&
config.plugins.getOptions("picture-in-picture")["isInPiP"]; const position = win.getPosition();
const isPiPEnabled
= config.plugins.isEnabled('picture-in-picture')
&& config.plugins.getOptions('picture-in-picture').isInPiP;
if (!isPiPEnabled) { if (!isPiPEnabled) {
lateSave("window-position", { x: position[0], y: position[1] }); lateSave('window-position', { x: position[0], y: position[1] });
} else if(config.plugins.getOptions("picture-in-picture")["savePosition"]) { } else if (config.plugins.getOptions('picture-in-picture').savePosition) {
lateSave("pip-position", position, setPiPOptions); lateSave('pip-position', position, setPiPOptions);
} }
}); });
let winWasMaximized; let winWasMaximized;
win.on("resize", () => { win.on('resize', () => {
const windowSize = win.getSize(); const windowSize = win.getSize();
const isMaximized = win.isMaximized(); const isMaximized = win.isMaximized();
const isPiPEnabled = const isPiPEnabled
config.plugins.isEnabled("picture-in-picture") && = config.plugins.isEnabled('picture-in-picture')
config.plugins.getOptions("picture-in-picture")["isInPiP"]; && config.plugins.getOptions('picture-in-picture').isInPiP;
if (!isPiPEnabled && winWasMaximized !== isMaximized) { if (!isPiPEnabled && winWasMaximized !== isMaximized) {
winWasMaximized = isMaximized; winWasMaximized = isMaximized;
config.set("window-maximized", isMaximized); config.set('window-maximized', isMaximized);
}
if (isMaximized) {
return;
} }
if (isMaximized) return;
if (!isPiPEnabled) { if (!isPiPEnabled) {
lateSave("window-size", { lateSave('window-size', {
width: windowSize[0], width: windowSize[0],
height: windowSize[1], height: windowSize[1],
}); });
} else if(config.plugins.getOptions("picture-in-picture")["saveSize"]) { } else if (config.plugins.getOptions('picture-in-picture').saveSize) {
lateSave("pip-size", windowSize, setPiPOptions); lateSave('pip-size', windowSize, setPiPOptions);
} }
}); });
let savedTimeouts = {}; const savedTimeouts = {};
function lateSave(key, value, fn = config.set) { function lateSave(key, value, fn = config.set) {
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]); if (savedTimeouts[key]) {
clearTimeout(savedTimeouts[key]);
}
savedTimeouts[key] = setTimeout(() => { savedTimeouts[key] = setTimeout(() => {
fn(key, value); fn(key, value);
@ -231,12 +244,12 @@ function createMainWindow() {
}, 600); }, 600);
} }
win.webContents.on("render-process-gone", (event, webContents, details) => { win.webContents.on('render-process-gone', (event, webContents, details) => {
showUnresponsiveDialog(win, details); showUnresponsiveDialog(win, details);
}); });
win.once("ready-to-show", () => { win.once('ready-to-show', () => {
if (config.get("options.appVisible")) { if (config.get('options.appVisible')) {
win.show(); win.show();
} }
}); });
@ -246,29 +259,30 @@ function createMainWindow() {
return win; 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 }); cb({ requestHeaders: details.requestHeaders });
}); });
} }
@ -276,7 +290,7 @@ app.once("browser-window-created", (event, win) => {
setupSongInfo(win); setupSongInfo(win);
setupAppControls(); setupAppControls();
win.webContents.on("did-fail-load", ( win.webContents.on('did-fail-load', (
_event, _event,
errorCode, errorCode,
errorDescription, errorDescription,
@ -286,30 +300,31 @@ app.once("browser-window-created", (event, win) => {
frameRoutingId, frameRoutingId,
) => { ) => {
const log = JSON.stringify({ const log = JSON.stringify({
error: "did-fail-load", error: 'did-fail-load',
errorCode, errorCode,
errorDescription, errorDescription,
validatedURL, validatedURL,
isMainFrame, isMainFrame,
frameProcessId, frameProcessId,
frameRoutingId, frameRoutingId,
}, null, "\t"); }, null, '\t');
if (is.dev()) { if (is.dev()) {
console.log(log); 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); if (!(config.plugins.isEnabled('in-app-menu') && errorCode === -3)) { // -3 is a false positive with in-app-menu
win.webContents.loadFile(path.join(__dirname, "error.html")); win.webContents.send('log', log);
win.webContents.loadFile(path.join(__dirname, 'error.html'));
} }
}); });
win.webContents.on("will-prevent-unload", (event) => { win.webContents.on('will-prevent-unload', (event) => {
event.preventDefault(); event.preventDefault();
}); });
}); });
app.on("window-all-closed", () => { app.on('window-all-closed', () => {
if (process.platform !== "darwin") { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
@ -317,7 +332,7 @@ app.on("window-all-closed", () => {
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) {
@ -327,45 +342,46 @@ app.on("activate", () => {
} }
}); });
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(); electron.session.defaultSession.clearCache();
clearTimeout(clearCacheTimeout); clearTimeout(clearCacheTimeout);
}, 20000); }, 20_000);
} }
// Register appID on windows // Register appID on windows
if (is.windows()) { if (is.windows()) {
const appID = "com.github.th-ch.youtube-music"; const appID = 'com.github.th-ch.youtube-music';
app.setAppUserModelId(appID); app.setAppUserModelId(appID);
const appLocation = process.execPath; const appLocation = process.execPath;
const appData = app.getPath("appData"); const appData = app.getPath('appData');
// check shortcut validity if not in dev mode / running portable app // Check shortcut validity if not in dev mode / running portable app
if (!is.dev() && !appLocation.startsWith(path.join(appData, "..", "Local", "Temp"))) { if (!is.dev() && !appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))) {
const shortcutPath = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "YouTube Music.lnk"); const shortcutPath = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'YouTube Music.lnk');
try { // check if shortcut is registered and valid try { // Check if shortcut is registered and valid
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
if ( if (
shortcutDetails.target !== appLocation || shortcutDetails.target !== appLocation
shortcutDetails.appUserModelId !== appID || shortcutDetails.appUserModelId !== appID
) { ) {
throw "needUpdate"; throw 'needUpdate';
} }
} catch (error) { // if not valid -> Register shortcut } catch (error) { // If not valid -> Register shortcut
electron.shell.writeShortcutLink( electron.shell.writeShortcutLink(
shortcutPath, shortcutPath,
error === "needUpdate" ? "update" : "create", error === 'needUpdate' ? 'update' : 'create',
{ {
target: appLocation, target: appLocation,
cwd: path.dirname(appLocation), cwd: path.dirname(appLocation),
description: "YouTube Music Desktop App - including custom plugins", description: 'YouTube Music Desktop App - including custom plugins',
appUserModelId: appID, appUserModelId: appID,
} },
); );
} }
} }
@ -379,77 +395,95 @@ app.on("ready", () => {
app.on('second-instance', (_event, commandLine, _workingDirectory) => { app.on('second-instance', (_event, commandLine, _workingDirectory) => {
const uri = `${APP_PROTOCOL}://`; const uri = `${APP_PROTOCOL}://`;
const protocolArgv = commandLine.find(arg => arg.startsWith(uri)); const protocolArgv = commandLine.find((arg) => arg.startsWith(uri));
if (protocolArgv) { if (protocolArgv) {
const lastIndex = protocolArgv.endsWith("/") ? -1 : undefined; const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
const command = protocolArgv.slice(uri.length, lastIndex); const command = protocolArgv.slice(uri.length, lastIndex);
if (is.dev()) console.debug(`Received command over protocol: "${command}"`); if (is.dev()) {
console.debug(`Received command over protocol: "${command}"`);
}
handleProtocol(command); handleProtocol(command);
return; return;
} }
if (!mainWindow) return;
if (mainWindow.isMinimized()) mainWindow.restore(); if (!mainWindow) {
if (!mainWindow.isVisible()) mainWindow.show(); return;
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus(); mainWindow.focus();
}); });
// Autostart at login // Autostart at login
app.setLoginItemSettings({ app.setLoginItemSettings({
openAtLogin: config.get("options.startAtLogin"), openAtLogin: config.get('options.startAtLogin'),
}); });
if (!is.dev() && config.get("options.autoUpdates")) { if (!is.dev() && config.get('options.autoUpdates')) {
const updateTimeout = setTimeout(() => { const updateTimeout = setTimeout(() => {
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
clearTimeout(updateTimeout); clearTimeout(updateTimeout);
}, 2000); }, 2000);
autoUpdater.on("update-available", () => { autoUpdater.on('update-available', () => {
const downloadLink = const downloadLink
"https://github.com/th-ch/youtube-music/releases/latest"; = 'https://github.com/th-ch/youtube-music/releases/latest';
const dialogOpts = { const dialogOptions = {
type: "info", type: 'info',
buttons: ["OK", "Download", "Disable updates"], buttons: ['OK', 'Download', 'Disable updates'],
title: "Application Update", title: 'Application Update',
message: "A new version is available", message: 'A new version is available',
detail: `A new version is available and can be downloaded at ${downloadLink}`, detail: `A new version is available and can be downloaded at ${downloadLink}`,
}; };
electron.dialog.showMessageBox(dialogOpts).then((dialogOutput) => { electron.dialog.showMessageBox(dialogOptions).then((dialogOutput) => {
switch (dialogOutput.response) { switch (dialogOutput.response) {
// Download // Download
case 1: case 1: {
electron.shell.openExternal(downloadLink); electron.shell.openExternal(downloadLink);
break; break;
}
// Disable updates // Disable updates
case 2: case 2: {
config.set("options.autoUpdates", false); config.set('options.autoUpdates', false);
break; break;
default: }
default: {
break; break;
} }
}
}); });
}); });
} }
if (config.get("options.hideMenu") && !config.get("options.hideMenuWarned")) { if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
electron.dialog.showMessageBox(mainWindow, { electron.dialog.showMessageBox(mainWindow, {
type: 'info', title: 'Hide Menu Enabled', type: 'info', title: 'Hide Menu Enabled',
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)" message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
}); });
config.set("options.hideMenuWarned", true); config.set('options.hideMenuWarned', true);
} }
// Optimized for Mac OS X // Optimized for Mac OS X
if (is.macOS() && !config.get("options.appVisible")) { if (is.macOS() && !config.get('options.appVisible')) {
app.dock.hide(); app.dock.hide();
} }
let forceQuit = false; let forceQuit = false;
app.on("before-quit", () => { app.on('before-quit', () => {
forceQuit = true; forceQuit = true;
}); });
if (is.macOS() || config.get("options.tray")) { if (is.macOS() || config.get('options.tray')) {
mainWindow.on("close", (event) => { mainWindow.on('close', (event) => {
// Hide the window instead of quitting (quit is available in tray options) // Hide the window instead of quitting (quit is available in tray options)
if (!forceQuit) { if (!forceQuit) {
event.preventDefault(); event.preventDefault();
@ -460,26 +494,34 @@ app.on("ready", () => {
}); });
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, { electron.dialog.showMessageBox(win, {
type: "error", type: 'error',
title: "Window Unresponsive", title: 'Window Unresponsive',
message: "The Application is Unresponsive", message: 'The Application is Unresponsive',
details: "We are sorry for the inconvenience! please choose what to do:", details: 'We are sorry for the inconvenience! please choose what to do:',
buttons: ["Wait", "Relaunch", "Quit"], buttons: ['Wait', 'Relaunch', 'Quit'],
cancelId: 0 cancelId: 0,
}).then( result => { }).then((result) => {
switch (result.response) { switch (result.response) {
case 1: restart(); break; case 1: {
case 2: app.quit(); break; 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.
@ -487,18 +529,18 @@ function removeContentSecurityPolicy(
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) {
@ -508,7 +550,7 @@ function removeContentSecurityPolicy(
const result = await listener.apply(); const result = await listener.apply();
return { ...accumulator, ...result }; return { ...accumulator, ...result };
}, },
{ cancel: false } { cancel: false },
); );
return response; return response;

423
menu.js
View File

@ -1,30 +1,30 @@
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");
const pluginEnabledMenu = (plugin, label = "", hasSubmenu = false, refreshMenu = undefined) => ({
label: label || plugin, label: label || plugin,
type: "checkbox", type: 'checkbox',
checked: config.plugins.isEnabled(plugin), checked: config.plugins.isEnabled(plugin),
click: (item) => { click(item) {
if (item.checked) { if (item.checked) {
config.plugins.enable(plugin); config.plugins.enable(plugin);
} else { } else {
config.plugins.disable(plugin); config.plugins.disable(plugin);
} }
if (hasSubmenu) { if (hasSubmenu) {
refreshMenu(); refreshMenu();
} }
@ -35,54 +35,58 @@ 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", label: 'Plugins',
submenu: [ submenu:
...getAllPlugins().map((plugin) => { getAllPlugins().map((plugin) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js") const pluginPath = path.join(__dirname, 'plugins', plugin, 'menu.js');
if (existsSync(pluginPath)) { if (existsSync(pluginPath)) {
let pluginLabel = plugin; let pluginLabel = plugin;
if (pluginLabel === "crossfade") { if (pluginLabel === 'crossfade') {
pluginLabel = "crossfade [beta]"; pluginLabel = 'crossfade [beta]';
} }
if (!config.plugins.isEnabled(plugin)) { if (!config.plugins.isEnabled(plugin)) {
return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu); return pluginEnabledMenu(plugin, pluginLabel, true, refreshMenu);
} }
const getPluginMenu = require(pluginPath); const getPluginMenu = require(pluginPath);
return { return {
label: pluginLabel, label: pluginLabel,
submenu: [ submenu: [
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu), pluginEnabledMenu(plugin, 'Enabled', true, refreshMenu),
{ type: "separator" }, { type: 'separator' },
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu), ...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
], ],
}; };
} }
return pluginEnabledMenu(plugin); return pluginEnabledMenu(plugin);
}), })
], ,
}, },
{ {
label: "Options", label: 'Options',
submenu: [ submenu: [
{ {
label: "Auto-update", label: 'Auto-update',
type: "checkbox", type: 'checkbox',
checked: config.get("options.autoUpdates"), checked: config.get('options.autoUpdates'),
click: (item) => { click(item) {
config.setMenuOption("options.autoUpdates", item.checked); config.setMenuOption('options.autoUpdates', item.checked);
}, },
}, },
{ {
label: "Resume last song when app starts", label: 'Resume last song when app starts',
type: "checkbox", type: 'checkbox',
checked: config.get("options.resumeOnStart"), checked: config.get('options.resumeOnStart'),
click: (item) => { click(item) {
config.setMenuOption("options.resumeOnStart", item.checked); config.setMenuOption('options.resumeOnStart', item.checked);
}, },
}, },
{ {
@ -91,74 +95,74 @@ const mainMenuTemplate = (win) => {
label: name, label: name,
type: 'radio', type: 'radio',
checked: config.get('options.startingPage') === name, checked: config.get('options.startingPage') === name,
click: () => { click() {
config.set('options.startingPage', name); config.set('options.startingPage', name);
}, },
})) })),
}, },
{ {
label: "Visual Tweaks", label: 'Visual Tweaks',
submenu: [ submenu: [
{ {
label: "Remove upgrade button", label: 'Remove upgrade button',
type: "checkbox", type: 'checkbox',
checked: config.get("options.removeUpgradeButton"), checked: config.get('options.removeUpgradeButton'),
click: (item) => { click(item) {
config.setMenuOption("options.removeUpgradeButton", item.checked); config.setMenuOption('options.removeUpgradeButton', item.checked);
}, },
}, },
{ {
label: "Like buttons", label: 'Like buttons',
submenu: [ submenu: [
{ {
label: "Default", label: 'Default',
type: "radio", type: 'radio',
checked: !config.get("options.likeButtons"), checked: !config.get('options.likeButtons'),
click: () => { click() {
config.set("options.likeButtons", ''); config.set('options.likeButtons', '');
}, },
}, },
{ {
label: "Force show", label: 'Force show',
type: "radio", type: 'radio',
checked: config.get("options.likeButtons") === 'force', checked: config.get('options.likeButtons') === 'force',
click: () => { click() {
config.set("options.likeButtons", 'force'); config.set('options.likeButtons', 'force');
} },
}, },
{ {
label: "Hide", label: 'Hide',
type: "radio", type: 'radio',
checked: config.get("options.likeButtons") === 'hide', checked: config.get('options.likeButtons') === 'hide',
click: () => { click() {
config.set("options.likeButtons", 'hide'); config.set('options.likeButtons', 'hide');
} },
}, },
], ],
}, },
{ {
label: "Theme", label: 'Theme',
submenu: [ submenu: [
{ {
label: "No theme", label: 'No theme',
type: "radio", type: 'radio',
checked: !config.get("options.themes"), // todo rename "themes" checked: !config.get('options.themes'), // Todo rename "themes"
click: () => { click() {
config.set("options.themes", []); config.set('options.themes', []);
}, },
}, },
{ type: "separator" }, { type: 'separator' },
{ {
label: "Import custom CSS file", label: 'Import custom CSS file',
type: "radio", type: 'radio',
checked: false, checked: false,
click: async () => { async click() {
const { filePaths } = await dialog.showOpenDialog({ const { filePaths } = await dialog.showOpenDialog({
filters: [{ name: "CSS Files", extensions: ["css"] }], filters: [{ name: 'CSS Files', extensions: ['css'] }],
properties: ["openFile", "multiSelections"], properties: ['openFile', 'multiSelections'],
}); });
if (filePaths) { if (filePaths) {
config.set("options.themes", filePaths); config.set('options.themes', filePaths);
} }
}, },
}, },
@ -167,37 +171,38 @@ const mainMenuTemplate = (win) => {
], ],
}, },
{ {
label: "Single instance lock", label: 'Single instance lock',
type: "checkbox", type: 'checkbox',
checked: true, checked: true,
click: (item) => { click(item) {
if (!item.checked && app.hasSingleInstanceLock()) if (!item.checked && app.hasSingleInstanceLock()) {
app.releaseSingleInstanceLock(); app.releaseSingleInstanceLock();
else if (item.checked && !app.hasSingleInstanceLock()) } else if (item.checked && !app.hasSingleInstanceLock()) {
app.requestSingleInstanceLock(); app.requestSingleInstanceLock();
}
}, },
}, },
{ {
label: "Always on top", label: 'Always on top',
type: "checkbox", type: 'checkbox',
checked: config.get("options.alwaysOnTop"), checked: config.get('options.alwaysOnTop'),
click: (item) => { click(item) {
config.setMenuOption("options.alwaysOnTop", item.checked); config.setMenuOption('options.alwaysOnTop', item.checked);
win.setAlwaysOnTop(item.checked); win.setAlwaysOnTop(item.checked);
}, },
}, },
...(is.windows() || is.linux() ...(is.windows() || is.linux()
? [ ? [
{ {
label: "Hide menu", label: 'Hide menu',
type: "checkbox", type: 'checkbox',
checked: config.get("options.hideMenu"), checked: config.get('options.hideMenu'),
click: (item) => { click(item) {
config.setMenuOption("options.hideMenu", item.checked); config.setMenuOption('options.hideMenu', item.checked);
if (item.checked && !config.get("options.hideMenuWarned")) { if (item.checked && !config.get('options.hideMenuWarned')) {
dialog.showMessageBox(win, { dialog.showMessageBox(win, {
type: 'info', title: 'Hide Menu Enabled', 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)" message: 'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
}); });
} }
}, },
@ -209,108 +214,108 @@ const mainMenuTemplate = (win) => {
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows // https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
[ [
{ {
label: "Start at login", label: 'Start at login',
type: "checkbox", type: 'checkbox',
checked: config.get("options.startAtLogin"), checked: config.get('options.startAtLogin'),
click: (item) => { click(item) {
config.setMenuOption("options.startAtLogin", item.checked); config.setMenuOption('options.startAtLogin', item.checked);
}, },
}, },
] ]
: []), : []),
{ {
label: "Tray", label: 'Tray',
submenu: [ submenu: [
{ {
label: "Disabled", label: 'Disabled',
type: "radio", type: 'radio',
checked: !config.get("options.tray"), checked: !config.get('options.tray'),
click: () => { click() {
config.setMenuOption("options.tray", false); config.setMenuOption('options.tray', false);
config.setMenuOption("options.appVisible", true); config.setMenuOption('options.appVisible', true);
}, },
}, },
{ {
label: "Enabled + app visible", label: 'Enabled + app visible',
type: "radio", type: 'radio',
checked: checked:
config.get("options.tray") && config.get("options.appVisible"), config.get('options.tray') && config.get('options.appVisible'),
click: () => { click() {
config.setMenuOption("options.tray", true); config.setMenuOption('options.tray', true);
config.setMenuOption("options.appVisible", true); config.setMenuOption('options.appVisible', true);
}, },
}, },
{ {
label: "Enabled + app hidden", label: 'Enabled + app hidden',
type: "radio", type: 'radio',
checked: checked:
config.get("options.tray") && !config.get("options.appVisible"), config.get('options.tray') && !config.get('options.appVisible'),
click: () => { click() {
config.setMenuOption("options.tray", true); config.setMenuOption('options.tray', true);
config.setMenuOption("options.appVisible", false); config.setMenuOption('options.appVisible', false);
}, },
}, },
{ type: "separator" }, { type: 'separator' },
{ {
label: "Play/Pause on click", label: 'Play/Pause on click',
type: "checkbox", type: 'checkbox',
checked: config.get("options.trayClickPlayPause"), checked: config.get('options.trayClickPlayPause'),
click: (item) => { click(item) {
config.setMenuOption("options.trayClickPlayPause", item.checked); config.setMenuOption('options.trayClickPlayPause', item.checked);
}, },
}, },
], ],
}, },
{ type: "separator" }, { type: 'separator' },
{ {
label: "Advanced options", label: 'Advanced options',
submenu: [ submenu: [
{ {
label: "Proxy", label: 'Proxy',
type: "checkbox", type: 'checkbox',
checked: !!config.get("options.proxy"), checked: Boolean(config.get('options.proxy')),
click: (item) => { click(item) {
setProxy(item, win); setProxy(item, win);
}, },
}, },
{ {
label: "Override useragent", label: 'Override useragent',
type: "checkbox", type: 'checkbox',
checked: config.get("options.overrideUserAgent"), checked: config.get('options.overrideUserAgent'),
click: (item) => { click(item) {
config.setMenuOption("options.overrideUserAgent", item.checked); config.setMenuOption('options.overrideUserAgent', item.checked);
}
},
{
label: "Disable hardware acceleration",
type: "checkbox",
checked: config.get("options.disableHardwareAcceleration"),
click: (item) => {
config.setMenuOption("options.disableHardwareAcceleration", item.checked);
}, },
}, },
{ {
label: "Restart on config changes", label: 'Disable hardware acceleration',
type: "checkbox", type: 'checkbox',
checked: config.get("options.restartOnConfigChanges"), checked: config.get('options.disableHardwareAcceleration'),
click: (item) => { click(item) {
config.setMenuOption("options.restartOnConfigChanges", item.checked); config.setMenuOption('options.disableHardwareAcceleration', item.checked);
}, },
}, },
{ {
label: "Reset App cache when app starts", label: 'Restart on config changes',
type: "checkbox", type: 'checkbox',
checked: config.get("options.autoResetAppCache"), checked: config.get('options.restartOnConfigChanges'),
click: (item) => { click(item) {
config.setMenuOption("options.autoResetAppCache", item.checked); config.setMenuOption('options.restartOnConfigChanges', item.checked);
}, },
}, },
{ type: "separator" },
is.macOS() ?
{ {
label: "Toggle DevTools", label: 'Reset App cache when app starts',
type: 'checkbox',
checked: config.get('options.autoResetAppCache'),
click(item) {
config.setMenuOption('options.autoResetAppCache', item.checked);
},
},
{ type: 'separator' },
is.macOS()
? {
label: 'Toggle DevTools',
// Cannot use "toggleDevTools" role in MacOS // Cannot use "toggleDevTools" role in MacOS
click: () => { click() {
const { webContents } = win; const { webContents } = win;
if (webContents.isDevToolsOpened()) { if (webContents.isDevToolsOpened()) {
webContents.closeDevTools(); webContents.closeDevTools();
@ -319,93 +324,93 @@ const mainMenuTemplate = (win) => {
webContents.openDevTools(devToolsOptions); webContents.openDevTools(devToolsOptions);
} }
}, },
} : }
{ role: "toggleDevTools" }, : { role: 'toggleDevTools' },
{ {
label: "Edit config.json", label: 'Edit config.json',
click: () => { click() {
config.edit(); config.edit();
}, },
}, },
] ],
}, },
], ],
}, },
{ {
label: "View", label: 'View',
submenu: [ submenu: [
{ role: "reload" }, { role: 'reload' },
{ role: "forceReload" }, { role: 'forceReload' },
{ type: "separator" }, { type: 'separator' },
{ role: "zoomIn" }, { role: 'zoomIn' },
{ role: "zoomOut" }, { role: 'zoomOut' },
{ role: "resetZoom" }, { role: 'resetZoom' },
{ type: "separator" }, { type: 'separator' },
{ role: "togglefullscreen" }, { role: 'togglefullscreen' },
], ],
}, },
{ {
label: "Navigation", label: 'Navigation',
submenu: [ submenu: [
{ {
label: "Go back", label: 'Go back',
click: () => { click() {
if (win.webContents.canGoBack()) { if (win.webContents.canGoBack()) {
win.webContents.goBack(); win.webContents.goBack();
} }
}, },
}, },
{ {
label: "Go forward", label: 'Go forward',
click: () => { click() {
if (win.webContents.canGoForward()) { if (win.webContents.canGoForward()) {
win.webContents.goForward(); win.webContents.goForward();
} }
}, },
}, },
{ {
label: "Copy current URL", label: 'Copy current URL',
click: () => { click() {
const currentURL = win.webContents.getURL(); const currentURL = win.webContents.getURL();
clipboard.writeText(currentURL); clipboard.writeText(currentURL);
}, },
}, },
{ {
label: "Restart App", label: 'Restart App',
click: restart click: restart,
}, },
{ role: "quit" }, { 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' },
], ],
}); });
} }
@ -418,20 +423,20 @@ 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
} }
} }

2522
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -146,30 +146,16 @@
"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",
"eslint": "8.48.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-prettier": "5.0.0",
"node-gyp": "9.4.0", "node-gyp": "9.4.0",
"playwright": "1.37.1", "playwright": "1.37.1"
"xo": "0.56.0"
}, },
"auto-changelog": { "auto-changelog": {
"hideCredit": true, "hideCredit": true,
"package": true, "package": true,
"unreleased": true, "unreleased": true,
"output": "changelog.md" "output": "changelog.md"
},
"xo": {
"envs": [
"node",
"browser"
],
"rules": {
"quotes": [
"error",
"double",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
]
}
} }
} }

View File

@ -1,5 +1,5 @@
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()) {

View File

@ -1,32 +1,32 @@
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,
} }
@ -40,21 +40,21 @@ const loadAdBlockerEngine = (
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 };

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,7 +7,7 @@
*/ */
{ {
let pruner = function (o) { const pruner = function (o) {
delete o.playerAds; delete o.playerAds;
delete o.adPlacements; delete o.adPlacements;
// //
@ -15,60 +15,108 @@
delete o.playerResponse.playerAds; delete o.playerResponse.playerAds;
delete o.playerResponse.adPlacements; delete o.playerResponse.adPlacements;
} }
// //
return o; return o;
}; };
JSON.parse = new Proxy(JSON.parse, { JSON.parse = new Proxy(JSON.parse, {
apply: function () { apply() {
return pruner(Reflect.apply(...arguments)); return pruner(Reflect.apply(...arguments));
}, },
}); });
Response.prototype.json = new Proxy(Response.prototype.json, { Response.prototype.json = new Proxy(Response.prototype.json, {
apply: function () { apply() {
return Reflect.apply(...arguments).then((o) => pruner(o)); 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") case "''": {
cValue = '';
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 () { cValue = function () {
return true; return true;
}; };
else if (cValue === "falseFunc")
break;
}
case 'falseFunc': {
cValue = function () { cValue = function () {
return false; return false;
}; };
else if (/^\d+$/.test(cValue)) {
cValue = parseFloat(cValue); break;
}
default: {
if (/^\d+$/.test(cValue)) {
cValue = Number.parseFloat(cValue);
// //
if (isNaN(cValue)) return; if (isNaN(cValue)) {
if (Math.abs(cValue) > 0x7fff) return; return;
}
if (Math.abs(cValue) > 0x7F_FF) {
return;
}
} else { } else {
return; return;
} }
}
}
// //
let aborted = false; let aborted = false;
const mustAbort = function (v) { const mustAbort = function (v) {
if (aborted) return true; if (aborted) {
aborted = return true;
v !== undefined && }
v !== null &&
cValue !== undefined && aborted
cValue !== null && = v !== undefined
typeof v !== typeof cValue; && v !== null
&& cValue !== undefined
&& cValue !== null
&& typeof v !== typeof cValue;
return aborted; return aborted;
}; };
@ -81,28 +129,41 @@
if (handler.init(owner[prop]) === false) { if (handler.init(owner[prop]) === false) {
return; return;
} }
// //
const odesc = Object.getOwnPropertyDescriptor(owner, prop); const odesc = Object.getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter; let previousGetter;
let previousSetter;
if (odesc instanceof Object) { if (odesc instanceof Object) {
if (odesc.configurable === false) return; if (odesc.configurable === false) {
if (odesc.get instanceof Function) prevGetter = odesc.get; return;
if (odesc.set instanceof Function) prevSetter = odesc.set;
} }
if (odesc.get instanceof Function) {
previousGetter = odesc.get;
}
if (odesc.set instanceof Function) {
previousSetter = odesc.set;
}
}
// //
Object.defineProperty(owner, prop, { Object.defineProperty(owner, prop, {
configurable, configurable,
get() { get() {
if (prevGetter !== undefined) { if (previousGetter !== undefined) {
prevGetter(); previousGetter();
} }
// //
return handler.getter(); return handler.getter();
}, },
set(a) { set(a) {
if (prevSetter !== undefined) { if (previousSetter !== undefined) {
prevSetter(a); previousSetter(a);
} }
// //
handler.setter(a); handler.setter(a);
}, },
@ -110,19 +171,25 @@
}; };
const trapChain = function (owner, chain) { const trapChain = function (owner, chain) {
const pos = chain.indexOf("."); const pos = chain.indexOf('.');
if (pos === -1) { if (pos === -1) {
trapProp(owner, chain, false, { trapProp(owner, chain, false, {
v: undefined, v: undefined,
getter: function () { getter() {
return document.currentScript === thisScript ? this.v : cValue; return document.currentScript === thisScript ? this.v : cValue;
}, },
setter: function (a) { setter(a) {
if (mustAbort(a) === false) return; if (mustAbort(a) === false) {
return;
}
cValue = a; cValue = a;
}, },
init: function (v) { init(v) {
if (mustAbort(v)) return false; if (mustAbort(v)) {
return false;
}
// //
this.v = v; this.v = v;
return true; return true;
@ -131,72 +198,124 @@
// //
return; return;
} }
// //
const prop = chain.slice(0, pos); const prop = chain.slice(0, pos);
const v = owner[prop]; const v = owner[prop];
// //
chain = chain.slice(pos + 1); chain = chain.slice(pos + 1);
if (v instanceof Object || (typeof v === "object" && v !== null)) { if (v instanceof Object || (typeof v === 'object' && v !== null)) {
trapChain(v, chain); trapChain(v, chain);
return; return;
} }
// //
trapProp(owner, prop, true, { trapProp(owner, prop, true, {
v: undefined, v: undefined,
getter: function () { getter() {
return this.v; return this.v;
}, },
setter: function (a) { setter(a) {
this.v = a; this.v = a;
if (a instanceof Object) trapChain(a, chain); if (a instanceof Object) {
trapChain(a, chain);
}
}, },
init: function (v) { init(v) {
this.v = v; this.v = v;
return true; return true;
}, },
}); });
}; };
// //
trapChain(window, chain); 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") case "''": {
cValue = '';
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 () { cValue = function () {
return true; return true;
}; };
else if (cValue === "falseFunc")
break;
}
case 'falseFunc': {
cValue = function () { cValue = function () {
return false; return false;
}; };
else if (/^\d+$/.test(cValue)) {
cValue = parseFloat(cValue); break;
}
default: {
if (/^\d+$/.test(cValue)) {
cValue = Number.parseFloat(cValue);
// //
if (isNaN(cValue)) return; if (isNaN(cValue)) {
if (Math.abs(cValue) > 0x7fff) return; return;
}
if (Math.abs(cValue) > 0x7F_FF) {
return;
}
} else { } else {
return; return;
} }
}
}
// //
let aborted = false; let aborted = false;
const mustAbort = function (v) { const mustAbort = function (v) {
if (aborted) return true; if (aborted) {
aborted = return true;
v !== undefined && }
v !== null &&
cValue !== undefined && aborted
cValue !== null && = v !== undefined
typeof v !== typeof cValue; && v !== null
&& cValue !== undefined
&& cValue !== null
&& typeof v !== typeof cValue;
return aborted; return aborted;
}; };
@ -209,28 +328,41 @@
if (handler.init(owner[prop]) === false) { if (handler.init(owner[prop]) === false) {
return; return;
} }
// //
const odesc = Object.getOwnPropertyDescriptor(owner, prop); const odesc = Object.getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter; let previousGetter;
let previousSetter;
if (odesc instanceof Object) { if (odesc instanceof Object) {
if (odesc.configurable === false) return; if (odesc.configurable === false) {
if (odesc.get instanceof Function) prevGetter = odesc.get; return;
if (odesc.set instanceof Function) prevSetter = odesc.set;
} }
if (odesc.get instanceof Function) {
previousGetter = odesc.get;
}
if (odesc.set instanceof Function) {
previousSetter = odesc.set;
}
}
// //
Object.defineProperty(owner, prop, { Object.defineProperty(owner, prop, {
configurable, configurable,
get() { get() {
if (prevGetter !== undefined) { if (previousGetter !== undefined) {
prevGetter(); previousGetter();
} }
// //
return handler.getter(); return handler.getter();
}, },
set(a) { set(a) {
if (prevSetter !== undefined) { if (previousSetter !== undefined) {
prevSetter(a); previousSetter(a);
} }
// //
handler.setter(a); handler.setter(a);
}, },
@ -238,19 +370,25 @@
}; };
const trapChain = function (owner, chain) { const trapChain = function (owner, chain) {
const pos = chain.indexOf("."); const pos = chain.indexOf('.');
if (pos === -1) { if (pos === -1) {
trapProp(owner, chain, false, { trapProp(owner, chain, false, {
v: undefined, v: undefined,
getter: function () { getter() {
return document.currentScript === thisScript ? this.v : cValue; return document.currentScript === thisScript ? this.v : cValue;
}, },
setter: function (a) { setter(a) {
if (mustAbort(a) === false) return; if (mustAbort(a) === false) {
return;
}
cValue = a; cValue = a;
}, },
init: function (v) { init(v) {
if (mustAbort(v)) return false; if (mustAbort(v)) {
return false;
}
// //
this.v = v; this.v = v;
return true; return true;
@ -259,31 +397,36 @@
// //
return; return;
} }
// //
const prop = chain.slice(0, pos); const prop = chain.slice(0, pos);
const v = owner[prop]; const v = owner[prop];
// //
chain = chain.slice(pos + 1); chain = chain.slice(pos + 1);
if (v instanceof Object || (typeof v === "object" && v !== null)) { if (v instanceof Object || (typeof v === 'object' && v !== null)) {
trapChain(v, chain); trapChain(v, chain);
return; return;
} }
// //
trapProp(owner, prop, true, { trapProp(owner, prop, true, {
v: undefined, v: undefined,
getter: function () { getter() {
return this.v; return this.v;
}, },
setter: function (a) { setter(a) {
this.v = a; this.v = a;
if (a instanceof Object) trapChain(a, chain); if (a instanceof Object) {
trapChain(a, chain);
}
}, },
init: function (v) { init(v) {
this.v = v; this.v = v;
return true; return true;
}, },
}); });
}; };
// //
trapChain(window, chain); trapChain(window, chain);
})(); })();

View File

@ -1,14 +1,14 @@
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,5 +1,5 @@
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;
@ -13,7 +13,7 @@ const applyCompressor = (e) => {
}; };
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,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,13 +1,17 @@
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 () => {
@ -17,61 +21,63 @@ module.exports = async () => {
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"); api.loadModule('captions');
setTimeout(async () => { setTimeout(async () => {
captionTrackList = api.getOption("captions", "tracklist"); captionTrackList = api.getOption('captions', 'tracklist');
if (config.autoload && config.lastCaptionsCode) { if (config.autoload && config.lastCaptionsCode) {
api.setOption("captions", "track", { api.setOption('captions', 'track', {
languageCode: config.lastCaptionsCode, languageCode: config.lastCaptionsCode,
}); });
} }
captionsSettingsButton.style.display = captionTrackList?.length captionsSettingsButton.style.display = captionTrackList?.length
? "inline-block" ? 'inline-block'
: "none"; : 'none';
}, 250); }, 250);
}); });
captionsSettingsButton.onclick = async () => { captionsSettingsButton.addEventListener('click', async () => {
if (captionTrackList?.length) { if (captionTrackList?.length) {
const currentCaptionTrack = api.getOption("captions", "track"); const currentCaptionTrack = api.getOption('captions', 'track');
let currentIndex = !currentCaptionTrack ? let currentIndex = currentCaptionTrack
null : ? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode))
captionTrackList.indexOf(captionTrackList.find(track => track.languageCode === currentCaptionTrack.languageCode)); : null;
const captionLabels = [ const captionLabels = [
...captionTrackList.map(track => track.displayName), ...captionTrackList.map((track) => track.displayName),
'None' 'None',
]; ];
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex);
if (currentIndex === null) return; if (currentIndex === null) {
return;
}
const newCaptions = captionTrackList[currentIndex]; const newCaptions = captionTrackList[currentIndex];
configProvider.set('lastCaptionsCode', newCaptions?.languageCode); configProvider.set('lastCaptionsCode', newCaptions?.languageCode);
if (newCaptions) { if (newCaptions) {
api.setOption("captions", "track", { languageCode: newCaptions.languageCode }); api.setOption('captions', 'track', { languageCode: newCaptions.languageCode });
} else { } else {
api.setOption("captions", "track", {}); api.setOption('captions', 'track', {});
} }
setTimeout(() => api.playVideo()); 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", label: 'No captions by default',
type: "checkbox", type: 'checkbox',
checked: config.get("disabledCaptions"), checked: config.get('disabledCaptions'),
click: (item) => { click(item) {
config.set('disableCaptions', item.checked); config.set('disableCaptions', item.checked);
}, },
} },
]; ];

View File

@ -1,12 +1,16 @@
<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">
<svg class="style-scope yt-icon"
focusable="false" preserveAspectRatio="xMidYMid meet"
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="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" class="style-scope tp-yt-iron-icon"
class="style-scope tp-yt-iron-icon"></path> 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> </g>
</svg> </svg>
</tp-yt-iron-icon> </tp-yt-iron-icon>

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,12 +1,12 @@
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);

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,21 +16,21 @@
*/ */
(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 { } else {
// abort and throw an exception // Abort and throw an exception
throw new TypeError("Number between 0 and 1 expected as volume!"); throw new TypeError('Number between 0 and 1 expected as volume!');
} }
}; };
// main class // Main class
class VolumeFader { class VolumeFader {
/** /**
* VolumeFader Constructor * VolumeFader Constructor
@ -46,67 +46,67 @@
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms) * .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
*/ */
constructor(media, options) { constructor(media, options) {
// passed media element of correct type? // Passed media element of correct type?
if (media instanceof HTMLMediaElement) { if (media instanceof HTMLMediaElement) {
// save reference to media element // Save reference to media element
this.media = media; this.media = media;
} else { } else {
// abort and throw an exception // Abort and throw an exception
throw new TypeError("Media element expected!"); throw new TypeError('Media element expected!');
} }
// make sure options is an object // Make sure options is an object
options = options || {}; options = options || {};
// log function passed? // Log function passed?
if (typeof options.logger == "function") { if (typeof options.logger === 'function') {
// set log function to the one specified // Set log function to the one specified
this.logger = options.logger; this.logger = options.logger;
} else { } else {
// set log function explicitly to false // Set log function explicitly to false
this.logger = false; this.logger = false;
} }
// linear volume fading? // Linear volume fading?
if (options.fadeScaling == "linear") { if (options.fadeScaling == 'linear') {
// pass levels unchanged // Pass levels unchanged
this.scale = { this.scale = {
internalToVolume: (level) => level, internalToVolume: (level) => level,
volumeToInternal: (level) => level, volumeToInternal: (level) => level,
}; };
// log setting // Log setting
this.logger && this.logger("Using linear fading."); this.logger && this.logger('Using linear fading.');
} }
// no linear, but logarithmic fading… // No linear, but logarithmic fading…
else { else {
let dynamicRange; let dynamicRange;
// default dynamic range? // Default dynamic range?
if ( if (
options.fadeScaling === undefined || options.fadeScaling === undefined
options.fadeScaling == "logarithmic" || options.fadeScaling == 'logarithmic'
) { ) {
// set default of 60 dB // Set default of 60 dB
dynamicRange = 3; dynamicRange = 3;
} }
// custom dynamic range? // Custom dynamic range?
else if ( else if (
!Number.isNaN(options.fadeScaling) && !Number.isNaN(options.fadeScaling)
options.fadeScaling > 0 && options.fadeScaling > 0
) { ) {
// turn amplitude dB into a multiple of 10 power dB // Turn amplitude dB into a multiple of 10 power dB
dynamicRange = options.fadeScaling / 2 / 10; dynamicRange = options.fadeScaling / 2 / 10;
} }
// unsupported value // Unsupported value
else { else {
// abort and throw exception // Abort and throw exception
throw new TypeError( throw new TypeError(
"Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!" "Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!",
); );
} }
// use exponential/logarithmic scaler for expansion/compression // Use exponential/logarithmic scaler for expansion/compression
this.scale = { this.scale = {
internalToVolume: (level) => internalToVolume: (level) =>
this.exponentialScaler(level, dynamicRange), this.exponentialScaler(level, dynamicRange),
@ -114,45 +114,45 @@
this.logarithmicScaler(level, dynamicRange), this.logarithmicScaler(level, dynamicRange),
}; };
// log setting if not default // Log setting if not default
options.fadeScaling && options.fadeScaling
this.logger && && this.logger
this.logger( && this.logger(
"Using logarithmic fading with " + 'Using logarithmic fading with '
String(10 * dynamicRange) + + String(10 * dynamicRange)
" dB dynamic range." + ' dB dynamic range.',
); );
} }
// set initial volume? // Set initial volume?
if (options.initialVolume !== undefined) { if (options.initialVolume !== undefined) {
// validate volume level and throw if invalid // Validate volume level and throw if invalid
validateVolumeLevel(options.initialVolume); validateVolumeLevel(options.initialVolume);
// set initial volume // Set initial volume
this.media.volume = options.initialVolume; this.media.volume = options.initialVolume;
// log setting // Log setting
this.logger && this.logger
this.logger( && this.logger(
"Set initial volume to " + String(this.media.volume) + "." 'Set initial volume to ' + String(this.media.volume) + '.',
); );
} }
// fade duration given? // Fade duration given?
if (options.fadeDuration !== undefined) { if (options.fadeDuration === undefined) {
// try to set given fade duration (will log if successful and throw if not) // Set default fade duration (1000 ms)
this.setFadeDuration(options.fadeDuration);
} else {
// set default fade duration (1000 ms)
this.fadeDuration = 1000; this.fadeDuration = 1000;
} else {
// Try to set given fade duration (will log if successful and throw if not)
this.setFadeDuration(options.fadeDuration);
} }
// indicate that fader is not active yet // Indicate that fader is not active yet
this.active = false; this.active = false;
// initialization done // Initialization done
this.logger && this.logger("Initialized for", this.media); this.logger && this.logger('Initialized for', this.media);
} }
/** /**
@ -162,13 +162,13 @@
* @return {Object} VolumeFader instance for chaining * @return {Object} VolumeFader instance for chaining
*/ */
start() { start() {
// set fader to be active // Set fader to be active
this.active = true; this.active = true;
// start by running the update method // Start by running the update method
this.updateVolume(); this.updateVolume();
// return instance for chaining // Return instance for chaining
return this; return this;
} }
@ -179,10 +179,10 @@
* @return {Object} VolumeFader instance for chaining * @return {Object} VolumeFader instance for chaining
*/ */
stop() { stop() {
// set fader to be inactive // Set fader to be inactive
this.active = false; this.active = false;
// return instance for chaining // Return instance for chaining
return this; return this;
} }
@ -195,20 +195,20 @@
* @return {Object} VolumeFader instance for chaining * @return {Object} VolumeFader instance for chaining
*/ */
setFadeDuration(fadeDuration) { setFadeDuration(fadeDuration) {
// if duration is a valid number > 0… // If duration is a valid number > 0…
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) { if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
// set fade duration // Set fade duration
this.fadeDuration = fadeDuration; this.fadeDuration = fadeDuration;
// log setting // Log setting
this.logger && this.logger
this.logger("Set fade duration to " + String(fadeDuration) + " ms."); && this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
} else { } else {
// abort and throw an exception // Abort and throw an exception
throw new TypeError("Positive number expected as fade duration!"); throw new TypeError('Positive number expected as fade duration!');
} }
// return instance for chaining // Return instance for chaining
return this; return this;
} }
@ -221,39 +221,40 @@
* @return {Object} VolumeFader instance for chaining * @return {Object} VolumeFader instance for chaining
*/ */
fadeTo(targetVolume, callback) { fadeTo(targetVolume, callback) {
// validate volume and throw if invalid // Validate volume and throw if invalid
validateVolumeLevel(targetVolume); validateVolumeLevel(targetVolume);
// define new fade // Define new fade
this.fade = { this.fade = {
// volume start and end point on internal fading scale // Volume start and end point on internal fading scale
volume: { volume: {
start: this.scale.volumeToInternal(this.media.volume), start: this.scale.volumeToInternal(this.media.volume),
end: this.scale.volumeToInternal(targetVolume), end: this.scale.volumeToInternal(targetVolume),
}, },
// time start and end point // Time start and end point
time: { time: {
start: Date.now(), start: Date.now(),
end: Date.now() + this.fadeDuration, end: Date.now() + this.fadeDuration,
}, },
// optional callback function // Optional callback function
callback: callback, callback,
}; };
// start fading // Start fading
this.start(); this.start();
// log new fade // Log new fade
this.logger && this.logger("New fade started:", this.fade); this.logger && this.logger('New fade started:', this.fade);
// return instance for chaining // Return instance for chaining
return this; return this;
} }
// convenience shorthand methods for common fades // Convenience shorthand methods for common fades
fadeIn(callback) { fadeIn(callback) {
this.fadeTo(1, callback); this.fadeTo(1, callback);
} }
fadeOut(callback) { fadeOut(callback) {
this.fadeTo(0, callback); this.fadeTo(0, callback);
} }
@ -266,45 +267,45 @@
* @param {Function} callback - (optional) function to be called when fade is complete * @param {Function} callback - (optional) function to be called when fade is complete
*/ */
updateVolume() { updateVolume() {
// fader active and fade available to process? // Fader active and fade available to process?
if (this.active && this.fade) { if (this.active && this.fade) {
// get current time // Get current time
let now = Date.now(); const now = Date.now();
// time left for fading? // Time left for fading?
if (now < this.fade.time.end) { if (now < this.fade.time.end) {
// compute current fade progress // Compute current fade progress
let progress = const progress
(now - this.fade.time.start) / = (now - this.fade.time.start)
(this.fade.time.end - this.fade.time.start); / (this.fade.time.end - this.fade.time.start);
// compute current level on internal scale // Compute current level on internal scale
let level = const level
progress * (this.fade.volume.end - this.fade.volume.start) + = progress * (this.fade.volume.end - this.fade.volume.start)
this.fade.volume.start; + this.fade.volume.start;
// map fade level to volume level and apply it to media element // Map fade level to volume level and apply it to media element
this.media.volume = this.scale.internalToVolume(level); this.media.volume = this.scale.internalToVolume(level);
// schedule next update // Schedule next update
root.requestAnimationFrame(this.updateVolume.bind(this)); root.requestAnimationFrame(this.updateVolume.bind(this));
} else { } else {
// log end of fade // Log end of fade
this.logger && this.logger
this.logger( && this.logger(
"Fade to " + String(this.fade.volume.end) + " complete." 'Fade to ' + String(this.fade.volume.end) + ' complete.',
); );
// time is up, jump to target volume // Time is up, jump to target volume
this.media.volume = this.scale.internalToVolume(this.fade.volume.end); this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
// set fader to be inactive // Set fader to be inactive
this.active = false; this.active = false;
// done, call back (if callable) // Done, call back (if callable)
typeof this.fade.callback == "function" && this.fade.callback(); typeof this.fade.callback === 'function' && this.fade.callback();
// clear fade // Clear fade
this.fade = undefined; this.fade = undefined;
} }
} }
@ -318,19 +319,19 @@
* @return {Number} - expanded level (float, 0…1) * @return {Number} - expanded level (float, 0…1)
*/ */
exponentialScaler(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) {
// since the dynamic range is limited, // Since the dynamic range is limited,
// allow a zero to produce a plain zero instead of a small faction // allow a zero to produce a plain zero instead of a small faction
// (audio would not be recognized as silent otherwise) // (audio would not be recognized as silent otherwise)
return 0; return 0;
} else { }
// scale 0…1 to minus something × 10 dB
// Scale 0…1 to minus something × 10 dB
input = (input - 1) * dynamicRange; input = (input - 1) * dynamicRange;
// compute power of 10 // Compute power of 10
return Math.pow(10, input); return 10 ** input;
}
} }
/** /**
@ -341,20 +342,20 @@
* @return {Number} - compressed level (float, 0…1) * @return {Number} - compressed level (float, 0…1)
*/ */
logarithmicScaler(input, dynamicRange) { logarithmicScaler(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 // Logarithm of zero would be -∞, which would map to zero anyway
return 0; return 0;
} else { }
// compute base-10 logarithm
// Compute base-10 logarithm
input = Math.log10(input); input = Math.log10(input);
// scale minus something × 10 dB to 0…1 (clipping at 0) // Scale minus something × 10 dB to 0…1 (clipping at 0)
return Math.max(1 + input / dynamicRange, 0); return Math.max(1 + input / dynamicRange, 0);
} }
} }
}
// export class to root scope // Export class to root scope
root.VolumeFader = VolumeFader; root.VolumeFader = VolumeFader;
})(window); })(window);

View File

@ -1,44 +1,41 @@
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(() => {
@ -56,6 +53,7 @@ const createAudioForCrossfade = async (url) => {
if (transitionAudio) { if (transitionAudio) {
transitionAudio.unload(); transitionAudio.unload();
} }
transitionAudio = new Howl({ transitionAudio = new Howl({
src: url, src: url,
html5: true, html5: true,
@ -65,23 +63,25 @@ const createAudioForCrossfade = async (url) => {
}; };
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 = () => {
video.addEventListener('pause', () => {
transitionAudio.pause(); transitionAudio.pause();
}; });
video.onplay = async () => {
video.addEventListener('play', async () => {
await transitionAudio.play(); await transitionAudio.play();
await transitionAudio.seek(video.currentTime); await transitionAudio.seek(video.currentTime);
@ -89,21 +89,22 @@ const syncVideoWithTransitionAudio = async () => {
const videoVolume = video.volume; const videoVolume = video.volume;
video.volume = 0; video.volume = 0;
videoFader.fadeTo(videoVolume); videoFader.fadeTo(videoVolume);
}; });
// Exit just before the end for the transition // Exit just before the end for the transition
const transitionBeforeEnd = () => { const transitionBeforeEnd = () => {
if ( if (
video.currentTime >= video.duration - configGetNum("secondsBeforeEnd") && video.currentTime >= video.duration - configGetNumber('secondsBeforeEnd')
isReadyToCrossfade() && isReadyToCrossfade()
) { ) {
video.removeEventListener("timeupdate", transitionBeforeEnd); video.removeEventListener('timeupdate', transitionBeforeEnd);
// Go to next video - XXX: does not support "repeat 1" mode // Go to next video - XXX: does not support "repeat 1" mode
document.querySelector(".next-button").click(); document.querySelector('.next-button').click();
} }
}; };
video.ontimeupdate = transitionBeforeEnd;
video.addEventListener('timeupdate', transitionBeforeEnd);
}; };
const onApiLoaded = () => { const onApiLoaded = () => {
@ -113,6 +114,7 @@ const onApiLoaded = () => {
if (!url) { if (!url) {
return; return;
} }
await createAudioForCrossfade(url); await createAudioForCrossfade(url);
}); });
}; };
@ -124,16 +126,16 @@ const crossfade = async (cb) => {
} }
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
@ -151,7 +153,7 @@ module.exports = async () => {
config = newConfig; config = newConfig;
}); });
document.addEventListener("apiLoaded", onApiLoaded, { document.addEventListener('apiLoaded', onApiLoaded, {
once: true, once: true,
passive: true, passive: true,
}); });

View File

@ -1,15 +1,19 @@
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);
}
}, },
}, },
]; ];
@ -17,42 +21,42 @@ module.exports = (win) => [
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,
}, },
], ],
@ -62,7 +66,10 @@ async function promptCrossfadeValues(win, options) {
}, },
win, win,
).catch(console.error); ).catch(console.error);
if (!res) return undefined; if (!res) {
return undefined;
}
return { return {
fadeInDuration: Number(res[0]), fadeInDuration: Number(res[0]),
fadeOutDuration: Number(res[1]), fadeOutDuration: Number(res[1]),

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
@ -20,7 +20,7 @@ const clientId = "1043858434585526382";
*/ */
const info = { const info = {
rpc: new Discord.Client({ rpc: new Discord.Client({
clientId clientId,
}), }),
ready: false, ready: false,
autoReconnect: true, autoReconnect: true,
@ -35,21 +35,33 @@ 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) {
@ -58,33 +70,49 @@ info.rpc.on("disconnected", () => {
}); });
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) {
return;
}
info.rpc.login().then(resolve).catch(reject); 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) {
return;
}
connectTimeout().catch(connectRecursive); 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; return;
} }
info.ready = false; info.ready = false;
// Startup the rpc client // Startup the rpc client
info.rpc.login({ clientId }).catch(err => { info.rpc.login({ clientId }).catch((error) => {
resetInfo(); resetInfo();
if (dev()) console.error(err); if (dev()) {
console.error(error);
}
if (info.autoReconnect) { if (info.autoReconnect) {
connectRecursive(); connectRecursive();
} else if (showError) {
dialog.showMessageBox(window, {
title: 'Connection failed',
message: error.message || String(error),
type: 'error',
});
} }
else if (showErr) dialog.showMessageBox(window, { title: 'Connection failed', message: err.message || String(err), type: 'error' });
}); });
}; };
@ -101,22 +129,23 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
// 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; info.lastSongInfo = songInfo;
// stop the clear activity timout // Stop the clear activity timout
clearTimeout(clearActivity); clearTimeout(clearActivity);
// stop early if discord connection is not ready // Stop early if discord connection is not ready
// do this after clearTimeout to avoid unexpected clears // do this after clearTimeout to avoid unexpected clears
if (!info.rpc || !info.ready) { if (!info.rpc || !info.ready) {
return; return;
} }
// clear directly if timeout is 0 // Clear directly if timeout is 0
if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) { if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) {
info.rpc.user?.clearActivity().catch(console.error); info.rpc.user?.clearActivity().catch(console.error);
return; return;
@ -131,38 +160,42 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
largeImageKey: songInfo.imageSrc, largeImageKey: songInfo.imageSrc,
largeImageText: songInfo.album, largeImageText: songInfo.album,
buttons: listenAlong ? [ buttons: listenAlong ? [
{ label: "Listen Along", url: songInfo.url }, { label: 'Listen Along', url: songInfo.url },
] : undefined, ] : undefined,
}; };
if (songInfo.isPaused) { if (songInfo.isPaused) {
// Add a paused icon to show that the song is paused // Add a paused icon to show that the song is paused
activityInfo.smallImageKey = "paused"; activityInfo.smallImageKey = 'paused';
activityInfo.smallImageText = "Paused"; activityInfo.smallImageText = 'Paused';
// Set start the timer so the activity gets cleared after a while if enabled // Set start the timer so the activity gets cleared after a while if enabled
if (activityTimoutEnabled) if (activityTimoutEnabled) {
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), activityTimoutTime ?? 10000); clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), activityTimoutTime ?? 10_000);
}
} else if (!hideDurationLeft) { } else if (!hideDurationLeft) {
// Add the start and end time of the song // Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000; const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
activityInfo.startTimestamp = songStartTime; activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp = activityInfo.endTimestamp
songStartTime + songInfo.songDuration * 1000; = songStartTime + songInfo.songDuration * 1000;
} }
info.rpc.user?.setActivity(activityInfo).catch(console.error); info.rpc.user?.setActivity(activityInfo).catch(console.error);
}; };
// If the page is ready, register the callback // If the page is ready, register the callback
win.once("ready-to-show", () => { win.once('ready-to-show', () => {
registerCallback(updateActivity); registerCallback(updateActivity);
connect(); connect();
}); });
app.on('window-all-closed', module.exports.clear) app.on('window-all-closed', module.exports.clear);
}; };
module.exports.clear = () => { module.exports.clear = () => {
if (info.rpc) info.rpc.user?.clearActivity(); if (info.rpc) {
info.rpc.user?.clearActivity();
}
clearTimeout(clearActivity); clearTimeout(clearActivity);
}; };

View File

@ -1,10 +1,10 @@
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);
@ -15,70 +15,70 @@ module.exports = (win, options, 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,11 +134,14 @@ 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
}`; }`;
@ -140,12 +149,12 @@ async function downloadSongUnsafe(
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}`,
); );
@ -154,7 +163,7 @@ async function downloadSongUnsafe(
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}
@ -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,13 +1,13 @@
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;
@ -15,16 +15,27 @@ 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; }
if (menu.contains(downloadButton)) {
return;
}
const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
if (!menuUrl?.includes('watch?') && doneFirstLoad) return; if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return;
}
menu.prepend(downloadButton); menu.prepend(downloadButton);
progress = document.querySelector("#ytmcustom-download"); progress = document.querySelector('#ytmcustom-download');
if (doneFirstLoad) {
return;
}
if (doneFirstLoad) return;
setTimeout(() => doneFirstLoad ||= true, 500); setTimeout(() => doneFirstLoad ||= true, 500);
}); });
@ -33,13 +44,14 @@ const menuObserver = new MutationObserver(() => {
// 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=')) { if (videoUrl.includes('?playlist=')) {
ipcRenderer.send('download-playlist-request', videoUrl); ipcRenderer.send('download-playlist-request', videoUrl);
return; return;
@ -57,13 +69,13 @@ module.exports = () => {
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,35 +1,35 @@
<div <div
class="style-scope menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
aria-disabled="false" aria-disabled="false"
aria-selected="false" aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer"
onclick="download()" onclick="download()"
role="option"
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"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon" class="style-scope yt-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
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>

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,7 +2,7 @@
// 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;
@ -10,9 +10,9 @@ const exponentialVolume = () => {
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);
@ -23,11 +23,11 @@ const exponentialVolume = () => {
// 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;
@ -41,7 +41,7 @@ const exponentialVolume = () => {
}; };
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,20 +1,24 @@
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()) {
@ -25,16 +29,16 @@ module.exports = (options) => {
}; };
$('.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();
}); });
} }
@ -43,32 +47,32 @@ module.exports = (options) => {
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

@ -61,6 +61,7 @@ yt-page-navigation-progress,
-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);
@ -76,6 +77,7 @@ yt-page-navigation-progress,
-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;

View File

@ -1,69 +1,77 @@
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 createQueryString = (parameters, api_sig) => {
// Creates a querystring
const queryData = [];
parameters.api_sig = api_sig;
for (const key in parameters) {
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`);
}
return '?' + queryData.join('&');
};
const createApiSig = (parameters, secret) => {
// This function creates the api signature, see: https://www.last.fm/api/authspec
const keys = []; const keys = [];
for (const key in params) { for (const key in parameters) {
keys.push(key); keys.push(key);
} }
keys.sort(); keys.sort();
let sig = ''; let sig = '';
for (const key of keys) { for (const key of keys) {
if (String(key) === 'format') if (String(key) === 'format') {
continue continue;
sig += `${key}${params[key]}`;
} }
sig += `${key}${parameters[key]}`;
}
sig += secret; sig += secret;
sig = md5(sig); sig = md5(sig);
return 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',
@ -73,23 +81,26 @@ const getAndSetSessionKey = async config => {
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; config.session_key = res?.session?.key;
setOptions('last-fm', config); setOptions('last-fm', config);
return 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',
@ -97,65 +108,64 @@ const postSongDataToAPI = async (songInfo, config, 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,22 +1,24 @@
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;
const eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
let revRomanized = false; 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'));
ipcMain.on('search-genius-lyrics', async (event, extractedSongInfo) => {
const metadata = JSON.parse(extractedSongInfo); const metadata = JSON.parse(extractedSongInfo);
event.returnValue = await fetchFromGenius(metadata); event.returnValue = await fetchFromGenius(metadata);
}); });
@ -45,9 +47,10 @@ const fetchFromGenius = async (metadata) => {
/* 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;
}; };
@ -57,8 +60,8 @@ const fetchFromGenius = async (metadata) => {
* @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;
@ -68,16 +71,17 @@ const getLyricsList = async (queryString) => {
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);
const lyrics = await getLyrics(url);
return lyrics; return lyrics;
} };
/** /**
* *
@ -89,24 +93,26 @@ const getLyrics = async (url) => {
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
if (is.dev()) { if (is.dev()) {
console.log("Fetching lyrics from Genius:", url); console.log('Fetching lyrics from Genius:', url);
} }
const html = await response.text(); const html = await response.text();
const lyrics = convert(html, { const lyrics = convert(html, {
baseElements: { baseElements: {
selectors: ['[class^="Lyrics__Container"]', ".lyrics"], selectors: ['[class^="Lyrics__Container"]', '.lyrics'],
}, },
selectors: [ selectors: [
{ {
selector: "a", selector: 'a',
format: "linkFormatter", format: 'linkFormatter',
}, },
], ],
formatters: { formatters: {
// Remove links by keeping only the content // Remove links by keeping only the content
linkFormatter: (elem, walk, builder) => { linkFormatter(element, walk, builder) {
walk(elem.children, builder); walk(element.children, builder);
}, },
}, },
}); });

View File

@ -1,25 +1,25 @@
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
@ -31,7 +31,7 @@ module.exports = () => {
} }
if (is.dev()) { if (is.dev()) {
console.log("Fetched lyrics from Genius"); console.log('Fetched lyrics from Genius');
} }
enableLyricsTab(); enableLyricsTab();
@ -40,8 +40,8 @@ module.exports = () => {
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());
}); });
@ -50,15 +50,16 @@ module.exports = () => {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
}; });
function checkLyricsContainer(callback = () => {}) { function checkLyricsContainer(callback = () => {
}) {
const lyricsContainer = document.querySelector( const lyricsContainer = document.querySelector(
'[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer' '[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer',
); );
if (lyricsContainer) { if (lyricsContainer) {
callback(); callback();
setLyrics(lyricsContainer) setLyrics(lyricsContainer);
} }
} }
@ -66,8 +67,8 @@ module.exports = () => {
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>
@ -81,14 +82,14 @@ module.exports = () => {
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", label: 'Romanized Lyrics',
type: "checkbox", type: 'checkbox',
checked: options.romanizedLyrics, checked: options.romanizedLyrics,
click: (item) => { click(item) {
options.romanizedLyrics = item.checked; options.romanizedLyrics = item.checked;
setOptions('lyrics-genius', options); setOptions('lyrics-genius', options);
toggleRomanized(); toggleRomanized();
}, },
}, },
]; ];
};

View File

@ -1,9 +1,9 @@
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() {
@ -15,10 +15,10 @@ function goToPreviousPage() {
} }
module.exports = { module.exports = {
CHANNEL: CHANNEL, CHANNEL,
ACTIONS: ACTIONS, ACTIONS,
actions: { actions: {
goToNextPage: goToNextPage, goToNextPage,
goToPreviousPage: goToPreviousPage, goToPreviousPage,
}, },
}; };

View File

@ -1,27 +1,35 @@
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; break;
case ACTIONS.BACK: }
case ACTIONS.BACK: {
if (win.webContents.canGoBack()) { if (win.webContents.canGoBack()) {
win.webContents.goBack(); win.webContents.goBack();
} }
break; break;
default: }
console.log("Unknown action: " + action);
default: {
console.log('Unknown action: ' + action);
}
} }
}); });
} }

View File

@ -1,14 +1,14 @@
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);

View File

@ -1,26 +1,26 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
tab-id="FEmusic_back"
role="tab"
onclick="goToPreviousPage()" onclick="goToPreviousPage()"
role="tab"
tab-id="FEmusic_back"
> >
<div <div
aria-disabled="false"
class="search-icon style-scope ytmusic-search-box" class="search-icon style-scope ytmusic-search-box"
role="button" role="button"
tabindex="0" tabindex="0"
aria-disabled="false"
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"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope iron-icon" class="style-scope iron-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
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

View File

@ -1,26 +1,26 @@
<div <div
class="style-scope ytmusic-pivot-bar-renderer navigation-item" class="style-scope ytmusic-pivot-bar-renderer navigation-item"
tab-id="FEmusic_next"
role="tab"
onclick="goToNextPage()" onclick="goToNextPage()"
role="tab"
tab-id="FEmusic_next"
> >
<div <div
aria-disabled="false"
class="search-icon style-scope ytmusic-search-box" class="search-icon style-scope ytmusic-search-box"
role="button" role="button"
tabindex="0" tabindex="0"
aria-disabled="false"
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"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope iron-icon" class="style-scope iron-icon"
focusable="false"
preserveAspectRatio="xMidYMid meet"
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

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,32 +1,32 @@
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,

View File

@ -1,14 +1,15 @@
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 // Fill the notification with content
const notification = { const notification = {
title: info.title || "Playing", title: info.title || 'Playing',
body: info.artist, body: info.artist,
icon: notificationImage(info), icon: notificationImage(info),
silent: true, silent: true,
@ -17,7 +18,7 @@ const notify = (info) => {
// Send the notification // Send the notification
const currentNotification = new Notification(notification); const currentNotification = new Notification(notification);
currentNotification.show() currentNotification.show();
return currentNotification; return currentNotification;
}; };
@ -26,21 +27,23 @@ 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,13 +1,16 @@
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;
@ -21,24 +24,29 @@ module.exports = (win) => {
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) {
return;
}
savedSongInfo = { ...songInfo }; savedSongInfo = { ...songInfo };
if (!songInfo.isPaused && if (!songInfo.isPaused
(songInfo.url !== lastUrl || config.get("unpauseNotification")) && (songInfo.url !== lastUrl || config.get('unpauseNotification'))
) { ) {
lastUrl = songInfo.url lastUrl = songInfo.url;
sendNotification(songInfo); sendNotification(songInfo);
} }
}); });
if (config.get("trayControls")) { if (config.get('trayControls')) {
setTrayOnClick(() => { setTrayOnClick(() => {
if (savedNotification) { if (savedNotification) {
savedNotification.close(); savedNotification.close();
@ -46,45 +54,45 @@ module.exports = (win) => {
} else if (savedSongInfo) { } else if (savedSongInfo) {
sendNotification({ sendNotification({
...savedSongInfo, ...savedSongInfo,
elapsedSeconds: currentSeconds elapsedSeconds: currentSeconds,
}) });
} }
}); });
setTrayOnDoubleClick(() => { setTrayOnDoubleClick(() => {
if (win.isVisible()) { if (win.isVisible()) {
win.hide(); win.hide();
} else win.show(); } else {
}) win.show();
}
});
} }
app.once('before-quit', () => {
app.once("before-quit", () => {
savedNotification?.close(); savedNotification?.close();
}); });
changeProtocolHandler( changeProtocolHandler(
(cmd) => { (cmd) => {
if (Object.keys(songControls).includes(cmd)) { if (Object.keys(songControls).includes(cmd)) {
songControls[cmd](); songControls[cmd]();
if (config.get("refreshOnPlayPause") && ( if (config.get('refreshOnPlayPause') && (
cmd === 'pause' || cmd === 'pause'
(cmd === 'play' && !config.get("unpauseNotification")) || (cmd === 'play' && !config.get('unpauseNotification'))
) )
) { ) {
setImmediate(() => setImmediate(() =>
sendNotification({ sendNotification({
...savedSongInfo, ...savedSongInfo,
isPaused: cmd === 'pause', isPaused: cmd === 'pause',
elapsedSeconds: currentSeconds elapsedSeconds: currentSeconds,
}) }),
); );
} }
} }
} },
) );
} };
function sendNotification(songInfo) { function sendNotification(songInfo) {
const iconSrc = notificationImage(songInfo); const iconSrc = notificationImage(songInfo);
@ -92,7 +100,7 @@ function sendNotification(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,
@ -103,7 +111,7 @@ function sendNotification(songInfo) {
toastXml: get_xml(songInfo, iconSrc), toastXml: get_xml(songInfo, iconSrc),
}); });
savedNotification.on("close", (_) => { savedNotification.on('close', (_) => {
savedNotification = undefined; savedNotification = undefined;
}); });
@ -111,38 +119,49 @@ function sendNotification(songInfo) {
} }
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 `\ return `\
content="${config.get("hideButtonText") ? "" : kind.charAt(0).toUpperCase() + kind.slice(1)}"\ 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}"/>`;
@ -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>\
`; `;
@ -225,11 +243,15 @@ const xml_banner_centered_top = ({ title, artist, isPaused }, imgSrc) => toast(`
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) {
return 'Title';
} else {
return 'Subtitle';
} }
}
if (title.length <= 22) {
return 'Subheader';
}
if (title.length <= 26) {
return 'Title';
}
return 'Subtitle';
};

View File

@ -1,17 +1,18 @@
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),
})), })),
}, },
] ]
@ -19,62 +20,62 @@ module.exports = (_win, options) => [
...(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,14 +1,17 @@
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,
@ -17,27 +20,27 @@ module.exports.ToastStyles = {
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,
@ -46,48 +49,59 @@ const nativeImageToLogo = cache((nativeImage) => {
}); });
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')) {
return nativeImageToLogo(songInfo.image);
}
switch (config.get('toastStyle')) {
case module.exports.ToastStyles.logo: case module.exports.ToastStyles.logo:
case module.exports.ToastStyles.legacy: case module.exports.ToastStyles.legacy: {
return this.saveImage(nativeImageToLogo(songInfo.image), tempIcon); return this.saveImage(nativeImageToLogo(songInfo.image), temporaryIcon);
default: }
return this.saveImage(songInfo.image, tempBanner);
}; 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;
}
const iconPath = path.resolve(__dirname, '../../assets/media-icons-black', `${kind}.png`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFile(iconPath, destinationPath, () => { }); 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,13 +15,13 @@ 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;
@ -29,19 +29,24 @@ const togglePiP = async () => {
if (isInPiP) { if (isInPiP) {
originalFullScreen = win.isFullScreen(); originalFullScreen = win.isFullScreen();
if (originalFullScreen) win.setFullScreen(false); if (originalFullScreen) {
win.setFullScreen(false);
}
originalMaximized = win.isMaximized(); originalMaximized = win.isMaximized();
if (originalMaximized) win.unmaximize(); if (originalMaximized) {
win.unmaximize();
}
originalPosition = win.getPosition(); originalPosition = win.getPosition();
originalSize = win.getSize(); originalSize = win.getSize();
win.webContents.on("before-input-event", blockShortcutsInPiP); win.webContents.on('before-input-event', blockShortcutsInPiP);
win.setMaximizable(false); win.setMaximizable(false);
win.setFullScreenable(false); win.setFullScreenable(false);
win.webContents.send("pip-toggle", true); win.webContents.send('pip-toggle', true);
app.dock?.hide(); app.dock?.hide();
win.setVisibleOnAllWorkspaces(true, { win.setVisibleOnAllWorkspaces(true, {
@ -49,20 +54,25 @@ const togglePiP = async () => {
}); });
app.dock?.show(); app.dock?.show();
if (options.alwaysOnTop) { if (options.alwaysOnTop) {
win.setAlwaysOnTop(true, "screen-saver", 1); win.setAlwaysOnTop(true, 'screen-saver', 1);
} }
} else { } else {
win.webContents.removeListener("before-input-event", blockShortcutsInPiP); win.webContents.removeListener('before-input-event', blockShortcutsInPiP);
win.setMaximizable(true); win.setMaximizable(true);
win.setFullScreenable(true); win.setFullScreenable(true);
win.webContents.send("pip-toggle", false); win.webContents.send('pip-toggle', false);
win.setVisibleOnAllWorkspaces(false); win.setVisibleOnAllWorkspaces(false);
win.setAlwaysOnTop(false); win.setAlwaysOnTop(false);
if (originalFullScreen) win.setFullScreen(true); if (originalFullScreen) {
if (originalMaximized) win.maximize(); win.setFullScreen(true);
}
if (originalMaximized) {
win.maximize();
}
} }
const [x, y] = isInPiP ? pipPosition() : originalPosition; const [x, y] = isInPiP ? pipPosition() : originalPosition;
@ -76,20 +86,20 @@ const togglePiP = async () => {
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();
}); });
}; };

View File

@ -1,26 +1,27 @@
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;
} }
@ -32,13 +33,21 @@ function cloneButton(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; }
if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) {
return;
}
const menuUrl = $( const menuUrl = $(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint' 'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
)?.href; )?.href;
if (menuUrl && !menuUrl.includes("watch?")) return; if (menuUrl && !menuUrl.includes('watch?')) {
return;
}
menu.prepend(pipButton); menu.prepend(pipButton);
}); });
@ -46,7 +55,7 @@ const observer = new MutationObserver(() => {
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)
@ -54,72 +63,79 @@ global.togglePictureInPicture = async () => {
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();
expandMenu.addEventListener('mouseleave', () => middleControls.click());
if (!playerPage.playerPageOpen_) { if (!playerPage.playerPageOpen_) {
togglePlayerPageButton.click(); togglePlayerPageButton.click();
} }
fullScreenButton.click(); fullScreenButton.click();
appLayout.classList.add("pip"); appLayout.classList.add('pip');
if (titlebar) titlebar.style.display = "none"; if (titlebar) {
titlebar.style.display = 'none';
}
} else { } else {
$(".exit-fullscreen-button").replaceWith(originalExitButton); $('.exit-fullscreen-button').replaceWith(originalExitButton);
player.onDoubleClick_ = onPlayerDblClick; player.onDoubleClick_ = onPlayerDblClick;
expandMenu.onmouseleave = undefined; expandMenu.onmouseleave = undefined;
originalExitButton.click(); originalExitButton.click();
appLayout.classList.remove("pip"); appLayout.classList.remove('pip');
if (titlebar) titlebar.style.display = "flex"; 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 },
); );
} }
@ -128,10 +144,10 @@ module.exports = (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,56 +1,57 @@
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", label: 'Save window position',
type: "checkbox", type: 'checkbox',
checked: options.savePosition, checked: options.savePosition,
click: (item) => { click(item) {
setOptions({ savePosition: item.checked }); setOptions({ savePosition: item.checked });
}, },
}, },
{ {
label: "Save window size", label: 'Save window size',
type: "checkbox", type: 'checkbox',
checked: options.saveSize, checked: options.saveSize,
click: (item) => { click(item) {
setOptions({ saveSize: item.checked }); setOptions({ saveSize: item.checked });
}, },
}, },
{ {
label: "Hotkey", label: 'Hotkey',
type: "checkbox", type: 'checkbox',
checked: options.hotkey, checked: options.hotkey,
click: async (item) => { async click(item) {
const output = await prompt({ const output = await prompt({
title: "Picture in Picture Hotkey", title: 'Picture in Picture Hotkey',
label: "Choose a hotkey for toggling Picture in Picture", label: 'Choose a hotkey for toggling Picture in Picture',
type: "keybind", type: 'keybind',
keybindOptions: [{ keybindOptions: [{
value: "hotkey", value: 'hotkey',
label: "Hotkey", label: 'Hotkey',
default: options.hotkey default: options.hotkey,
}], }],
...promptOptions() ...promptOptions(),
}, win) }, 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;
@ -58,11 +59,11 @@ module.exports = (win, options) => [
}, },
}, },
{ {
label: "Use native PiP", label: 'Use native PiP',
type: "checkbox", type: 'checkbox',
checked: options.useNativePiP, checked: options.useNativePiP,
click: (item) => { click(item) {
setOptions({ useNativePiP: item.checked }); setOptions({ useNativePiP: item.checked });
}, },
} },
]; ];

View File

@ -1,29 +1,28 @@
<div <div
class="style-scope menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
aria-disabled="false" aria-disabled="false"
aria-selected="false" aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer"
onclick="togglePictureInPicture()" onclick="togglePictureInPicture()"
role="option"
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"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 512 512"
style="enable-background: new 0 0 512 512" style="enable-background: new 0 0 512 512"
version="1.1"
viewBox="0 0 512 512"
x="0px"
xml:space="preserve" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
y="0px"
> >
<style type="text/css"> <style type="text/css">
.st0 { .st0 {
@ -32,11 +31,11 @@
</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>

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;
@ -16,7 +18,7 @@ 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;
} }
@ -25,13 +27,14 @@ const updatePlayBackSpeed = () => {
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 = () => {
@ -53,31 +56,32 @@ const observePopupContainer = () => {
}; };
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 ? // E.deltaY < 0 means wheel-up
Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED) : playbackSpeed = roundToTwo(e.deltaY < 0
Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED) ? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
); );
updatePlayBackSpeed(); updatePlayBackSpeed();
// update slider position // Update slider position
$('#playback-speed-slider').value = playbackSpeed; $('#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;
} }
} }
@ -86,5 +90,5 @@ module.exports = () => {
observePopupContainer(); observePopupContainer();
observeVideo(); observeVideo();
setupWheelListener(); setupWheelListener();
}, { once: true, passive: true }) }, { once: true, passive: true });
}; };

View File

@ -1,72 +1,74 @@
<div <div
aria-disabled="false"
aria-selected="false"
class="style-scope menu-item ytmusic-menu-popup-renderer" class="style-scope menu-item ytmusic-menu-popup-renderer"
role="option" role="option"
tabindex="-1" tabindex="-1"
aria-disabled="false"
aria-selected="false"
> >
<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"
aria-label="Playback speed"
aria-valuemax="2"
aria-valuemin="0"
aria-valuenow="1"
class="volume-slider style-scope ytmusic-player-bar on-hover" class="volume-slider style-scope ytmusic-player-bar on-hover"
style="display: inherit !important" dir="ltr"
id="playback-speed-slider"
max="2" max="2"
min="0" min="0"
step="0.125"
dir="ltr"
title="Playback speed"
aria-label="Playback speed"
role="slider" role="slider"
step="0.125"
style="display: inherit !important"
tabindex="0" tabindex="0"
aria-valuemin="0" title="Playback speed"
aria-valuemax="2"
aria-valuenow="1"
aria-disabled="false"
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-hidden="true"
class="style-scope tp-yt-paper-slider"
role="progressbar"
value="1"
aria-valuenow="1"
aria-valuemin="0"
aria-valuemax="2"
aria-disabled="false" aria-disabled="false"
aria-hidden="true"
aria-valuemax="2"
aria-valuemin="0"
aria-valuenow="1"
class="style-scope tp-yt-paper-slider"
id="sliderBar"
role="progressbar"
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
>
</dom-if>
<div <div
id="sliderKnob"
class="slider-knob style-scope tp-yt-paper-slider" class="slider-knob style-scope tp-yt-paper-slider"
id="sliderKnob"
style="left: 50%; touch-action: none" style="left: 50%; touch-action: none"
> >
<div <div
@ -76,8 +78,11 @@
</div> </div>
</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 >
></tp-yt-paper-slider> <template is="dom-if"></template>
</dom-if
>
</tp-yt-paper-slider>
<div <div
class="text style-scope ytmusic-menu-navigation-item-renderer" class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-playback-speed" id="ytmcustom-playback-speed"

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
@ -11,14 +12,15 @@ 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) { if (options.globalShortcuts?.volumeDown) {
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false)); globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
} }
} };
module.exports.enabled = () => enabled; module.exports.enabled = () => enabled;

View File

@ -1,33 +1,39 @@
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) {
return;
}
volumeHud.style.top = showVideo volumeHud.style.top = showVideo
? `${($("ytmusic-player").clientHeight - $("video").clientHeight) / 2}px` ? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px`
: 0; : 0;
}, 250); }, 250);
@ -36,13 +42,12 @@ const hideVolumeHud = debounce((volumeHud) => {
}, 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);
@ -55,43 +60,45 @@ function firstRun() {
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;
@ -101,7 +108,7 @@ function showVolumeHud(volume) {
/** 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);
@ -113,23 +120,23 @@ function saveVolume(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();
@ -137,11 +144,11 @@ function setupPlaybar() {
/** 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);
@ -150,9 +157,9 @@ function setupSliderObserver() {
}); });
// 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,
}); });
} }
@ -161,7 +168,7 @@ function 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
@ -172,39 +179,39 @@ function setVolume(value) {
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) {
@ -216,17 +223,23 @@ function setTooltip(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) {
return;
}
switch (event.code) { switch (event.code) {
case "ArrowUp": case 'ArrowUp': {
event.preventDefault(); event.preventDefault();
changeVolume(true); changeVolume(true);
break; break;
case "ArrowDown": }
case 'ArrowDown': {
event.preventDefault(); event.preventDefault();
changeVolume(false); changeVolume(false);
break; break;
} }
}
}); });
} }
} }

View File

@ -1,80 +1,84 @@
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 // Dynamically change setting if plugin is enabled
if (enabled()) { if (enabled()) {
win.webContents.send("setOptions", changedOptions); win.webContents.send('setOptions', changedOptions);
} else { // Fallback to usual method if disabled } else { // Fallback to usual method if disabled
setMenuOptions("precise-volume", options); 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); changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
item.checked = !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown; item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
} else { } else {
// Reset checkbox if prompt was canceled // Reset checkbox if prompt was canceled
item.checked = !item.checked; item.checked = !item.checked;

View File

@ -1,8 +1,8 @@
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() {
@ -11,8 +11,8 @@ function overrideAddEventListener() {
// 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()) {

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) {
return;
}
const newQuality = qualityLevels[promise.response]; const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality); api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality) api.setPlaybackQuality(newQuality);
});
}); });
}
} }

View File

@ -1,12 +1,15 @@
<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"
focusable="false" preserveAspectRatio="xMidYMid meet"
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="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" class="style-scope yt-icon"
class="style-scope yt-icon"></path> 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>
</g> </g>
</svg> </svg>
</tp-yt-iron-icon> </tp-yt-iron-icon>

View File

@ -1,8 +1,10 @@
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, () => {
@ -21,15 +23,17 @@ function registerShortcuts(win, options) {
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 };
@ -44,15 +48,15 @@ function registerShortcuts(win, options) {
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]);
} }
} }

View File

@ -1,18 +1,19 @@
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) {
@ -20,34 +21,36 @@ function setOption(options, key = null, 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) { for (const { value, accelerator } of output) {
options.global[value] = accelerator; options.global[value] = accelerator;
} }
setOption(options); setOption(options);
} }
// else -> pressed cancel // Else -> pressed cancel
} }

View File

@ -1,18 +1,19 @@
const mpris = require("mpris-service"); const { ipcMain } = require('electron');
const { ipcMain } = require("electron"); const mpris = require('mpris-service');
const registerCallback = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls"); const registerCallback = require('../../providers/song-info');
const config = require("../../config"); const getSongControls = require('../../providers/song-controls');
const config = require('../../config');
function setupMPRIS() { function setupMPRIS() {
const player = mpris({ const player = mpris({
name: "youtube-music", name: 'youtube-music',
identity: "YouTube Music", identity: 'YouTube Music',
canRaise: true, canRaise: true,
supportedUriSchemes: ["https"], supportedUriSchemes: ['https'],
supportedMimeTypes: ["audio/mpeg"], supportedMimeTypes: ['audio/mpeg'],
supportedInterfaces: ["player"], supportedInterfaces: ['player'],
desktopEntry: "youtube-music", desktopEntry: 'youtube-music',
}); });
return player; return player;
@ -23,19 +24,19 @@ function registerMPRIS(win) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls; const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
try { try {
const secToMicro = n => Math.round(Number(n) * 1e6); const secToMicro = (n) => Math.round(Number(n) * 1e6);
const microToSec = n => Math.round(Number(n) / 1e6); const microToSec = (n) => Math.round(Number(n) / 1e6);
const seekTo = e => win.webContents.send("seekTo", microToSec(e.position)); const seekTo = (e) => win.webContents.send('seekTo', microToSec(e.position));
const seekBy = o => win.webContents.send("seekBy", microToSec(o)); const seekBy = (o) => win.webContents.send('seekBy', microToSec(o));
const player = setupMPRIS(); const player = setupMPRIS();
ipcMain.on("apiLoaded", () => { ipcMain.on('apiLoaded', () => {
win.webContents.send("setupSeekedListener", "mpris"); win.webContents.send('setupSeekedListener', 'mpris');
win.webContents.send("setupTimeChangedListener", "mpris"); win.webContents.send('setupTimeChangedListener', 'mpris');
win.webContents.send("setupRepeatChangedListener", "mpris"); win.webContents.send('setupRepeatChangedListener', 'mpris');
win.webContents.send("setupVolumeChangedListener", "mpris"); win.webContents.send('setupVolumeChangedListener', 'mpris');
}); });
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t))); ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
@ -43,16 +44,30 @@ function registerMPRIS(win) {
let currentSeconds = 0; let currentSeconds = 0;
ipcMain.on('timeChanged', (_, t) => currentSeconds = t); ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
ipcMain.on("repeatChanged", (_, mode) => { ipcMain.on('repeatChanged', (_, mode) => {
if (mode === "NONE") switch (mode) {
case 'NONE': {
player.loopStatus = mpris.LOOP_STATUS_NONE; player.loopStatus = mpris.LOOP_STATUS_NONE;
else if (mode === "ONE") //MPRIS Playlist and Track Codes are switched to look the same as yt-music icons break;
}
case 'ONE': {
player.loopStatus = mpris.LOOP_STATUS_PLAYLIST; player.loopStatus = mpris.LOOP_STATUS_PLAYLIST;
else if (mode === "ALL") break;
}
case 'ALL': {
{
player.loopStatus = mpris.LOOP_STATUS_TRACK; player.loopStatus = mpris.LOOP_STATUS_TRACK;
// No default
}
break;
}
}
}); });
player.on("loopStatus", (status) => { player.on('loopStatus', (status) => {
// switchRepeat cycles between states in that order // SwitchRepeat cycles between states in that order
const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK]; const switches = [mpris.LOOP_STATUS_NONE, mpris.LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_TRACK];
const currentIndex = switches.indexOf(player.loopStatus); const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status); const targetIndex = switches.indexOf(status);
@ -60,34 +75,34 @@ function registerMPRIS(win) {
// Get a delta in the range [0,2] // Get a delta in the range [0,2]
const delta = (targetIndex - currentIndex + 3) % 3; const delta = (targetIndex - currentIndex + 3) % 3;
songControls.switchRepeat(delta); songControls.switchRepeat(delta);
}) });
player.getPosition = () => secToMicro(currentSeconds) player.getPosition = () => secToMicro(currentSeconds);
player.on("raise", () => { player.on('raise', () => {
win.setSkipTaskbar(false); win.setSkipTaskbar(false);
win.show(); win.show();
}); });
player.on("play", () => { player.on('play', () => {
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) { if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) {
player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING; player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING;
playPause() playPause();
} }
}); });
player.on("pause", () => { player.on('pause', () => {
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) { if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) {
player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED; player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED;
playPause() playPause();
} }
}); });
player.on("playpause", () => { player.on('playpause', () => {
player.playbackStatus = player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING; player.playbackStatus = player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING;
playPause(); playPause();
}); });
player.on("next", next); player.on('next', next);
player.on("previous", previous); player.on('previous', previous);
player.on('seek', seekBy); player.on('seek', seekBy);
player.on('position', seekTo); player.on('position', seekTo);
@ -99,13 +114,13 @@ function registerMPRIS(win) {
let mprisVolNewer = false; let mprisVolNewer = false;
let autoUpdate = false; let autoUpdate = false;
ipcMain.on('volumeChanged', (_, newVol) => { ipcMain.on('volumeChanged', (_, newVol) => {
if (parseInt(player.volume * 100) !== newVol) { if (Number.parseInt(player.volume * 100) !== newVol) {
if (mprisVolNewer) { if (mprisVolNewer) {
mprisVolNewer = false; mprisVolNewer = false;
autoUpdate = false; autoUpdate = false;
} else { } else {
autoUpdate = true; autoUpdate = true;
player.volume = parseFloat((newVol / 100).toFixed(2)); player.volume = Number.parseFloat((newVol / 100).toFixed(2));
mprisVolNewer = false; mprisVolNewer = false;
autoUpdate = false; autoUpdate = false;
} }
@ -115,31 +130,30 @@ function registerMPRIS(win) {
player.on('volume', (newVolume) => { player.on('volume', (newVolume) => {
if (config.plugins.isEnabled('precise-volume')) { if (config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value. // With precise volume we can set the volume to the exact value.
let newVol = parseInt(newVolume * 100); const newVol = Number.parseInt(newVolume * 100);
if (parseInt(player.volume * 100) !== newVol) { if (Number.parseInt(player.volume * 100) !== newVol && !autoUpdate) {
if (!autoUpdate) {
mprisVolNewer = true; mprisVolNewer = true;
autoUpdate = false; autoUpdate = false;
win.webContents.send('setVolume', newVol); win.webContents.send('setVolume', newVol);
} }
}
} else { } else {
// With keyboard shortcuts we can only change the volume in increments of 10, so round it. // With keyboard shortcuts we can only change the volume in increments of 10, so round it.
let deltaVolume = Math.round((newVolume - player.volume) * 10); let deltaVolume = Math.round((newVolume - player.volume) * 10);
while (deltaVolume !== 0 && deltaVolume > 0) { while (deltaVolume !== 0 && deltaVolume > 0) {
volumePlus10(); volumePlus10();
player.volume = player.volume + 0.1; player.volume += 0.1;
deltaVolume--; deltaVolume--;
} }
while (deltaVolume !== 0 && deltaVolume < 0) { while (deltaVolume !== 0 && deltaVolume < 0) {
volumeMinus10(); volumeMinus10();
player.volume = player.volume - 0.1; player.volume -= 0.1;
deltaVolume++; deltaVolume++;
} }
} }
}); });
registerCallback(songInfo => { registerCallback((songInfo) => {
if (player) { if (player) {
const data = { const data = {
'mpris:length': secToMicro(songInfo.songDuration), 'mpris:length': secToMicro(songInfo.songDuration),
@ -147,17 +161,19 @@ function registerMPRIS(win) {
'xesam:title': songInfo.title, 'xesam:title': songInfo.title,
'xesam:url': songInfo.url, 'xesam:url': songInfo.url,
'xesam:artist': [songInfo.artist], 'xesam:artist': [songInfo.artist],
'mpris:trackid': '/' 'mpris:trackid': '/',
}; };
if (songInfo.album) data['xesam:album'] = songInfo.album; if (songInfo.album) {
data['xesam:album'] = songInfo.album;
}
player.metadata = data; player.metadata = data;
player.seeked(secToMicro(songInfo.elapsedSeconds)); player.seeked(secToMicro(songInfo.elapsedSeconds));
player.playbackStatus = songInfo.isPaused ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING; player.playbackStatus = songInfo.isPaused ? mpris.PLAYBACK_STATUS_PAUSED : mpris.PLAYBACK_STATUS_PLAYING;
} }
}) });
} catch (error) {
} catch (e) { console.warn('Error in MPRIS', error);
console.warn("Error in MPRIS", e);
} }
} }

View File

@ -3,16 +3,16 @@ module.exports = (options) => {
let hasAudioStarted = false; let hasAudioStarted = false;
const smoothing = 0.1; const smoothing = 0.1;
const threshold = -100; // dB (-100 = absolute silence, 0 = loudest) const threshold = -100; // DB (-100 = absolute silence, 0 = loudest)
const interval = 2; // ms const interval = 2; // Ms
const history = 10; const history = 10;
const speakingHistory = Array(history).fill(0); const speakingHistory = Array.from({ length: history }).fill(0);
document.addEventListener( document.addEventListener(
"audioCanPlay", 'audioCanPlay',
(e) => { (e) => {
const video = document.querySelector("video"); const video = document.querySelector('video');
const audioContext = e.detail.audioContext; const { audioContext } = e.detail;
const sourceNode = e.detail.audioSource; const sourceNode = e.detail.audioSource;
// Use an audio analyser similar to Hark // Use an audio analyser similar to Hark
@ -31,7 +31,7 @@ module.exports = (options) => {
let history = 0; let history = 0;
if (currentVolume > threshold && isSilent) { if (currentVolume > threshold && isSilent) {
// trigger quickly, short history // Trigger quickly, short history
for ( for (
let i = speakingHistory.length - 3; let i = speakingHistory.length - 3;
i < speakingHistory.length; i < speakingHistory.length;
@ -39,37 +39,39 @@ module.exports = (options) => {
) { ) {
history += speakingHistory[i]; history += speakingHistory[i];
} }
if (history >= 2) { if (history >= 2) {
// Not silent // Not silent
isSilent = false; isSilent = false;
hasAudioStarted = true; hasAudioStarted = true;
} }
} else if (currentVolume < threshold && !isSilent) { } else if (currentVolume < threshold && !isSilent) {
for (let i = 0; i < speakingHistory.length; i++) { for (const element of speakingHistory) {
history += speakingHistory[i]; history += element;
} }
if (history == 0) {
// Silent if (history == 0 // Silent
if (
!( && !(
video.paused || video.paused
video.seeking || || video.seeking
video.ended || || video.ended
video.muted || || video.muted
video.volume === 0 || video.volume === 0
) )
) { ) {
isSilent = true; isSilent = true;
skipSilence(); skipSilence();
} }
} }
}
speakingHistory.shift(); speakingHistory.shift();
speakingHistory.push(0 + (currentVolume > threshold)); speakingHistory.push(0 + (currentVolume > threshold));
looper(); looper();
}, interval); }, interval);
}; };
looper(); looper();
const skipSilence = () => { const skipSilence = () => {
@ -78,31 +80,31 @@ module.exports = (options) => {
} }
if (isSilent && !video.paused) { if (isSilent && !video.paused) {
video.currentTime += 0.2; // in s video.currentTime += 0.2; // In s
} }
}; };
video.addEventListener("play", function () { video.addEventListener('play', () => {
hasAudioStarted = false; hasAudioStarted = false;
skipSilence(); skipSilence();
}); });
video.addEventListener("seeked", function () { video.addEventListener('seeked', () => {
hasAudioStarted = false; hasAudioStarted = false;
skipSilence(); skipSilence();
}); });
}, },
{ {
passive: true, passive: true,
} },
); );
}; };
function getMaxVolume(analyser, fftBins) { function getMaxVolume(analyser, fftBins) {
var maxVolume = -Infinity; let maxVolume = Number.NEGATIVE_INFINITY;
analyser.getFloatFrequencyData(fftBins); analyser.getFloatFrequencyData(fftBins);
for (var i = 4, ii = fftBins.length; i < ii; i++) { for (let i = 4, ii = fftBins.length; i < ii; i++) {
if (fftBins[i] > maxVolume && fftBins[i] < 0) { if (fftBins[i] > maxVolume && fftBins[i] < 0) {
maxVolume = fftBins[i]; maxVolume = fftBins[i];
} }

View File

@ -1,9 +1,10 @@
const fetch = require("node-fetch"); const { ipcMain } = require('electron');
const is = require("electron-is"); const fetch = require('node-fetch');
const { ipcMain } = require("electron"); const is = require('electron-is');
const defaultConfig = require("../../config/defaults"); const { sortSegments } = require('./segments');
const { sortSegments } = require("./segments");
const defaultConfig = require('../../config/defaults');
let videoID; let videoID;
@ -13,39 +14,40 @@ module.exports = (win, options) => {
...options, ...options,
}; };
ipcMain.on("video-src-changed", async (_, data) => { ipcMain.on('video-src-changed', async (_, data) => {
videoID = JSON.parse(data)?.videoDetails?.videoId; videoID = JSON.parse(data)?.videoDetails?.videoId;
const segments = await fetchSegments(apiURL, categories); const segments = await fetchSegments(apiURL, categories);
win.webContents.send("sponsorblock-skip", segments); win.webContents.send('sponsorblock-skip', segments);
}); });
}; };
const fetchSegments = async (apiURL, categories) => { const fetchSegments = async (apiURL, categories) => {
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify( const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
categories categories,
)}`; )}`;
try { try {
const resp = await fetch(sponsorBlockURL, { const resp = await fetch(sponsorBlockURL, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
redirect: "follow", redirect: 'follow',
}); });
if (resp.status !== 200) { if (resp.status !== 200) {
return []; return [];
} }
const segments = await resp.json(); const segments = await resp.json();
const sortedSegments = sortSegments( const sortedSegments = sortSegments(
segments.map((submission) => submission.segment) segments.map((submission) => submission.segment),
); );
return sortedSegments; return sortedSegments;
} catch (e) { } catch (error) {
if (is.dev()) { if (is.dev()) {
console.log('error on sponsorblock request:', e); console.log('error on sponsorblock request:', error);
} }
return []; return [];
} }
}; };

View File

@ -1,31 +1,30 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require('electron');
const is = require('electron-is');
const is = require("electron-is");
let currentSegments = []; let currentSegments = [];
module.exports = () => { module.exports = () => {
ipcRenderer.on("sponsorblock-skip", (_, segments) => { ipcRenderer.on('sponsorblock-skip', (_, segments) => {
currentSegments = segments; currentSegments = segments;
}); });
document.addEventListener('apiLoaded', () => { document.addEventListener('apiLoaded', () => {
const video = document.querySelector('video'); const video = document.querySelector('video');
video.addEventListener('timeupdate', e => { video.addEventListener('timeupdate', (e) => {
currentSegments.forEach((segment) => { for (const segment of currentSegments) {
if ( if (
e.target.currentTime >= segment[0] && e.target.currentTime >= segment[0]
e.target.currentTime < segment[1] && e.target.currentTime < segment[1]
) { ) {
e.target.currentTime = segment[1]; e.target.currentTime = segment[1];
if (is.dev()) { if (is.dev()) {
console.log("SponsorBlock: skipping segment", segment); console.log('SponsorBlock: skipping segment', segment);
}
} }
} }
}); });
})
// Reset segments on song end // Reset segments on song end
video.addEventListener('emptied', () => currentSegments = []); video.addEventListener('emptied', () => currentSegments = []);
}, { once: true, passive: true }) }, { once: true, passive: true });
}; };

View File

@ -3,26 +3,27 @@ module.exports.sortSegments = (segments) => {
segments.sort((segment1, segment2) => segments.sort((segment1, segment2) =>
segment1[0] === segment2[0] segment1[0] === segment2[0]
? segment1[1] - segment2[1] ? segment1[1] - segment2[1]
: segment1[0] - segment2[0] : segment1[0] - segment2[0],
); );
const compiledSegments = []; const compiledSegments = [];
let currentSegment; let currentSegment;
segments.forEach((segment) => { for (const segment of segments) {
if (!currentSegment) { if (!currentSegment) {
currentSegment = segment; currentSegment = segment;
return; continue;
} }
if (currentSegment[1] < segment[0]) { if (currentSegment[1] < segment[0]) {
compiledSegments.push(currentSegment); compiledSegments.push(currentSegment);
currentSegment = segment; currentSegment = segment;
return; continue;
} }
currentSegment[1] = Math.max(currentSegment[1], segment[1]); currentSegment[1] = Math.max(currentSegment[1], segment[1]);
}); }
compiledSegments.push(currentSegment); compiledSegments.push(currentSegment);
return compiledSegments; return compiledSegments;

View File

@ -1,14 +1,14 @@
const { test, expect } = require("@playwright/test"); const { test, expect } = require('@playwright/test');
const { sortSegments } = require("../segments"); const { sortSegments } = require('../segments');
test("Segment sorting", () => { test('Segment sorting', () => {
expect( expect(
sortSegments([ sortSegments([
[0, 3], [0, 3],
[7, 8], [7, 8],
[5, 6], [5, 6],
]) ]),
).toEqual([ ).toEqual([
[0, 3], [0, 3],
[5, 6], [5, 6],
@ -20,7 +20,7 @@ test("Segment sorting", () => {
[0, 5], [0, 5],
[6, 8], [6, 8],
[4, 6], [4, 6],
]) ]),
).toEqual([[0, 8]]); ).toEqual([[0, 8]]);
expect( expect(
@ -28,7 +28,7 @@ test("Segment sorting", () => {
[0, 6], [0, 6],
[7, 8], [7, 8],
[4, 6], [4, 6],
]) ]),
).toEqual([ ).toEqual([
[0, 6], [0, 6],
[7, 8], [7, 8],

View File

@ -1,25 +1,26 @@
const path = require('node:path');
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 path = require('path');
let controls; let controls;
let currentSongInfo; let currentSongInfo;
module.exports = win => { module.exports = (win) => {
const { playPause, next, previous } = getSongControls(win); const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous }; controls = { playPause, next, previous };
registerCallback(songInfo => { registerCallback((songInfo) => {
//update currentsonginfo for win.on('show') // Update currentsonginfo for win.on('show')
currentSongInfo = songInfo; currentSongInfo = songInfo;
// update thumbar // Update thumbar
setThumbar(win, songInfo); setThumbar(win, songInfo);
}); });
// need to set thumbar again after win.show // Need to set thumbar again after win.show
win.on("show", () => { win.on('show', () => {
setThumbar(win, currentSongInfo) setThumbar(win, currentSongInfo);
}) });
}; };
function setThumbar(win, songInfo) { function setThumbar(win, songInfo) {
@ -33,21 +34,27 @@ function setThumbar(win, songInfo) {
{ {
tooltip: 'Previous', tooltip: 'Previous',
icon: get('previous'), icon: get('previous'),
click() { controls.previous(win.webContents); } click() {
controls.previous(win.webContents);
},
}, { }, {
tooltip: 'Play/Pause', tooltip: 'Play/Pause',
// Update icon based on play state // Update icon based on play state
icon: songInfo.isPaused ? get('play') : get('pause'), icon: songInfo.isPaused ? get('play') : get('pause'),
click() { controls.playPause(win.webContents); } click() {
controls.playPause(win.webContents);
},
}, { }, {
tooltip: 'Next', tooltip: 'Next',
icon: get('next'), icon: get('next'),
click() { controls.next(win.webContents); } click() {
} controls.next(win.webContents);
},
},
]); ]);
} }
// Util // Util
function get(kind) { function get(kind) {
return path.join(__dirname, "../../assets/media-icons-black", `${kind}.png`); return path.join(__dirname, '../../assets/media-icons-black', `${kind}.png`);
} }

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