Compare commits

...

427 Commits

Author SHA1 Message Date
TC
f44b6f0c33 Bump version to 1.15.0 2021-12-30 19:39:20 +01:00
TC
c45e4e50fc Bump electron to 16.0.5 2021-12-30 19:39:03 +01:00
TC
9839a973f7 nit: re-format package.json 2021-12-30 19:35:53 +01:00
9ea967f03b Merge pull request #531 from th-ch/fix-tests
Switch from spectron to playwright to fix tests
2021-12-30 19:35:00 +01:00
TC
9d6765125b Switch from spectron to playwright to fix tests 2021-12-30 19:26:01 +01:00
TC
8d66735585 Add presets to FFmpeg in menu 2021-12-30 18:46:43 +01:00
14b4c55ce7 Merge pull request #529 from th-ch/snyk-upgrade-27f67e987dd094f8f1db19ad7f90c292
[Snyk] Upgrade @cliqz/adblocker-electron from 1.23.0 to 1.23.1
2021-12-30 17:59:53 +01:00
1d1f4bbcc3 Merge branch 'master' into snyk-upgrade-27f67e987dd094f8f1db19ad7f90c292 2021-12-30 17:59:34 +01:00
bd520c7eff Merge pull request #525 from Araxeus/fix-precise-volume-options-sync
fix precise-volume options sync
2021-12-30 17:57:13 +01:00
73e201bb2c Merge pull request #524 from MulverineX/patch-1
Add album art/thumbnail to discord activity
2021-12-30 15:10:07 +01:00
81b08917ae Merge pull request #521 from Araxeus/fix-skip-silences
fix skip-silences plugin
2021-12-30 15:08:56 +01:00
81c2ab34d9 Merge pull request #520 from th-ch/snyk-upgrade-7535be87da222abdba60d9fa36da34b5
[Snyk] Upgrade electron-updater from 4.6.2 to 4.6.3
2021-12-30 15:06:06 +01:00
TC
33faa2deb3 nit: improve comment for shared Array Buffer 2021-12-30 14:59:11 +01:00
TC
4d4ac56486 Ensure NODE_OPTIONS are unset in dev mode to avoid warning 2021-12-30 14:58:21 +01:00
56ac2b3b06 Merge pull request #515 from Araxeus/fix-useragents
update electron & remote & user agents
2021-12-30 14:57:36 +01:00
c72ea4bad5 fix: upgrade @cliqz/adblocker-electron from 1.23.0 to 1.23.1
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.23.0 to 1.23.1.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-12-27 20:52:46 +00:00
d60069555e Merge pull request #513 from markbaas/master
fixes mpris bug in snap
2021-12-27 16:12:40 +01:00
ed7025b4a2 fix precise-volume options sync 2021-12-24 02:13:21 +02:00
5fbc0f8122 Add album art/thumbnail to discord activity 2021-12-23 11:36:11 -07:00
02a989ca07 fix unnecessary skips 2021-12-18 21:03:32 +02:00
7c6fe6748e fix: upgrade electron-updater from 4.6.2 to 4.6.3
Snyk has created this PR to upgrade electron-updater from 4.6.2 to 4.6.3.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-12-18 11:02:24 +00:00
8f2ed3039a add comment to useragent fix 2021-12-16 19:29:09 +02:00
baeebd1959 downloader fixes
* --experimental-wasm-bulk-memory
* SharedArrayBuffer
* getFolder from front
* ytdl-core 4.9.2
2021-12-16 19:15:55 +02:00
49edbf723f fix custom-electron-prompt & remote 2021-12-16 18:28:49 +02:00
3764ce9a7c update custom-electron-titlebar 2021-12-16 18:01:11 +02:00
46943520bd Merge branch 'master' into fix-useragents 2021-12-15 19:32:14 +02:00
1048b3f99a Merge pull request #519 from th-ch/skip-silences-plugin
Add "Skip silences" plugin
2021-12-14 23:31:14 +01:00
TC
b1e40271e6 nit: re-order dependencies 2021-12-14 23:17:06 +01:00
TC
11429978c9 Add plugin to skip silences 2021-12-14 23:16:41 +01:00
47ca6e0b1f use session.webRequest.onBeforeSendHeaders for useragent 2021-12-14 19:22:02 +02:00
a273f6f73c fix video toggle button appearing when in song mode 2021-12-14 00:34:32 +02:00
c8ba85be76 use firefox as falllback useragent 2021-12-14 00:34:10 +02:00
6633243628 use @rozzzly/custom-electron-titlebar 2021-12-13 21:10:59 +02:00
2fb47933ac fix useragents 2021-12-13 21:06:52 +02:00
4fd683ed23 update electron & remote module 2021-12-13 20:10:36 +02:00
e1e9748002 always on useragent 2021-12-13 01:11:13 +02:00
dd122666c5 update useragents 2021-12-13 00:56:13 +02:00
5483f0ee36 Merge pull request #510 from MiepHD/master
Aligned lyric design
2021-12-10 21:15:36 +01:00
2c6c80d829 Merge pull request #509 from Araxeus/mpris-urgent-fix
Fix mpris bugs - follows #480
2021-12-10 21:14:31 +01:00
584d3e83c6 fixes mpris bug in snap 2021-12-10 20:52:16 +01:00
58d5256dd2 1vw would fit perfectly 2021-12-05 17:41:59 +01:00
920d61a1c6 Aligned lyric design
I aligned the lyric to the normal lyrics so that the lyric isn't that tiny
2021-12-04 20:53:19 +01:00
c5c2d5b74c Hide cast button which doesn't work 2021-12-03 16:56:51 +02:00
2daee01ff7 Fix bugs from bad merge conflict solving
-fix missing songControls
-use player.seeked directly
-fix 'seeked' event listener
-fix e.target instead of e.detail in apiLoaded event
-fix document.querySelector('video') before apiLoaded
-setup timeChange Listener if linux+shortcuts enabled
2021-12-03 15:55:40 +02:00
d13c9b7ca6 Merge pull request #476 from Araxeus/mix-fixes
Various small fixes (discord, video-toggle, precise-volume, playback-speed, shortcuts, lyrics)
2021-12-02 21:11:28 +01:00
5296a88525 Merge pull request #480 from Araxeus/mpris+tuna-fix
Mpris + obs-tuna fixes
2021-12-02 21:03:03 +01:00
8ce4b5b297 lint 2021-12-01 21:44:48 +02:00
2c39c0efed Merge branch 'master' into mpris+tuna-fix 2021-12-01 20:11:59 +02:00
e917abaec9 fix merge error 2021-12-01 00:08:55 +02:00
bdd0a2e8db Merge branch 'master' into mix-fixes 2021-12-01 00:06:15 +02:00
362003e10e Merge pull request #498 from th-ch/snyk-upgrade-9d2eea8c019b6593f1ef01f7fe8f404b
[Snyk] Upgrade node-fetch from 2.6.5 to 2.6.6
2021-11-30 00:16:34 +01:00
4e4b557413 Merge pull request #491 from Araxeus/fix-blur
fix interaction between blur navbar & in-app-menu
2021-11-30 00:14:49 +01:00
3a068af925 Merge pull request #475 from th-ch/snyk-upgrade-55c8a0f6d6911f431ebf75ad846e8f6c
[Snyk] Upgrade @cliqz/adblocker-electron from 1.22.7 to 1.23.0
2021-11-30 00:12:21 +01:00
44ca812330 Merge pull request #488 from Rubecks/exponential-volume-plugin
New Plugin: Exponential Volume
2021-11-30 00:10:48 +01:00
74a69e1c7a Merge pull request #474 from th-ch/snyk-upgrade-267eeda31c348d529d38d5a6413ef858
[Snyk] Upgrade electron-updater from 4.6.0 to 4.6.1
2021-11-30 00:06:42 +01:00
c3ef16c3dd Merge pull request #477 from Araxeus/fix-loadeddata/metdata-events-rarely-not-firing
Fix loadeddata/metadata video events rarely not firing (+other small fixes)
2021-11-29 23:52:23 +01:00
8c5ac17cdf fix multiple songInfo calls on start 2021-11-23 18:53:04 +02:00
c99b95e611 use config.plugins.isEnabled 2021-11-22 22:52:38 +02:00
4362101c0a lint 2021-11-22 22:08:24 +02:00
7ba205cc6c fix backquotes in keybind prompt 2021-11-22 18:59:29 +02:00
abc1712cf7 fix counter prompt 2021-11-22 18:33:32 +02:00
92452f804f fix song-info-request 2021-11-22 18:20:12 +02:00
c76df84ce3 fix: upgrade node-fetch from 2.6.5 to 2.6.6
Snyk has created this PR to upgrade node-fetch from 2.6.5 to 2.6.6.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-11-22 04:09:33 +00:00
185ebbf417 fix downloader playlist download 2021-11-21 19:44:01 +02:00
6726e2600b rework songInfo pause listener 2021-11-14 23:49:19 +02:00
93e0664f95 fix height & blur of search page item header 2021-11-14 22:02:37 +02:00
bf45ed10aa fix #490 2021-11-14 21:45:34 +02:00
8da78d50c4 Exponential Volume Plugin 2021-11-12 17:58:39 -03:00
b27a959c2b fix video-toggle&precise-volume interaction 2021-11-12 18:42:58 +02:00
cfe719b6bd use native thumbnail without modifiers 2021-11-12 17:46:40 +02:00
071799c435 fix some shortcuts 2021-11-12 16:39:43 +02:00
87ee7ed83d lint video-toggle 2021-11-10 22:35:49 +02:00
08fdd07969 speed up sponsorblock 2021-11-10 20:44:13 +02:00
02d5b78f55 add songInfo.album 2021-11-10 20:11:45 +02:00
5492afe5f6 add catch to fetch 2021-11-10 19:08:19 +02:00
9a7baeac23 fix tuna time update 2021-11-10 18:45:42 +02:00
ccfe7434bf fix mpris 2021-11-10 18:23:55 +02:00
6dbed73e6b fix disable autoplay 2021-11-09 18:52:03 +02:00
895136af0a used youtube's videodatachange event 2021-11-09 17:57:06 +02:00
72b4398024 lint&fix video-toggle plugin 2021-11-09 15:17:26 +02:00
65ce62adc1 use $('video') srcChanged event instead of loadeddata/metadata 2021-11-09 13:29:41 +02:00
eafdd5046d fix lyric text size 2021-11-09 10:42:32 +02:00
bbece751c0 lint playback speed 2021-11-09 10:03:06 +02:00
719c244e32 fix #472 2021-11-09 10:01:33 +02:00
e70b41b256 fix: upgrade @cliqz/adblocker-electron from 1.22.7 to 1.23.0
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.22.7 to 1.23.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-11-09 05:45:20 +00:00
f4b6fd53f3 fix: upgrade electron-updater from 4.6.0 to 4.6.1
Snyk has created this PR to upgrade electron-updater from 4.6.0 to 4.6.1.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-11-09 05:45:17 +00:00
f40ed04899 Merge pull request #467 from th-ch/snyk-upgrade-cfd310bb818846c87b2239715bd6d1c1
[Snyk] Upgrade custom-electron-prompt from 1.1.0 to 1.2.0
2021-11-07 15:56:27 +01:00
c897323be0 fix: upgrade custom-electron-prompt from 1.1.0 to 1.2.0
Snyk has created this PR to upgrade custom-electron-prompt from 1.1.0 to 1.2.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-11-02 06:46:27 +00:00
TC
d7c4716a6e Re-order plugins in readme 2021-11-01 22:49:34 +01:00
TC
461cac741b nit: add new line at end of file 2021-11-01 22:42:28 +01:00
TC
cee2e066b9 Fix apiLoaded event listener in video-toggle plugin 2021-11-01 22:42:03 +01:00
TC
953d6fe3e4 Migrate old "hide-video-player" plugin to new one 2021-11-01 22:40:26 +01:00
c592a26e42 Merge pull request #448 from Araxeus/new-hide-video-player
Video Toggle Plugin
2021-11-01 22:11:38 +01:00
62bacf76d0 change file names to lower case 2021-11-01 23:10:15 +02:00
fc254db010 Merge pull request #462 from Araxeus/fix-playback-speed-plugin
fix playback speed plugin
2021-11-01 22:10:02 +01:00
0287b69424 fix missing thumbnails
+forced push fix woupsie
2021-11-01 23:04:39 +02:00
ceebd99927 Merge pull request #465 from Araxeus/fix-sponsorblock-v2
Fix sponsorblock skipping when not needed
2021-11-01 21:54:05 +01:00
41285ac9fc fix slider not working on tap 2021-11-01 22:23:26 +02:00
7b9415033f use loadedmetadata instead of loadeddata 2021-11-01 20:03:57 +02:00
b1ffd93bc2 lint in-app-menu 2021-11-01 19:57:28 +02:00
64637a6ac5 Reset segments on song end 2021-11-01 19:57:15 +02:00
b83afa22d9 Merge remote-tracking branch 'upstream/master' into fix-playback-speed-plugin 2021-11-01 18:07:54 +02:00
c68daabeab lint audio-compressor plugin 2021-11-01 18:07:21 +02:00
9ec0830a65 Merge remote-tracking branch 'upstream/master' into new-hide-video-player 2021-11-01 17:59:35 +02:00
48943ee74b lint 2021-11-01 16:06:55 +02:00
29d5b3c7db lint 2021-11-01 16:02:27 +02:00
TC
3de574a2e4 Remove debug log from genius plugin + add a dev log 2021-11-01 12:13:22 +01:00
e765d18ab0 Merge pull request #463 from Araxeus/use-apiLoaded-event-for-time-update
Sponsorblock fix + use new apiLoaded event
2021-11-01 12:03:47 +01:00
4629759eec Merge pull request #458 from Araxeus/use-apiReady-on-audio-compressor-plugin
use apiLoaded event in audio-compressor plugin
2021-11-01 11:47:11 +01:00
a14d27da70 Merge pull request #456 from Araxeus/hide-menu-initial-alert
alert on initial hide-menu enabled
2021-11-01 11:44:27 +01:00
5256ffcf77 Merge pull request #451 from Araxeus/fix-blur-plugin
Blur plugin tweaks and integration with in-app-menu
2021-11-01 11:34:54 +01:00
e2bbc6abbc Merge pull request #449 from Araxeus/fix-resume-on-start
set resume on start url to songInfo.url
2021-11-01 11:30:03 +01:00
6243e6fd48 set max playback speed to 16 ;) 2021-10-30 15:56:12 +03:00
1434849142 simplify playback rate steps 2021-10-30 15:40:45 +03:00
9bd089adb0 add check for lowest playback speed (0.07) 2021-10-30 14:57:16 +03:00
6c1a4c0ac2 playbackSpeed wheel listener 2021-10-30 14:49:03 +03:00
d35fef82fe fix sponsoblock spam updating time 2021-10-30 13:30:38 +03:00
bca22d8e24 update electron to v12.2.2 fixes fetch CERT_HAS_EXPIRED error 2021-10-30 13:17:47 +03:00
754eac6ee0 use apiLoaded 'once'
lint
2021-10-30 12:53:14 +03:00
762566cce6 lint 2021-10-30 12:52:04 +03:00
286bc0113e fix sponsorblock plugin 2021-10-30 12:43:51 +03:00
62e8e673eb use apiLoaded 'once' 2021-10-30 12:17:32 +03:00
68996809f0 add apiLoaded event to disable-autoplay plugin 2021-10-30 12:05:03 +03:00
48a2a13163 fix playback speed plugin 2021-10-30 12:00:29 +03:00
6e94422b15 use apiLoaded event in audio-compressor plugin 2021-10-26 20:57:56 +03:00
713e005aa8 Update readme.md 2021-10-26 20:45:43 +03:00
3f3ab766ce Update readme.md 2021-10-26 20:34:44 +03:00
00e1bbf994 Update readme.md 2021-10-26 20:31:27 +03:00
b45adac847 Update readme.md 2021-10-26 20:03:01 +03:00
3d9b495863 Update readme.md 2021-10-26 19:50:15 +03:00
a70364facf lint 2021-10-26 19:23:10 +03:00
02cb39602f alert on initial hide-menu enabled 2021-10-26 19:07:10 +03:00
12c31725fe fixes scrollbar position relative to navbar depending on player-page-open 2021-10-25 22:45:11 +03:00
e0841060df fix comment typo 2021-10-25 20:20:52 +03:00
b7b55b5c83 Add blur to (library)header + make in-app-menu compatible 2021-10-25 00:43:42 +03:00
43a9093eb7 set resume on start url to songInfo.url 2021-10-24 21:17:45 +03:00
67e43bc0e3 Video Toggle Plugin 2021-10-24 20:09:30 +03:00
TC
a47c906ab2 Add audio compressor plugin to readme 2021-10-24 17:55:37 +02:00
TC
41a01ba58a Bump dev deps 2021-10-24 14:32:35 +02:00
TC
ca2bd011e2 Document quality changer plugin in readme 2021-10-24 14:08:47 +02:00
2106914aff Merge pull request #446 from Araxeus/quality-changer-plugin
quality-changer-plugin
2021-10-24 14:06:08 +02:00
1c11ddbb7d Merge branch 'master' into quality-changer-plugin 2021-10-24 14:03:40 +02:00
TC
fc1211f7a1 Bump version to 1.14.0 2021-10-24 14:02:13 +02:00
TC
18f87c7b0d Add migration for precise-volume plugin (globalShortcuts key) 2021-10-24 14:02:01 +02:00
TC
f9a4bffa55 Fix styling of store migrations 2021-10-24 14:01:29 +02:00
TC
02f4aabead Fix condition on query selector 2021-10-24 14:01:00 +02:00
f97dade168 Merge pull request #443 from Araxeus/songInfo-straight-from-youtube-api
get songInfo from youtube API
2021-10-24 13:42:59 +02:00
8c3a6472f8 Merge branch 'master' into songInfo-straight-from-youtube-api 2021-10-24 13:40:48 +02:00
315f9783f5 Merge pull request #442 from cdaydreamer/master
New plugin: Blur navigation bar
2021-10-24 13:34:45 +02:00
ada78837ce Merge branch 'master' into master 2021-10-24 13:34:15 +02:00
58c6a12e53 Merge pull request #440 from cpiber/discord-clean
Discord plugin: Clean Up Export (follow-up #380)
2021-10-24 13:29:34 +02:00
005c930d58 Merge pull request #434 from Araxeus/remove-upgrade-button
remove upgrade button + makes images unselectable
2021-10-24 13:28:43 +02:00
7c2891b732 Merge branch 'master' into remove-upgrade-button 2021-10-24 13:28:15 +02:00
8b36139dab Merge pull request #433 from Araxeus/new-auto-confirm
new auto confirm when paused
2021-10-24 12:44:19 +02:00
a045d65e58 Merge pull request #431 from itzmanish/fix/mpris
fix: mpris instance not registering itself and media controls
2021-10-24 12:42:31 +02:00
7b064c1e6f Merge pull request #288 from thymue/compressor-plugin
Audio compressor plugin
2021-10-24 12:17:28 +02:00
6a2e3ab6c1 Merge pull request #275 from Araxeus/precise-volume-HUD
precise-volume plugin fixes & updates
2021-10-24 11:40:54 +02:00
362da8c308 Merge pull request #243 from Araxeus/custom-electron-prompt
Custom Prompt for changing options
2021-10-24 11:22:20 +02:00
5658765f54 quality-changer-plugin 2021-10-24 02:24:25 +03:00
38449f003a use apiLoad event 2021-10-23 18:28:38 +03:00
df75e480a6 use apiLoad event 2021-10-23 18:27:35 +03:00
79a95f133b use apiLoad event 2021-10-23 18:22:17 +03:00
9b1a5b8d26 clarify var names in cleanupName() 2021-10-23 17:18:58 +03:00
bb2e1bd616 use youtube native api to change volume 2021-10-23 16:57:44 +03:00
2d518abc19 remove leftover console.log 2021-10-23 16:26:45 +03:00
978aca1f9a use loadeddata instead of loadedmetadata
send event closer to actual initial start time of video
2021-10-23 16:21:42 +03:00
2224786478 lint 2021-10-23 16:19:45 +03:00
51364b63e7 get songInfo from youtube API 2021-10-23 16:13:06 +03:00
c897bedd90 New plugin: Blur navigation bar 2021-10-23 15:38:46 +03:00
831b1ea8e1 access window._lact directly 2021-10-23 15:21:49 +03:00
4d4dacbc71 discord: clean up export (follow-up #380) 2021-10-19 18:40:27 +02:00
1cd4f53657 Merge branch 'master' into custom-electron-prompt 2021-10-19 15:55:00 +03:00
2eda0e4948 Merge branch 'master' into new-auto-confirm 2021-10-19 15:19:58 +03:00
f2e04f9170 Merge pull request #412 from th-ch/snyk-upgrade-3ac551b4f96cf4c26f3157f42a916769
[Snyk] Upgrade async-mutex from 0.3.1 to 0.3.2
2021-10-18 23:34:29 +02:00
c92b3915d9 Merge pull request #414 from th-ch/dependabot/npm_and_yarn/tmpl-1.0.5
build(deps): bump tmpl from 1.0.4 to 1.0.5
2021-10-18 23:31:59 +02:00
6118a17b08 Merge pull request #416 from th-ch/snyk-upgrade-d31d43c15fe12eab06e73b6f8faeda29
[Snyk] Upgrade node-fetch from 2.6.1 to 2.6.2
2021-10-18 23:30:04 +02:00
1490c0f179 Merge pull request #429 from th-ch/snyk-upgrade-129504aaea5df4956b1cd910a6775c4a
[Snyk] Upgrade @cliqz/adblocker-electron from 1.22.5 to 1.22.6
2021-10-18 23:23:27 +02:00
fdf203e70a Merge pull request #430 from th-ch/dependabot/npm_and_yarn/electron-12.1.0
build(deps-dev): bump electron from 12.0.8 to 12.1.0
2021-10-18 23:18:29 +02:00
8114a28964 Merge pull request #380 from cpiber/discord
Fix discord clearActivity, menu, listen along option
2021-10-18 23:10:24 +02:00
663507b3f8 fix: player status when play at start 2021-10-17 11:06:26 +05:30
79d0c7b666 fix: typo 2021-10-17 10:54:05 +05:30
ce4580605d remove upgrade button + makes img unselectable 2021-10-15 15:40:34 +03:00
dda18a72af new auto confirm when paused 2021-10-15 14:38:09 +03:00
f7a1de05c8 defensive coding 2021-10-15 05:41:47 +03:00
361606427a fix: remove unused play pause functions 2021-10-14 10:18:05 +05:30
81fb5118aa fix: don't create play pause method unneccessarily 2021-10-14 10:16:36 +05:30
a76f12c01c feat: add play and pause seperate song controller. 2021-10-13 12:27:13 +05:30
88ee0fb989 fix: mpris was not registering itself before.
Sorry I missed that somehow, because playerctl controls were working.
That was because of chromium was also registering itself for mpris.
2021-10-13 12:26:38 +05:30
3ec49bca74 build(deps-dev): bump electron from 12.0.8 to 12.1.0
Bumps [electron](https://github.com/electron/electron) from 12.0.8 to 12.1.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v12.0.8...v12.1.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-12 22:10:41 +00:00
1908921ae6 fix: upgrade @cliqz/adblocker-electron from 1.22.5 to 1.22.6
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.22.5 to 1.22.6.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-10-11 21:23:58 +00:00
b9dbd8bd4d Allow disable listen along (#426) 2021-10-11 15:02:24 +02:00
587818b91e Add type, clear on close 2021-10-05 10:10:36 +02:00
157ae05f80 fix: upgrade node-fetch from 2.6.1 to 2.6.2
Snyk has created this PR to upgrade node-fetch from 2.6.1 to 2.6.2.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-09-27 19:13:33 +00:00
f2039e29e7 build(deps): bump tmpl from 1.0.4 to 1.0.5
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-21 21:12:33 +00:00
ea2d33c3cf fix: upgrade async-mutex from 0.3.1 to 0.3.2
Snyk has created this PR to upgrade async-mutex from 0.3.1 to 0.3.2.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-09-21 03:24:11 +00:00
d775e3d588 Merge pull request #406 from th-ch/snyk-upgrade-3c48abddd3d31d59f08172a32b5dd378
[Snyk] Upgrade @cliqz/adblocker-electron from 1.22.4 to 1.22.5
2021-09-19 16:13:09 +02:00
e7ec15e90f fix: upgrade @cliqz/adblocker-electron from 1.22.4 to 1.22.5
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.22.4 to 1.22.5.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-09-16 00:27:20 +00:00
TC
403470be69 Bump version 2021-09-15 22:59:06 +02:00
6dc0ba74c4 Merge pull request #384 from konhi/useragent
Fix incorrect Google alert caused by changing user agent coresponding to current platform
2021-09-15 22:54:10 +02:00
TC
6dcfb336c2 Fix missing import in shortcuts plugin 2021-09-15 22:51:58 +02:00
84516b2ac1 Merge pull request #401 from th-ch/snyk-upgrade-4b273faf3969a8ede6309124c5ce3e75
[Snyk] Upgrade electron-updater from 4.4.3 to 4.4.6
2021-09-15 22:50:15 +02:00
57cf2a8cdd fix: upgrade electron-updater from 4.4.3 to 4.4.6
Snyk has created this PR to upgrade electron-updater from 4.4.3 to 4.4.6.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-09-13 01:17:33 +00:00
e3ae97fec4 Merge pull request #370 from th-ch/snyk-upgrade-eeb9d7e7304322861f42850feedf7f74
[Snyk] Upgrade electron-updater from 4.4.0 to 4.4.1
2021-09-12 23:10:47 +02:00
TC
ee76e2cb45 Merge branch 'master' of github.com:th-ch/youtube-music into snyk-upgrade-eeb9d7e7304322861f42850feedf7f74
* 'master' of github.com:th-ch/youtube-music:
  Bump node to v14
  nit: fix code style for tuna obs (+ typo)
  add tuna plugin for obs
  Add mpris support
  Update menu buttons to new format
  Bump jszip from 3.5.0 to 3.7.1
  Add Genius lyrics plugin
  Apply clean up util to title + enrich prefixes
  Bump node to v14
  fix: upgrade @cliqz/adblocker-electron from 1.22.2 to 1.22.3
  Add "Listen Along" button
  sort alphabetically
  suggestions from @Araxeus and @cpiber
  Fix broken link
  update descriptions and add images
  List missing plugins
  Bump path-parse from 1.0.6 to 1.0.7
2021-09-12 23:03:23 +02:00
de01bb6e75 Merge pull request #375 from th-ch/dependabot/npm_and_yarn/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7
2021-09-12 23:02:52 +02:00
TC
42668c3e99 Merge branch 'master' of github.com:th-ch/youtube-music into dependabot/npm_and_yarn/path-parse-1.0.7
* 'master' of github.com:th-ch/youtube-music:
  Bump node to v14
  nit: fix code style for tuna obs (+ typo)
  add tuna plugin for obs
  Add mpris support
  Update menu buttons to new format
  Bump jszip from 3.5.0 to 3.7.1
  Add Genius lyrics plugin
  Apply clean up util to title + enrich prefixes
  Bump node to v14
  fix: upgrade @cliqz/adblocker-electron from 1.22.2 to 1.22.3
  Add "Listen Along" button
  sort alphabetically
  suggestions from @Araxeus and @cpiber
  Fix broken link
  update descriptions and add images
  List missing plugins
2021-09-12 22:55:08 +02:00
05f3c56e47 Merge pull request #385 from th-ch/snyk-upgrade-f132bed3bdbd1c212a2d1580af9a445a
[Snyk] Upgrade @cliqz/adblocker-electron from 1.22.2 to 1.22.3
2021-09-12 22:53:39 +02:00
TC
d54977b9ee Bump node to v14 2021-09-12 22:42:31 +02:00
b89fb4dc2f Merge pull request #388 from th-ch/dependabot/npm_and_yarn/jszip-3.7.1
Bump jszip from 3.5.0 to 3.7.1
2021-09-12 22:33:50 +02:00
a0cf77edfb Merge pull request #382 from konhi/patch-1
List missing plugins
2021-09-12 22:20:53 +02:00
TC
069f9855d1 nit: fix code style for tuna obs (+ typo) 2021-09-12 22:17:54 +02:00
e3e0775401 Merge pull request #397 from mesmerx/master
add tuna plugin for obs
2021-09-12 22:16:22 +02:00
d255e5ffe1 Merge pull request #389 from th-ch/fix-menu-buttons
Update menu buttons to new format
2021-09-12 22:13:46 +02:00
fea460a374 Merge pull request #387 from th-ch/lyrics-genius-plugin
Plugin to fetch lyrics from Genius
2021-09-12 22:10:43 +02:00
302d3f693f add tuna plugin for obs 2021-09-08 22:45:49 -03:00
9cc320d74b Merge pull request #395 from itzmanish/feat/mpris-support
Add mpris support with cherry picked commit from previous PR https://github.com/th-ch/youtube-music/pull/394
2021-09-07 00:12:34 +02:00
e255777283 Add mpris support 2021-09-02 16:00:42 +05:30
ef66612cc8 Discord show error dialog on reconnect error 2021-08-27 17:07:23 +02:00
4bed835347 Merge branch 'master' of https://github.com/th-ch/youtube-music into discord 2021-08-27 16:37:50 +02:00
b5fd6b4969 Discord add reconnecting functionality
Clear rpc on disconnect
Add menu button to reconnect
2021-08-27 16:32:55 +02:00
fe0f213919 Merge pull request #383 from konhi/discord
Add "Listen Along" button, solve #353
2021-08-23 01:19:00 +02:00
TC
e888b5c896 Update menu buttons to new format 2021-08-23 01:17:29 +02:00
f27ff52689 Bump jszip from 3.5.0 to 3.7.1
Bumps [jszip](https://github.com/Stuk/jszip) from 3.5.0 to 3.7.1.
- [Release notes](https://github.com/Stuk/jszip/releases)
- [Changelog](https://github.com/Stuk/jszip/blob/master/CHANGES.md)
- [Commits](https://github.com/Stuk/jszip/compare/v3.5.0...v3.7.1)

---
updated-dependencies:
- dependency-name: jszip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-22 23:16:24 +00:00
TC
acbe0ac25d Add Genius lyrics plugin 2021-08-23 01:12:53 +02:00
TC
c66ff2bf05 Apply clean up util to title + enrich prefixes 2021-08-23 01:12:53 +02:00
d089487aa8 Merge pull request #386 from th-ch/bump-node-version
Bump node to v14
2021-08-23 01:12:27 +02:00
TC
6bc1d1606f Bump node to v14 2021-08-23 01:05:45 +02:00
9df5d921c7 Chrome -> Firefox, simplified using electron-is
suggestions by @Araxeus
2021-08-20 12:33:46 +02:00
4b1dfa1173 fix: upgrade @cliqz/adblocker-electron from 1.22.2 to 1.22.3
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.22.2 to 1.22.3.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-08-19 21:20:28 +00:00
f98318e737 Add platform-based user-agent 2021-08-19 16:49:34 +02:00
7fa1278b31 Add "Listen Along" button 2021-08-19 16:16:49 +02:00
878ec1f6c1 sort alphabetically 2021-08-19 15:09:31 +02:00
086048780a suggestions from @Araxeus and @cpiber 2021-08-19 15:04:47 +02:00
65eaaecae5 Merge branch 'master' into custom-electron-prompt 2021-08-19 13:44:42 +03:00
aff0415816 Fix broken link
Oops.
2021-08-19 00:15:25 +02:00
6040fe1cbd update descriptions and add images 2021-08-19 00:08:17 +02:00
36bc9c62b0 Discord timeout 0 clear activity directly 2021-08-18 21:39:40 +02:00
3901457218 Discord add menu button for clearing activity 2021-08-17 10:09:47 +02:00
52f4e9d796 List missing plugins 2021-08-16 18:49:15 +02:00
183bad43f6 Fix discord clearActivity, menu
The callback sends multiple events, in particular two pause when going to the
next song, so the timeout wasn't properly cleared.
Add menu buttons for the two options
2021-08-15 12:25:00 +02:00
09fe80cae7 Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-12 03:24:48 +00:00
817b48dc9d fix: upgrade electron-updater from 4.4.0 to 4.4.1
Snyk has created this PR to upgrade electron-updater from 4.4.0 to 4.4.1.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-08-06 21:42:35 +00:00
c6f8c42c45 Merge pull request #350 from th-ch/snyk-upgrade-4809caaf0847354b9b537aa0f8a0999d
[Snyk] Upgrade electron-updater from 4.3.9 to 4.3.10
2021-08-06 23:23:12 +02:00
TC
0535686129 Merge branch 'master' of github.com:th-ch/youtube-music into snyk-upgrade-4809caaf0847354b9b537aa0f8a0999d
# By TC (1) and snyk-bot (1)
# Via GitHub (1) and TC (1)
* 'master' of github.com:th-ch/youtube-music:
  Bump ytdl/ytpl
  fix: upgrade chokidar from 3.5.1 to 3.5.2
2021-08-06 23:22:44 +02:00
53a77255ca Merge pull request #354 from th-ch/snyk-upgrade-1a48119c8989bb533950b3384b3bce29
[Snyk] Upgrade chokidar from 3.5.1 to 3.5.2
2021-08-06 23:17:51 +02:00
TC
c01506dc44 Bump ytdl/ytpl 2021-08-06 23:12:29 +02:00
a49817fdc3 Merge branch 'master' into custom-electron-prompt 2021-07-20 11:06:08 +03:00
52a4608d76 create options.global if needed 2021-07-20 10:57:32 +03:00
6f5f9386ff fix: upgrade chokidar from 3.5.1 to 3.5.2
Snyk has created this PR to upgrade chokidar from 3.5.1 to 3.5.2.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-07-07 05:50:15 +00:00
fddd0607e6 fix: upgrade electron-updater from 4.3.9 to 4.3.10
Snyk has created this PR to upgrade electron-updater from 4.3.9 to 4.3.10.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-07-02 06:17:28 +00:00
TC
2cb6f56feb Bump version 2021-07-01 22:43:35 +02:00
TC
46285a5ed0 Bump glob-parent and add lint script (xo) 2021-06-27 21:19:33 +02:00
TC
496836b33b Bump dependencies to fix vulnerabilities 2021-06-27 21:12:28 +02:00
af127879a5 Merge pull request #339 from th-ch/fix-downloader-plugin
Fix downloader plugin
2021-06-27 21:10:35 +02:00
TC
38ef452801 Bump ffmpeg version 2021-06-27 20:46:48 +02:00
TC
a9a5d99676 Do not add network filters in adblocker cache to fix session enhancing 2021-06-27 20:46:48 +02:00
TC
e5ab50cebd Override content security policy to allow FFmpeg worker 2021-06-27 20:46:46 +02:00
TC
49194f8141 nit: re-order dependencies 2021-06-27 20:39:05 +02:00
TC
641ae27efd Update ytdl-core and ytpl 2021-06-27 20:38:08 +02:00
47a5dec465 Merge pull request #337 from th-ch/snyk-upgrade-fe663d0d7c5fc658f327e05ae5966f76
[Snyk] Upgrade @cliqz/adblocker-electron from 1.22.0 to 1.22.1
2021-06-25 23:11:25 +02:00
c93eabb400 Merge pull request #249 from Araxeus/update-in-app-menu
Update and simplify in-app-menu
2021-06-25 23:03:05 +02:00
664be51de2 Merge branch 'master' into custom-electron-prompt 2021-06-25 12:21:40 +03:00
492a47321d Merge branch 'master' into update-in-app-menu 2021-06-25 12:19:39 +03:00
c89f6af8c6 fix: upgrade @cliqz/adblocker-electron from 1.22.0 to 1.22.1
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.22.0 to 1.22.1.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-06-25 04:53:24 +00:00
9687c6c8e4 Merge pull request #331 from th-ch/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-06-24 22:06:27 +02:00
ef0a89126a Merge pull request #330 from th-ch/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2021-06-24 22:05:46 +02:00
8ce71d628d Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

---
updated-dependencies:
- dependency-name: lodash
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-24 20:04:35 +00:00
ca95d105c8 Merge pull request #328 from th-ch/snyk-upgrade-dacfb8f0b574367961e405e4912a4659
[Snyk] Upgrade ytdl-core from 4.8.0 to 4.8.2
2021-06-24 22:04:32 +02:00
12568c2b09 Merge pull request #324 from th-ch/snyk-upgrade-f650e2139b30a36951a4a00ed1b78d70
[Snyk] Upgrade electron-updater from 4.3.8 to 4.3.9
2021-06-24 22:03:15 +02:00
82abb4d4d3 Merge pull request #323 from th-ch/dependabot/npm_and_yarn/normalize-url-4.5.1
Bump normalize-url from 4.5.0 to 4.5.1
2021-06-24 22:02:52 +02:00
3c0a5dbbe5 Merge pull request #320 from th-ch/dependabot/npm_and_yarn/trim-newlines-3.0.1
Bump trim-newlines from 3.0.0 to 3.0.1
2021-06-24 22:02:33 +02:00
0b98eef06f Merge pull request #317 from th-ch/snyk-upgrade-a785f5d95c7765e2d47737e150a2263d
[Snyk] Upgrade @ffmpeg/core from 0.9.0 to 0.10.0
2021-06-24 22:02:05 +02:00
TC
18e69c9f2a Merge branch 'master' of github.com:th-ch/youtube-music into snyk-upgrade-a785f5d95c7765e2d47737e150a2263d
# By Araxeus (2) and snyk-bot (2)
# Via GitHub (3) and Araxeus (1)
* 'master' of github.com:th-ch/youtube-music:
  check if native image is empty before writing id tag
  fix unsupported hidden webp coverart
  fix: upgrade @ffmpeg/ffmpeg from 0.9.8 to 0.10.0
  fix: upgrade custom-electron-titlebar from 3.2.6 to 3.2.7
2021-06-24 22:01:16 +02:00
8f5d06d420 Merge pull request #316 from th-ch/snyk-upgrade-6e93904bf885d198521c6a5d8110bde3
[Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.8 to 0.10.0
2021-06-24 22:00:07 +02:00
8a299461a0 Merge pull request #311 from th-ch/snyk-upgrade-44a5db26689d4091f6a2c3c3c69b869a
[Snyk] Upgrade custom-electron-titlebar from 3.2.6 to 3.2.7
2021-06-24 21:49:53 +02:00
0c58bec921 Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

---
updated-dependencies:
- dependency-name: hosted-git-info
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-19 20:29:37 +00:00
e0cb132686 Merge pull request #318 from Araxeus/fix-hidden-webp-cover-art
fix hidden webp thumbnail throwing MIME type error in downloader
2021-06-19 22:28:59 +02:00
2a192f39f9 fix: upgrade ytdl-core from 4.8.0 to 4.8.2
Snyk has created this PR to upgrade ytdl-core from 4.8.0 to 4.8.2.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-06-19 05:30:04 +00:00
b7ebb7d499 fix: upgrade electron-updater from 4.3.8 to 4.3.9
Snyk has created this PR to upgrade electron-updater from 4.3.8 to 4.3.9.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-06-12 04:23:30 +00:00
fffeac21b7 Bump normalize-url from 4.5.0 to 4.5.1
Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/sindresorhus/normalize-url/releases)
- [Commits](https://github.com/sindresorhus/normalize-url/commits)

---
updated-dependencies:
- dependency-name: normalize-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-11 04:46:20 +00:00
4387cb485d Bump trim-newlines from 3.0.0 to 3.0.1
Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-09 18:57:55 +00:00
2a58dc823a check if native image is empty before writing id tag 2021-06-09 20:05:14 +03:00
8eb38271ff fix unsupported hidden webp coverart 2021-06-09 19:53:04 +03:00
1987ad1d4f fix: upgrade @ffmpeg/core from 0.9.0 to 0.10.0
Snyk has created this PR to upgrade @ffmpeg/core from 0.9.0 to 0.10.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-06-08 04:08:44 +00:00
cc4dae60ef fix: upgrade @ffmpeg/ffmpeg from 0.9.8 to 0.10.0
Snyk has created this PR to upgrade @ffmpeg/ffmpeg from 0.9.8 to 0.10.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-06-08 04:08:41 +00:00
1943116aa1 fix: upgrade custom-electron-titlebar from 3.2.6 to 3.2.7
Snyk has created this PR to upgrade custom-electron-titlebar from 3.2.6 to 3.2.7.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-06-05 03:53:12 +00:00
3485d26b11 Merge pull request #308 from th-ch/sponsorblock-plugin
Add Sponsorblock plugin
2021-06-04 22:25:26 +02:00
TC
4a60aa9f20 Keep segments when skipping 2021-06-03 22:15:36 +02:00
TC
cda07c9675 Update adblocking 2021-06-03 22:04:49 +02:00
TC
ca64a77ed0 Add SponsorBlock plugin 2021-06-03 21:47:26 +02:00
TC
30e94d1d6f Refactor videoElement getter into a provider with callback 2021-06-03 21:45:28 +02:00
TC
b8c6ebfa53 Set test environment per test file 2021-06-03 21:43:07 +02:00
b26748ded8 Merge pull request #305 from th-ch/snyk-upgrade-15656c519a90f5bc0f5c8742a9fb04e9
[Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.7 to 0.9.8
2021-05-31 22:06:17 +02:00
f186da0834 fix: upgrade @ffmpeg/ffmpeg from 0.9.7 to 0.9.8
Snyk has created this PR to upgrade @ffmpeg/ffmpeg from 0.9.7 to 0.9.8.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-05-29 04:38:22 +00:00
c52c2d886a Merge pull request #303 from th-ch/dependabot/npm_and_yarn/ws-7.4.6
Bump ws from 7.4.3 to 7.4.6
2021-05-28 23:40:55 +02:00
e5dc1f8a58 Bump ws from 7.4.3 to 7.4.6
Bumps [ws](https://github.com/websockets/ws) from 7.4.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.3...7.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-28 21:39:25 +00:00
6dbf4134de Merge pull request #301 from th-ch/dependabot/npm_and_yarn/browserslist-4.16.6
Bump browserslist from 4.16.3 to 4.16.6
2021-05-28 23:38:48 +02:00
e1cc49a74d Merge pull request #300 from th-ch/snyk-upgrade-ce2777733d9dee231391c0a822c24a84
[Snyk] Upgrade @cliqz/adblocker-electron from 1.20.4 to 1.20.5
2021-05-28 23:35:11 +02:00
TC
4489a400b7 Merge branch 'master' of github.com:th-ch/youtube-music into snyk-upgrade-ce2777733d9dee231391c0a822c24a84
# By Araxeus (5) and others
# Via GitHub (5) and TC (2)
* 'master' of github.com:th-ch/youtube-music:
  Bump electron to 12.0.8
  fix: upgrade ytdl-core from 4.5.0 to 4.7.0
  fix: upgrade @ffmpeg/core from 0.8.5 to 0.9.0
  fix notificationOnUnpause option
  fix: upgrade filenamify from 4.2.0 to 4.3.0
  switch to `registerCallback()` on song info
  fix: upgrade ytpl from 2.1.1 to 2.2.0
  lint
  refactor notifications plugin
  setup SongInfo **once**
2021-05-28 23:33:20 +02:00
28aa1c0b22 Merge pull request #299 from th-ch/snyk-upgrade-37e43892f5a34f935e8486a98b3598fb
[Snyk] Upgrade ytdl-core from 4.5.0 to 4.7.0
2021-05-28 23:32:29 +02:00
TC
c540788d20 Merge branch 'master' of github.com:th-ch/youtube-music into snyk-upgrade-37e43892f5a34f935e8486a98b3598fb
# By Araxeus (5) and others
# Via GitHub (4) and TC (1)
* 'master' of github.com:th-ch/youtube-music:
  Bump electron to 12.0.8
  fix: upgrade @ffmpeg/core from 0.8.5 to 0.9.0
  fix notificationOnUnpause option
  fix: upgrade filenamify from 4.2.0 to 4.3.0
  switch to `registerCallback()` on song info
  fix: upgrade ytpl from 2.1.1 to 2.2.0
  lint
  refactor notifications plugin
  setup SongInfo **once**
2021-05-28 23:32:05 +02:00
4ab07dc875 Merge pull request #298 from th-ch/snyk-upgrade-d305cdce94133f6db9615e7b16007c87
[Snyk] Upgrade @ffmpeg/core from 0.8.5 to 0.9.0
2021-05-28 23:28:26 +02:00
5033de13ef Merge pull request #293 from th-ch/snyk-upgrade-335ec370c7caa5a759c54a46b2e27cf5
[Snyk] Upgrade filenamify from 4.2.0 to 4.3.0
2021-05-28 23:23:49 +02:00
55a8787a16 Merge pull request #285 from th-ch/snyk-upgrade-7b9c569f5bdd092f76adf7d412b0eea1
[Snyk] Upgrade ytpl from 2.1.1 to 2.2.0
2021-05-28 23:21:51 +02:00
3515bf364d Merge pull request #269 from Araxeus/fix-XHR-duplicate-callback
fix song-info callback duplication
2021-05-28 23:18:10 +02:00
TC
d8f3246e46 Bump electron to 12.0.8 2021-05-28 23:13:07 +02:00
cd613aaba2 Bump browserslist from 4.16.3 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-27 02:56:37 +00:00
c5f84b568b fix: upgrade @cliqz/adblocker-electron from 1.20.4 to 1.20.5
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.20.4 to 1.20.5.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-05-26 04:36:08 +00:00
14dc78984f fix: upgrade ytdl-core from 4.5.0 to 4.7.0
Snyk has created this PR to upgrade ytdl-core from 4.5.0 to 4.7.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-05-26 04:36:06 +00:00
fb61dbfa6c fix: upgrade @ffmpeg/core from 0.8.5 to 0.9.0
Snyk has created this PR to upgrade @ffmpeg/core from 0.8.5 to 0.9.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-05-26 04:36:03 +00:00
33855f17dd update refreshMenu() function 2021-05-23 17:57:54 +03:00
8124623142 fix notificationOnUnpause option 2021-05-22 18:38:36 +03:00
e99c91ce6e get rid of (hopefully) unnecessary watchDOMElement 2021-05-19 21:50:40 +02:00
177ce5721f fix: upgrade filenamify from 4.2.0 to 4.3.0
Snyk has created this PR to upgrade filenamify from 4.2.0 to 4.3.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-05-19 05:31:34 +00:00
4fb0b1dd08 switch to registerCallback() on song info 2021-05-19 00:22:12 +03:00
177ad2ce7c minify firstRun() 2021-05-18 19:20:09 +03:00
9b88769585 remove leftover console.log 2021-05-18 19:02:56 +03:00
fd044072a1 use front load event instead of webcontents.did-finish-load 2021-05-18 18:53:00 +03:00
bae5155e19 use let and const instead of var 2021-05-17 13:42:27 +02:00
1e2085b990 compressor plugin 2021-05-17 12:33:43 +02:00
bbe5a7d50b fix: upgrade ytpl from 2.1.1 to 2.2.0
Snyk has created this PR to upgrade ytpl from 2.1.1 to 2.2.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-05-17 04:44:57 +00:00
e5473cdfe4 lint 2021-05-17 00:07:28 +03:00
9c7a70e056 use .toFixed(2) on volume decimals 2021-05-16 23:50:59 +03:00
5bc8e86353 Merge pull request #270 from Araxeus/fix-normal-notification-showing-appID
fix notification showing appID instead of app name on windows
2021-05-16 22:46:54 +02:00
5b00465558 Merge pull request #273 from th-ch/upgrade-electron
Upgrade electron to v12
2021-05-16 22:31:07 +02:00
28b70f6459 added timeout when writing volume to config 2021-05-16 22:59:05 +03:00
6961cdee95 override youtube automatically changing the volume 2021-05-16 22:39:38 +03:00
58557505ae show mute icon when volume=0 2021-05-16 22:39:11 +03:00
65178b259f fix video muting when volume < 3 2021-05-16 21:32:40 +03:00
541c7f34b7 restore menuItems roles that were fixed 2021-05-15 17:24:01 +03:00
b2c209837c fix empty string input validation in setProxy 2021-05-14 03:12:27 +03:00
355f61188a use placeholder proxy example 2021-05-13 07:08:17 +03:00
ea672c2423 show volume hud in videoplayer if available 2021-05-13 03:18:24 +03:00
71ba6b8e55 lint 2021-05-12 20:58:43 +03:00
8a07fccf8f setup on page reload 2021-05-12 18:58:29 +03:00
7bc35f4cee add volume hud 2021-05-12 18:31:11 +03:00
002081bcb9 use store migration 2021-05-12 00:47:41 +03:00
TC
0e8e78362b Upgrade electron to v12 2021-05-11 22:56:07 +02:00
cb6a5a478e lint 2021-05-11 16:47:51 +03:00
d615030222 register appID on windows reguardless of shortcut 2021-05-11 00:34:40 +03:00
e43c01da64 lint 2021-05-11 00:20:00 +03:00
cb5ef1d6e5 check that app is installed / unpacked 2021-05-11 00:15:54 +03:00
78a7dcb7e8 lint 2021-05-10 23:46:56 +03:00
8cca9f3eeb create shortcut only if needed 2021-05-10 23:23:17 +03:00
93d4d3c976 writeShortcut on windows 2021-05-10 22:13:45 +03:00
8284b56075 set appID on windows 2021-05-10 20:40:25 +03:00
580caeffb9 destructure keybind output 2021-05-10 16:53:57 +03:00
b266037bb4 lint 2021-05-10 06:05:39 +03:00
cb743de7fd refactor notifications plugin 2021-05-10 05:00:58 +03:00
7cf78c6635 setup SongInfo **once** 2021-05-10 04:15:56 +03:00
0eca30367f lint 2021-05-10 01:56:41 +03:00
36317c953a globalize promptOptions 2021-05-10 01:43:50 +03:00
f910593fb6 use spread operator + async await 2021-05-10 00:40:02 +03:00
7942efa202 Merge pull request #267 from Araxeus/last-fm-config-check-fix
fix last-fm overwrite config on each start
2021-05-09 21:47:07 +02:00
792c2931b0 fix config overwrite on each start 2021-05-09 22:10:20 +03:00
TC
a3778af48a Downloader: rename UrlToJPG to urlToJPG 2021-05-08 22:48:18 +02:00
TC
163dc7e1d1 Downloader: catch error when fetching playlist 2021-05-08 22:41:56 +02:00
0a59122ac2 Merge pull request #265 from Araxeus/ensure-download-from-radio-button
Downloader tweaks + taskbar progress bar
2021-05-08 22:40:24 +02:00
d2a5110f3b querySelector optimization #2 2021-05-08 23:06:18 +03:00
cf4bbf94e4 update radioButton querySelector 2021-05-08 23:02:52 +03:00
d7e42471a4 lint 2021-05-08 23:01:57 +03:00
d634c41e75 Merge pull request #262 from Araxeus/remove-open-dependency
remove `open` dependency from last-fm plugin
2021-05-08 21:58:51 +02:00
6b88397f82 lint 2021-05-08 21:21:07 +03:00
da3c709ff0 remove videoDetails?.media query from XHR
(it never exists in the XHR responce)
2021-05-08 20:46:15 +03:00
ccd320d8ff minimize getArtist() 2021-05-08 20:40:44 +03:00
3831e61d10 differentiate names of different metadata sources 2021-05-08 19:45:33 +03:00
e46e7b74e2 fix sendError() 2021-05-08 19:34:57 +03:00
b3da77a6bc small refactor 2021-05-08 19:24:25 +03:00
96a74f8955 use original metadata only if not already captured from ytpl.getInfo() 2021-05-08 19:19:11 +03:00
3ea17e6f46 refactor 2021-05-08 08:05:38 +03:00
a8ac2c3af9 download progress bar on taskbar
+ Get the best possible artwork
2021-05-08 07:11:54 +03:00
2168cbca30 use image from imageSrc if transfered 2021-05-08 04:04:35 +03:00
cceb45319a debug videoUrl from start Radio button in menu 2021-05-08 04:03:34 +03:00
e985b78241 lint 2021-05-08 01:08:24 +03:00
090ca828c0 Merge branch 'master' into update-in-app-menu 2021-05-08 01:03:14 +03:00
3522925dec remove open dependency 2021-05-08 01:00:23 +03:00
5418ef7ae2 Merge branch 'master' into custom-electron-prompt 2021-05-08 00:21:16 +03:00
5faeddb99b Merge pull request #252 from Araxeus/fix-download-idtag-if-not-playing
Fix downloader metadata if not currently playing
2021-05-07 23:11:10 +02:00
1140c3e2e7 lint 2021-05-07 23:40:14 +03:00
TC
3fb08d27c7 Add start:debug command to enable dev with electron debug 2021-05-07 22:29:21 +02:00
250940d083 Merge pull request #259 from Araxeus/force-pause
fix playPause bugs by directly playPause video element
2021-05-07 22:24:03 +02:00
e00be8f010 lint 2021-05-07 05:43:26 +03:00
8b471c0772 create cleanupArtistName() in song-info 2021-05-07 04:47:24 +03:00
88e738c796 check if yns_pause exists 2021-05-07 04:17:17 +03:00
TC
c76d8c79d8 Bump version to 1.12.1 2021-05-06 22:29:04 +02:00
b396431a8b Merge pull request #260 from th-ch/dependabot/npm_and_yarn/ua-parser-js-0.7.28
Bump ua-parser-js from 0.7.23 to 0.7.28
2021-05-06 22:18:37 +02:00
d1795a82f7 Merge pull request #253 from Araxeus/fix-precise-volume-listener-override
Fix precise volume listener override
2021-05-06 21:45:09 +02:00
9b821a0dfe Bump ua-parser-js from 0.7.23 to 0.7.28
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.23 to 0.7.28.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.23...0.7.28)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 19:41:13 +00:00
bf89842ee8 Merge pull request #255 from Araxeus/fix-css-on-reload
fix css not inserting on reload
2021-05-06 21:40:13 +02:00
d274e80f75 minify 2021-05-06 21:41:08 +03:00
a98b8945fb lint 2021-05-06 21:32:44 +03:00
6b72599f80 directly playPause video element 2021-05-06 21:20:42 +03:00
4b6fe78a1a fix css not inserting on reload 2021-05-06 18:40:56 +03:00
d000c03fca lint 2021-05-06 05:40:31 +03:00
c0a185ba68 refactor addEventListener 2021-05-06 05:18:06 +03:00
ef0813e638 fix preventDefault() being called on *all* keys 2021-05-06 04:58:05 +03:00
25e9f44260 fix listener override condition 2021-05-06 04:56:06 +03:00
2d6e858e8f lint 2021-05-06 04:43:31 +03:00
53bf7c5068 playlist download progressBar using chokidar 2021-05-06 04:41:58 +03:00
13fb686188 started playlist downlaod messageBox 2021-05-06 03:33:49 +03:00
61c5494588 custom metadata on playlist-download 2021-05-06 03:02:06 +03:00
d96fefbc24 fix error thrown when downloading playlist 2021-05-06 02:51:10 +03:00
e18b7c1013 allow unlimited playlist size 2021-05-06 02:28:35 +03:00
f190b51dcc lint 2021-05-06 02:17:48 +03:00
ca41c12f7c use media propery if exist in song-info 2021-05-06 02:05:48 +03:00
844edbe2f4 fix metadata when downloading unplayed song 2021-05-06 01:46:14 +03:00
78974c02e5 save in-app-menu activation state on launch 2021-05-05 21:12:52 +03:00
4508464fd1 update custom-electron-titlebar version 2021-05-05 20:39:08 +03:00
dd6455a559 update in-app-menu 2021-05-05 20:37:29 +03:00
6b147b098a fix prompt width 2021-05-05 03:48:07 +03:00
834f8674a3 massive prompt speed boost with v1.1.0 2021-05-05 03:27:47 +03:00
5cee331abe update prompt version and lint 2021-05-05 02:53:05 +03:00
98c00f7a60 format proxy example 2021-05-05 02:30:08 +03:00
db8d946178 fix electron dependency 2021-05-05 02:08:24 +03:00
b97a86f6dc Update yarn.lock 2021-05-05 01:55:25 +03:00
34a4e6be3d proxy url check 2021-05-05 01:47:53 +03:00
22c5ea5000 lint 2021-05-05 01:25:45 +03:00
79acf6c0ba Volume Steps Prompt
Precise-Volume Global Shortcuts Prompt
2021-05-05 00:42:42 +03:00
ebaa01896f Merge remote-tracking branch 'upstream/master' into custom-electron-prompt 2021-05-04 23:48:27 +03:00
ec981ac547 refactor registerAllShortcuts 2021-04-30 05:09:49 +03:00
d0d4ada7c2 restore original menu lint 2021-04-30 04:33:23 +03:00
54cbe3faa4 lint 2021-04-30 04:29:01 +03:00
49e51de274 update shortcuts config 2021-04-30 04:13:03 +03:00
e456035f29 fix typo 2021-04-30 03:22:36 +03:00
964974c142 add keybind changer v1 2021-04-30 03:04:38 +03:00
a229ba9c15 disable reload of plugins on window created 2021-04-28 02:46:13 +03:00
e4eed2e519 add custom-electron-prompt
also use it to set proxy option
2021-04-28 02:41:44 +03:00
73 changed files with 4894 additions and 4357 deletions

View File

@ -18,7 +18,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v1
with:
node-version: "12.x"
node-version: "14.x"
- name: Get yarn cache directory path
id: yarn-cache-dir-path

View File

@ -30,23 +30,25 @@ const defaultConfig = {
// Disabled plugins
shortcuts: {
enabled: false,
overrideMediaKeys: false,
},
downloader: {
enabled: false,
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined, // Custom download folder (absolute path)
preset: "mp3",
},
"last-fm": {
enabled: false,
api_root: "http://ws.audioscrobbler.com/2.0/",
api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123
secret: "a5d2a36fdf64819290f6982481eaffa2",
suffixesToRemove: [' - Topic', 'VEVO'] // removes suffixes of the artist name, for better recognition
},
discord: {
enabled: false,
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
},
notifications: {
enabled: false,
@ -59,12 +61,27 @@ const defaultConfig = {
steps: 1, //percentage of volume to change
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: {
enabled: false, // enable global shortcuts
volumeUp: "Shift+PageUp", // Keybind default can be changed
volumeDown: "Shift+PageDown"
volumeUp: "",
volumeDown: ""
},
savedVolume: undefined //plugin save volume between session here
}
},
sponsorblock: {
enabled: false,
apiURL: "https://sponsor.ajay.app",
categories: [
"sponsor",
"intro",
"outro",
"interaction",
"selfpromo",
"music_offtopic",
],
},
"video-toggle": {
enabled: false,
forceHide: false,
},
},
};

View File

@ -3,6 +3,44 @@ const Store = require("electron-store");
const defaults = require("./defaults");
const migrations = {
">=1.14.0": (store) => {
if (
typeof store.get("plugins.precise-volume.globalShortcuts") !== "object"
) {
store.set("plugins.precise-volume.globalShortcuts", {});
}
if (store.get("plugins.hide-video-player.enabled")) {
store.delete("plugins.hide-video-player");
store.set("plugins.video-toggle.enabled", true);
}
},
">=1.13.0": (store) => {
if (store.get("plugins.discord.listenAlong") === undefined) {
store.set("plugins.discord.listenAlong", true);
}
},
">=1.12.0": (store) => {
const options = store.get("plugins.shortcuts");
let updated = false;
for (const optionType of ["global", "local"]) {
if (Array.isArray(options[optionType])) {
const updatedOptions = {};
for (const optionObject of options[optionType]) {
if (optionObject.action && optionObject.shortcut) {
updatedOptions[optionObject.action] = optionObject.shortcut;
}
}
options[optionType] = updatedOptions;
updated = true;
}
}
if (updated) {
store.set("plugins.shortcuts", options);
}
},
">=1.11.0": (store) => {
if (store.get("options.resumeOnStart") === undefined) {
store.set("options.resumeOnStart", true);

146
index.js
View File

@ -2,6 +2,9 @@
const path = require("path");
const electron = require("electron");
const remote = require('@electron/remote/main');
remote.initialize();
const enhanceWebRequest = require("electron-better-web-request").default;
const is = require("electron-is");
const unhandled = require("electron-unhandled");
const { autoUpdater } = require("electron-updater");
@ -11,6 +14,7 @@ const { setApplicationMenu } = require("./menu");
const { fileExists, injectCSS } = require("./plugins/utils");
const { isTesting } = require("./utils/testing");
const { setUpTray } = require("./tray");
const { setupSongInfo } = require("./providers/song-info");
// Catch errors and log them
unhandled({
@ -22,8 +26,9 @@ const app = electron.app;
app.commandLine.appendSwitch(
"js-flags",
// WebAssembly flags
"--experimental-wasm-threads --experimental-wasm-bulk-memory"
"--experimental-wasm-threads"
);
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
if (config.get("options.disableHardwareAcceleration")) {
if (is.dev()) {
@ -37,7 +42,9 @@ if (config.get("options.proxy")) {
}
// 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
});
// Prevent window being garbage collected
let mainWindow;
@ -58,7 +65,7 @@ function onClosed() {
function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
win.webContents.on("did-finish-load", () => {
win.webContents.once("did-finish-load", () => {
if (is.dev()) {
console.log("did finish load");
win.webContents.openDevTools();
@ -94,7 +101,6 @@ function createMainWindow() {
preload: path.join(__dirname, "preload.js"),
nodeIntegrationInSubFrames: true,
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
enableRemoteModule: true,
affinity: "main-window", // main window, and addition windows should work in one process
...(isTesting()
? {
@ -112,6 +118,7 @@ function createMainWindow() {
: "default",
autoHideMenuBar: config.get("options.hideMenu"),
});
remote.enable(win.webContents);
if (windowPosition) {
const { x, y } = windowPosition;
win.setPosition(x, y);
@ -142,21 +149,49 @@ function createMainWindow() {
});
}
});
win.webContents.on("render-process-gone", (event, webContents, details) => {
showUnresponsiveDialog(win, details);
});
win.once("ready-to-show", () => {
if (config.get("options.appVisible")) {
win.show();
}
});
removeContentSecurityPolicy();
return win;
}
app.once("browser-window-created", (event, win) => {
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
const originalUserAgent = win.webContents.userAgent;
const userAgents = {
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",
linux: "Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0",
}
const updatedUserAgent =
is.macOS() ? userAgents.mac :
is.windows() ? userAgents.windows :
userAgents.linux;
win.webContents.userAgent = updatedUserAgent;
app.userAgentFallback = updatedUserAgent;
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
// 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")){
details.requestHeaders["User-Agent"] = originalUserAgent;
}
cb({ requestHeaders: details.requestHeaders });
});
setupSongInfo(win);
loadPlugins(win);
win.webContents.on("did-fail-load", (
@ -190,28 +225,6 @@ app.once("browser-window-created", (event, win) => {
event.preventDefault();
});
win.webContents.on("did-navigate-in-page", () => {
const url = win.webContents.getURL();
if (url.startsWith("https://music.youtube.com")) {
config.set("url", url);
}
});
win.webContents.on("will-navigate", (_, url) => {
if (url.startsWith("https://accounts.google.com")) {
// Force user-agent "Firefox Windows" for Google OAuth to work
// From https://github.com/firebase/firebase-js-sdk/issues/2478#issuecomment-571356751
// Only set on accounts.google.com, otherwise querySelectors in preload scripts fail (?)
const userAgent =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0";
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
details.requestHeaders["User-Agent"] = userAgent;
cb({ requestHeaders: details.requestHeaders });
});
}
});
win.webContents.on(
"new-window",
(e, url, frameName, disposition, options) => {
@ -255,6 +268,35 @@ app.on("ready", () => {
}, 20000);
}
// Register appID on windows
if (is.windows()) {
const appID = "com.github.th-ch.youtube-music";
app.setAppUserModelId(appID);
const appLocation = process.execPath;
const appData = app.getPath("appData");
// check shortcut validity if not in dev mode / running portable app
if (!is.dev() && !appLocation.startsWith(path.join(appData, "..", "Local", "Temp"))) {
const shortcutPath = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "YouTube Music.lnk");
try { // check if shortcut is registered and valid
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
if (shortcutDetails.target !== appLocation || shortcutDetails.appUserModelId !== appID) {
throw "needUpdate";
}
} catch (error) { // if not valid -> Register shortcut
electron.shell.writeShortcutLink(
shortcutPath,
error === "needUpdate" ? "update" : "create",
{
target: appLocation,
cwd: appLocation.slice(0, appLocation.lastIndexOf(path.sep)),
description: "YouTube Music Desktop App - including custom plugins",
appUserModelId: appID
}
);
}
}
}
mainWindow = createMainWindow();
setApplicationMenu(mainWindow);
if (config.get("options.restartOnConfigChanges")) {
@ -302,6 +344,14 @@ app.on("ready", () => {
});
}
if (config.get("options.hideMenu") && !config.get("options.hideMenuWarned")) {
electron.dialog.showMessageBox(mainWindow, {
type: 'info', title: 'Hide Menu Enabled',
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
});
config.set("options.hideMenuWarned", true);
}
// Optimized for Mac OS X
if (is.macOS() && !config.get("options.appVisible")) {
app.dock.hide();
@ -342,7 +392,45 @@ function showUnresponsiveDialog(win, details) {
app.quit();
break;
default:
break;
break;
}
});
}
function removeContentSecurityPolicy(
session = electron.session.defaultSession
) {
// Allows defining multiple "onHeadersReceived" listeners
// by enhancing the session.
// Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener
enhanceWebRequest(session);
// Custom listener to tweak the content security policy
session.webRequest.onHeadersReceived(function (details, callback) {
if (
!details.responseHeaders["content-security-policy-report-only"] &&
!details.responseHeaders["content-security-policy"]
)
return callback({ cancel: false });
delete details.responseHeaders["content-security-policy-report-only"];
delete details.responseHeaders["content-security-policy"];
callback({ cancel: false, responseHeaders: details.responseHeaders });
});
// When multiple listeners are defined, apply them all
session.webRequest.setResolver("onHeadersReceived", (listeners) => {
const response = listeners.reduce(
async (accumulator, listener) => {
if (accumulator.cancel) {
return accumulator;
}
const result = await listener.apply();
return { ...accumulator, ...result };
},
{ cancel: false }
);
return response;
});
}

View File

@ -2,6 +2,5 @@ module.exports = {
globals: {
__APP__: undefined, // A different app will be launched in each test environment
},
testEnvironment: "./tests/environment",
testTimeout: 30000, // 30s
};

486
menu.js
View File

@ -1,18 +1,22 @@
const { existsSync } = require("fs");
const path = require("path");
const { app, Menu } = require("electron");
const { app, Menu, dialog } = require("electron");
const is = require("electron-is");
const { getAllPlugins } = require("./plugins/utils");
const config = require("./config");
const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
const prompt = require("custom-electron-prompt");
const promptOptions = require("./providers/prompt-options");
// true only if in-app-menu was loaded on launch
const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
const pluginEnabledMenu = (plugin, label = "", hasSubmenu = false, refreshMenu = undefined) => ({
label: label || plugin,
type: "checkbox",
checked: config.plugins.isEnabled(plugin),
//Submenu check used in in-app-menu
hasSubmenu: hasSubmenu || undefined,
click: (item) => {
if (item.checked) {
config.plugins.enable(plugin);
@ -20,252 +24,248 @@ const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
config.plugins.disable(plugin);
}
if (hasSubmenu) {
this.setApplicationMenu(win);
refreshMenu();
}
},
});
const mainMenuTemplate = (win) => [
{
label: "Plugins",
submenu: [
...getAllPlugins().map((plugin) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js")
if (existsSync(pluginPath)) {
if (!config.plugins.isEnabled(plugin)) {
return pluginEnabledMenu(win, plugin, "", true);
const mainMenuTemplate = (win) => {
const refreshMenu = () => {
this.setApplicationMenu(win);
if (inAppMenuActive) {
win.webContents.send("updateMenu", true);
}
}
return [
{
label: "Plugins",
submenu: [
...getAllPlugins().map((plugin) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js")
if (existsSync(pluginPath)) {
if (!config.plugins.isEnabled(plugin)) {
return pluginEnabledMenu(plugin, "", true, refreshMenu);
}
const getPluginMenu = require(pluginPath);
return {
label: plugin,
submenu: [
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu),
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
],
};
}
const getPluginMenu = require(pluginPath);
return {
label: plugin,
submenu: [
pluginEnabledMenu(win, plugin, "Enabled", true),
...getPluginMenu(win, config.plugins.getOptions(plugin), () =>
module.exports.setApplicationMenu(win)
),
],
};
}
return pluginEnabledMenu(win, plugin);
}),
],
},
{
label: "Options",
submenu: [
{
label: "Auto-update",
type: "checkbox",
checked: config.get("options.autoUpdates"),
click: (item) => {
config.set("options.autoUpdates", item.checked);
return pluginEnabledMenu(plugin);
}),
],
},
{
label: "Options",
submenu: [
{
label: "Auto-update",
type: "checkbox",
checked: config.get("options.autoUpdates"),
click: (item) => {
config.set("options.autoUpdates", item.checked);
},
},
},
{
label: "Resume last song when app starts",
type: "checkbox",
checked: config.get("options.resumeOnStart"),
click: (item) => {
config.set("options.resumeOnStart", item.checked);
{
label: "Resume last song when app starts",
type: "checkbox",
checked: config.get("options.resumeOnStart"),
click: (item) => {
config.set("options.resumeOnStart", item.checked);
},
},
},
...(is.windows() || is.linux()
? [
{
label: "Hide menu",
{
label: "Remove upgrade button",
type: "checkbox",
checked: config.get("options.removeUpgradeButton"),
click: (item) => {
config.set("options.removeUpgradeButton", item.checked);
},
},
...(is.windows() || is.linux()
? [
{
label: "Hide menu",
type: "checkbox",
checked: config.get("options.hideMenu"),
click: (item) => {
config.set("options.hideMenu", item.checked);
if (item.checked && !config.get("options.hideMenuWarned")) {
dialog.showMessageBox(win, {
type: 'info', title: 'Hide Menu Enabled',
message: "Menu will be hidden on next launch, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
});
}
},
},
]
: []),
...(is.windows() || is.macOS()
? // Only works on Win/Mac
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
[
{
label: "Start at login",
type: "checkbox",
checked: config.get("options.startAtLogin"),
click: (item) => {
config.set("options.startAtLogin", item.checked);
},
},
]
: []),
{
label: "Tray",
submenu: [
{
label: "Disabled",
type: "radio",
checked: !config.get("options.tray"),
click: () => {
config.set("options.tray", false);
config.set("options.appVisible", true);
},
},
{
label: "Enabled + app visible",
type: "radio",
checked:
config.get("options.tray") && config.get("options.appVisible"),
click: () => {
config.set("options.tray", true);
config.set("options.appVisible", true);
},
},
{
label: "Enabled + app hidden",
type: "radio",
checked:
config.get("options.tray") && !config.get("options.appVisible"),
click: () => {
config.set("options.tray", true);
config.set("options.appVisible", false);
},
},
{ type: "separator" },
{
label: "Play/Pause on click",
type: "checkbox",
checked: config.get("options.trayClickPlayPause"),
click: (item) => {
config.set("options.trayClickPlayPause", item.checked);
},
},
],
},
{ type: "separator" },
{
label: "Advanced options",
submenu: [
{
label: "Proxy",
type: "checkbox",
checked: config.get("options.hideMenu"),
checked: !!config.get("options.proxy"),
click: (item) => {
config.set("options.hideMenu", item.checked);
setProxy(item, win);
},
},
{
label: "Disable hardware acceleration",
type: "checkbox",
checked: config.get("options.disableHardwareAcceleration"),
click: (item) => {
config.set("options.disableHardwareAcceleration", item.checked);
},
},
},
]
: []),
...(is.windows() || is.macOS()
? // Only works on Win/Mac
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
[
{
label: "Start at login",
type: "checkbox",
checked: config.get("options.startAtLogin"),
click: (item) => {
config.set("options.startAtLogin", item.checked);
{
label: "Restart on config changes",
type: "checkbox",
checked: config.get("options.restartOnConfigChanges"),
click: (item) => {
config.set("options.restartOnConfigChanges", item.checked);
},
},
},
]
: []),
{
label: "Tray",
submenu: [
{
label: "Disabled",
type: "radio",
checked: !config.get("options.tray"),
click: () => {
config.set("options.tray", false);
config.set("options.appVisible", true);
{
label: "Reset App cache when app starts",
type: "checkbox",
checked: config.get("options.autoResetAppCache"),
click: (item) => {
config.set("options.autoResetAppCache", item.checked);
},
},
},
{
label: "Enabled + app visible",
type: "radio",
checked:
config.get("options.tray") && config.get("options.appVisible"),
click: () => {
config.set("options.tray", true);
config.set("options.appVisible", true);
{ type: "separator" },
is.macOS() ?
{
label: "Toggle DevTools",
// Cannot use "toggleDevTools" role in MacOS
click: () => {
const { webContents } = win;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
const devToolsOptions = {};
webContents.openDevTools(devToolsOptions);
}
},
} :
{ role: "toggleDevTools" },
{
label: "Edit config.json",
click: () => {
config.edit();
},
},
},
{
label: "Enabled + app hidden",
type: "radio",
checked:
config.get("options.tray") && !config.get("options.appVisible"),
click: () => {
config.set("options.tray", true);
config.set("options.appVisible", false);
},
},
{ type: "separator" },
{
label: "Play/Pause on click",
type: "checkbox",
checked: config.get("options.trayClickPlayPause"),
click: (item) => {
config.set("options.trayClickPlayPause", item.checked);
},
},
],
},
{ type: "separator" },
{
label: "Advanced options",
submenu: [
{
label: "Disable hardware acceleration",
type: "checkbox",
checked: config.get("options.disableHardwareAcceleration"),
click: (item) => {
config.set("options.disableHardwareAcceleration", item.checked);
},
},
{
label: "Restart on config changes",
type: "checkbox",
checked: config.get("options.restartOnConfigChanges"),
click: (item) => {
config.set("options.restartOnConfigChanges", item.checked);
},
},
{
label: "Reset App cache when app starts",
type: "checkbox",
checked: config.get("options.autoResetAppCache"),
click: (item) => {
config.set("options.autoResetAppCache", item.checked);
},
},
{ type: "separator" },
{
label: "Toggle DevTools",
// Cannot use "toggleDevTools" role in MacOS
click: () => {
const { webContents } = win;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
const devToolsOptions = {};
webContents.openDevTools(devToolsOptions);
}
},
},
{
label: "Edit config.json",
click: () => {
config.edit();
},
},
]
},
],
},
{
label: "View",
submenu: [
{
label: "Reload",
click: () => {
win.webContents.reload();
]
},
},
{
label: "Force Reload",
click: () => {
win.webContents.reloadIgnoringCache();
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ type: "separator" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ role: "resetZoom" },
],
},
{
label: "Navigation",
submenu: [
{
label: "Go back",
click: () => {
if (win.webContents.canGoBack()) {
win.webContents.goBack();
}
},
},
},
{ type: "separator" },
{
label: "Zoom In",
click: () => {
win.webContents.setZoomLevel(
win.webContents.getZoomLevel() + 1
);
{
label: "Go forward",
click: () => {
if (win.webContents.canGoForward()) {
win.webContents.goForward();
}
},
},
},
{
label: "Zoom Out",
click: () => {
win.webContents.setZoomLevel(
win.webContents.getZoomLevel() - 1
);
{
label: "Restart App",
click: () => {
app.relaunch();
app.quit();
},
},
},
{
label: "Reset Zoom",
click: () => {
win.webContents.setZoomLevel(0);
},
},
],
},
{
label: "Navigation",
submenu: [
{
label: "Go back",
click: () => {
if (win.webContents.canGoBack()) {
win.webContents.goBack();
}
},
},
{
label: "Go forward",
click: () => {
if (win.webContents.canGoForward()) {
win.webContents.goForward();
}
},
},
{
label: "Restart App",
click: () => {
app.relaunch();
app.quit();
},
},
{
label: "Quit App",
click: () => {
app.quit();
},
},
],
},
];
{ role: "quit" },
],
},
];
}
module.exports.mainMenuTemplate = mainMenuTemplate;
module.exports.setApplicationMenu = (win) => {
@ -300,3 +300,25 @@ module.exports.setApplicationMenu = (win) => {
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
};
async function setProxy(item, win) {
const output = await prompt({
title: 'Set Proxy',
label: 'Enter Proxy Address: (leave empty to disable)',
value: config.get("options.proxy"),
type: 'input',
inputAttrs: {
type: 'url',
placeholder: "Example: 'socks5://127.0.0.1:9999"
},
width: 450,
...promptOptions()
}, win);
if (typeof output === "string") {
config.set("options.proxy", output);
item.checked = output !== "";
} else { //user pressed cancel
item.checked = !item.checked; //reset checkbox
}
}

View File

@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
"version": "1.12.0",
"version": "1.15.0",
"description": "YouTube Music Desktop App - including custom plugins",
"license": "MIT",
"repository": "th-ch/youtube-music",
@ -37,11 +37,21 @@
"deb",
"rpm"
]
},
"snap": {
"slots": [
{
"mpris": {
"interface": "mpris"
}
}
]
}
},
"scripts": {
"test": "jest",
"start": "electron .",
"start": "NODE_OPTIONS= electron .",
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
"generate:package": "node utils/generate-package-json.js",
"postinstall": "yarn run icon && yarn run plugins",
@ -50,52 +60,57 @@
"build:linux": "yarn run clean && electron-builder --linux",
"build:mac": "yarn run clean && electron-builder --mac",
"build:win": "yarn run clean && electron-builder --win",
"plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
"lint": "xo",
"plugins": "yarn run plugin:adblocker",
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
"plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
"release:linux": "yarn run clean && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "yarn run clean && electron-builder --mac -p always",
"release:win": "yarn run clean && electron-builder --win -p always"
},
"engines": {
"node": ">=12.16.1",
"node": ">=14.0.0",
"npm": "Please use yarn and not npm"
},
"dependencies": {
"@cliqz/adblocker-electron": "^1.20.4",
"@ffmpeg/core": "^0.8.5",
"@ffmpeg/ffmpeg": "^0.9.7",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
"async-mutex": "^0.3.1",
"@cliqz/adblocker-electron": "^1.23.1",
"@electron/remote": "^2.0.1",
"@ffmpeg/core": "^0.10.0",
"@ffmpeg/ffmpeg": "^0.10.0",
"async-mutex": "^0.3.2",
"browser-id3-writer": "^4.4.0",
"custom-electron-titlebar": "^3.2.6",
"chokidar": "^3.5.2",
"custom-electron-prompt": "^1.4.0",
"custom-electron-titlebar": "^3.2.9",
"discord-rpc": "^3.2.0",
"electron-better-web-request": "^1.0.1",
"electron-debug": "^3.2.0",
"electron-is": "^3.0.0",
"electron-localshortcut": "^3.2.1",
"electron-store": "^7.0.3",
"electron-unhandled": "^3.0.2",
"electron-updater": "^4.3.8",
"filenamify": "^4.2.0",
"electron-updater": "^4.6.3",
"filenamify": "^4.3.0",
"hark": "^1.2.3",
"md5": "^2.3.0",
"node-fetch": "^2.6.1",
"mpris-service": "^2.1.2",
"node-fetch": "^2.6.6",
"node-notifier": "^9.0.1",
"open": "^8.0.3",
"ytdl-core": "^4.5.0",
"ytpl": "^2.1.1"
"ytdl-core": "^4.9.2",
"ytpl": "^2.2.3"
},
"devDependencies": {
"electron": "^11.4.4",
"electron": "^16.0.5",
"electron-builder": "^22.10.5",
"electron-devtools-installer": "^3.1.1",
"electron-icon-maker": "0.0.5",
"get-port": "^5.1.1",
"jest": "^26.6.3",
"jest": "^27.3.1",
"playwright": "^1.17.1",
"rimraf": "^3.0.2",
"spectron": "^13.0.0",
"xo": "^0.38.2"
"xo": "^0.45.0"
},
"resolutions": {
"glob-parent": "5.1.2",
"minimist": "1.2.5",
"yargs-parser": "18.1.3"
},
"xo": {

View File

@ -8,7 +8,9 @@ const SOURCES = [
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt",
// uBlock Origin
"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-2021.txt",
// Fanboy Annoyances
"https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt",
];
const loadAdBlockerEngine = (
@ -31,7 +33,17 @@ const loadAdBlockerEngine = (
...additionalBlockLists,
];
ElectronBlocker.fromLists(fetch, lists, {}, cachingOptions)
ElectronBlocker.fromLists(
fetch,
lists,
{
// when generating the engine for caching, do not load network filters
// So that enhancing the session works as expected
// Allowing to define multiple webRequest listeners
loadNetworkFilters: session !== undefined,
},
cachingOptions
)
.then((blocker) => {
if (session) {
blocker.enableBlockingInSession(session);

View File

@ -0,0 +1,17 @@
const applyCompressor = () => {
const audioContext = new AudioContext();
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.ratio.value = 12;
compressor.knee.value = 40;
compressor.attack.value = 0;
compressor.release.value = 0.25;
const source = audioContext.createMediaElementSource(document.querySelector("video"));
source.connect(compressor);
compressor.connect(audioContext.destination);
};
module.exports = () => document.addEventListener('apiLoaded', applyCompressor, { once: true, passive: true });

View File

@ -1,12 +0,0 @@
// Define global chrome object to be compliant with the extension code
global.chrome = {
runtime: {
getManifest: () => ({
version: 1
})
}
};
module.exports = () => {
require("YoutubeNonStop/autoconfirm.js");
};

View File

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

View File

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

View File

@ -1,25 +1,14 @@
let videoElement = null;
const observer = new MutationObserver((mutations, observer) => {
if (!videoElement) {
videoElement = document.querySelector("video");
}
if (videoElement) {
videoElement.ontimeupdate = () => {
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
// auto-confirm-when-paused plugin can interfere here if not disabled!
videoElement.pause();
}
};
}
});
function observeVideoElement() {
observer.observe(document, {
childList: true,
subtree: true,
});
}
module.exports = observeVideoElement;
module.exports = () => {
document.addEventListener('apiLoaded', apiEvent => {
apiEvent.detail.addEventListener('videodatachange', name => {
if (name === 'dataloaded') {
apiEvent.detail.pauseVideo();
document.querySelector('video').ontimeupdate = e => {
e.target.pause();
}
} else {
document.querySelector('video').ontimeupdate = null;
}
})
}, { once: true, passive: true })
};

View File

@ -1,60 +1,148 @@
const Discord = require("discord-rpc");
const { dev } = require("electron-is");
const { dialog, app } = require("electron");
const getSongInfo = require("../../providers/song-info");
const rpc = new Discord.Client({
transport: "ipc",
});
const registerCallback = require("../../providers/song-info");
// Application ID registered by @semvis123
const clientId = "790655993809338398";
let clearActivity;
/**
* @typedef {Object} Info
* @property {import('discord-rpc').Client} rpc
* @property {boolean} ready
* @property {import('../../providers/song-info').SongInfo} lastSongInfo
*/
/**
* @type {Info}
*/
const info = {
rpc: null,
ready: false,
lastSongInfo: null,
};
/**
* @type {(() => void)[]}
*/
const refreshCallbacks = [];
const resetInfo = () => {
info.rpc = null;
info.ready = false;
clearTimeout(clearActivity);
if (dev()) console.log("discord disconnected");
refreshCallbacks.forEach(cb => cb());
};
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
const registerCallback = getSongInfo(win);
let window;
const connect = (showErr = false) => {
if (info.rpc) {
if (dev())
console.log('Attempted to connect with active RPC object');
return;
}
info.rpc = new Discord.Client({
transport: "ipc",
});
info.ready = false;
info.rpc.once("connected", () => {
if (dev()) console.log("discord connected");
refreshCallbacks.forEach(cb => cb());
});
info.rpc.once("ready", () => {
info.ready = true;
if (info.lastSongInfo) updateActivity(info.lastSongInfo)
});
info.rpc.once("disconnected", resetInfo);
// Startup the rpc client
info.rpc.login({ clientId }).catch(err => {
resetInfo();
if (dev()) console.error(err);
if (showErr) dialog.showMessageBox(window, { title: 'Connection failed', message: err.message || String(err), type: 'error' });
});
};
let clearActivity;
/**
* @type {import('../../providers/song-info').songInfoCallback}
*/
let updateActivity;
module.exports = (win, { activityTimoutEnabled, activityTimoutTime, listenAlong }) => {
window = win;
// We get multiple events
// Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
// Skip time: PAUSE(N), PLAY(N)
updateActivity = songInfo => {
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
return;
}
info.lastSongInfo = songInfo;
// stop the clear activity timout
clearTimeout(clearActivity);
// stop early if discord connection is not ready
// do this after clearTimeout to avoid unexpected clears
if (!info.rpc || !info.ready) {
return;
}
// clear directly if timeout is 0
if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) {
info.rpc.clearActivity().catch(console.error);
return;
}
// Song information changed, so lets update the rich presence
// @see https://discord.com/developers/docs/topics/gateway#activity-object
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
const activityInfo = {
type: 2, // Listening, addressed in https://github.com/discordjs/RPC/pull/149
details: songInfo.title,
state: songInfo.artist,
largeImageKey: songInfo.imageSrc,
largeImageText: [
songInfo.uploadDate,
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views",
].join(' || '),
buttons: listenAlong ? [
{ label: "Listen Along", url: songInfo.url },
] : undefined,
};
if (songInfo.isPaused) {
// Add an idle icon to show that the song is paused
activityInfo.smallImageKey = "idle";
activityInfo.smallImageText = "idle/paused";
// Set start the timer so the activity gets cleared after a while if enabled
if (activityTimoutEnabled)
clearActivity = setTimeout(() => info.rpc.clearActivity().catch(console.error), activityTimoutTime ?? 10000);
} else {
// Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp =
songStartTime + songInfo.songDuration * 1000;
}
info.rpc.setActivity(activityInfo).catch(console.error);
};
// If the page is ready, register the callback
win.once("ready-to-show", () => {
rpc.once("ready", () => {
// Register the callback
registerCallback((songInfo) => {
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
return;
}
// Song information changed, so lets update the rich presence
const activityInfo = {
details: songInfo.title,
state: songInfo.artist,
largeImageKey: "logo",
largeImageText: [
songInfo.uploadDate,
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
].join(' || '),
};
if (songInfo.isPaused) {
// Add an idle icon to show that the song is paused
activityInfo.smallImageKey = "idle";
activityInfo.smallImageText = "idle/paused";
// Set start the timer so the activity gets cleared after a while if enabled
if (activityTimoutEnabled)
clearActivity = setTimeout(()=>rpc.clearActivity(), activityTimoutTime||10000);
} else {
// stop the clear activity timout
clearTimeout(clearActivity);
// Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp =
songStartTime + songInfo.songDuration * 1000;
}
rpc.setActivity(activityInfo);
});
});
// Startup the rpc client
rpc.login({ clientId }).catch(console.error);
registerCallback(updateActivity);
connect();
});
app.on('window-all-closed', module.exports.clear)
};
module.exports.clear = () => {
if (info.rpc) info.rpc.clearActivity();
clearTimeout(clearActivity);
};
module.exports.connect = connect;
module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb);
module.exports.isConnected = () => info.rpc !== null;

47
plugins/discord/menu.js Normal file
View File

@ -0,0 +1,47 @@
const { setOptions } = require("../../config/plugins");
const { edit } = require("../../config");
const { clear, connect, registerRefresh, isConnected } = require("./back");
let hasRegisterred = false;
module.exports = (win, options, refreshMenu) => {
if (!hasRegisterred) {
registerRefresh(refreshMenu);
hasRegisterred = true;
}
return [
{
label: isConnected() ? "Connected" : "Reconnect",
enabled: !isConnected(),
click: connect,
},
{
label: "Clear activity",
click: clear,
},
{
label: "Clear activity after timeout",
type: "checkbox",
checked: options.activityTimoutEnabled,
click: (item) => {
options.activityTimoutEnabled = item.checked;
setOptions('discord', options);
},
},
{
label: "Listen Along",
type: "checkbox",
checked: options.listenAlong,
click: (item) => {
options.listenAlong = item.checked;
setOptions('discord', options);
},
},
{
label: "Set timeout time in config",
// open config.json
click: edit,
},
];
};

View File

@ -2,6 +2,7 @@ const CHANNEL = "downloader";
const ACTIONS = {
ERROR: "error",
METADATA: "metadata",
PROGRESS: "progress",
};
module.exports = {

View File

@ -4,60 +4,73 @@ const { join } = require("path");
const ID3Writer = require("browser-id3-writer");
const { dialog, ipcMain } = require("electron");
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const { injectCSS, listenAction } = require("../utils");
const { cropMaxWidth } = require("./utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { getImage } = require("../../providers/song-info");
const sendError = (win, err) => {
const dialogOpts = {
const sendError = (win, error) => {
win.setProgressBar(-1); // close progress bar
dialog.showMessageBox({
type: "info",
buttons: ["OK"],
title: "Error in download!",
message: "Argh! Apologies, download failed…",
detail: err.toString(),
};
dialog.showMessageBox(dialogOpts);
detail: error.toString(),
});
};
let metadata = {};
let nowPlayingMetadata = {};
function handle(win) {
injectCSS(win.webContents, join(__dirname, "style.css"));
const registerCallback = getSongInfo(win);
registerCallback((info) => {
metadata = info;
nowPlayingMetadata = info;
});
listenAction(CHANNEL, (event, action, error) => {
listenAction(CHANNEL, (event, action, arg) => {
switch (action) {
case ACTIONS.ERROR:
sendError(win, error);
case ACTIONS.ERROR: // arg = error
sendError(win, arg);
break;
case ACTIONS.METADATA:
event.returnValue = JSON.stringify(metadata);
event.returnValue = JSON.stringify(nowPlayingMetadata);
break;
case ACTIONS.PROGRESS: // arg = progress
win.setProgressBar(arg);
break;
default:
console.log("Unknown action: " + action);
}
});
ipcMain.on("add-metadata", (event, filePath, songBuffer, currentMetadata) => {
ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => {
let fileBuffer = songBuffer;
const songMetadata = { ...metadata, ...currentMetadata };
const songMetadata = currentMetadata.imageSrcYTPL ? // This means metadata come from ytpl.getInfo();
{
...currentMetadata,
image: cropMaxWidth(await getImage(currentMetadata.imageSrcYTPL))
} :
{ ...nowPlayingMetadata, ...currentMetadata };
try {
const coverBuffer = songMetadata.image.toPNG();
const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ?
songMetadata.image.toPNG() : null;
const writer = new ID3Writer(songBuffer);
// Create the metadata tags
writer
.setFrame("TIT2", songMetadata.title)
.setFrame("TPE1", [songMetadata.artist])
.setFrame("APIC", {
.setFrame("TPE1", [songMetadata.artist]);
if (coverBuffer) {
writer.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: "",
description: ""
});
}
writer.addTag();
fileBuffer = Buffer.from(writer.arrayBuffer);
} catch (error) {

View File

@ -25,6 +25,7 @@ const observer = new MutationObserver((mutations, observer) => {
});
const reinit = () => {
triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar
if (!progress) {
console.warn("Cannot update progress");
} else {
@ -38,22 +39,31 @@ const baseUrl = defaultConfig.url;
// contextBridge.exposeInMainWorld("downloader", {
// download: () => {
global.download = () => {
triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar
let metadata;
let videoUrl = getSongMenu()
.querySelector("ytmusic-menu-navigation-item-renderer")
.querySelector("#navigation-endpoint")
.getAttribute("href");
videoUrl = !videoUrl
? global.songInfo.url || window.location.href
: baseUrl + "/" + videoUrl;
// selector of first button which is always "Start Radio"
?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint')
?.getAttribute("href");
if (videoUrl) {
videoUrl = baseUrl + "/" + videoUrl;
metadata = null;
} else {
metadata = global.songInfo;
videoUrl = metadata.url || window.location.href;
}
downloadVideoToMP3(
videoUrl,
(feedback) => {
(feedback, ratio = undefined) => {
if (!progress) {
console.warn("Cannot update progress");
} else {
progress.innerHTML = feedback;
}
if (ratio) {
triggerAction(CHANNEL, ACTIONS.PROGRESS, ratio);
}
},
(error) => {
triggerAction(CHANNEL, ACTIONS.ERROR, error);
@ -61,7 +71,7 @@ global.download = () => {
},
reinit,
pluginOptions,
global.songInfo
metadata
);
};
// });

View File

@ -1,25 +1,23 @@
const { existsSync, mkdirSync } = require("fs");
const { join } = require("path");
const { URL } = require("url");
const { dialog, ipcMain } = require("electron");
const is = require("electron-is");
const ytpl = require("ytpl");
const chokidar = require('chokidar');
const { setOptions } = require("../../config/plugins");
const getSongInfo = require("../../providers/song-info");
const { sendError } = require("./back");
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils");
let downloadLabel = defaultMenuDownloadLabel;
let metadataURL = undefined;
let playingPlaylistId = undefined;
let callbackIsRegistered = false;
module.exports = (win, options, refreshMenu) => {
module.exports = (win, options) => {
if (!callbackIsRegistered) {
const registerCallback = getSongInfo(win);
registerCallback((info) => {
metadataURL = info.url;
ipcMain.on("video-src-changed", async (_, data) => {
playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId;
});
callbackIsRegistered = true;
}
@ -28,14 +26,23 @@ module.exports = (win, options, refreshMenu) => {
{
label: downloadLabel,
click: async () => {
const currentURL = metadataURL || win.webContents.getURL();
const playlistID = new URL(currentURL).searchParams.get("list");
if (!playlistID) {
const currentPagePlaylistId = new URL(win.webContents.getURL()).searchParams.get("list");
const playlistId = currentPagePlaylistId || playingPlaylistId;
if (!playlistId) {
sendError(win, new Error("No playlist ID found"));
return;
}
const playlist = await ytpl(playlistID);
console.log(`trying to get playlist ID: '${playlistId}'`);
let playlist;
try {
playlist = await ytpl(playlistId, {
limit: options.playlistMaxItems || Infinity,
});
} catch (e) {
sendError(win, e);
return;
}
const playlistTitle = playlist.title;
const folder = getFolder(options.downloadFolder);
@ -49,24 +56,40 @@ module.exports = (win, options, refreshMenu) => {
}
mkdirSync(playlistFolder, { recursive: true });
ipcMain.on("downloader-feedback", (_, feedback) => {
downloadLabel = feedback;
refreshMenu();
dialog.showMessageBox({
type: "info",
buttons: ["OK"],
title: "Started Download",
message: `Downloading Playlist "${playlistTitle}"`,
detail: `(${playlist.items.length} songs)`,
});
downloadLabel = `Downloading "${playlistTitle}"`;
refreshMenu();
if (is.dev()) {
console.log(
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
);
}
playlist.items.slice(0, options.playlistMaxItems).forEach((song) => {
const steps = 1 / playlist.items.length;
let progress = 0;
win.setProgressBar(2); // starts with indefinite bar
let dirWatcher = chokidar.watch(playlistFolder);
dirWatcher.on('add', () => {
progress += steps;
if (progress >= 0.9999) {
win.setProgressBar(-1); // close progress bar
dirWatcher.close().then(() => dirWatcher = null);
} else {
win.setProgressBar(progress);
}
});
playlist.items.forEach((song) => {
win.webContents.send(
"downloader-download-playlist",
song,
song.url,
playlistTitle,
options
);
@ -86,5 +109,17 @@ module.exports = (win, options, refreshMenu) => {
} // else = user pressed cancel
},
},
{
label: "Presets",
submenu: Object.keys(presets).map((preset) => ({
label: preset,
type: "radio",
click: () => {
options.preset = preset;
setOptions("downloader", options);
},
checked: options.preset === preset || presets[preset] === undefined,
})),
},
];
};

View File

@ -6,8 +6,16 @@
cursor: pointer;
}
.menu-item > .yt-simple-endpoint:hover {
background-color: var(--ytmusic-menu-item-hover-background-color);
}
.menu-icon {
flex: var(--ytmusic-menu-item-icon_-_flex);
margin: var(--ytmusic-menu-item-icon_-_margin);
fill: var(--ytmusic-menu-item-icon_-_fill);
stroke: var(--iron-icon-stroke-color, none);
width: var(--iron-icon-width, 24px);
height: var(--iron-icon-height, 24px);
animation: var(--iron-icon_-_animation);
}

View File

@ -1,5 +1,5 @@
<div
class="menu-item ytmusic-menu-popup-renderer"
class="style-scope menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
aria-disabled="false"
@ -7,31 +7,39 @@
onclick="download()"
>
<div
class="menu-icon yt-icon-container yt-icon ytmusic-toggle-menu-service-item-renderer"
id="navigation-endpoint"
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
tabindex="-1"
>
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%;"
<div
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
>
<g class="style-scope yt-icon">
<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" fill="#aaaaaa"
/>
<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" fill="#aaaaaa"
/>
</g>
</svg>
</div>
<div
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
id="ytmcustom-download"
>
Download
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<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"
fill="#aaaaaa"
/>
<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"
fill="#aaaaaa"
/>
</g>
</svg>
</div>
<div
class="text style-scope ytmusic-menu-navigation-item-renderer"
id="ytmcustom-download"
>
Download
</div>
</div>
</div>

View File

@ -1,5 +1,39 @@
const electron = require("electron");
module.exports.getFolder = (customFolder) =>
customFolder || (electron.app || electron.remote.app).getPath("downloads");
module.exports.getFolder = customFolder => customFolder || electron.app.getPath("downloads");
module.exports.defaultMenuDownloadLabel = "Download playlist";
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
module.exports.urlToJPG = (imgUrl, videoId) => {
if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl;
//it will almost never get further than hqdefault
for (const quality of orderedQualityList) {
if (imgUrl.includes(quality)) {
return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
}
}
return `https://img.youtube.com/vi/${videoId}/default.jpg`;
}
module.exports.cropMaxWidth = (image) => {
const imageSize = image.getSize();
// standart youtube artwork width with margins from both sides is 280 + 720 + 280
if (imageSize.width === 1280 && imageSize.height === 720) {
return image.crop({
x: 280,
y: 0,
width: 720,
height: 720
});
}
return image;
}
// Presets for FFmpeg
module.exports.presets = {
"None (defaults to mp3)": undefined,
opus: {
extension: "opus",
ffmpegArgs: ["-acodec", "libopus"],
},
};

View File

@ -3,6 +3,7 @@ const { join } = require("path");
const Mutex = require("async-mutex").Mutex;
const { ipcRenderer } = require("electron");
const remote = require('@electron/remote');
const is = require("electron-is");
const filenamify = require("filenamify");
@ -14,7 +15,8 @@ const ytdl = require("ytdl-core");
const { triggerAction, triggerActionSync } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
const { presets, urlToJPG } = require("./utils");
const { cleanupName } = require("../../providers/song-info");
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
@ -24,7 +26,7 @@ const ffmpeg = createFFmpeg({
});
const ffmpegMutex = new Mutex();
const downloadVideoToMP3 = (
const downloadVideoToMP3 = async (
videoUrl,
sendFeedback,
sendError,
@ -35,6 +37,21 @@ const downloadVideoToMP3 = (
) => {
sendFeedback("Downloading…");
if (metadata === null) {
const { videoDetails } = await ytdl.getInfo(videoUrl);
const thumbnails = videoDetails?.thumbnails;
metadata = {
artist:
videoDetails?.media?.artist ||
cleanupName(videoDetails?.author?.name) ||
"",
title: videoDetails?.media?.song || videoDetails?.title || "",
imageSrcYTPL: thumbnails ?
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
: ""
}
}
let videoName = "YouTube Music - Unknown title";
let videoReadableStream;
try {
@ -54,9 +71,10 @@ const downloadVideoToMP3 = (
.on("data", (chunk) => {
chunks.push(chunk);
})
.on("progress", (chunkLength, downloaded, total) => {
const progress = Math.floor((downloaded / total) * 100);
sendFeedback("Download: " + progress + "%");
.on("progress", (_chunkLength, downloaded, total) => {
const ratio = downloaded / total;
const progress = Math.floor(ratio * 100);
sendFeedback("Download: " + progress + "%", ratio);
})
.on("info", (info, format) => {
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
@ -95,13 +113,14 @@ const toMP3 = async (
existingMetadata = undefined,
subfolder = ""
) => {
const convertOptions = { ...presets[options.preset], ...options };
const safeVideoName = randomBytes(32).toString("hex");
const extension = options.extension || "mp3";
const extension = convertOptions.extension || "mp3";
const releaseFFmpegMutex = await ffmpegMutex.acquire();
try {
if (!ffmpeg.isLoaded()) {
sendFeedback("Loading…");
sendFeedback("Loading…", 2); // indefinite progress bar after download
await ffmpeg.load();
}
@ -114,11 +133,11 @@ const toMP3 = async (
"-i",
safeVideoName,
...getFFmpegMetadataArgs(metadata),
...(options.ffmpegArgs || []),
...(convertOptions.ffmpegArgs || []),
safeVideoName + "." + extension
);
const folder = getFolder(options.downloadFolder);
const folder = options.downloadFolder || remote.app.getPath("downloads");
const name = metadata.title
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
: videoName;
@ -135,6 +154,7 @@ const toMP3 = async (
ipcRenderer.send("add-metadata", filePath, fileBuffer, {
artist: metadata.artist,
title: metadata.title,
imageSrcYTPL: metadata.imageSrcYTPL
});
ipcRenderer.once("add-metadata-done", reinit);
} catch (e) {
@ -165,22 +185,16 @@ module.exports = {
ipcRenderer.on(
"downloader-download-playlist",
(_, songMetadata, playlistFolder, options) => {
const reinit = () =>
ipcRenderer.send("downloader-feedback", defaultMenuDownloadLabel);
(_, url, playlistFolder, options) => {
downloadVideoToMP3(
songMetadata.url,
(feedback) => {
ipcRenderer.send("downloader-feedback", feedback);
},
url,
() => {},
(error) => {
triggerAction(CHANNEL, ACTIONS.ERROR, error);
reinit();
},
reinit,
() => {},
options,
songMetadata,
null,
playlistFolder
);
}

View File

@ -0,0 +1,47 @@
// "Youtube Music fix volume ratio 0.4" by Marco Pfeiffer
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
const exponentialVolume = () => {
// 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
// 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 storedOriginalVolumes = new WeakMap();
const { get, set } = Object.getOwnPropertyDescriptor(
HTMLMediaElement.prototype,
"volume"
);
Object.defineProperty(HTMLMediaElement.prototype, "volume", {
get() {
const lowVolume = get.call(this);
const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT);
// The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values.
// To avoid this, I'll store the unmodified volume to return it when read here.
// This mostly solves the issue, but the initial read has no stored value and the volume can also change though external influences.
// To avoid ill effects, I check if the stored volume is somewhere in the same range as the calculated volume.
const storedOriginalVolume = storedOriginalVolumes.get(this);
const storedDeviation = Math.abs(
storedOriginalVolume - calculatedOriginalVolume
);
const originalVolume =
storedDeviation < 0.01
? storedOriginalVolume
: calculatedOriginalVolume;
return originalVolume;
},
set(originalVolume) {
const lowVolume = originalVolume ** EXPONENT;
storedOriginalVolumes.set(this, originalVolume);
set.call(this, lowVolume);
},
});
};
module.exports = () =>
document.addEventListener("apiLoaded", exponentialVolume, {
once: true,
passive: true,
});

View File

@ -1,83 +1,33 @@
const path = require("path");
const { Menu } = require("electron");
const electronLocalshortcut = require("electron-localshortcut");
const config = require("../../config");
const { setApplicationMenu } = require("../../menu");
const { injectCSS } = require("../utils");
//tracks menu visibility
let visible = true;
// win hook for fixing menu
let win;
const originalBuildMenu = Menu.buildFromTemplate;
// This function natively gets called on all submenu so no more reason to use recursion
Menu.buildFromTemplate = (template) => {
// Fix checkboxes and radio buttons
updateTemplate(template);
// return as normal
return originalBuildMenu(template);
};
module.exports = (winImport) => {
win = winImport;
module.exports = (win) => {
// css for custom scrollbar + disable drag area(was causing bugs)
injectCSS(win.webContents, path.join(__dirname, "style.css"));
win.once("ready-to-show", () => {
setApplicationMenu(win);
//register keyboard shortcut && hide menu if hideMenu is enabled
if (config.get("options.hideMenu")) {
electronLocalshortcut.register(win, "Esc", () => {
switchMenuVisibility();
setMenuVisibility(!visible);
});
}
});
//set menu visibility on load
win.webContents.once("did-finish-load", () => {
// fix bug with menu not applying on start when no internet connection available
setMenuVisibility(!config.get("options.hideMenu"));
});
function setMenuVisibility(value) {
visible = value;
win.webContents.send("updateMenu", visible);
}
};
function switchMenuVisibility() {
setMenuVisibility(!visible);
}
function setMenuVisibility(value) {
visible = value;
win.webContents.send("updateMenu", visible);
}
function updateCheckboxesAndRadioButtons(item, isRadio, hasSubmenu) {
if (!isRadio) {
//fix checkbox
item.checked = !item.checked;
}
//update menu if radio / hasSubmenu
if (isRadio || hasSubmenu) {
win.webContents.send("updateMenu", true);
}
}
// Update checkboxes/radio buttons
function updateTemplate(template) {
for (let item of template) {
// Change onClick of checkbox+radio
if ((item.type === "checkbox" || item.type === "radio") && !item.fixed) {
const originalOnclick = item.click;
item.click = (itemClicked) => {
originalOnclick(itemClicked);
updateCheckboxesAndRadioButtons(itemClicked, item.type === "radio", item.hasSubmenu);
};
item.fixed = true;
}
}
}

View File

@ -1,6 +1,9 @@
const { remote, ipcRenderer } = require("electron");
const { ipcRenderer } = require("electron");
const { Menu } = require("@electron/remote");
const customTitlebar = require("custom-electron-titlebar");
function $(selector) { return document.querySelector(selector); }
module.exports = () => {
const bar = new customTitlebar.Titlebar({
@ -10,15 +13,21 @@ module.exports = () => {
bar.updateTitle(" ");
document.title = "Youtube Music";
ipcRenderer.on("updateMenu", function (event, menu) {
if (menu) {
bar.updateMenu(remote.Menu.getApplicationMenu());
} else {
try {
bar.updateMenu(null);
} catch (e) {
//will always throw type error - null isn't menu, but it works
}
}
ipcRenderer.on("updateMenu", function (_event, showMenu) {
bar.updateMenu(showMenu ? Menu.getApplicationMenu() : null);
});
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
document.addEventListener('apiLoaded', () => {
setNavbarMargin();
const playPageObserver = new MutationObserver(setNavbarMargin);
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] })
}, { once: true, passive: true })
};
function setNavbarMargin() {
$('#nav-bar-background').style.right =
$('ytmusic-app-layout').playerPageOpen_ ?
'0px' :
'12px';
}

View File

@ -4,14 +4,13 @@
font-size: 14px !important;
}
/* allow submenu's to show correctly */
.menubar-menu-container {
overflow-y: visible !important;
}
/* fixes scrollbar positioning relative to nav bar */
#nav-bar-background.ytmusic-app-layout {
right: 15px !important;
/* fixes nav-bar-background opacity bug, reposition it, and allows clicking scrollbar through it */
#nav-bar-background {
opacity: 1 !important;
pointer-events: none !important;
position: sticky !important;
top: 0 !important;
height: 75px !important;
}
/* remove window dragging for nav bar (conflict with titlebar drag) */
@ -21,14 +20,10 @@ ytmusic-pivot-bar-item-renderer {
-webkit-app-region: unset !important;
}
/* navbar background black */
.center-content.ytmusic-nav-bar {
background: #030303;
}
/* move up item selectrion renderer by 15 px */
ytmusic-item-section-renderer[has-item-section-tabbed-header-renderer_] #header.ytmusic-item-section-renderer {
top: 75 !important;
/* move up item selection renderers */
ytmusic-item-section-renderer.stuck #header.ytmusic-item-section-renderer,
ytmusic-tabs.stuck {
top: calc(var(--ytmusic-nav-bar-height) - 15px) !important;
}
/* fix weird positioning in search screen*/
@ -37,8 +32,7 @@ ytmusic-header-renderer.ytmusic-search-page {
}
/* Move navBar downwards */
ytmusic-app-layout > [slot="nav-bar"],
#nav-bar-background.ytmusic-app-layout {
ytmusic-nav-bar[slot="nav-bar"] {
top: 17px !important;
}

View File

@ -1,25 +1,14 @@
const fetch = require('node-fetch');
const md5 = require('md5');
const open = require("open");
const { shell } = require('electron');
const { setOptions } = require('../../config/plugins');
const getSongInfo = require('../../providers/song-info');
const registerCallback = require('../../providers/song-info');
const defaultConfig = require('../../config/defaults');
const cleanupArtistName = (config, artist) => {
// removes the suffixes of the artist name for more recognition by last.fm
const { suffixesToRemove } = config;
if (suffixesToRemove === undefined) return artist;
for (suffix of suffixesToRemove) {
artist = artist.replace(suffix, '');
}
return artist;
}
const createFormData = params => {
// creates the body for in the post request
const formData = new URLSearchParams();
for (key in params) {
for (const key in params) {
formData.append(key, params[key]);
}
return formData;
@ -28,7 +17,7 @@ const createQueryString = (params, api_sig) => {
// creates a querystring
const queryData = [];
params.api_sig = api_sig;
for (key in params) {
for (const key in params) {
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
}
return '?'+queryData.join('&');
@ -37,12 +26,12 @@ const createQueryString = (params, api_sig) => {
const createApiSig = (params, secret) => {
// this function creates the api signature, see: https://www.last.fm/api/authspec
const keys = [];
for (key in params) {
for (const key in params) {
keys.push(key);
}
keys.sort();
let sig = '';
for (key of keys) {
for (const key of keys) {
if (String(key) === 'format')
continue
sig += `${key}${params[key]}`;
@ -69,7 +58,7 @@ const authenticate = async config => {
// asks the user for authentication
config.token = await createToken(config);
setOptions('last-fm', config);
open(`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;
}
@ -139,10 +128,8 @@ const setNowPlaying = (songInfo, config) => {
// this will store the timeout that will trigger addScrobble
let scrobbleTimer = undefined;
const lastfm = async (win, config) => {
const registerCallback = getSongInfo(win);
if (!config.api_root || !config.suffixesToRemove) {
const lastfm = async (_win, config) => {
if (!config.api_root) {
// settings are not present, creating them with the default values
config = defaultConfig.plugins['last-fm'];
config.enabled = true;
@ -157,8 +144,6 @@ const lastfm = async (win, config) => {
registerCallback( songInfo => {
// set remove the old scrobble timer
clearTimeout(scrobbleTimer);
// make the artist name a bit cleaner
songInfo.artist = cleanupArtistName(config, songInfo.artist);
if (!songInfo.isPaused) {
setNowPlaying(songInfo, config);
// scrobble when the song is half way through, or has passed the 4 minute mark

View File

@ -0,0 +1,52 @@
const { join } = require("path");
const { ipcMain } = require("electron");
const is = require("electron-is");
const fetch = require("node-fetch");
const { cleanupName } = require("../../providers/song-info");
const { injectCSS } = require("../utils");
module.exports = async (win) => {
injectCSS(win.webContents, join(__dirname, "style.css"));
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
const metadata = JSON.parse(extractedSongInfo);
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
metadata.title
)}`;
let response = await fetch(
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(
queryString
)}`
);
if (!response.ok) {
event.returnValue = null;
return;
}
const info = await response.json();
let url = "";
try {
url = info.response.sections.filter(
(section) => section.type === "song"
)[0].hits[0].result.url;
} catch {
event.returnValue = null;
return;
}
if (is.dev()) {
console.log("Fetching lyrics from Genius:", url);
}
response = await fetch(url);
if (!response.ok) {
event.returnValue = null;
return;
}
event.returnValue = await response.text();
});
};

View File

@ -0,0 +1,67 @@
const { ipcRenderer } = require("electron");
const is = require("electron-is");
module.exports = () => {
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
const lyricsTab = document.querySelector('tp-yt-paper-tab[tabindex="-1"]');
// Check if disabled
if (!lyricsTab || !lyricsTab.hasAttribute("disabled")) {
return;
}
const html = ipcRenderer.sendSync(
"search-genius-lyrics",
extractedSongInfo
);
if (!html) {
return;
} else if (is.dev()) {
console.log("Fetched lyrics from Genius");
}
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const lyricsSelector1 = wrapper.querySelector(".lyrics");
const lyricsSelector2 = wrapper.querySelector(
'[class^="Lyrics__Container"]'
);
const lyrics = lyricsSelector1
? lyricsSelector1.innerHTML
: lyricsSelector2
? lyricsSelector2.innerHTML
: null;
if (!lyrics) {
return;
}
lyricsTab.removeAttribute("disabled");
lyricsTab.removeAttribute("aria-disabled");
document.querySelector("tp-yt-paper-tab").onclick = () => {
lyricsTab.removeAttribute("disabled");
lyricsTab.removeAttribute("aria-disabled");
};
lyricsTab.onclick = () => {
const tabContainer = document.querySelector("ytmusic-tab-renderer");
const observer = new MutationObserver((_, observer) => {
const lyricsContainer = document.querySelector(
'[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer'
);
if (lyricsContainer) {
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer genius-lyrics">
${lyrics}
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer">Source&nbsp;: Genius</yt-formatted-string>
</div>`;
observer.disconnect();
}
});
observer.observe(tabContainer, {
attributes: true,
childList: true,
subtree: true,
});
};
});
};

View File

@ -0,0 +1,12 @@
/* Disable links in Genius lyrics */
.genius-lyrics a {
color: var(--ytmusic-text-primary);
display: inline-block;
pointer-events: none;
text-decoration: none;
}
#contents.genius-lyrics {
font-size: 1vw;
opacity: 0.9;
}

View File

@ -1,9 +1,9 @@
const { Notification } = require("electron");
const is = require("electron-is");
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const { notificationImage } = require("./utils");
const { setupInteractive, notifyInteractive } = require("./interactive")
const setupInteractive = require("./interactive")
const notify = (info, options) => {
@ -23,38 +23,24 @@ const notify = (info, options) => {
return currentNotification;
};
module.exports = (win, options) => {
const isInteractive = is.windows() && options.interactive;
//setup interactive notifications for windows
if (isInteractive) {
setupInteractive(win, options.unpauseNotification);
}
const registerCallback = getSongInfo(win);
const setup = (options) => {
let oldNotification;
let oldURL = "";
win.once("ready-to-show", () => {
// Register the callback for new song information
registerCallback(songInfo => {
// on pause - reset url? and skip notification
if (songInfo.isPaused) {
//reset oldURL if unpause notification option is on
if (options.unpauseNotification) {
oldURL = "";
}
return;
}
// If url isn't the same as last one - send notification
if (songInfo.url !== oldURL) {
oldURL = songInfo.url;
if (isInteractive) {
notifyInteractive(songInfo);
} else {
// Close the old notification
oldNotification?.close();
// This fixes a weird bug that would cause the notification to be updated instead of showing
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
}
}
});
let currentUrl;
registerCallback(songInfo => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
// Close the old notification
oldNotification?.close();
currentUrl = songInfo.url;
// This fixes a weird bug that would cause the notification to be updated instead of showing
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
}
});
}
module.exports = (win, options) => {
// Register the callback for new song information
is.windows() && options.interactive ?
setupInteractive(win, options.unpauseNotification) :
setup(options);
};

View File

@ -1,17 +1,27 @@
const { notificationImage, icons } = require("./utils");
const getSongControls = require('../../providers/song-controls');
const registerCallback = require("../../providers/song-info");
const notifier = require("node-notifier");
//store song controls reference on launch
let controls;
let notificationOnPause;
let notificationOnUnpause;
//Save controls and onPause option
module.exports.setupInteractive = (win, unpauseNotification) => {
module.exports = (win, unpauseNotification) => {
//Save controls and onPause option
const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous };
notificationOnUnpause = unpauseNotification;
notificationOnPause = unpauseNotification;
let currentUrl;
// Register songInfoCallback
registerCallback(songInfo => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
currentUrl = songInfo.url;
sendToaster(songInfo);
}
});
win.webContents.once("closed", () => {
deleteNotification()
@ -33,7 +43,7 @@ function deleteNotification() {
}
//New notification
module.exports.notifyInteractive = function sendToaster(songInfo) {
function sendToaster(songInfo) {
deleteNotification();
//download image and get path
let imgSrc = notificationImage(songInfo, true);
@ -71,7 +81,7 @@ module.exports.notifyInteractive = function sendToaster(songInfo) {
// dont delete notification on play/pause
toDelete = undefined;
//manually send notification if not sending automatically
if (!notificationOnPause) {
if (!notificationOnUnpause) {
songInfo.isPaused = false;
sendToaster(songInfo);
}

View File

@ -1,83 +1,93 @@
const {
getSongMenu,
watchDOMElement,
} = require("../../providers/dom-elements");
const { getSongMenu } = require("../../providers/dom-elements");
const { ElementFromFile, templatePath } = require("../utils");
function $(selector) { return document.querySelector(selector); }
const slider = ElementFromFile(templatePath(__dirname, "slider.html"));
const MIN_PLAYBACK_SPEED = 0.25;
const MAX_PLAYBACK_SPEED = 2;
const roundToTwo = n => Math.round(n * 1e2) / 1e2;
let videoElement;
let playbackSpeedPercentage = 50; // = Playback speed of 1
const MIN_PLAYBACK_SPEED = 0.07;
const MAX_PLAYBACK_SPEED = 16;
const computePlayBackSpeed = () => {
if (playbackSpeedPercentage <= 50) {
// Slow down video by setting a playback speed between MIN_PLAYBACK_SPEED and 1
return (
MIN_PLAYBACK_SPEED +
((1 - MIN_PLAYBACK_SPEED) / 50) * playbackSpeedPercentage
);
}
// Accelerate video by setting a playback speed between 1 and MAX_PLAYBACK_SPEED
return 1 + ((MAX_PLAYBACK_SPEED - 1) / 50) * (playbackSpeedPercentage - 50);
};
let playbackSpeed = 1;
const updatePlayBackSpeed = () => {
const playbackSpeed = Math.round(computePlayBackSpeed() * 100) / 100;
$('video').playbackRate = playbackSpeed;
if (!videoElement || videoElement.playbackRate === playbackSpeed) {
return;
}
videoElement.playbackRate = playbackSpeed;
const playbackSpeedElement = document.querySelector("#playback-speed-value");
const playbackSpeedElement = $("#playback-speed-value");
if (playbackSpeedElement) {
playbackSpeedElement.innerHTML = playbackSpeed;
}
};
module.exports = () => {
watchDOMElement(
"video",
(document) => document.querySelector("video"),
(element) => {
videoElement = element;
updatePlayBackSpeed();
}
);
let menu;
let observingSlider = false;
watchDOMElement(
"menu",
(document) => getSongMenu(document),
(menuElement) => {
if (!menuElement.contains(slider)) {
menuElement.prepend(slider);
const observePopupContainer = () => {
const observer = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
}
if (menu && !menu.contains(slider)) {
menu.prepend(slider);
if (!observingSlider) {
setupSliderListener();
observingSlider = true;
}
const playbackSpeedElement = document.querySelector(
"#playback-speed-slider #sliderKnob .slider-knob-inner"
);
const playbackSpeedObserver = new MutationObserver((mutations) => {
mutations.forEach(function (mutation) {
if (mutation.type == "attributes") {
const value = playbackSpeedElement.getAttribute("value");
playbackSpeedPercentage = parseInt(value, 10);
if (isNaN(playbackSpeedPercentage)) {
playbackSpeedPercentage = 50;
}
updatePlayBackSpeed();
return;
}
});
});
playbackSpeedObserver.observe(playbackSpeedElement, {
attributes: true,
});
}
);
});
observer.observe($('ytmusic-popup-container'), {
childList: true,
subtree: true,
});
};
const observeVideo = () => {
$('video').addEventListener('ratechange', forcePlaybackRate)
$('video').addEventListener('srcChanged', forcePlaybackRate)
}
const setupWheelListener = () => {
slider.addEventListener('wheel', e => {
e.preventDefault();
if (isNaN(playbackSpeed)) {
playbackSpeed = 1;
}
// e.deltaY < 0 means wheel-up
playbackSpeed = roundToTwo(e.deltaY < 0 ?
Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED) :
Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED)
);
updatePlayBackSpeed();
// update slider position
$('#playback-speed-slider').value = playbackSpeed;
})
}
function setupSliderListener() {
$('#playback-speed-slider').addEventListener('immediate-value-changed', e => {
playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED;
if (isNaN(playbackSpeed)) {
playbackSpeed = 1;
}
updatePlayBackSpeed();
})
}
function forcePlaybackRate(e) {
if (e.target.playbackRate !== playbackSpeed) {
e.target.playbackRate = playbackSpeed
}
}
module.exports = () => {
document.addEventListener('apiLoaded', e => {
observePopupContainer();
observeVideo();
setupWheelListener();
}, { once: true, passive: true })
};

View File

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

View File

@ -1,23 +1,9 @@
const { isEnabled } = require("../../config/plugins");
/*
This is used to determine if plugin is actually active
(not if its only enabled in options)
*/
let enabled = false;
module.exports = (win) => {
enabled = true;
module.exports = () => enabled = true;
// youtube-music register some of the target listeners after DOMContentLoaded
// did-finish-load is called after all elements finished loading, including said listeners
// Thats the reason the timing is controlled from main
win.webContents.once("did-finish-load", () => {
win.webContents.send("restoreAddEventListener");
win.webContents.send("setupVideoPlayerVolumeMousewheel", !isEnabled("hide-video-player"));
});
};
module.exports.enabled = () => {
return enabled;
};
module.exports.enabled = () => enabled;

View File

@ -1,29 +1,84 @@
const { ipcRenderer, remote } = require("electron");
const { ipcRenderer } = require("electron");
const { globalShortcut } = require('@electron/remote');
const { setOptions } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); }
let api;
module.exports = (options) => {
document.addEventListener('apiLoaded', e => {
api = e.detail;
firstRun(options);
}, { once: true, passive: true })
};
/** Restore saved volume and setup tooltip */
function firstRun(options) {
if (typeof options.savedVolume === "number") {
// Set saved volume as tooltip
setTooltip(options.savedVolume);
if (api.getVolume() !== options.savedVolume) {
api.setVolume(options.savedVolume);
}
}
setupPlaybar(options);
setupSliderObserver(options);
setupLocalArrowShortcuts(options);
if (options.globalShortcuts?.enabled) {
setupGlobalShortcuts(options);
setupGlobalShortcuts(options);
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
injectVolumeHud(noVid);
if (!noVid) {
setupVideoPlayerOnwheel(options);
}
firstRun(options);
// This way the ipc listener gets cleared either way
ipcRenderer.once("setupVideoPlayerVolumeMousewheel", (_event, toEnable) => {
if (toEnable)
setupVideoPlayerOnwheel(options);
// Change options from renderer to keep sync
ipcRenderer.on("setOptions", (_event, newOptions = {}) => {
for (option in newOptions) {
options[option] = newOptions[option];
}
setOptions("precise-volume", options);
});
};
}
function injectVolumeHud(noVid) {
if (noVid) {
const position = "top: 18px; right: 60px; z-index: 999; position: absolute;";
const mainStyle = "font-size: xx-large; padding: 10px; transition: opacity 1s; pointer-events: none;";
$(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend",
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
} else {
const position = `top: 10px; left: 10px; z-index: 999; position: absolute;`;
const mainStyle = "font-size: xxx-large; padding: 10px; transition: opacity 0.6s; webkit-text-stroke: 1px black; font-weight: 600; pointer-events: none;";
$("#song-video").insertAdjacentHTML('afterend',
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
}
}
let hudFadeTimeout;
function showVolumeHud(volume) {
let volumeHud = $("#volumeHud");
if (!volumeHud) return;
volumeHud.textContent = volume + '%';
volumeHud.style.opacity = 1;
if (hudFadeTimeout) {
clearTimeout(hudFadeTimeout);
}
hudFadeTimeout = setTimeout(() => {
volumeHud.style.opacity = 0;
hudFadeTimeout = null;
}, 2000);
}
/** Add onwheel event to video player */
function setupVideoPlayerOnwheel(options) {
@ -34,35 +89,20 @@ function setupVideoPlayerOnwheel(options) {
});
}
function toPercent(volume) {
return Math.round(Number.parseFloat(volume) * 100);
}
function saveVolume(volume, options) {
options.savedVolume = volume;
setOptions("precise-volume", options);
writeOptions(options);
}
/** Restore saved volume and setup tooltip */
function firstRun(options) {
const videoStream = $(".video-stream");
const slider = $("#volume-slider");
// Those elements load abit after DOMContentLoaded
if (videoStream && slider) {
// Set saved volume IF it pass checks
if (options.savedVolume
&& options.savedVolume >= 0 && options.savedVolume <= 100
&& Math.abs(slider.value - options.savedVolume) < 5
// If plugin was disabled and volume changed then diff>4
) {
videoStream.volume = options.savedVolume / 100;
slider.value = options.savedVolume;
}
// Set current volume as tooltip
setTooltip(toPercent(videoStream.volume));
} else {
setTimeout(firstRun, 500, options); // Try again in 500 milliseconds
}
//without this function it would rewrite config 20 time when volume change by 20
let writeTimeout;
function writeOptions(options) {
if (writeTimeout) clearTimeout(writeTimeout);
writeTimeout = setTimeout(() => {
setOptions("precise-volume", options);
writeTimeout = null;
}, 1000)
}
/** Add onwheel event to play bar and also track if play bar is hovered*/
@ -83,32 +123,63 @@ function setupPlaybar(options) {
playerbar.addEventListener("mouseleave", () => {
playerbar.classList.remove("on-hover");
});
setupSliderObserver(options);
}
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
function setupSliderObserver(options) {
const sliderObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
// This checks that volume-slider was manually set
if (mutation.oldValue !== mutation.target.value &&
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
// Diff>4 means it was manually set
setTooltip(mutation.target.value);
saveVolume(mutation.target.value, options);
}
}
});
// Observing only changes in 'value' of volume-slider
sliderObserver.observe($("#volume-slider"), {
attributeFilter: ["value"],
attributeOldValue: true
});
}
/** if (toIncrease = false) then volume decrease */
function changeVolume(toIncrease, options) {
// Need to change both the actual volume and the slider
const videoStream = $(".video-stream");
const slider = $("#volume-slider");
// Apply volume change if valid
const steps = (options.steps || 1) / 100;
videoStream.volume = toIncrease ?
Math.min(videoStream.volume + steps, 1) :
Math.max(videoStream.volume - steps, 0);
const steps = Number(options.steps || 1);
api.setVolume(toIncrease ?
Math.min(api.getVolume() + steps, 100) :
Math.max(api.getVolume() - steps, 0));
// Save the new volume
saveVolume(toPercent(videoStream.volume), options);
// Slider value automatically rounds to multiples of 5
slider.value = options.savedVolume;
saveVolume(api.getVolume(), options);
// change slider position (important)
updateVolumeSlider(options);
// Change tooltips to new value
setTooltip(options.savedVolume);
// Show volume slider on volume change
showVolumeSlider(slider);
// Show volume slider
showVolumeSlider();
// Show volume HUD
showVolumeHud(options.savedVolume);
}
function updateVolumeSlider(options) {
// Slider value automatically rounds to multiples of 5
$("#volume-slider").value = options.savedVolume > 0 && options.savedVolume < 5 ?
5 : options.savedVolume;
}
let volumeHoverTimeoutID;
function showVolumeSlider(slider) {
function showVolumeSlider() {
const slider = $("#volume-slider");
// This class display the volume slider if not in minimized mode
slider.classList.add("on-hover");
// Reset timeout if previous one hasn't completed
@ -124,27 +195,6 @@ function showVolumeSlider(slider) {
}, 3000);
}
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
function setupSliderObserver(options) {
const sliderObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
// This checks that volume-slider was manually set
if (mutation.oldValue !== mutation.target.value &&
(!options.savedVolume || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
// Diff>4 means it was manually set
setTooltip(mutation.target.value);
saveVolume(mutation.target.value, options);
}
}
});
// Observing only changes in 'value' of volume-slider
sliderObserver.observe($("#volume-slider"), {
attributeFilter: ["value"],
attributeOldValue: true
});
}
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
const tooltipTargets = [
"#volume-slider",
@ -161,47 +211,26 @@ function setTooltip(volume) {
function setupGlobalShortcuts(options) {
if (options.globalShortcuts.volumeUp) {
remote.globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
}
if (options.globalShortcuts.volumeDown) {
remote.globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
}
}
function setupLocalArrowShortcuts(options) {
if (options.arrowsShortcut) {
addListener();
}
// Change options from renderer to keep sync
ipcRenderer.on("setArrowsShortcut", (_event, isEnabled) => {
options.arrowsShortcut = isEnabled;
setOptions("precise-volume", options);
// This allows changing this setting without restarting app
if (isEnabled) {
addListener();
} else {
removeListener();
}
});
function addListener() {
window.addEventListener('keydown', callback);
}
function removeListener() {
window.removeEventListener("keydown", callback);
}
function callback(event) {
event.preventDefault();
switch (event.code) {
case "ArrowUp":
changeVolume(true, options);
break;
case "ArrowDown":
changeVolume(false, options);
break;
}
window.addEventListener('keydown', (event) => {
switch (event.code) {
case "ArrowUp":
event.preventDefault();
changeVolume(true, options);
break;
case "ArrowDown":
event.preventDefault();
changeVolume(false, options);
break;
}
});
}
}

View File

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

View File

@ -1,28 +1,33 @@
const { ipcRenderer } = require("electron");
const is = require("electron-is");
let ignored = {
id: ["volume-slider", "expand-volume-slider"],
types: ["mousewheel", "keydown", "keyup"]
};
// Override specific listeners of volume-slider by modifying Element.prototype
function overrideAddEventListener() {
// Events to ignore
const nativeEvents = ["mousewheel", "keydown", "keyup"];
// Save native addEventListener
Element.prototype._addEventListener = Element.prototype.addEventListener;
// Override addEventListener to Ignore specific events in volume-slider
Element.prototype.addEventListener = function (type, listener, useCapture = false) {
if (this.tagName === "TP-YT-PAPER-SLIDER") { // tagName of #volume-slider
for (const eventType of nativeEvents) {
if (eventType === type) {
return;
}
}
}//else
this._addEventListener(type, listener, useCapture);
if (!(
ignored.id.includes(this.id) &&
ignored.types.includes(type)
)) {
this._addEventListener(type, listener, useCapture);
} else if (is.dev()) {
console.log(`Ignoring event: "${this.id}.${type}()"`);
}
};
}
module.exports = () => {
overrideAddEventListener();
// Restore original function after did-finish-load to avoid keeping Element.prototype altered
ipcRenderer.once("restoreAddEventListener", () => { //called from Main to make sure page is completly loaded
// Restore original function after finished loading to avoid keeping Element.prototype altered
window.addEventListener('load', () => {
Element.prototype.addEventListener = Element.prototype._addEventListener;
});
Element.prototype._addEventListener = undefined;
ignored = undefined;
}, { once: true });
};

View File

@ -0,0 +1,41 @@
const { ElementFromFile, templatePath } = require("../utils");
const { dialog } = require('@electron/remote');
function $(selector) { return document.querySelector(selector); }
const qualitySettingsButton = ElementFromFile(
templatePath(__dirname, "qualitySettingsTemplate.html")
);
module.exports = () => {
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
}
function setup(event) {
const api = event.detail;
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
qualitySettingsButton.onclick = function chooseQuality() {
if (api.getPlayerState() === 2) api.playVideo();
else if (api.getPlayerState() === 1) api.pauseVideo();
const currentIndex = api.getAvailableQualityLevels().indexOf(api.getPlaybackQuality())
dialog.showMessageBox({
type: "question",
buttons: api.getAvailableQualityLabels(),
defaultId: currentIndex,
title: "Choose Video Quality",
message: "Choose Video Quality:",
detail: `Current Quality: ${api.getAvailableQualityLabels()[currentIndex]}`,
cancelId: -1
}).then((promise) => {
if (promise.response === -1) return;
const newQuality = api.getAvailableQualityLevels()[promise.response];
api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality)
})
}
}

View File

@ -0,0 +1,13 @@
<tp-yt-paper-icon-button class="player-quality-button style-scope ytmusic-player" icon="yt-icons:settings"
title="Open player quality changer" aria-label="Open player quality changer" role="button" tabindex="0" aria-disabled="false">
<tp-yt-iron-icon id="icon" class="style-scope tp-yt-paper-icon-button"><svg viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%;">
<g 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"
class="style-scope yt-icon"></path>
</g>
</svg>
</tp-yt-iron-icon>
</tp-yt-paper-icon-button>

View File

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

53
plugins/shortcuts/menu.js Normal file
View File

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

View File

@ -0,0 +1,85 @@
const mpris = require("mpris-service");
const { ipcMain } = require("electron");
const registerCallback = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls");
function setupMPRIS() {
const player = mpris({
name: "youtube-music",
identity: "YouTube Music",
canRaise: true,
supportedUriSchemes: ["https"],
supportedMimeTypes: ["audio/mpeg"],
supportedInterfaces: ["player"],
desktopEntry: "youtube-music",
});
return player;
}
function registerMPRIS(win) {
const songControls = getSongControls(win);
const { playPause, next, previous } = songControls;
try {
const secToMicro = 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 seekBy = o => win.webContents.send("seekBy", microToSec(o));
const player = setupMPRIS();
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
let currentSeconds = 0;
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
player.getPosition = () => secToMicro(currentSeconds)
player.on("raise", () => {
win.setSkipTaskbar(false);
win.show();
});
player.on("play", () => {
if (player.playbackStatus !== 'Playing') {
player.playbackStatus = 'Playing';
playPause()
}
});
player.on("pause", () => {
if (player.playbackStatus !== 'Paused') {
player.playbackStatus = 'Paused';
playPause()
}
});
player.on("playpause", playPause);
player.on("next", next);
player.on("previous", previous);
player.on('seek', seekBy);
player.on('position', seekTo);
registerCallback(songInfo => {
if (player) {
const data = {
'mpris:length': secToMicro(songInfo.songDuration),
'mpris:artUrl': songInfo.imageSrc,
'xesam:title': songInfo.title,
'xesam:artist': songInfo.artist,
'mpris:trackid': '/'
};
if (songInfo.album) data['xesam:album'] = songInfo.album;
player.metadata = data;
player.seeked(secToMicro(songInfo.elapsedSeconds))
player.playbackStatus = songInfo.isPaused ? "Paused" : "Playing"
}
})
} catch (e) {
console.warn("Error in MPRIS", e);
}
}
module.exports = registerMPRIS;

View File

@ -0,0 +1,37 @@
const hark = require("hark/hark.bundle.js");
module.exports = () => {
let isSilent = false;
document.addEventListener("apiLoaded", (apiEvent) => {
const video = document.querySelector("video");
const speechEvents = hark(video, {
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
interval: 2, // ms
});
const skipSilence = () => {
if (isSilent && !video.paused) {
video.currentTime += 0.2; // in s
}
};
speechEvents.on("speaking", function () {
isSilent = false;
});
speechEvents.on("stopped_speaking", function () {
if (!(video.paused || video.seeking || video.ended)) {
isSilent = true;
skipSilence();
}
});
video.addEventListener("play", function () {
skipSilence();
});
video.addEventListener("seeked", function () {
skipSilence();
});
});
};

View File

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

View File

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

View File

@ -0,0 +1,29 @@
// Segments are an array [ [start, end], … ]
module.exports.sortSegments = (segments) => {
segments.sort((segment1, segment2) =>
segment1[0] === segment2[0]
? segment1[1] - segment2[1]
: segment1[0] - segment2[0]
);
const compiledSegments = [];
let currentSegment;
segments.forEach((segment) => {
if (!currentSegment) {
currentSegment = segment;
return;
}
if (currentSegment[1] < segment[0]) {
compiledSegments.push(currentSegment);
currentSegment = segment;
return;
}
currentSegment[1] = Math.max(currentSegment[1], segment[1]);
});
compiledSegments.push(currentSegment);
return compiledSegments;
};

View File

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

View File

@ -1,12 +1,11 @@
const getSongControls = require('../../providers/song-controls');
const getSongInfo = require('../../providers/song-info');
const registerCallback = require('../../providers/song-info');
const path = require('path');
let controls;
let currentSongInfo;
module.exports = win => {
const registerCallback = getSongInfo(win);
const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous };

View File

@ -7,7 +7,7 @@ const {
TouchBarScrubber,
} = TouchBar;
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls");
// Songtitle label
@ -59,7 +59,6 @@ const touchBar = new TouchBar({
});
module.exports = (win) => {
const registerCallback = getSongInfo(win);
const { playPause, next, previous, like, dislike } = getSongControls(win);
// If the page is ready, register the callback

52
plugins/tuna-obs/back.js Normal file
View File

@ -0,0 +1,52 @@
const { ipcMain } = require("electron");
const fetch = require('node-fetch');
const registerCallback = require("../../providers/song-info");
const secToMilisec = t => Math.round(Number(t) * 1e3);
const data = {
cover_url: '',
title: '',
artists: [],
status: '',
progress: 0,
duration: 0,
album_url: '',
album: undefined
};
const post = async (data) => {
const port = 1608;
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Origin': '*'
}
const url = `http://localhost:${port}/`;
fetch(url, { method: 'POST', headers, body: JSON.stringify({ data }) }).catch(e => console.log(`Error: '${e.code || e.errno}' - when trying to access obs-tuna webserver at port ${port}`));
}
module.exports = async (win) => {
ipcMain.on('timeChanged', async (_, t) => {
if (!data.title) return;
data.progress = secToMilisec(t);
post(data);
});
registerCallback((songInfo) => {
if (!songInfo.title && !songInfo.artist) {
return;
}
data.duration = secToMilisec(songInfo.songDuration)
data.progress = secToMilisec(songInfo.elapsedSeconds)
data.cover_url = songInfo.imageSrc;
data.album_url = songInfo.imageSrc;
data.title = songInfo.title;
data.artists = [songInfo.artist];
data.status = songInfo.isPaused ? 'stopped' : 'playing';
data.album = songInfo.album;
post(data);
})
}

View File

@ -43,7 +43,7 @@ module.exports.fileExists = (path, callbackIfExists) => {
};
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
webContents.once("did-finish-load", async () => {
webContents.on("did-finish-load", async () => {
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
if (cb) {
cb();

View File

@ -0,0 +1,10 @@
const { injectCSS } = require("../utils");
const path = require("path");
module.exports = (win, options) => {
if (options.forceHide) {
injectCSS(win.webContents, path.join(__dirname, "force-hide.css"));
} else {
injectCSS(win.webContents, path.join(__dirname, "button-switcher.css"));
}
};

View File

@ -0,0 +1,77 @@
#main-panel.ytmusic-player-page {
align-items: unset !important;
}
.video-switch-button {
z-index: 999;
box-sizing: border-box;
padding: 0;
margin-top: 20px;
margin-left: 10px;
background: rgba(33, 33, 33, 0.4);
border-radius: 30px;
overflow: hidden;
width: 240px;
text-align: center;
font-size: 18px;
letter-spacing: 1px;
color: #fff;
padding-right: 120px;
position: absolute;
}
.video-switch-button:before {
content: "Video";
position: absolute;
top: 0;
bottom: 0;
right: 0;
width: 120px;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
pointer-events: none;
}
.video-switch-button-checkbox {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 2;
}
.video-switch-button-label-span {
position: relative;
}
.video-switch-button-checkbox:checked+.video-switch-button-label:before {
transform: translateX(120px);
transition: transform 300ms linear;
}
.video-switch-button-checkbox+.video-switch-button-label {
position: relative;
padding: 15px 0;
display: block;
user-select: none;
pointer-events: none;
}
.video-switch-button-checkbox+.video-switch-button-label:before {
content: "";
background: rgba(60, 60, 60, 0.4);
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: 30px;
transform: translateX(0);
transition: transform 300ms;
}

View File

@ -0,0 +1,116 @@
const { ElementFromFile, templatePath } = require("../utils");
const { setOptions } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); }
let options, player, video, api;
const switchButtonDiv = ElementFromFile(
templatePath(__dirname, "button_template.html")
);
module.exports = (_options) => {
if (_options.forceHide) return;
options = _options;
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
}
function setup(e) {
api = e.detail;
player = $('ytmusic-player');
video = $('video');
$('ytmusic-player-page').prepend(switchButtonDiv);
$('#song-image.ytmusic-player').style.display = "block";
if (options.hideVideo) {
$('.video-switch-button-checkbox').checked = false;
changeDisplay(false);
forcePlaybackMode();
// fix black video
video.style.height = "auto";
}
// button checked = show video
switchButtonDiv.addEventListener('change', (e) => {
options.hideVideo = !e.target.checked;
changeDisplay(e.target.checked);
setOptions("video-toggle", options);
})
video.addEventListener('srcChanged', videoStarted);
observeThumbnail();
}
function changeDisplay(showVideo) {
player.style.margin = showVideo ? '' : 'auto 0px';
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
$('#song-video.ytmusic-player').style.display = showVideo ? 'unset' : 'none';
if (showVideo && !video.style.top) {
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
}
moveVolumeHud(showVideo);
}
function videoStarted() {
if (api.getPlayerResponse().videoDetails.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV') {
// switch to high res thumbnail
forceThumbnail($('#song-image img'));
// show toggle button
switchButtonDiv.style.display = "initial";
// change display to video mode if video exist & video is hidden & option.hideVideo = false
if (!options.hideVideo && $('#song-video.ytmusic-player').style.display === "none") {
changeDisplay(true);
} else {
moveVolumeHud(!options.hideVideo);
}
} else {
// video doesn't exist -> switch to song mode
changeDisplay(false);
// hide toggle button
switchButtonDiv.style.display = "none";
}
}
// on load, after a delay, the page overrides the playback-mode to 'OMV_PREFERRED' which causes weird aspect ratio in the image container
// this function fix the problem by overriding that override :)
function forcePlaybackMode() {
const playbackModeObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.target.getAttribute('playback-mode') !== "ATV_PREFERRED") {
playbackModeObserver.disconnect();
mutation.target.setAttribute('playback-mode', "ATV_PREFERRED");
}
});
});
playbackModeObserver.observe(player, { attributeFilter: ["playback-mode"] });
}
// if precise volume plugin is enabled, move its hud to be on top of the video
function moveVolumeHud(showVideo) {
const volumeHud = $('#volumeHud');
if (volumeHud)
volumeHud.style.top = showVideo ? `${(player.clientHeight - video.clientHeight) / 2}px` : 0;
}
function observeThumbnail() {
const playbackModeObserver = new MutationObserver(mutations => {
if (!player.videoMode_) return;
mutations.forEach(mutation => {
if (!mutation.target.src.startsWith('data:')) return;
forceThumbnail(mutation.target)
});
});
playbackModeObserver.observe($('#song-image img'), { attributeFilter: ["src"] })
}
function forceThumbnail(img) {
const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails;
if (thumbnails && thumbnails.length > 0) {
img.src = thumbnails[thumbnails.length - 1].url.split("?")[0];
}
}

View File

@ -0,0 +1,13 @@
const { setOptions } = require("../../config/plugins");
module.exports = (win, options) => [
{
label: "Force Remove Video Tab",
type: "checkbox",
checked: options.forceHide,
click: item => {
options.forceHide = item.checked;
setOptions("video-toggle", options);
}
}
];

View File

@ -0,0 +1,4 @@
<div class="video-switch-button">
<input class="video-switch-button-checkbox" type="checkbox" checked="true"></input>
<label class="video-switch-button-label" for=""><span class="video-switch-button-label-span">Song</span></label>
</div>

View File

@ -1,13 +1,17 @@
const path = require("path");
const { remote } = require("electron");
const remote = require('@electron/remote');
const config = require("./config");
const { fileExists } = require("./plugins/utils");
const setupFrontLogger = require("./providers/front-logger");
const setupSongInfo = require("./providers/song-info-front");
const { setupSongControls } = require("./providers/song-controls-front");
const plugins = config.plugins.getEnabled();
let api;
plugins.forEach(([plugin, options]) => {
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js");
fileExists(preloadPath, () => {
@ -36,9 +40,14 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
// wait for complete load of youtube api
listenForApiLoad();
// inject song-info provider
const songInfoProviderPath = path.join(__dirname, "providers", "song-info-front.js")
fileExists(songInfoProviderPath, require(songInfoProviderPath));
setupSongInfo();
// inject song-controls
setupSongControls();
// inject front logger
setupFrontLogger();
@ -46,4 +55,37 @@ document.addEventListener("DOMContentLoaded", () => {
// Add action for reloading
global.reload = () =>
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
setInterval(() => window._lact = Date.now(), 900000);
});
function listenForApiLoad() {
api = document.querySelector('#movie_player');
if (api) {
onApiLoaded();
return;
}
const observer = new MutationObserver(() => {
api = document.querySelector('#movie_player');
if (api) {
observer.disconnect();
onApiLoaded();
}
})
observer.observe(document.documentElement, { childList: true, subtree: true });
}
function onApiLoaded() {
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
// Remove upgrade button
if (config.get("options.removeUpgradeButton")) {
const upgradeButtton = document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')
if (upgradeButtton) {
upgradeButtton.style.display = "none";
}
}
}

View File

@ -1,23 +1,4 @@
let domElements = {};
const watchDOMElement = (name, selectorFn, cb) => {
const observer = new MutationObserver((mutations, observer) => {
if (!domElements[name]) {
domElements[name] = selectorFn(document);
}
if (domElements[name]) {
cb(domElements[name]);
}
});
observer.observe(document, {
childList: true,
subtree: true,
});
};
const getSongMenu = () =>
document.querySelector("ytmusic-menu-popup-renderer tp-yt-paper-listbox");
module.exports = { getSongMenu, watchDOMElement };
module.exports = { getSongMenu };

View File

@ -0,0 +1,14 @@
const customTitlebar = require("custom-electron-titlebar");
module.exports = () => {
new customTitlebar.Titlebar({
backgroundColor: customTitlebar.Color.fromHex("#050505"),
minimizable: false,
maximizable: false,
menu: null
});
const mainStyle = document.querySelector("#container").style;
mainStyle.width = "100%";
mainStyle.position = "fixed";
mainStyle.border = "unset";
};

View File

@ -0,0 +1,18 @@
const path = require("path");
const is = require("electron-is");
const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png");
const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js");
const promptOptions = is.macOS() ? {
customStylesheet: "dark",
icon: iconPath
} : {
customStylesheet: "dark",
// The following are used for custom titlebar
frame: false,
customScript: customTitlebarPath,
enableRemoteModule: true
};
module.exports = () => promptOptions;

View File

@ -0,0 +1,13 @@
const { ipcRenderer } = require("electron");
const config = require("../config");
const is = require("electron-is");
module.exports.setupSongControls = () => {
document.addEventListener('apiLoaded', e => {
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
if (is.linux() && config.plugins.isEnabled('shortcuts')) { // MPRIS Enabled
document.querySelector('video').addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
}
}, { once: true, passive: true })
};

View File

@ -13,8 +13,8 @@ module.exports = (win) => {
previous: () => pressKey(win, "k"),
next: () => pressKey(win, "j"),
playPause: () => pressKey(win, "space"),
like: () => pressKey(win, "_"),
dislike: () => pressKey(win, "+"),
like: () => pressKey(win, "+"),
dislike: () => pressKey(win, "_"),
go10sBack: () => pressKey(win, "h"),
go10sForward: () => pressKey(win, "l"),
go1sBack: () => pressKey(win, "h", ["shift"]),
@ -24,8 +24,6 @@ module.exports = (win) => {
// General
volumeMinus10: () => pressKey(win, "-"),
volumePlus10: () => pressKey(win, "="),
dislikeAndNext: () => pressKey(win, "-", ["shift"]),
like: () => pressKey(win, "=", ["shift"]),
fullscreen: () => pressKey(win, "f"),
muteUnmute: () => pressKey(win, "m"),
maximizeMinimisePlayer: () => pressKey(win, "q"),
@ -38,14 +36,14 @@ module.exports = (win) => {
pressKey(win, "g");
pressKey(win, "l");
},
goToHotlist: () => {
pressKey(win, "g");
pressKey(win, "t");
},
goToSettings: () => {
pressKey(win, "g");
pressKey(win, ",");
},
goToExplore: () => {
pressKey(win, "g");
pressKey(win, "e");
},
search: () => pressKey(win, "/"),
showShortcuts: () => pressKey(win, "/", ["shift"]),
};

View File

@ -1,32 +1,60 @@
const { ipcRenderer } = require("electron");
const is = require('electron-is');
const { getImage } = require("./song-info");
const config = require("../config");
global.songInfo = {};
function $(selector) { return document.querySelector(selector); }
ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
global.songInfo = JSON.parse(extractedSongInfo);
global.songInfo.image = await getImage(global.songInfo.imageSrc);
});
const injectListener = () => {
var oldXHR = window.XMLHttpRequest;
function newXHR() {
var realXHR = new oldXHR();
realXHR.addEventListener(
"readystatechange",
() => {
if (realXHR.readyState == 4 && realXHR.status == 200) {
if (realXHR.responseURL.includes("/player")) {
// if the request contains the song info, send the response to ipcMain
ipcRenderer.send("song-info-request", realXHR.responseText);
}
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
const srcChangedEvent = new CustomEvent('srcChanged');
module.exports = () => {
document.addEventListener('apiLoaded', apiEvent => {
if (config.plugins.isEnabled('tuna-obs') ||
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
setupTimeChangeListener();
}
const video = $('video');
// name = "dataloaded" and abit later "dataupdated"
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
if (name !== 'dataloaded') return;
video.dispatchEvent(srcChangedEvent);
sendSongInfo();
})
for (const status of ['playing', 'pause']) {
video.addEventListener(status, e => {
if (Math.round(e.target.currentTime) > 0) {
ipcRenderer.send("playPaused", {
isPaused: status === 'pause',
elapsedSeconds: Math.floor(e.target.currentTime)
});
}
},
false
);
return realXHR;
}
window.XMLHttpRequest = newXHR;
});
}
function sendSongInfo() {
const data = apiEvent.detail.getPlayerResponse();
data.videoDetails.album = $('ytmusic-player-page')?.__data?.playerPageWatchMetadata?.albumName?.runs[0].text
data.videoDetails.elapsedSeconds = Math.floor(video.currentTime);
data.videoDetails.isPaused = false;
ipcRenderer.send("video-src-changed", JSON.stringify(data));
}
}, { once: true, passive: true });
};
module.exports = injectListener;
function setupTimeChangeListener() {
const progressObserver = new MutationObserver(mutations => {
ipcRenderer.send('timeChanged', mutations[0].target.value);
global.songInfo.elapsedSeconds = mutations[0].target.value;
});
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
}

View File

@ -2,46 +2,12 @@ const { ipcMain, nativeImage } = require("electron");
const fetch = require("node-fetch");
// This selects the progress bar, used for current progress
const progressSelector = "#progress-bar";
// Grab the progress using the selector
const getProgress = async (win) => {
// Get current value of the progressbar element
const elapsedSeconds = await win.webContents.executeJavaScript(
'document.querySelector("' + progressSelector + '").value'
);
return elapsedSeconds;
};
// Grab the native image using the src
const getImage = async (src) => {
const result = await fetch(src);
const buffer = await result.buffer();
return nativeImage.createFromBuffer(buffer);
};
// To find the paused status, we check if the title contains `-`
const getPausedStatus = async (win) => {
const title = await win.webContents.executeJavaScript("document.title");
return !title.includes("-");
};
const getArtist = async (win) => {
return await win.webContents.executeJavaScript(
`
var bar = document.getElementsByClassName('subtitle ytmusic-player-bar')[0];
var artistName = (bar.getElementsByClassName('yt-formatted-string')[0]) || (bar.getElementsByClassName('byline ytmusic-player-bar')[0]);
if (artistName) {
artistName.textContent;
}
`
)
}
const config = require("../config");
// Fill songInfo with empty values
/**
* @typedef {songInfo} SongInfo
*/
const songInfo = {
title: "",
artist: "",
@ -53,54 +19,111 @@ const songInfo = {
songDuration: 0,
elapsedSeconds: 0,
url: "",
album: undefined,
videoId: "",
playlistId: "",
};
// Grab the native image using the src
const getImage = async (src) => {
const result = await fetch(src);
const buffer = await result.buffer();
const output = nativeImage.createFromBuffer(buffer);
if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
return getImage(src.slice(0, src.lastIndexOf(".jpg") + 4));
} else {
return output;
}
};
const handleData = async (responseText, win) => {
let data = JSON.parse(responseText);
songInfo.title = data?.videoDetails?.title;
songInfo.artist = await getArtist(win) || data?.videoDetails?.author;
songInfo.views = data?.videoDetails?.viewCount;
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
songInfo.image = await getImage(songInfo.imageSrc);
songInfo.uploadDate = data?.microformat?.microformatDataRenderer?.uploadDate;
songInfo.url = data?.microformat?.microformatDataRenderer?.urlCanonical;
const data = JSON.parse(responseText);
if (!data) return;
win.webContents.send("update-song-info", JSON.stringify(songInfo));
const microformat = data.microformat?.microformatDataRenderer;
if (microformat) {
songInfo.uploadDate = microformat.uploadDate;
songInfo.url = microformat.urlCanonical?.split("&")[0];
songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get("list");
// used for options.resumeOnStart
config.set("url", microformat.urlCanonical);
}
const videoDetails = data.videoDetails;
if (videoDetails) {
songInfo.title = cleanupName(videoDetails.title);
songInfo.artist = cleanupName(videoDetails.author);
songInfo.views = videoDetails.viewCount;
songInfo.songDuration = videoDetails.lengthSeconds;
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
songInfo.isPaused = videoDetails.isPaused;
songInfo.videoId = videoDetails.videoId;
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
const oldUrl = songInfo.imageSrc;
songInfo.imageSrc = videoDetails.thumbnail?.thumbnails?.pop()?.url.split("?")[0];
if (oldUrl !== songInfo.imageSrc) {
songInfo.image = await getImage(songInfo.imageSrc);
}
win.webContents.send("update-song-info", JSON.stringify(songInfo));
}
};
// This variable will be filled with the callbacks once they register
const callbacks = [];
// This function will allow plugins to register callback that will be triggered when data changes
/**
* @callback songInfoCallback
* @param {songInfo} songInfo
* @returns {void}
*/
/**
* @param {songInfoCallback} callback
*/
const registerCallback = (callback) => {
callbacks.push(callback);
};
const registerProvider = (win) => {
// This variable will be filled with the callbacks once they register
const callbacks = [];
// This function will allow plugins to register callback that will be triggered when data changes
const registerCallback = (callback) => {
callbacks.push(callback);
};
win.on("page-title-updated", async () => {
// Get and set the new data
songInfo.isPaused = await getPausedStatus(win);
const elapsedSeconds = await getProgress(win);
songInfo.elapsedSeconds = elapsedSeconds;
// Trigger the callbacks
callbacks.forEach((c) => {
c(songInfo);
});
});
// This will be called when the song-info-front finds a new request with song data
ipcMain.on("song-info-request", async (_, responseText) => {
ipcMain.on("video-src-changed", async (_, responseText) => {
await handleData(responseText, win);
callbacks.forEach((c) => {
c(songInfo);
});
});
return registerCallback;
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
songInfo.isPaused = isPaused;
songInfo.elapsedSeconds = elapsedSeconds;
callbacks.forEach((c) => {
c(songInfo);
});
})
};
module.exports = registerProvider;
const suffixesToRemove = [
" - topic",
"vevo",
" (performance video)",
" (official music video)",
" (official video)",
" (clip officiel)",
];
function cleanupName(name) {
if (!name) return name;
const lowCaseName = name.toLowerCase();
for (const suffix of suffixesToRemove) {
if (lowCaseName.endsWith(suffix)) {
return name.slice(0, -suffix.length);
}
}
return name;
}
module.exports = registerCallback;
module.exports.setupSongInfo = registerProvider;
module.exports.getImage = getImage;
module.exports.cleanupName = cleanupName;

View File

@ -34,16 +34,32 @@ You can check out the [latest release](https://github.com/th-ch/youtube-music/re
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
## Available plugins:
- **Ad Blocker**: block all ads and tracking out of the box
- **Downloader**: download to MP3 directly from the interface (youtube-dl)
- **No Google Login**: remove Google login buttons and links from the interface
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music
- **Audio compressor**: apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)
- **Blur navigation bar**: makes navigation bar transparent and blurry
- **Disable autoplay**: makes every song start in "paused" mode
- [**Discord**](https://discord.com/): show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
- **Downloader**: downloads MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
- **In-app menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
> (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem accessing the menu after enabling this plugin and hide-menu option)
- [**Last.fm**](https://www.last.fm/): scrobbles support
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
- **Auto confirm when paused**: when the "Continue Watching?" modal appears, automatically click "Yes"
- **Hide video player**: no video in the interface when playing music
- **Notifications**: display a notification when a song starts playing
- **No Google Login**: remove Google login buttons and links from the interface
- **Notifications**: display a notification when a song starts playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) are available on windows)
- **Playback speed**: listen fast, listen slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
- **Precise volume**: customizable volume steps for more comfort, allows controlling the volume precisely using mousewheel
- **Quality changer**: Allows changing the video quality with a [button](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) on the video overlay
- **Shortcuts**: Allows setting global hotkeys for playback (play/pause/next/previous) + disable [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + [custom hotkeys](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) for [advanced users](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): skips non-music parts
- **Taskbar media control**: control playback from your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
- **Touchbar**: custom TouchBar layout for macOS
- **Video Toggle**: Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab
---
- **Auto confirm when paused** (Always Enabled): disable the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) popup that pause music after a certain time
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
## Dev

View File

@ -1,9 +1,7 @@
const path = require("path");
const getPort = require("get-port");
const NodeEnvironment = require("jest-environment-node");
const electronPath = require("electron");
const { Application } = require("spectron");
const { _electron: electron } = require("playwright");
class TestEnvironment extends NodeEnvironment {
constructor(config) {
@ -14,21 +12,12 @@ class TestEnvironment extends NodeEnvironment {
await super.setup();
const appPath = path.resolve(__dirname, "..");
const port = await getPort();
this.global.__APP__ = new Application({
path: electronPath,
args: [appPath],
port,
});
await this.global.__APP__.start();
const { client } = this.global.__APP__;
await client.waitUntilWindowLoaded();
this.global.__APP__ = await electron.launch({ args: [appPath] });
}
async teardown() {
if (this.global.__APP__.isRunning()) {
await this.global.__APP__.stop();
if (this.global.__APP__) {
await this.global.__APP__.close();
}
await super.teardown();
}

View File

@ -1,23 +1,16 @@
/**
* @jest-environment ./tests/environment
*/
describe("YouTube Music App", () => {
const app = global.__APP__;
test("With default settings, app is launched and visible", async () => {
expect(app.isRunning()).toBe(true);
const win = app.browserWindow;
const isMenuVisible = await win.isMenuBarVisible();
expect(isMenuVisible).toBe(true);
const isVisible = await win.isVisible();
expect(isVisible).toBe(true);
const { width, height } = await win.getBounds();
expect(width).toBeGreaterThan(0);
expect(height).toBeGreaterThan(0);
const { client } = app;
const title = await client.getTitle();
const window = await app.firstWindow();
const title = await window.title();
expect(title).toEqual("YouTube Music");
const url = window.url();
expect(url.startsWith("https://music.youtube.com")).toBe(true);
});
});

View File

@ -63,12 +63,7 @@ module.exports.setUpTray = (app, win) => {
app.quit();
},
},
{
label: "Quit",
click: () => {
app.quit();
},
},
{ role: "quit" }
];
const trayMenu = Menu.buildFromTemplate(template);

5624
yarn.lock

File diff suppressed because it is too large Load Diff

View File

@ -28,3 +28,14 @@ ytmusic-search-box.ytmusic-nav-bar {
ytmusic-mealbar-promo-renderer {
display: none !important;
}
/* Disable Image Selection */
img {
-webkit-user-select: none;
user-select: none;
}
/* Hide cast button which doesn't work */
ytmusic-cast-button {
display: none !important;
}