Compare commits
1206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 022f8ff65c | |||
| 5e63cc2e89 | |||
| 880ed99846 | |||
| 222e78c85b | |||
| 050d55c736 | |||
| 13ef8560ff | |||
| 78d990c079 | |||
| 4d3e2c09da | |||
| aa899d247a | |||
| ee0c512529 | |||
| 5f9b522307 | |||
| c207e29980 | |||
| df4d2d6b72 | |||
| c3dd20cabd | |||
| 7a6db95d1a | |||
| bc6825d63b | |||
| 5e79e9e0f2 | |||
| 5e303c2ba8 | |||
| 0bd9c16356 | |||
| f0f5d9da2f | |||
| f46c431f4c | |||
| 62410e9ee2 | |||
| 46f76f1408 | |||
| 5e071e16d8 | |||
| c0238588bd | |||
| 30002d660a | |||
| 48eeb6bca3 | |||
| e67699fed5 | |||
| 8aeae45965 | |||
| ce7491941b | |||
| 1dce03c4f2 | |||
| 62eae6d5d0 | |||
| 15b2b26b84 | |||
| 9664c17c47 | |||
| 8067dad2fa | |||
| 4dcaa510d9 | |||
| b6e918089d | |||
| 1c9e6b1bb8 | |||
| ebd304c252 | |||
| 36083c4173 | |||
| a084b060d8 | |||
| 432c79b606 | |||
| 0f1f0ee933 | |||
| 9b1a4b8d88 | |||
| 1a7a665915 | |||
| 623ecf7fb8 | |||
| 0dc9c6a1a9 | |||
| 72c5eaa5ff | |||
| 0f47b94b7d | |||
| 9abe15f1ad | |||
| 96afda92c8 | |||
| 5c6fd4a739 | |||
| 23b87a876d | |||
| 737fd05369 | |||
| c5bcd89f16 | |||
| 377e1be0b2 | |||
| a92049c0c9 | |||
| 27a2955bba | |||
| cc940e2020 | |||
| 203c80767d | |||
| f564039438 | |||
| d6566fb870 | |||
| 55ab17e789 | |||
| e0eeb720cd | |||
| 53ac7ff257 | |||
| a4e2f10afa | |||
| c9c9d766e6 | |||
| b82d161180 | |||
| 7cadacd8cf | |||
| fc1a7cda62 | |||
| da3bc5aeb7 | |||
| ee98344064 | |||
| da10eabf99 | |||
| ce3c63c386 | |||
| f8c3cff5a9 | |||
| 08f369f8bc | |||
| 1517230ede | |||
| 45aeadef86 | |||
| 5ec7c346ca | |||
| 4b68de1606 | |||
| 7710bcdacc | |||
| 4c19bcc983 | |||
| 2f50b1423a | |||
| 15768c9691 | |||
| 442eb75b8d | |||
| 8facd27125 | |||
| a302cb6ebf | |||
| 32422c7ba9 | |||
| ca131cb156 | |||
| 5f8262ede1 | |||
| 88db9d3693 | |||
| 0fa6f344f9 | |||
| d1642b3dfb | |||
| 7599fce5f5 | |||
| 9bd0dc9a7b | |||
| dad7cafebc | |||
| 4febb28201 | |||
| ced943bad3 | |||
| 202b8717a2 | |||
| eba7026b89 | |||
| 6b2adca1f4 | |||
| 0c9d3cc057 | |||
| 45ec11aaed | |||
| 4d47fa3fac | |||
| 6322d5bb09 | |||
| 2e8a579049 | |||
| 9db7e99d7f | |||
| ef73989995 | |||
| 61024084e9 | |||
| 1c71b2020f | |||
| 3b2b545388 | |||
| fafc7f94fd | |||
| 5fa88b5ce9 | |||
| a8fe4167db | |||
| 0e6b0ddc26 | |||
| af88db3865 | |||
| 0a1bef556c | |||
| 7030181dc1 | |||
| 98cb2f61ab | |||
| a601d0b3d2 | |||
| 8d18923065 | |||
| 4b6a7e0bd7 | |||
| 44f87853d6 | |||
| 4ff47bee03 | |||
| 7a2c28df21 | |||
| f7241d1e4a | |||
| 2b5daf3a75 | |||
| d7184cf75d | |||
| 1313d18d44 | |||
| 605e9481d9 | |||
| cf4c89f825 | |||
| f4f1d4d373 | |||
| 6557b40924 | |||
| e01a29c5d1 | |||
| d9a7e16d63 | |||
| d170199d85 | |||
| e650aae491 | |||
| 49058dbeab | |||
| 31d2d91566 | |||
| abf2a52b46 | |||
| d0ca10e1a1 | |||
| fe99a04174 | |||
| 916900ae6a | |||
| 826c9ba48e | |||
| 7783cc5f30 | |||
| 035d7f19ea | |||
| 294cf15d58 | |||
| 11fa0dcbc6 | |||
| 85b53db439 | |||
| 200df100f4 | |||
| ba202a8572 | |||
| daf05239a1 | |||
| b73b5735ec | |||
| 292248dd35 | |||
| 2e240541c8 | |||
| ab62ae3682 | |||
| 5c3f5d05d3 | |||
| c81af537b6 | |||
| 7295c73371 | |||
| 5a65e08a94 | |||
| 45e5bc7df5 | |||
| a81cb9515c | |||
| 88e5cc2728 | |||
| 1685648328 | |||
| 9334adacf6 | |||
| efbe557dce | |||
| 52b2625486 | |||
| aa0424db08 | |||
| 157619aa4f | |||
| ce637968b2 | |||
| b2a3ed7428 | |||
| ca856e4d88 | |||
| 2ef2536766 | |||
| 1b22633388 | |||
| db306ad4e0 | |||
| 34a44cb9c6 | |||
| 8c499a6e20 | |||
| 3b816a6fd9 | |||
| 35aba8e25a | |||
| 0eb6723f27 | |||
| 2250ec9372 | |||
| ef02b53284 | |||
| 57df7cf3f9 | |||
| f0cd540726 | |||
| 7e919395eb | |||
| facc7252c2 | |||
| 09b8943f69 | |||
| aa84ffc7e1 | |||
| 10162c948b | |||
| e8c1cbdd94 | |||
| 5459f623ec | |||
| 2a2cba3539 | |||
| 1ce84607e6 | |||
| 075c54d39f | |||
| aabb826cb8 | |||
| 3791b1ae1c | |||
| d4b87d098b | |||
| 57e9c13d13 | |||
| e48e570e69 | |||
| b7d4d5b022 | |||
| dc8b0173e9 | |||
| 840729126e | |||
| ed90b97a92 | |||
| bbe4f07651 | |||
| 66df83a86d | |||
| dfedd8f091 | |||
| 58ec6eebfb | |||
| 3ba4130bc6 | |||
| 7dbbf38e78 | |||
| 51e04162a7 | |||
| d3325e2490 | |||
| d2708ee32f | |||
| 781a09c717 | |||
| 11605293dd | |||
| 3f0b946190 | |||
| 74972f053a | |||
| d85190ace1 | |||
| 5755b4ac7f | |||
| 3c24f2edcd | |||
| 6828520853 | |||
| 2ba7dddb95 | |||
| 57a8922d04 | |||
| 640098860a | |||
| 53384d9f3b | |||
| 89d8d98a35 | |||
| 7b78ba6761 | |||
| 3926a9a0c0 | |||
| 0708cd5a38 | |||
| e20e9ca771 | |||
| be1038bafd | |||
| ebc087963b | |||
| 020bdc0811 | |||
| a0d1ad6a47 | |||
| 62e5679791 | |||
| 35568bd299 | |||
| 1e40b377af | |||
| 1073de1b45 | |||
| 1d5788acaf | |||
| 8fb446588d | |||
| 6cec34b2ac | |||
| 763d3e8f74 | |||
| 7da0a913f1 | |||
| ca04c4561b | |||
| 7d8fbf49a8 | |||
| 75e15b948d | |||
| 125b69fd75 | |||
| a68d6b64dd | |||
| a60d4264dc | |||
| 9e2c6b1afa | |||
| 14965a93e9 | |||
| f62664b6a5 | |||
| 60cb7f32f1 | |||
| 008b3ad710 | |||
| 8cae64f496 | |||
| 5cdc1bc762 | |||
| 15c455105b | |||
| 14407a98c9 | |||
| 0d004d5caf | |||
| 4b75a2405c | |||
| 1f7e28b6fb | |||
| c41b2ce861 | |||
| 7f02afc5a6 | |||
| 496b3ffc1b | |||
| e9a395f67a | |||
| 0660f0b7ce | |||
| 3ac09b9dc1 | |||
| fe4904a4af | |||
| d8c8bd17ec | |||
| e9d4d5ba14 | |||
| 5b2e69588f | |||
| c1591402a0 | |||
| e2e9c03895 | |||
| deac4ef56b | |||
| 7c39e658ce | |||
| 6b026f57bc | |||
| dc07cbda6f | |||
| 1cf43fcd42 | |||
| e2cf550bed | |||
| 2917da1138 | |||
| b74eeb5688 | |||
| 0b084a6441 | |||
| 865efa1b12 | |||
| 6a248e5336 | |||
| eb9c256a5d | |||
| 4bd54dcb2d | |||
| 17b035d317 | |||
| 28bcd1fefc | |||
| 59bb1d9124 | |||
| d9255c1cec | |||
| 4ab4bb4cb3 | |||
| a6c8b887e3 | |||
| 1db0abf32d | |||
| ff899b8720 | |||
| 18004c4441 | |||
| ce1cde72bd | |||
| 453f4d92c9 | |||
| 37740e78b4 | |||
| 8ace123179 | |||
| bcdb9de41a | |||
| 9fdb6eb7e5 | |||
| 88cd1d2390 | |||
| 943bcd322d | |||
| 7774128d7e | |||
| b9b9e2ba00 | |||
| 0ce4f20ec5 | |||
| 51b87312c4 | |||
| 9ffd7af8a7 | |||
| 4a453a4f3d | |||
| dc8a472cdb | |||
| d2eabaa4bb | |||
| 39c8ca66d1 | |||
| 806098a5ef | |||
| 5f6cfd9558 | |||
| b4b7ad824b | |||
| 7b5d602f63 | |||
| 7eeeb89457 | |||
| 6e8447b5d1 | |||
| a6445bacf0 | |||
| bd9b4f1b1a | |||
| 9a816b3f07 | |||
| 4dcac23688 | |||
| 97ef6ff997 | |||
| 244a656671 | |||
| f8a2829adb | |||
| 24daadbef8 | |||
| bf2ac88847 | |||
| e42423b100 | |||
| de0c02efaf | |||
| b8290417f8 | |||
| b92205a228 | |||
| 5f642007ba | |||
| ee40d278d4 | |||
| 02d2e8ea92 | |||
| 70715e5e8a | |||
| 5ae4f564b7 | |||
| 123eabd77a | |||
| fd3438a20d | |||
| c8554a12f6 | |||
| 4a687ade9c | |||
| f77aa372cc | |||
| 0b9eef94c4 | |||
| 71b2f69f98 | |||
| 4b61c5307e | |||
| a617b91263 | |||
| fc79bdd0f3 | |||
| e5e1e547d5 | |||
| edac9b0c20 | |||
| dfaf3cf95a | |||
| bae90ce8f3 | |||
| 188e56ce30 | |||
| 19b48b123f | |||
| 549961f297 | |||
| ba7bc68ac3 | |||
| bbfe272d41 | |||
| 8a3e0a31ca | |||
| 8fbda97885 | |||
| 1856deb0f5 | |||
| fec7c5c130 | |||
| 936b4b28bb | |||
| f3092d0778 | |||
| fc1adfae6c | |||
| e279aaed64 | |||
| 4d346a9471 | |||
| cfc504da34 | |||
| 0919a4b9b7 | |||
| f46ad2ea0e | |||
| 252719bc71 | |||
| 45f49361ea | |||
| c4a74c6c7e | |||
| 05f197948d | |||
| 5a1d230538 | |||
| a7ad260a00 | |||
| ef068cccd9 | |||
| 166067920d | |||
| 8227853cf9 | |||
| 324a539b89 | |||
| ce7557353c | |||
| 7b7923fe9b | |||
| 105d5c78e7 | |||
| b25183a8f5 | |||
| adde33d1f5 | |||
| ad325ccb10 | |||
| 2e7ea6969c | |||
| 7401cf69ad | |||
| 7f71c36dc0 | |||
| a3104fda4b | |||
| 44c42310f1 | |||
| a22a8ac5c9 | |||
| aa5c3bac4e | |||
| 30b3beee18 | |||
| b059e43fb1 | |||
| 3b04d0ba19 | |||
| 959f99beae | |||
| ed402933d3 | |||
| ef8bb95884 | |||
| 1b79d2e429 | |||
| ec786748be | |||
| 06f1c7effe | |||
| d78da237fc | |||
| 4c0cce89ee | |||
| 888ced8fd1 | |||
| e1690720b3 | |||
| bbff0a6bc2 | |||
| 5db759150c | |||
| ae239f6700 | |||
| 1d26d10e57 | |||
| da70a4ce7e | |||
| 75ae9f4fad | |||
| 8f7933c111 | |||
| 29a0dedcce | |||
| 4d62993177 | |||
| 8714f33fa2 | |||
| 5dacd50ff6 | |||
| 8d06dcc7b6 | |||
| b8f6dd2584 | |||
| 0650205b86 | |||
| 3e8a0ec49a | |||
| 04d7b32d3f | |||
| eaaf170cc8 | |||
| 09450fb8c7 | |||
| ac0b78eefb | |||
| 90103d9853 | |||
| bf27c73f1d | |||
| 845c9365be | |||
| 91cf5f5c25 | |||
| 783a892e26 | |||
| 41d8f86962 | |||
| 252349579e | |||
| 99b1cfbde4 | |||
| 3f70d912d7 | |||
| bf33c4e7b4 | |||
| 3152842a30 | |||
| d84416b27c | |||
| cc38978bd3 | |||
| 7a76079ff4 | |||
| 2fe28cf126 | |||
| 3ffbfbe0e3 | |||
| 4fad456619 | |||
| 7591f13505 | |||
| 11d06c50a5 | |||
| e0a3489640 | |||
| e55a1d3076 | |||
| 563d431c00 | |||
| 3a1b77ebd8 | |||
| 3f8030a9c5 | |||
| e12e67af0e | |||
| 3ab4cd5d05 | |||
| 738adbed98 | |||
| 365a078600 | |||
| 04fc43e18b | |||
| 54273baec7 | |||
| 51e62ef47b | |||
| a330ebcda7 | |||
| a023fff2d0 | |||
| abb25ea6fb | |||
| ef49bcdb5f | |||
| b4f1b112d6 | |||
| f24ec0ae9d | |||
| ebb51fe37b | |||
| e8ee18f903 | |||
| a593de705c | |||
| 03dd024704 | |||
| 528c3535dd | |||
| 0e0f80a2d0 | |||
| 6b67fb136a | |||
| 9fe1c14869 | |||
| 8a96dddf54 | |||
| 230422c98b | |||
| d16ffc531f | |||
| f614199ea5 | |||
| 55a1c2e9e3 | |||
| bee1f77812 | |||
| fdf982ada5 | |||
| ff02fc7855 | |||
| 01ed289400 | |||
| aedb2db655 | |||
| 10a54b9de0 | |||
| ccd029c040 | |||
| 3a431841b7 | |||
| deceae8354 | |||
| c8628670cf | |||
| ffe53d5596 | |||
| a4f4ecb569 | |||
| 2097f42efb | |||
| 9c59f56aac | |||
| dfcc4107b7 | |||
| ef71abfff1 | |||
| bc916f3a6e | |||
| c7ff0dcbf6 | |||
| 7242f9bfd0 | |||
| bb2e865880 | |||
| 6ab3cf9ac9 | |||
| b77f5c9ecc | |||
| b470dbd6b9 | |||
| 1f96b6b44d | |||
| de0b228ae8 | |||
| f35d192650 | |||
| 794d00ce9e | |||
| 739e7a448b | |||
| 7fa8a454b6 | |||
| 5cd1d9abe8 | |||
| e0e17cac99 | |||
| 840039330f | |||
| 734409dc3f | |||
| 34564c8c55 | |||
| afe6accab8 | |||
| b6e7e75ae8 | |||
| 06dc0e80f0 | |||
| 47cccbce7c | |||
| 269352af97 | |||
| fa62f79dce | |||
| 9f88b37f41 | |||
| 55ae9eac1e | |||
| 05564d4a58 | |||
| 59426c56db | |||
| 18cd4c0c9a | |||
| a0e2a33e28 | |||
| 7bdb46e161 | |||
| f560b62de0 | |||
| adc1f6822b | |||
| 2da29fcfa7 | |||
| c5d0314db6 | |||
| 8c052faedd | |||
| 37067ff950 | |||
| 6366dc026e | |||
| 6e52178074 | |||
| 47f38cc690 | |||
| fdd6d9929f | |||
| 1707261f49 | |||
| 6712fced6d | |||
| 6dabfaa9ba | |||
| a41db79c35 | |||
| 87786d9aef | |||
| 22f5866050 | |||
| 04894fbcf5 | |||
| c17c624ba4 | |||
| bfe7249df8 | |||
| 13c570efe9 | |||
| b299846f0f | |||
| 59e9289d27 | |||
| 8dc29caa1b | |||
| 7fedf88654 | |||
| 5da0202425 | |||
| 6288d0b171 | |||
| 4248d20e8e | |||
| 0b413492ad | |||
| dc73561c8a | |||
| 949a2f6428 | |||
| bceaa05197 | |||
| 776cdac30d | |||
| 4333891cca | |||
| 8a89bbccf7 | |||
| fa4c69d228 | |||
| c25def8901 | |||
| 284a59b721 | |||
| 5fcba8619a | |||
| f3cd759276 | |||
| 9d3981e361 | |||
| 787326948b | |||
| 779251933c | |||
| 1efe835c69 | |||
| 5702978227 | |||
| fa3d742838 | |||
| c460cc2296 | |||
| 4e4af5e830 | |||
| 9a4e98063b | |||
| 8bfe04bb50 | |||
| 6774d54f5e | |||
| 9705f8489d | |||
| a7229cbe14 | |||
| 7577aba45e | |||
| d78fbe476e | |||
| bfe4b2bba7 | |||
| 7625a3aa52 | |||
| 30c8dcf730 | |||
| 00a3e8d35e | |||
| 4d01cdfa6c | |||
| f924b6c8e3 | |||
| 926d98174c | |||
| 41b3972f54 | |||
| 467f29e363 | |||
| 9cc13c3757 | |||
| f8ccb86156 | |||
| b316aa2301 | |||
| 5c49b28664 | |||
| dedf96afd3 | |||
| 3bb5bc2ca1 | |||
| c79fdd9887 | |||
| d7b821727d | |||
| 21c45faf20 | |||
| 92cab89d17 | |||
| fa160b2e90 | |||
| 308ac38e6b | |||
| a62cafb601 | |||
| bf9e3b5f48 | |||
| 3c6b3aeff0 | |||
| 37181a7b5e | |||
| 0b363d6487 | |||
| e9398adac3 | |||
| 6901713036 | |||
| 1d5b2997bd | |||
| 572a023aaa | |||
| 9187f1e240 | |||
| df13d7d0f3 | |||
| 85228fd7d2 | |||
| 17ba071057 | |||
| d7df4d7d10 | |||
| 7aa970cebc | |||
| f08f003cf4 | |||
| 9f99eded9e | |||
| c512f13009 | |||
| b475f780ff | |||
| 2294102006 | |||
| d69a07d025 | |||
| 4f4995c20c | |||
| b6894dca29 | |||
| 73f14e581d | |||
| 2f2e64af4a | |||
| 5710307ddc | |||
| 52ba2dc9ff | |||
| 926b9fb5e6 | |||
| a6c9b3381a | |||
| 5dc13a4698 | |||
| a69085c591 | |||
| a22f7fed21 | |||
| 8b7045fb1b | |||
| efd1b92514 | |||
| 969f6d7bba | |||
| 4f7c92d6a0 | |||
| 24d4a50574 | |||
| 7693a3ba4a | |||
| 7ca4dc5c85 | |||
| 21ff09b605 | |||
| fbf4b3b8b5 | |||
| 5812eb0147 | |||
| b5dbfaf686 | |||
| 6b7fd5ba63 | |||
| 73a049a7bc | |||
| ef0c30e23a | |||
| 59ed2326d9 | |||
| 07a02c8c82 | |||
| f1050cb676 | |||
| 7131893f1c | |||
| e4dfb2ff33 | |||
| 187fad6834 | |||
| 26df435db0 | |||
| 0bee281d1d | |||
| 26de5802a0 | |||
| c258a4855e | |||
| b7b6d50ba2 | |||
| 0376a30fbb | |||
| ca92031e89 | |||
| 986d2ad5b1 | |||
| d9b8d8c48d | |||
| 0ef34d7c71 | |||
| f87607d25d | |||
| cc0bfae067 | |||
| e7d2d04f5a | |||
| f4319ebc6b | |||
| a1f025e23c | |||
| c002263c3b | |||
| 2d69dfd333 | |||
| 9d99ffdc72 | |||
| a859b27eba | |||
| be26827609 | |||
| 457a8b5018 | |||
| 0442e427a6 | |||
| d45ca960b4 | |||
| 0bcfdbf39c | |||
| e3b5afda3e | |||
| 97297a2c49 | |||
| 11ac756da5 | |||
| a273d13086 | |||
| ca11120036 | |||
| 7dac9a2454 | |||
| 30e0e99467 | |||
| 275d8cb2b9 | |||
| 1cc46daead | |||
| 9048da22f9 | |||
| 70fa5aa217 | |||
| 6bf7f3b9eb | |||
| 67579877bc | |||
| 534f96921e | |||
| 22491ae0a0 | |||
| dd39bdd84c | |||
| 935a307235 | |||
| 517e9c0472 | |||
| b2c27b9fdb | |||
| 8acfabf9f8 | |||
| 375fb082f0 | |||
| 8b65f1d6e4 | |||
| d36fb592d0 | |||
| 575a643e55 | |||
| 07853d8b39 | |||
| da9cb8e2f5 | |||
| 22acaf688f | |||
| 063ba1b6c7 | |||
| 0229ccaa1e | |||
| df1e28546b | |||
| 1806d5a0a2 | |||
| 59efba4dec | |||
| 670ed62360 | |||
| 6a2b393b45 | |||
| 4488a3adba | |||
| eb7ef7ab36 | |||
| dcadf0a31a | |||
| 3dda8b7c2e | |||
| 947007cc59 | |||
| bf5ac285d3 | |||
| 5dfbdd4882 | |||
| 3cd1f79886 | |||
| 04234f0b3f | |||
| 7b30896091 | |||
| 2cf29fe88d | |||
| 09ce665df1 | |||
| 299f34d98e | |||
| 1af73a7cf8 | |||
| bcc7397f26 | |||
| 95ac01c9ba | |||
| edd7b80fcd | |||
| 231514ae0d | |||
| 0c948d5ea1 | |||
| 50117ea51b | |||
| 78d8160823 | |||
| 81b2303a6f | |||
| f7a09082a5 | |||
| ca318450b8 | |||
| e86739c99c | |||
| 92a3a55803 | |||
| 7479f2f697 | |||
| 371a7eb475 | |||
| 84f6e46efc | |||
| 110dbd3e18 | |||
| 4b7d94b1d5 | |||
| 9c4aa4bcb2 | |||
| a7fd8bc21b | |||
| 548f82ba0a | |||
| 127e325b2b | |||
| 40745d3946 | |||
| 2c337953eb | |||
| 61cb3135f3 | |||
| 1a2f20042b | |||
| 6e315b9af2 | |||
| 9438cd8b26 | |||
| 6eadc7f7e5 | |||
| 399c6e37ce | |||
| 16a32d1946 | |||
| 45b7422ea8 | |||
| 76c570413a | |||
| b9ea98c115 | |||
| 2c38b8a764 | |||
| 7af418a040 | |||
| bc40fc3d49 | |||
| 831aa3d391 | |||
| 2fe391beba | |||
| 47bd015549 | |||
| 2cb05c9d86 | |||
| 5a7774e7b1 | |||
| a5fe8bc589 | |||
| aed1bbc6d7 | |||
| 042083b112 | |||
| 61b04e9b42 | |||
| f655cdf953 | |||
| 4118b4b6c4 | |||
| 5cd2e78e88 | |||
| 70b5e579b1 | |||
| 71f2123f27 | |||
| 40fa1bac92 | |||
| 10049d1ee9 | |||
| faaf54d0b0 | |||
| 7224620350 | |||
| 549f0f7c7a | |||
| 529d5e165c | |||
| 35f6064b7a | |||
| c11ec3341a | |||
| b7142000ab | |||
| e3d41ccb95 | |||
| 1067417dbd | |||
| c554ed79b1 | |||
| 8fd6bdbdf3 | |||
| 92ecf6a0b0 | |||
| 8d475eda0a | |||
| 46d3a85cc0 | |||
| 72660f5aa1 | |||
| 8fa1c7e5a8 | |||
| f532398a9c | |||
| 6bb33453c7 | |||
| 170e2a696e | |||
| 1ff69c933c | |||
| 0935edd516 | |||
| e7e3e8abe0 | |||
| 3e77064cd3 | |||
| 4651d6d241 | |||
| 7418a1f4b2 | |||
| c6bba51166 | |||
| c8b149281b | |||
| 3276e318d8 | |||
| d0d739e61f | |||
| 3f3a5483ed | |||
| c90ab00c09 | |||
| 5e29235c03 | |||
| e81671f4da | |||
| fbf92971a5 | |||
| 0c06d59a47 | |||
| 5237311f1f | |||
| 7d355ea1f2 | |||
| ad8b9c9bf7 | |||
| f2b532d8fa | |||
| c6ee222e43 | |||
| 9739dbe27f | |||
| 1d6f1d2216 | |||
| 563daae11a | |||
| 451d33707a | |||
| ce264c5d65 | |||
| 9b6e3c850a | |||
| ae1e106ccd | |||
| b362118207 | |||
| 73287cf8b2 | |||
| 42ad78c6cc | |||
| 031875ad86 | |||
| 27086e759f | |||
| 913c69a33d | |||
| 2eaa660a6d | |||
| d811ebadb4 | |||
| 774815c4e5 | |||
| a5bdb257d4 | |||
| 72c8c49edf | |||
| 68d985acba | |||
| 76a7b303fa | |||
| b5472c11df | |||
| 0c45f9850b | |||
| 999d4ab4ab | |||
| 5069913c56 | |||
| 88dea85f03 | |||
| 85793d70f7 | |||
| 12825d8bf2 | |||
| 53f5bda382 | |||
| c0d7972da3 | |||
| 278618bc83 | |||
| 03c1ab0e98 | |||
| 8b5a094eb5 | |||
| 1f52995dc4 | |||
| d30755e5fa | |||
| 82bcadcd64 | |||
| 3e3fdb3c3f | |||
| b67a4ed9bb | |||
| 06b9cf9255 | |||
| 4284bcc329 | |||
| aacc2d261b | |||
| 92da06eb96 | |||
| 897cfd3c7d | |||
| c722896a73 | |||
| 31a7588cee | |||
| da69d4c5a6 | |||
| ce0ee82648 | |||
| c837f104f7 | |||
| 2f73548701 | |||
| efb92a3513 | |||
| 98fc8e3b9d | |||
| 6f0d4fbbe4 | |||
| dab97293be | |||
| f2149e3b72 | |||
| 48b0469a4e | |||
| 1add1a2233 | |||
| 9f4187e64a | |||
| f1bbae69ac | |||
| 98a2c0d82b | |||
| d0733e25dc | |||
| 34aded725d | |||
| bb385d440e | |||
| 1ed43e11ad | |||
| bf8b88cb60 | |||
| f0f85955dc | |||
| d7950fbf70 | |||
| c8f12990eb | |||
| eb14286315 | |||
| 217c3f41ee | |||
| a9227b529c | |||
| 9f77dcc348 | |||
| 3b6a7c82ef | |||
| 69f560cdd1 | |||
| c488c30015 | |||
| 135e1002e6 | |||
| f51e625c29 | |||
| 040946ca9e | |||
| 5098ddb98c | |||
| 80c152f795 | |||
| 9cde19d906 | |||
| 3f33eb8c07 | |||
| 0bba2980c7 | |||
| 7e60049143 | |||
| e74098e9a5 | |||
| 4fe02baace | |||
| eecc13852f | |||
| 2135e01ee1 | |||
| 346d301fe4 | |||
| 263a335c96 | |||
| 20db77f965 | |||
| d0ab23fa88 | |||
| a669b1ed3a | |||
| 660fce8c99 | |||
| 4bfca93713 | |||
| bb1295c5f7 | |||
| 5f97255908 | |||
| 7a50bbd0c6 | |||
| 7f7267d806 | |||
| 12d9b07c8d | |||
| 0b6b707ccd | |||
| 5a775b238c | |||
| cad8e4fe83 | |||
| 4b9af14c40 | |||
| f8db04e3fc | |||
| e8b712b3fa | |||
| 556b1a213e | |||
| 8daf2462ec | |||
| 99e0eec9fe | |||
| f36283d63f | |||
| 1a73e74039 | |||
| 454061ece9 | |||
| e6746722c5 | |||
| 4c07817b72 | |||
| ac9b59dc84 | |||
| 4e10dab5a8 | |||
| 8124c2b218 | |||
| fe813df0b5 | |||
| 05278ab643 | |||
| f722cf86dd | |||
| b909df9e66 | |||
| 55a442e90e | |||
| af569c3eee | |||
| 45fa963818 | |||
| 764f08ebfb | |||
| 94f2cbaf06 | |||
| 2ad097c743 | |||
| 648d102101 | |||
| 212009a69b | |||
| 4364d3be71 | |||
| 62e2e8a471 | |||
| 5d8b04b8d6 | |||
| 3526197d93 | |||
| 494b1d9515 | |||
| 5f71be280b | |||
| e8c3716106 | |||
| 9840956ef7 | |||
| 605401166d | |||
| 03f4654518 | |||
| e87099c816 | |||
| 83eb187d91 | |||
| 20cdaf2317 | |||
| 4f4372b65a | |||
| 325026e3ea | |||
| a6242d13ae | |||
| bc2a1f7f71 | |||
| d5c2ad2115 | |||
| 3abef7cb8a | |||
| b45c628142 | |||
| 9d6a78bc57 | |||
| 7018481b1d | |||
| 9f9e991aec | |||
| 640ba26d55 | |||
| f5758bfe93 | |||
| a3ea37d412 | |||
| 89c664b4d2 | |||
| 51871a3fec | |||
| 04ca4e8537 | |||
| 023258f1d7 | |||
| 848bb36c64 | |||
| a6e9c140fe | |||
| e972fd15c2 | |||
| ed5fb06504 | |||
| 51c2b76c8b | |||
| f193c0b547 | |||
| 7ac3cf69b6 | |||
| 69cd5ed613 | |||
| 1649bd9c2d | |||
| a97888a207 | |||
| f1073e37b5 | |||
| 7abc67b456 | |||
| b652a011a5 | |||
| 83abbdb25a | |||
| 837e888462 | |||
| 66ccd71b7c | |||
| 108c778f6d | |||
| af2b6782e8 | |||
| 7d93e9f031 | |||
| 8c311bf630 | |||
| bdfdf83c24 | |||
| 96e6b5d018 | |||
| c5ef9a9ebb | |||
| 4ace5e3647 | |||
| 476e13de9f | |||
| 659cb35525 | |||
| a507a8cd71 | |||
| 8cca8c89b3 | |||
| 476b45096c | |||
| 43be177b66 | |||
| 2edeab567f | |||
| 26fb48fd37 | |||
| c5781962f4 | |||
| 61dd477c27 | |||
| 6ca64d68ca | |||
| ad484ab745 | |||
| 7b3280c12b | |||
| cd41f093be | |||
| a2eb3a3319 | |||
| e4b1d38f85 | |||
| 560e323893 | |||
| a31e59fbc7 | |||
| 2cbc73d2ed | |||
| 099e5d8491 | |||
| 54d3f925e6 | |||
| ec6107138d | |||
| 83d7befc45 | |||
| b6f9404ff5 | |||
| 16a0b6a893 | |||
| 9ff40611ce | |||
| f2d20d05c4 | |||
| f650ee1e70 | |||
| ab7ba1c280 | |||
| c4ced889b5 | |||
| 79e71dae26 | |||
| 87cbdc3dc3 | |||
| a3c5be0cc2 | |||
| 0e3982d199 | |||
| 8bfbbca044 | |||
| 35fa794395 | |||
| bfb392a326 | |||
| 8adfcdc002 | |||
| 4ec0b7ff30 | |||
| 0e683444d3 | |||
| 7282a227fd | |||
| c0d0c267a7 | |||
| 6577bcdad8 | |||
| 34f56df2ec | |||
| 7f66ff2c0c | |||
| 735fd39c3c | |||
| 7df7b32eea | |||
| d9f1c589e9 | |||
| 5dd8d1a274 | |||
| 2f117117d8 | |||
| d8a6453a8d | |||
| 27d8bbdf85 | |||
| 7bdbab5a2d | |||
| 96f23ea8d5 | |||
| 0a8ac31c1e | |||
| e0d7117970 | |||
| e70f843ac3 | |||
| 7932408b47 | |||
| 97c6cad503 | |||
| 455a89ad28 | |||
| 9ec07b5fb7 | |||
| b9aa6ffdd4 | |||
| ff1847d1e2 | |||
| fa4a55f97e | |||
| 781a726f4b | |||
| 0b49d57969 | |||
| 70c55ca587 | |||
| 3277a8e6c9 | |||
| 9c54fccf93 | |||
| f3a6d4dd18 | |||
| 721b048151 | |||
| 35859a6c3a | |||
| 7f099eef4e | |||
| 9da0e4305f | |||
| a81476100b | |||
| 7cbc99fc19 | |||
| 8a9a3fc69d | |||
| f422b25cb6 | |||
| d44fb5c840 | |||
| b4713196fe | |||
| 8bf2c8397e | |||
| 317e3af412 | |||
| a4a3564136 | |||
| bc49e09810 | |||
| 79890e019a | |||
| 70361afbaf | |||
| 333b695b16 | |||
| c6bb0cfe88 | |||
| b665343fd9 | |||
| 236034a1f9 | |||
| c61a719f59 | |||
| 96b1b69629 | |||
| 1eb0269434 | |||
| 5909af42d2 | |||
| 4eaeaafa7c | |||
| fe42f8d953 | |||
| 0f09f8a8ed | |||
| cb2c9fe1cd | |||
| b5cddd2d49 | |||
| 4957bccaad | |||
| b518866d24 | |||
| a51ed89281 | |||
| 3482ec4ec7 | |||
| 3c3530367a | |||
| fae1f67a64 | |||
| eb9b0b4cd1 | |||
| 91e111d483 | |||
| dbfddebbc2 | |||
| b541dd0312 | |||
| 3a822f611a | |||
| 0c53f7ffeb | |||
| acdff69919 | |||
| a80219ae40 | |||
| fecafe5a19 | |||
| a8769faea8 | |||
| 3ed4a30915 | |||
| 832195f29c | |||
| 9869063a5d | |||
| b63e7ed402 | |||
| 1b7bb4703a | |||
| 2cc347ff94 | |||
| 4674a25746 | |||
| 237423da1d | |||
| 6edc94ae98 | |||
| 98e677a76f | |||
| d289b30782 | |||
| 9b14a274ce | |||
| 7701c03e2b | |||
| 0cf72074f3 | |||
| 6e96b355bd | |||
| 210a16a32b | |||
| 3389679287 | |||
| 06eacea9a5 | |||
| 759b3844db | |||
| 2b4e996743 | |||
| 0e99f96f01 | |||
| b3f561cf2f | |||
| f9820df6c6 | |||
| bc5023c360 | |||
| 1c5d61854e | |||
| a8f3451e04 | |||
| 8728784c02 | |||
| b77c62128e | |||
| 70522173b7 | |||
| 35bd62cc0d | |||
| 52b67af59c | |||
| 027d4ce3f0 | |||
| 05d0ac963a | |||
| 97c5dc25be | |||
| 14b0315ed9 | |||
| 2c49f6c740 | |||
| bc23131e48 | |||
| e6146940b1 | |||
| 3412b3504f | |||
| fcb92fda84 | |||
| 51fdbe2086 | |||
| 74535b696c | |||
| 31ab27c39f | |||
| a13606b361 | |||
| d0ed64928d | |||
| 2b8b825f4c | |||
| 74c9fe13e2 | |||
| a2a2f18058 | |||
| e587f02bd9 | |||
| c38c416813 | |||
| 138b6df5a4 | |||
| cf2add8d91 | |||
| 453fe3f87a | |||
| ccedb17545 | |||
| 43c501b6d8 | |||
| a1bed628f4 | |||
| 7052a74a77 | |||
| 254758a4f2 | |||
| 5d85108c8a | |||
| 46bfec299c | |||
| de7bc828b1 | |||
| 335d515e22 | |||
| 3fb219fcd1 | |||
| 64114e8e9d | |||
| ca6225d47b | |||
| bf580645ae | |||
| 4361cf2b2b | |||
| c2fbc89b91 | |||
| 3605e32b25 | |||
| 49eae89886 | |||
| ee01ae1c00 | |||
| d199a5fce9 | |||
| 350e8fb706 | |||
| 3fdc6e2f09 | |||
| 938210e8f9 | |||
| c8a852bf2e | |||
| f58c10b02d | |||
| c281b8ba98 | |||
| 1fef3c4aab | |||
| 762ef4eede | |||
| fe9b26ebdd | |||
| 77173c1347 | |||
| be3a2880eb | |||
| d761d92861 | |||
| 0e7fd4d36d | |||
| 073ea27bba | |||
| 9441a6a694 | |||
| c9f610f7fc | |||
| 22b75bbfeb | |||
| 0063be02fb | |||
| cc1c13cece | |||
| 7f96c89f41 | |||
| cdb8bdcfb4 | |||
| 8c817e0862 | |||
| b4ec6a791d | |||
| 82ced02a5e | |||
| 8bbf18cd6b | |||
| 927596d0c1 | |||
| 0c0cb0501c | |||
| a2847c5007 | |||
| f6b3347d0a |
@ -1,7 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
.eslintrc.js
|
||||
80
.eslintrc.js
Normal file
@ -0,0 +1,80 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
],
|
||||
plugins: ['prettier', '@typescript-eslint', 'import'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
'arrow-parens': ['error', 'always'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
'import/first': 'error',
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-default-export': 'off',
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
|
||||
'newlines-between': 'always-and-inside-groups',
|
||||
'alphabetize': {order: 'ignore', caseInsensitive: false}
|
||||
}
|
||||
],
|
||||
'import/prefer-default-export': 'off',
|
||||
'camelcase': ['error', {properties: 'never'}],
|
||||
'class-methods-use-this': 'off',
|
||||
'lines-around-comment': [
|
||||
'error',
|
||||
{
|
||||
beforeBlockComment: false,
|
||||
afterBlockComment: false,
|
||||
beforeLineComment: false,
|
||||
afterLineComment: false,
|
||||
},
|
||||
],
|
||||
'max-len': 'off',
|
||||
'no-mixed-operators': 'error',
|
||||
'no-multi-spaces': ['error', {ignoreEOLComments: true}],
|
||||
'no-tabs': 'error',
|
||||
'no-void': 'error',
|
||||
'no-empty': 'off',
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
'quotes': ['error', 'single', {
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: false,
|
||||
}],
|
||||
'quote-props': ['error', 'consistent'],
|
||||
'semi': ['error', 'always'],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
ignorePatterns: ['dist', 'node_modules'],
|
||||
root: true,
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts']
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
4
.gitattributes
vendored
@ -1,2 +1,2 @@
|
||||
* text=auto
|
||||
*.js text eol=lf
|
||||
* text=auto eol=lf
|
||||
*.js text
|
||||
|
||||
94
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
name: Bug Report
|
||||
description: Report a YouTube Music bug
|
||||
title: "[Bug]: "
|
||||
labels: "bug :beetle:"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I use the latest version of YouTube Music (Application).
|
||||
required: true
|
||||
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- label: I understand that **th-ch/youtube-music has NO affiliation with Google or YouTube**
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: YouTube Music (Application) Version
|
||||
description: |
|
||||
What version of the YouTube Music Application are you using?
|
||||
|
||||
Note: Please check if this issue is reproducible with the latest stable release.
|
||||
placeholder: 2.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Are you using the portable version of the YouTube Music Application?
|
||||
options:
|
||||
- label: I use the portable version of the YouTube Music Application.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What operating system are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Ubuntu
|
||||
- Other Linux
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What operating system version are you using? On Windows, click the Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What arch are you using?
|
||||
options:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64 (including Apple Silicon)
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Last Known Working YouTube Music (Application) version
|
||||
description: (If applicable) What is the last version of YouTube Music this worked in?
|
||||
placeholder: 1.20.0
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Provide steps to reproduce the issue.
|
||||
placeholder: 1. Enable the X plugin.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Enabled plugins
|
||||
description: Provide the list of plugins you enabled.
|
||||
placeholder: 1. Album Color Theme
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for YouTube Music
|
||||
title: "[Feature Request]: "
|
||||
labels: "enhancement :sparkles:"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I use the latest version of YouTube Music (Application).
|
||||
required: true
|
||||
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a feature request that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe the solution you'd like in a clear and concise manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
209
.github/workflows/build.yml
vendored
@ -1,7 +1,12 @@
|
||||
name: Build YouTube Music
|
||||
|
||||
on:
|
||||
- push
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
NODE_VERSION: "20.x"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -10,109 +15,149 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
os: [ macos-latest, ubuntu-latest, windows-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16.x"
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: yarn-cache
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
######################
|
||||
# Patch SnoreToast to fix App ID - see https://github.com/th-ch/youtube-music/issues/479#issuecomment-965473559
|
||||
- name: SnoreToast - parameters
|
||||
id: snoretoast-params
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
shell: bash
|
||||
# Only vite build without release if it is a fork, or it is a pull-request
|
||||
- name: Vite Build
|
||||
if: github.repository == 'th-ch/youtube-music' && github.event_name == 'pull_request'
|
||||
run: |
|
||||
echo "::set-output name=version::v0.8.0"
|
||||
echo "::set-output name=path::./vendor/snoretoast"
|
||||
pnpm build
|
||||
|
||||
- name: SnoreToast - cache
|
||||
id: snoretoast-cache
|
||||
uses: actions/cache@v2
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
with:
|
||||
path: ${{ steps.snoretoast-params.outputs.path }}
|
||||
key: snoretoast-${{ steps.snoretoast-params.outputs.version }}
|
||||
|
||||
- name: SnoreToast - compile
|
||||
if: |
|
||||
startsWith(matrix.os, 'windows') &&
|
||||
steps.snoretoast-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
# Build and release if it's the main repository and is not pull-request
|
||||
- name: Build and release on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
SNORETOAST_TAG="${{ steps.snoretoast-params.outputs.version }}"
|
||||
echo "Compiling SnoreToast $SNORETOAST_TAG"
|
||||
pnpm release:mac
|
||||
|
||||
git config --global user.email "th-ch@users.noreply.github.com"
|
||||
git config --global user.name "YouTube Music"
|
||||
git clone -c advice.detachedHead=false --branch $SNORETOAST_TAG --depth 1 https://github.com/KDE/snoretoast.git ${{ steps.snoretoast-params.outputs.path }}
|
||||
cd ${{ steps.snoretoast-params.outputs.path }}
|
||||
|
||||
# Apply https://github.com/KDE/snoretoast/pull/15/commits/c5faeceaf36f4b9fb27e5269990b716a25ecbe43
|
||||
# Patch generated with `git format-patch -1 c5faeceaf36f4b9fb27e5269990b716a25ecbe43`
|
||||
git am < ../snoretoast-patch/0001-Fix-activation-not-writing-to-pipe.patch
|
||||
|
||||
# Compile for win32
|
||||
cmake -A Win32 -B build32
|
||||
cmake --build build32 --config Release
|
||||
|
||||
# Compile for x64
|
||||
cmake -A x64 -B build64
|
||||
cmake --build build64 --config Release
|
||||
|
||||
- name: SnoreToast - overwrite with custom build
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
shell: bash
|
||||
- name: Build and release on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Override SnoreToast with the patched versions
|
||||
cp ${{ steps.snoretoast-params.outputs.path }}/build32/bin/Release/snoretoast.exe ./node_modules/node-notifier/vendor/snoreToast/snoretoast-x86.exe
|
||||
cp ${{ steps.snoretoast-params.outputs.path }}/build64/bin/Release/snoretoast.exe ./node_modules/node-notifier/vendor/snoreToast/snoretoast-x64.exe
|
||||
# End of SnoreToast patch
|
||||
######################
|
||||
pnpm release:linux
|
||||
|
||||
- name: Build and release on Windows
|
||||
if: startsWith(matrix.os, 'windows') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pnpm release:win
|
||||
|
||||
- name: Test
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
uses: coactions/setup-xvfb@v1
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
with:
|
||||
run: yarn test
|
||||
run: pnpm test:debug
|
||||
|
||||
- name: Build on Mac
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
name: Release YouTube Music
|
||||
if: github.repository == 'th-ch/youtube-music' && github.ref == 'refs/heads/master'
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
yarn run release:mac
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Build on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
yarn run release:linux
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build on Windows
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- name: Get version
|
||||
run: |
|
||||
yarn run release:win
|
||||
echo "VERSION_TAG=v$(node -pe "require('./package.json').version")" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if version already exists in tags
|
||||
run: |
|
||||
echo "VERSION_HASH=$(git rev-parse -q --verify 'refs/tags/${{ env.VERSION_TAG }}')" >> $GITHUB_ENV
|
||||
echo "CHANGELOG_ANCHOR=$(echo $VERSION_TAG | sed -e 's/\.//g')" >> $GITHUB_ENV
|
||||
|
||||
- name: Fetch draft release
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: cardinalby/git-get-release-action@v1
|
||||
id: get_draft_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
latest: true
|
||||
draft: true
|
||||
searchLimit: 1
|
||||
|
||||
- name: Publish Release (if it does not exist)
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
id: ${{ steps.get_draft_release.outputs.id }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
replacename: true
|
||||
name: ${{ env.VERSION_TAG }}
|
||||
replacebody: true
|
||||
body: |
|
||||
See [changelog](https://github.com/th-ch/youtube-music/blob/master/changelog.md#${{ env.CHANGELOG_ANCHOR }}) for the list of updates and the full diff.
|
||||
|
||||
Thanks to all contributors! 🏅
|
||||
|
||||
(Note for Windows: `YouTube-Music-Web-Setup-${{ env.VERSION_TAG }}.exe` is an installer, and `YouTube-Music-${{ env.VERSION_TAG }}.exe` is a portable version)
|
||||
|
||||
- name: Update changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
run: |
|
||||
pnpm changelog
|
||||
|
||||
- name: Commit changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Update changelog for ${{ env.VERSION_TAG }}
|
||||
file_pattern: "changelog.md"
|
||||
commit_user_name: CI
|
||||
commit_user_email: th-ch@users.noreply.github.com
|
||||
|
||||
20
.github/workflows/dependency-review.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: "Dependency Review"
|
||||
on: [ pull_request ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
20
.github/workflows/winget-cla.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Submit CLA to Winget PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_url:
|
||||
description: "Specific PR URL"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment to PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Submit CLA to Windows Package Manager Community Repository Pull Request
|
||||
run: gh pr comment $PR_URL --body "@microsoft-github-policy-service agree"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
PR_URL: ${{ inputs.pr_url }}
|
||||
30
.github/workflows/winget-submission.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Submit to Windows Package Manager Community Repository
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ released ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: "Specific tag name"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
winget:
|
||||
name: Publish winget package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set winget version env
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
run: echo "WINGET_TAG_NAME=$(echo ${TAG_NAME#v})" >> $GITHUB_ENV
|
||||
- name: Submit package to Windows Package Manager Community Repository
|
||||
uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: th-ch.YouTubeMusic
|
||||
installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'
|
||||
version: ${{ env.WINGET_TAG_NAME }}
|
||||
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
fork-user: youtube-music-winget
|
||||
12
.gitignore
vendored
@ -1,5 +1,15 @@
|
||||
node_modules
|
||||
/dist
|
||||
/assets/generated
|
||||
/pack
|
||||
electron-builder.yml
|
||||
.vscode/settings.json
|
||||
.idea
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.vite-inspect
|
||||
|
||||
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
363
README.md
Normal file
@ -0,0 +1,363 @@
|
||||
# YouTube Music
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Read this in other languages: [🇰🇷](./docs/readme/README-ko.md)
|
||||
|
||||
**Electron wrapper around YouTube Music featuring:**
|
||||
|
||||
- Native look & feel, aims at keeping the original interface
|
||||
- Framework for custom plugins: change YouTube Music to your needs (style, content, features), enable/disable plugins in
|
||||
one click
|
||||
|
||||
## Translation
|
||||
|
||||
You can help with translation on [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="translation status" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="translation status 2" />
|
||||
</a>
|
||||
|
||||
## Download
|
||||
|
||||
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).
|
||||
|
||||
### MacOS
|
||||
|
||||
You can install the app using Homebrew (see the [cask definition](https://github.com/th-ch/homebrew-youtube-music)):
|
||||
|
||||
```bash
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
If you install the app manually and get an error "is damaged and can’t be opened." when launching the app, run the following in the Terminal:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
You can use the [Scoop package manager](https://scoop.sh) to install the `youtube-music` package from
|
||||
the [`extras` bucket](https://github.com/ScoopInstaller/Extras).
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
Alternately you can use [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), Windows 11s
|
||||
official CLI package manager to install the `th-ch.YouTubeMusic` package.
|
||||
|
||||
*Note: Microsoft Defender SmartScreen might block the installation since it is from an "unknown publisher". This is also
|
||||
true for the manual installation when trying to run the executable(.exe) after a manual download here on github (same
|
||||
file).*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### How to install without a network connection? (in Windows)
|
||||
|
||||
- Download the `*.nsis.7z` file for _your device architecture_ in [release page](https://github.com/th-ch/youtube-music/releases/latest).
|
||||
- `x64` for 64-bit Windows
|
||||
- `ia32` for 32-bit Windows
|
||||
- `arm64` for ARM64 Windows
|
||||
- Download installer in release page. (`*-Setup.exe`)
|
||||
- Place them in the **same directory**.
|
||||
- Run the installer.
|
||||
|
||||
## Features:
|
||||
|
||||
- **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
|
||||
|
||||
- And more ...
|
||||
|
||||
## Available plugins:
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
|
||||
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
|
||||
|
||||
- **Ambient Mode**: Applies a lighting effect by casting gentle colors from the video, into your screen’s background.
|
||||
|
||||
- **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
|
||||
volume of the softest parts)
|
||||
|
||||
- **Blur Nav Bar**: makes navigation bar transparent and blurry
|
||||
|
||||
- **Bypass age restrictions**: bypass YouTube's age verification
|
||||
|
||||
- **Captions selector**: Enable captions
|
||||
|
||||
- **Compact sidebar**: Always set the sidebar in compact mode
|
||||
|
||||
- **Crossfade**: Crossfade between songs
|
||||
|
||||
- **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)
|
||||
|
||||
- **Exponential Volume**: Makes the volume
|
||||
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
|
||||
select lower volumes.
|
||||
|
||||
- **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
|
||||
|
||||
- **Lumia Stream**: Adds [Lumia Stream](https://lumiastream.com/) support
|
||||
|
||||
- **Lyrics Genius**: Adds lyrics support for most songs
|
||||
|
||||
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||
|
||||
- **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)
|
||||
|
||||
- **Picture in picture**: allows to switch the app to picture-in-picture mode
|
||||
|
||||
- **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**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume
|
||||
steps
|
||||
|
||||
- **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)
|
||||
|
||||
- **Skip-Silences** - Automatically skip silenced sections
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or
|
||||
parts of music videos where the song isn't playing
|
||||
|
||||
- **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
|
||||
|
||||
- **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s
|
||||
plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||
|
||||
- **Video Toggle**: Adds
|
||||
a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to
|
||||
switch between Video/Song mode. can also optionally remove the whole video tab
|
||||
|
||||
- **Visualizer**: Different music visualizers
|
||||
|
||||
## Themes
|
||||
|
||||
You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes).
|
||||
|
||||
Some predefined themes are available in https://github.com/kerichdev/themes-for-ytmdesktop-player.
|
||||
|
||||
## Dev
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Build your own plugins
|
||||
|
||||
Using plugins, you can:
|
||||
|
||||
- manipulate the app - the `BrowserWindow` from electron is passed to the plugin handler
|
||||
- change the front by manipulating the HTML/CSS
|
||||
|
||||
### Creating a plugin
|
||||
|
||||
Create a folder in `plugins/YOUR-PLUGIN-NAME`:
|
||||
|
||||
- `index.ts`: the main file of the plugin
|
||||
```typescript
|
||||
import style from './style.css?inline'; // import style as inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic show restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
stylesheets: [style], // your custom style,
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// All *Config methods are wrapped Promise<T>
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// you can communicate with renderer plugin
|
||||
ipc.handle('some-event', () => {
|
||||
return 'hello';
|
||||
});
|
||||
},
|
||||
// it fired when config changed
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// it fired when plugin disabled
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('some-event'));
|
||||
},
|
||||
// Only renderer available hook
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
|
||||
// set plugin config easily
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Common use cases
|
||||
|
||||
- injecting custom CSS: create a `style.css` file in the same folder then:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // import style as inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic will show a restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
stylesheets: [style], // your custom style
|
||||
renderer() {} // define renderer hook
|
||||
});
|
||||
```
|
||||
|
||||
- If you want to change the HTML:
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic will show the restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
renderer() {
|
||||
// Remove the login button
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // define renderer hook
|
||||
});
|
||||
```
|
||||
|
||||
- communicating between the front and back: can be done using the ipcMain module from electron. See `index.ts` file and
|
||||
example in `sponsorblock` plugin.
|
||||
|
||||
## Build
|
||||
|
||||
1. Clone the repo
|
||||
2. Follow [this guide](https://pnpm.io/installation) to install `pnpm`
|
||||
3. Run `pnpm install --frozen-lockfile` to install dependencies
|
||||
4. Run `pnpm build:OS`
|
||||
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux
|
||||
- `pnpm dist:mac` - MacOS
|
||||
|
||||
Builds the app for macOS, Linux, and Windows,
|
||||
using [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
|
||||
## Production Preview
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Uses [Playwright](https://playwright.dev/) to test the app.
|
||||
|
||||
## License
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## Most asked questions
|
||||
|
||||
### Why apps menu isn't showing up?
|
||||
|
||||
If `Hide Menu` option is on - you can show the menu with the <kbd>alt</kbd> key (or <kbd>\`</kbd> [backtick] if using
|
||||
the in-app-menu plugin)
|
||||
BIN
assets/generated/icons/mac/icon.icns
Normal file
BIN
assets/generated/icons/png/1024x1024.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/generated/icons/png/128x128.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
assets/generated/icons/png/16x16.png
Normal file
|
After Width: | Height: | Size: 600 B |
BIN
assets/generated/icons/png/24x24.png
Normal file
|
After Width: | Height: | Size: 931 B |
BIN
assets/generated/icons/png/256x256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/generated/icons/png/32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/generated/icons/png/48x48.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/generated/icons/png/512x512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/generated/icons/png/64x64.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/generated/icons/win/icon.ico
Normal file
|
After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 192 B After Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 265 B |
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 269 B |
6
assets/youtube-music.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176" width="32" height="32">
|
||||
<circle fill="red" cx="88" cy="88" r="88"/>
|
||||
<path fill="#FFF"
|
||||
d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.9-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/>
|
||||
<path fill="#FFF" d="M72 111l39-24-39-22z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
331
changelog.md
@ -2,8 +2,339 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v3.1.1](https://github.com/th-ch/youtube-music/compare/v3.1.0...v3.1.1)
|
||||
|
||||
- fix: fix renderer plugin load timing [`#1522`](https://github.com/th-ch/youtube-music/issues/1522)
|
||||
- chore(i18n): Translated using Weblate (Lithuanian) [`fc1a7cd`](https://github.com/th-ch/youtube-music/commit/fc1a7cda62b6e33e5f5d57a5a6e0adef6a32bf9a)
|
||||
- chore(i18n): Translated using Weblate (Chinese (Simplified)) [`eba7026`](https://github.com/th-ch/youtube-music/commit/eba7026b89bbfdd3ac07cf728a66ba9bdd274ec0)
|
||||
- chore(deps): update dependency rollup to v4.8.0 [`a601d0b`](https://github.com/th-ch/youtube-music/commit/a601d0b3d2dee0fabad79a18e1a7dd0ca84ccf01)
|
||||
|
||||
#### [v3.1.0](https://github.com/th-ch/youtube-music/compare/v3.0.2...v3.1.0)
|
||||
|
||||
> 11 December 2023
|
||||
|
||||
- chore(deps): update dependency electron to v28 [`#1498`](https://github.com/th-ch/youtube-music/pull/1498)
|
||||
- Enable/Disable Navigation without restart [`#1507`](https://github.com/th-ch/youtube-music/pull/1507)
|
||||
- Turkish(tr)_lang_file [`#1513`](https://github.com/th-ch/youtube-music/pull/1513)
|
||||
- Skip Disliked Songs [`#1505`](https://github.com/th-ch/youtube-music/pull/1505)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.13.2 [`#1452`](https://github.com/th-ch/youtube-music/pull/1452)
|
||||
- fix: Homebrew latest release url parsing [`#1496`](https://github.com/th-ch/youtube-music/pull/1496)
|
||||
- fix: in-player adblocker inject timing issue [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
|
||||
- fix(package.json): fix RPM version `libuuid` issue [`#1508`](https://github.com/th-ch/youtube-music/issues/1508)
|
||||
- Translated using Weblate (Polish) [`7b78ba6`](https://github.com/th-ch/youtube-music/commit/7b78ba67613f14be65a45751efeb06431b405a91)
|
||||
- Translated using Weblate (French) [`ebc0879`](https://github.com/th-ch/youtube-music/commit/ebc087963b23265ff00528c8305d51597abf587a)
|
||||
- Translated using Weblate (Chinese (Traditional)) [`020bdc0`](https://github.com/th-ch/youtube-music/commit/020bdc0811ea45ad6c2853c62a05ae6695c5c4f9)
|
||||
|
||||
#### [v3.0.2](https://github.com/th-ch/youtube-music/compare/v3.0.1...v3.0.2)
|
||||
|
||||
> 3 December 2023
|
||||
|
||||
- fix(adblocker): fix In-Player adblocker [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
|
||||
- fix(menu): crash on linux [`#1477`](https://github.com/th-ch/youtube-music/issues/1477)
|
||||
- fix: update pnpm-lock.yaml [`9e2c6b1`](https://github.com/th-ch/youtube-music/commit/9e2c6b1afa33b5708853c8328946e68ec45b09c3)
|
||||
- Translated using Weblate (Chinese (Traditional)) [`125b69f`](https://github.com/th-ch/youtube-music/commit/125b69fd75a05c3eb893886119e2d9f2332b3e56)
|
||||
- Translated using Weblate (French) [`15c4551`](https://github.com/th-ch/youtube-music/commit/15c455105b5100a8ee2bd0a4631548d3d455f047)
|
||||
|
||||
#### [v3.0.1](https://github.com/th-ch/youtube-music/compare/v3.0.0...v3.0.1)
|
||||
|
||||
> 2 December 2023
|
||||
|
||||
- hotfix(adblocker): fix #1475 [`#1475`](https://github.com/th-ch/youtube-music/issues/1475)
|
||||
- Translated using Weblate (French) [`7f02afc`](https://github.com/th-ch/youtube-music/commit/7f02afc5a6839adfe8437d4e2cc8dee13a93b311)
|
||||
- Update changelog for v3.0.0 [`d8c8bd1`](https://github.com/th-ch/youtube-music/commit/d8c8bd17ecfbdf96ebd29eb4c5748c07876ee242)
|
||||
- Translated using Weblate (German) [`0660f0b`](https://github.com/th-ch/youtube-music/commit/0660f0b7ce6895ef5800f48ade1da2d7f8e0c1f7)
|
||||
|
||||
### [v3.0.0](https://github.com/th-ch/youtube-music/compare/v2.2.0...v3.0.0)
|
||||
|
||||
> 2 December 2023
|
||||
|
||||
- Add text to Translation section [`#1470`](https://github.com/th-ch/youtube-music/pull/1470)
|
||||
- fix(deps): update dependency youtubei.js to v8 [`#1473`](https://github.com/th-ch/youtube-music/pull/1473)
|
||||
- chore(deps): update dependency electron to v27.1.3 [`#1471`](https://github.com/th-ch/youtube-music/pull/1471)
|
||||
- fix(deps): update dependency @xhayper/discord-rpc to v1.1.1 [`#1472`](https://github.com/th-ch/youtube-music/pull/1472)
|
||||
- feat: add support i18n [`#1468`](https://github.com/th-ch/youtube-music/pull/1468)
|
||||
- chore(deps): update dependency electron to v27.1.2 [`#1441`](https://github.com/th-ch/youtube-music/pull/1441)
|
||||
- Nicer Readme [`#1439`](https://github.com/th-ch/youtube-music/pull/1439)
|
||||
- Windows Zoom, ScaleFactor [`#1402`](https://github.com/th-ch/youtube-music/pull/1402)
|
||||
- chore(deps): bump axios from 1.5.1 to 1.6.1 [`#1400`](https://github.com/th-ch/youtube-music/pull/1400)
|
||||
- Updated mac icon to better reflect the Mac styling [`#1395`](https://github.com/th-ch/youtube-music/pull/1395)
|
||||
- feat: rename plugins to clarify context [`#1392`](https://github.com/th-ch/youtube-music/pull/1392)
|
||||
- feat: refactor plugin utils [`#1391`](https://github.com/th-ch/youtube-music/pull/1391)
|
||||
- feat: plugin auto-importer with `vite-plugin-resolve` [`#1385`](https://github.com/th-ch/youtube-music/pull/1385)
|
||||
- feat: migrate from `rollup` to `electron-vite` [`#1364`](https://github.com/th-ch/youtube-music/pull/1364)
|
||||
- feat: enable `context-isolation` [`#1361`](https://github.com/th-ch/youtube-music/pull/1361)
|
||||
- fix: add workaround for `podcast` type video [`#1362`](https://github.com/th-ch/youtube-music/pull/1362)
|
||||
- fix: fix broken menu-layout [`#1360`](https://github.com/th-ch/youtube-music/pull/1360)
|
||||
- Add Homebrew cask install option for MacOS. [`#1357`](https://github.com/th-ch/youtube-music/pull/1357)
|
||||
- feat: changed Zoom shortcuts to standard [`#1458`](https://github.com/th-ch/youtube-music/issues/1458)
|
||||
- fix(in-app-menu): fix #1436 [`#1436`](https://github.com/th-ch/youtube-music/issues/1436)
|
||||
- fix(discord): update application client-id [`#1431`](https://github.com/th-ch/youtube-music/issues/1431)
|
||||
- chore(deps): update dependency electron to v27.0.4 [`#1324`](https://github.com/th-ch/youtube-music/issues/1324)
|
||||
- fix(in-app-menu): panel should close with the window when it is closed [`#1389`](https://github.com/th-ch/youtube-music/issues/1389)
|
||||
- fix: change titleBarOverlay height based on zoomFactor [`#1375`](https://github.com/th-ch/youtube-music/issues/1375)
|
||||
- fix: fixed an issue if "Always on top" is enabled, the dialog is displayed below the window [`#1379`](https://github.com/th-ch/youtube-music/issues/1379)
|
||||
- fix: fix winget version (fix #1363) [`#1363`](https://github.com/th-ch/youtube-music/issues/1363)
|
||||
- feat: run prettier [`a3104fd`](https://github.com/th-ch/youtube-music/commit/a3104fda4b0d58b076d0c737111636a66e468acc)
|
||||
- Translated using Weblate (Korean) [`b4b7ad8`](https://github.com/th-ch/youtube-music/commit/b4b7ad824b8c489ae483eba139b46e5b200231fc)
|
||||
- Translated using Weblate (English) [`d2eabaa`](https://github.com/th-ch/youtube-music/commit/d2eabaa4bbccd89eae529eae52cec035e8e2620c)
|
||||
|
||||
#### [v2.2.0](https://github.com/th-ch/youtube-music/compare/v2.1.3...v2.2.0)
|
||||
|
||||
> 27 October 2023
|
||||
|
||||
- feat(ambient-mode): add config for `ambient-mode` plugin [`#1349`](https://github.com/th-ch/youtube-music/pull/1349)
|
||||
- bump deps [`4248d20`](https://github.com/th-ch/youtube-music/commit/4248d20e8ef926ce7b1d07eb83743755a341d9f6)
|
||||
- Update changelog for v2.1.3 [`dc73561`](https://github.com/th-ch/youtube-music/commit/dc73561c8a8acfc8ba91aff2dc78e4267869f2fd)
|
||||
- Bump version to 2.2.0 [`6288d0b`](https://github.com/th-ch/youtube-music/commit/6288d0b171a65ea015922cdf3af6c7bd9a1f269b)
|
||||
|
||||
#### [v2.1.3](https://github.com/th-ch/youtube-music/compare/v2.1.2...v2.1.3)
|
||||
|
||||
> 23 October 2023
|
||||
|
||||
- fix: fixed bugs in downloader [`#1342`](https://github.com/th-ch/youtube-music/pull/1342)
|
||||
- feat(discord): rename `Listen Along` to `Play on YTM` [`#1341`](https://github.com/th-ch/youtube-music/issues/1341)
|
||||
- chore(deps): bump deps [`4333891`](https://github.com/th-ch/youtube-music/commit/4333891ccabe42aedf756fd48618be715db13262)
|
||||
- Update changelog for v2.1.2 [`fa4c69d`](https://github.com/th-ch/youtube-music/commit/fa4c69d228d4e06a7858e2b22fcdfa075a8ca766)
|
||||
- fix(store): fix listenAlong statement [`bceaa05`](https://github.com/th-ch/youtube-music/commit/bceaa05197d47a4a4bbd22e767d1e4d6ec277514)
|
||||
|
||||
#### [v2.1.2](https://github.com/th-ch/youtube-music/compare/v2.1.1...v2.1.2)
|
||||
|
||||
> 19 October 2023
|
||||
|
||||
- feat(in-app-menu): add an option to hide the window controls [`#1335`](https://github.com/th-ch/youtube-music/pull/1335)
|
||||
- fix: fixed an issue where the album name was missing [`#1334`](https://github.com/th-ch/youtube-music/pull/1334)
|
||||
- chore(deps): update dependency electron to v27.0.1 [`#1331`](https://github.com/th-ch/youtube-music/pull/1331)
|
||||
- fix: fixed an issue where only the first 100 songs in a playlist were downloaded [`#1329`](https://github.com/th-ch/youtube-music/pull/1329)
|
||||
- Updated readme plugins list [`#1326`](https://github.com/th-ch/youtube-music/pull/1326)
|
||||
- QOL: Move source code under the src directory. [`#1318`](https://github.com/th-ch/youtube-music/pull/1318)
|
||||
- feat: migrate from `npm` to `pnpm` [`#1316`](https://github.com/th-ch/youtube-music/pull/1316)
|
||||
- fix: fix unresponsive (fix #1325) [`#1325`](https://github.com/th-ch/youtube-music/issues/1325)
|
||||
- fix(blocker): remove the `app.isPackaged` check (fix #1315) [`#1315`](https://github.com/th-ch/youtube-music/issues/1315)
|
||||
- fix(discord): `Discord RPC fails if a song's title is only one character` (fix #1314) [`#1314`](https://github.com/th-ch/youtube-music/issues/1314)
|
||||
- chore(deps): Bump @rollup/plugin-commonjs, pnpm version, Remove ytpl [`9705f84`](https://github.com/th-ch/youtube-music/commit/9705f8489d7bf262bfd8b15ab84c2d3485f10eae)
|
||||
- chore(deps): Bump rollup, @xhayper/discord-rpc version [`00a3e8d`](https://github.com/th-ch/youtube-music/commit/00a3e8d35ec335e1913be19f30ae09dbe0b7acdd)
|
||||
- chore(deps): update dependency rollup to v4.1.4 [`6774d54`](https://github.com/th-ch/youtube-music/commit/6774d54f5eca432edc2e11743d9d1b1c2fda9ac8)
|
||||
|
||||
#### [v2.1.1](https://github.com/th-ch/youtube-music/compare/v2.1.0...v2.1.1)
|
||||
|
||||
> 14 October 2023
|
||||
|
||||
- hotfix(downloader): can't get an album title (fix #1313) [`#1313`](https://github.com/th-ch/youtube-music/issues/1313)
|
||||
- Update changelog for v2.1.0 [`92cab89`](https://github.com/th-ch/youtube-music/commit/92cab89d17175741e60e65ea61633e23ebdc1f45)
|
||||
- Bump version to 2.1.1 [`3bb5bc2`](https://github.com/th-ch/youtube-music/commit/3bb5bc2ca1856f4e222ee1e01e865f1ab804fdba)
|
||||
- Add "about" menu to show app version [`21c45fa`](https://github.com/th-ch/youtube-music/commit/21c45faf2043cf72a7c14d5cf6c8d848d0448528)
|
||||
|
||||
#### [v2.1.0](https://github.com/th-ch/youtube-music/compare/v2.0.4...v2.1.0)
|
||||
|
||||
> 14 October 2023
|
||||
|
||||
- feat(downloader): Added support for audio format auto-detection [`#1310`](https://github.com/th-ch/youtube-music/pull/1310)
|
||||
- feat(in-app-menu): enable in-app-menu by default (in Windows) [`#1311`](https://github.com/th-ch/youtube-music/pull/1311)
|
||||
- fix: winget publish [`#1307`](https://github.com/th-ch/youtube-music/pull/1307)
|
||||
- hotfix(downloader): fix invalid query selector (fix #1308) [`#1308`](https://github.com/th-ch/youtube-music/issues/1308)
|
||||
- chore(deps): bump dependencies [`3c6b3ae`](https://github.com/th-ch/youtube-music/commit/3c6b3aeff0aae32adb2f2ad9c091b0a9701d3c24)
|
||||
- chore(actions): create winget-cla.yml [`37181a7`](https://github.com/th-ch/youtube-music/commit/37181a7b5e2aa5bed6a36298eac3a66aac2762b8)
|
||||
- Update changelog for v2.0.4 [`e9398ad`](https://github.com/th-ch/youtube-music/commit/e9398adac34a8abb11801e32999a915a8be0ece6)
|
||||
|
||||
#### [v2.0.4](https://github.com/th-ch/youtube-music/compare/v2.0.3...v2.0.4)
|
||||
|
||||
> 12 October 2023
|
||||
|
||||
- hotfix(adblocker): fix `ipcRenderer.sendSync() with ...` [`#1301`](https://github.com/th-ch/youtube-music/pull/1301)
|
||||
- fix(downloader): Korean filename is broken on non-macOS devices [`#1297`](https://github.com/th-ch/youtube-music/pull/1297)
|
||||
- chore(deps): bump deps [`b6894dc`](https://github.com/th-ch/youtube-music/commit/b6894dca2974c63fa2945d3a4995665d11eb2a78)
|
||||
- fix: bump dependencies [`7aa970c`](https://github.com/th-ch/youtube-music/commit/7aa970cebc8e1407ff6937b402ba303e14c73efd)
|
||||
- fix(downloader): private playlist download [`1d5b299`](https://github.com/th-ch/youtube-music/commit/1d5b2997bd0c72c1c007c57b145509e4a8f77fef)
|
||||
|
||||
#### [v2.0.3](https://github.com/th-ch/youtube-music/compare/v2.0.2...v2.0.3)
|
||||
|
||||
> 10 October 2023
|
||||
|
||||
- feat(discord): add `Hide GitHub link Button` [`#1293`](https://github.com/th-ch/youtube-music/pull/1293)
|
||||
- feat(deps): bundle `youtubei.js` (temporary solution) [`#1292`](https://github.com/th-ch/youtube-music/pull/1292)
|
||||
- fix(mpris): fixed an issue where MPRIS information was incorrect [`#1291`](https://github.com/th-ch/youtube-music/pull/1291)
|
||||
- fix(discord): fixed an issue where `timeChanged` was not being applied to Discord activities [`#1290`](https://github.com/th-ch/youtube-music/pull/1290)
|
||||
- Fix: typo in README [`#1286`](https://github.com/th-ch/youtube-music/pull/1286)
|
||||
- fix: chore(deps): update dependency @jellybrick/mpris-service to 2.1.4 (fix #971) [`#971`](https://github.com/th-ch/youtube-music/issues/971)
|
||||
- chore(deps): Bump `@cliqz/adblocker-electron` to 1.26.8 (fix #1269) [`#1269`](https://github.com/th-ch/youtube-music/issues/1269)
|
||||
- fix: missing icons taskbar-mediacontrol [`fbf4b3b`](https://github.com/th-ch/youtube-music/commit/fbf4b3b8b5e39c61975e67efc990c45f62de76d8)
|
||||
- remove: migration scripts [`52ba2dc`](https://github.com/th-ch/youtube-music/commit/52ba2dc9ffd8e235251d1279686f55e33b3fa3bb)
|
||||
- feat: add migration script [`926b9fb`](https://github.com/th-ch/youtube-music/commit/926b9fb5e6db69b69935ec5d7be9a76a84e54ceb)
|
||||
|
||||
#### [v2.0.2](https://github.com/th-ch/youtube-music/compare/v2.0.1...v2.0.2)
|
||||
|
||||
> 8 October 2023
|
||||
|
||||
- fix: discord-rpc [`#1278`](https://github.com/th-ch/youtube-music/pull/1278)
|
||||
- Bump version to 2.0.2 [`b5dbfaf`](https://github.com/th-ch/youtube-music/commit/b5dbfaf68691a546d72f5c1818fd3a44802eb0fa)
|
||||
- Merge pull request #1272 from th-ch/feat/resolves-1265 [`6b7fd5b`](https://github.com/th-ch/youtube-music/commit/6b7fd5ba630888de08004105179c059c6d93e028)
|
||||
- Merge pull request #1279 from th-ch/fix/1274 [`73a049a`](https://github.com/th-ch/youtube-music/commit/73a049a7bc5161f0d53c252cf510f1e2a6f6eeb3)
|
||||
|
||||
#### [v2.0.1](https://github.com/th-ch/youtube-music/compare/v2.0.0...v2.0.1)
|
||||
|
||||
> 8 October 2023
|
||||
|
||||
- Update changelog for v2.0.0 [`2d69dfd`](https://github.com/th-ch/youtube-music/commit/2d69dfd333c3223ecc7de13a0abc98fd99aa3a2b)
|
||||
- hotfix: hotfix for #1267 [`c002263`](https://github.com/th-ch/youtube-music/commit/c002263c3bdd51890b8ffb431283afb60405d8fe)
|
||||
- Bump version to 2.0.1 [`a1f025e`](https://github.com/th-ch/youtube-music/commit/a1f025e23c599fe5eb63b32ea38ee81200d232d6)
|
||||
|
||||
### [v2.0.0](https://github.com/th-ch/youtube-music/compare/v1.20.0...v2.0.0)
|
||||
|
||||
> 7 October 2023
|
||||
|
||||
- Bump version to 2.0.0 [`#1257`](https://github.com/th-ch/youtube-music/pull/1257)
|
||||
- feat(GitHub): add issue template [`#1264`](https://github.com/th-ch/youtube-music/pull/1264)
|
||||
- feat: I guess it's TypeScript [`#1235`](https://github.com/th-ch/youtube-music/pull/1235)
|
||||
- chore(deps): update dependency rollup to v4 [`#44`](https://github.com/th-ch/youtube-music/pull/44)
|
||||
- feat: apply rollup 🚀 [`#20`](https://github.com/th-ch/youtube-music/pull/20)
|
||||
- fix: Fixes the video-toggle being displayed at the wrong position on fullscreen [`#1218`](https://github.com/th-ch/youtube-music/pull/1218)
|
||||
- Change Winget Releaser job to `ubuntu-latest` [`#1225`](https://github.com/th-ch/youtube-music/pull/1225)
|
||||
- Fixes the video-toggle being displayed at the wrong position on fullscreen [`#1218`](https://github.com/th-ch/youtube-music/pull/1218)
|
||||
- Fix Remove upgrade button [`#1206`](https://github.com/th-ch/youtube-music/pull/1206)
|
||||
- Fixed Age Restriction Bypass [`#1221`](https://github.com/th-ch/youtube-music/pull/1221)
|
||||
- fix(tuna): handle `playPaused` [`#1`](https://github.com/th-ch/youtube-music/pull/1)
|
||||
- Add plugin to always use the compact sidebar [`#1190`](https://github.com/th-ch/youtube-music/pull/1190)
|
||||
- Hide login elements [`#1189`](https://github.com/th-ch/youtube-music/pull/1189)
|
||||
- Fix navigation arrows [`#1191`](https://github.com/th-ch/youtube-music/pull/1191)
|
||||
- MacOS better copy paste in readme.md [`#1156`](https://github.com/th-ch/youtube-music/pull/1156)
|
||||
- feat(build-windows): Add support for IA32 (resolves #1110) [`#1110`](https://github.com/th-ch/youtube-music/issues/1110)
|
||||
- fix: fix the downloader to work in a proxy environment (resolve #46) [`#46`](https://github.com/th-ch/youtube-music/issues/46)
|
||||
- fix: fix #34 [`#34`](https://github.com/th-ch/youtube-music/issues/34)
|
||||
- fix: fix #32 [`#32`](https://github.com/th-ch/youtube-music/issues/32)
|
||||
- fix: fix #29 [`#29`](https://github.com/th-ch/youtube-music/issues/29)
|
||||
- fix: fix #30 [`#30`](https://github.com/th-ch/youtube-music/issues/30)
|
||||
- fix: fix #29 [`#29`](https://github.com/th-ch/youtube-music/issues/29)
|
||||
- fix: fix #30 [`#30`](https://github.com/th-ch/youtube-music/issues/30)
|
||||
- hotfix: fix #28 [`#28`](https://github.com/th-ch/youtube-music/issues/28)
|
||||
- fix: resolve #12 [`#12`](https://github.com/th-ch/youtube-music/issues/12)
|
||||
- fix(precise-volume): fix slider ui does not sync [`#15`](https://github.com/th-ch/youtube-music/issues/15)
|
||||
- fix(video-toggle): fix video config not load config [`#16`](https://github.com/th-ch/youtube-music/issues/16)
|
||||
- refactor(in-app-menu): refactor in-app-menu plugin [`#13`](https://github.com/th-ch/youtube-music/issues/13)
|
||||
- feat(disable-autoplay): add `apply once`, resolve #9 [`#9`](https://github.com/th-ch/youtube-music/issues/9)
|
||||
- fix: fix #4 [`#4`](https://github.com/th-ch/youtube-music/issues/4)
|
||||
- fix: fix #7 [`#7`](https://github.com/th-ch/youtube-music/issues/7)
|
||||
- fix: fix #1187 [`#1187`](https://github.com/th-ch/youtube-music/issues/1187)
|
||||
- fix: resolves #978 [`#978`](https://github.com/th-ch/youtube-music/issues/978)
|
||||
- fix: resolves #958 [`#958`](https://github.com/th-ch/youtube-music/issues/958)
|
||||
- Merge pull request #1259 from organization/feat/fork-to-main [`457a8b5`](https://github.com/th-ch/youtube-music/commit/457a8b5018695d82b043cb7fa7264fbcf43f996c)
|
||||
- fix: remove `xo`, migration to `eslint` [`c722896`](https://github.com/th-ch/youtube-music/commit/c722896a73cfbca3bbbab67bfcdfa639474e9030)
|
||||
- fix: rollback changelog [`9048da2`](https://github.com/th-ch/youtube-music/commit/9048da22f98b9091ab606464a6cbdaad8bc185ae)
|
||||
|
||||
#### [v1.20.0](https://github.com/th-ch/youtube-music/compare/v1.19.0...v1.20.0)
|
||||
|
||||
> 18 May 2023
|
||||
|
||||
- Bump version to 1.20.0 [`#1117`](https://github.com/th-ch/youtube-music/pull/1117)
|
||||
- Multiple implementations for the Adblocker plugin [`#1134`](https://github.com/th-ch/youtube-music/pull/1134)
|
||||
- add xesam:url mpris from songInfo.url [`#1138`](https://github.com/th-ch/youtube-music/pull/1138)
|
||||
- revert adblocker bump [`#1124`](https://github.com/th-ch/youtube-music/pull/1124)
|
||||
- fix security issues in dependencies [`#1116`](https://github.com/th-ch/youtube-music/pull/1116)
|
||||
- commit assets/generated [`#1118`](https://github.com/th-ch/youtube-music/pull/1118)
|
||||
- remove `electron.remote` dependency [`#1113`](https://github.com/th-ch/youtube-music/pull/1113)
|
||||
- .gitattributes set `eol=lf` on *all* files [`#1115`](https://github.com/th-ch/youtube-music/pull/1115)
|
||||
- [crossfade] add `[beta]` tag to warn of possible bugs [`#1096`](https://github.com/th-ch/youtube-music/pull/1096)
|
||||
- [crossfade] add menu options [`#1065`](https://github.com/th-ch/youtube-music/pull/1065)
|
||||
- [captions-selector] add `autoload` option [`#1079`](https://github.com/th-ch/youtube-music/pull/1079)
|
||||
- [downloader] Cleanup metadata [`#1091`](https://github.com/th-ch/youtube-music/pull/1091)
|
||||
- fix protocol handler on unix [`#1099`](https://github.com/th-ch/youtube-music/pull/1099)
|
||||
- fix merge conflict mistake in #1032 [`#1090`](https://github.com/th-ch/youtube-music/pull/1090)
|
||||
- Create providers/decorators.js [`#1068`](https://github.com/th-ch/youtube-music/pull/1068)
|
||||
- [adblocker] fix ads showing on program start [`#1100`](https://github.com/th-ch/youtube-music/pull/1100)
|
||||
- Allow downloading age restricted videos [`#1086`](https://github.com/th-ch/youtube-music/pull/1086)
|
||||
- add starting page option [`#1073`](https://github.com/th-ch/youtube-music/pull/1073)
|
||||
- [downloader] plugin overhaul [`#1054`](https://github.com/th-ch/youtube-music/pull/1054)
|
||||
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.25.2 to 1.26.0 [`#1070`](https://github.com/th-ch/youtube-music/pull/1070)
|
||||
- [in-app-menu] fix css style of the library of uploaded songs [`#1072`](https://github.com/th-ch/youtube-music/pull/1072)
|
||||
- add option to hide the like buttons [`#1077`](https://github.com/th-ch/youtube-music/pull/1077)
|
||||
- Nitpick: Fix name casing in tray icon tooltip [`#1081`](https://github.com/th-ch/youtube-music/pull/1081)
|
||||
- [lyrics-genius] Improved reliability of east asian language detection #1080 [`#1082`](https://github.com/th-ch/youtube-music/pull/1082)
|
||||
- Add dynamic synced plugin config provider [`#1064`](https://github.com/th-ch/youtube-music/pull/1064)
|
||||
- [captions-selector] fix button showing when there aren't any captions available [`#1063`](https://github.com/th-ch/youtube-music/pull/1063)
|
||||
- [in-app-menu] fix items hidden by navbar in library [`#1067`](https://github.com/th-ch/youtube-music/pull/1067)
|
||||
- Fix Youtube Music logo is draggable [`#1061`](https://github.com/th-ch/youtube-music/pull/1061)
|
||||
- fix build action failing on forks, and run it on pull requests [`#1069`](https://github.com/th-ch/youtube-music/pull/1069)
|
||||
- try to fix songInfo time&album [`#1032`](https://github.com/th-ch/youtube-music/pull/1032)
|
||||
- [lyrics] Romanization toggle for Genius plugin [`#1039`](https://github.com/th-ch/youtube-music/pull/1039)
|
||||
- [Snyk] Upgrade html-to-text from 9.0.3 to 9.0.4 [`#1056`](https://github.com/th-ch/youtube-music/pull/1056)
|
||||
- [in-app-menu] add toggle menu icon [`#988`](https://github.com/th-ch/youtube-music/pull/988)
|
||||
- Fix playback speed slider not showing and PiP button showing when it shouldn't [`#1048`](https://github.com/th-ch/youtube-music/pull/1048)
|
||||
- [lyrics-genius] Fix lyrics not showing up or showing up when they shouldn't [`#1052`](https://github.com/th-ch/youtube-music/pull/1052)
|
||||
- [in-app-menu] disable nav-bar drag when menu is open [`#1055`](https://github.com/th-ch/youtube-music/pull/1055)
|
||||
- [Notifications] [Windows] Native interactive notifications [`#946`](https://github.com/th-ch/youtube-music/pull/946)
|
||||
- automate winget releases [`#1049`](https://github.com/th-ch/youtube-music/pull/1049)
|
||||
- build win target on ARM [`#1029`](https://github.com/th-ch/youtube-music/pull/1029)
|
||||
- feat: auto reconnect rpc and CSP fix [`#961`](https://github.com/th-ch/youtube-music/pull/961)
|
||||
- [in-app-menu] make navbar draggable [`#989`](https://github.com/th-ch/youtube-music/pull/989)
|
||||
- Add option `useNativePiP` in PiP plugin to use native PiP [`#1013`](https://github.com/th-ch/youtube-music/pull/1013)
|
||||
- [PiP] fix hotkey activating when typing in the search box [`#1025`](https://github.com/th-ch/youtube-music/pull/1025)
|
||||
- [PiP] Remove titlebar when in-app-menu is enabled [`#1024`](https://github.com/th-ch/youtube-music/pull/1024)
|
||||
- [Shortcuts] MPRIS fixes, Repeat Language bug fix [`#1005`](https://github.com/th-ch/youtube-music/pull/1005)
|
||||
- Build without release in forks [`#1023`](https://github.com/th-ch/youtube-music/pull/1023)
|
||||
- [in-app-menu] fix navbar position [`#997`](https://github.com/th-ch/youtube-music/pull/997)
|
||||
- Migrate to yarn v3 [`#1022`](https://github.com/th-ch/youtube-music/pull/1022)
|
||||
- [precise-volume] fix arrows shortcuts active in search box [`#1002`](https://github.com/th-ch/youtube-music/pull/1002)
|
||||
- [new plugin] Add first version for crossfade plugin [`#1012`](https://github.com/th-ch/youtube-music/pull/1012)
|
||||
- Fix bypass-age-restriction lib import [`#984`](https://github.com/th-ch/youtube-music/pull/984)
|
||||
- Add menu entry to copy current URL [`#977`](https://github.com/th-ch/youtube-music/pull/977)
|
||||
- Remove deprecated code [`#979`](https://github.com/th-ch/youtube-music/pull/979)
|
||||
- Update dev dependencies [`#976`](https://github.com/th-ch/youtube-music/pull/976)
|
||||
- Update electron and various dependencies [`#974`](https://github.com/th-ch/youtube-music/pull/974)
|
||||
- Add CI job for dependency review [`#973`](https://github.com/th-ch/youtube-music/pull/973)
|
||||
- Improve captions plugin [`#972`](https://github.com/th-ch/youtube-music/pull/972)
|
||||
- fix malformed json in tuna-obs [`#817`](https://github.com/th-ch/youtube-music/pull/817)
|
||||
- Add Captions selector [`#866`](https://github.com/th-ch/youtube-music/pull/866)
|
||||
- fix SnoreToast implementation [`#941`](https://github.com/th-ch/youtube-music/pull/941)
|
||||
- Bump json5 from 1.0.1 to 1.0.2 [`#942`](https://github.com/th-ch/youtube-music/pull/942)
|
||||
- [Snyk] Upgrade custom-electron-titlebar from 4.1.3 to 4.1.5 [`#969`](https://github.com/th-ch/youtube-music/pull/969)
|
||||
- Fixed video-toggle aligning running before #main-panel exists [`#956`](https://github.com/th-ch/youtube-music/pull/956)
|
||||
- [New plugin] Music visualizers [`#953`](https://github.com/th-ch/youtube-music/pull/953)
|
||||
- fix PiP buttons not showing up [`#964`](https://github.com/th-ch/youtube-music/pull/964)
|
||||
- Use same audio context/source everywhere [`#951`](https://github.com/th-ch/youtube-music/pull/951)
|
||||
- revert adblocker bump [`#1105`](https://github.com/th-ch/youtube-music/issues/1105)
|
||||
- Allow downloading age restricted videos [`#1084`](https://github.com/th-ch/youtube-music/issues/1084)
|
||||
- add option to hide the like buttons [`#1075`](https://github.com/th-ch/youtube-music/issues/1075)
|
||||
- add starting page option [`#1071`](https://github.com/th-ch/youtube-music/issues/1071)
|
||||
- add slight delay to lyrics genius [`#1041`](https://github.com/th-ch/youtube-music/issues/1041)
|
||||
- fix unescaped url params [`#1050`](https://github.com/th-ch/youtube-music/issues/1050)
|
||||
- fix playback speed selector [`#1045`](https://github.com/th-ch/youtube-music/issues/1045)
|
||||
- fix PiP button [`#959`](https://github.com/th-ch/youtube-music/issues/959)
|
||||
- fix security issues in deps [`9cde19d`](https://github.com/th-ch/youtube-music/commit/9cde19d906081fe1851f90fa44581b2b74c328e3)
|
||||
- rome lint [`325026e`](https://github.com/th-ch/youtube-music/commit/325026e3eae3daed33a6d66d1ef9f898d6805b28)
|
||||
- lint [`b652a01`](https://github.com/th-ch/youtube-music/commit/b652a011a5a08978db6660aeca6908c47a7cf07a)
|
||||
|
||||
#### [v1.19.0](https://github.com/th-ch/youtube-music/compare/v1.18.0...v1.19.0)
|
||||
|
||||
> 31 December 2022
|
||||
|
||||
- Automatic release by CI when version is updated [`#936`](https://github.com/th-ch/youtube-music/pull/936)
|
||||
- Center toggle of video-toggle [`#894`](https://github.com/th-ch/youtube-music/pull/894)
|
||||
- Load plugins as soon as the window is created [`#890`](https://github.com/th-ch/youtube-music/pull/890)
|
||||
- Bump qs from 6.5.2 to 6.5.3 [`#913`](https://github.com/th-ch/youtube-music/pull/913)
|
||||
- [Snyk] Upgrade custom-electron-titlebar from 4.1.1 to 4.1.2 [`#900`](https://github.com/th-ch/youtube-music/pull/900)
|
||||
- Add option in skip-silences plugin to only skip at the beginning [`#931`](https://github.com/th-ch/youtube-music/pull/931)
|
||||
- Replace rimraf by del-cli [`#932`](https://github.com/th-ch/youtube-music/pull/932)
|
||||
- docs: Added winget install instructions [`#873`](https://github.com/th-ch/youtube-music/pull/873)
|
||||
- [Snyk] Upgrade async-mutex from 0.3.2 to 0.4.0 [`#855`](https://github.com/th-ch/youtube-music/pull/855)
|
||||
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.25.0 to 1.25.1 [`#856`](https://github.com/th-ch/youtube-music/pull/856)
|
||||
- [Snyk] Upgrade custom-electron-titlebar from 4.1.0 to 4.1.1 [`#865`](https://github.com/th-ch/youtube-music/pull/865)
|
||||
- [Snyk] Upgrade @ffmpeg/ffmpeg from 0.11.5 to 0.11.6 [`#876`](https://github.com/th-ch/youtube-music/pull/876)
|
||||
- Discord Plugin RPC Fix [`#888`](https://github.com/th-ch/youtube-music/pull/888)
|
||||
- Bump FFMpeg [`#854`](https://github.com/th-ch/youtube-music/pull/854)
|
||||
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.8 to 1.23.9 [`#823`](https://github.com/th-ch/youtube-music/pull/823)
|
||||
- [Snyk] Upgrade electron-store from 8.0.2 to 8.1.0 [`#801`](https://github.com/th-ch/youtube-music/pull/801)
|
||||
- proposal: Adding an option to hide duration before the song ends [`#802`](https://github.com/th-ch/youtube-music/pull/802)
|
||||
- [Snyk] Security upgrade node-fetch from 2.6.7 to 3.2.10 [`#790`](https://github.com/th-ch/youtube-music/pull/790)
|
||||
- Update README.md with a new theme repo [`#807`](https://github.com/th-ch/youtube-music/pull/807)
|
||||
- Fix likes on touchbar (they were inverted) [`#822`](https://github.com/th-ch/youtube-music/pull/822)
|
||||
- Add Scoop install directions for Windows 🪟 [`#839`](https://github.com/th-ch/youtube-music/pull/839)
|
||||
- Bump version and change release type when publishing a new version [`31ab27c`](https://github.com/th-ch/youtube-music/commit/31ab27c39ff6319116a6514d952eed1f02dd45fd)
|
||||
- Lock node-fetch to v2 for commonJS [`c9f610f`](https://github.com/th-ch/youtube-music/commit/c9f610f7fcfcce1317338364045ab0e1bf4249a4)
|
||||
- fix: upgrade @cliqz/adblocker-electron from 1.25.0 to 1.25.1 [`762ef4e`](https://github.com/th-ch/youtube-music/commit/762ef4eede29b53aae912b3b50a1ca53f6765c53)
|
||||
|
||||
#### [v1.18.0](https://github.com/th-ch/youtube-music/compare/v1.17.0...v1.18.0)
|
||||
|
||||
> 5 September 2022
|
||||
|
||||
- Bump ytdl-core (bug fix) [`#816`](https://github.com/th-ch/youtube-music/pull/816)
|
||||
- Bump electron and fix tests in CI [`#813`](https://github.com/th-ch/youtube-music/pull/813)
|
||||
- Allow user to pass custom CSS file [`#800`](https://github.com/th-ch/youtube-music/pull/800)
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
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,
|
||||
autoResetAppCache: false,
|
||||
resumeOnStart: true,
|
||||
proxy: "",
|
||||
},
|
||||
plugins: {
|
||||
// Enabled plugins
|
||||
navigation: {
|
||||
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
|
||||
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",
|
||||
},
|
||||
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
|
||||
listenAlong: true, // add a "listen along" button to rich presence
|
||||
},
|
||||
notifications: {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: "normal", //has effect only on Linux
|
||||
interactive: false //has effect only on Windows
|
||||
},
|
||||
"precise-volume": {
|
||||
enabled: false,
|
||||
steps: 1, //percentage of volume to change
|
||||
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts
|
||||
globalShortcuts: {
|
||||
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,
|
||||
mode: "custom",
|
||||
forceHide: false,
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"enabled": false,
|
||||
"alwaysOnTop": true,
|
||||
"savePosition": true,
|
||||
"saveSize": false,
|
||||
"hotkey": "P"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = defaultConfig;
|
||||
@ -1,30 +0,0 @@
|
||||
const defaultConfig = require("./defaults");
|
||||
const plugins = require("./plugins");
|
||||
const store = require("./store");
|
||||
const { restart } = require("../providers/app-controls");
|
||||
|
||||
const set = (key, value) => {
|
||||
store.set(key, value);
|
||||
};
|
||||
|
||||
function setMenuOption(key, value) {
|
||||
set(key, value);
|
||||
if (store.get("options.restartOnConfigChanges")) restart();
|
||||
}
|
||||
|
||||
const get = (key) => {
|
||||
return store.get(key);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
defaultConfig,
|
||||
get,
|
||||
set,
|
||||
setMenuOption,
|
||||
edit: () => store.openInEditor(),
|
||||
watch: (cb) => {
|
||||
store.onDidChange("options", cb);
|
||||
store.onDidChange("plugins", cb);
|
||||
},
|
||||
plugins,
|
||||
};
|
||||
@ -1,53 +0,0 @@
|
||||
const store = require("./store");
|
||||
const { restart } = require("../providers/app-controls");
|
||||
|
||||
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 setMenuOptions(plugin, options) {
|
||||
setOptions(plugin, options);
|
||||
if (store.get("options.restartOnConfigChanges")) restart();
|
||||
}
|
||||
|
||||
function getOptions(plugin) {
|
||||
return store.get("plugins")[plugin];
|
||||
}
|
||||
|
||||
function enable(plugin) {
|
||||
setMenuOptions(plugin, { enabled: true });
|
||||
}
|
||||
|
||||
function disable(plugin) {
|
||||
setMenuOptions(plugin, { enabled: false });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isEnabled,
|
||||
getEnabled,
|
||||
enable,
|
||||
disable,
|
||||
setOptions,
|
||||
setMenuOptions,
|
||||
getOptions,
|
||||
};
|
||||
@ -1,96 +0,0 @@
|
||||
const Store = require("electron-store");
|
||||
|
||||
const defaults = require("./defaults");
|
||||
|
||||
const setDefaultPluginOptions = (store, plugin) => {
|
||||
if (!store.get(`plugins.${plugin}`)) {
|
||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||
}
|
||||
}
|
||||
|
||||
const migrations = {
|
||||
">=1.17.0": (store) => {
|
||||
setDefaultPluginOptions(store, "picture-in-picture");
|
||||
|
||||
if (store.get("plugins.video-toggle.mode") === undefined) {
|
||||
store.set("plugins.video-toggle.mode", "custom");
|
||||
}
|
||||
},
|
||||
">=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);
|
||||
}
|
||||
},
|
||||
">=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,
|
||||
});
|
||||
@ -1 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"><g transform="translate(183.604 196.396)" stroke="#fff" stroke-width="2.23"><path style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;block-progression:tb;marker:none" d="M-116.99 106.245l31.82 31.82 236.31-236.31-31.82-31.82z" color="#000" font-weight="400" font-family="Sans" overflow="visible" fill="#fff" stroke="none"/><circle r="171.304" cy="4" cx="16" fill="none" stroke-width="44.6"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
|
||||
<g transform="translate(183.604 196.396)" stroke="#fff" stroke-width="2.23">
|
||||
<path
|
||||
style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;block-progression:tb;marker:none"
|
||||
d="M-116.99 106.245l31.82 31.82 236.31-236.31-31.82-31.82z" color="#000" font-weight="400"
|
||||
font-family="Sans" overflow="visible" fill="#fff" stroke="none"/>
|
||||
<circle r="171.304" cy="4" cx="16" fill="none" stroke-width="44.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 588 B |
@ -1 +1,23 @@
|
||||
<svg width="1440" height="347" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#606483" stop-opacity="0" offset="0%"/><stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b"><stop stop-color="#0B0D19" offset="0%"/><stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path d="M177.486 208.219c78.18 89.285 218.65-81.067 218.65-119.337 0-38.27-86.408-69.295-193-69.295-106.59 0-193 31.024-193 69.295 0 38.27 89.17 30.051 167.35 119.337z" transform="rotate(6 -140.175 3980.948)" fill="url(#a)"/><path d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z" fill="url(#a)" transform="rotate(24 321.92 -247.724)"/><path d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z" fill="url(#b)" transform="rotate(24 338.741 -285.505)"/></g></svg>
|
||||
<svg width="1440" height="347" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a">
|
||||
<stop stop-color="#606483" stop-opacity="0" offset="0%"/>
|
||||
<stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b">
|
||||
<stop stop-color="#0B0D19" offset="0%"/>
|
||||
<stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M177.486 208.219c78.18 89.285 218.65-81.067 218.65-119.337 0-38.27-86.408-69.295-193-69.295-106.59 0-193 31.024-193 69.295 0 38.27 89.17 30.051 167.35 119.337z"
|
||||
transform="rotate(6 -140.175 3980.948)" fill="url(#a)"/>
|
||||
<path
|
||||
d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z"
|
||||
fill="url(#a)" transform="rotate(24 321.92 -247.724)"/>
|
||||
<path
|
||||
d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z"
|
||||
fill="url(#b)" transform="rotate(24 338.741 -285.505)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@ -1 +1,32 @@
|
||||
<svg width="1440" height="318" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="38.706%" y1="-187.115%" x2="18.675%" y2="110.984%" id="a"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#c3352e" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="c"><stop stop-color="#606483" stop-opacity="0" offset="0%"/><stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="d"><stop stop-color="#0B0D19" stop-opacity=".32" offset="0%"/><stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/></linearGradient><filter id="b"><feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/><feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/><feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/><feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M88.494 90c67.04 7.177 161.094-24.753 224.996-90H.2c25.3 48.079 42.361 85.083 88.294 90z" transform="translate(1051)" fill="url(#a)" filter="url(#b)"/><path d="M250.464 367.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z" fill="url(#c)" transform="rotate(143 810.285 354.367)"/><path d="M373.408 256.178c88.026 32.429 156-25.04 156-55.929 0-30.888-69.843-55.929-156-55.929-86.156 0-156 25.04-156 55.93 0 30.888 67.975 23.5 156 55.928z" fill="url(#d)" transform="rotate(136 905.21 332.676)"/></g></svg>
|
||||
<svg width="1440" height="318" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient x1="38.706%" y1="-187.115%" x2="18.675%" y2="110.984%" id="a">
|
||||
<stop stop-color="#FFF" stop-opacity="0" offset="0%"/>
|
||||
<stop stop-color="#c3352e" offset="100%"/>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="c">
|
||||
<stop stop-color="#606483" stop-opacity="0" offset="0%"/>
|
||||
<stop stop-color="#0B0D19" stop-opacity=".72" offset="100%"/>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="d">
|
||||
<stop stop-color="#0B0D19" stop-opacity=".32" offset="0%"/>
|
||||
<stop stop-color="#0B0D19" stop-opacity="0" offset="100%"/>
|
||||
</linearGradient>
|
||||
<filter id="b">
|
||||
<feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/>
|
||||
<feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/>
|
||||
<feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/>
|
||||
<feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M88.494 90c67.04 7.177 161.094-24.753 224.996-90H.2c25.3 48.079 42.361 85.083 88.294 90z"
|
||||
transform="translate(1051)" fill="url(#a)" filter="url(#b)"/>
|
||||
<path
|
||||
d="M250.464 367.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z"
|
||||
fill="url(#c)" transform="rotate(143 810.285 354.367)"/>
|
||||
<path
|
||||
d="M373.408 256.178c88.026 32.429 156-25.04 156-55.929 0-30.888-69.843-55.929-156-55.929-86.156 0-156 25.04-156 55.93 0 30.888 67.975 23.5 156 55.928z"
|
||||
fill="url(#d)" transform="rotate(136 905.21 332.676)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.9 KiB |
@ -1 +1,5 @@
|
||||
<svg width="96" height="48" xmlns="http://www.w3.org/2000/svg"><text y="35" x="48" fill="#fff" stroke-width="0" font-size="36" font-family="Monospace" text-anchor="middle" stroke="#fff"></></text></svg>
|
||||
<svg width="96" height="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<text y="35" x="48" fill="#fff" stroke-width="0" font-size="36" font-family="Monospace" text-anchor="middle"
|
||||
stroke="#fff"></>
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 208 B After Width: | Height: | Size: 224 B |
@ -1 +1,8 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="style-scope yt-icon" style="width:100%;height:100%" pointer-events="none" display="block" fill="#fff"><g class="style-scope yt-icon"><path d="M25.462 19.105v6.848H4.515v-6.848H.489v8.861c0 1.111.9 2.012 2.016 2.012h24.967c1.115 0 2.016-.9 2.016-2.012v-8.861h-4.026zM14.62 18.426l-5.764-6.965s-.877-.828.074-.828h3.248V9.217.494S12.049 0 12.793 0h4.572c.536 0 .524.416.524.416V10.424h2.998c1.154 0 .285.867.285.867s-4.904 6.51-5.588 7.193c-.492.495-.964-.058-.964-.058z" class="style-scope yt-icon"/></g></svg>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="style-scope yt-icon" style="width:100%;height:100%"
|
||||
pointer-events="none" display="block" fill="#fff">
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M25.462 19.105v6.848H4.515v-6.848H.489v8.861c0 1.111.9 2.012 2.016 2.012h24.967c1.115 0 2.016-.9 2.016-2.012v-8.861h-4.026zM14.62 18.426l-5.764-6.965s-.877-.828.074-.828h3.248V9.217.494S12.049 0 12.793 0h4.572c.536 0 .524.416.524.416V10.424h2.998c1.154 0 .285.867.285.867s-4.904 6.51-5.588 7.193c-.492.495-.964-.058-.964-.058z"
|
||||
class="style-scope yt-icon"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 576 B After Width: | Height: | Size: 634 B |
@ -1 +1,35 @@
|
||||
<svg width="1440" height="582" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#606483" stop-opacity="0" offset="0%"/><stop stop-color="#363636" stop-opacity=".72" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b"><stop stop-color="#363636" offset="0%"/><stop stop-color="#363636" stop-opacity="0" offset="100%"/></linearGradient><radialGradient cx="33.3%" cy="43.394%" fx="33.3%" fy="43.394%" r="57.93%" gradientTransform="matrix(.24796 -.96592 .92535 .25883 -.151 .643)" id="c"><stop stop-color="#c3352e" stop-opacity="0" offset="0%"/><stop stop-color="#c3352e" stop-opacity=".64" offset="51.712%"/><stop stop-color="#c3352e" stop-opacity=".24" offset="100%"/></radialGradient><filter id="d"><feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/><feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/><feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/><feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z" fill="url(#a)" transform="rotate(24 -272.272 -82.087)"/><path d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z" fill="url(#b)" transform="rotate(24 -255.451 -119.868)"/><path d="M103.064 315.218c128.156 12.998 192.38 157.059 218.627 106.632 26.247-50.427-44.059-106.456 60.397-202.707 104.457-96.252-143.2-285.785-172.392-122.551C180.503 259.825-25.091 302.22 103.064 315.218z" transform="translate(1176 -33)" fill="url(#c)" filter="url(#d)"/></g></svg>
|
||||
<svg width="1440" height="582" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a">
|
||||
<stop stop-color="#606483" stop-opacity="0" offset="0%"/>
|
||||
<stop stop-color="#363636" stop-opacity=".72" offset="100%"/>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="39.334%" y2="79.282%" id="b">
|
||||
<stop stop-color="#363636" offset="0%"/>
|
||||
<stop stop-color="#363636" stop-opacity="0" offset="100%"/>
|
||||
</linearGradient>
|
||||
<radialGradient cx="33.3%" cy="43.394%" fx="33.3%" fy="43.394%" r="57.93%"
|
||||
gradientTransform="matrix(.24796 -.96592 .92535 .25883 -.151 .643)" id="c">
|
||||
<stop stop-color="#c3352e" stop-opacity="0" offset="0%"/>
|
||||
<stop stop-color="#c3352e" stop-opacity=".64" offset="51.712%"/>
|
||||
<stop stop-color="#c3352e" stop-opacity=".24" offset="100%"/>
|
||||
</radialGradient>
|
||||
<filter id="d">
|
||||
<feTurbulence type="fractalNoise" numOctaves="2" baseFrequency=".3" result="turb"/>
|
||||
<feComposite in="turb" operator="arithmetic" k1=".1" k2=".1" k3=".1" k4=".1" result="result1"/>
|
||||
<feComposite operator="in" in="result1" in2="SourceGraphic" result="finalFilter"/>
|
||||
<feBlend mode="multiply" in="finalFilter" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M252.464 335.471c101.27 115.965 283.227-105.29 283.227-154.996 0-49.705-111.929-90-250-90s-250 40.295-250 90c0 49.706 115.503 39.032 216.773 154.996z"
|
||||
fill="url(#a)" transform="rotate(24 -272.272 -82.087)"/>
|
||||
<path
|
||||
d="M302.512 242.909c88.025 32.428 156-25.04 156-55.93 0-30.888-69.844-55.928-156-55.928-86.157 0-156 25.04-156 55.929 0 30.888 67.974 23.5 156 55.929z"
|
||||
fill="url(#b)" transform="rotate(24 -255.451 -119.868)"/>
|
||||
<path
|
||||
d="M103.064 315.218c128.156 12.998 192.38 157.059 218.627 106.632 26.247-50.427-44.059-106.456 60.397-202.707 104.457-96.252-143.2-285.785-172.392-122.551C180.503 259.825-25.091 302.22 103.064 315.218z"
|
||||
transform="translate(1176 -33)" fill="url(#c)" filter="url(#d)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.2 KiB |
@ -1 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" fill="#fff"><path d="M45.563 29.174l-22-15A1 1 0 0022 15v30a.999.999 0 001.563.826l22-15a1 1 0 000-1.652zM24 43.107V16.893L43.225 30 24 43.107z"/><path d="M30 0C13.458 0 0 13.458 0 30s13.458 30 30 30 30-13.458 30-30S46.542 0 30 0zm0 58C14.561 58 2 45.439 2 30S14.561 2 30 2s28 12.561 28 28-12.561 28-28 28z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" fill="#fff">
|
||||
<path
|
||||
d="M45.563 29.174l-22-15A1 1 0 0022 15v30a.999.999 0 001.563.826l22-15a1 1 0 000-1.652zM24 43.107V16.893L43.225 30 24 43.107z"/>
|
||||
<path
|
||||
d="M30 0C13.458 0 0 13.458 0 30s13.458 30 30 30 30-13.458 30-30S46.542 0 30 0zm0 58C14.561 58 2 45.439 2 30S14.561 2 30 2s28 12.561 28 28-12.561 28-28 28z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 391 B |
@ -1 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176" width="32" height="32"><circle fill="red" cx="88" cy="88" r="88"/><path fill="#FFF" d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.9-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/><path fill="#FFF" d="M72 111l39-24-39-22z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 176" width="32" height="32">
|
||||
<circle fill="red" cx="88" cy="88" r="88"/>
|
||||
<path fill="#FFF"
|
||||
d="M88 46c23.1 0 42 18.8 42 42s-18.8 42-42 42-42-18.8-42-42 18.9-42 42-42m0-4c-25.4 0-46 20.6-46 46s20.6 46 46 46 46-20.6 46-46-20.6-46-46-46z"/>
|
||||
<path fill="#FFF" d="M72 111l39-24-39-22z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 360 B |
574
docs/index.html
@ -1,138 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<head>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>YouTube Music Desktop App (Unofficial)</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./favicon/favicon.ico"
|
||||
sizes="16x16"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./favicon/favicon_32.png"
|
||||
sizes="32x32"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./favicon/favicon_48.png"
|
||||
sizes="48x48"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./favicon/favicon_96.png"
|
||||
sizes="96x96"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./favicon/favicon_144.png"
|
||||
sizes="144x144"
|
||||
type="image/png"
|
||||
/>
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
<title>YouTube Music Desktop App (Unofficial)</title>
|
||||
<link
|
||||
href="./favicon/favicon.ico"
|
||||
rel="icon"
|
||||
sizes="16x16"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<link
|
||||
href="./favicon/favicon_32.png"
|
||||
rel="icon"
|
||||
sizes="32x32"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
href="./favicon/favicon_48.png"
|
||||
rel="icon"
|
||||
sizes="48x48"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
href="./favicon/favicon_96.png"
|
||||
rel="icon"
|
||||
sizes="96x96"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
href="./favicon/favicon_144.png"
|
||||
rel="icon"
|
||||
sizes="144x144"
|
||||
type="image/png"
|
||||
/>
|
||||
|
||||
<meta name="theme-color" content="#131313" />
|
||||
<meta
|
||||
name="description"
|
||||
content="YouTube Music Unofficial Desktop App with built-in ad blocker and downloader"
|
||||
/>
|
||||
<meta
|
||||
property="og:site_name"
|
||||
content="YouTube Music Desktop App"
|
||||
/>
|
||||
<meta
|
||||
property="og:url"
|
||||
class="meta-url"
|
||||
content="https://th-ch.github.io/youtube-music"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="twitter:url"
|
||||
class="meta-url"
|
||||
content="https://th-ch.github.io/youtube-music"
|
||||
/>
|
||||
<meta content="#131313" name="theme-color"/>
|
||||
<meta
|
||||
content="YouTube Music Unofficial Desktop App with built-in ad blocker and downloader"
|
||||
name="description"
|
||||
/>
|
||||
<meta
|
||||
content="YouTube Music Desktop App"
|
||||
property="og:site_name"
|
||||
/>
|
||||
<meta
|
||||
class="meta-url"
|
||||
content="https://th-ch.github.io/youtube-music"
|
||||
property="og:url"
|
||||
/>
|
||||
<meta content="website" property="og:type"/>
|
||||
<meta
|
||||
class="meta-url"
|
||||
content="https://th-ch.github.io/youtube-music"
|
||||
name="twitter:url"
|
||||
/>
|
||||
|
||||
<link href="./style/fonts.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="./style/style.css" />
|
||||
<script src="https://unpkg.com/scrollreveal"></script>
|
||||
</head>
|
||||
<body class="has-animations vsc-initialized" style="height: 100%;">
|
||||
<div class="body-wrap boxed-container">
|
||||
<header class="site-header text-light">
|
||||
<div class="container">
|
||||
<div class="site-header-inner">
|
||||
<div class="brand header-brand">
|
||||
<h1 class="m-0">
|
||||
<a href="https://github.com/th-ch/youtube-music">
|
||||
<img
|
||||
class="header-logo-image"
|
||||
src="./img/youtube-music.svg"
|
||||
alt="YouTube Music"
|
||||
/>
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<link href="./style/fonts.css" rel="stylesheet"/>
|
||||
<link href="./style/style.css" rel="stylesheet"/>
|
||||
<script src="https://unpkg.com/scrollreveal"></script>
|
||||
</head>
|
||||
<body class="has-animations vsc-initialized" style="height: 100%;">
|
||||
<div class="body-wrap boxed-container">
|
||||
<header class="site-header text-light">
|
||||
<div class="container">
|
||||
<div class="site-header-inner">
|
||||
<div class="brand header-brand">
|
||||
<h1 class="m-0">
|
||||
<a href="https://github.com/th-ch/youtube-music">
|
||||
<img
|
||||
alt="YouTube Music"
|
||||
class="header-logo-image"
|
||||
src="./img/youtube-music.svg"
|
||||
/>
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero text-center text-light">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-particles-container">
|
||||
<canvas id="hero-particles"></canvas>
|
||||
</div>
|
||||
<div class="container-sm">
|
||||
<div class="hero-inner">
|
||||
<div class="hero-copy">
|
||||
<h1 class="hero-title mt-0">
|
||||
Custom YouTube Music Desktop App
|
||||
</h1>
|
||||
<p class="hero-paragraph">
|
||||
Open source, cross-platform, unofficial YouTube Music Desktop
|
||||
App with built-in <strong>ad blocker</strong> and
|
||||
<strong>downloader</strong>
|
||||
</p>
|
||||
<div class="hero-cta">
|
||||
<a
|
||||
class="button button-primary button-wide-mobile"
|
||||
href="https://github.com/th-ch/youtube-music/releases/latest"
|
||||
>Download</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup-container">
|
||||
<div class="mockup-bg">
|
||||
<img
|
||||
src="./img/youtube-music.png"
|
||||
id="mockup-header-img"
|
||||
alt="YouTube Music"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<main>
|
||||
<section class="hero text-center text-light">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-particles-container">
|
||||
<canvas id="hero-particles"></canvas>
|
||||
</div>
|
||||
<div class="container-sm">
|
||||
<div class="hero-inner">
|
||||
<div class="hero-copy">
|
||||
<h1 class="hero-title mt-0">
|
||||
Custom YouTube Music Desktop App
|
||||
</h1>
|
||||
<p class="hero-paragraph">
|
||||
Open source, cross-platform, unofficial YouTube Music Desktop
|
||||
App with built-in <strong>ad blocker</strong> and
|
||||
<strong>downloader</strong>
|
||||
</p>
|
||||
<div class="hero-cta">
|
||||
<a
|
||||
class="button button-primary button-wide-mobile"
|
||||
href="https://github.com/th-ch/youtube-music/releases/latest"
|
||||
>Download</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup-container">
|
||||
<div class="mockup-bg">
|
||||
<img
|
||||
alt="YouTube Music"
|
||||
id="mockup-header-img"
|
||||
src="./img/youtube-music.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-extended section">
|
||||
<div class="features-extended-inner section-inner">
|
||||
<div class="features-extended-wrap">
|
||||
<div class="container">
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
class="device-mockup"
|
||||
src="./img/adblock.svg"
|
||||
width="100px"
|
||||
alt="Adblocker"
|
||||
data-sr-id="0"
|
||||
style="
|
||||
<section class="features-extended section">
|
||||
<div class="features-extended-inner section-inner">
|
||||
<div class="features-extended-wrap">
|
||||
<div class="container">
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
alt="Adblocker"
|
||||
class="device-mockup"
|
||||
data-sr-id="0"
|
||||
src="./img/adblock.svg"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -157,12 +156,13 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="5"
|
||||
style="
|
||||
width="100px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="5"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -187,19 +187,19 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Built-in adblocker</h3>
|
||||
<p class="m-0">Block all ads and tracking out of the box</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
class="device-mockup"
|
||||
src="./img/download.svg"
|
||||
alt="Downloader"
|
||||
data-sr-id="2"
|
||||
style="
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Built-in adblocker</h3>
|
||||
<p class="m-0">Block all ads and tracking out of the box</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
alt="Downloader"
|
||||
class="device-mockup"
|
||||
data-sr-id="2"
|
||||
src="./img/download.svg"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -224,12 +224,12 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="6"
|
||||
style="
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="6"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -254,22 +254,22 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Built-in downloader</h3>
|
||||
<p class="m-0">
|
||||
Download (like youtube-dl) to custom formats (mp3, opus,
|
||||
etc) directly from the interface
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
class="device-mockup"
|
||||
src="./img/plugins.svg"
|
||||
alt="Plugins"
|
||||
data-sr-id="3"
|
||||
style="
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Built-in downloader</h3>
|
||||
<p class="m-0">
|
||||
Download (like youtube-dl) to custom formats (mp3, opus,
|
||||
etc) directly from the interface
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
alt="Plugins"
|
||||
class="device-mockup"
|
||||
data-sr-id="3"
|
||||
src="./img/plugins.svg"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -294,12 +294,12 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="7"
|
||||
style="
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="7"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -324,24 +324,24 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Many other plugins in one click</h3>
|
||||
<p class="m-0">
|
||||
Enhance your user experience with media keys, integrations
|
||||
(Discord), cosmetic filters, notifications, TouchBar,
|
||||
auto-unpause and many more! Every plugin can be enabled or
|
||||
disabled in one click.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
class="device-mockup"
|
||||
src="./img/code.svg"
|
||||
alt="Code"
|
||||
data-sr-id="4"
|
||||
style="
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Many other plugins in one click</h3>
|
||||
<p class="m-0">
|
||||
Enhance your user experience with media keys, integrations
|
||||
(Discord), cosmetic filters, notifications, TouchBar,
|
||||
auto-unpause and many more! Every plugin can be enabled or
|
||||
disabled in one click.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-extended">
|
||||
<div class="feature-extended-image">
|
||||
<img
|
||||
alt="Code"
|
||||
class="device-mockup"
|
||||
data-sr-id="4"
|
||||
src="./img/code.svg"
|
||||
style="
|
||||
visibility: visible;
|
||||
width: 200%;
|
||||
opacity: 1;
|
||||
@ -367,12 +367,12 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="8"
|
||||
style="
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="feature-extended-body"
|
||||
data-sr-id="8"
|
||||
style="
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: matrix3d(
|
||||
@ -397,94 +397,94 @@
|
||||
cubic-bezier(0.215, 0.61, 0.355, 1) 0s,
|
||||
transform 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||
"
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Open source & Cross platform</h3>
|
||||
<p class="m-0">
|
||||
Available for Windows (installer and portable), Mac and
|
||||
Linux (AppImage, deb, etc)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-particles-container">
|
||||
<canvas id="main-particles"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
>
|
||||
<h3 class="mt-0 mb-16">Open source & Cross platform</h3>
|
||||
<p class="m-0">
|
||||
Available for Windows (installer and portable), Mac and
|
||||
Linux (AppImage, deb, etc)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-particles-container">
|
||||
<canvas id="main-particles"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-particles-container">
|
||||
<canvas id="footer-particles"></canvas>
|
||||
</div>
|
||||
<div class="site-footer-top">
|
||||
<section class="cta section text-light">
|
||||
<div class="container-sm">
|
||||
<div class="cta-inner section-inner">
|
||||
<div class="cta-header text-center">
|
||||
<h2 class="section-title mt-0">Download and/or contribute</h2>
|
||||
<p class="section-paragraph">Pull requests welcome!</p>
|
||||
<div class="cta-cta">
|
||||
<a
|
||||
class="button button-primary button-wide-mobile"
|
||||
href="https://github.com/th-ch/youtube-music"
|
||||
>Go to code</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="site-footer-bottom">
|
||||
<div class="container">
|
||||
<div class="site-footer-inner">
|
||||
<div class="brand footer-brand">
|
||||
<a href="https://github.com/th-ch/youtube-music">
|
||||
<img src="./img/youtube-music.svg" alt="YouTube Music logo" />
|
||||
</a>
|
||||
</div>
|
||||
<ul class="footer-links list-reset">
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music">Main page</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music/issues"
|
||||
>Issues</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music/pulls"
|
||||
>Pull requests</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="footer-social-links list-reset">
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music">
|
||||
<span class="screen-reader-text">GitHub</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 1792 1792"
|
||||
>
|
||||
<path
|
||||
d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5q0 251-146.5 451.5t-378.5 277.5q-27 5-40-7t-13-30q0-3 .5-76.5t.5-134.5q0-97-52-142 57-6 102.5-18t94-39 81-66.5 53-105 20.5-150.5q0-119-79-206 37-91-8-204-28-9-81 11t-92 44l-38 24q-93-26-192-26t-192 26q-16-11-42.5-27t-83.5-38.5-85-13.5q-45 113-8 204-79 87-79 206 0 85 20.5 150t52.5 105 80.5 67 94 39 102.5 18q-39 36-49 103-21 10-45 15t-57 5-65.5-21.5-55.5-62.5q-19-32-48.5-52t-49.5-24l-20-3q-21 0-29 4.5t-5 11.5 9 14 13 12l7 5q22 10 43.5 38t31.5 51l10 23q13 38 44 61.5t67 30 69.5 7 55.5-3.5l23-4q0 38 .5 88.5t.5 54.5q0 18-13 30t-40 7q-232-77-378.5-277.5t-146.5-451.5q0-209 103-385.5t279.5-279.5 385.5-103zm-477 1103q3-7-7-12-10-3-13 2-3 7 7 12 9 6 13-2zm31 34q7-5-2-16-10-9-16-3-7 5 2 16 10 10 16 3zm30 45q9-7 0-19-8-13-17-6-9 5 0 18t17 7zm42 42q8-8-4-19-12-12-20-3-9 8 4 19 12 12 20 3zm57 25q3-11-13-16-15-4-19 7t13 15q15 6 19-6zm63 5q0-13-17-11-16 0-16 11 0 13 17 11 16 0 16-11zm58-10q-2-11-18-9-16 3-14 15t18 8 14-14z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="footer-copyright">© 2021 th-ch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="footer-particles-container">
|
||||
<canvas id="footer-particles"></canvas>
|
||||
</div>
|
||||
<div class="site-footer-top">
|
||||
<section class="cta section text-light">
|
||||
<div class="container-sm">
|
||||
<div class="cta-inner section-inner">
|
||||
<div class="cta-header text-center">
|
||||
<h2 class="section-title mt-0">Download and/or contribute</h2>
|
||||
<p class="section-paragraph">Pull requests welcome!</p>
|
||||
<div class="cta-cta">
|
||||
<a
|
||||
class="button button-primary button-wide-mobile"
|
||||
href="https://github.com/th-ch/youtube-music"
|
||||
>Go to code</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="site-footer-bottom">
|
||||
<div class="container">
|
||||
<div class="site-footer-inner">
|
||||
<div class="brand footer-brand">
|
||||
<a href="https://github.com/th-ch/youtube-music">
|
||||
<img alt="YouTube Music logo" src="./img/youtube-music.svg"/>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="footer-links list-reset">
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music">Main page</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music/issues"
|
||||
>Issues</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music/pulls"
|
||||
>Pull requests</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="footer-social-links list-reset">
|
||||
<li>
|
||||
<a href="https://github.com/th-ch/youtube-music">
|
||||
<span class="screen-reader-text">GitHub</span>
|
||||
<svg
|
||||
height="16"
|
||||
viewBox="0 0 1792 1792"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5q0 251-146.5 451.5t-378.5 277.5q-27 5-40-7t-13-30q0-3 .5-76.5t.5-134.5q0-97-52-142 57-6 102.5-18t94-39 81-66.5 53-105 20.5-150.5q0-119-79-206 37-91-8-204-28-9-81 11t-92 44l-38 24q-93-26-192-26t-192 26q-16-11-42.5-27t-83.5-38.5-85-13.5q-45 113-8 204-79 87-79 206 0 85 20.5 150t52.5 105 80.5 67 94 39 102.5 18q-39 36-49 103-21 10-45 15t-57 5-65.5-21.5-55.5-62.5q-19-32-48.5-52t-49.5-24l-20-3q-21 0-29 4.5t-5 11.5 9 14 13 12l7 5q22 10 43.5 38t31.5 51l10 23q13 38 44 61.5t67 30 69.5 7 55.5-3.5l23-4q0 38 .5 88.5t.5 54.5q0 18-13 30t-40 7q-232-77-378.5-277.5t-146.5-451.5q0-209 103-385.5t279.5-279.5 385.5-103zm-477 1103q3-7-7-12-10-3-13 2-3 7 7 12 9 6 13-2zm31 34q7-5-2-16-10-9-16-3-7 5 2 16 10 10 16 3zm30 45q9-7 0-19-8-13-17-6-9 5 0 18t17 7zm42 42q8-8-4-19-12-12-20-3-9 8 4 19 12 12 20 3zm57 25q3-11-13-16-15-4-19 7t13 15q15 6 19-6zm63 5q0-13-17-11-16 0-16 11 0 13 17 11 16 0 16-11zm58-10q-2-11-18-9-16 3-14 15t18 8 14-14z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="footer-copyright">© 2021 th-ch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="./js/main.js"></script>
|
||||
</body>
|
||||
<script src="./js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
128
docs/js/main.js
@ -1,46 +1,49 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// Constants
|
||||
const element = document.documentElement,
|
||||
body = document.body,
|
||||
revealOnScroll = (window.sr = ScrollReveal({ mobile: false }));
|
||||
const element = document.documentElement;
|
||||
const { body } = document;
|
||||
const revealOnScroll = (window.sr = ScrollReveal({ mobile: false }));
|
||||
|
||||
// Load animations
|
||||
element.classList.remove("no-js");
|
||||
element.classList.add("js");
|
||||
window.addEventListener("load", function () {
|
||||
body.classList.add("is-loaded");
|
||||
element.classList.remove('no-js');
|
||||
element.classList.add('js');
|
||||
window.addEventListener('load', () => {
|
||||
body.classList.add('is-loaded');
|
||||
});
|
||||
|
||||
if (body.classList.contains("has-animations")) {
|
||||
window.addEventListener("load", function () {
|
||||
revealOnScroll.reveal(".feature-extended .device-mockup", {
|
||||
if (body.classList.contains('has-animations')) {
|
||||
window.addEventListener('load', () => {
|
||||
revealOnScroll.reveal('.feature-extended .device-mockup', {
|
||||
duration: 600,
|
||||
distance: "100px",
|
||||
easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
|
||||
origin: "bottom",
|
||||
distance: '100px',
|
||||
easing: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
|
||||
origin: 'bottom',
|
||||
viewFactor: 0.6,
|
||||
});
|
||||
revealOnScroll.reveal(".feature-extended .feature-extended-body", {
|
||||
revealOnScroll.reveal('.feature-extended .feature-extended-body', {
|
||||
duration: 600,
|
||||
distance: "40px",
|
||||
easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
|
||||
origin: "top",
|
||||
distance: '40px',
|
||||
easing: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
|
||||
origin: 'top',
|
||||
viewFactor: 0.6,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Bubble canvas
|
||||
let bubbleCanvas = function (t) {
|
||||
let e = this;
|
||||
const bubbleCanvas = function (t) {
|
||||
const e = this;
|
||||
e.parentNode = t;
|
||||
e.setCanvasSize();
|
||||
window.addEventListener("resize", function () {
|
||||
window.addEventListener('resize', () => {
|
||||
e.setCanvasSize();
|
||||
});
|
||||
e.mouseX = 0;
|
||||
e.mouseY = 0;
|
||||
window.addEventListener("mousemove", function (t) {
|
||||
(e.mouseX = t.clientX), (e.mouseY = t.clientY);
|
||||
window.addEventListener('mousemove', (t) => {
|
||||
e.mouseX = t.clientX;
|
||||
e.mouseY = t.clientY;
|
||||
});
|
||||
e.randomise();
|
||||
};
|
||||
@ -55,15 +58,15 @@ bubbleCanvas.prototype.generateDecimalBetween = function (start, end) {
|
||||
};
|
||||
|
||||
bubbleCanvas.prototype.update = function () {
|
||||
let t = this;
|
||||
t.translateX = t.translateX - t.movementX;
|
||||
t.translateY = t.translateY - t.movementY;
|
||||
const t = this;
|
||||
t.translateX -= t.movementX;
|
||||
t.translateY -= t.movementY;
|
||||
t.posX += (t.mouseX / (t.staticity / t.magnetism) - t.posX) / t.smoothFactor;
|
||||
t.posY += (t.mouseY / (t.staticity / t.magnetism) - t.posY) / t.smoothFactor;
|
||||
if (
|
||||
t.translateY + t.posY < 0 ||
|
||||
t.translateX + t.posX < 0 ||
|
||||
t.translateX + t.posX > t.canvasWidth
|
||||
t.translateY + t.posY < 0
|
||||
|| t.translateX + t.posX < 0
|
||||
|| t.translateX + t.posX > t.canvasWidth
|
||||
) {
|
||||
t.randomise();
|
||||
t.translateY = t.canvasHeight;
|
||||
@ -71,7 +74,7 @@ bubbleCanvas.prototype.update = function () {
|
||||
};
|
||||
|
||||
bubbleCanvas.prototype.randomise = function () {
|
||||
this.colors = ["195,53,46", "172,54,46"];
|
||||
this.colors = ['195,53,46', '172,54,46'];
|
||||
|
||||
this.velocity = 20;
|
||||
this.smoothFactor = 50;
|
||||
@ -88,17 +91,17 @@ bubbleCanvas.prototype.randomise = function () {
|
||||
this.translateY = this.generateDecimalBetween(0, this.canvasHeight);
|
||||
};
|
||||
|
||||
let drawBubbleCanvas = function (t) {
|
||||
const drawBubbleCanvas = function (t) {
|
||||
this.canvas = document.getElementById(t);
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.dpr = window.devicePixelRatio;
|
||||
};
|
||||
|
||||
drawBubbleCanvas.prototype.start = function (bubbleDensity) {
|
||||
let t = this;
|
||||
const t = this;
|
||||
t.bubbleDensity = bubbleDensity;
|
||||
t.setCanvasSize();
|
||||
window.addEventListener("resize", function () {
|
||||
window.addEventListener('resize', () => {
|
||||
t.setCanvasSize();
|
||||
});
|
||||
t.bubblesList = [];
|
||||
@ -114,23 +117,24 @@ drawBubbleCanvas.prototype.setCanvasSize = function () {
|
||||
this.hdpi = this.h * this.dpr;
|
||||
this.canvas.width = this.wdpi;
|
||||
this.canvas.height = this.hdpi;
|
||||
this.canvas.style.width = this.w + "px";
|
||||
this.canvas.style.height = this.h + "px";
|
||||
this.canvas.style.width = this.w + 'px';
|
||||
this.canvas.style.height = this.h + 'px';
|
||||
this.ctx.scale(this.dpr, this.dpr);
|
||||
};
|
||||
|
||||
drawBubbleCanvas.prototype.animate = function () {
|
||||
let t = this;
|
||||
const t = this;
|
||||
t.ctx.clearRect(0, 0, t.canvas.clientWidth, t.canvas.clientHeight);
|
||||
t.bubblesList.forEach(function (e) {
|
||||
for (const e of t.bubblesList) {
|
||||
e.update();
|
||||
t.ctx.translate(e.translateX, e.translateY);
|
||||
t.ctx.beginPath();
|
||||
t.ctx.arc(e.posX, e.posY, e.size, 0, 2 * Math.PI);
|
||||
t.ctx.fillStyle = "rgba(" + e.color + "," + e.alpha + ")";
|
||||
t.ctx.fillStyle = 'rgba(' + e.color + ',' + e.alpha + ')';
|
||||
t.ctx.fill();
|
||||
t.ctx.setTransform(t.dpr, 0, 0, t.dpr, 0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
};
|
||||
|
||||
@ -139,15 +143,16 @@ drawBubbleCanvas.prototype.addBubble = function (t) {
|
||||
};
|
||||
|
||||
drawBubbleCanvas.prototype.generateBubbles = function () {
|
||||
let t = this;
|
||||
for (let e = 0; e < t.bubbleDensity; e++)
|
||||
const t = this;
|
||||
for (let e = 0; e < t.bubbleDensity; e++) {
|
||||
t.addBubble(new bubbleCanvas(t.canvas.parentNode));
|
||||
}
|
||||
};
|
||||
|
||||
// Night sky with stars canvas
|
||||
let starCanvas = function (t) {
|
||||
const starCanvas = function (t) {
|
||||
this.canvas = document.getElementById(t);
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.dpr = window.devicePixelRatio;
|
||||
};
|
||||
|
||||
@ -156,17 +161,17 @@ starCanvas.prototype.start = function () {
|
||||
let h;
|
||||
|
||||
const setCanvasExtents = () => {
|
||||
w = this.canvas.parentNode.clientWidth;
|
||||
h = this.canvas.parentNode.clientHeight;
|
||||
w = this.canvas.parentNode.clientWidth;
|
||||
h = this.canvas.parentNode.clientHeight;
|
||||
this.canvas.width = w;
|
||||
this.canvas.height = h;
|
||||
};
|
||||
|
||||
setCanvasExtents();
|
||||
|
||||
window.onresize = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
setCanvasExtents();
|
||||
};
|
||||
});
|
||||
|
||||
const makeStars = (count) => {
|
||||
const out = [];
|
||||
@ -178,19 +183,20 @@ starCanvas.prototype.start = function () {
|
||||
};
|
||||
out.push(s);
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
let stars = makeStars(10000);
|
||||
const stars = makeStars(10_000);
|
||||
|
||||
const clear = () => {
|
||||
this.ctx.fillStyle = "#212121";
|
||||
this.ctx.fillStyle = '#212121';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
};
|
||||
|
||||
const putPixel = (x, y, brightness) => {
|
||||
const intensity = brightness * 255;
|
||||
const rgb = "rgb(" + intensity + "," + intensity + "," + intensity + ")";
|
||||
const rgb = 'rgb(' + intensity + ',' + intensity + ',' + intensity + ')';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, 0.9, 0, 2 * Math.PI);
|
||||
this.ctx.fillStyle = rgb;
|
||||
@ -199,7 +205,7 @@ starCanvas.prototype.start = function () {
|
||||
|
||||
const moveStars = (distance) => {
|
||||
const count = stars.length;
|
||||
for (var i = 0; i < count; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const s = stars[i];
|
||||
s.z -= distance;
|
||||
while (s.z <= 1) {
|
||||
@ -208,15 +214,15 @@ starCanvas.prototype.start = function () {
|
||||
}
|
||||
};
|
||||
|
||||
let prevTime;
|
||||
let previousTime;
|
||||
const init = (time) => {
|
||||
prevTime = time;
|
||||
previousTime = time;
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const tick = (time) => {
|
||||
let elapsed = time - prevTime;
|
||||
prevTime = time;
|
||||
const elapsed = time - previousTime;
|
||||
previousTime = time;
|
||||
|
||||
moveStars(elapsed * 0.1);
|
||||
|
||||
@ -226,7 +232,7 @@ starCanvas.prototype.start = function () {
|
||||
const cy = h / 2;
|
||||
|
||||
const count = stars.length;
|
||||
for (var i = 0; i < count; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const star = stars[i];
|
||||
|
||||
const x = cx + star.x / (star.z * 0.001);
|
||||
@ -236,7 +242,7 @@ starCanvas.prototype.start = function () {
|
||||
continue;
|
||||
}
|
||||
|
||||
const d = star.z / 1000.0;
|
||||
const d = star.z / 1000;
|
||||
const b = 1 - d * d;
|
||||
|
||||
putPixel(x, y, b);
|
||||
@ -249,12 +255,12 @@ starCanvas.prototype.start = function () {
|
||||
};
|
||||
|
||||
// Start canvas animations
|
||||
window.addEventListener("load", function () {
|
||||
window.addEventListener('load', () => {
|
||||
// Stars
|
||||
const headCanvas = new starCanvas("hero-particles");
|
||||
const headCanvas = new starCanvas('hero-particles');
|
||||
// Bubbles
|
||||
const footerCanvas = new drawBubbleCanvas("footer-particles");
|
||||
const mainCanvas = new drawBubbleCanvas("main-particles");
|
||||
const footerCanvas = new drawBubbleCanvas('footer-particles');
|
||||
const mainCanvas = new drawBubbleCanvas('main-particles');
|
||||
|
||||
headCanvas.start();
|
||||
footerCanvas.start(30);
|
||||
|
||||
327
docs/readme/README-ko.md
Normal file
@ -0,0 +1,327 @@
|
||||
# 유튜브 뮤직 (YouTube Music)
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="../../web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
**유튜브 뮤직의 Electron 래퍼; 기능:**
|
||||
|
||||
- 원래의 인터페이스를 유지하는 것을 목표로 하는 네이티브 디자인 및 느낌
|
||||
- 맞춤 플러그인을 위한 프레임워크: 스타일, 콘텐츠, 기능 등 필요에 따라 유튜브 뮤직을 변경하고, 클릭 한 번으로 플러그인을 활성화/비활성화할 수 있습니다.
|
||||
|
||||
## 번역
|
||||
|
||||
[Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/)에서 번역을 도울 수 있습니다.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="번역 상태" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="번역 상태 2" />
|
||||
</a>
|
||||
|
||||
## 다운로드
|
||||
|
||||
[최신 릴리즈](https://github.com/th-ch/youtube-music/releases/latest)를 확인하여 최신 버전을 빠르게 찾을 수 있습니다.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
AUR에서 `youtube-music-bin` 패키지를 설치합니다. AUR 설치 지침은 [이 위키 페이지](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages)를 참조하세요.
|
||||
|
||||
### MacOS
|
||||
|
||||
Homebrew를 사용하여 앱을 설치할 수 있습니다:
|
||||
```bash
|
||||
brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb
|
||||
```
|
||||
|
||||
(앱을 수동으로 설치하고) 앱을 실행할 때 `손상되었기 때문에 열 수 없습니다.`라는 오류가 발생하면 터미널에서 다음을 실행하세요:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
[Scoop 패키지 매니저](https://scoop.sh)를 사용하여 [`extras` 버킷](https://github.com/ScoopInstaller/Extras)에서 `youtube-music` 패키지를 설치할 수 있습니다.
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
또는 Windows 11의 공식 CLI 패키지 관리자인 [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)을 사용하여 `th-ch.YouTubeMusic` 패키지를 설치할 수 있습니다.
|
||||
|
||||
*참고: "알 수 없는 게시자"의 파일이기 때문에 Microsoft Defender의 SmartScreen에서 설치를 차단할 수 있습니다. 이는 GitHub에서 동일 파일을 수동으로 다운로드한 후 실행 파일(.exe)을 실행하려고 할 때도 마찬가지로 발생합니다.*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### (Windows에서) 네트워크에 연결하지 않고 설치하는 방법은 무엇인가요?
|
||||
|
||||
- [릴리즈 페이지](https://github.com/th-ch/youtube-music/releases/latest)에서 _본인 기기 아키텍처_에 맞는 `*.nsis.7z` 파일을 다운로드하세요.
|
||||
- `x64`는 64비트 Windows 용입니다.
|
||||
- `ia32`는 32비트 Windows 용입니다.
|
||||
- `arm64`는 ARM64 Windows 용입니다.
|
||||
- 릴리즈 페이지에서 설치기를 다운로드하세요. (`*-Setup.exe`)
|
||||
- 두 파일을 **동일한 위치**에 놓아주세요.
|
||||
- 설치기를 실행하세요.
|
||||
|
||||
## 기능:
|
||||
|
||||
- **일시 정지 시 자동 확인** (항상 활성화 됨): 일정 시간이 지나면 음악을 일시 정지하는 ["계속 시청하시겠습니까?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) 팝업을 비활성화합니다.
|
||||
|
||||
- 이외에 더 많은 기능 ...
|
||||
|
||||
## 사용 가능한 플러그인:
|
||||
|
||||
- **애드블록**: 모든 광고와 트래커를 즉시 차단합니다
|
||||
|
||||
- **앨범 컬러 기반 테마**: 앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다
|
||||
|
||||
- **앰비언트 모드**: 영상의 간접 조명을 화면 배경에 투사합니다.
|
||||
|
||||
- **오디오 컴프레서**: 오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)
|
||||
|
||||
- **네비게이션 바 흐림 효과**: 내비게이션 바를 투명하고 흐릿하게 만듭니다
|
||||
|
||||
- **나이 제한 우회**: 유튜브의 나이 제한을 우회합니다
|
||||
|
||||
- **자막 선택기**: 자막을 활성화합니다
|
||||
|
||||
- **컴팩트 사이드바**: 사이드바를 항상 컴팩트 모드로 설정합니다
|
||||
|
||||
- **크로스페이드**: 노래 사이에 크로스페이드 효과를 적용합니다
|
||||
|
||||
- **자동 재생 해제**: 노래를 '일시 정지' 모드로 시작하게 합니다
|
||||
|
||||
- [**디스코드 활동 상태**](https://discord.com/): [활동 상태 (Rich Presence)](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)를 사용하여 친구들에게 내가 듣는 음악을 보여주세요
|
||||
|
||||
- **다운로더**: UI에서 [직접](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) MP3/소스 오디오를 다운로드하세요
|
||||
|
||||
- **지수 볼륨**: 음량 슬라이더를 [지수적](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/)으로 만들어 더 낮은 음량을 쉽게 선택할 수 있도록 합니다.
|
||||
|
||||
- **인앱 메뉴**: [메뉴 표시줄을 더 멋지게, 그리고 다크 또는 앨범의 색상으로 만듭니다](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (이 플러그인 및 메뉴 숨기기 옵션을 활성화한 후 메뉴에 액세스하는 데 문제가 있는 경우 [이 글](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709)을 참조하세요)
|
||||
|
||||
- [**Last.fm**](https://www.last.fm/): Last.fm에 대한 스크러블 지원을 추가합니다
|
||||
|
||||
- **Lumia Stream**: [Lumia Stream](https://lumiastream.com/) 지원을 추가합니다
|
||||
|
||||
- **Genius 가사**: 더 많은 곡에 대해 가사 지원을 추가합니다
|
||||
|
||||
- **네비게이션**: 브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표를 추가합니다
|
||||
|
||||
- **Google 로그인 제거**: UI에서 Google 로그인 버튼 및 링크 제거하기
|
||||
|
||||
- **알림**: 노래 재생이 시작되면 알림을 표시 (Windows에서는 [대화형 알림](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) 사용 가능)
|
||||
|
||||
- **PiP**: 앱을 PiP 모드로 전환할 수 있게 허용합니다
|
||||
|
||||
- **재생 속도**: 빨리 듣거나, 천천히 들어보세요! [노래 속도를 제어하는 슬라이더를 추가합니다](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **정확한 음량**: 사용자 지정 HUD와 사용자 지정 음량 단계 및 마우스 휠/단축키를 사용하여 음량을 정확하게 제어하세요
|
||||
|
||||
- **영상 품질 체인저**: 영상 오버레이의 [버튼](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png)으로 영상 품질을 변경할 수 있게 합니다
|
||||
|
||||
- **단축키 (& MPRIS)**: 재생을 위한 전역 단축키 설정 허용 (재생/일시 정지/다음/이전) + 미디어 키를 재정의하여 [미디어 osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) 비활성화 + Ctrl/CMD + F 검색 활성화 + 미디어 키에 대한 리눅스 MPRIS 지원 활성화 + [고급 사용자](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)를 위한 [사용자 지정 단축키](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) 지원
|
||||
|
||||
- **무음 건너뛰기** - 노래의 무음 부분을 자동으로 건너뜁니다
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): 인트로/아웃트로와 같은 음악이 아닌 부분이나, 노래가 재생되지 않는 뮤직 비디오의 일부를 자동으로 건너뜁니다
|
||||
|
||||
- **작업표시줄 미디어 컨트롤**: [Windows 작업표시줄](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)에서 재생을 제어하세요
|
||||
|
||||
- **TouchBar**: macOS 사용자를 위한 TouchBar 위젯을 추가합니다
|
||||
|
||||
- **Tuna-OBS**: [OBS](https://obsproject.com/)의 플러그인, [Tuna](https://obsproject.com/forum/resources/tuna.843/)와 통합을 활성화합니다
|
||||
|
||||
- **영상 전환**: 영상/노래 모드를 전환하는 [버튼](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png)을 추가합니다. 선택적으로 전체 영상 탭을 제거할 수도 있습니다
|
||||
|
||||
- **비주얼라이저**: 플레이어에 시각화 도구 추가
|
||||
|
||||
## 테마
|
||||
|
||||
CSS 파일을 로드하여 애플리케이션의 모양을 변경할 수 있습니다(설정 > 시각적 변경 > 테마).
|
||||
|
||||
일부 사전 정의 테마는 https://github.com/kerichdev/themes-for-ytmdesktop-player 에서 사용할 수 있습니다.
|
||||
|
||||
## 개발
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 나만의 플러그인 만들기
|
||||
|
||||
플러그인을 사용하면 할 수 있는 것들:
|
||||
|
||||
- 앱 조작 - Electron에서 `BrowserWindow`가 플러그인 핸들러로 전달
|
||||
- HTML/CSS를 조작하여 프론트엔드를 변경
|
||||
|
||||
### 플러그인 만들기
|
||||
|
||||
`plugins/나만의-플러그인-이름`에 폴더를 만듭니다:
|
||||
|
||||
- `index.ts`: 플러그인의 메인 파일입니다.
|
||||
```typescript
|
||||
import style from './style.css?inline'; // 스타일을 인라인으로 가져옵니다
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // 나의 커스텀 config
|
||||
stylesheets: [style], // 나의 스타일
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// 모든 *Config 메서드는 Promise<T>로 래핑됩니다
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// 이를 사용하여 렌더러 플러그인과 통신할 수 있습니다
|
||||
ipc.handle('some-event', () => {
|
||||
return 'hello';
|
||||
});
|
||||
},
|
||||
// config가 변경되면 실행됩니다
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// 플러그인이 비활성화되면 실행됩니다
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('some-event'));
|
||||
},
|
||||
// 렌더러에서만 사용 가능한 훅입니다
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext<T>) {
|
||||
// 플러그인의 config를 간단하게 설정할 수 있습니다
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 일반적인 사용 예
|
||||
|
||||
- 사용자 정의 CSS 삽입: 같은 폴더에 `style.css` 파일을 생성합니다:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // 스타일을 인라인으로 가져옵니다
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // 나의 커스텀 config
|
||||
stylesheets: [style], // 나의 커스텀 스타일
|
||||
renderer() {} // 렌더러 훅 정의
|
||||
});
|
||||
```
|
||||
|
||||
- HTML을 변경하려는 경우:
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // 나의 커스텀 config
|
||||
renderer() {
|
||||
// 로그인 버튼을 제거합니다
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // 렌더러 훅 정의
|
||||
});
|
||||
```
|
||||
|
||||
- 프론트엔드와 백엔드 간의 통신: Electron의 `ipcMain` 모듈을 사용하여 수행할 수 있습니다. `SponsorBlock` 플러그인의 `index.ts` 파일과 예제를 참조하세요.
|
||||
|
||||
## 빌드
|
||||
|
||||
1. 레포지토리를 복제 (clone) 합니다
|
||||
2. [이 가이드](https://pnpm.io/installation)에 따라 `pnpm`을 설치합니다.
|
||||
3. `pnpm install --frozen-lockfile`을 실행하여 종속성을 설치합니다.
|
||||
4. `pnpm build:OS`을 실행합니다.
|
||||
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux
|
||||
- `pnpm dist:mac` - MacOS
|
||||
|
||||
[electron-builder](https://github.com/electron-userland/electron-builder)를 사용하여 macOS, Linux 및 Windows용 앱을 빌드합니다.
|
||||
|
||||
## 프로덕션 빌드 미리보기
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 테스트
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
[Playwright](https://playwright.dev/)를 사용하여 앱을 테스트합니다.
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## 자주 묻는 질문
|
||||
|
||||
### 앱 메뉴가 표시되지 않는 이유는 무엇인가요?
|
||||
|
||||
`메뉴 숨기기` 옵션이 켜져 있는 경우 - <kbd>alt</kbd> 키(또는 인앱 메뉴 플러그인을 사용하는 경우 <kbd>\`</kbd> [백틱] 키)로 메뉴를 표시할 수 있습니다.
|
||||
@ -6,6 +6,7 @@
|
||||
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H0TbFhsqMA6aw.woff2) format('woff2');
|
||||
unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Heebo';
|
||||
@ -14,6 +15,7 @@
|
||||
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H2TbFhsqMA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* hebrew */
|
||||
@font-face {
|
||||
font-family: 'Heebo';
|
||||
@ -22,6 +24,7 @@
|
||||
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H0TbFhsqMA6aw.woff2) format('woff2');
|
||||
unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Heebo';
|
||||
@ -30,6 +33,7 @@
|
||||
src: url(https://fonts.gstatic.com/s/heebo/v9/NGS6v5_NC0k9P9H2TbFhsqMA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Oxygen';
|
||||
@ -38,6 +42,7 @@
|
||||
src: url(https://fonts.gstatic.com/s/oxygen/v10/2sDcZG1Wl4LcnbuCNWgzZmW5Kb8VZBHR.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Oxygen';
|
||||
|
||||
158
electron.vite.config.mts
Normal file
@ -0,0 +1,158 @@
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { defineConfig, defineViteConfig } from 'electron-vite';
|
||||
import builtinModules from 'builtin-modules';
|
||||
import viteResolve from 'vite-plugin-resolve';
|
||||
import Inspect from 'vite-plugin-inspect';
|
||||
|
||||
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs';
|
||||
import pluginLoader from './vite-plugins/plugin-loader.mjs';
|
||||
|
||||
import type { UserConfig } from 'vite';
|
||||
import { i18nImporter } from './vite-plugins/i18n-importer.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const resolveAlias = {
|
||||
'@': resolve(__dirname, './src'),
|
||||
'@assets': resolve(__dirname, './assets'),
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
main: defineViteConfig(({ mode }) => {
|
||||
const commonConfig: UserConfig = {
|
||||
plugins: [
|
||||
pluginLoader('backend'),
|
||||
viteResolve({
|
||||
'virtual:i18n': i18nImporter(),
|
||||
'virtual:plugins': pluginVirtualModuleGenerator('main'),
|
||||
}),
|
||||
],
|
||||
publicDir: 'assets',
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
outDir: 'dist/main',
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
input: './src/index.ts',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: resolveAlias,
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
commonConfig.plugins?.push(
|
||||
Inspect({ build: true, outputDir: '.vite-inspect/backend' }),
|
||||
);
|
||||
return commonConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...commonConfig,
|
||||
build: {
|
||||
...commonConfig.build,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
preload: defineViteConfig(({ mode }) => {
|
||||
const commonConfig: UserConfig = {
|
||||
plugins: [
|
||||
pluginLoader('preload'),
|
||||
viteResolve({
|
||||
'virtual:i18n': i18nImporter(),
|
||||
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/preload.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
outDir: 'dist/preload',
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
input: './src/preload.ts',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: resolveAlias,
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
commonConfig.plugins?.push(
|
||||
Inspect({ build: true, outputDir: '.vite-inspect/preload' }),
|
||||
);
|
||||
return commonConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...commonConfig,
|
||||
build: {
|
||||
...commonConfig.build,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
renderer: defineViteConfig(({ mode }) => {
|
||||
const commonConfig: UserConfig = {
|
||||
plugins: [
|
||||
pluginLoader('renderer'),
|
||||
viteResolve({
|
||||
'virtual:i18n': i18nImporter(),
|
||||
'virtual:plugins': pluginVirtualModuleGenerator('renderer'),
|
||||
}),
|
||||
],
|
||||
root: './src/',
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.html',
|
||||
formats: ['iife'],
|
||||
name: 'renderer',
|
||||
},
|
||||
outDir: 'dist/renderer',
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', ...builtinModules],
|
||||
input: './src/index.html',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: resolveAlias,
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
commonConfig.plugins?.push(
|
||||
Inspect({ build: true, outputDir: '.vite-inspect/renderer' }),
|
||||
);
|
||||
return commonConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...commonConfig,
|
||||
build: {
|
||||
...commonConfig.build,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
50
error.html
@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Cannot load YouTube Music</title>
|
||||
<style>
|
||||
body {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-right: -50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #065fd4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
font: inherit;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
padding: 8px 22px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<p>Cannot load YouTube Music… Internet disconnected?</p>
|
||||
<a href="#" class="button" onclick="reload()">Retry</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
518
index.js
@ -1,518 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
|
||||
const electron = require("electron");
|
||||
const enhanceWebRequest = require("electron-better-web-request").default;
|
||||
const is = require("electron-is");
|
||||
const unhandled = require("electron-unhandled");
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
|
||||
const config = require("./config");
|
||||
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");
|
||||
const { setupAppControls, restart } = require("./providers/app-controls");
|
||||
|
||||
// Catch errors and log them
|
||||
unhandled({
|
||||
logger: console.error,
|
||||
showDialog: false,
|
||||
});
|
||||
|
||||
// Disable Node options if the env var is set
|
||||
process.env.NODE_OPTIONS = "";
|
||||
|
||||
const app = electron.app;
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
if(config.get("options.singleInstanceLock")){
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
app.on('second-instance', () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
if (!mainWindow.isVisible()) mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch(
|
||||
"js-flags",
|
||||
// WebAssembly flags
|
||||
"--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()) {
|
||||
console.log("Disabling hardware acceleration");
|
||||
}
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
if (is.linux() && config.plugins.isEnabled("shortcuts")) {
|
||||
//stops chromium from launching it's own mpris service
|
||||
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
||||
}
|
||||
|
||||
if (config.get("options.proxy")) {
|
||||
app.commandLine.appendSwitch("proxy-server", config.get("options.proxy"));
|
||||
}
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
require("electron-debug")({
|
||||
showDevTools: false //disable automatic devTools on new window
|
||||
});
|
||||
|
||||
let icon = "assets/youtube-music.png";
|
||||
if (process.platform == "win32") {
|
||||
icon = "assets/generated/icon.ico";
|
||||
} else if (process.platform == "darwin") {
|
||||
icon = "assets/generated/icon.icns";
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
// Dereference the window
|
||||
// For multiple windows store them in an array
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
function loadPlugins(win) {
|
||||
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||
// Load user CSS
|
||||
const themes = config.get("options.themes");
|
||||
if (Array.isArray(themes)) {
|
||||
themes.forEach((cssFile) => {
|
||||
fileExists(
|
||||
cssFile,
|
||||
() => {
|
||||
injectCSS(win.webContents, cssFile);
|
||||
},
|
||||
() => {
|
||||
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
win.webContents.once("did-finish-load", () => {
|
||||
if (is.dev()) {
|
||||
console.log("did finish load");
|
||||
win.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
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, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
const windowSize = config.get("window-size");
|
||||
const windowMaximized = config.get("window-maximized");
|
||||
const windowPosition = config.get("window-position");
|
||||
const useInlineMenu = config.plugins.isEnabled("in-app-menu");
|
||||
|
||||
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,
|
||||
affinity: "main-window", // main window, and addition windows should work in one process
|
||||
...(!isTesting()
|
||||
? {
|
||||
// Sandbox is only enabled in tests for now
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
|
||||
sandbox: false,
|
||||
}
|
||||
: undefined),
|
||||
},
|
||||
frame: !is.macOS() && !useInlineMenu,
|
||||
titleBarStyle: useInlineMenu
|
||||
? "hidden"
|
||||
: is.macOS()
|
||||
? "hiddenInset"
|
||||
: "default",
|
||||
autoHideMenuBar: config.get("options.hideMenu"),
|
||||
});
|
||||
|
||||
if (windowPosition) {
|
||||
const { x, y } = windowPosition;
|
||||
const winSize = win.getSize();
|
||||
const displaySize =
|
||||
electron.screen.getDisplayNearestPoint(windowPosition).bounds;
|
||||
if (
|
||||
x + winSize[0] < displaySize.x - 8 ||
|
||||
x - winSize[0] > displaySize.x + displaySize.width ||
|
||||
y < displaySize.y - 8 ||
|
||||
y > displaySize.y + displaySize.height
|
||||
) {
|
||||
//Window is offscreen
|
||||
if (is.dev()) {
|
||||
console.log(
|
||||
`Window tried to render offscreen, windowSize=${winSize}, displaySize=${displaySize}, position=${windowPosition}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
win.setPosition(x, y);
|
||||
}
|
||||
}
|
||||
if (windowMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
if(config.get("options.alwaysOnTop")){
|
||||
win.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
const urlToLoad = config.get("options.resumeOnStart")
|
||||
? config.get("url")
|
||||
: config.defaultConfig.url;
|
||||
win.webContents.loadURL(urlToLoad);
|
||||
win.on("closed", onClosed);
|
||||
|
||||
const setPiPOptions = config.plugins.isEnabled("picture-in-picture")
|
||||
? (key, value) => require("./plugins/picture-in-picture/back").setOptions({ [key]: value })
|
||||
: () => {};
|
||||
|
||||
win.on("move", () => {
|
||||
if (win.isMaximized()) return;
|
||||
let position = win.getPosition();
|
||||
const isPiPEnabled =
|
||||
config.plugins.isEnabled("picture-in-picture") &&
|
||||
config.plugins.getOptions("picture-in-picture")["isInPiP"];
|
||||
if (!isPiPEnabled) {
|
||||
lateSave("window-position", { x: position[0], y: position[1] });
|
||||
} else if(config.plugins.getOptions("picture-in-picture")["savePosition"]) {
|
||||
lateSave("pip-position", position, setPiPOptions);
|
||||
}
|
||||
});
|
||||
|
||||
let winWasMaximized;
|
||||
|
||||
win.on("resize", () => {
|
||||
const windowSize = win.getSize();
|
||||
const isMaximized = win.isMaximized();
|
||||
|
||||
const isPiPEnabled =
|
||||
config.plugins.isEnabled("picture-in-picture") &&
|
||||
config.plugins.getOptions("picture-in-picture")["isInPiP"];
|
||||
|
||||
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
|
||||
winWasMaximized = isMaximized;
|
||||
config.set("window-maximized", isMaximized);
|
||||
}
|
||||
if (isMaximized) return;
|
||||
|
||||
if (!isPiPEnabled) {
|
||||
lateSave("window-size", {
|
||||
width: windowSize[0],
|
||||
height: windowSize[1],
|
||||
});
|
||||
} else if(config.plugins.getOptions("picture-in-picture")["saveSize"]) {
|
||||
lateSave("pip-size", windowSize, setPiPOptions);
|
||||
}
|
||||
});
|
||||
|
||||
let savedTimeouts = {};
|
||||
function lateSave(key, value, fn = config.set) {
|
||||
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]);
|
||||
|
||||
savedTimeouts[key] = setTimeout(() => {
|
||||
fn(key, value);
|
||||
savedTimeouts[key] = undefined;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
win.webContents.on("render-process-gone", (event, webContents, details) => {
|
||||
showUnresponsiveDialog(win, details);
|
||||
});
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
if (config.get("options.appVisible")) {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
|
||||
removeContentSecurityPolicy();
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
app.once("browser-window-created", (event, win) => {
|
||||
if (config.get("options.overrideUserAgent")) {
|
||||
// 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);
|
||||
setupAppControls();
|
||||
|
||||
win.webContents.on("did-fail-load", (
|
||||
_event,
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
isMainFrame,
|
||||
frameProcessId,
|
||||
frameRoutingId,
|
||||
) => {
|
||||
const log = JSON.stringify({
|
||||
error: "did-fail-load",
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
isMainFrame,
|
||||
frameProcessId,
|
||||
frameRoutingId,
|
||||
}, null, "\t");
|
||||
if (is.dev()) {
|
||||
console.log(log);
|
||||
}
|
||||
if( !(config.plugins.isEnabled("in-app-menu") && errorCode === -3)) { // -3 is a false positive with in-app-menu
|
||||
win.webContents.send("log", log);
|
||||
win.webContents.loadFile(path.join(__dirname, "error.html"));
|
||||
}
|
||||
});
|
||||
|
||||
win.webContents.on("will-prevent-unload", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
win.webContents.on(
|
||||
"new-window",
|
||||
(e, url, frameName, disposition, options) => {
|
||||
// hook on new opened window
|
||||
|
||||
// at now new window in mainWindow renderer process.
|
||||
// Also, this will automatically get an option `nodeIntegration=false`(not override to true, like in iframe's) - like in regular browsers
|
||||
options.webPreferences.affinity = "main-window";
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// Unregister all shortcuts.
|
||||
electron.globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
mainWindow = createMainWindow();
|
||||
} else if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("ready", () => {
|
||||
if (config.get("options.autoResetAppCache")) {
|
||||
// Clear cache after 20s
|
||||
const clearCacheTimeout = setTimeout(() => {
|
||||
if (is.dev()) {
|
||||
console.log("Clearing app cache.");
|
||||
}
|
||||
electron.session.defaultSession.clearCache();
|
||||
clearTimeout(clearCacheTimeout);
|
||||
}, 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);
|
||||
setUpTray(app, mainWindow);
|
||||
|
||||
// Autostart at login
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: config.get("options.startAtLogin"),
|
||||
});
|
||||
|
||||
if (!is.dev() && config.get("options.autoUpdates")) {
|
||||
const updateTimeout = setTimeout(() => {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
clearTimeout(updateTimeout);
|
||||
}, 2000);
|
||||
autoUpdater.on("update-available", () => {
|
||||
const downloadLink =
|
||||
"https://github.com/th-ch/youtube-music/releases/latest";
|
||||
const dialogOpts = {
|
||||
type: "info",
|
||||
buttons: ["OK", "Download", "Disable updates"],
|
||||
title: "Application Update",
|
||||
message: "A new version is available",
|
||||
detail: `A new version is available and can be downloaded at ${downloadLink}`,
|
||||
};
|
||||
electron.dialog.showMessageBox(dialogOpts).then((dialogOutput) => {
|
||||
switch (dialogOutput.response) {
|
||||
// Download
|
||||
case 1:
|
||||
electron.shell.openExternal(downloadLink);
|
||||
break;
|
||||
// Disable updates
|
||||
case 2:
|
||||
config.set("options.autoUpdates", false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.get("options.hideMenu") && !config.get("options.hideMenuWarned")) {
|
||||
electron.dialog.showMessageBox(mainWindow, {
|
||||
type: 'info', title: 'Hide Menu Enabled',
|
||||
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
|
||||
});
|
||||
config.set("options.hideMenuWarned", true);
|
||||
}
|
||||
|
||||
// Optimized for Mac OS X
|
||||
if (is.macOS() && !config.get("options.appVisible")) {
|
||||
app.dock.hide();
|
||||
}
|
||||
|
||||
let forceQuit = false;
|
||||
app.on("before-quit", () => {
|
||||
forceQuit = true;
|
||||
});
|
||||
|
||||
if (is.macOS() || config.get("options.tray")) {
|
||||
mainWindow.on("close", (event) => {
|
||||
// Hide the window instead of quitting (quit is available in tray options)
|
||||
if (!forceQuit) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function showUnresponsiveDialog(win, details) {
|
||||
if (!!details) {
|
||||
console.log("Unresponsive Error!\n"+JSON.stringify(details, null, "\t"))
|
||||
}
|
||||
electron.dialog.showMessageBox(win, {
|
||||
type: "error",
|
||||
title: "Window Unresponsive",
|
||||
message: "The Application is Unresponsive",
|
||||
details: "We are sorry for the inconvenience! please choose what to do:",
|
||||
buttons: ["Wait", "Relaunch", "Quit"],
|
||||
cancelId: 0
|
||||
}).then( result => {
|
||||
switch (result.response) {
|
||||
case 1: restart(); break;
|
||||
case 2: app.quit(); 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;
|
||||
});
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
__APP__: undefined, // A different app will be launched in each test environment
|
||||
},
|
||||
testTimeout: 30000, // 30s
|
||||
};
|
||||
396
menu.js
@ -1,396 +0,0 @@
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { app, Menu, dialog } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const { restart } = require("./providers/app-controls");
|
||||
|
||||
const { getAllPlugins } = require("./plugins/utils");
|
||||
const config = require("./config");
|
||||
|
||||
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),
|
||||
click: (item) => {
|
||||
if (item.checked) {
|
||||
config.plugins.enable(plugin);
|
||||
} else {
|
||||
config.plugins.disable(plugin);
|
||||
}
|
||||
if (hasSubmenu) {
|
||||
refreshMenu();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const mainMenuTemplate = (win) => {
|
||||
const refreshMenu = () => {
|
||||
this.setApplicationMenu(win);
|
||||
if (inAppMenuActive) {
|
||||
win.webContents.send("refreshMenu");
|
||||
}
|
||||
}
|
||||
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),
|
||||
{ type: "separator" },
|
||||
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return pluginEnabledMenu(plugin);
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Options",
|
||||
submenu: [
|
||||
{
|
||||
label: "Auto-update",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.autoUpdates"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.autoUpdates", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Resume last song when app starts",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.resumeOnStart"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.resumeOnStart", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Visual Tweaks",
|
||||
submenu: [
|
||||
{
|
||||
label: "Remove upgrade button",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.removeUpgradeButton"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.removeUpgradeButton", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Force show like buttons",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.ForceShowLikeButtons"),
|
||||
click: (item) => {
|
||||
config.set("options.ForceShowLikeButtons", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Theme",
|
||||
submenu: [
|
||||
{
|
||||
label: "No theme",
|
||||
type: "radio",
|
||||
checked: !config.get("options.themes"), // todo rename "themes"
|
||||
click: () => {
|
||||
config.set("options.themes", []);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Import custom CSS file",
|
||||
type: "radio",
|
||||
checked: false,
|
||||
click: async () => {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
filters: [{ name: "CSS Files", extensions: ["css"] }],
|
||||
properties: ["openFile", "multiSelections"],
|
||||
});
|
||||
if (filePaths) {
|
||||
config.set("options.themes", filePaths);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Single instance lock",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.singleInstanceLock"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.singleInstanceLock", item.checked);
|
||||
if (item.checked && !app.hasSingleInstanceLock()) {
|
||||
app.requestSingleInstanceLock();
|
||||
} else if (!item.checked && app.hasSingleInstanceLock()) {
|
||||
app.releaseSingleInstanceLock();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Always on top",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.alwaysOnTop"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.alwaysOnTop", item.checked);
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
...(is.windows() || is.linux()
|
||||
? [
|
||||
{
|
||||
label: "Hide menu",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.hideMenu"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.hideMenu", item.checked);
|
||||
if (item.checked && !config.get("options.hideMenuWarned")) {
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'info', title: 'Hide Menu Enabled',
|
||||
message: "Menu will be hidden on next launch, use 'Alt' to show it (or '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.setMenuOption("options.startAtLogin", item.checked);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Tray",
|
||||
submenu: [
|
||||
{
|
||||
label: "Disabled",
|
||||
type: "radio",
|
||||
checked: !config.get("options.tray"),
|
||||
click: () => {
|
||||
config.setMenuOption("options.tray", false);
|
||||
config.setMenuOption("options.appVisible", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Enabled + app visible",
|
||||
type: "radio",
|
||||
checked:
|
||||
config.get("options.tray") && config.get("options.appVisible"),
|
||||
click: () => {
|
||||
config.setMenuOption("options.tray", true);
|
||||
config.setMenuOption("options.appVisible", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Enabled + app hidden",
|
||||
type: "radio",
|
||||
checked:
|
||||
config.get("options.tray") && !config.get("options.appVisible"),
|
||||
click: () => {
|
||||
config.setMenuOption("options.tray", true);
|
||||
config.setMenuOption("options.appVisible", false);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Play/Pause on click",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.trayClickPlayPause"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.trayClickPlayPause", item.checked);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Advanced options",
|
||||
submenu: [
|
||||
{
|
||||
label: "Proxy",
|
||||
type: "checkbox",
|
||||
checked: !!config.get("options.proxy"),
|
||||
click: (item) => {
|
||||
setProxy(item, win);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Override useragent",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.overrideUserAgent"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.overrideUserAgent", item.checked);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Disable hardware acceleration",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.disableHardwareAcceleration"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.disableHardwareAcceleration", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restart on config changes",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.restartOnConfigChanges"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.restartOnConfigChanges", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Reset App cache when app starts",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.autoResetAppCache"),
|
||||
click: (item) => {
|
||||
config.setMenuOption("options.autoResetAppCache", item.checked);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
is.macOS() ?
|
||||
{
|
||||
label: "Toggle DevTools",
|
||||
// Cannot use "toggleDevTools" role in MacOS
|
||||
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: "View",
|
||||
submenu: [
|
||||
{ role: "reload" },
|
||||
{ role: "forceReload" },
|
||||
{ type: "separator" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ role: "resetZoom" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
{
|
||||
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: restart
|
||||
},
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports.mainMenuTemplate = mainMenuTemplate;
|
||||
module.exports.setApplicationMenu = (win) => {
|
||||
const menuTemplate = [...mainMenuTemplate(win)];
|
||||
if (process.platform === "darwin") {
|
||||
const name = app.name;
|
||||
menuTemplate.unshift({
|
||||
label: name,
|
||||
submenu: [
|
||||
{ role: "about" },
|
||||
{ type: "separator" },
|
||||
{ role: "hide" },
|
||||
{ role: "hideothers" },
|
||||
{ role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Select All",
|
||||
accelerator: "CmdOrCtrl+A",
|
||||
selector: "selectAll:",
|
||||
},
|
||||
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
|
||||
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
|
||||
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
|
||||
{ type: "separator" },
|
||||
{ role: "minimize" },
|
||||
{ role: "close" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
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.setMenuOption("options.proxy", output);
|
||||
item.checked = output !== "";
|
||||
} else { //user pressed cancel
|
||||
item.checked = !item.checked; //reset checkbox
|
||||
}
|
||||
}
|
||||
378
package.json
@ -1,161 +1,221 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "1.18.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"license": "MIT",
|
||||
"repository": "th-ch/youtube-music",
|
||||
"author": {
|
||||
"name": "th-ch",
|
||||
"email": "th-ch@users.noreply.github.com",
|
||||
"url": "https://github.com/th-ch/youtube-music"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.github.th-ch.youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"mac": {
|
||||
"identity": null,
|
||||
"files": [
|
||||
"!plugins/taskbar-mediacontrol${/*}"
|
||||
],
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/generated/icons/mac/icon.icns"
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/generated/icons/win/icon.ico",
|
||||
"files": [
|
||||
"!plugins/touchbar${/*}"
|
||||
],
|
||||
"target": [
|
||||
"nsis",
|
||||
"portable"
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"runAfterFinish": false
|
||||
},
|
||||
"linux": {
|
||||
"icon": "assets/generated/icons/png",
|
||||
"files": [
|
||||
"!plugins/{touchbar,taskbar-mediacontrol}${/*}"
|
||||
],
|
||||
"category": "AudioVideo",
|
||||
"target": [
|
||||
"AppImage",
|
||||
"snap",
|
||||
"freebsd",
|
||||
"deb",
|
||||
"rpm"
|
||||
]
|
||||
},
|
||||
"snap": {
|
||||
"slots": [
|
||||
{
|
||||
"mpris": {
|
||||
"interface": "mpris"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:debug": "DEBUG=pw:browser* playwright test",
|
||||
"start": "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",
|
||||
"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 dmg:x64",
|
||||
"build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64",
|
||||
"build:win": "yarn run clean && electron-builder --win",
|
||||
"lint": "xo",
|
||||
"changelog": "auto-changelog",
|
||||
"plugins": "yarn run plugin:adblocker",
|
||||
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
||||
"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": ">=14.0.0",
|
||||
"npm": "Please use yarn and not npm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.23.8",
|
||||
"@ffmpeg/core": "^0.10.0",
|
||||
"@ffmpeg/ffmpeg": "^0.10.1",
|
||||
"Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.4.6",
|
||||
"async-mutex": "^0.3.2",
|
||||
"browser-id3-writer": "^4.4.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"custom-electron-prompt": "^1.5.0",
|
||||
"custom-electron-titlebar": "^4.1.0",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-better-web-request": "^1.0.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-is": "^3.0.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-store": "^8.0.2",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"electron-updater": "^4.6.3",
|
||||
"filenamify": "^4.3.0",
|
||||
"hark": "^1.2.3",
|
||||
"html-to-text": "^8.2.1",
|
||||
"md5": "^2.3.0",
|
||||
"mpris-service": "^2.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-notifier": "^10.0.1",
|
||||
"ytdl-core": "^4.11.1",
|
||||
"ytpl": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.25.1",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"electron": "^20.1.1",
|
||||
"electron-builder": "^23.0.3",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-icon-maker": "0.0.5",
|
||||
"playwright": "^1.25.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"xo": "^0.45.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"glob-parent": "5.1.2",
|
||||
"minimist": "1.2.6",
|
||||
"yargs-parser": "18.1.3"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"hideCredit": true,
|
||||
"package": true,
|
||||
"unreleased": true,
|
||||
"output": "changelog.md"
|
||||
},
|
||||
"xo": {
|
||||
"envs": [
|
||||
"node",
|
||||
"browser"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "3.2.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/main/index.js",
|
||||
"license": "MIT",
|
||||
"repository": "th-ch/youtube-music",
|
||||
"author": {
|
||||
"name": "th-ch",
|
||||
"email": "th-ch@users.noreply.github.com",
|
||||
"url": "https://github.com/th-ch/youtube-music"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.github.th-ch.youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"files": [
|
||||
"!*",
|
||||
"dist",
|
||||
"assets",
|
||||
"license",
|
||||
"!node_modules",
|
||||
"node_modules/custom-electron-prompt/**",
|
||||
"node_modules/@cliqz/adblocker-electron-preload/**",
|
||||
"node_modules/@ffmpeg.wasm/core-mt/**",
|
||||
"!node_modules/**/*.map",
|
||||
"!node_modules/**/*.ts"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"assets"
|
||||
],
|
||||
"mac": {
|
||||
"identity": null,
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "assets/generated/icons/mac/icon.icns"
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/generated/icons/win/icon.ico",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis-web",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsisWeb": {
|
||||
"runAfterFinish": false
|
||||
},
|
||||
"linux": {
|
||||
"icon": "assets/generated/icons/png",
|
||||
"category": "AudioVideo",
|
||||
"target": [
|
||||
"AppImage",
|
||||
"snap",
|
||||
"freebsd",
|
||||
"deb",
|
||||
"rpm"
|
||||
]
|
||||
},
|
||||
"rpm": {
|
||||
"depends": [
|
||||
"/usr/lib64/libuuid.so.1"
|
||||
]
|
||||
},
|
||||
"snap": {
|
||||
"slots": [
|
||||
{
|
||||
"mpris": {
|
||||
"interface": "mpris"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"directories": {
|
||||
"output": "./pack/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||
"build": "electron-vite build",
|
||||
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
||||
"start": "electron-vite preview",
|
||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||
"dev": "electron-vite dev --watch",
|
||||
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never",
|
||||
"dist:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
||||
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||
"lint": "eslint .",
|
||||
"changelog": "npx --yes auto-changelog",
|
||||
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
||||
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"esbuild": "0.18.20",
|
||||
"usocket": "1.0.1",
|
||||
"rollup": "4.9.2",
|
||||
"node-gyp": "10.0.1",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "2.0.1",
|
||||
"@babel/runtime": "7.23.7"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "1.26.12",
|
||||
"@cliqz/adblocker-electron-preload": "1.26.12",
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
"@electron/remote": "2.1.1",
|
||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||
"@ffmpeg.wasm/main": "0.12.0",
|
||||
"@foobar404/wave": "2.0.5",
|
||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||
"@jellybrick/mpris-service": "2.1.4",
|
||||
"@xhayper/discord-rpc": "1.1.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"butterchurn": "3.0.0-beta.4",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"color": "4.2.3",
|
||||
"conf": "10.2.0",
|
||||
"custom-electron-prompt": "1.5.7",
|
||||
"dbus-next": "0.10.2",
|
||||
"deepmerge-ts": "5.1.0",
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-is": "3.0.0",
|
||||
"electron-localshortcut": "3.2.1",
|
||||
"electron-store": "8.1.0",
|
||||
"electron-unhandled": "4.0.1",
|
||||
"electron-updater": "6.1.7",
|
||||
"fast-average-color": "9.4.0",
|
||||
"fast-equals": "5.0.1",
|
||||
"filenamify": "6.0.0",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "23.7.13",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
"keyboardevents-areequal": "0.2.2",
|
||||
"node-html-parser": "6.1.12",
|
||||
"node-id3": "0.2.6",
|
||||
"peerjs": "1.5.2",
|
||||
"semver": "7.5.4",
|
||||
"serve": "14.2.1",
|
||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||
"ts-morph": "21.0.1",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.41.0-alpha-dec-18-2023",
|
||||
"@total-typescript/ts-reset": "0.5.1",
|
||||
"@types/color": "3.0.6",
|
||||
"@types/electron-localshortcut": "3.1.3",
|
||||
"@types/howler": "2.2.11",
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@types/semver": "7.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.16.0",
|
||||
"bufferutil": "4.0.8",
|
||||
"builtin-modules": "3.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "5.1.0",
|
||||
"electron": "28.1.0",
|
||||
"electron-builder": "24.9.1",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"electron-vite": "2.0.0-beta.2",
|
||||
"esbuild": "0.18.20",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-prettier": "5.1.2",
|
||||
"glob": "10.3.10",
|
||||
"node-gyp": "10.0.1",
|
||||
"playwright": "1.41.0-alpha-dec-18-2023",
|
||||
"rollup": "4.9.2",
|
||||
"typescript": "5.3.3",
|
||||
"utf-8-validate": "6.0.3",
|
||||
"vite": "5.0.10",
|
||||
"vite-plugin-inspect": "0.8.1",
|
||||
"vite-plugin-resolve": "2.5.1",
|
||||
"ws": "8.16.0"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"hideCredit": true,
|
||||
"package": true,
|
||||
"unreleased": true,
|
||||
"output": "changelog.md"
|
||||
},
|
||||
"packageManager": "pnpm@8.13.1"
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
const { loadAdBlockerEngine } = require("./blocker");
|
||||
module.exports = (win, options) =>
|
||||
loadAdBlockerEngine(
|
||||
win.webContents.session,
|
||||
options.cache,
|
||||
options.additionalBlockLists,
|
||||
options.disableDefaultLists
|
||||
);
|
||||
@ -1,60 +0,0 @@
|
||||
const { promises } = require("fs"); // used for caching
|
||||
const path = require("path");
|
||||
|
||||
const { ElectronBlocker } = require("@cliqz/adblocker-electron");
|
||||
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-2021.txt",
|
||||
// Fanboy Annoyances
|
||||
"https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt",
|
||||
];
|
||||
|
||||
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,
|
||||
{
|
||||
// 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);
|
||||
} else {
|
||||
console.log("Successfully generated adBlocker engine.");
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log("Error loading adBlocker engine", err));
|
||||
};
|
||||
|
||||
module.exports = { loadAdBlockerEngine };
|
||||
if (require.main === module) {
|
||||
loadAdBlockerEngine(); // Generate the engine without enabling it
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = () => {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
require("@cliqz/adblocker-electron-preload/dist/preload.cjs");
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
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 });
|
||||
@ -1,6 +0,0 @@
|
||||
const path = require("path");
|
||||
const { injectCSS } = require("../utils");
|
||||
|
||||
module.exports = win => {
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = () => {
|
||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||
require("simple-youtube-age-restriction-bypass/Simple-YouTube-Age-Restriction-Bypass.user.js");
|
||||
};
|
||||
@ -1,14 +0,0 @@
|
||||
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 })
|
||||
};
|
||||
@ -1,145 +0,0 @@
|
||||
const Discord = require("discord-rpc");
|
||||
const { dev } = require("electron-is");
|
||||
const { dialog, app } = require("electron");
|
||||
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
|
||||
// Application ID registered by @xn-oah
|
||||
const clientId = "942539762227630162";
|
||||
|
||||
/**
|
||||
* @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());
|
||||
};
|
||||
|
||||
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.album,
|
||||
buttons: listenAlong ? [
|
||||
{ label: "Listen Along", url: songInfo.url },
|
||||
] : undefined,
|
||||
};
|
||||
|
||||
if (songInfo.isPaused) {
|
||||
// Add a paused icon to show that the song is paused
|
||||
activityInfo.smallImageKey = "paused";
|
||||
activityInfo.smallImageText = "Paused";
|
||||
// Set start the timer so the activity gets cleared after a while if enabled
|
||||
if (activityTimoutEnabled)
|
||||
clearActivity = setTimeout(() => info.rpc.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", () => {
|
||||
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;
|
||||
@ -1,65 +0,0 @@
|
||||
const prompt = require("custom-electron-prompt");
|
||||
|
||||
const { setMenuOptions } = require("../../config/plugins");
|
||||
const promptOptions = require("../../providers/prompt-options");
|
||||
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;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Listen Along",
|
||||
type: "checkbox",
|
||||
checked: options.listenAlong,
|
||||
click: (item) => {
|
||||
options.listenAlong = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Set inactivity timeout",
|
||||
click: () => setInactivityTimeout(win, options),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
async function setInactivityTimeout(win, options) {
|
||||
let output = await prompt({
|
||||
title: 'Set Inactivity Timeout',
|
||||
label: 'Enter inactivity timeout in seconds:',
|
||||
value: Math.round((options.activityTimoutTime ?? 0) / 1e3),
|
||||
type: "counter",
|
||||
counterOptions: { minimum: 0, multiFire: true },
|
||||
width: 450,
|
||||
...promptOptions()
|
||||
}, win)
|
||||
|
||||
if (output) {
|
||||
options.activityTimoutTime = Math.round(output * 1e3);
|
||||
setMenuOptions("discord", options);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
const CHANNEL = "downloader";
|
||||
const ACTIONS = {
|
||||
ERROR: "error",
|
||||
METADATA: "metadata",
|
||||
PROGRESS: "progress",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
CHANNEL: CHANNEL,
|
||||
ACTIONS: ACTIONS,
|
||||
};
|
||||
@ -1,98 +0,0 @@
|
||||
const { writeFileSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const ID3Writer = require("browser-id3-writer");
|
||||
const { dialog, ipcMain } = require("electron");
|
||||
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
const { injectCSS, listenAction } = require("../utils");
|
||||
const { cropMaxWidth } = require("./utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { isEnabled } = require("../../config/plugins");
|
||||
const { getImage } = require("../../providers/song-info");
|
||||
const { fetchFromGenius } = require("../lyrics-genius/back");
|
||||
|
||||
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: error.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
let nowPlayingMetadata = {};
|
||||
|
||||
function handle(win) {
|
||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||
registerCallback((info) => {
|
||||
nowPlayingMetadata = info;
|
||||
});
|
||||
|
||||
listenAction(CHANNEL, (event, action, arg) => {
|
||||
switch (action) {
|
||||
case ACTIONS.ERROR: // arg = error
|
||||
sendError(win, arg);
|
||||
break;
|
||||
case ACTIONS.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", async (event, filePath, songBuffer, currentMetadata) => {
|
||||
let fileBuffer = songBuffer;
|
||||
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 && !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]);
|
||||
if (coverBuffer) {
|
||||
writer.setFrame("APIC", {
|
||||
type: 3,
|
||||
data: coverBuffer,
|
||||
description: ""
|
||||
});
|
||||
}
|
||||
if (isEnabled("lyrics-genius")) {
|
||||
const lyrics = await fetchFromGenius(songMetadata);
|
||||
if (lyrics) {
|
||||
writer.setFrame("USLT", {
|
||||
description: lyrics,
|
||||
lyrics: lyrics,
|
||||
});
|
||||
}
|
||||
}
|
||||
writer.addTag();
|
||||
fileBuffer = Buffer.from(writer.arrayBuffer);
|
||||
} catch (error) {
|
||||
sendError(win, error);
|
||||
}
|
||||
|
||||
writeFileSync(filePath, fileBuffer);
|
||||
// Notify the youtube-dl file
|
||||
event.reply("add-metadata-done");
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = handle;
|
||||
module.exports.sendError = sendError;
|
||||
@ -1,98 +0,0 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
const { defaultConfig } = require("../../config");
|
||||
const { getSongMenu } = require("../../providers/dom-elements");
|
||||
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(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) return;
|
||||
}
|
||||
if (menu.contains(downloadButton)) return;
|
||||
const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
|
||||
if (menuUrl && !menuUrl.includes('watch?')) return;
|
||||
|
||||
menu.prepend(downloadButton);
|
||||
progress = document.querySelector("#ytmcustom-download");
|
||||
});
|
||||
|
||||
const reinit = () => {
|
||||
triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar
|
||||
if (!progress) {
|
||||
console.warn("Cannot update progress");
|
||||
} else {
|
||||
progress.innerHTML = "Download";
|
||||
}
|
||||
};
|
||||
|
||||
const baseUrl = defaultConfig.url;
|
||||
|
||||
// TODO: re-enable once contextIsolation is set to true
|
||||
// contextBridge.exposeInMainWorld("downloader", {
|
||||
// download: () => {
|
||||
global.download = () => {
|
||||
triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar
|
||||
let metadata;
|
||||
let videoUrl = getSongMenu()
|
||||
// selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||
?.getAttribute("href");
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
videoUrl = baseUrl + "/" + videoUrl;
|
||||
}
|
||||
if (videoUrl.includes('?playlist=')) {
|
||||
ipcRenderer.send('download-playlist-request', videoUrl);
|
||||
return;
|
||||
}
|
||||
metadata = null;
|
||||
} else {
|
||||
metadata = global.songInfo;
|
||||
videoUrl = metadata.url || window.location.href;
|
||||
}
|
||||
|
||||
downloadVideoToMP3(
|
||||
videoUrl,
|
||||
(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);
|
||||
reinit();
|
||||
},
|
||||
reinit,
|
||||
pluginOptions,
|
||||
metadata
|
||||
);
|
||||
};
|
||||
// });
|
||||
|
||||
function observeMenu(options) {
|
||||
pluginOptions = { ...pluginOptions, ...options };
|
||||
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
observer.observe(document.querySelector('ytmusic-popup-container'), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, { once: true, passive: true })
|
||||
}
|
||||
|
||||
module.exports = observeMenu;
|
||||
@ -1,151 +0,0 @@
|
||||
const { existsSync, mkdirSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const { dialog, ipcMain } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const ytpl = require("ytpl");
|
||||
const chokidar = require('chokidar');
|
||||
const filenamify = require('filenamify');
|
||||
|
||||
const { setMenuOptions } = require("../../config/plugins");
|
||||
const { sendError } = require("./back");
|
||||
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
||||
|
||||
let downloadLabel = defaultMenuDownloadLabel;
|
||||
let playingUrl = undefined;
|
||||
let callbackIsRegistered = false;
|
||||
|
||||
// Playlist radio modifier needs to be cut from playlist ID
|
||||
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
|
||||
|
||||
const getPlaylistID = aURL => {
|
||||
const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist");
|
||||
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
|
||||
return result.slice(6)
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = (win, options) => {
|
||||
if (!callbackIsRegistered) {
|
||||
ipcMain.on("video-src-changed", async (_, data) => {
|
||||
playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical;
|
||||
});
|
||||
ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options));
|
||||
callbackIsRegistered = true;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: downloadLabel,
|
||||
click: () => downloadPlaylist(undefined, win, options),
|
||||
},
|
||||
{
|
||||
label: "Choose download folder",
|
||||
click: () => {
|
||||
let result = dialog.showOpenDialogSync({
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
defaultPath: getFolder(options.downloadFolder),
|
||||
});
|
||||
if (result) {
|
||||
options.downloadFolder = result[0];
|
||||
setMenuOptions("downloader", options);
|
||||
} // else = user pressed cancel
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Presets",
|
||||
submenu: Object.keys(presets).map((preset) => ({
|
||||
label: preset,
|
||||
type: "radio",
|
||||
click: () => {
|
||||
options.preset = preset;
|
||||
setMenuOptions("downloader", options);
|
||||
},
|
||||
checked: options.preset === preset || presets[preset] === undefined,
|
||||
})),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
async function downloadPlaylist(givenUrl, win, options) {
|
||||
if (givenUrl) {
|
||||
try {
|
||||
givenUrl = new URL(givenUrl);
|
||||
} catch {
|
||||
givenUrl = undefined;
|
||||
};
|
||||
}
|
||||
const playlistId = getPlaylistID(givenUrl)
|
||||
|| getPlaylistID(new URL(win.webContents.getURL()))
|
||||
|| getPlaylistID(new URL(playingUrl));
|
||||
|
||||
if (!playlistId) {
|
||||
sendError(win, new Error("No playlist ID found"));
|
||||
return;
|
||||
}
|
||||
|
||||
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 safePlaylistTitle = filenamify(playlist.title, {replacement: ' '});
|
||||
|
||||
const folder = getFolder(options.downloadFolder);
|
||||
const playlistFolder = join(folder, safePlaylistTitle);
|
||||
if (existsSync(playlistFolder)) {
|
||||
sendError(
|
||||
win,
|
||||
new Error(`The folder ${playlistFolder} already exists`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
mkdirSync(playlistFolder, { recursive: true });
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: "info",
|
||||
buttons: ["OK"],
|
||||
title: "Started Download",
|
||||
message: `Downloading Playlist "${playlist.title}"`,
|
||||
detail: `(${playlist.items.length} songs)`,
|
||||
});
|
||||
|
||||
if (is.dev()) {
|
||||
console.log(
|
||||
`Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`
|
||||
);
|
||||
}
|
||||
|
||||
win.setProgressBar(2); // starts with indefinite bar
|
||||
|
||||
let downloadCount = 0;
|
||||
setBadge(playlist.items.length);
|
||||
|
||||
let dirWatcher = chokidar.watch(playlistFolder);
|
||||
dirWatcher.on('add', () => {
|
||||
downloadCount += 1;
|
||||
if (downloadCount >= playlist.items.length) {
|
||||
win.setProgressBar(-1); // close progress bar
|
||||
setBadge(0); // close badge counter
|
||||
dirWatcher.close().then(() => (dirWatcher = null));
|
||||
} else {
|
||||
win.setProgressBar(downloadCount / playlist.items.length);
|
||||
setBadge(playlist.items.length - downloadCount);
|
||||
}
|
||||
});
|
||||
|
||||
playlist.items.forEach((song) => {
|
||||
win.webContents.send(
|
||||
"downloader-download-playlist",
|
||||
song.url,
|
||||
safePlaylistTitle,
|
||||
options
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
.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-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);
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
<div
|
||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
onclick="download()"
|
||||
>
|
||||
<div
|
||||
id="navigation-endpoint"
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="icon menu-icon style-scope ytmusic-menu-navigation-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"
|
||||
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>
|
||||
@ -1,46 +0,0 @@
|
||||
const electron = require("electron");
|
||||
const is = require('electron-is');
|
||||
|
||||
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"],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.setBadge = n => {
|
||||
if (is.linux() || is.macOS()) {
|
||||
electron.app.setBadgeCount(n);
|
||||
}
|
||||
}
|
||||
@ -1,200 +0,0 @@
|
||||
const { randomBytes } = require("crypto");
|
||||
const { join } = require("path");
|
||||
|
||||
const Mutex = require("async-mutex").Mutex;
|
||||
const { ipcRenderer } = require("electron");
|
||||
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 { triggerAction, triggerActionSync } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { presets, urlToJPG } = require("./utils");
|
||||
const { cleanupName } = require("../../providers/song-info");
|
||||
|
||||
const { createFFmpeg } = FFmpeg;
|
||||
const ffmpeg = createFFmpeg({
|
||||
log: false,
|
||||
logger: () => {}, // console.log,
|
||||
progress: () => {}, // console.log,
|
||||
});
|
||||
const ffmpegMutex = new Mutex();
|
||||
|
||||
const downloadVideoToMP3 = async (
|
||||
videoUrl,
|
||||
sendFeedback,
|
||||
sendError,
|
||||
reinit,
|
||||
options,
|
||||
metadata = undefined,
|
||||
subfolder = ""
|
||||
) => {
|
||||
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 {
|
||||
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 ratio = downloaded / total;
|
||||
const progress = Math.floor(ratio * 100);
|
||||
sendFeedback("Download: " + progress + "%", ratio);
|
||||
})
|
||||
.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", async () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
await toMP3(
|
||||
videoName,
|
||||
buffer,
|
||||
sendFeedback,
|
||||
sendError,
|
||||
reinit,
|
||||
options,
|
||||
metadata,
|
||||
subfolder
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const toMP3 = async (
|
||||
videoName,
|
||||
buffer,
|
||||
sendFeedback,
|
||||
sendError,
|
||||
reinit,
|
||||
options,
|
||||
existingMetadata = undefined,
|
||||
subfolder = ""
|
||||
) => {
|
||||
const convertOptions = { ...presets[options.preset], ...options };
|
||||
const safeVideoName = randomBytes(32).toString("hex");
|
||||
const extension = convertOptions.extension || "mp3";
|
||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||
|
||||
try {
|
||||
if (!ffmpeg.isLoaded()) {
|
||||
sendFeedback("Loading…", 2); // indefinite progress bar after download
|
||||
await ffmpeg.load();
|
||||
}
|
||||
|
||||
sendFeedback("Preparing file…");
|
||||
ffmpeg.FS("writeFile", safeVideoName, buffer);
|
||||
|
||||
sendFeedback("Converting…");
|
||||
const metadata = existingMetadata || getMetadata();
|
||||
await ffmpeg.run(
|
||||
"-i",
|
||||
safeVideoName,
|
||||
...getFFmpegMetadataArgs(metadata),
|
||||
...(convertOptions.ffmpegArgs || []),
|
||||
safeVideoName + "." + extension
|
||||
);
|
||||
|
||||
const folder = options.downloadFolder || await ipcRenderer.invoke('getDownloadsFolder');
|
||||
const name = metadata.title
|
||||
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
||||
: videoName;
|
||||
const filename = filenamify(name + "." + extension, {
|
||||
replacement: "_",
|
||||
maxLength: 255,
|
||||
});
|
||||
|
||||
const filePath = join(folder, subfolder, filename);
|
||||
const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension);
|
||||
|
||||
// Add the metadata
|
||||
sendFeedback("Adding metadata…");
|
||||
ipcRenderer.send("add-metadata", filePath, fileBuffer, {
|
||||
artist: metadata.artist,
|
||||
title: metadata.title,
|
||||
imageSrcYTPL: metadata.imageSrcYTPL
|
||||
});
|
||||
ipcRenderer.once("add-metadata-done", reinit);
|
||||
} catch (e) {
|
||||
sendError(e);
|
||||
} finally {
|
||||
releaseFFmpegMutex();
|
||||
}
|
||||
};
|
||||
|
||||
const getMetadata = () => {
|
||||
return JSON.parse(triggerActionSync(CHANNEL, ACTIONS.METADATA));
|
||||
};
|
||||
|
||||
const getFFmpegMetadataArgs = (metadata) => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []),
|
||||
...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []),
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
downloadVideoToMP3,
|
||||
};
|
||||
|
||||
ipcRenderer.on(
|
||||
"downloader-download-playlist",
|
||||
(_, url, playlistFolder, options) => {
|
||||
downloadVideoToMP3(
|
||||
url,
|
||||
() => {},
|
||||
(error) => {
|
||||
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
||||
},
|
||||
() => {},
|
||||
options,
|
||||
null,
|
||||
playlistFolder
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -1,47 +0,0 @@
|
||||
// "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,
|
||||
});
|
||||
@ -1,33 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const electronLocalshortcut = require("electron-localshortcut");
|
||||
|
||||
const config = require("../../config");
|
||||
const { injectCSS } = require("../utils");
|
||||
|
||||
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
|
||||
setupTitlebar();
|
||||
|
||||
//tracks menu visibility
|
||||
let visible = !config.get("options.hideMenu");
|
||||
|
||||
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", () => {
|
||||
attachTitlebarToWindow(win);
|
||||
|
||||
//register keyboard shortcut && hide menu if hideMenu is enabled
|
||||
if (config.get("options.hideMenu")) {
|
||||
electronLocalshortcut.register(win, "Esc", () => {
|
||||
setMenuVisibility(!visible);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function setMenuVisibility(value) {
|
||||
visible = value;
|
||||
win.webContents.send("refreshMenu", visible);
|
||||
}
|
||||
};
|
||||
@ -1,54 +0,0 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const config = require("../../config");
|
||||
const { Titlebar, Color } = require("custom-electron-titlebar");
|
||||
const { isEnabled } = require("../../config/plugins");
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
|
||||
module.exports = (options) => {
|
||||
let visible = !config.get("options.hideMenu");
|
||||
const bar = new Titlebar({
|
||||
backgroundColor: Color.fromHex("#050505"),
|
||||
itemBackgroundColor: Color.fromHex("#1d1d1d"),
|
||||
svgColor: Color.WHITE,
|
||||
menu: visible ? undefined : null
|
||||
});
|
||||
bar.updateTitle(" ");
|
||||
document.title = "Youtube Music";
|
||||
|
||||
const hideIcon = hide => $('.cet-window-icon').style.display = hide ? 'none' : 'flex';
|
||||
|
||||
if (options.hideIcon) hideIcon(true);
|
||||
|
||||
ipcRenderer.on("refreshMenu", (_, showMenu) => {
|
||||
if (showMenu === undefined && !visible) return;
|
||||
if (showMenu === false) {
|
||||
bar.updateMenu(null);
|
||||
visible = false;
|
||||
} else {
|
||||
bar.refreshMenu();
|
||||
visible = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isEnabled("picture-in-picture")) {
|
||||
ipcRenderer.on("pip-toggle", (_, pipEnabled) => {
|
||||
bar.refreshMenu();
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide));
|
||||
|
||||
// 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';
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
const { setOptions } = require("../../config/plugins");
|
||||
|
||||
module.exports = (win, options) => [
|
||||
{
|
||||
label: "Hide Icon",
|
||||
type: "checkbox",
|
||||
checked: options.hideIcon,
|
||||
click: (item) => {
|
||||
win.webContents.send("hideIcon", item.checked);
|
||||
options.hideIcon = item.checked;
|
||||
setOptions("in-app-menu", options);
|
||||
},
|
||||
}
|
||||
];
|
||||
@ -1,82 +0,0 @@
|
||||
/* increase font size for menu and menuItems */
|
||||
.titlebar,
|
||||
.menubar-menu-container .action-label {
|
||||
font-size: 14px !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;
|
||||
}
|
||||
|
||||
/* fixes top gap between nav-bar and browse-page */
|
||||
#browse-page {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
||||
ytmusic-nav-bar,
|
||||
.tab-titleiron-icon,
|
||||
ytmusic-pivot-bar-item-renderer {
|
||||
-webkit-app-region: unset !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*/
|
||||
ytmusic-header-renderer.ytmusic-search-page {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
/* Move navBar downwards */
|
||||
ytmusic-nav-bar[slot="nav-bar"] {
|
||||
top: 17px !important;
|
||||
}
|
||||
|
||||
/* fix page progress bar position*/
|
||||
yt-page-navigation-progress,
|
||||
#progress.yt-page-navigation-progress {
|
||||
top: 30px !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
background-color: #030303;
|
||||
border-radius: 100px;
|
||||
-moz-border-radius: 100px;
|
||||
-webkit-border-radius: 100px;
|
||||
}
|
||||
/* hover effect for both scrollbar area, and scrollbar 'thumb' */
|
||||
::-webkit-scrollbar:hover {
|
||||
background-color: rgba(15, 15, 15, 0.699);
|
||||
}
|
||||
|
||||
/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
|
||||
::-webkit-scrollbar-thumb:vertical {
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
|
||||
background: #3a3a3a;
|
||||
background-clip: padding-box;
|
||||
border-radius: 100px;
|
||||
-moz-border-radius: 100px;
|
||||
-webkit-border-radius: 100px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:vertical:active {
|
||||
background: #4d4c4c; /* Some darker color when you click it */
|
||||
border-radius: 100px;
|
||||
-moz-border-radius: 100px;
|
||||
-webkit-border-radius: 100px;
|
||||
}
|
||||
|
||||
.cet-menubar-menu-container .cet-action-item {
|
||||
background-color: inherit
|
||||
}
|
||||
@ -1,161 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
const md5 = require('md5');
|
||||
const { shell } = require('electron');
|
||||
const { setOptions } = require('../../config/plugins');
|
||||
const registerCallback = require('../../providers/song-info');
|
||||
const defaultConfig = require('../../config/defaults');
|
||||
|
||||
const createFormData = params => {
|
||||
// creates the body for in the post request
|
||||
const formData = new URLSearchParams();
|
||||
for (const key in params) {
|
||||
formData.append(key, params[key]);
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
const createQueryString = (params, api_sig) => {
|
||||
// creates a querystring
|
||||
const queryData = [];
|
||||
params.api_sig = api_sig;
|
||||
for (const key in params) {
|
||||
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
|
||||
}
|
||||
return '?'+queryData.join('&');
|
||||
}
|
||||
|
||||
const createApiSig = (params, secret) => {
|
||||
// this function creates the api signature, see: https://www.last.fm/api/authspec
|
||||
const keys = [];
|
||||
for (const key in params) {
|
||||
keys.push(key);
|
||||
}
|
||||
keys.sort();
|
||||
let sig = '';
|
||||
for (const key of keys) {
|
||||
if (String(key) === 'format')
|
||||
continue
|
||||
sig += `${key}${params[key]}`;
|
||||
}
|
||||
sig += secret;
|
||||
sig = md5(sig);
|
||||
return sig;
|
||||
}
|
||||
|
||||
const createToken = async ({ api_key, api_root, secret }) => {
|
||||
// creates and stores the auth token
|
||||
const data = {
|
||||
method: 'auth.gettoken',
|
||||
api_key: api_key,
|
||||
format: 'json'
|
||||
};
|
||||
const api_sig = createApiSig(data, secret);
|
||||
let response = await fetch(`${api_root}${createQueryString(data, api_sig)}`);
|
||||
response = await response.json();
|
||||
return response?.token;
|
||||
}
|
||||
|
||||
const authenticate = async config => {
|
||||
// asks the user for authentication
|
||||
config.token = await createToken(config);
|
||||
setOptions('last-fm', config);
|
||||
shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||
return config;
|
||||
}
|
||||
|
||||
const getAndSetSessionKey = async config => {
|
||||
// get and store the session key
|
||||
const data = {
|
||||
api_key: config.api_key,
|
||||
format: 'json',
|
||||
method: 'auth.getsession',
|
||||
token: config.token,
|
||||
};
|
||||
const api_sig = createApiSig(data, config.secret);
|
||||
let res = await fetch(`${config.api_root}${createQueryString(data, api_sig)}`);
|
||||
res = await res.json();
|
||||
if (res.error)
|
||||
await authenticate(config);
|
||||
config.session_key = res?.session?.key;
|
||||
setOptions('last-fm', config);
|
||||
return config;
|
||||
}
|
||||
|
||||
const postSongDataToAPI = async (songInfo, config, data) => {
|
||||
// this sends a post request to the api, and adds the common data
|
||||
if (!config.session_key)
|
||||
await getAndSetSessionKey(config);
|
||||
|
||||
const postData = {
|
||||
track: songInfo.title,
|
||||
duration: songInfo.songDuration,
|
||||
artist: songInfo.artist,
|
||||
...(songInfo.album ? { album: songInfo.album } : undefined), // will be undefined if current song is a video
|
||||
api_key: config.api_key,
|
||||
sk: config.session_key,
|
||||
format: 'json',
|
||||
...data,
|
||||
};
|
||||
|
||||
postData.api_sig = createApiSig(postData, config.secret);
|
||||
fetch('https://ws.audioscrobbler.com/2.0/', {method: 'POST', body: createFormData(postData)})
|
||||
.catch(res => {
|
||||
if (res.response.data.error == 9) {
|
||||
// session key is invalid, so remove it from the config and reauthenticate
|
||||
config.session_key = undefined;
|
||||
setOptions('last-fm', config);
|
||||
authenticate(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const addScrobble = (songInfo, config) => {
|
||||
// this adds one scrobbled song to last.fm
|
||||
const data = {
|
||||
method: 'track.scrobble',
|
||||
timestamp: ~~((Date.now() - songInfo.elapsedSeconds) / 1000),
|
||||
};
|
||||
postSongDataToAPI(songInfo, config, data);
|
||||
}
|
||||
|
||||
const setNowPlaying = (songInfo, config) => {
|
||||
// this sets the now playing status in last.fm
|
||||
const data = {
|
||||
method: 'track.updateNowPlaying',
|
||||
};
|
||||
postSongDataToAPI(songInfo, config, data);
|
||||
}
|
||||
|
||||
|
||||
// this will store the timeout that will trigger addScrobble
|
||||
let scrobbleTimer = undefined;
|
||||
|
||||
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;
|
||||
setOptions('last-fm', config);
|
||||
}
|
||||
|
||||
if (!config.session_key) {
|
||||
// not authenticated
|
||||
config = await getAndSetSessionKey(config);
|
||||
}
|
||||
|
||||
registerCallback( songInfo => {
|
||||
// set remove the old scrobble timer
|
||||
clearTimeout(scrobbleTimer);
|
||||
if (!songInfo.isPaused) {
|
||||
setNowPlaying(songInfo, config);
|
||||
// scrobble when the song is half way through, or has passed the 4 minute mark
|
||||
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
||||
if (scrobbleTime > songInfo.elapsedSeconds) {
|
||||
// scrobble still needs to happen
|
||||
const timeToWait = (scrobbleTime - songInfo.elapsedSeconds) * 1000;
|
||||
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = lastfm;
|
||||
@ -1,71 +0,0 @@
|
||||
const { join } = require("path");
|
||||
|
||||
const { ipcMain } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const { convert } = require("html-to-text");
|
||||
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);
|
||||
event.returnValue = await fetchFromGenius(metadata);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFromGenius = async (metadata) => {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = await response.json();
|
||||
let url = "";
|
||||
try {
|
||||
url = info.response.sections.filter((section) => section.type === "song")[0]
|
||||
.hits[0].result.url;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is.dev()) {
|
||||
console.log("Fetching lyrics from Genius:", url);
|
||||
}
|
||||
|
||||
response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const lyrics = convert(html, {
|
||||
baseElements: {
|
||||
selectors: ['[class^="Lyrics__Container"]', ".lyrics"],
|
||||
},
|
||||
selectors: [
|
||||
{
|
||||
selector: "a",
|
||||
format: "linkFormatter",
|
||||
},
|
||||
],
|
||||
formatters: {
|
||||
// Remove links by keeping only the content
|
||||
linkFormatter: (elem, walk, builder) => {
|
||||
walk(elem.children, builder);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return lyrics;
|
||||
};
|
||||
|
||||
module.exports.fetchFromGenius = fetchFromGenius;
|
||||
@ -1,94 +0,0 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const is = require("electron-is");
|
||||
|
||||
module.exports = () => {
|
||||
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
|
||||
const tabList = document.querySelectorAll("tp-yt-paper-tab");
|
||||
const tabs = {
|
||||
upNext: tabList[0],
|
||||
lyrics: tabList[1],
|
||||
discover: tabList[2],
|
||||
}
|
||||
|
||||
// Check if disabled
|
||||
if (!tabs.lyrics?.hasAttribute("disabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasLyrics = true;
|
||||
|
||||
const lyrics = ipcRenderer.sendSync(
|
||||
"search-genius-lyrics",
|
||||
extractedSongInfo
|
||||
);
|
||||
if (!lyrics) {
|
||||
// Delete previous lyrics if tab is open and couldn't get new lyrics
|
||||
checkLyricsContainer(() => {
|
||||
hasLyrics = false;
|
||||
setTabsOnclick(undefined);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (is.dev()) {
|
||||
console.log("Fetched lyrics from Genius");
|
||||
}
|
||||
|
||||
enableLyricsTab();
|
||||
|
||||
setTabsOnclick(enableLyricsTab);
|
||||
|
||||
checkLyricsContainer();
|
||||
|
||||
tabs.lyrics.onclick = () => {
|
||||
const tabContainer = document.querySelector("ytmusic-tab-renderer");
|
||||
const observer = new MutationObserver((_, observer) => {
|
||||
checkLyricsContainer(() => observer.disconnect());
|
||||
});
|
||||
observer.observe(tabContainer, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
|
||||
function checkLyricsContainer(callback = () => {}) {
|
||||
const lyricsContainer = document.querySelector(
|
||||
'[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer'
|
||||
);
|
||||
if (lyricsContainer) {
|
||||
callback();
|
||||
setLyrics(lyricsContainer)
|
||||
}
|
||||
}
|
||||
|
||||
function setLyrics(lyricsContainer) {
|
||||
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
|
||||
${
|
||||
hasLyrics
|
||||
? lyrics.replace(/(?:\r\n|\r|\n)/g, "<br/>")
|
||||
: "Could not retrieve lyrics from genius"
|
||||
}
|
||||
|
||||
</div>
|
||||
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
|
||||
if (hasLyrics) {
|
||||
lyricsContainer.querySelector('.footer').textContent = 'Source: Genius';
|
||||
enableLyricsTab();
|
||||
}
|
||||
}
|
||||
|
||||
function setTabsOnclick(callback) {
|
||||
for (const tab of [tabs.upNext, tabs.discover]) {
|
||||
if (tab) {
|
||||
tab.onclick = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enableLyricsTab() {
|
||||
tabs.lyrics.removeAttribute("disabled");
|
||||
tabs.lyrics.removeAttribute("aria-disabled");
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
/* Disable links in Genius lyrics */
|
||||
.genius-lyrics a {
|
||||
color: var(--ytmusic-text-primary);
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: clamp(1.4rem, 1.1vmax, 3rem) !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
const { triggerAction } = require("../utils");
|
||||
|
||||
const CHANNEL = "navigation";
|
||||
const ACTIONS = {
|
||||
NEXT: "next",
|
||||
BACK: "back",
|
||||
};
|
||||
|
||||
function goToNextPage() {
|
||||
triggerAction(CHANNEL, ACTIONS.NEXT);
|
||||
}
|
||||
|
||||
function goToPreviousPage() {
|
||||
triggerAction(CHANNEL, ACTIONS.BACK);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CHANNEL: CHANNEL,
|
||||
ACTIONS: ACTIONS,
|
||||
actions: {
|
||||
goToNextPage: goToNextPage,
|
||||
goToPreviousPage: goToPreviousPage,
|
||||
},
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const { injectCSS, listenAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
|
||||
function handle(win) {
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"), () => {
|
||||
win.webContents.send("navigation-css-ready");
|
||||
});
|
||||
|
||||
listenAction(CHANNEL, (event, action) => {
|
||||
switch (action) {
|
||||
case ACTIONS.NEXT:
|
||||
if (win.webContents.canGoForward()) {
|
||||
win.webContents.goForward();
|
||||
}
|
||||
break;
|
||||
case ACTIONS.BACK:
|
||||
if (win.webContents.canGoBack()) {
|
||||
win.webContents.goBack();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown action: " + action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = handle;
|
||||
@ -1,19 +0,0 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
|
||||
function run() {
|
||||
ipcRenderer.on("navigation-css-ready", () => {
|
||||
const forwardButton = ElementFromFile(
|
||||
templatePath(__dirname, "forward.html")
|
||||
);
|
||||
const backButton = ElementFromFile(templatePath(__dirname, "back.html"));
|
||||
const menu = document.querySelector("ytmusic-pivot-bar-renderer");
|
||||
|
||||
if (menu) {
|
||||
menu.prepend(backButton, forwardButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = run;
|
||||
@ -1,35 +0,0 @@
|
||||
.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;
|
||||
--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);
|
||||
}
|
||||
|
||||
.navigation-item:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.navigation-icon {
|
||||
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);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
<div
|
||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||
tab-id="FEmusic_back"
|
||||
role="tab"
|
||||
onclick="goToPreviousPage()"
|
||||
>
|
||||
<div
|
||||
class="search-icon style-scope ytmusic-search-box"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-disabled="false"
|
||||
title="Go to previous page"
|
||||
>
|
||||
<div
|
||||
id="icon"
|
||||
class="tab-icon style-scope paper-icon-button navigation-icon"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 492 492"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope iron-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
>
|
||||
<g class="style-scope iron-icon">
|
||||
<path
|
||||
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,35 +0,0 @@
|
||||
<div
|
||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||
tab-id="FEmusic_next"
|
||||
role="tab"
|
||||
onclick="goToNextPage()"
|
||||
>
|
||||
<div
|
||||
class="search-icon style-scope ytmusic-search-box"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-disabled="false"
|
||||
title="Go to next page"
|
||||
>
|
||||
<div
|
||||
id="icon"
|
||||
class="tab-icon style-scope paper-icon-button navigation-icon"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 492 492"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope iron-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%;"
|
||||
>
|
||||
<g class="style-scope iron-icon">
|
||||
<path
|
||||
d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1
|
||||
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"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,6 +0,0 @@
|
||||
const { injectCSS } = require("../utils");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = win => {
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
function removeLoginElements() {
|
||||
const elementsToRemove = [
|
||||
".sign-in-link.ytmusic-nav-bar",
|
||||
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]'
|
||||
];
|
||||
|
||||
elementsToRemove.forEach(selector => {
|
||||
const node = document.querySelector(selector);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = removeLoginElements;
|
||||
@ -1,4 +0,0 @@
|
||||
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"],
|
||||
.sign-in-link {
|
||||
display: none !important;
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
const { Notification } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
const { notificationImage } = require("./utils");
|
||||
|
||||
const notify = (info, options) => {
|
||||
|
||||
// Fill the notification with content
|
||||
const notification = {
|
||||
title: info.title || "Playing",
|
||||
body: info.artist,
|
||||
icon: notificationImage(info),
|
||||
silent: true,
|
||||
urgency: options.urgency,
|
||||
};
|
||||
|
||||
// Send the notification
|
||||
const currentNotification = new Notification(notification);
|
||||
currentNotification.show()
|
||||
|
||||
return currentNotification;
|
||||
};
|
||||
|
||||
const setup = (options) => {
|
||||
let oldNotification;
|
||||
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 ?
|
||||
require("./interactive")(win, options.unpauseNotification) :
|
||||
setup(options);
|
||||
};
|
||||
@ -1,106 +0,0 @@
|
||||
const { notificationImage, icons } = require("./utils");
|
||||
const getSongControls = require('../../providers/song-controls');
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
const is = require("electron-is");
|
||||
const WindowsToaster = require('node-notifier').WindowsToaster;
|
||||
|
||||
const notifier = new WindowsToaster({ withFallback: true });
|
||||
|
||||
//store song controls reference on launch
|
||||
let controls;
|
||||
let notificationOnUnpause;
|
||||
|
||||
module.exports = (win, unpauseNotification) => {
|
||||
//Save controls and onPause option
|
||||
const { playPause, next, previous } = getSongControls(win);
|
||||
controls = { playPause, next, previous };
|
||||
notificationOnUnpause = unpauseNotification;
|
||||
|
||||
let currentUrl;
|
||||
|
||||
// Register songInfoCallback
|
||||
registerCallback(songInfo => {
|
||||
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
|
||||
currentUrl = songInfo.url;
|
||||
sendToaster(songInfo);
|
||||
}
|
||||
});
|
||||
|
||||
win.webContents.once("closed", () => {
|
||||
deleteNotification()
|
||||
});
|
||||
}
|
||||
|
||||
//delete old notification
|
||||
let toDelete;
|
||||
function deleteNotification() {
|
||||
if (toDelete !== undefined) {
|
||||
// To remove the notification it has to be done this way
|
||||
const removeNotif = Object.assign(toDelete, {
|
||||
remove: toDelete.id
|
||||
})
|
||||
notifier.notify(removeNotif)
|
||||
|
||||
toDelete = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
//New notification
|
||||
function sendToaster(songInfo) {
|
||||
deleteNotification();
|
||||
//download image and get path
|
||||
let imgSrc = notificationImage(songInfo, true);
|
||||
toDelete = {
|
||||
appID: is.dev() ? undefined : "com.github.th-ch.youtube-music",
|
||||
title: songInfo.title || "Playing",
|
||||
message: songInfo.artist,
|
||||
id: parseInt(Math.random() * 1000000, 10),
|
||||
icon: imgSrc,
|
||||
actions: [
|
||||
icons.previous,
|
||||
songInfo.isPaused ? icons.play : icons.pause,
|
||||
icons.next
|
||||
],
|
||||
sound: false,
|
||||
};
|
||||
//send notification
|
||||
notifier.notify(
|
||||
toDelete,
|
||||
(err, data) => {
|
||||
// Will also wait until notification is closed.
|
||||
if (err) {
|
||||
console.log(`ERROR = ${err.toString()}\n DATA = ${data}`);
|
||||
}
|
||||
switch (data) {
|
||||
//buttons
|
||||
case icons.previous.normalize():
|
||||
controls.previous();
|
||||
return;
|
||||
case icons.next.normalize():
|
||||
controls.next();
|
||||
return;
|
||||
case icons.play.normalize():
|
||||
controls.playPause();
|
||||
// dont delete notification on play/pause
|
||||
toDelete = undefined;
|
||||
//manually send notification if not sending automatically
|
||||
if (!notificationOnUnpause) {
|
||||
songInfo.isPaused = false;
|
||||
sendToaster(songInfo);
|
||||
}
|
||||
return;
|
||||
case icons.pause.normalize():
|
||||
controls.playPause();
|
||||
songInfo.isPaused = true;
|
||||
toDelete = undefined;
|
||||
sendToaster(songInfo);
|
||||
return;
|
||||
//Native datatype
|
||||
case "dismissed":
|
||||
case "timeout":
|
||||
deleteNotification();
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
const { urgencyLevels, setOption } = require("./utils");
|
||||
const is = require("electron-is");
|
||||
|
||||
module.exports = (win, options) => [
|
||||
...(is.linux() ?
|
||||
[{
|
||||
label: "Notification Priority",
|
||||
submenu: urgencyLevels.map(level => ({
|
||||
label: level.name,
|
||||
type: "radio",
|
||||
checked: options.urgency === level.value,
|
||||
click: () => setOption(options, "urgency", level.value)
|
||||
})),
|
||||
}] :
|
||||
[]),
|
||||
...(is.windows() ?
|
||||
[{
|
||||
label: "Interactive Notifications",
|
||||
type: "checkbox",
|
||||
checked: options.interactive,
|
||||
click: (item) => setOption(options, "interactive", item.checked)
|
||||
}] :
|
||||
[]),
|
||||
{
|
||||
label: "Show notification on unpause",
|
||||
type: "checkbox",
|
||||
checked: options.unpauseNotification,
|
||||
click: (item) => setOption(options, "unpauseNotification", item.checked)
|
||||
},
|
||||
];
|
||||
@ -1,56 +0,0 @@
|
||||
const { setMenuOptions } = require("../../config/plugins");
|
||||
const path = require("path");
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
|
||||
const icon = "assets/youtube-music.png";
|
||||
const tempIcon = path.join(app.getPath("userData"), "tempIcon.png");
|
||||
|
||||
module.exports.icons = {
|
||||
play: "\u{1405}", // ᐅ
|
||||
pause: "\u{2016}", // ‖
|
||||
next: "\u{1433}", // ᐳ
|
||||
previous: "\u{1438}" // ᐸ
|
||||
}
|
||||
|
||||
module.exports.setOption = (options, option, value) => {
|
||||
options[option] = value;
|
||||
setMenuOptions("notifications", options)
|
||||
}
|
||||
|
||||
module.exports.urgencyLevels = [
|
||||
{ name: "Low", value: "low" },
|
||||
{ name: "Normal", value: "normal" },
|
||||
{ name: "High", value: "critical" },
|
||||
];
|
||||
|
||||
module.exports.notificationImage = function (songInfo, saveIcon = false) {
|
||||
//return local path to temp icon
|
||||
if (saveIcon && !!songInfo.image) {
|
||||
try {
|
||||
fs.writeFileSync(tempIcon,
|
||||
centerNativeImage(songInfo.image)
|
||||
.toPNG()
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(`Error writing song icon to disk:\n${err.toString()}`)
|
||||
return icon;
|
||||
}
|
||||
return tempIcon;
|
||||
}
|
||||
//else: return image
|
||||
return songInfo.image
|
||||
? centerNativeImage(songInfo.image)
|
||||
: icon
|
||||
};
|
||||
|
||||
function centerNativeImage(nativeImage) {
|
||||
const tempImage = nativeImage.resize({ height: 256 });
|
||||
const margin = Math.max((tempImage.getSize().width - 256), 0);
|
||||
|
||||
return tempImage.crop({
|
||||
x: Math.round(margin / 2),
|
||||
y: 0,
|
||||
width: 256, height: 256
|
||||
})
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
const { Menu, app } = require("electron");
|
||||
const { setApplicationMenu } = require("../../../menu");
|
||||
|
||||
module.exports = (win, options, setOptions, togglePip, isInPip) => {
|
||||
if (isInPip) {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||
{
|
||||
label: "App",
|
||||
submenu: [
|
||||
{
|
||||
label: "Exit Picture in Picture",
|
||||
click: togglePip,
|
||||
},
|
||||
{
|
||||
label: "Always on top",
|
||||
type: "checkbox",
|
||||
checked: options.alwaysOnTop,
|
||||
click: (item) => {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
click: () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
]));
|
||||
} else {
|
||||
setApplicationMenu(win);
|
||||
}
|
||||
};
|
||||
@ -1,109 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const { app, ipcMain } = require("electron");
|
||||
const electronLocalshortcut = require("electron-localshortcut");
|
||||
|
||||
const { setOptions, isEnabled } = require("../../config/plugins");
|
||||
const { injectCSS } = require("../utils");
|
||||
|
||||
let isInPiP = false;
|
||||
let originalPosition;
|
||||
let originalSize;
|
||||
let originalFullScreen;
|
||||
let originalMaximized;
|
||||
|
||||
let win;
|
||||
let options;
|
||||
|
||||
const pipPosition = () => (options.savePosition && options["pip-position"]) || [10, 10];
|
||||
const pipSize = () => (options.saveSize && options["pip-size"]) || [450, 275];
|
||||
|
||||
const setLocalOptions = (_options) => {
|
||||
options = { ...options, ..._options };
|
||||
setOptions("picture-in-picture", _options);
|
||||
}
|
||||
|
||||
|
||||
const adaptors = [];
|
||||
const runAdaptors = () => adaptors.forEach(a => a());
|
||||
|
||||
if (isEnabled("in-app-menu")) {
|
||||
let adaptor = require("./adaptors/in-app-menu");
|
||||
adaptors.push(() => adaptor(win, options, setLocalOptions, togglePiP, isInPiP));
|
||||
}
|
||||
|
||||
const togglePiP = async () => {
|
||||
isInPiP = !isInPiP;
|
||||
setLocalOptions({ isInPiP });
|
||||
|
||||
if (isInPiP) {
|
||||
originalFullScreen = win.isFullScreen();
|
||||
if (originalFullScreen) win.setFullScreen(false);
|
||||
originalMaximized = win.isMaximized();
|
||||
if (originalMaximized) win.unmaximize();
|
||||
|
||||
originalPosition = win.getPosition();
|
||||
originalSize = win.getSize();
|
||||
|
||||
win.webContents.on("before-input-event", blockShortcutsInPiP);
|
||||
|
||||
win.setFullScreenable(false);
|
||||
|
||||
runAdaptors();
|
||||
win.webContents.send("pip-toggle", true);
|
||||
|
||||
app.dock?.hide();
|
||||
win.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
app.dock?.show();
|
||||
if (options.alwaysOnTop) {
|
||||
win.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
runAdaptors();
|
||||
win.webContents.send("pip-toggle", false);
|
||||
|
||||
win.setVisibleOnAllWorkspaces(false);
|
||||
win.setAlwaysOnTop(false);
|
||||
|
||||
if (originalFullScreen) win.setFullScreen(true);
|
||||
if (originalMaximized) win.maximize();
|
||||
}
|
||||
|
||||
const [x, y] = isInPiP ? pipPosition() : originalPosition;
|
||||
const [w, h] = isInPiP ? pipSize() : originalSize;
|
||||
win.setPosition(x, y);
|
||||
win.setSize(w, h);
|
||||
|
||||
win.setWindowButtonVisibility?.(!isInPiP);
|
||||
};
|
||||
|
||||
const blockShortcutsInPiP = (event, input) => {
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
if (key === "f") {
|
||||
event.preventDefault();
|
||||
} else if (key === 'escape') {
|
||||
togglePiP();
|
||||
event.preventDefault();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (_win, _options) => {
|
||||
options ??= _options;
|
||||
win ??= _win;
|
||||
setLocalOptions({ isInPiP });
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
ipcMain.on("picture-in-picture", async () => {
|
||||
await togglePiP();
|
||||
});
|
||||
if (options.hotkey) {
|
||||
electronLocalshortcut.register(win, options.hotkey, togglePiP);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.setOptions = setLocalOptions;
|
||||
@ -1,90 +0,0 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
const { getSongMenu } = require("../../providers/dom-elements");
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
|
||||
let menu = null;
|
||||
const pipButton = ElementFromFile(
|
||||
templatePath(__dirname, "picture-in-picture.html")
|
||||
);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) return;
|
||||
}
|
||||
if (menu.contains(pipButton)) return;
|
||||
const menuUrl = $(
|
||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
|
||||
)?.href;
|
||||
if (menuUrl && !menuUrl.includes("watch?")) return;
|
||||
|
||||
menu.prepend(pipButton);
|
||||
});
|
||||
|
||||
global.togglePictureInPicture = () => {
|
||||
ipcRenderer.send("picture-in-picture");
|
||||
};
|
||||
|
||||
const listenForToggle = () => {
|
||||
const originalExitButton = $(".exit-fullscreen-button");
|
||||
const clonedExitButton = originalExitButton.cloneNode(true);
|
||||
clonedExitButton.onclick = () => togglePictureInPicture();
|
||||
|
||||
const appLayout = $("ytmusic-app-layout");
|
||||
const expandMenu = $('#expanding-menu');
|
||||
const middleControls = $('.middle-controls');
|
||||
const playerPage = $("ytmusic-player-page");
|
||||
const togglePlayerPageButton = $(".toggle-player-page-button");
|
||||
const fullScreenButton = $(".fullscreen-button");
|
||||
const player = $('#player');
|
||||
const onPlayerDblClick = player.onDoubleClick_;
|
||||
|
||||
ipcRenderer.on('pip-toggle', (_, isPip) => {
|
||||
if (isPip) {
|
||||
$(".exit-fullscreen-button").replaceWith(clonedExitButton);
|
||||
player.onDoubleClick_ = () => {};
|
||||
expandMenu.onmouseleave = () => middleControls.click();
|
||||
if (!playerPage.playerPageOpen_) {
|
||||
togglePlayerPageButton.click();
|
||||
}
|
||||
fullScreenButton.click();
|
||||
appLayout.classList.add("pip");
|
||||
} else {
|
||||
$(".exit-fullscreen-button").replaceWith(originalExitButton);
|
||||
player.onDoubleClick_ = onPlayerDblClick;
|
||||
expandMenu.onmouseleave = undefined;
|
||||
originalExitButton.click();
|
||||
appLayout.classList.remove("pip");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function observeMenu(options) {
|
||||
document.addEventListener(
|
||||
"apiLoaded",
|
||||
() => {
|
||||
listenForToggle();
|
||||
const minButton = $(".player-minimize-button");
|
||||
// remove native listeners
|
||||
minButton.replaceWith(minButton.cloneNode(true));
|
||||
$(".player-minimize-button").onclick = () => {
|
||||
global.togglePictureInPicture();
|
||||
setTimeout(() => $('#player').click());
|
||||
};
|
||||
|
||||
// allows easily closing the menu by programmatically clicking outside of it
|
||||
$("#expanding-menu").removeAttribute("no-cancel-on-outside-click");
|
||||
// TODO: think about wether an additional button in songMenu is needed
|
||||
observer.observe($("ytmusic-popup-container"), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
},
|
||||
{ once: true, passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = observeMenu;
|
||||
@ -1,60 +0,0 @@
|
||||
const prompt = require("custom-electron-prompt");
|
||||
|
||||
const promptOptions = require("../../providers/prompt-options");
|
||||
const { setOptions } = require("./back.js");
|
||||
|
||||
module.exports = (win, options) => [
|
||||
{
|
||||
label: "Always on top",
|
||||
type: "checkbox",
|
||||
checked: options.alwaysOnTop,
|
||||
click: (item) => {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Save window position",
|
||||
type: "checkbox",
|
||||
checked: options.savePosition,
|
||||
click: (item) => {
|
||||
setOptions({ savePosition: item.checked });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Save window size",
|
||||
type: "checkbox",
|
||||
checked: options.saveSize,
|
||||
click: (item) => {
|
||||
setOptions({ saveSize: item.checked });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Hotkey",
|
||||
type: "checkbox",
|
||||
checked: options.hotkey,
|
||||
click: async (item) => {
|
||||
const output = await prompt({
|
||||
title: "Picture in Picture Hotkey",
|
||||
label: "Choose a hotkey for toggling Picture in Picture",
|
||||
type: "keybind",
|
||||
keybindOptions: [{
|
||||
value: "hotkey",
|
||||
label: "Hotkey",
|
||||
default: options.hotkey
|
||||
}],
|
||||
...promptOptions()
|
||||
}, win)
|
||||
|
||||
if (output) {
|
||||
const { value, accelerator } = output[0];
|
||||
setOptions({ [value]: accelerator });
|
||||
|
||||
item.checked = !!accelerator;
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
@ -1,26 +0,0 @@
|
||||
/* improve visibility of the player bar elements */
|
||||
ytmusic-app-layout.pip ytmusic-player-bar svg,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .time-info,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string {
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
color: white !important;
|
||||
fill: white !important;
|
||||
}
|
||||
|
||||
/* improve the style of the player bar expanding menu */
|
||||
ytmusic-app-layout.pip ytmusic-player-expanding-menu {
|
||||
border-radius: 30px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(5px) brightness(20%);
|
||||
}
|
||||
|
||||
/* fix volumeHud position when both in-app-menu and PiP are active */
|
||||
.cet-container ytmusic-app-layout.pip #volumeHud {
|
||||
top: 22px !important;
|
||||
}
|
||||
|
||||
/* disable the video-toggle button when in PiP mode */
|
||||
ytmusic-app-layout.pip .video-switch-button {
|
||||
display: none !important;
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
<div
|
||||
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
onclick="togglePictureInPicture()"
|
||||
>
|
||||
<div
|
||||
id="navigation-endpoint"
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
|
||||
>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
style="enable-background: new 0 0 512 512"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #aaaaaa;
|
||||
}
|
||||
</style>
|
||||
<g id="XMLID_6_">
|
||||
<path
|
||||
id="XMLID_11_"
|
||||
class="st0"
|
||||
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
||||
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
||||
v326.8H464.8z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="ytmcustom-pip"
|
||||
>
|
||||
Picture in picture
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,93 +0,0 @@
|
||||
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 roundToTwo = n => Math.round(n * 1e2) / 1e2;
|
||||
|
||||
const MIN_PLAYBACK_SPEED = 0.07;
|
||||
const MAX_PLAYBACK_SPEED = 16;
|
||||
|
||||
let playbackSpeed = 1;
|
||||
|
||||
const updatePlayBackSpeed = () => {
|
||||
$('video').playbackRate = playbackSpeed;
|
||||
|
||||
const playbackSpeedElement = $("#playback-speed-value");
|
||||
if (playbackSpeedElement) {
|
||||
playbackSpeedElement.innerHTML = playbackSpeed;
|
||||
}
|
||||
};
|
||||
|
||||
let menu;
|
||||
let observingSlider = false;
|
||||
|
||||
const observePopupContainer = () => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
}
|
||||
|
||||
if (menu && menu.lastElementChild.lastElementChild.innerText.startsWith('Stats') && !menu.contains(slider)) {
|
||||
menu.prepend(slider);
|
||||
if (!observingSlider) {
|
||||
setupSliderListener();
|
||||
observingSlider = 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', () => {
|
||||
observePopupContainer();
|
||||
observeVideo();
|
||||
setupWheelListener();
|
||||
}, { once: true, passive: true })
|
||||
};
|
||||