Compare commits

...

197 Commits

Author SHA1 Message Date
TC
644950ccc0 Bump version 2021-01-15 09:31:34 +01:00
TC
d848937e6a Rename discord plugin 2021-01-14 23:16:59 +01:00
446aa8becc Merge pull request #121 from th-ch/snyk-upgrade-d29b51f488ba9deb228b72952a3f7b8e
[Snyk] Upgrade electron-debug from 3.1.0 to 3.2.0
2021-01-14 23:12:27 +01:00
TC
d52dbee13c Prettier utils 2021-01-14 23:09:16 +01:00
TC
ed09304ed7 Refactor tray to use provider + play/pause on click 2021-01-14 23:09:04 +01:00
TC
2861473097 Set title/artist metadata in downloader 2021-01-14 23:01:26 +01:00
25fd48697b Merge pull request #125 from th-ch/refactor-providers
Refactor providers
2021-01-14 21:03:26 +01:00
TC
aec542e95e Update discord plugin for new provider + wait for ready 2021-01-13 22:22:22 +01:00
TC
eae95befe1 Merge branch 'master' of github.com:th-ch/youtube-music into refactor-providers
# By TC (9) and semvis123 (2)
# Via GitHub (4) and semvis123 (1)
* 'master' of github.com:th-ch/youtube-music:
  renamed DiscordRPC to Discord
  Downloader plugin: log audio bitrate
  Disable context isolation (to load ffmpeg wasm)
  Use contextBridge in preload script + update navigation plugin
  Fix downloader plugin with context isolation
  Bump version
  Add portable target to windows builds
  Added Discord rich presence and added extra properties to songinfo provider
  Bump version
  Allow custom audio extensions in downloader
  Defensive: handle null/undefined ffmpeg args
2021-01-13 22:10:10 +01:00
f0200e7b38 Merge pull request #124 from semvis123/master
Added Discord rich presence and added extra properties to songInfo provider
2021-01-13 22:08:10 +01:00
70775f4988 renamed DiscordRPC to Discord 2021-01-13 21:56:53 +01:00
074840ef56 Merge pull request #127 from th-ch/fix-plugins-context-isolation
Fix plugins with context isolation
2021-01-13 21:44:37 +01:00
TC
b54c501eeb Downloader plugin: log audio bitrate 2021-01-13 21:36:16 +01:00
6d587cb432 Merge pull request #126 from th-ch/windows-portable
Windows portable exe
2021-01-13 21:32:53 +01:00
TC
39c8031cd7 Disable context isolation (to load ffmpeg wasm) 2021-01-13 21:28:42 +01:00
TC
9ad1dad6df Use contextBridge in preload script + update navigation plugin 2021-01-12 22:54:04 +01:00
TC
79e8fc2fac Fix downloader plugin with context isolation 2021-01-12 22:52:21 +01:00
TC
67c4422eb8 Bump version 2021-01-12 21:37:02 +01:00
TC
18df1223af Add portable target to windows builds 2021-01-12 21:36:38 +01:00
0fafed7c53 Merge pull request #118 from th-ch/dl-custom-audio-format
Downloader plugin - custom audio format
2021-01-12 21:35:55 +01:00
TC
f7cbf2c221 No autoloading of providers (loaded on demand in plugins) 2021-01-12 21:19:01 +01:00
TC
87d2693e2b Use refactored provider in shortcuts plugin 2021-01-12 21:18:32 +01:00
TC
de1e4196d9 Use refactored provider in notification plugin 2021-01-12 21:18:05 +01:00
TC
9110e79c16 Use refactored providers in touchbar plugin 2021-01-12 21:17:40 +01:00
TC
0743034de0 Split providers in 2 2021-01-12 21:17:08 +01:00
TC
f1ddb92886 nit: prettier 2021-01-12 21:16:29 +01:00
a8ce87f2cc Added Discord rich presence and added extra properties to songinfo provider 2021-01-12 20:16:49 +01:00
86a329a61b fix: upgrade electron-debug from 3.1.0 to 3.2.0
Snyk has created this PR to upgrade electron-debug from 3.1.0 to 3.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-01-12 02:24:30 +00:00
77e24f41a5 Merge pull request #102 from semvis123/master
Globalized the song info and song controls, and updated Touch Bar for it.
2021-01-10 22:15:11 +01:00
3a5d9bd973 Loads providers before plugins 2021-01-10 21:37:50 +01:00
69f486d53f moved the song info file and removed the capital letters in folder name 2021-01-10 21:22:01 +01:00
5d89043884 fixed typo, plugins/songInfo/back.js
Co-authored-by: th-ch <th-ch@users.noreply.github.com>
2021-01-10 20:31:55 +01:00
2b297c245a Merge pull request #120 from th-ch/bump-electron
Bump electron to v11
2021-01-10 14:37:07 +01:00
TC
945a61fafd Remove warning by setting contextIsolation 2021-01-10 14:10:16 +01:00
TC
1ba166a172 Bump electron to v11 2021-01-10 12:29:12 +01:00
TC
a9a840b6c3 Bump version 2021-01-08 22:41:17 +01:00
TC
6a100c8cb1 Allow custom audio extensions in downloader 2021-01-08 22:29:25 +01:00
TC
b04e2ea130 Defensive: handle null/undefined ffmpeg args 2021-01-08 22:26:44 +01:00
5ac356bf91 Merge pull request #116 from th-ch/snyk-upgrade-041c3272121410c1cb380e1fbc7f9dce
[Snyk] Upgrade electron-updater from 4.3.5 to 4.3.6
2021-01-08 21:29:50 +01:00
35ceb7e83e Merge pull request #117 from th-ch/snyk-upgrade-7528cf91d37d9330a45f611ce738e40e
[Snyk] Upgrade @cliqz/adblocker-electron from 1.18.8 to 1.19.0
2021-01-08 21:28:08 +01:00
5c0cc08d80 fix: upgrade @cliqz/adblocker-electron from 1.18.8 to 1.19.0
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.18.8 to 1.19.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-01-07 02:19:51 +00:00
0bf77e592a fix: upgrade electron-updater from 4.3.5 to 4.3.6
Snyk has created this PR to upgrade electron-updater from 4.3.5 to 4.3.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-01-05 02:20:56 +00:00
ec3c1578d2 Merge pull request #109 from th-ch/snyk-upgrade-4e8a81615c57edf69b21ad2a68431dfd
[Snyk] Upgrade ytdl-core from 4.1.1 to 4.1.2
2020-12-29 16:35:55 +01:00
e8ed580ecc Merge pull request #104 from th-ch/dependabot/npm_and_yarn/node-notifier-8.0.1
Bump node-notifier from 8.0.0 to 8.0.1
2020-12-29 16:30:59 +01:00
e2cc2628ae fix: upgrade ytdl-core from 4.1.1 to 4.1.2
Snyk has created this PR to upgrade ytdl-core from 4.1.1 to 4.1.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
2020-12-25 02:20:03 +00:00
9e31e753f0 Bump node-notifier from 8.0.0 to 8.0.1
Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/mikaelbr/node-notifier/releases)
- [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md)
- [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-22 16:02:44 +00:00
588e0019d6 Changed function style in notifications 2020-12-21 22:11:23 +01:00
5bffdbd628 Simplifies the notification plugin to use the globalized song info 2020-12-21 21:35:02 +01:00
ee239da647 removed unnecessary await keyword 2020-12-21 21:11:00 +01:00
9be3e1afe9 Globalized the songinfo and song controls, and changed the pause/play button.
Globalized the songinfo and song controls, and changed the pause/play button. The songInfo file should eventually get another location, because it isn't really a plugin.
2020-12-21 18:18:34 +01:00
TC
af02b60ce3 Bump version 2020-12-20 20:17:24 +01:00
fbdae0452c Add touchbar plugin to list 2020-12-20 20:16:35 +01:00
d68d73eb4c Merge pull request #101 from semvis123/master
Added Touch Bar plugin
2020-12-20 20:14:49 +01:00
2de27d2e24 Merge pull request #99 from th-ch/snyk-upgrade-d3ff12c0e23681ac817994e603d37491
[Snyk] Upgrade @ffmpeg/core from 0.8.4 to 0.8.5
2020-12-19 22:03:40 +01:00
TC
15591e24d7 Merge branch 'master' of github.com:th-ch/youtube-music into snyk-upgrade-d3ff12c0e23681ac817994e603d37491
# By snyk-bot
# Via GitHub (1) and snyk-bot (1)
* 'master' of github.com:th-ch/youtube-music:
  fix: upgrade @ffmpeg/ffmpeg from 0.9.5 to 0.9.6
2020-12-19 21:50:24 +01:00
715d59e3d4 Merge pull request #100 from th-ch/snyk-upgrade-9aa7c0fdfaaeba26ca66fb92b731a1af
[Snyk] Upgrade @ffmpeg/ffmpeg from 0.9.5 to 0.9.6
2020-12-19 21:32:10 +01:00
f71e0e9da9 Switched like/dislike positions
Switched like/dislike positions to match youtube musics layout
2020-12-16 20:30:01 +01:00
ba3779db07 Updated comment - touchbar plugin 2020-12-16 20:10:05 +01:00
d99aa0242f Added like and dislike buttons 2020-12-16 19:54:44 +01:00
3ea859de09 Added song image 2020-12-16 19:44:26 +01:00
7473677477 touchbar plugin - fixed code style 2020-12-16 15:57:24 +01:00
c3e2c13808 added initial touchbar support 2020-12-16 15:52:01 +01:00
5074ba2f48 fix: upgrade @ffmpeg/ffmpeg from 0.9.5 to 0.9.6
Snyk has created this PR to upgrade @ffmpeg/ffmpeg from 0.9.5 to 0.9.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
2020-12-16 02:20:20 +00:00
a8b8f1079f fix: upgrade @ffmpeg/core from 0.8.4 to 0.8.5
Snyk has created this PR to upgrade @ffmpeg/core from 0.8.4 to 0.8.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
2020-12-16 02:20:16 +00:00
e52987df92 Merge pull request #96 from th-ch/svg-readme
[Readme] Web folder for readme assets + new SVG animation
2020-12-13 16:40:55 +01:00
TC
01fc965170 Web folder for readme assets + new svg animation 2020-12-13 16:19:33 +01:00
ff71f29206 Merge pull request #94 from th-ch/linux-deb-rpm
Add new Linux targets (deb, freebsd, rpm)
2020-12-13 13:40:07 +01:00
TC
a47c5144ac Additional linux targets 2020-12-12 23:08:05 +01:00
TC
9b979b2273 Bump version 2020-12-12 22:28:38 +01:00
TC
81198192bb Add new linux targets (deb, rpm) 2020-12-12 22:28:30 +01:00
c03a08e76a Merge pull request #92 from th-ch/dependabot/npm_and_yarn/ini-1.3.7
Bump ini from 1.3.5 to 1.3.7
2020-12-12 11:26:49 +01:00
807e21eabf Bump ini from 1.3.5 to 1.3.7
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-11 18:36:02 +00:00
36f9d640df Merge pull request #90 from th-ch/adblocking
Fix adblocking
2020-12-11 19:35:10 +01:00
TC
39f30b143b Bump version to 1.7.4 2020-12-10 18:42:17 +01:00
TC
49497d0efb Bump adblocker dependency 2020-12-10 18:42:00 +01:00
TC
79c795927a Add uBlock Origin filters to default sources 2020-12-10 18:40:31 +01:00
TC
66c5ce46ca Fix adblocker preloading to inject scripts/styles 2020-12-10 18:39:58 +01:00
TC
22c7f70c93 Adblocker: add option to disable default lists 2020-12-08 21:40:03 +01:00
TC
467171a17e Bugfix: only use cache with no additional blocklists 2020-12-06 17:55:51 +01:00
3022facbea Readme: add youtube-music logo to badges 2020-12-05 12:33:26 +01:00
a67bf5ea43 Readme: add link to releases 2020-12-05 12:31:14 +01:00
3a1a3d4241 Merge pull request #82 from hbarsaiyan/master
Add AUR badge + beautify badges
2020-12-05 12:29:26 +01:00
9197f4a0e1 Add link to AUR badge
Co-authored-by: th-ch <th-ch@users.noreply.github.com>
2020-12-05 16:18:54 +05:30
d21220693b Add AUR tag + beautify tags 2020-12-05 08:07:17 +00:00
TC
e07cac2406 Bump version to 1.7.1 2020-12-03 22:30:15 +01:00
TC
fd97576611 Option to restart the app on config changes 2020-12-03 22:29:46 +01:00
6ab01056e0 Merge pull request #79 from th-ch/advanced-config
Refactor config, custom plugin options
2020-12-03 21:48:57 +01:00
TC
4dcbd2e7f0 Add plugin options in migration 2020-12-03 20:43:04 +01:00
TC
b94d0d4e8b Adblocker - advanced options (caching or not, additional lists) 2020-12-03 20:43:04 +01:00
TC
7b20b9339d Download plugin - advanced options (folder, FFmpeg args) 2020-12-03 20:43:04 +01:00
TC
4d4a1f038b Custom plugin options 2020-12-03 20:42:58 +01:00
TC
e5ec79e345 Watch changes in config and update menu 2020-12-03 18:31:01 +01:00
TC
f4fe5c2a58 Allow editing config (advanced) 2020-12-03 18:25:31 +01:00
TC
a5130c1d3f Move migrations into separate const 2020-12-03 18:25:08 +01:00
TC
8ab2da0482 Refactor config for simpler use and advanced options in plugins 2020-12-03 18:16:37 +01:00
TC
1b54b19f3f Add "build:linux" script 2020-12-02 22:22:05 +01:00
TC
be7e6e431f Bump version to 1.6.5 2020-12-02 22:14:43 +01:00
6e42b097f8 Merge pull request #77 from th-ch/disable-hardware-acceleration
Add option to disable hardware acceleration
2020-12-02 22:12:42 +01:00
TC
ef9cd8cd24 Add option to disable hardware acceleration 2020-12-02 22:07:15 +01:00
8f3e165917 Merge pull request #76 from th-ch/downloader-plugin-retry-and-upgrade-dep
Downloader plugin - retry and upgrade dependencies
2020-12-02 21:41:04 +01:00
TC
33a11efe9a Update ytdl-core to 4.1.1 2020-12-02 21:16:39 +01:00
TC
9a97436cd8 Allow up to 3 retries in downloader 2020-12-02 21:16:12 +01:00
f7935c0024 Merge pull request #70 from hbarsaiyan/master
Reflect Arch Linux package name change
2020-11-29 21:17:02 +01:00
2b33d4e857 Reflect Arch Linux package name change
Package name has been changed to youtube-music-bin.
2020-11-29 23:57:07 +05:30
ed16c35a57 Merge pull request #67 from th-ch/hide-menu
Option to hide menu
2020-11-28 18:45:53 +01:00
TC
ae5b85d8d7 Autoupdate modal: add download/disable updates buttons 2020-11-28 18:13:41 +01:00
47b4414eb3 Merge pull request #68 from hbarsaiyan/master
Add Arch Linux installation instructions
2020-11-28 11:09:07 +01:00
e329bb2201 Add Arch Linux installation instructions 2020-11-28 15:22:34 +05:30
TC
155ef9e5f5 Bump version 2020-11-27 21:39:33 +01:00
TC
4bac3ace18 Option to hide menu (win/linux) 2020-11-27 21:39:15 +01:00
1d2b53f6ee Merge pull request #64 from th-ch/ci
Improve CI
2020-11-24 00:32:07 +01:00
TC
0fd49330d3 CI: cache yarn directory 2020-11-24 00:13:01 +01:00
TC
4b0e79345f Fail fast (Can only re-run all jobs, not just one) 2020-11-24 00:13:01 +01:00
0c819e9aa9 Merge pull request #63 from th-ch/menu-visible
Ensure menu is visible on all platforms
2020-11-23 23:41:07 +01:00
20d591c554 Merge pull request #62 from th-ch/snyk-upgrade-407335ffa685010c89f68cbb1a8b27a6
[Snyk] Upgrade @cliqz/adblocker-electron from 1.18.3 to 1.18.4
2020-11-23 23:29:04 +01:00
TC
002469a98d Bump adblocker 2020-11-23 23:16:54 +01:00
TC
3bc8430201 No CI run on pull_request (already on every push) 2020-11-23 23:11:44 +01:00
TC
db447a5d62 Bump patch version 2020-11-23 23:04:16 +01:00
TC
72527d0522 Disable autoHideMenuBar (so menu bar is always visible) 2020-11-23 22:59:48 +01:00
TC
cf4827d780 Run CI on every push/PR 2020-11-23 22:55:20 +01:00
TC
9b02591767 Test that menu bar is visible 2020-11-23 22:52:00 +01:00
2b243f6dcb fix: upgrade @cliqz/adblocker-electron from 1.18.3 to 1.18.4
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.18.3 to 1.18.4.

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
2020-11-23 02:19:19 +00:00
TC
4d4aadfdfc Re-init download button in case of error 2020-11-22 10:23:20 +01:00
2937db3dde Update plugins list in readme 2020-11-22 10:20:03 +01:00
TC
e1166c06fa Do not cancel CI jobs if one fails 2020-11-22 10:10:31 +01:00
3ee0777628 Merge pull request #60 from th-ch/github-action
Add github action to build/release
2020-11-22 01:01:05 +01:00
3441a6f215 Link build badge to releases 2020-11-22 00:40:47 +01:00
235150a0cc Update build badge (GitHub Actions) 2020-11-22 00:34:49 +01:00
c1427c24d8 Merge pull request #59 from th-ch/node-12
Bump to node 12
2020-11-22 00:33:51 +01:00
TC
941dd90d77 Delete AppVeyor/Travis CI integration 2020-11-22 00:14:28 +01:00
TC
3da76020b1 Bump version (new release through GH actions) 2020-11-22 00:13:46 +01:00
TC
575dc5177d Use xvfb to run tests on linux 2020-11-22 00:04:08 +01:00
TC
27255dc477 Add GH token env var from secret 2020-11-21 23:53:36 +01:00
TC
fc4754a170 GH action to build/release 2020-11-21 23:38:04 +01:00
2e3a177f01 Merge pull request #59 from th-ch/node-12
Bump to node 12
2020-11-21 23:19:39 +01:00
TC
059f756d89 Bump to node 12 2020-11-21 22:54:43 +01:00
TC
e197087a50 Add downloader (video -> mp3) plugin (in music menu) 2020-11-21 22:50:33 +01:00
TC
e0f61f128e Bump version 2020-11-13 22:21:31 +01:00
TC
9ee7598375 Auto-hide menu bar 2020-11-13 22:16:11 +01:00
TC
fc48b920a8 Bump app version to 1.6.0 2020-11-11 12:22:36 +01:00
TC
9bc81da6f2 Plugins/event handlers in each window 2020-11-11 12:16:01 +01:00
TC
2b3363f5dc Bump YoutubeNonStop 2020-11-11 11:36:40 +01:00
TC
bcff6e5134 Add notifications plugin (notify of song on play event) 2020-11-11 11:35:58 +01:00
1dcf76b006 Merge pull request #54 from th-ch/snyk-upgrade-1b704161a30f8a56030e33c24f620f14
[Snyk] Upgrade electron-store from 6.0.0 to 6.0.1
2020-11-04 22:36:53 +01:00
7bdc0f42a2 fix: upgrade electron-store from 6.0.0 to 6.0.1
Snyk has created this PR to upgrade electron-store from 6.0.0 to 6.0.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
2020-10-27 02:19:22 +00:00
TC
023f5b6bc3 Block alert if leaving page when playing (onbeforeunload event) 2020-10-19 22:44:39 +02:00
TC
3e97e9307c Option to toggle devtools 2020-10-18 17:02:12 +02:00
TC
4299ba7865 Update version to 1.5.0 2020-10-04 18:27:07 +02:00
TC
855d8007a7 Add plugin to hide video player 2020-10-04 18:23:30 +02:00
TC
f239ec3232 Force yargs-parser version to fix vulnerability 2020-10-04 18:08:15 +02:00
TC
5f0dcbb3fc Bump electron to v10 (+ remove devtron, bump spectron) 2020-10-04 17:39:18 +02:00
TC
97dce5ad41 Bump dependencies 2020-10-04 17:20:43 +02:00
TC
1b3e4df1b2 Tests: get electron path using require 2020-10-04 17:18:33 +02:00
TC
8d74a0a9b5 Navigation plugin: fix arrow style 2020-10-04 15:06:52 +02:00
fbce109554 Merge pull request #45 from th-ch/dependabot/npm_and_yarn/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1
2020-10-04 14:18:24 +02:00
f4641acdbb Merge pull request #47 from th-ch/snyk-upgrade-468d0cfdab69d8231f03ed7e76703d57
[Snyk] Upgrade @cliqz/adblocker-electron from 1.17.0 to 1.18.0
2020-10-04 14:16:16 +02:00
02ae9f8187 Bump node-fetch from 2.6.0 to 2.6.1
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-04 12:15:45 +00:00
9f428415bb Merge pull request #40 from th-ch/snyk-upgrade-2d5f77a2ca49e9d611dfcac4a5d0c7e1
[Snyk] Upgrade electron-updater from 4.3.3 to 4.3.4
2020-10-04 14:14:57 +02:00
c7d3741b97 fix: upgrade @cliqz/adblocker-electron from 1.17.0 to 1.18.0
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.17.0 to 1.18.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
2020-09-15 02:19:07 +00:00
6404ec0919 fix: upgrade electron-updater from 4.3.3 to 4.3.4
Snyk has created this PR to upgrade electron-updater from 4.3.3 to 4.3.4.

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
2020-08-07 02:19:20 +00:00
4cd2d10cad Merge pull request #38 from th-ch/dependabot/npm_and_yarn/elliptic-6.5.3
Bump elliptic from 6.5.2 to 6.5.3
2020-08-03 10:44:10 +02:00
57f5d302d5 Merge pull request #37 from th-ch/snyk-upgrade-414f996ee5dc52155ccdd3d4b64505ac
[Snyk] Upgrade @cliqz/adblocker-electron from 1.16.0 to 1.16.1
2020-08-03 10:42:30 +02:00
168524ba50 Merge pull request #34 from th-ch/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-08-03 10:39:29 +02:00
149362f2a7 Bump elliptic from 6.5.2 to 6.5.3
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 20:48:22 +00:00
6c330046b7 fix: upgrade @cliqz/adblocker-electron from 1.16.0 to 1.16.1
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.16.0 to 1.16.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
2020-07-30 02:19:12 +00:00
884a2cc226 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 21:35:20 +00:00
3882118121 Merge pull request #32 from th-ch/start-at-login
Option to start at login
2020-07-13 11:25:32 +02:00
TC
0fd2e7f51c Bump version to 1.4.0 2020-07-12 21:14:41 +02:00
TC
408aa9bb59 Option to start at login 2020-07-12 21:14:41 +02:00
eec6993b95 Merge pull request #31 from th-ch/dependabot/npm_and_yarn/electron-8.2.4
Bump electron from 8.2.1 to 8.2.4
2020-07-12 21:13:56 +02:00
eb8e0a37d6 Bump electron from 8.2.1 to 8.2.4
Bumps [electron](https://github.com/electron/electron) from 8.2.1 to 8.2.4.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v8.2.1...v8.2.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-07 02:01:10 +00:00
TC
2a915f4fae Fix electron deprecation warnings 2020-07-05 15:57:25 +02:00
TC
26994000d7 readme: yarn run build -> `yarn build 2020-07-05 15:44:26 +02:00
78f143ea10 Merge pull request #30 from th-ch/snyk-upgrade-3427638aafd84464f9f38998c7e3bc6f
[Snyk] Upgrade electron-store from 5.1.1 to 5.2.0
2020-07-05 15:43:02 +02:00
684a369fed fix: upgrade electron-store from 5.1.1 to 5.2.0
Snyk has created this PR to upgrade electron-store from 5.1.1 to 5.2.0.

See this package in NPM:
https://www.npmjs.com/package/electron-store

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
2020-06-28 02:19:06 +00:00
b19470ad0b Merge pull request #29 from th-ch/snyk-upgrade-a4c19e5e6b1b92c533cd6d3ed8538fa2
[Snyk] Upgrade @cliqz/adblocker-electron from 1.14.4 to 1.15.0
2020-06-14 22:40:24 +02:00
6691cbf8c0 Merge pull request #28 from th-ch/snyk-upgrade-54c1400dcbff5fcbde1071153f1be456
[Snyk] Upgrade electron-debug from 3.0.1 to 3.1.0
2020-06-14 22:39:18 +02:00
7ac98cf024 fix: upgrade @cliqz/adblocker-electron from 1.14.4 to 1.15.0
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.14.4 to 1.15.0.

See this package in NPM:
https://www.npmjs.com/package/@cliqz/adblocker-electron

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
2020-06-14 02:19:04 +00:00
3d835fbeaa fix: upgrade electron-debug from 3.0.1 to 3.1.0
Snyk has created this PR to upgrade electron-debug from 3.0.1 to 3.1.0.

See this package in NPM:
https://www.npmjs.com/package/electron-debug

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
2020-06-13 02:19:27 +00:00
d0824f52ea Merge pull request #27 from th-ch/snyk-upgrade-9a04364d99d1aed92239f3dd981f3d67
[Snyk] Upgrade electron-updater from 4.3.1 to 4.3.2
2020-06-05 22:48:14 +02:00
8c945100e2 fix: upgrade electron-updater from 4.3.1 to 4.3.2
Snyk has created this PR to upgrade electron-updater from 4.3.1 to 4.3.2.

See this package in NPM:
https://www.npmjs.com/package/electron-updater

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
2020-06-05 02:19:01 +00:00
68801c8e2b Merge pull request #26 from th-ch/snyk-upgrade-107ef53d6b16bb061dc783179b8991a7
[Snyk] Upgrade electron-updater from 4.3.0 to 4.3.1
2020-05-29 21:32:29 +02:00
a06eece4a0 fix: upgrade electron-updater from 4.3.0 to 4.3.1
Snyk has created this PR to upgrade electron-updater from 4.3.0 to 4.3.1.

See this package in NPM:
https://www.npmjs.com/package/electron-updater

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
2020-05-19 04:19:35 +02:00
873e093ff0 fix: upgrade electron-updater from 4.3.0 to 4.3.1
Snyk has created this PR to upgrade electron-updater from 4.3.0 to 4.3.1.

See this package in NPM:
https://www.npmjs.com/package/electron-updater

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
2020-05-19 04:19:34 +02:00
9c343d58ef Merge pull request #25 from th-ch/snyk-upgrade-86a869f28fc1113ae78c2dcd1eea4b90
[Snyk] Upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2
2020-05-14 22:14:50 +02:00
fed7a1df17 fix: upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2.

See this package in NPM:
https://www.npmjs.com/package/@cliqz/adblocker-electron

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
2020-05-14 04:19:22 +02:00
89d8907bc6 fix: upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.14.1 to 1.14.2.

See this package in NPM:
https://www.npmjs.com/package/@cliqz/adblocker-electron

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
2020-05-14 04:19:21 +02:00
08d145b6fc Merge pull request #24 from th-ch/tests
[Tests] Add integration tests
2020-05-03 13:35:41 +02:00
TC
57d3545701 Launch tests in CI 2020-05-03 12:58:28 +02:00
TC
08710558f5 Add tests section to readme 2020-05-03 11:44:23 +02:00
TC
b80007bbd5 Tests: only stop if running (in case of error when launching app) 2020-05-03 11:44:00 +02:00
TC
ab0c93d443 Test script in package.json 2020-05-01 18:59:00 +02:00
TC
e84f045201 Add simple test to ensure app launches properly 2020-05-01 18:58:08 +02:00
TC
692b6b22aa Enable nodeIntegration in test env (required by Spectron) 2020-05-01 18:34:23 +02:00
TC
954a58bcf5 Add util to detect test env 2020-05-01 18:34:01 +02:00
TC
bce5b7d8eb Add jest config and test environment to launch app 2020-05-01 18:33:43 +02:00
TC
736a706801 Add jest, spectron and getPort util for tests 2020-05-01 18:33:02 +02:00
47 changed files with 5783 additions and 1413 deletions

62
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Build YouTube Music
on:
- push
jobs:
build:
name: Build YouTube Music
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- name: Setup NodeJS
uses: actions/setup-node@v1
with:
node-version: "12.x"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Test
uses: GabrielBB/xvfb-action@v1
with:
run: yarn test
- name: Build on Mac
if: startsWith(matrix.os, 'macOS')
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
yarn run release:mac
- name: Build on Linux
if: startsWith(matrix.os, 'ubuntu')
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
yarn run release:linux
- name: Build on Windows
if: startsWith(matrix.os, 'windows')
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
yarn run release:win

View File

@ -1,35 +0,0 @@
language: node_js
node_js: "10"
env:
- ELECTRON_CACHE=$HOME/.cache/electron ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
jobs:
include:
- os: osx
osx_image: xcode11.3
- os: linux
dist: xenial
cache:
yarn: false
directories:
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
script:
- |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
yarn run release:linux
else
yarn run release:mac
fi
before_cache:
- rm -rf $HOME/.cache/electron-builder
before_install:
- rm -rf node_modules
# Install dependencies
- travis_wait 30 yarn --frozen-lockfile
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"

View File

@ -1,27 +0,0 @@
image: Visual Studio 2019
platform:
- x64
cache:
- node_modules
- '%USERPROFILE%\.electron'
init:
- git config --global core.autocrlf input
install:
# Install node
- ps: Install-Product node 10 x64
# Install dependencies
- yarn --frozen-lockfile
# on_finish:
# # Enable RDP to the build worker (using APPVEYOR_RDP_PASSWORD env var)
# # https://www.appveyor.com/docs/how-to/rdp-to-build-worker/
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
build_script:
- yarn run release:win
test: off

39
config/defaults.js Normal file
View File

@ -0,0 +1,39 @@
const defaultConfig = {
"window-size": {
width: 1100,
height: 550,
},
url: "https://music.youtube.com",
options: {
tray: false,
appVisible: true,
autoUpdates: true,
hideMenu: false,
startAtLogin: false,
disableHardwareAcceleration: false,
restartOnConfigChanges: false,
trayClickPlayPause: false,
},
plugins: {
// Enabled plugins
navigation: {
enabled: true,
},
shortcuts: {
enabled: true,
},
adblocker: {
enabled: true,
cache: true,
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
},
// Disabled plugins
downloader: {
enabled: false,
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined, // Custom download folder (absolute path)
},
},
};
module.exports = defaultConfig;

21
config/index.js Normal file
View File

@ -0,0 +1,21 @@
const plugins = require("./plugins");
const store = require("./store");
const set = (key, value) => {
store.set(key, value);
};
const get = (key) => {
return store.get(key);
};
module.exports = {
get,
set,
edit: () => store.openInEditor(),
watch: (cb) => {
store.onDidChange("options", cb);
store.onDidChange("plugins", cb);
},
plugins,
};

41
config/plugins.js Normal file
View File

@ -0,0 +1,41 @@
const store = require("./store");
function getEnabled() {
const plugins = store.get("plugins");
const enabledPlugins = Object.entries(plugins).filter(([plugin, options]) =>
isEnabled(plugin)
);
return enabledPlugins;
}
function isEnabled(plugin) {
const pluginConfig = store.get("plugins")[plugin];
return pluginConfig !== undefined && pluginConfig.enabled;
}
function setOptions(plugin, options) {
const plugins = store.get("plugins");
store.set("plugins", {
...plugins,
[plugin]: {
...plugins[plugin],
...options,
},
});
}
function enable(plugin) {
setOptions(plugin, { enabled: true });
}
function disable(plugin) {
setOptions(plugin, { enabled: false });
}
module.exports = {
isEnabled,
getEnabled,
enable,
disable,
setOptions,
};

40
config/store.js Normal file
View File

@ -0,0 +1,40 @@
const Store = require("electron-store");
const defaults = require("./defaults");
const migrations = {
">=1.7.0": (store) => {
const enabledPlugins = store.get("plugins");
if (!Array.isArray(enabledPlugins)) {
console.warn("Plugins are not in array format, cannot migrate");
return;
}
// Include custom options
const plugins = {
adblocker: {
enabled: true,
cache: true,
additionalBlockLists: [],
},
downloader: {
enabled: false,
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined, // Custom download folder (absolute path)
},
};
enabledPlugins.forEach((enabledPlugin) => {
plugins[enabledPlugin] = {
...plugins[enabledPlugin],
enabled: true,
};
});
store.set("plugins", plugins);
},
};
module.exports = new Store({
defaults,
clearInvalidConfig: false,
migrations,
});

191
index.js
View File

@ -5,18 +5,25 @@ const electron = require("electron");
const is = require("electron-is");
const { autoUpdater } = require("electron-updater");
const config = require("./config");
const { setApplicationMenu } = require("./menu");
const {
autoUpdate,
getEnabledPlugins,
isAppVisible,
isTrayEnabled,
store,
} = require("./store");
const { fileExists, injectCSS } = require("./plugins/utils");
const { isTesting } = require("./utils/testing");
const { setUpTray } = require("./tray");
const app = electron.app;
app.commandLine.appendSwitch(
"js-flags",
// WebAssembly flags
"--experimental-wasm-threads --experimental-wasm-bulk-memory"
);
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
if (config.get("options.disableHardwareAcceleration")) {
if (is.dev()) {
console.log("Disabling hardware acceleration");
}
app.disableHardwareAcceleration();
}
// Adds debug features like hotkeys for triggering dev tools and reload
require("electron-debug")();
@ -38,32 +45,7 @@ function onClosed() {
mainWindow = null;
}
function createMainWindow() {
const windowSize = store.get("window-size");
const windowMaximized = store.get("window-maximized");
const win = new electron.BrowserWindow({
icon: icon,
width: windowSize.width,
height: windowSize.height,
backgroundColor: "#000",
show: false,
webPreferences: {
nodeIntegration: false,
preload: path.join(__dirname, "preload.js"),
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
affinity: "main-window", // main window, and addition windows should work in one process
},
frame: !is.macOS(),
titleBarStyle: is.macOS() ? "hiddenInset" : "default",
});
if (windowMaximized) {
win.maximize();
}
win.webContents.loadURL(store.get("url"));
win.on("closed", onClosed);
function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
win.webContents.on("did-finish-load", () => {
if (is.dev()) {
@ -72,14 +54,82 @@ function createMainWindow() {
}
});
getEnabledPlugins().forEach((plugin) => {
config.plugins.getEnabled().forEach(([plugin, options]) => {
console.log("Loaded plugin - " + plugin);
const pluginPath = path.join(__dirname, "plugins", plugin, "back.js");
fileExists(pluginPath, () => {
const handle = require(pluginPath);
handle(win);
handle(win, options);
});
});
}
function createMainWindow() {
const windowSize = config.get("window-size");
const windowMaximized = config.get("window-maximized");
const win = new electron.BrowserWindow({
icon: icon,
width: windowSize.width,
height: windowSize.height,
backgroundColor: "#000",
show: false,
webPreferences: {
// TODO: re-enable contextIsolation once it can work with ffmepg.wasm
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
contextIsolation: false,
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()
? {
// Only necessary when testing with Spectron
contextIsolation: false,
nodeIntegration: true,
}
: undefined),
},
frame: !is.macOS(),
titleBarStyle: is.macOS() ? "hiddenInset" : "default",
autoHideMenuBar: config.get("options.hideMenu"),
});
if (windowMaximized) {
win.maximize();
}
win.webContents.loadURL(config.get("url"));
win.on("closed", onClosed);
win.on("move", () => {
let position = win.getPosition();
config.set("window-position", { x: position[0], y: position[1] });
});
win.on("resize", () => {
const windowSize = win.getSize();
config.set("window-maximized", win.isMaximized());
if (!win.isMaximized()) {
config.set("window-size", {
width: windowSize[0],
height: windowSize[1],
});
}
});
win.once("ready-to-show", () => {
if (config.get("options.appVisible")) {
win.show();
}
});
return win;
}
app.on("browser-window-created", (event, win) => {
loadPlugins(win);
win.webContents.on("did-fail-load", () => {
if (is.dev()) {
@ -88,10 +138,14 @@ function createMainWindow() {
win.webContents.loadFile(path.join(__dirname, "error.html"));
});
win.webContents.on("will-prevent-unload", (event) => {
event.preventDefault();
});
win.webContents.on("did-navigate-in-page", () => {
const url = win.webContents.getURL();
if (url.startsWith("https://music.youtube.com")) {
store.set("url", url);
config.set("url", url);
}
});
@ -120,29 +174,7 @@ function createMainWindow() {
options.webPreferences.affinity = "main-window";
}
);
win.on("move", () => {
let position = win.getPosition();
store.set("window-position", { x: position[0], y: position[1] });
});
win.on("resize", () => {
const windowSize = win.getSize();
store.set("window-maximized", win.isMaximized());
if (!win.isMaximized()) {
store.set("window-size", { width: windowSize[0], height: windowSize[1] });
}
});
win.once("ready-to-show", () => {
if (isAppVisible()) {
win.show();
}
});
return win;
}
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
@ -164,28 +196,53 @@ app.on("activate", () => {
});
app.on("ready", () => {
setApplicationMenu();
mainWindow = createMainWindow();
setApplicationMenu(mainWindow);
if (config.get("options.restartOnConfigChanges")) {
config.watch(() => {
app.relaunch();
app.exit();
});
}
setUpTray(app, mainWindow);
if (!is.dev() && autoUpdate()) {
// Autostart at login
app.setLoginItemSettings({
openAtLogin: config.get("options.startAtLogin"),
});
if (!is.dev() && config.get("options.autoUpdates")) {
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on("update-available", () => {
const downloadLink =
"https://github.com/th-ch/youtube-music/releases/latest";
const dialogOpts = {
type: "info",
buttons: ["OK"],
buttons: ["OK", "Download", "Disable updates"],
title: "Application Update",
message: "A new version is available",
detail:
"A new version is available and can be downloaded at https://github.com/th-ch/youtube-music/releases/latest",
detail: `A new version is available and can be downloaded at ${downloadLink}`,
};
electron.dialog.showMessageBox(dialogOpts);
electron.dialog.showMessageBox(dialogOpts).then((dialogOutput) => {
switch (dialogOutput.response) {
// Download
case 1:
electron.shell.openExternal(downloadLink);
break;
// Disable updates
case 2:
config.set("options.autoUpdates", false);
break;
default:
break;
}
});
});
}
// Optimized for Mac OS X
if (is.macOS()) {
if (!isAppVisible()) {
if (!config.get("options.appVisible")) {
app.dock.hide();
}
}
@ -195,7 +252,7 @@ app.on("ready", () => {
forceQuit = true;
});
if (is.macOS() || isTrayEnabled()) {
if (is.macOS() || config.get("options.tray")) {
mainWindow.on("close", (event) => {
// Hide the window instead of quitting (quit is available in tray options)
if (!forceQuit) {

7
jest.config.js Normal file
View File

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

152
menu.js
View File

@ -1,33 +1,35 @@
const { app, Menu } = require("electron");
const is = require("electron-is");
const { getAllPlugins } = require("./plugins/utils");
const {
isPluginEnabled,
enablePlugin,
disablePlugin,
autoUpdate,
isAppVisible,
isTrayEnabled,
setOptions,
} = require("./store");
const config = require("./config");
const mainMenuTemplate = [
const mainMenuTemplate = (win) => [
{
label: "Plugins",
submenu: getAllPlugins().map((plugin) => {
return {
label: plugin,
type: "checkbox",
checked: isPluginEnabled(plugin),
click: (item) => {
if (item.checked) {
enablePlugin(plugin);
} else {
disablePlugin(plugin);
}
submenu: [
...getAllPlugins().map((plugin) => {
return {
label: plugin,
type: "checkbox",
checked: config.plugins.isEnabled(plugin),
click: (item) => {
if (item.checked) {
config.plugins.enable(plugin);
} else {
config.plugins.disable(plugin);
}
},
};
}),
{ type: "separator" },
{
label: "Advanced options",
click: () => {
config.edit();
},
};
}),
},
],
},
{
label: "Options",
@ -35,43 +37,125 @@ const mainMenuTemplate = [
{
label: "Auto-update",
type: "checkbox",
checked: autoUpdate(),
checked: config.get("options.autoUpdates"),
click: (item) => {
setOptions({ autoUpdates: item.checked });
config.set("options.autoUpdates", item.checked);
},
},
{
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);
},
},
...(is.windows() || is.linux()
? [
{
label: "Hide menu",
type: "checkbox",
checked: config.get("options.hideMenu"),
click: (item) => {
config.set("options.hideMenu", 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: "Tray",
submenu: [
{
label: "Disabled",
type: "radio",
checked: !isTrayEnabled(),
click: () => setOptions({ tray: false, appVisible: true }),
checked: !config.get("options.tray"),
click: () => {
config.set("options.tray", false);
config.set("options.appVisible", true);
},
},
{
label: "Enabled + app visible",
type: "radio",
checked: isTrayEnabled() && isAppVisible(),
click: () => setOptions({ tray: true, appVisible: true }),
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: isTrayEnabled() && !isAppVisible(),
click: () => setOptions({ tray: true, appVisible: false }),
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: "Toggle DevTools",
// Cannot use "toggleDevTools" role in MacOS
click: () => {
const { webContents } = win;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
const devToolsOptions = {};
webContents.openDevTools(devToolsOptions);
}
},
},
{
label: "Advanced options",
click: () => {
config.edit();
},
},
],
},
];
module.exports.mainMenuTemplate = mainMenuTemplate;
module.exports.setApplicationMenu = () => {
const menuTemplate = [...mainMenuTemplate];
module.exports.setApplicationMenu = (win) => {
const menuTemplate = [...mainMenuTemplate(win)];
if (process.platform === "darwin") {
const name = app.getName();
const name = app.name;
menuTemplate.unshift({
label: name,
submenu: [

View File

@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
"version": "1.3.3",
"version": "1.9.0",
"description": "YouTube Music Desktop App - including custom plugins",
"license": "MIT",
"repository": "th-ch/youtube-music",
@ -18,21 +18,30 @@
"icon": "assets/generated/icons/mac/icon.icns"
},
"win": {
"icon": "assets/generated/icons/win/icon.ico"
"icon": "assets/generated/icons/win/icon.ico",
"target": ["nsis", "portable"]
},
"linux": {
"icon": "assets/generated/icons/png",
"category": "AudioVideo"
"category": "AudioVideo",
"target": [
"AppImage",
"snap",
"freebsd",
"deb",
"rpm"
]
}
},
"scripts": {
"test": "xo",
"test": "jest",
"start": "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",
"clean": "rimraf dist",
"build": "yarn run clean && electron-builder --win --mac --linux",
"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",
@ -43,26 +52,38 @@
"release:win": "yarn run clean && electron-builder --win -p always"
},
"engines": {
"node": ">=12.16.1",
"npm": "Please use yarn and not npm"
},
"dependencies": {
"@cliqz/adblocker-electron": "^1.14.1",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.7.1",
"electron-debug": "^3.0.1",
"@cliqz/adblocker-electron": "^1.19.0",
"@ffmpeg/core": "^0.8.5",
"@ffmpeg/ffmpeg": "^0.9.6",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
"discord-rpc": "^3.1.4",
"downloads-folder": "^3.0.1",
"electron-debug": "^3.2.0",
"electron-is": "^3.0.0",
"electron-localshortcut": "^3.2.1",
"electron-store": "^5.1.1",
"electron-updater": "^4.3.0",
"node-fetch": "^2.6.0"
"electron-store": "^6.0.1",
"electron-updater": "^4.3.6",
"filenamify": "^4.2.0",
"node-fetch": "^2.6.1",
"ytdl-core": "^4.1.2"
},
"devDependencies": {
"devtron": "^1.4.0",
"electron": "^8.2.1",
"electron-builder": "^22.4.1",
"electron-devtools-installer": "^3.0.0",
"electron-icon-maker": "0.0.4",
"electron": "^11.1.1",
"electron-builder": "^22.8.1",
"electron-devtools-installer": "^3.1.1",
"electron-icon-maker": "0.0.5",
"get-port": "^5.1.1",
"jest": "^26.4.2",
"rimraf": "^3.0.2",
"xo": "^0.29.0"
"spectron": "^13.0.0",
"xo": "^0.33.1"
},
"resolutions": {
"yargs-parser": "18.1.3"
},
"xo": {
"envs": [

View File

@ -1,2 +1,8 @@
const { loadAdBlockerEngine } = require("./blocker");
module.exports = (win) => loadAdBlockerEngine(win.webContents.session);
module.exports = (win, options) =>
loadAdBlockerEngine(
win.webContents.session,
options.cache,
options.additionalBlockLists,
options.disableDefaultLists
);

View File

@ -6,19 +6,32 @@ const fetch = require("node-fetch");
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",
];
const loadAdBlockerEngine = (session = undefined) =>
ElectronBlocker.fromLists(
fetch,
SOURCES,
{},
{
path: path.resolve(__dirname, "ad-blocker-engine.bin"),
read: promises.readFile,
write: promises.writeFile,
}
)
const loadAdBlockerEngine = (
session = undefined,
cache = true,
additionalBlockLists = [],
disableDefaultLists = false
) => {
// Only use cache if no additional blocklists are passed
const cachingOptions =
cache && additionalBlockLists.length === 0
? {
path: path.resolve(__dirname, "ad-blocker-engine.bin"),
read: promises.readFile,
write: promises.writeFile,
}
: undefined;
const lists = [
...(disableDefaultLists ? [] : SOURCES),
...additionalBlockLists,
];
ElectronBlocker.fromLists(fetch, lists, {}, cachingOptions)
.then((blocker) => {
if (session) {
blocker.enableBlockingInSession(session);
@ -27,6 +40,7 @@ const loadAdBlockerEngine = (session = undefined) =>
}
})
.catch((err) => console.log("Error loading adBlocker engine", err));
};
module.exports = { loadAdBlockerEngine };
if (require.main === module) {

View File

@ -0,0 +1,4 @@
module.exports = () => {
// Preload adblocker to inject scripts/styles
require("@cliqz/adblocker-electron-preload/dist/preload.cjs");
};

51
plugins/discord/back.js Normal file
View File

@ -0,0 +1,51 @@
const Discord = require("discord-rpc");
const getSongInfo = require("../../providers/song-info");
const rpc = new Discord.Client({
transport: "ipc",
});
// Application ID registered by @semvis123
const clientId = "790655993809338398";
module.exports = (win) => {
const registerCallback = getSongInfo(win);
// If the page is ready, register the callback
win.on("ready-to-show", () => {
rpc.on("ready", () => {
// Register the callback
registerCallback((songInfo) => {
// Song information changed, so lets update the rich presence
const activityInfo = {
details: songInfo.title,
state: songInfo.artist,
largeImageKey: "logo",
largeImageText: songInfo.views + " - " + songInfo.likes,
};
if (songInfo.isPaused) {
// Add an idle icon to show that the song is paused
activityInfo.smallImageKey = "idle";
activityInfo.smallImageText = "idle/paused";
} 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;
}
rpc.setActivity(activityInfo);
});
});
// Startup the rpc client
rpc
.login({
clientId,
})
.catch(console.error);
});
};

View File

@ -0,0 +1,10 @@
const CHANNEL = "downloader";
const ACTIONS = {
ERROR: "error",
METADATA: "metadata",
};
module.exports = {
CHANNEL: CHANNEL,
ACTIONS: ACTIONS,
};

View File

@ -0,0 +1,46 @@
const { join } = require("path");
const { dialog } = require("electron");
const getSongInfo = require("../../providers/song-info");
const { injectCSS, listenAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const sendError = (win, err) => {
const dialogOpts = {
type: "info",
buttons: ["OK"],
title: "Error in download!",
message: "Argh! Apologies, download failed…",
detail: err.toString(),
};
dialog.showMessageBox(dialogOpts);
};
let metadata = {};
function handle(win) {
injectCSS(win.webContents, join(__dirname, "style.css"));
const registerCallback = getSongInfo(win);
registerCallback((info) => {
metadata = {
...info,
image: info.image ? info.image.toDataURL() : undefined,
};
});
listenAction(CHANNEL, (event, action, error) => {
switch (action) {
case ACTIONS.ERROR:
sendError(win, error);
break;
case ACTIONS.METADATA:
event.returnValue = JSON.stringify(metadata);
break;
default:
console.log("Unknown action: " + action);
}
});
}
module.exports = handle;

View File

@ -0,0 +1,66 @@
const { contextBridge } = require("electron");
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { downloadVideoToMP3 } = require("./youtube-dl");
let menu = null;
let progress = null;
const downloadButton = ElementFromFile(
templatePath(__dirname, "download.html")
);
let pluginOptions = {};
const observer = new MutationObserver((mutations, observer) => {
if (!menu) {
menu = document.querySelector("ytmusic-menu-popup-renderer paper-listbox");
}
if (menu && !menu.contains(downloadButton)) {
menu.prepend(downloadButton);
progress = document.querySelector("#ytmcustom-download");
}
});
const reinit = () => {
if (!progress) {
console.warn("Cannot update progress");
} else {
progress.innerHTML = "Download";
}
};
// TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld("downloader", {
// download: () => {
global.download = () => {
const videoUrl = window.location.href;
downloadVideoToMP3(
videoUrl,
(feedback) => {
if (!progress) {
console.warn("Cannot update progress");
} else {
progress.innerHTML = feedback;
}
},
(error) => {
triggerAction(CHANNEL, ACTIONS.ERROR, error);
reinit();
},
reinit,
pluginOptions
);
};
// });
function observeMenu(options) {
pluginOptions = { ...pluginOptions, ...options };
observer.observe(document, {
childList: true,
subtree: true,
});
}
module.exports = observeMenu;

View File

@ -0,0 +1,13 @@
.menu-item {
display: var(--ytmusic-menu-item_-_display);
height: var(--ytmusic-menu-item_-_height);
align-items: var(--ytmusic-menu-item_-_align-items);
padding: var(--ytmusic-menu-item_-_padding);
cursor: pointer;
}
.menu-icon {
flex: var(--ytmusic-menu-item-icon_-_flex);
margin: var(--ytmusic-menu-item-icon_-_margin);
fill: var(--ytmusic-menu-item-icon_-_fill);
}

View File

@ -0,0 +1,37 @@
<div
class="menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
aria-disabled="false"
aria-selected="false"
onclick="download()"
>
<div
class="menu-icon yt-icon-container yt-icon ytmusic-toggle-menu-service-item-renderer"
>
<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"
/>
<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"
/>
</g>
</svg>
</div>
<div
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
id="ytmcustom-download"
>
Download
</div>
</div>

View File

@ -0,0 +1,135 @@
const { randomBytes } = require("crypto");
const { writeFileSync } = require("fs");
const { join } = require("path");
const downloadsFolder = require("downloads-folder");
const is = require("electron-is");
const filenamify = require("filenamify");
// Browser version of FFmpeg (in renderer process) instead of loading @ffmpeg/ffmpeg
// because --js-flags cannot be passed in the main process when the app is packaged
// See https://github.com/electron/electron/issues/22705
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
const ytdl = require("ytdl-core");
const { triggerActionSync } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
log: false,
logger: () => {}, // console.log,
progress: () => {}, // console.log,
});
const downloadVideoToMP3 = (
videoUrl,
sendFeedback,
sendError,
reinit,
options
) => {
sendFeedback("Downloading…");
let videoName = "YouTube Music - Unknown title";
let videoReadableStream;
try {
videoReadableStream = ytdl(videoUrl, {
filter: "audioonly",
quality: "highestaudio",
highWaterMark: 32 * 1024 * 1024, // 32 MB
requestOptions: { maxRetries: 3 },
});
} catch (err) {
sendError(err);
return;
}
const chunks = [];
videoReadableStream
.on("data", (chunk) => {
chunks.push(chunk);
})
.on("progress", (chunkLength, downloaded, total) => {
const progress = Math.floor((downloaded / total) * 100);
sendFeedback("Download: " + progress + "%");
})
.on("info", (info, format) => {
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
if (is.dev()) {
console.log(
"Downloading video - name:",
videoName,
"- quality:",
format.audioBitrate + "kbits/s"
);
}
})
.on("error", sendError)
.on("end", () => {
const buffer = Buffer.concat(chunks);
toMP3(videoName, buffer, sendFeedback, sendError, reinit, options);
});
};
const toMP3 = async (
videoName,
buffer,
sendFeedback,
sendError,
reinit,
options
) => {
const safeVideoName = randomBytes(32).toString("hex");
const extension = options.extension || "mp3";
try {
if (!ffmpeg.isLoaded()) {
sendFeedback("Loading…");
await ffmpeg.load();
}
sendFeedback("Preparing file…");
ffmpeg.FS("writeFile", safeVideoName, buffer);
sendFeedback("Converting…");
await ffmpeg.run(
"-i",
safeVideoName,
...getFFmpegMetadataArgs(),
...(options.ffmpegArgs || []),
safeVideoName + "." + extension
);
const folder = options.downloadFolder || downloadsFolder();
const filename = filenamify(videoName + "." + extension, {
replacement: "_",
});
writeFileSync(
join(folder, filename),
ffmpeg.FS("readFile", safeVideoName + "." + extension)
);
reinit();
} catch (e) {
sendError(e);
}
};
const getFFmpegMetadataArgs = () => {
const metadata = JSON.parse(triggerActionSync(CHANNEL, ACTIONS.METADATA));
if (!metadata) {
return;
}
return [
"-metadata",
`title=${metadata.title}`,
"-metadata",
`artist=${metadata.artist}`,
];
};
module.exports = {
downloadVideoToMP3,
};

View File

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

View File

@ -0,0 +1,11 @@
/* Hide the video player */
#main-panel {
display: none !important;
}
/* Make the side-panel full width */
.side-panel.ytmusic-player-page {
max-width: 100% !important;
width: 100% !important;
margin: 0 !important;
}

View File

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

View File

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

View File

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

View File

@ -4,26 +4,6 @@
role="tab"
onclick="goToPreviousPage()"
>
<div
class="tab-icon style-scope ytmusic-pivot-bar-item-renderer yt-icon-container"
>
<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
class="st0"
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
<div
class="search-icon style-scope ytmusic-search-box"
role="button"
@ -31,7 +11,10 @@
aria-disabled="false"
title="Go to previous page"
>
<div id="icon" class="style-scope paper-icon-button navigation-icon">
<div
id="icon"
class="tab-icon style-scope paper-icon-button navigation-icon"
>
<svg
viewBox="0 0 492 492"
preserveAspectRatio="xMidYMid meet"
@ -41,7 +24,6 @@
>
<g class="style-scope iron-icon">
<path
class="st0"
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
></path>
</g>

View File

@ -4,26 +4,6 @@
role="tab"
onclick="goToNextPage()"
>
<div class="tab-icon style-scope ytmusic-pivot-bar-item-renderer">
<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="M382.678,226.804L163.73,7.86C158.666,2.792,151.906,0,144.698,0s-13.968,2.792-19.032,7.86l-16.124,16.12
c-10.492,10.504-10.492,27.576,0,38.064L293.398,245.9l-184.06,184.06c-5.064,5.068-7.86,11.824-7.86,19.028
c0,7.212,2.796,13.968,7.86,19.04l16.124,16.116c5.068,5.068,11.824,7.86,19.032,7.86s13.968-2.792,19.032-7.86L382.678,265
c5.076-5.084,7.864-11.872,7.848-19.088C390.542,238.668,387.754,231.884,382.678,226.804z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
<div
class="search-icon style-scope ytmusic-search-box"
role="button"
@ -31,7 +11,10 @@
aria-disabled="false"
title="Go to next page"
>
<div id="icon" class="style-scope paper-icon-button navigation-icon">
<div
id="icon"
class="tab-icon style-scope paper-icon-button navigation-icon"
>
<svg
viewBox="0 0 492 492"
preserveAspectRatio="xMidYMid meet"
@ -41,7 +24,6 @@
>
<g class="style-scope iron-icon">
<path
class="st0"
d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1
l183.9,183.9L109.3,430c-5.1,5.1-7.9,11.8-7.9,19c0,7.2,2.8,14,7.9,19l16.1,16.1c5.1,5.1,11.8,7.9,19,7.9s14-2.8,19-7.9L382.7,265
c5.1-5.1,7.9-11.9,7.8-19.1C390.5,238.7,387.8,231.9,382.7,226.8z"

View File

@ -0,0 +1,35 @@
const { Notification } = require("electron");
const getSongInfo = require("../../providers/song-info");
const notify = (info) => {
let notificationImage = "assets/youtube-music.png";
if (info.image) {
notificationImage = info.image.resize({ height: 256, width: 256 });
}
// Fill the notification with content
const notification = {
title: info.title || "Playing",
body: info.artist,
icon: notificationImage,
silent: true,
};
// Send the notification
new Notification(notification).show();
};
module.exports = (win) => {
const registerCallback = getSongInfo(win);
win.on("ready-to-show", () => {
// Register the callback for new song information
registerCallback((songInfo) => {
// If song is playing send notification
if (!songInfo.isPaused) {
notify(songInfo);
}
});
});
};

View File

@ -1,12 +1,7 @@
const { globalShortcut } = require("electron");
const electronLocalshortcut = require("electron-localshortcut");
const {
playPause,
nextTrack,
previousTrack,
startSearch
} = require("./youtube.js");
const getSongControls = require("../../providers/song-controls");
function _registerGlobalShortcut(webContents, shortcut, action) {
globalShortcut.register(shortcut, () => {
@ -21,11 +16,13 @@ function _registerLocalShortcut(win, shortcut, action) {
}
function registerShortcuts(win) {
const { playPause, next, previous, search } = getSongControls(win);
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
_registerGlobalShortcut(win.webContents, "MediaNextTrack", nextTrack);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previousTrack);
_registerLocalShortcut(win, "CommandOrControl+F", startSearch);
_registerLocalShortcut(win, "CommandOrControl+L", startSearch);
_registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
_registerLocalShortcut(win, "CommandOrControl+F", search);
_registerLocalShortcut(win, "CommandOrControl+L", search);
}
module.exports = registerShortcuts;

View File

@ -1,29 +0,0 @@
function _keyboardInput(webContents, key) {
return webContents.sendInputEvent({
type : "keydown",
keyCode: key
});
}
function playPause(webContents) {
return _keyboardInput(webContents, "Space");
}
function nextTrack(webContents) {
return _keyboardInput(webContents, "j");
}
function previousTrack(webContents) {
return _keyboardInput(webContents, "k");
}
function startSearch(webContents) {
return _keyboardInput(webContents, "/");
}
module.exports = {
playPause : playPause,
nextTrack : nextTrack,
previousTrack: previousTrack,
startSearch : startSearch
};

87
plugins/touchbar/back.js Normal file
View File

@ -0,0 +1,87 @@
const { TouchBar } = require("electron");
const {
TouchBarButton,
TouchBarLabel,
TouchBarSpacer,
TouchBarSegmentedControl,
TouchBarScrubber,
} = TouchBar;
const getSongInfo = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls");
// Songtitle label
const songTitle = new TouchBarLabel({
label: "",
});
// This will store the song controls once available
let controls = [];
// This will store the song image once available
const songImage = {};
// Pause/play button
const pausePlayButton = new TouchBarButton();
// The song control buttons (control functions are in the same order)
const buttons = new TouchBarSegmentedControl({
mode: "buttons",
segments: [
new TouchBarButton({
label: "⏮",
}),
pausePlayButton,
new TouchBarButton({
label: "⏭",
}),
new TouchBarButton({
label: "👎",
}),
new TouchBarButton({
label: "👍",
}),
],
change: (i) => controls[i](),
});
// This is the touchbar object, this combines everything with proper layout
const touchBar = new TouchBar({
items: [
new TouchBarScrubber({
items: [songImage, songTitle],
continuous: false,
}),
new TouchBarSpacer({
size: "flexible",
}),
buttons,
],
});
module.exports = (win) => {
const registerCallback = getSongInfo(win);
const { playPause, next, previous, like, dislike } = getSongControls(win);
// If the page is ready, register the callback
win.on("ready-to-show", () => {
controls = [previous, playPause, next, like, dislike];
// Register the callback
registerCallback((songInfo) => {
// Song information changed, so lets update the touchBar
// Set the song title
songTitle.label = songInfo.title;
// Changes the pause button if paused
pausePlayButton.label = songInfo.isPaused ? "▶️" : "⏸";
// Get image source
songImage.icon = songInfo.image
? songInfo.image.resize({ height: 23 })
: null;
win.setTouchBar(touchBar);
});
});
};

View File

@ -4,15 +4,15 @@ const path = require("path");
const { ipcMain, ipcRenderer } = require("electron");
// Creates a DOM element from a HTML string
module.exports.ElementFromHtml = html => {
var template = document.createElement("template");
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
module.exports.ElementFromHtml = (html) => {
var template = document.createElement("template");
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
};
// Creates a DOM element from a HTML file
module.exports.ElementFromFile = filepath => {
module.exports.ElementFromFile = (filepath) => {
return module.exports.ElementFromHtml(fs.readFileSync(filepath, "utf8"));
};
@ -20,8 +20,12 @@ module.exports.templatePath = (pluginPath, name) => {
return path.join(pluginPath, "templates", name);
};
module.exports.triggerAction = (channel, action) => {
return ipcRenderer.send(channel, action);
module.exports.triggerAction = (channel, action, ...args) => {
return ipcRenderer.send(channel, action, ...args);
};
module.exports.triggerActionSync = (channel, action, ...args) => {
return ipcRenderer.sendSync(channel, action, ...args);
};
module.exports.listenAction = (channel, callback) => {
@ -29,7 +33,7 @@ module.exports.listenAction = (channel, callback) => {
};
module.exports.fileExists = (path, callbackIfExists) => {
fs.access(path, fs.F_OK, err => {
fs.access(path, fs.F_OK, (err) => {
if (err) {
return;
}
@ -45,10 +49,10 @@ module.exports.injectCSS = (webContents, filepath) => {
};
module.exports.getAllPlugins = () => {
const isDirectory = source => fs.lstatSync(source).isDirectory();
const isDirectory = (source) => fs.lstatSync(source).isDirectory();
return fs
.readdirSync(__dirname)
.map(name => path.join(__dirname, name))
.map((name) => path.join(__dirname, name))
.filter(isDirectory)
.map(name => path.basename(name));
.map((name) => path.basename(name));
};

View File

@ -1,32 +1,35 @@
const path = require("path");
const { getCurrentWindow } = require("electron").remote;
const { contextBridge, remote } = require("electron");
const { getEnabledPlugins, store } = require("./store");
const { fileExists } = require("./plugins/utils");
const config = require("./config");
const { fileExists } = require("./plugins/utils");
const plugins = getEnabledPlugins();
const plugins = config.plugins.getEnabled();
plugins.forEach(plugin => {
plugins.forEach(([plugin, options]) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js");
fileExists(pluginPath, () => {
const actions = require(pluginPath).global || {};
Object.keys(actions).forEach(actionName => {
const actions = require(pluginPath).actions || {};
// TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld(plugin + "Actions", actions);
Object.keys(actions).forEach((actionName) => {
global[actionName] = actions[actionName];
});
});
});
document.addEventListener("DOMContentLoaded", () => {
plugins.forEach(plugin => {
plugins.forEach(([plugin, options]) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "front.js");
fileExists(pluginPath, () => {
const run = require(pluginPath);
run();
run(options);
});
});
// Add action for reloading
global.reload = () =>
getCurrentWindow().webContents.loadURL(store.get("url"));
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
});

View File

@ -0,0 +1,18 @@
// This is used for to control the songs
const pressKey = (window, key) => {
window.webContents.sendInputEvent({
type: "keydown",
keyCode: key,
});
};
module.exports = (win) => {
return {
previous: () => pressKey(win, "k"),
next: () => pressKey(win, "j"),
playPause: () => pressKey(win, "space"),
like: () => pressKey(win, "_"),
dislike: () => pressKey(win, "+"),
search: () => pressKey(win, "/"),
};
};

137
providers/song-info.js Normal file
View File

@ -0,0 +1,137 @@
const { nativeImage } = require("electron");
const fetch = require("node-fetch");
// This selects the song title
const titleSelector = ".title.style-scope.ytmusic-player-bar";
// This selects the song image
const imageSelector =
"#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img";
// This selects the song subinfo, this includes artist, views, likes
const subInfoSelector =
"#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.content-info-wrapper.style-scope.ytmusic-player-bar > span";
// This selects the progress bar, used for songlength and current progress
const progressSelector = "#progress-bar";
// Grab the title using the selector
const getTitle = (win) => {
return win.webContents
.executeJavaScript(
"document.querySelector('" + titleSelector + "').innerText"
)
.catch((error) => {
console.log(error);
});
};
// Grab the image src using the selector
const getImageSrc = (win) => {
return win.webContents
.executeJavaScript("document.querySelector('" + imageSelector + "').src")
.catch((error) => {
console.log(error);
});
};
// Grab the subinfo using the selector
const getSubInfo = async (win) => {
// Get innerText of subinfo element
const subInfoString = await win.webContents.executeJavaScript(
'document.querySelector("' + subInfoSelector + '").innerText'
);
// Split and clean the string
const splittedSubInfo = subInfoString.replaceAll("\n", "").split(" • ");
// Make sure we always return 3 elements in the aray
const subInfo = [];
for (let i = 0; i < 3; i++) {
// Fill array with empty string if not defined
subInfo.push(splittedSubInfo[i] || "");
}
return subInfo;
};
// Grab the progress using the selector
const getProgress = async (win) => {
// Get max value of the progressbar element
const songDuration = await win.webContents.executeJavaScript(
'document.querySelector("' + progressSelector + '").max'
);
// Get current value of the progressbar element
const elapsedSeconds = await win.webContents.executeJavaScript(
'document.querySelector("' + progressSelector + '").value'
);
return { songDuration, 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);
};
const getPausedStatus = async (win) => {
const title = await win.webContents.executeJavaScript("document.title");
return !title.includes("-");
};
// Fill songInfo with empty values
const songInfo = {
title: "",
artist: "",
views: "",
likes: "",
imageSrc: "",
image: null,
isPaused: true,
songDuration: 0,
elapsedSeconds: 0,
};
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 () => {
// Save the old title temporarily
const oldTitle = songInfo.title;
// Get and set the new data
songInfo.title = await getTitle(win);
songInfo.isPaused = await getPausedStatus(win);
const { songDuration, elapsedSeconds } = await getProgress(win);
songInfo.songDuration = songDuration;
songInfo.elapsedSeconds = elapsedSeconds;
// If title changed then we do need to update other info
if (oldTitle !== songInfo.title) {
const subInfo = await getSubInfo(win);
songInfo.artist = subInfo[0];
songInfo.views = subInfo[1];
songInfo.likes = subInfo[2];
songInfo.imageSrc = await getImageSrc(win);
songInfo.image = await getImage(songInfo.imageSrc);
}
// Trigger the callbacks
callbacks.forEach((c) => {
c(songInfo);
});
});
return registerCallback;
};
module.exports = registerProvider;

View File

@ -1,14 +1,24 @@
# YouTube Music
[![GitHub release](https://img.shields.io/github/release/th-ch/youtube-music.svg)](https://GitHub.com/th-ch/youtube-music/releases/)
[![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg)](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![Build status](https://ci.appveyor.com/api/projects/status/tgre12r150ynvwl2?svg=true)](https://ci.appveyor.com/project/th-ch/youtube-music)
[![Build Status](https://travis-ci.org/th-ch/youtube-music.svg?branch=master)](https://travis-ci.org/th-ch/youtube-music)
[![Known Vulnerabilities](https://snyk.io/test/github/th-ch/youtube-music/badge.svg)](https://snyk.io/test/github/th-ch/youtube-music)
![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total)
<div align="center">
![Screenshot](screenshot.jpg "Screenshot")
[![GitHub release](https://img.shields.io/github/release/th-ch/youtube-music.svg?style=for-the-badge&logo=youtube-music)](https://github.com/th-ch/youtube-music/releases/)
[![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg?style=for-the-badge)](https://github.com/sindresorhus/xo)
[![Build status](https://img.shields.io/github/workflow/status/th-ch/youtube-music/Build%20YouTube%20Music?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![Known Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/github/th-ch/youtube-music?style=for-the-badge)](https://snyk.io/test/github/th-ch/youtube-music)
[![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin)
</div>
![Screenshot](web/screenshot.jpg "Screenshot")
<div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest">
<img src="web/youtube-music.svg" width="400" height="100">
</a>
</div>
**Electron wrapper around YouTube Music featuring:**
@ -19,13 +29,21 @@
You can check out the [latest release](https://github.com/th-ch/youtube-music/releases/latest) to quickly find the latest version.
### Arch Linux
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
- **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
- **Touchbar**: custom TouchBar layout for macOS
## Dev
@ -93,11 +111,19 @@ module.exports = () => {
## Build
```sh
yarn run build
yarn build
```
Builds the app for macOS, Linux, and Windows, using [electron-builder](https://github.com/electron-userland/electron-builder).
## Tests
```sh
yarn test
```
Uses [Spectron](https://www.electronjs.org/spectron) to test the app.
## License
MIT © [th-ch](https://github.com/th-ch/youtube-music)

View File

@ -1,33 +0,0 @@
const Store = require("electron-store");
const plugins = require("./plugins");
const store = new Store({
defaults: {
"window-size": {
width: 1100,
height: 550
},
url: "https://music.youtube.com",
plugins: ["navigation", "shortcuts", "adblocker"],
options: {
tray: false,
appVisible: true,
autoUpdates: true
}
}
});
module.exports = {
store: store,
// Plugins
isPluginEnabled: plugin => plugins.isEnabled(store, plugin),
getEnabledPlugins: () => plugins.getEnabledPlugins(store),
enablePlugin: plugin => plugins.enablePlugin(store, plugin),
disablePlugin: plugin => plugins.disablePlugin(store, plugin),
// Options
setOptions: options =>
store.set("options", { ...store.get("options"), ...options }),
isTrayEnabled: () => store.get("options.tray"),
isAppVisible: () => store.get("options.appVisible"),
autoUpdate: () => store.get("options.autoUpdates")
};

View File

@ -1,31 +0,0 @@
function getEnabledPlugins(store) {
return store.get("plugins");
}
function isEnabled(store, plugin) {
return store.get("plugins").indexOf(plugin) > -1;
}
function enablePlugin(store, plugin) {
let plugins = getEnabledPlugins(store);
if (plugins.indexOf(plugin) === -1) {
plugins.push(plugin);
store.set("plugins", plugins);
}
}
function disablePlugin(store, plugin) {
let plugins = getEnabledPlugins(store);
let index = plugins.indexOf(plugin);
if (index > -1) {
plugins.splice(index, 1);
store.set("plugins", plugins);
}
}
module.exports = {
isEnabled : isEnabled,
getEnabledPlugins: getEnabledPlugins,
enablePlugin : enablePlugin,
disablePlugin : disablePlugin
};

41
tests/environment.js Normal file
View File

@ -0,0 +1,41 @@
const path = require("path");
const getPort = require("get-port");
const NodeEnvironment = require("jest-environment-node");
const electronPath = require("electron");
const { Application } = require("spectron");
class TestEnvironment extends NodeEnvironment {
constructor(config) {
super(config);
}
async setup() {
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();
}
async teardown() {
if (this.global.__APP__.isRunning()) {
await this.global.__APP__.stop();
}
await super.teardown();
}
runScript(script) {
return super.runScript(script);
}
}
module.exports = TestEnvironment;

23
tests/index.test.js Normal file
View File

@ -0,0 +1,23 @@
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();
expect(title).toEqual("YouTube Music");
});
});

30
tray.js
View File

@ -2,19 +2,20 @@ const path = require("path");
const { Menu, nativeImage, Tray } = require("electron");
const config = require("./config");
const { mainMenuTemplate } = require("./menu");
const { isTrayEnabled } = require("./store");
const { clickInYoutubeMusic } = require("./utils/youtube-music");
const getSongControls = require("./providers/song-controls");
// Prevent tray being garbage collected
let tray;
module.exports.setUpTray = (app, win) => {
if (!isTrayEnabled()) {
if (!config.get("options.tray")) {
tray = undefined;
return;
}
const { playPause, next, previous } = getSongControls(win);
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
let trayIcon = nativeImage.createFromPath(iconPath).resize({
width: 16,
@ -24,35 +25,30 @@ module.exports.setUpTray = (app, win) => {
tray.setToolTip("Youtube Music");
tray.setIgnoreDoubleClickEvents(true);
tray.on("click", () => {
win.isVisible() ? win.hide() : win.show();
if (config.get("options.trayClickPlayPause")) {
playPause();
} else {
win.isVisible() ? win.hide() : win.show();
}
});
const trayMenu = Menu.buildFromTemplate([
{
label: "Play/Pause",
click: () => {
clickInYoutubeMusic(
win,
"#left-controls > div > paper-icon-button.play-pause-button.style-scope.ytmusic-player-bar"
);
playPause();
},
},
{
label: "Next",
click: () => {
clickInYoutubeMusic(
win,
"#left-controls > div > paper-icon-button.next-button.style-scope.ytmusic-player-bar"
);
next();
},
},
{
label: "Previous",
click: () => {
clickInYoutubeMusic(
win,
"#left-controls > div > paper-icon-button.previous-button.style-scope.ytmusic-player-bar"
);
previous();
},
},
{
@ -61,7 +57,7 @@ module.exports.setUpTray = (app, win) => {
win.show();
},
},
...mainMenuTemplate,
...mainMenuTemplate(win),
{
label: "Quit",
click: () => {

3
utils/testing.js Normal file
View File

@ -0,0 +1,3 @@
const isTesting = () => process.env.NODE_ENV === "test";
module.exports = { isTesting };

View File

@ -1,8 +0,0 @@
const clickInYoutubeMusic = (win, selector) => {
win.webContents.executeJavaScript(
`document.querySelector("${selector}").click();`,
true
);
};
module.exports = { clickInYoutubeMusic };

View File

Before

Width:  |  Height:  |  Size: 816 KiB

After

Width:  |  Height:  |  Size: 816 KiB

379
web/youtube-music.svg Normal file
View File

@ -0,0 +1,379 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 800 300" width="800" height="300">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">
<style>
.container {
width: 100%;
position: relative;
overflow: hidden;
}
a {
text-decoration: none;
}
h1.main, p.demos {
-webkit-animation-delay: 18s;
-moz-animation-delay: 18s;
-ms-animation-delay: 18s;
animation-delay: 18s;
}
.container {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
z-index: 0;
}
.content {
position: absolute;
width: 100%;
height: 100%;
left: 0px;
top: 0px;
z-index: 1000;
}
.container h2 {
position: absolute;
top: 50%;
line-height: 100px;
height: 90px;
margin-top: -90px;
font-size: 90px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
width: 100%;
text-align: center;
color: transparent;
-webkit-animation: blurFadeInOut 3s ease-in backwards;
-moz-animation: blurFadeInOut 3s ease-in backwards;
-ms-animation: blurFadeInOut 3s ease-in backwards;
animation: blurFadeInOut 3s ease-in backwards;
}
.container h2.frame-1 {
-webkit-animation-delay: 0s;
-moz-animation-delay: 0s;
-ms-animation-delay: 0s;
animation-delay: 0s;
}
.container h2.frame-2 {
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
}
.container h2.frame-3 {
-webkit-animation-delay: 6s;
-moz-animation-delay: 6s;
-ms-animation-delay: 6s;
animation-delay: 6s;
}
.container h2.frame-4 {
-webkit-animation-delay: 9s;
-moz-animation-delay: 9s;
-ms-animation-delay: 9s;
animation-delay: 9s;
}
.container h2.frame-5 {
-webkit-animation: none;
-moz-animation: none;
-ms-animation: none;
animation: none;
color: transparent;
text-shadow: 0px 0px 1px #fff;
}
.container h2.frame-5 span {
-webkit-animation: blurFadeIn 3s ease-in 12s backwards;
-moz-animation: blurFadeIn 1s ease-in 12s backwards;
-ms-animation: blurFadeIn 3s ease-in 12s backwards;
animation: blurFadeIn 3s ease-in 12s backwards;
color: transparent;
text-shadow: 0px 0px 1px #fff;
}
.container h2.frame-5 span:nth-child(2) {
-webkit-animation-delay: 13s;
-moz-animation-delay: 13s;
-ms-animation-delay: 13s;
animation-delay: 13s;
}
.container h2.frame-5 span:nth-child(3) {
-webkit-animation-delay: 14s;
-moz-animation-delay: 14s;
-ms-animation-delay: 14s;
animation-delay: 14s;
}
.circle-link {
position: absolute;
left: 50%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
bottom: 50px;
margin-left: -100px;
text-align: center;
line-height: 200px;
width: 200px;
height: 200px;
background: #cc0000;
color: #fff;
font-size: 25px;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
-webkit-animation: fadeInRotate 0.8s ease 16s backwards;
-moz-animation: fadeInRotate 0.8s ease 16s backwards;
-ms-animation: fadeInRotate 0.8s ease 16s backwards;
animation: fadeInRotate 0.8s ease 16s backwards;
-webkit-transform: scale(1) rotate(0deg);
-moz-transform: scale(1) rotate(0deg);
-o-transform: scale(1) rotate(0deg);
-ms-transform: scale(1) rotate(0deg);
transform: scale(1) rotate(0deg);
background-repeat: no-repeat;
background-position: -250px -250px, 0 0;
background-image: -webkit-linear-gradient(
top left,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.2) 37%,
rgba(255, 255, 255, 0.8) 45%,
rgba(255, 255, 255, 0.0) 50%
);
background-image: -moz-linear-gradient(
0 0,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.2) 37%,
rgba(255, 255, 255, 0.8) 45%,
rgba(255, 255, 255, 0.0) 50%
);
background-image: -o-linear-gradient(
0 0,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.2) 37%,
rgba(255, 255, 255, 0.8) 45%,
rgba(255, 255, 255, 0.0) 50%
);
background-image: linear-gradient(
0 0,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.2) 37%,
rgba(255, 255, 255, 0.8) 45%,
rgba(255, 255, 255, 0.0) 50%
);
-moz-background-size: 250% 250%, 100% 100%;
background-size: 250% 250%, 100% 100%;
-webkit-transition: background-position 0s ease;
-moz-transition: background-position 0s ease;
-o-transition: background-position 0s ease;
transition: background-position 0s ease;
}
.circle-link:hover {
background-position: 0 0, 0 0;
-webkit-transition-duration: 0.5s;
-moz-transition-duration: 0.5s;
transition-duration: 0.5s;
}
@-webkit-keyframes blurFadeInOut {
0% {
opacity: 0;
color: #cc0000;
text-shadow: 0px 0px 40px #fff;
-webkit-transform: scale(1.3);
}
20%, 75% {
opacity: 1;
color: #cc0000;
text-shadow: 0px 0px 1px #fff;
-webkit-transform: scale(1);
}
100% {
opacity: 0;
text-shadow: 0px 0px 50px #fff;
-webkit-transform: scale(0);
}
}
@-webkit-keyframes blurFadeIn {
0% {
opacity: 0;
color: #cc0000;
text-shadow: 0px 0px 40px #fff;
-webkit-transform: scale(1.3);
}
50% {
opacity: 0.5;
color: #cc0000;
text-shadow: 0px 0px 10px #fff;
-webkit-transform: scale(1.1);
}
100% {
opacity: 1;
text-shadow: 0px 0px 1px #fff;
-webkit-transform: scale(1);
}
}
@-webkit-keyframes fadeInBack {
0% {
opacity: 0;
-webkit-transform: scale(0);
}
50% {
opacity: 0.4;
-webkit-transform: scale(2);
}
100% {
opacity: 0.2;
-webkit-transform: scale(5);
}
}
@-webkit-keyframes fadeInRotate {
0% {
opacity: 0;
-webkit-transform: scale(0) rotate(360deg);
}
100% {
opacity: 1;
-webkit-transform: scale(1) rotate(0deg);
}
}
@-moz-keyframes blurFadeInOut {
0% {
opacity: 0;
color: #cc0000;
text-shadow: 0px 0px 40px #fff;
-moz-transform: scale(1.3);
}
20%, 75% {
opacity: 1;
color: #cc0000;
text-shadow: 0px 0px 1px #fff;
-moz-transform: scale(1);
}
100% {
opacity: 0;
text-shadow: 0px 0px 50px #fff;
-moz-transform: scale(0);
}
}
@-moz-keyframes blurFadeIn {
0% {
opacity: 0;
color: #cc0000;
text-shadow: 0px 0px 40px #fff;
-moz-transform: scale(1.3);
}
100% {
opacity: 1;
text-shadow: 0px 0px 1px #fff;
-moz-transform: scale(1);
}
}
@-moz-keyframes fadeInBack {
0% {
opacity: 0;
-moz-transform: scale(0);
}
50% {
opacity: 0.4;
-moz-transform: scale(2);
}
100% {
opacity: 0.2;
-moz-transform: scale(5);
}
}
@-moz-keyframes fadeInRotate {
0% {
opacity: 0;
-moz-transform: scale(0) rotate(360deg);
}
100% {
opacity: 1;
-moz-transform: scale(1) rotate(0deg);
}
}
@keyframes blurFadeInOut {
0% {
opacity: 0;
color: #cc0000;
text-shadow: 0px 0px 40px #fff;
transform: scale(1.3);
}
20%, 75% {
opacity: 1;
color: #cc0000;
text-shadow: 0px 0px 1px #fff;
transform: scale(1);
}
100% {
opacity: 0;
text-shadow: 0px 0px 50px #fff;
transform: scale(0);
}
}
@keyframes blurFadeIn {
0% {
opacity: 0;
color: #cc0000;
text-shadow: 0px 0px 40px #fff;
transform: scale(1.3);
}
50% {
opacity: 0.5;
color: #cc0000;
text-shadow: 0px 0px 10px #fff;
transform: scale(1.1);
}
100% {
opacity: 1;
text-shadow: 0px 0px 1px #fff;
transform: scale(1);
}
}
@keyframes fadeInBack {
0% {
opacity: 0;
transform: scale(0);
}
50% {
opacity: 0.4;
transform: scale(2);
}
100% {
opacity: 0.2;
transform: scale(5);
}
}
@keyframes fadeInRotate {
0% {
opacity: 0;
transform: scale(0) rotate(360deg);
}
100% {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}
</style>
<div class="container">
<div class="content">
<h2 class="frame-1">YouTube Music Desktop App</h2>
<h2 class="frame-2">With built-in ad blocker</h2>
<h2 class="frame-3">And built-in downloader</h2>
<h2 class="frame-4">Cross-platform</h2>
<h2 class="frame-5">
<span>Free,</span>
<span>Open source</span>
</h2>
<a class="circle-link" href="https://github.com/th-ch/youtube-music/releases/latest">
Download
</a>
</div>
</div>
</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

5007
yarn.lock

File diff suppressed because it is too large Load Diff