Compare commits
	
		
			512 Commits
		
	
	
		
			5.0.0
			...
			renovate/e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 39f113ec95 | ||
| 3c782c460c | |||
|  | feb9e2cb53 | ||
| be2c00f3b4 | |||
|  | f992ca3ed0 | ||
| c89fe5e573 | |||
|  | 255b59c87c | ||
| 2bbca7df24 | |||
| eecae61405 | |||
|  | 15dbadcc73 | ||
|  | 14b92cc5de | ||
| 959fc6631f | |||
| 91f70752c8 | |||
|  | 4f0dcee359 | ||
|  | a4a963c17b | ||
| 2ecacff0e1 | |||
|  | b426f0857e | ||
| 36c9887552 | |||
|  | 9efdbb8234 | ||
| 4d5b6a35e2 | |||
|  | 7923303e18 | ||
| cc95089201 | |||
| 9b010ea2bf | |||
| 3828591177 | |||
|  | 1e471df549 | ||
|  | 96acbd3559 | ||
|  | 224689b15b | ||
| 5f1fc8731a | |||
| 02ad1289c5 | |||
| b684d290bd | |||
|  | f35fd542b2 | ||
|  | 6831e55681 | ||
| 69ad060485 | |||
| 985831e961 | |||
|  | 218f7652cd | ||
|  | e46b34e076 | ||
|  | 5f49d0d3a9 | ||
| c1850436e1 | |||
|  | 7efbd52553 | ||
| 5ea91bbfde | |||
| 76b8adb038 | |||
| a746e53f7a | |||
|  | 149c0c975f | ||
|  | 98339444f7 | ||
|  | fd5bc505da | ||
| 5f94cfbf63 | |||
| 466c39134f | |||
| da8baebf29 | |||
|  | aa6a8b3417 | ||
| 4a1152175c | |||
|  | 721c3f5047 | ||
|  | d4b0552c14 | ||
|  | 44c8c01b8b | ||
| 8d4dd1ec9d | |||
| d012ecb04e | |||
|  | fa4b4da9a5 | ||
|  | 8f1a5dc344 | ||
| 604f34b31f | |||
| 3ff37f78e8 | |||
| 8eaca2221f | |||
| 8e9e9fed4f | |||
|  | d839dba58d | ||
| bbbd919655 | |||
| d4104b21e4 | |||
| 6a23873d3d | |||
| 8d6f08a7bb | |||
| 521b8b2170 | |||
|  | 99e4cb7f48 | ||
|  | 4cd130ee0d | ||
|  | a2a2c800b6 | ||
|  | 83210f9e1d | ||
| 5d40d2d736 | |||
| 405c2d7c9f | |||
|  | 9f860d2eec | ||
|  | 86487863f8 | ||
| 3312211df2 | |||
|  | a84bb9efde | ||
| 31c37b6c7f | |||
| be9724f0cb | |||
|  | 7277482c06 | ||
|  | 323b591f4f | ||
| c63b46ee06 | |||
|  | b4a8e155af | ||
| 2702d99aca | |||
|  | 7cf9ae5c36 | ||
| 3a1402b47a | |||
|  | 4366caa16a | ||
|  | 9389b4195e | ||
| 87d8bc07ad | |||
|  | 316bf054f5 | ||
| 0d4975ce62 | |||
|  | 912f064fed | ||
| bff95bcf10 | |||
| b59dce9c9b | |||
|  | f71fb60d38 | ||
|  | fab566e9da | ||
| d2e93e7062 | |||
|  | 52a78a6a01 | ||
| f878371fab | |||
| e81aaf1384 | |||
|  | 98b4068b62 | ||
|  | b7c1cee64d | ||
| 350b4434be | |||
| 702a16357f | |||
| c6619be068 | |||
| bf7cd70f20 | |||
| 03cb14e31e | |||
| 697298da38 | |||
| 5f5b3b11a8 | |||
| d474b7b78f | |||
| ef374ea283 | |||
|  | 96ff46737b | ||
|  | dee0044876 | ||
| 3b1f456d4c | |||
|  | f380839631 | ||
| fca5194873 | |||
|  | 92d1fc159d | ||
| b632c287b0 | |||
| 5a98b13e70 | |||
| 3543253f3a | |||
|  | fff8399c9b | ||
|  | 78b9b32dbd | ||
|  | c773e59432 | ||
| 535de65e17 | |||
| b80599eab2 | |||
| c161ef8f60 | |||
| 7ed160f266 | |||
| b23975227f | |||
| 314e69dd4b | |||
|  | 10251e2f30 | ||
|  | cf06655008 | ||
|  | d5804ed4da | ||
|  | 76500ca6b8 | ||
|  | f5c80e97c5 | ||
|  | 154698c084 | ||
| 9837e77768 | |||
| 340bd82fe6 | |||
| b765ab9f48 | |||
| 6562549897 | |||
| 9089bc1d4e | |||
| 5a3609676e | |||
| 76c3082d5c | |||
| 7f7a8b7f5b | |||
|  | 2d49504818 | ||
|  | 5f4f0c67d5 | ||
|  | 3a5f1f155a | ||
|  | 3c9328fb92 | ||
|  | f262c54be2 | ||
|  | cb6e3e8b18 | ||
|  | 7f9b8dcee4 | ||
|  | 99740f4335 | ||
| 985bc0f1d5 | |||
| 1f9b3a297a | |||
| 98ca73d17d | |||
| 396ed223d3 | |||
|  | b0458f4de5 | ||
|  | b875d091ec | ||
|  | 1a0b69d042 | ||
|  | e23a71a1a6 | ||
| 510ffa1509 | |||
|  | e17ef32cde | ||
| d4c3999d9c | |||
| b0695600b1 | |||
|  | 22ecc7ade7 | ||
| a5102ebd03 | |||
|  | d01f08508f | ||
| e87d8f6922 | |||
|  | c044476014 | ||
| 75df43864b | |||
|  | 174d0a38a1 | ||
|  | cf1aeefe67 | ||
| 280d927a03 | |||
|  | f62294ef20 | ||
| 237e354f80 | |||
|  | 3a5172cb23 | ||
|  | f8f12cca62 | ||
| 79bbab6a82 | |||
| 7376df7f30 | |||
| 7f8760c4e9 | |||
|  | 1cf5a01ad7 | ||
| 8fc49f500e | |||
|  | 49f5a01b58 | ||
| 48f4fe47ef | |||
| a819e1eb45 | |||
| 57e6342b5f | |||
| 58a543a3c8 | |||
| 1c915d99fe | |||
|  | 8994360415 | ||
| a5c0d9e6e8 | |||
| f8eb36f4c7 | |||
| 5ca90c25d3 | |||
| cf86969cfc | |||
| afd05ae88b | |||
| 72c25dfdc1 | |||
|  | 25b30edf31 | ||
| 35ed4807e3 | |||
| deff9524a8 | |||
| 34c7777eeb | |||
| 048e949f30 | |||
|  | 4a976bc58c | ||
| ff4c51234b | |||
|  | c4ee6b51b9 | ||
| 45fe336598 | |||
|  | fe9f50aaf5 | ||
| aaf7a1b662 | |||
| 755be0ee30 | |||
|  | 9d736b2bd9 | ||
| f4d4b1a1df | |||
|  | 0c27c815f5 | ||
|  | a3aa45a96b | ||
| ae699887b2 | |||
| a8c635932f | |||
| 66d0d004bf | |||
| d1e0321058 | |||
| 2ab5a556ab | |||
| baf719fc60 | |||
| 15c8f6a418 | |||
| f73521e2e5 | |||
|  | 0f5e00c4df | ||
|  | 974877ea4f | ||
|  | 6be5774001 | ||
|  | f96cf2e8da | ||
|  | f832bd2712 | ||
|  | cb8667bd41 | ||
| 91156e5936 | |||
|  | 57e9a2dcb8 | ||
|  | 21edcd6ad5 | ||
|  | 3dc42eceb0 | ||
|  | b1830f5684 | ||
|  | 1a0c15e17f | ||
| 6e59d59a1d | |||
|  | fab7497311 | ||
| fccbcc77ea | |||
|  | 041c19fb52 | ||
| e36c562afa | |||
| 1589aa5251 | |||
| 8c672dd1eb | |||
|  | 3769550f24 | ||
|  | 5a06b6c53f | ||
|  | 1d4ef66d27 | ||
| e02f07401b | |||
| 118f92e75a | |||
|  | 788d302ce8 | ||
|  | cc09f35b49 | ||
| ef13933c66 | |||
| 4f72e1b35d | |||
| 2c1c76d2d0 | |||
| e2a84e119a | |||
| 5b85e59fc3 | |||
| c3b772919a | |||
|  | 70f2f5c248 | ||
|  | ffcb563b35 | ||
|  | 2dd96dd48e | ||
| 65f3b251f4 | |||
| 45f8c13c5b | |||
| 9dc7267a4d | |||
| f5c56ae8c5 | |||
| 308b527469 | |||
| f4aa8e070e | |||
| 19c12b57de | |||
| b5713651de | |||
| 268027734c | |||
| f711ea9000 | |||
|  | 506f86f014 | ||
| 4e827e120f | |||
|  | 5b62154ebc | ||
| 15cc6bb6d4 | |||
| e0e9d99173 | |||
| daa797fc00 | |||
|  | 4e011a47d8 | ||
|  | 965d19318e | ||
| 9cd89c9f31 | |||
|  | c7b97a49c4 | ||
| c63e05357e | |||
| 42dac4a7f1 | |||
|  | 595895dbc1 | ||
| 1ff94e45a7 | |||
|  | 95776d1aab | ||
| 5669cb4c6a | |||
| 5946c47442 | |||
|  | 111238f6b2 | ||
|  | 65a4600c4d | ||
| dd6f81386f | |||
| 54316d31b5 | |||
| 28a9458dfc | |||
| 3641f07558 | |||
| ecbfa7e226 | |||
| 9321acc06e | |||
| 5b656ae229 | |||
| 0a8efc730d | |||
| b49bd925da | |||
| 1e6b9f7dcf | |||
| 51f7a96634 | |||
| 46074c5de5 | |||
| 2667f62674 | |||
| ac949dc211 | |||
| 40bc20582f | |||
|  | 180d9c97a7 | ||
| 5dc136138b | |||
| 1edc6a1b2b | |||
| 7c6831c771 | |||
| d47da91e93 | |||
| b481108af1 | |||
| 3740ce5a12 | |||
| a0f9faa753 | |||
| 5e3583534b | |||
|  | 5f8cf33249 | ||
|  | 2d94b4bf49 | ||
| 6e43cbb4d7 | |||
| f95f13b44a | |||
| f911564d8a | |||
| db8a2c2741 | |||
| 000853414e | |||
| 53603c4cad | |||
| 0b595f920f | |||
| 81143af3fa | |||
|  | 8d1ac3be3b | ||
|  | 666e602c02 | ||
|  | 04ec850005 | ||
| 943d9b5bd8 | |||
|  | 755816c2b8 | ||
| 25afd05ad7 | |||
| 6e5024742a | |||
| 417afaab85 | |||
| d225c0056b | |||
|  | a75b0336db | ||
|  | 29465ce13a | ||
|  | d333047269 | ||
|  | 712330f8f1 | ||
|  | 84fd35ce0e | ||
|  | 326038f262 | ||
| a6c1d35a60 | |||
|  | c09a4bc4a8 | ||
|  | 554cb12a01 | ||
| 2e31b5d913 | |||
| 2fd29c1b83 | |||
| b2f27a2afe | |||
| 8e11fd7f09 | |||
| 17b2818b70 | |||
| 4ef76c262e | |||
| fd0dae2762 | |||
| aa59bdc6dd | |||
| 5b5b6ecb38 | |||
| 5983145857 | |||
| 0c7d579951 | |||
|  | 235d916749 | ||
|  | 2d9f268866 | ||
| ae65e57e32 | |||
|  | 3f2d69f2f4 | ||
| 5ff2cc68d3 | |||
| daabe5bdbb | |||
|  | 456727c0e0 | ||
|  | ba50e0c095 | ||
|  | 312e90e8cb | ||
| 76769dfab3 | |||
|  | 565d32ae3d | ||
| 7be6f79040 | |||
|  | f894c82b12 | ||
| 21cb0ea79d | |||
| 49bc737485 | |||
| 10a4af8e90 | |||
| 317a685813 | |||
| 4da6d9feda | |||
| b11dbbd6d8 | |||
| 887a3d8a45 | |||
|  | 2e17b066a3 | ||
|  | e37b2f99cc | ||
| 1afd4d22a6 | |||
|  | 12a919df45 | ||
|  | 36a2367397 | ||
|  | 7c6d2df16a | ||
|  | 22383a9f45 | ||
|  | 623033ccd7 | ||
| 5bd28913da | |||
| 5240f1eeeb | |||
| 5e82c18d8a | |||
| 1d19857977 | |||
| 98f75418eb | |||
|  | 0d1a533f71 | ||
| 000bade444 | |||
| 69f2e26ca9 | |||
|  | 60d7da4652 | ||
|  | 6ef6bc0d40 | ||
| 89592bcf4d | |||
| f5185c6627 | |||
|  | a01fcd0791 | ||
|  | 69eef58f8e | ||
| ff060a31e5 | |||
| cbc7fc4a4e | |||
| 173c502143 | |||
|  | 5111af6a71 | ||
|  | f094139794 | ||
| b3d9b187c1 | |||
|  | 276632ea9d | ||
|  | c298b73773 | ||
| d14dbad9ea | |||
| 9e8f6a61f3 | |||
| 387a544b0f | |||
|  | 76fa8de96c | ||
|  | fc2d5d20ca | ||
|  | 9ba2d0fe26 | ||
| d94d42e2bd | |||
|  | 00db9f753e | ||
| 696a2730be | |||
| 6a5814c446 | |||
|  | 2326c6dd6a | ||
| 0dadce4596 | |||
| 9e2cbaed38 | |||
| 33070c157a | |||
| eb91b66ac6 | |||
| 68f76a9e63 | |||
| cbb22ba688 | |||
| 3df82b93db | |||
|  | 789ba83936 | ||
| a962029b0b | |||
|  | b3fffc78ec | ||
| d8e4a493b9 | |||
| 3d0b38361a | |||
| 1c7385fa50 | |||
| 510812f384 | |||
| 5144f67fbc | |||
| 81b81580c6 | |||
| 534547ce67 | |||
|  | 1610e3cc05 | ||
|  | e50e7de12e | ||
|  | 42be522b8e | ||
| 40d80e0872 | |||
| 239139e674 | |||
| dc87b20ab8 | |||
| c7b3921514 | |||
| 89f1ff4228 | |||
| a0c73596e4 | |||
| aa17d80450 | |||
| 5ea3972053 | |||
| 4b81378423 | |||
| c7931cf913 | |||
| c6dff0b0e5 | |||
| 644beea2a6 | |||
| df1c45982b | |||
| ec82aa8401 | |||
| 586f7b595b | |||
|  | de8a5a1b07 | ||
|  | 38c1f05c35 | ||
|  | ed6f04b6d4 | ||
|  | ffe8278c8c | ||
|  | e9434cc5ea | ||
|  | d81912db0c | ||
|  | c0110632e6 | ||
|  | 3571289d28 | ||
| 11cc209025 | |||
| f5ccbda7d9 | |||
| e8cf1783e8 | |||
| 8037a73e57 | |||
| 45e191dae0 | |||
| f147536b12 | |||
| d03bb58afa | |||
| a39fef8d49 | |||
| 41ca1d5a43 | |||
| 6969de8270 | |||
| ad05b767d8 | |||
|  | 6d873ce287 | ||
| 63d123f96a | |||
| f038412c50 | |||
| ff02287df7 | |||
|  | f221ded108 | ||
| 1440f70100 | |||
| 439333e15a | |||
| b9854e0595 | |||
| 8b56c28d75 | |||
| 700a14fe88 | |||
| 3c835077d5 | |||
| 194de286c8 | |||
| a7dee5c2c9 | |||
| 8036cbb919 | |||
| 90cf231c76 | |||
| 42a70534f2 | |||
| b07865d98b | |||
| cc26bfa080 | |||
| 822bdf401e | |||
| a169c57a52 | |||
| 60eb1bbef9 | |||
| 1761c8dd40 | |||
| 62244f432a | |||
| a408a6a8cc | |||
| 6e5a2c626c | |||
| 4350ab9bd9 | |||
| 77a853e980 | |||
| 757f8511c0 | |||
| 2ef457be2c | |||
| 2c5d2b9530 | |||
| 757bd0da80 | |||
| d823f07ed8 | |||
| 32ade76ae3 | |||
| a1c02dfed3 | |||
| 21d6e57cb9 | |||
| 53e4711c39 | |||
| e8509d42e7 | |||
| 46d030cf8e | |||
| 412f1ae3e3 | |||
| 68f0c89ec2 | |||
| 8d44ff8afb | |||
| bccc979f43 | |||
| 6849952c41 | |||
| 07be74af9f | |||
| fc6adc25ca | |||
| 4498e8a73e | |||
| 3d2a9c3992 | |||
| af6bfaf55e | |||
|  | 8bac90e0f1 | ||
|  | 887c75f61a | ||
|  | cde7408cc4 | ||
|  | 0120391418 | 
							
								
								
									
										16
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| kind: pipeline | ||||
| type: docker | ||||
| name: default | ||||
|  | ||||
| steps: | ||||
|   - name: install | ||||
|     image: node:22.17.0 | ||||
|     commands: | ||||
|       - npm install | ||||
|  | ||||
|   - name: build_with_linux | ||||
|     image: node:22.17.0 | ||||
|     commands: | ||||
|       - apt-get update && apt-get upgrade -y | ||||
|       - apt-get install -y libarchive-tools rpm | ||||
|       - npm run build-unpacked | ||||
| @@ -10,6 +10,10 @@ insert_final_newline = true | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
|  | ||||
| [**.ts] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
|  | ||||
| [**.json] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| @@ -50,4 +54,4 @@ trim_trailing_whitespace = ignore | ||||
| charset = ignore | ||||
|  | ||||
| [{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**] | ||||
| insert_final_newline = false | ||||
| insert_final_newline = false | ||||
|   | ||||
							
								
								
									
										16
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| { | ||||
|   "root": true, | ||||
|   "env": { | ||||
|     "node": true, | ||||
|     "browser": true | ||||
|   }, | ||||
|   "parser": "@typescript-eslint/parser", | ||||
|   "plugins": [ | ||||
|     "@typescript-eslint" | ||||
|   ], | ||||
|   "extends": [ | ||||
|     "eslint:recommended", | ||||
|     "plugin:@typescript-eslint/eslint-recommended", | ||||
|     "plugin:@typescript-eslint/recommended" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										47
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5,6 +5,11 @@ on: | ||||
|     branches-ignore: | ||||
|       - master | ||||
|       - develop | ||||
|   pull_request: | ||||
|     branches-ignore: | ||||
|       - master | ||||
|       - develop | ||||
|   workflow_dispatch: | ||||
| jobs: | ||||
|   build_on_linux: | ||||
|     runs-on: ubuntu-latest | ||||
| @@ -16,26 +21,30 @@ jobs: | ||||
|       - uses: actions/checkout@master | ||||
|       - uses: actions/setup-node@master | ||||
|         with: | ||||
|           node-version: 19 | ||||
|           node-version: 22.12.0 | ||||
|       - run: npm install | ||||
|       - run: npm run build | ||||
|       # - uses: actions/upload-artifact@master | ||||
|       #   with: | ||||
|       #     name: linux-builds | ||||
|       #     path: dist/ | ||||
|  | ||||
|   build_on_mac: | ||||
|     runs-on: macos-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@master | ||||
|       - uses: actions/setup-node@master | ||||
|         with: | ||||
|           node-version: 19 | ||||
|       - run: npm install | ||||
|       - run: npm run build | ||||
|   # build_on_mac: | ||||
|   #   runs-on: macos-latest | ||||
|   #   steps: | ||||
|   #     - uses: actions/checkout@master | ||||
|   #     - uses: actions/setup-node@master | ||||
|   #       with: | ||||
|   #         node-version: 22.4 | ||||
|   #     - run: npm install | ||||
|   #     - run: npm run build | ||||
|  | ||||
|   build_on_win: | ||||
|     runs-on: windows-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@master | ||||
|       - uses: actions/setup-node@master | ||||
|         with: | ||||
|           node-version: 19 | ||||
|       - run: npm install | ||||
|       - run: npm run build | ||||
|   # build_on_win: | ||||
|   #   runs-on: windows-latest | ||||
|   #   steps: | ||||
|   #     - uses: actions/checkout@master | ||||
|   #     - uses: actions/setup-node@master | ||||
|   #       with: | ||||
|   #         node-version: 22.4 | ||||
|   #     - run: npm install | ||||
|   #     - run: npm run build | ||||
|   | ||||
							
								
								
									
										59
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5,6 +5,11 @@ on: | ||||
|     branches: | ||||
|       - master | ||||
|       - develop | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|   workflow_dispatch: | ||||
|  | ||||
| jobs: | ||||
|   build_on_linux: | ||||
|     runs-on: ubuntu-latest | ||||
| @@ -16,7 +21,7 @@ jobs: | ||||
|       - uses: actions/checkout@master | ||||
|       - uses: actions/setup-node@master | ||||
|         with: | ||||
|           node-version: 16 | ||||
|           node-version: 22.12.0 | ||||
|       - run: npm install | ||||
|       - run: npm run build | ||||
|       - uses: actions/upload-artifact@master | ||||
| @@ -24,30 +29,30 @@ jobs: | ||||
|           name: linux-builds | ||||
|           path: dist/ | ||||
|  | ||||
|   build_on_mac: | ||||
|     runs-on: macOS-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@master | ||||
|       - uses: actions/setup-node@master | ||||
|         with: | ||||
|           node-version: 16 | ||||
|       - run: npm install | ||||
|       - run: npm run build | ||||
|       - uses: actions/upload-artifact@master | ||||
|         with: | ||||
|           name: mac-builds | ||||
|           path: ./dist/ | ||||
|   # build_on_mac: | ||||
|   #   runs-on: macos-latest | ||||
|   #   steps: | ||||
|   #     - uses: actions/checkout@master | ||||
|   #     - uses: actions/setup-node@master | ||||
|   #       with: | ||||
|   #         node-version: 22.4 | ||||
|   #     - run: npm install | ||||
|   #     - run: npm run build | ||||
|   #     - uses: actions/upload-artifact@master | ||||
|   #       with: | ||||
|   #         name: mac-builds | ||||
|   #         path: ./dist/ | ||||
|  | ||||
|   build_on_win: | ||||
|     runs-on: windows-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@master | ||||
|       - uses: actions/setup-node@master | ||||
|         with: | ||||
|           node-version: 16 | ||||
|       - run: npm install | ||||
|       - run: npm run build | ||||
|       - uses: actions/upload-artifact@master | ||||
|         with: | ||||
|           name: windows-builds | ||||
|           path: dist/ | ||||
|   # build_on_win: | ||||
|   #   runs-on: windows-latest | ||||
|   #   steps: | ||||
|   #     - uses: actions/checkout@master | ||||
|   #     - uses: actions/setup-node@master | ||||
|   #       with: | ||||
|   #         node-version: 22.4 | ||||
|   #     - run: npm install | ||||
|   #     - run: npm run build | ||||
|   #     - uses: actions/upload-artifact@master | ||||
|   #       with: | ||||
|   #         name: windows-builds | ||||
|   #         path: dist/ | ||||
|   | ||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -12,3 +12,8 @@ build/linux/arch/* | ||||
|  | ||||
| # JetBrains IDE configuration | ||||
| .idea | ||||
| ts-dist/** | ||||
| ts-dist | ||||
| themes | ||||
| !src/themes | ||||
| .sass-cache | ||||
|   | ||||
							
								
								
									
										16
									
								
								.stylelintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| { | ||||
|   "plugins": [ | ||||
|     "stylelint-prettier" | ||||
|   ], | ||||
|   "extends": [ | ||||
|     "stylelint-config-standard-scss" | ||||
|   ], | ||||
|   "ignoreFiles": [ | ||||
|     "src/themes/**.scss" | ||||
|   ], | ||||
|   "rules": { | ||||
|     "prettier/prettier": true, | ||||
|     "scss/at-extend-no-missing-placeholder": null, | ||||
|     "no-descending-specificity": null | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								.vscode/http/settings/skipped-artists/addArtists.http
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| POST /settings/skipped-artists HTTP/1.1 | ||||
| Host: localhost:47836 | ||||
| Content-Type: application/json | ||||
|  | ||||
| ["abc", "def"] | ||||
							
								
								
									
										2
									
								
								.vscode/http/settings/skipped-artists/addCurrent.http
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| POST /settings/skipped-artists/current HTTP/1.1 | ||||
| Host: localhost:47836 | ||||
							
								
								
									
										5
									
								
								.vscode/http/settings/skipped-artists/removeArtists.http
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| POST /settings/skipped-artists/delete HTTP/1.1 | ||||
| Host: localhost:47836 | ||||
| Content-Type: application/json | ||||
|  | ||||
| ["abc", "def"] | ||||
							
								
								
									
										2
									
								
								.vscode/http/settings/skipped-artists/removeCurrent.http
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| DELETE /settings/skipped-artists/current HTTP/1.1 | ||||
| Host: localhost:47836 | ||||
							
								
								
									
										27
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,3 +1,28 @@ | ||||
| { | ||||
|   "cSpell.words": ["hifi", "rescrobbler", "widevine"] | ||||
|   "cSpell.words": [ | ||||
|     "Brainz", | ||||
|     "Castlabs", | ||||
|     "Fi's", | ||||
|     "flac", | ||||
|     "Flatpak", | ||||
|     "geqnfr", | ||||
|     "hifi", | ||||
|     "libnotify", | ||||
|     "listenbrainz", | ||||
|     "playpause", | ||||
|     "prs", | ||||
|     "rescrobbler", | ||||
|     "scrobble", | ||||
|     "scrobbling", | ||||
|     "trackid", | ||||
|     "tracklist", | ||||
|     "widevine", | ||||
|     "wvcus", | ||||
|     "xesam", | ||||
|     "xhayper" | ||||
|   ], | ||||
|   "sonarlint.connectedMode.project": { | ||||
|     "connectionId": "public-sonarcloud", | ||||
|     "projectKey": "Mastermindzh_tidal-hifi" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										233
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -4,9 +4,236 @@ All notable changes to this project will be documented in this file. | ||||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
|  | ||||
| ## [5.20.1] | ||||
|  | ||||
| - Updated electron to 37.2.5 | ||||
|  | ||||
| ## [5.20.0] | ||||
|  | ||||
| - Removes the `--enable-features=UseOzonePlatform` flag, as the Ozone platform has been the default on Linux since Electron 28 and this flag is no longer necessary. | ||||
| - Adds the `--enable-wayland-ime` flag to enable Input Method Editor (IME) support in Wayland environments, improving the input experience for CJK and other users. | ||||
| - Updated various dependencies | ||||
| - Updated Electron to 37, potentially fixing [#580](https://github.com/Mastermindzh/tidal-hifi/issues/580) | ||||
|  | ||||
| ## [5.19.0] | ||||
|  | ||||
| - Fixed the issue where media updates would cease to work after album names can't be found | ||||
|   - Will simply report an empty string when it can't find the album | ||||
| - Updated various dependencies | ||||
|  | ||||
| ## [5.18.2] | ||||
|  | ||||
| - Reverted to sass 1.79.4 to fix `Nix` builds | ||||
| - Changed electron-builder.base.yml to now generate the correct .desktop entries again | ||||
|   - Should fix flatpak build | ||||
|  | ||||
| ## [5.18.1] | ||||
|  | ||||
| - Fixed the login bug | ||||
| - Upgraded electron to 35.1.1 | ||||
| - Added Widevine/CDM info to startup | ||||
| - delayed remote electron initializer | ||||
|  | ||||
| ## [5.18.0] | ||||
|  | ||||
| - [Dianoga](https://github.com/Dianoga) fixed the duration selector, restoring mpris & partial API data. | ||||
|   - PR: #554 | ||||
| - Added `xesam:url` property to mpris metadata fixes [#506](https://github.com/Mastermindzh/tidal-hifi/issues/506) | ||||
|  | ||||
| ## [5.17.0] | ||||
|  | ||||
| - Added an option to disable the dynamic title and set it to a static one, [#491](https://github.com/Mastermindzh/tidal-hifi/pull/491) | ||||
| - Discord integration now says "Listening to" instead of "playing" [#488](https://github.com/Mastermindzh/tidal-hifi/pull/488) && [#454](https://github.com/Mastermindzh/tidal-hifi/pull/454) | ||||
| - Fixed several element names in the dom scraper | ||||
| - Removed the Songwhip (they shut down) integration and replaced it with TIDAL's universal link system | ||||
|  | ||||
| ## [5.16.0] | ||||
|  | ||||
| - Fix issue #449 Discord RPC stuck on "Browsing Tidal". | ||||
| - Fix issue #448 Add option to disable the discord rpc idle text | ||||
| - Notifications are now send at the end of the update process, allowing other events to happen sooner. | ||||
|  | ||||
| ## [5.15.0] | ||||
|  | ||||
| - Added all missing swagger/openApi info with the help of [Times-Z](https://github.com/Times-Z) | ||||
| - Updated most dependency versions | ||||
|   - This includes Electron 31! | ||||
|  | ||||
| - Added a channel selector so we can now use Tidal's staging environment directly from the app | ||||
|   - implements [#437](https://github.com/Mastermindzh/tidal-hifi/issues/437) | ||||
|  | ||||
| ## [5.14.1] | ||||
|  | ||||
| - Fixed `getAlbumName` not finding album name whilst on queue page | ||||
| - Added all mediaInfo to mpris interface using the `custom:` prefix | ||||
|  | ||||
| ## [5.14] | ||||
|  | ||||
| - Simplified `MediaInfo` & `Options` types | ||||
| - Added `playingFrom` information to the info API | ||||
|   - also changed the way we update Album info since Playing From now shows the correct Album. | ||||
| - API now allows you to set the `hostname` so you can control who can interact with the API. | ||||
| - Reworked swagger generation hotfix to properly generate `swagger.json` during the compile step | ||||
|   - Might switch to tsoa in the future, idk yet. | ||||
| - Added [Tidal Magazine](https://tidal.com/magazine/) integration (in the menubar or use `Ctrl + M`) | ||||
|  | ||||
| ## [5.13.1] | ||||
|  | ||||
| - removed Swagger generation step in favor of pre-generated file. | ||||
|   - This also fixes the API issue [#409](https://github.com/Mastermindzh/tidal-hifi/issues/409) | ||||
|   - This also stops TIDAL-hifi from scanning your entire home directory... the glob was very broad apparently. | ||||
|  | ||||
| ## [5.13.0] | ||||
|  | ||||
| - Fixed [#403](https://github.com/Mastermindzh/tidal-hifi/issues/403) "cannot read shuffle of undefined" error | ||||
| - Added an API to add & delete entries from the skippedArtists list in the settings. fixes [#405](https://github.com/Mastermindzh/tidal-hifi/issues/405) | ||||
|   - `GET /settings/skipped-artists` -> get list of skipped artists | ||||
|   - `POST /settings/skipped-artists` -> add to the list of skipped artists | ||||
|   - `POST /settings/skipped-artists/delete` -> delete from the list of skipped artists | ||||
|   - `POST /settings/skipped-artists/current` -> skip the current artist | ||||
|   - `DELETE /settings/skipped-artists/current` -> delete the current artist from the skip list | ||||
|   - Added Swagger documentation to the new endpoints: | ||||
|      | ||||
| - CORS support added by [Mjokfox](https://github.com/Mjokfox) | ||||
|  | ||||
| ## [5.12.0] | ||||
|  | ||||
| - Added Shuffle and Repeat state to API response - By [ThatGravyBoat](https://github.com/ThatGravyBoat) | ||||
|  | ||||
| ## [5.11.0] | ||||
|  | ||||
| - Re-implemented the API, added support for duration/current in seconds & shuffle+repeat | ||||
|   - made the original API "legacy" (still works the same) | ||||
|   - Now using the correct HTTP verb for all new endpoints | ||||
| - Implemented TIDAL's universal links. All links are now universal. | ||||
| - Custom `tidal://` protocol fixed - By [TheRockYT](https://github.com/TheRockYT) | ||||
| - Global media shortcuts removed since TIDAL includes them by default - By [TheRockYT](https://github.com/TheRockYT) | ||||
|  | ||||
| - Fixes | ||||
|   - [#390](https://github.com/Mastermindzh/tidal-hifi/issues/390) | ||||
|   - [#376](https://github.com/Mastermindzh/tidal-hifi/issues/376) | ||||
|   - [#383](https://github.com/Mastermindzh/tidal-hifi/issues/383) | ||||
|   - [#393](https://github.com/Mastermindzh/tidal-hifi/issues/393) | ||||
|  | ||||
| ## [5.10.0] | ||||
|  | ||||
| - TIDAL will now close the previous notification if a new one is sent whilst the old is still visible. [#364](https://github.com/Mastermindzh/tidal-hifi/pull/364) | ||||
| - Updated developer documentation to get started in README [#365](https://github.com/Mastermindzh/tidal-hifi/pull/365) | ||||
| - Links in the about window now open in the user's default browser. fixes [#360](https://github.com/Mastermindzh/tidal-hifi/issues/360) | ||||
| - Refactored "nowPlaying" code to always display the current state, even when the built-in UI is updated. | ||||
|   - fixes [#351](https://github.com/Mastermindzh/tidal-hifi/issues/351) | ||||
|   - fixes [#356](https://github.com/Mastermindzh/tidal-hifi/issues/356) | ||||
|   - fixes [#370](https://github.com/Mastermindzh/tidal-hifi/issues/370) | ||||
| - Reverted to using old icon syntax with icons in the build directory. fixes [#350](https://github.com/Mastermindzh/tidal-hifi/issues/350) | ||||
| - Enabled wayland platform flags by default when launching through .desktop file | ||||
|   - fixes [#273](https://github.com/Mastermindzh/tidal-hifi/issues/273) | ||||
|   - fixes [#347](https://github.com/Mastermindzh/tidal-hifi/issues/347) | ||||
|  | ||||
| ## [5.9.0] | ||||
|  | ||||
| - More Discord options: | ||||
|   - Added the ability to hide the current song from the discord activity and display a custom text instead | ||||
|   - Added the ability to customize the text that is shown when no song is playing | ||||
|   - Discord now reacts to pausing/unpausing events | ||||
| - Refactored media info updates so it only updates the required info, fixes #342, #306 | ||||
| - Added 5.9.0 logs/versions/migrations | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fixed chromium mediaSession instance showing up. fixes #338 #198 | ||||
| - Set a new icon, should fix #302 | ||||
| - Made sure settingsWindow exists before operating on it. fixes #344 | ||||
|  | ||||
| ## [5.8.0] | ||||
|  | ||||
| - Updated Electron to 28.1.1 (fixes [325](https://github.com/Mastermindzh/tidal-hifi/issues/325)) | ||||
| - Updated dependencies to latest | ||||
|   - added theme files to stylelint ignore | ||||
|   - fixed other stylelint errors | ||||
|  | ||||
| - Added functionality to favorite a song (fixes [#323](https://github.com/Mastermindzh/tidal-hifi/issues/323)) | ||||
|   - Added a hotkey to favorite ("Add to collection") songs: Control+a | ||||
|   - Added the "favorite" field in the `mediaInfo` and the API `/current` endpoint | ||||
|   - Added an endpoint to toggle favoriting a song: `http://localhost:47836/favorite/toggle` | ||||
|  | ||||
| - Fixed wrong "end time stamp" for currently playing song (fixes [#282](https://github.com/Mastermindzh/tidal-hifi/issues/282)) | ||||
|   - Affected the API + all integrations | ||||
|   - As requested we also added toggle to sync the timestamps to Discord (default = true) | ||||
|  | ||||
| ## [5.7.1] | ||||
|  | ||||
| - Fixed mpris not being set up correctly due to capitalization of the instance name. | ||||
|  | ||||
| ## [5.7.0] | ||||
|  | ||||
| - Renamed app to TIDAL Hi-Fi. | ||||
| - Made sure all windows run with the same web preferences set (compared to main app). | ||||
|   - Fixes the last.fm bug. | ||||
| - Added settings to customize the Discord rich presence information | ||||
|   - Discord settings are now also collapsible like the ListenBrainz ones are | ||||
| - Restyled settings menu to include version number and useful links on the about page | ||||
|      | ||||
| - The ListenBrainz integration has been extended with a configurable (5 seconds by default) delay in song reporting so that it doesn't spam the API when you are cycling through songs. | ||||
| - Custom CSS now also applies to settings window | ||||
|      | ||||
|  | ||||
| ## [5.6.0] | ||||
|  | ||||
| - Added support for Wayland (on by default) fixes [#262](https://github.com/Mastermindzh/tidal-hifi/issues/262) and [#157](https://github.com/Mastermindzh/tidal-hifi/issues/157) | ||||
| - Made it clear in the readme that this TIDAL Hi-Fi client supports High & Max audio settings. fixes [#261](https://github.com/Mastermindzh/tidal-hifi/issues/261) | ||||
| - Added app suspension inhibitors when music is playing. fixes [#257](https://github.com/Mastermindzh/tidal-hifi/issues/257) | ||||
| - Fixed bug with theme files from user directory trying to load: "an error occurred reading the theme file" | ||||
| - Fixed: config flags not being set correctly | ||||
| - [DEV]: | ||||
|   - Logger is now static and will automatically call either ipcRenderer or ipcMain | ||||
|  | ||||
| ## 5.5.0 | ||||
|  | ||||
| - ListenBrainz integration added (thanks @Mar0xy) | ||||
|  | ||||
| ## 5.4.0 | ||||
|  | ||||
| - Removed Windows builds (from publishes) as they don't work anymore. | ||||
| - Added [Songwhip](https://songwhip.com/) integration | ||||
| - Fixed bug with several hotkeys not working due to Tidal's HTML/css changes | ||||
| - [DEV]: | ||||
|   - added a logger to log into STDout | ||||
|   - added "watchStart" which will automatically restart electron when it detects a source code change | ||||
|   - added "listen.tidal.com-parsing-scripts" folder with a script to verify whether all elements (in the main preload.ts) are present on the page | ||||
|  | ||||
| ## 5.3.0 | ||||
|  | ||||
| - SPKChaosPhoenix updated the beautiful Tokyo Night theme: | ||||
|  | ||||
|  | ||||
|  | ||||
| ## 5.2.0 | ||||
|  | ||||
| - moved from Javascript to Typescript for all files | ||||
|  | ||||
|   - use `npm run watch` to watch for changes & recompile typescript and sass files | ||||
|  | ||||
| - Added support for theming the application | ||||
| - Added drone build file use `drone exec` or drone.ci to build it | ||||
|  | ||||
| ## 5.1.0 | ||||
|  | ||||
| ### New features | ||||
|  | ||||
| - Added proper updates through the MediaSession API | ||||
| - You can now add custom CSS in the "advanced" settings tab | ||||
| - You can now configure the updateFrequency in the settings window | ||||
|   - Default value is set to 500 and will overwrite the hardcoded value of 100 | ||||
|  | ||||
| ### Fixes | ||||
|  | ||||
| - Any songs **including** an artist listed in the `skipped artists` setting will now be skipped even if the song is a collaboration. | ||||
| - Linux desktop icons have been fixed. See [#222](https://github.com/Mastermindzh/tidal-hifi/pull/222) for details. | ||||
|  | ||||
| ## 5.0.0 | ||||
|  | ||||
| - Replaced "muting artists" with a full implementation of an Adblock mechanism | ||||
|  | ||||
|   > Disabled audio & visual ads, unlocked lyrics, suggested track, track info, unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495)) | ||||
|  | ||||
| - @thanasistrisp updated Electron to 24.1.2 and fixed the tray bug :) | ||||
| @@ -35,7 +262,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||
| - New settings window by BlueManCZ | ||||
| - Fixed the desktop files in electron-builder | ||||
|   - icon is set to new static path based on Arch/Debian | ||||
|   - Name has changed to Tidal-Hifi | ||||
|   - Name has changed to TIDAL Hi-Fi | ||||
|  | ||||
| ## 4.1.2 | ||||
|  | ||||
| @@ -83,7 +310,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||
|  | ||||
| - Updated to Electron 15 | ||||
| - Fixed the develop "build-unpacked" command | ||||
| - Added setting to disable multiple tidal-hifi windows (defaults to true) | ||||
| - Added setting to disable multiple TIDAL Hi-Fi windows (defaults to true) | ||||
| - Added setting to disable HardwareMediaKeyHandling (defaults to false) | ||||
|  | ||||
| ## 2.8.2 | ||||
| @@ -121,7 +348,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||
|  | ||||
| ## 2.5.0 | ||||
|  | ||||
| - Notify-send now correctly shows "Tidal HiFi" as the program name | ||||
| - Notify-send now correctly shows "Tidal Hi-Fi" as the program name | ||||
| - Updated dependencies (including electron itself) | ||||
|  | ||||
| ### known issues | ||||
|   | ||||
							
								
								
									
										173
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,42 +1,86 @@ | ||||
| # Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/> | ||||
| # TIDAL Hi-Fi (Max quality)<img src = "./build/icon.png" height="40" align="right"/> | ||||
|  | ||||
|  | ||||
|  [](https://github.com/Mastermindzh/tidal-hifi/actions) [](https://ci.mastermindzh.tech/Mastermindzh/tidal-hifi) [](https://discord.gg/yhNwf4v4He) | ||||
|  | ||||
| The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine. | ||||
| The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with Hi-Fi (High & Max) support thanks to widevine. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Table of Contents | ||||
|  | ||||
| <!-- toc --> | ||||
|  | ||||
| - [Installation](#installation) | ||||
|   - [Dependencies](#dependencies) | ||||
|   - [Using releases](#using-releases) | ||||
|   - [Snap](#snap) | ||||
|   - [Arch Linux](#arch-linux) | ||||
|   - [Flatpak](#flatpak) | ||||
|   - [Nix](#nix) | ||||
|   - [Using source](#using-source) | ||||
| - [Features](#features) | ||||
| - [Integrations](#integrations) | ||||
| - [TIDAL Hi-Fi (Max quality)](#tidal-hi-fi-max-quality) | ||||
|   - [Table of Contents](#table-of-contents) | ||||
|   - [Features](#features) | ||||
|   - [Contributions](#contributions) | ||||
|   - [Why did I create TIDAL Hi-Fi?](#why-did-i-create-tidal-hi-fi) | ||||
|     - [Why not extend existing projects?](#why-not-extend-existing-projects) | ||||
|   - [Installation](#installation) | ||||
|     - [Dependencies](#dependencies) | ||||
|     - [Using releases](#using-releases) | ||||
|     - [Snap](#snap) | ||||
|     - [Arch Linux](#arch-linux) | ||||
|     - [Flatpak](#flatpak) | ||||
|     - [Nix](#nix) | ||||
|     - [Using source](#using-source) | ||||
|   - [Integrations](#integrations) | ||||
|   - [Known bugs](#known-bugs) | ||||
|     - [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround) | ||||
| - [Why](#why) | ||||
| - [Why not extend existing projects?](#why-not-extend-existing-projects) | ||||
| - [Special thanks to](#special-thanks-to) | ||||
| - [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont) | ||||
| - [Images](#images) | ||||
|   - [Settings window](#settings-window) | ||||
|   - [User setups](#user-setups) | ||||
|     - [DRM not working on Windows (error S6007)](#drm-not-working-on-windows-error-s6007) | ||||
|   - [Special thanks to](#special-thanks-to) | ||||
|   - [Donations](#donations) | ||||
|   - [Images](#images) | ||||
|     - [Settings window](#settings-window) | ||||
|     - [User setups](#user-setups) | ||||
|  | ||||
| <!-- tocstop --> | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - HiFi playback (High & Max settings) | ||||
| - Notifications | ||||
| - Custom [theming](./docs/theming.md) | ||||
| - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) | ||||
| - Better icons thanks to [Papirus-icon-theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/) | ||||
| - [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`) | ||||
| - API for status, playback and settings (see the [/docs](http://localhost:47836/docs/) route) | ||||
| - Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495)) | ||||
| - AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847)) | ||||
| - Custom [integrations](#integrations) | ||||
|   - [ListenBrainz](https://listenbrainz.org/?redirect=false) integration | ||||
|   - Discord RPC integration (showing "now listening", "Browsing", etc) | ||||
|     - Flatpak version only works if both Discord and Tidal-HiFi are flatpaks | ||||
|   - MPRIS integration | ||||
| - UI + Json config (`~/.config/tidal-hifi/`, or `~/.var/app/com.mastermindzh.tidal-hifi/` for Flatpak) | ||||
|  | ||||
| ## Contributions | ||||
|  | ||||
| To contribute you can use the standard GitHub features (issues, prs, etc.) or join the discord server to talk with like-minded individuals. | ||||
|  | ||||
| -  [Join the Discord server](https://discord.gg/yhNwf4v4He) | ||||
|  | ||||
| ## Why did I create TIDAL Hi-Fi? | ||||
|  | ||||
| I moved from Spotify over to Tidal and found Linux support to be lacking. | ||||
| When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it. | ||||
| I made this app to support the highest quality audio available on the Linux platform. It used to be "hifi" but now is ["High & Max"](https://tidal.com/sound-quality). | ||||
|  | ||||
| ### Why not extend existing projects? | ||||
|  | ||||
| Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons: | ||||
|  | ||||
| - Lack of maintainers/developers. (no hotfixes, no issues being handled etc) | ||||
| - Most are simple web wrappers, not my cup of tea. | ||||
| - Some are DE-oriented. I want this to work on WM's too. | ||||
| - None have Widevine working at the moment | ||||
|  | ||||
| Sometimes it's just easier to start over, cover my own needs and after that making it available to the public :) | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| ### Dependencies | ||||
|  | ||||
| Note that you **need** a notification library such as [libnotify](https://github.com/GNOME/libnotify) or [dunst](https://github.com/dunst-project/dunst) in order for the software to work properly. | ||||
| Note that you **need** a notification library such as [libnotify](https://github.com/GNOME/libnotify) or [dunst](https://github.com/dunst-project/dunst) for the software to work properly. | ||||
|  | ||||
| ### Using releases | ||||
|  | ||||
| @@ -48,22 +92,22 @@ To install with `snap` you need to download the pre-packaged snap-package from t | ||||
|  | ||||
| 1. Download | ||||
|  | ||||
| ```sh | ||||
| wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap | ||||
| ``` | ||||
|    ```sh | ||||
|    wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap | ||||
|    ``` | ||||
|  | ||||
| 2. Install | ||||
|  | ||||
| ```sh | ||||
| snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap | ||||
| ``` | ||||
|    ```sh | ||||
|    snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap | ||||
|    ``` | ||||
|  | ||||
| ### Arch Linux | ||||
|  | ||||
| Arch Linux users can use the AUR to install tidal-hifi: | ||||
| Arch Linux users can use the AUR to install TIDAL Hi-Fi: | ||||
|  | ||||
| ```sh | ||||
| trizen tidal-hifi-bin | ||||
| trizen tidal-hifi-git | ||||
| ``` | ||||
|  | ||||
| ### Flatpak | ||||
| @@ -86,78 +130,49 @@ nix-env -iA nixpkgs.tidal-hifi | ||||
|  | ||||
| To install and work with the code on this project follow these steps: | ||||
|  | ||||
| - git clone [https://github.com/Mastermindzh/tidal-hifi.git](https://github.com/Mastermindzh/tidal-hifi.git) | ||||
| - cd tidal-hifi | ||||
| - npm install | ||||
| - npm start | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - HiFi playback | ||||
| - Notifications | ||||
| - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) | ||||
| - API for status and playback | ||||
| - Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495)) | ||||
| - Custom [integrations](#integrations) | ||||
| - [Settings feature](./docs/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`) | ||||
| - AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847)) | ||||
| - `git clone https://github.com/Mastermindzh/tidal-hifi.git` | ||||
| - `cd tidal-hifi` | ||||
| - `npm install` | ||||
| - `npm run watch` to watch for auto-reload of Typescript/SCSS changes. | ||||
|   - `npm run compile` can be used to trigger it once | ||||
| - `npm watchStart` to auto watch for any updates files and reload Tidal Hi-Fi | ||||
|   - `npm start` can be used to run Tidal Hi-Fi manually once | ||||
|  | ||||
| ## Integrations | ||||
|  | ||||
| Tidal-hifi comes with several integrations out of the box. | ||||
| TIDAL Hi-Fi comes with several integrations out of the box. | ||||
| You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| It currently includes: | ||||
|  | ||||
| - MPRIS - MPRIS media player controls/status | ||||
| - Discord - Shows what you're listening to on Discord. | ||||
|  | ||||
| Not included: | ||||
| Integrations with other projects that are not included natively: | ||||
|  | ||||
| - [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit) | ||||
| - [neptune](https://github.com/uwu/neptune) third party plugins & theming | ||||
|  | ||||
| ### Known bugs | ||||
| ## Known bugs | ||||
|  | ||||
| #### last.fm doesn't work out of the box. Use rescrobbler as a workaround | ||||
| ### DRM not working on Windows (error S6007) | ||||
|  | ||||
| The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4). | ||||
| However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled). | ||||
| For now that will be the default workaround. | ||||
| Most Windows users run into DRM issues when trying to use TIDAL Hi-Fi. | ||||
| Nothing I can do about that I'm afraid... Tidal is working on removing/changing DRM so when they finish with that we can give it another shot. | ||||
|  | ||||
| ## Why | ||||
|  | ||||
| I moved from Spotify over to Tidal and found Linux support to be lacking. | ||||
|  | ||||
| When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it. | ||||
|  | ||||
| ## Why not extend existing projects? | ||||
|  | ||||
| Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons: | ||||
|  | ||||
| - Lack of a maintainers/developers. (no hotfixes, no issues being handled etc) | ||||
| - Most are simple web wrappers, not my cup of tea. | ||||
| - Some are DE oriented. I want this to work on WM's too. | ||||
| - None have widevine working at the moment | ||||
|  | ||||
| Sometimes it's just easier to start over, cover my own needs and then making it available to the public :) | ||||
| Until then you'll have to use the official app unfortunately. | ||||
|  | ||||
| ## Special thanks to | ||||
|  | ||||
| - [Castlabs](https://castlabs.com/) | ||||
|   For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID) | ||||
|  | ||||
| ## Buy me a coffee? Please don't | ||||
| ## Donations | ||||
|  | ||||
| Instead spend some money on a charity I care for: [kwf.nl](https://www.kwf.nl/donatie/donation). | ||||
| Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429) | ||||
| You can find my Github sponsorship page at: [https://github.com/sponsors/Mastermindzh](https://github.com/sponsors/Mastermindzh) | ||||
|  | ||||
| ## Images | ||||
|  | ||||
| ### Settings window | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### User setups | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| # Security Policy | ||||
|  | ||||
| ## Supported Versions | ||||
|  | ||||
| Only the very latest 😄. | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
| If you find a vulnerability just add it as an issue. | ||||
| If there's an especially bad vulnerability that you don't want to make public just send me a private message (email, discord, wherever). | ||||
|  | ||||
| Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon.png
									
									
									
									
									
								
							
							
						
						| Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/128x128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/22x22.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/24x24.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/256x256.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/384x384.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/48x48.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/64x64.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.6 KiB | 
| @@ -1,32 +1,41 @@ | ||||
| appId: com.rickvanlieshout.tidal-hifi | ||||
| electronVersion: 24.1.2 | ||||
| electronVersion: 37.2.5 | ||||
| electronDownload: | ||||
|   version: 24.1.2+wvcus | ||||
|   version: 37.2.5+wvcus | ||||
|   mirror: https://github.com/castlabs/electron-releases/releases/download/v | ||||
| snap: | ||||
|   plugs: | ||||
|     - default | ||||
|     - screen-inhibit-control | ||||
| extraResources: | ||||
|   - "themes/**" | ||||
| linux: | ||||
|   category: AudioVideo | ||||
|   icon: icon.png | ||||
|   icon: build/icons | ||||
|   target: | ||||
|     - dir | ||||
|   executableName: tidal-hifi | ||||
|   executableArgs: | ||||
|     [ | ||||
|       "--ozone-platform-hint=auto", | ||||
|       "--enable-features=WaylandWindowDecorations", | ||||
|       "--enable-wayland-ime", | ||||
|       "--use-angle", | ||||
|     ] | ||||
|   desktop: | ||||
|     Encoding: UTF-8 | ||||
|     Name: TIDAL Hi-Fi | ||||
|     GenericName: TIDAL Hi-Fi | ||||
|     Comment: The web version of listen.tidal.com running in electron with hifi support thanks to widevine. | ||||
|     Icon: icon.png | ||||
|     StartupNotify: true | ||||
|     Terminal: false | ||||
|     Type: Application | ||||
|     Categories: Network;Application;AudioVideo;Audio;Video | ||||
|     StartupWMClass: tidal-hifi | ||||
|     X-PulseAudio-Properties: media.role=music | ||||
|     MimeType: x-scheme-handler/tidal; | ||||
|  | ||||
|     entry: | ||||
|       Encoding: "UTF-8" | ||||
|       Name: "TIDAL Hi-Fi" | ||||
|       GenericName: "TIDAL Hi-Fi" | ||||
|       Comment: "The web version of listen.tidal.com running in electron with hifi support thanks to widevine." | ||||
|       Icon: "tidal-hifi" | ||||
|       StartupNotify: "true" | ||||
|       Terminal: "false" | ||||
|       Type: "Application" | ||||
|       Categories: "Network;Application;AudioVideo;Audio;Video" | ||||
|       StartupWMClass: "tidal-hifi" | ||||
|       X-PulseAudio-Properties: "media.role=music" | ||||
|       MimeType: "x-scheme-handler/tidal;" | ||||
| mac: | ||||
|   category: public.app-category.entertainment | ||||
| win: | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| extends: ./build/electron-builder.base.yml | ||||
| linux: | ||||
|   icon: icon.png | ||||
|   target: | ||||
|     - pacman | ||||
|   | ||||
| Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icon.icns
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/icon.png
									
									
									
									
									
								
							
							
						
						| Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/128x128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/22x22.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/24x24.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/256x256 copy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/256x256.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/384x384.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/48x48.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/64x64.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/icon-inverted.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/icon.icns
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								build/icons/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/customcss-config.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 38 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/customcss-menu.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 262 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/customcss.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 36 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/discord.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/integrations.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 63 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/new-about.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 800 KiB After Width: | Height: | Size: 800 KiB | 
| Before Width: | Height: | Size: 726 KiB After Width: | Height: | Size: 726 KiB | 
| Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 317 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/swagger.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 88 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/theming.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 49 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/tokyo-night.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.7 MiB | 
| Before Width: | Height: | Size: 47 KiB | 
							
								
								
									
										38
									
								
								docs/theming.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | ||||
| # Theming TIDAL Hi-Fi | ||||
|  | ||||
| ## Table of contents | ||||
|  | ||||
| <!-- toc --> | ||||
|  | ||||
| - [Theming TIDAL Hi-Fi](#theming-TIDAL Hi-Fi) | ||||
|   - [Table of contents](#table-of-contents) | ||||
|   - [Custom CSS](#custom-css) | ||||
|   - [config](#config) | ||||
|   - [Warning! Themes might break](#warning-themes-might-break) | ||||
|  | ||||
| <!-- tocstop --> | ||||
|  | ||||
| By default TIDAL Hi-Fi comes with a few themes. | ||||
| You can select these in the settings window under the theming tab as shown below. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Custom CSS | ||||
|  | ||||
| The custom CSS will be added to the HTML document last. | ||||
| This means that it will overwrite any existing CSS, even that of themes, unless the original has an access modifier such as `$important`. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## config | ||||
|  | ||||
| The theme selector and customCSS are stored in the config file. | ||||
| The custom CSS is stored as a list of lines. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Warning! Themes might break | ||||
|  | ||||
| Themes might break at any point. Tidal changes their webpage structure a ton (they probably generate classNames and don't provide roles/ids/attributes.) | ||||
|  | ||||
| If one breaks you can create an Issue on GitHub or ask for assistance in the [Discord channel](https://discord.gg/yhNwf4v4He). | ||||
							
								
								
									
										8574
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										70
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,10 +1,18 @@ | ||||
| { | ||||
|   "name": "tidal-hifi", | ||||
|   "version": "5.0.0", | ||||
|   "version": "5.20.1", | ||||
|   "description": "Tidal on Electron with widevine(hifi) support", | ||||
|   "main": "src/main.js", | ||||
|   "main": "ts-dist/main.js", | ||||
|   "scripts": { | ||||
|     "start": "electron .", | ||||
|     "start": "electron --inspect=0.0.0.0:5858 --remote-debugging-port=8315 --remote-allow-origins=* .", | ||||
|     "watchStart": "nodemon dist -x \"npm run start\"", | ||||
|     "compile": "tsc && npm run sass-and-copy", | ||||
|     "deps": "npm run watch", | ||||
|     "watch": "tsc-watch --onSuccess \"npm run compile-all\"", | ||||
|     "copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist", | ||||
|     "copy-themes-dev": "copyfiles -u 1 \"./themes/*\" node_modules/electron/dist/resources", | ||||
|     "compile-all": "npm run sass-and-copy && ts-node scripts/generate-swagger.ts", | ||||
|     "sass-and-copy": "npm run sass && npm run copy-files && npm run copy-themes-dev", | ||||
|     "build": "npm run builder -- -c ./build/electron-builder.yml", | ||||
|     "build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml", | ||||
|     "build-unpacked": "npm run builder -- -c ./build/electron-builder.unpacked.yml", | ||||
| @@ -14,41 +22,63 @@ | ||||
|     "build-wl": "npm run builder -- -c ./build/electron-builder.yml -wl", | ||||
|     "build-mac": "npm run builder -- -c ./build/electron-builder.yml -m", | ||||
|     "build-base": "npm run builder -- -c ./build/electron-builder.base.yml", | ||||
|     "prestart": "npm run sass", | ||||
|     "prebuilder": "npm run sass", | ||||
|     "prebuilder": "npm run compile", | ||||
|     "builder": "electron-builder --publish=never", | ||||
|     "sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css", | ||||
|     "sass-lint": "sass-lint -vc ./sass-lint.yml ./src/pages/settings/settings.scss", | ||||
|     "sass-lint-fix": "sass-lint-auto-fix ./src/pages/settings/settings.scss --config-sass-lint ./sass-lint.yml" | ||||
|     "sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css && sass --no-source-map src/themes:themes", | ||||
|     "style-lint": "npx stylelint **/*.scss", | ||||
|     "style-lint-fix": "npx stylelint --fix **/*.scss" | ||||
|   }, | ||||
|   "keywords": [ | ||||
|     "electron", | ||||
|     "hifi", | ||||
|     "widevine", | ||||
|     "linux" | ||||
|     "linux", | ||||
|     "drm", | ||||
|     "castlabs" | ||||
|   ], | ||||
|   "author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)", | ||||
|   "homepage": "https://github.com/Mastermindzh/tidal-hifi", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@electron/remote": "^2.0.9", | ||||
|     "discord-rpc": "^4.0.1", | ||||
|     "electron-store": "^8.1.0", | ||||
|     "express": "^4.18.2", | ||||
|     "hotkeys-js": "^3.10.2", | ||||
|     "@electron/remote": "^2.1.3", | ||||
|     "@types/swagger-jsdoc": "^6.0.4", | ||||
|     "@xhayper/discord-rpc": "1.3.0", | ||||
|     "axios": "^1.12.0", | ||||
|     "cors": "^2.8.5", | ||||
|     "electron-store": "^8.2.0", | ||||
|     "express": "^5.1.0", | ||||
|     "hotkeys-js": "^3.13.15", | ||||
|     "mpris-service": "^2.1.2", | ||||
|     "request": "^2.88.2", | ||||
|     "sass": "^1.62.0" | ||||
|     "sass": "1.93.2", | ||||
|     "swagger-ui-express": "^5.0.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@mastermindzh/prettier-config": "^1.0.0", | ||||
|     "electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus", | ||||
|     "electron-builder": "^24.2.1", | ||||
|     "@types/cors": "^2.8.19", | ||||
|     "@types/express": "^5.0.3", | ||||
|     "@types/node": "^22.16.2", | ||||
|     "@types/request": "^2.48.12", | ||||
|     "@types/swagger-ui-express": "^4.1.8", | ||||
|     "@typescript-eslint/eslint-plugin": "^8.36.0", | ||||
|     "@typescript-eslint/parser": "^8.36.0", | ||||
|     "copyfiles": "^2.4.1", | ||||
|     "electron": "github:castlabs/electron-releases#v37.2.5+wvcus", | ||||
|     "electron-builder": "~26.0.12", | ||||
|     "eslint": "^9.30.1", | ||||
|     "js-yaml": "^4.1.0", | ||||
|     "markdown-toc": "^1.2.0", | ||||
|     "prettier": "^2.8.7", | ||||
|     "sass-lint": "^1.13.1", | ||||
|     "sass-lint-auto-fix": "^0.21.2" | ||||
|     "node-abi": "^4.12.0", | ||||
|     "nodemon": "^3.1.10", | ||||
|     "prettier": "^3.6.2", | ||||
|     "stylelint": "^16.21.1", | ||||
|     "stylelint-config-standard": "^39.0.0", | ||||
|     "stylelint-config-standard-scss": "^16.0.0", | ||||
|     "stylelint-prettier": "^5.0.3", | ||||
|     "swagger-jsdoc": "^6.2.8", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "tsc-watch": "^7.1.1", | ||||
|     "typescript": "^5.8.3" | ||||
|   }, | ||||
|   "prettier": "@mastermindzh/prettier-config" | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "ignoreDeps": ["@types/node", "electron-store", "@xhayper/discord-rpc"] | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| rules: | ||||
|   property-sort-order: | ||||
|     - 1 | ||||
|     - order: "smacss" | ||||
|   class-name-format: | ||||
|     - 1 | ||||
|     - convention: "hyphenatedbem" | ||||
|   quotes: | ||||
|     - 1 | ||||
|     - style: "double" | ||||
|   nesting-depth: | ||||
|     - 1 | ||||
|     - max-depth: 3 | ||||
|   placeholder-in-extend: | ||||
|     - 0 | ||||
|   no-vendor-prefixes: | ||||
|     - 0 | ||||
|   empty-line-between-blocks: | ||||
|     - 0 | ||||
|   force-pseudo-nesting: | ||||
|     - 0 | ||||
							
								
								
									
										30
									
								
								scripts/generate-swagger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| import fs from "fs"; | ||||
| import swaggerjsdoc from "swagger-jsdoc"; | ||||
| import packagejson from "./../package.json"; | ||||
|  | ||||
| const specs = swaggerjsdoc({ | ||||
|   definition: { | ||||
|     openapi: "3.1.0", | ||||
|     info: { | ||||
|       title: "TIDAL Hi-Fi API", | ||||
|       version: packagejson.version, | ||||
|       description: "", | ||||
|       license: { | ||||
|         name: packagejson.license, | ||||
|         url: "https://github.com/Mastermindzh/tidal-hifi/blob/master/LICENSE", | ||||
|       }, | ||||
|       contact: { | ||||
|         name: "Rick <mastermindzh> van Lieshout", | ||||
|         url: "https://www.rickvanlieshout.com", | ||||
|       }, | ||||
|     }, | ||||
|     externalDocs: { | ||||
|       description: "swagger.json", | ||||
|       url: "swagger.json", | ||||
|     }, | ||||
|   }, | ||||
|   apis: ["**/*.ts"], | ||||
| }); | ||||
|  | ||||
| fs.writeFileSync("src/features/api/swagger.json", JSON.stringify(specs, null, 2), "utf8"); | ||||
| console.log("Written swagger.json"); | ||||
							
								
								
									
										16
									
								
								scripts/resize-icons.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [ "$1" != "" ]; then # check if arg 1 is present | ||||
|     FILE=$1 | ||||
| else | ||||
|     echo "Please provide a file as an argument." | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| SIZES=("16x16" "22x22" "24x24" "32x32" "48x48" "64x64" "128x128" "256x256" "384x384") | ||||
|  | ||||
| echo "Resizing $FILE..." | ||||
|  | ||||
| for i in "${SIZES[@]}"; do | ||||
|     convert "$FILE" -resize "$i" "$i.png" | ||||
| done | ||||
							
								
								
									
										48
									
								
								scripts/verifyElements.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| // for some dumb reason `listen.tidal.com` has disabled console.log | ||||
| // we can simply return an array with values though... | ||||
| // run this on a playlist or mix page and observe the result | ||||
| // NOTE: play & pause can't live together so one or the other will throw an error | ||||
| (() => { | ||||
|   let elements = { | ||||
|     play: '*[data-test="play"]', | ||||
|     pause: '*[data-test="pause"]', | ||||
|     next: '*[data-test="next"]', | ||||
|     previous: 'button[data-test="previous"]', | ||||
|     title: '*[data-test^="footer-track-title"]', | ||||
|     artists: '*[data-test^="grid-item-detail-text-title-artist"]', | ||||
|     home: '*[data-test="menu--home"]', | ||||
|     back: '[title^="Back"]', | ||||
|     forward: '[title^="Next"]', | ||||
|     search: '[class^="searchField"]', | ||||
|     shuffle: '*[data-test="shuffle"]', | ||||
|     repeat: '*[data-test="repeat"]', | ||||
|     account: '*[data-test^="profile-image-button"]', | ||||
|     settings: '*[data-test^="sidebar-menu-button"]', | ||||
|     openSettings: '*[data-test^="open-settings"]', | ||||
|     media: '*[data-test="current-media-imagery"]', | ||||
|     image: "img", | ||||
|     current: '*[data-test="current-time"]', | ||||
|     duration: '*[class^=_playbackControlsContainer] *[data-test="duration"]', | ||||
|     bar: '*[data-test="progress-bar"]', | ||||
|     footer: "#footerPlayer", | ||||
|     mediaItem: "[data-type='mediaItem']", | ||||
|     album_header_title: '*[class^="_playingFrom"] span:nth-child(2)', | ||||
|     playing_from: '*[class^="_playingFrom"] span:nth-child(2)', | ||||
|     queue_album: "*[class^=playQueueItemsContainer] *[class^=groupTitle] span:nth-child(2)", | ||||
|     currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", | ||||
|     album_name_cell: '[class^="album"]', | ||||
|     tracklist_row: '[data-test="tracklist-row"]', | ||||
|     volume: '*[data-test="volume"]', | ||||
|     favorite: '*[data-test="footer-favorite-button"]', | ||||
|   }; | ||||
|  | ||||
|   let results = []; | ||||
|  | ||||
|   Object.entries(elements).forEach(([key, value]) => { | ||||
|     const returnValue = document.querySelector(`${value}`); | ||||
|     if (!returnValue) { | ||||
|       results.push(`element ${key} not found`); | ||||
|     } | ||||
|   }); | ||||
|   return results; | ||||
| })(); | ||||
| @@ -1,6 +0,0 @@ | ||||
| const flags = { | ||||
|   gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }], | ||||
|   disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }], | ||||
| }; | ||||
|  | ||||
| module.exports = flags; | ||||
							
								
								
									
										9
									
								
								src/constants/flags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| export const flags: { [key: string]: { flag: string; value?: string }[] } = { | ||||
|   gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }], | ||||
|   disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }], | ||||
|   enableWaylandSupport: [ | ||||
|     { flag: "enable-features", value: "UseOzonePlatform" }, | ||||
|     { flag: "ozone-platform-hint", value: "auto" }, | ||||
|     { flag: "enable-features", value: "WaylandWindowDecorations" }, | ||||
|   ], | ||||
| }; | ||||
| @@ -1,4 +1,4 @@ | ||||
| const globalEvents = { | ||||
| export const globalEvents = { | ||||
|   play: "play", | ||||
|   pause: "pause", | ||||
|   playPause: "playPause", | ||||
| @@ -10,6 +10,9 @@ const globalEvents = { | ||||
|   showSettings: "showSettings", | ||||
|   storeChanged: "storeChanged", | ||||
|   error: "error", | ||||
|   getUniversalLink: "getUniversalLink", | ||||
|   log: "log", | ||||
|   toggleFavorite: "toggleFavorite", | ||||
|   toggleShuffle: "toggleShuffle", | ||||
|   toggleRepeat: "toggleRepeat", | ||||
| }; | ||||
| 
 | ||||
| module.exports = globalEvents; | ||||
| @@ -1,9 +1,7 @@ | ||||
| const globalEvents = require("./globalEvents"); | ||||
| import { globalEvents } from "./globalEvents"; | ||||
| 
 | ||||
| const mediaKeys = { | ||||
| export const mediaKeys = { | ||||
|   MediaPlayPause: globalEvents.playPause, | ||||
|   MediaNextTrack: globalEvents.next, | ||||
|   MediaPreviousTrack: globalEvents.previous, | ||||
| }; | ||||
| 
 | ||||
| module.exports = mediaKeys; | ||||
| @@ -8,35 +8,60 @@ | ||||
|  *    }, | ||||
|  *    windowBounds: { width: 800, height: 600 }, | ||||
|  */ | ||||
| const settings = { | ||||
|   notifications: "notifications", | ||||
|   api: "api", | ||||
|   menuBar: "menuBar", | ||||
|   playBackControl: "playBackControl", | ||||
|   skipArtists: "skipArtists", | ||||
|   skippedArtists: "skippedArtists", | ||||
| export const settings = { | ||||
|   adBlock: "adBlock", | ||||
|   disableBackgroundThrottle: "disableBackgroundThrottle", | ||||
|   advanced: { | ||||
|     root: "advanced", | ||||
|     tidalUrl: "advanced.tidalUrl", | ||||
|   }, | ||||
|   api: "api", | ||||
|   apiSettings: { | ||||
|     root: "apiSettings", | ||||
|     port: "apiSettings.port", | ||||
|     hostname: "apiSettings.hostname", | ||||
|   }, | ||||
|   singleInstance: "singleInstance", | ||||
|   customCSS: "customCSS", | ||||
|   disableBackgroundThrottle: "disableBackgroundThrottle", | ||||
|   disableHardwareMediaKeys: "disableHardwareMediaKeys", | ||||
|   enableCustomHotkeys: "enableCustomHotkeys", | ||||
|   enableDiscord: "enableDiscord", | ||||
|   discord: { | ||||
|     detailsPrefix: "discord.detailsPrefix", | ||||
|     buttonText: "discord.buttonText", | ||||
|     includeTimestamps: "discord.includeTimestamps", | ||||
|     showSong: "discord.showSong", | ||||
|     showIdle: "discord.showIdle", | ||||
|     idleText: "discord.idleText", | ||||
|     usingText: "discord.usingText", | ||||
|   }, | ||||
|   ListenBrainz: { | ||||
|     root: "ListenBrainz", | ||||
|     enabled: "ListenBrainz.enabled", | ||||
|     api: "ListenBrainz.api", | ||||
|     token: "ListenBrainz.token", | ||||
|     delay: "ListenBrainz.delay", | ||||
|   }, | ||||
|   flags: { | ||||
|     root: "flags", | ||||
|     disableHardwareMediaKeys: "flags.disableHardwareMediaKeys", | ||||
|     gpuRasterization: "flags.gpuRasterization", | ||||
|     enableWaylandSupport: "flags.enableWaylandSupport", | ||||
|   }, | ||||
|   menuBar: "menuBar", | ||||
|   minimizeOnClose: "minimizeOnClose", | ||||
|   mpris: "mpris", | ||||
|   enableCustomHotkeys: "enableCustomHotkeys", | ||||
|   notifications: "notifications", | ||||
|   playBackControl: "playBackControl", | ||||
|   singleInstance: "singleInstance", | ||||
|   skipArtists: "skipArtists", | ||||
|   skippedArtists: "skippedArtists", | ||||
|   staticWindowTitle: "staticWindowTitle", | ||||
|   theme: "theme", | ||||
|   trayIcon: "trayIcon", | ||||
|   enableDiscord: "enableDiscord", | ||||
|   updateFrequency: "updateFrequency", | ||||
|   windowBounds: { | ||||
|     root: "windowBounds", | ||||
|     width: "windowBounds.width", | ||||
|     height: "windowBounds.height", | ||||
|   }, | ||||
|   minimizeOnClose: "minimizeOnClose", | ||||
| }; | ||||
| 
 | ||||
| module.exports = settings; | ||||
| @@ -1,4 +0,0 @@ | ||||
| module.exports = { | ||||
|   playing: "playing", | ||||
|   paused: "paused", | ||||
| }; | ||||
| @@ -1,3 +0,0 @@ | ||||
| module.exports = { | ||||
|   name: "tidal-hifi", | ||||
| }; | ||||
							
								
								
									
										3
									
								
								src/constants/values.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| export default { | ||||
|   name: "TIDAL Hi-Fi", | ||||
| }; | ||||
							
								
								
									
										1
									
								
								src/declarations.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| declare module "mpris-service"; | ||||
							
								
								
									
										124
									
								
								src/features/api/features/current.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,124 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import fs from "fs"; | ||||
| import { mediaInfo } from "../../../scripts/mediaInfo"; | ||||
|  | ||||
| export const addCurrentInfo = (expressApp: Router) => { | ||||
|   /** | ||||
|    * @swagger | ||||
|    * tags: | ||||
|    *   name: current | ||||
|    *   description: The current media info API | ||||
|    * components: | ||||
|    *   schemas: | ||||
|    *     MediaInfo: | ||||
|    *       type: object | ||||
|    *       properties: | ||||
|    *         title: | ||||
|    *           type: string | ||||
|    *         artists: | ||||
|    *           type: string | ||||
|    *         album: | ||||
|    *           type: string | ||||
|    *         icon: | ||||
|    *           type: string | ||||
|    *           format: uri | ||||
|    *         playingFrom: | ||||
|    *           type: string | ||||
|    *         status: | ||||
|    *           type: string | ||||
|    *         url: | ||||
|    *           type: string | ||||
|    *           format: uri | ||||
|    *         current: | ||||
|    *           type: string | ||||
|    *         currentInSeconds: | ||||
|    *           type: integer | ||||
|    *         duration: | ||||
|    *           type: string | ||||
|    *         durationInSeconds: | ||||
|    *           type: integer | ||||
|    *         image: | ||||
|    *           type: string | ||||
|    *           format: uri | ||||
|    *         favorite: | ||||
|    *           type: boolean | ||||
|    *         player: | ||||
|    *           type: object | ||||
|    *           properties: | ||||
|    *             status: | ||||
|    *               type: string | ||||
|    *             shuffle: | ||||
|    *               type: boolean | ||||
|    *             repeat: | ||||
|    *               type: string | ||||
|    *         artist: | ||||
|    *           type: string | ||||
|    *       example: | ||||
|    *         title: "Sample Title" | ||||
|    *         artists: "Sample Artist" | ||||
|    *         album: "Sample Album" | ||||
|    *         icon: "/path/to/sample/icon.jpg" | ||||
|    *         playingFrom: "Sample Playlist" | ||||
|    *         status: "playing" | ||||
|    *         url: "https://tidal.com/browse/track/sample" | ||||
|    *         current: "1:23" | ||||
|    *         currentInSeconds: 83 | ||||
|    *         duration: "3:45" | ||||
|    *         durationInSeconds: 225 | ||||
|    *         image: "https://example.com/sample-image.jpg" | ||||
|    *         favorite: true | ||||
|    *         player: | ||||
|    *           status: "playing" | ||||
|    *           shuffle: true | ||||
|    *           repeat: "one" | ||||
|    *         artist: "Sample Artist" | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /current: | ||||
|    *   get: | ||||
|    *     summary: Get current media info | ||||
|    *     tags: [current] | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *         description: Current media info | ||||
|    *         content: | ||||
|    *           application/json: | ||||
|    *             schema: | ||||
|    *               $ref: '#/components/schemas/MediaInfo' | ||||
|    */ | ||||
|   expressApp.get("/current", (_req, res) => { | ||||
|     res.json({ ...mediaInfo, artist: mediaInfo.artists }); | ||||
|   }); | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /current/image: | ||||
|    *   get: | ||||
|    *     summary: Get current media image | ||||
|    *     tags: [current] | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *         description: Current media image | ||||
|    *         content: | ||||
|    *           image/png: | ||||
|    *             schema: | ||||
|    *               type: string | ||||
|    *               format: binary | ||||
|    *       404: | ||||
|    *         description: Not found | ||||
|    */ | ||||
|   expressApp.get("/current/image", getCurrentImage); | ||||
| }; | ||||
|  | ||||
| export const getCurrentImage = (req: Request, res: Response) => { | ||||
|   const stream = fs.createReadStream(mediaInfo.icon); | ||||
|   stream.on("open", function () { | ||||
|     res.set("Content-Type", "image/png"); | ||||
|     stream.pipe(res); | ||||
|   }); | ||||
|   stream.on("error", function () { | ||||
|     res.set("Content-Type", "text/plain"); | ||||
|     res.status(404).end("Not found"); | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										164
									
								
								src/features/api/features/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,164 @@ | ||||
| import { BrowserWindow } from "electron"; | ||||
| import { Router } from "express"; | ||||
| import { globalEvents } from "../../../constants/globalEvents"; | ||||
| import { settings } from "../../../constants/settings"; | ||||
| import { MediaStatus } from "../../../models/mediaStatus"; | ||||
| import { mediaInfo } from "../../../scripts/mediaInfo"; | ||||
| import { settingsStore } from "../../../scripts/settings"; | ||||
| import { handleWindowEvent } from "../helpers/handleWindowEvent"; | ||||
|  | ||||
| export const addPlaybackControl = (expressApp: Router, mainWindow: BrowserWindow) => { | ||||
|   const windowEvent = handleWindowEvent(mainWindow); | ||||
|   const createRoute = (route: string) => `/player${route}`; | ||||
|  | ||||
|   /** | ||||
|    * @swagger | ||||
|    * tags: | ||||
|    *   name: player | ||||
|    *   description: The player control API | ||||
|    * components: | ||||
|    *   schemas: | ||||
|    *     OkResponse: | ||||
|    *       type: string | ||||
|    *       example: "OK" | ||||
|    */ | ||||
|   const createPlayerAction = (route: string, action: string) => { | ||||
|     expressApp.post(createRoute(route), (req, res) => windowEvent(res, action)); | ||||
|   }; | ||||
|  | ||||
|   if (settingsStore.get(settings.playBackControl)) { | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/play: | ||||
|      *   post: | ||||
|      *     summary: Play the current media | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/play", globalEvents.play); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/favorite/toggle: | ||||
|      *   post: | ||||
|      *     summary: Add the current media to your favorites, or remove it if its already added to your favorites | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/favorite/toggle", globalEvents.toggleFavorite); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/pause: | ||||
|      *   post: | ||||
|      *     summary: Pause the current media | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/pause", globalEvents.pause); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/next: | ||||
|      *   post: | ||||
|      *     summary: Play the next song | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/next", globalEvents.next); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/previous: | ||||
|      *   post: | ||||
|      *     summary: Play the previous song | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/previous", globalEvents.previous); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/shuffle/toggle: | ||||
|      *   post: | ||||
|      *     summary: Play the previous song | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/shuffle/toggle", globalEvents.toggleShuffle); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/repeat/toggle: | ||||
|      *   post: | ||||
|      *     summary: Toggle the repeat status, toggles between "off" , "single" and "all" | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     createPlayerAction("/repeat/toggle", globalEvents.toggleRepeat); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /player/playpause: | ||||
|      *   post: | ||||
|      *     summary: Start playing the media if paused, or pause the media if playing | ||||
|      *     tags: [player] | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.post(createRoute("/playpause"), (req, res) => { | ||||
|       if (mediaInfo.status === MediaStatus.playing) { | ||||
|         windowEvent(res, globalEvents.pause); | ||||
|       } else { | ||||
|         windowEvent(res, globalEvents.play); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										121
									
								
								src/features/api/features/settings/settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,121 @@ | ||||
| import { Request, Router } from "express"; | ||||
| import { settings } from "../../../../constants/settings"; | ||||
| import { mediaInfo } from "../../../../scripts/mediaInfo"; | ||||
| import { | ||||
|   addSkippedArtists, | ||||
|   removeSkippedArtists, | ||||
|   settingsStore, | ||||
| } from "../../../../scripts/settings"; | ||||
| import { BrowserWindow } from "electron"; | ||||
| import { globalEvents } from "../../../../constants/globalEvents"; | ||||
|  | ||||
| /** | ||||
|  * @swagger | ||||
|  * tags: | ||||
|  *   name: settings | ||||
|  *   description: The settings management API | ||||
|  * components: | ||||
|  *   schemas: | ||||
|  *     StringArray: | ||||
|  *       type: array | ||||
|  *       items: | ||||
|  *         type: string | ||||
|  *       example: ["Artist1", "Artist2"] | ||||
|  * | ||||
|  * @param expressApp | ||||
|  * @param mainWindow | ||||
|  */ | ||||
| export const addSettingsAPI = (expressApp: Router, mainWindow: BrowserWindow) => { | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /settings/skipped-artists: | ||||
|    *   get: | ||||
|    *     summary: get a list of artists that TIDAL Hi-Fi will skip if skipping is enabled | ||||
|    *     tags: [settings] | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *         description: The list book. | ||||
|    *         content: | ||||
|    *           application/json: | ||||
|    *             schema: | ||||
|    *              $ref: '#/components/schemas/StringArray' | ||||
|    */ | ||||
|   expressApp.get("/settings/skipped-artists", (req, res) => { | ||||
|     res.json(settingsStore.get<string, string[]>(settings.skippedArtists)); | ||||
|   }); | ||||
|  | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /settings/skipped-artists: | ||||
|    *   post: | ||||
|    *     summary: Add new artists to the list of skipped artists | ||||
|    *     tags: [settings] | ||||
|    *     requestBody: | ||||
|    *       required: true | ||||
|    *       content: | ||||
|    *         application/json: | ||||
|    *           schema: | ||||
|    *             $ref: '#/components/schemas/StringArray' | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *         description: Ok | ||||
|    */ | ||||
|   expressApp.post("/settings/skipped-artists", (req: Request<object, object, string[]>, res) => { | ||||
|     addSkippedArtists(req.body); | ||||
|     res.sendStatus(200); | ||||
|   }); | ||||
|  | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /settings/skipped-artists/delete: | ||||
|    *   post: | ||||
|    *     summary: Remove artists from the list of skipped artists | ||||
|    *     tags: [settings] | ||||
|    *     requestBody: | ||||
|    *       required: true | ||||
|    *       content: | ||||
|    *         application/json: | ||||
|    *           schema: | ||||
|    *             $ref: '#/components/schemas/StringArray' | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *         description: Ok | ||||
|    */ | ||||
|   expressApp.post( | ||||
|     "/settings/skipped-artists/delete", | ||||
|     (req: Request<object, object, string[]>, res) => { | ||||
|       removeSkippedArtists(req.body); | ||||
|       res.sendStatus(200); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /settings/skipped-artists/current: | ||||
|    *   post: | ||||
|    *     summary: Add the current artist to the list of skipped artists | ||||
|    *     tags: [settings] | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *        description: Ok | ||||
|    */ | ||||
|   expressApp.post("/settings/skipped-artists/current", (req, res) => { | ||||
|     addSkippedArtists([mediaInfo.artists]); | ||||
|     mainWindow.webContents.send("globalEvent", globalEvents.next); | ||||
|     res.sendStatus(200); | ||||
|   }); | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /settings/skipped-artists/current: | ||||
|    *   delete: | ||||
|    *     summary: Remove the current artist from the list of skipped artists | ||||
|    *     tags: [settings] | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *        description: Ok | ||||
|    */ | ||||
|   expressApp.delete("/settings/skipped-artists/current", (req, res) => { | ||||
|     removeSkippedArtists([mediaInfo.artists]); | ||||
|     res.sendStatus(200); | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										12
									
								
								src/features/api/helpers/handleWindowEvent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| import { BrowserWindow } from "electron"; | ||||
| import { Response } from "express"; | ||||
|  | ||||
| /** | ||||
|  * Shorthand to handle a fire and forget global event | ||||
|  * @param {*} res | ||||
|  * @param {*} action | ||||
|  */ | ||||
| export const handleWindowEvent = (mainWindow: BrowserWindow) => (res: Response, action: string) => { | ||||
|   mainWindow.webContents.send("globalEvent", action); | ||||
|   res.sendStatus(200); | ||||
| }; | ||||
							
								
								
									
										46
									
								
								src/features/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | ||||
| import cors from "cors"; | ||||
| import { BrowserWindow, dialog } from "electron"; | ||||
| import express from "express"; | ||||
| import swaggerUi from "swagger-ui-express"; | ||||
| import { settingsStore } from "../../scripts/settings"; | ||||
| import { settings } from "./../../constants/settings"; | ||||
| import { addCurrentInfo } from "./features/current"; | ||||
| import { addPlaybackControl } from "./features/player"; | ||||
| import { addSettingsAPI } from "./features/settings/settings"; | ||||
| import { addLegacyApi } from "./legacy"; | ||||
| import swaggerSpec from "./swagger.json"; | ||||
|  | ||||
| /** | ||||
|  * Function to enable TIDAL Hi-Fi's express api | ||||
|  */ | ||||
| export const startApi = (mainWindow: BrowserWindow) => { | ||||
|   const port = settingsStore.get<string, number>(settings.apiSettings.port); | ||||
|   const hostname = settingsStore.get<string, string>(settings.apiSettings.hostname) ?? "127.0.0.1"; | ||||
|  | ||||
|   const expressApp = express(); | ||||
|   expressApp.use(cors()); | ||||
|   expressApp.use(express.json()); | ||||
|   expressApp.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); | ||||
|   expressApp.get("/", (req, res) => { | ||||
|     res.send("Hello World!"); | ||||
|   }); | ||||
|   expressApp.get("/swagger.json", (req, res) => { | ||||
|     res.json(swaggerSpec); | ||||
|   }); | ||||
|  | ||||
|   // add features | ||||
|   addLegacyApi(expressApp, mainWindow); | ||||
|   addPlaybackControl(expressApp, mainWindow); | ||||
|   addCurrentInfo(expressApp); | ||||
|   addSettingsAPI(expressApp, mainWindow); | ||||
|  | ||||
|   const expressInstance = expressApp.listen(port, hostname); | ||||
|   expressInstance.on("error", function (e: { code: string }) { | ||||
|     let message = e.code; | ||||
|     if (e.code === "EADDRINUSE") { | ||||
|       message = `Port ${port} in use.`; | ||||
|     } | ||||
|  | ||||
|     dialog.showErrorBox("Api failed to start.", message); | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										160
									
								
								src/features/api/legacy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,160 @@ | ||||
| import { BrowserWindow } from "electron"; | ||||
| import { Response, Router } from "express"; | ||||
| import { globalEvents } from "../../constants/globalEvents"; | ||||
| import { settings } from "../../constants/settings"; | ||||
| import { MediaStatus } from "../../models/mediaStatus"; | ||||
| import { mediaInfo } from "../../scripts/mediaInfo"; | ||||
| import { settingsStore } from "../../scripts/settings"; | ||||
| import { getCurrentImage } from "./features/current"; | ||||
|  | ||||
| /** | ||||
|  * The legacy API, this will not be maintained and probably has duplicate code :) | ||||
|  * @param expressApp | ||||
|  * @param mainWindow | ||||
|  */ | ||||
| export const addLegacyApi = (expressApp: Router, mainWindow: BrowserWindow) => { | ||||
|   /** | ||||
|    * @swagger | ||||
|    * /image: | ||||
|    *   get: | ||||
|    *     summary: Get current image | ||||
|    *     tags: [legacy] | ||||
|    *     deprecated: true | ||||
|    *     responses: | ||||
|    *       200: | ||||
|    *         description: Current image | ||||
|    *         content: | ||||
|    *           image/png: | ||||
|    *             schema: | ||||
|    *               type: string | ||||
|    *               format: binary | ||||
|    *       404: | ||||
|    *         description: Not found | ||||
|    */ | ||||
|   expressApp.get("/image", getCurrentImage); | ||||
|  | ||||
|   if (settingsStore.get(settings.playBackControl)) { | ||||
|     addLegacyControls(); | ||||
|   } | ||||
|   function addLegacyControls() { | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /play: | ||||
|      *   get: | ||||
|      *     summary: Play the current media | ||||
|      *     tags: [legacy] | ||||
|      *     deprecated: true | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Action performed | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.get("/play", ({ res }) => handleGlobalEvent(res, globalEvents.play)); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /favorite/toggle: | ||||
|      *   get: | ||||
|      *     summary: Add the current media to your favorites, or remove it if its already added to your favorites | ||||
|      *     tags: [legacy] | ||||
|      *     deprecated: true | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.post("/favorite/toggle", (req, res) => | ||||
|       handleGlobalEvent(res, globalEvents.toggleFavorite) | ||||
|     ); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /pause: | ||||
|      *   get: | ||||
|      *     summary: Pause the current media | ||||
|      *     tags: [legacy] | ||||
|      *     deprecated: true | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause)); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /next: | ||||
|      *   get: | ||||
|      *     summary: Play the next song | ||||
|      *     tags: [legacy] | ||||
|      *     deprecated: true | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next)); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /previous: | ||||
|      *   get: | ||||
|      *     summary: Play the previous song | ||||
|      *     tags: [legacy] | ||||
|      *     deprecated: true | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous)); | ||||
|  | ||||
|     /** | ||||
|      * @swagger | ||||
|      * /playpause: | ||||
|      *   get: | ||||
|      *     summary: Toggle play/pause | ||||
|      *     tags: [legacy] | ||||
|      *     deprecated: true | ||||
|      *     responses: | ||||
|      *       200: | ||||
|      *         description: Ok | ||||
|      *         content: | ||||
|      *           text/plain: | ||||
|      *             schema: | ||||
|      *               $ref: '#/components/schemas/OkResponse' | ||||
|      */ | ||||
|     expressApp.get("/playpause", (req, res) => { | ||||
|       if (mediaInfo.status === MediaStatus.playing) { | ||||
|         handleGlobalEvent(res, globalEvents.pause); | ||||
|       } else { | ||||
|         handleGlobalEvent(res, globalEvents.play); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Shorthand to handle a fire and forget global event | ||||
|    * @param {*} res | ||||
|    * @param {*} action | ||||
|    */ | ||||
|   function handleGlobalEvent(res: Response, action: string) { | ||||
|     mainWindow.webContents.send("globalEvent", action); | ||||
|     res.sendStatus(200); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										535
									
								
								src/features/api/swagger.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,535 @@ | ||||
| { | ||||
|   "openapi": "3.1.0", | ||||
|   "info": { | ||||
|     "title": "TIDAL Hi-Fi API", | ||||
|     "version": "5.20.1", | ||||
|     "description": "", | ||||
|     "license": { | ||||
|       "name": "MIT", | ||||
|       "url": "https://github.com/Mastermindzh/tidal-hifi/blob/master/LICENSE" | ||||
|     }, | ||||
|     "contact": { | ||||
|       "name": "Rick <mastermindzh> van Lieshout", | ||||
|       "url": "https://www.rickvanlieshout.com" | ||||
|     } | ||||
|   }, | ||||
|   "externalDocs": { | ||||
|     "description": "swagger.json", | ||||
|     "url": "swagger.json" | ||||
|   }, | ||||
|   "paths": { | ||||
|     "/current": { | ||||
|       "get": { | ||||
|         "summary": "Get current media info", | ||||
|         "tags": ["current"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Current media info", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/MediaInfo" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/current/image": { | ||||
|       "get": { | ||||
|         "summary": "Get current media image", | ||||
|         "tags": ["current"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Current media image", | ||||
|             "content": { | ||||
|               "image/png": { | ||||
|                 "schema": { | ||||
|                   "type": "string", | ||||
|                   "format": "binary" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           "404": { | ||||
|             "description": "Not found" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/play": { | ||||
|       "post": { | ||||
|         "summary": "Play the current media", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/favorite/toggle": { | ||||
|       "post": { | ||||
|         "summary": "Add the current media to your favorites, or remove it if its already added to your favorites", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/pause": { | ||||
|       "post": { | ||||
|         "summary": "Pause the current media", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/next": { | ||||
|       "post": { | ||||
|         "summary": "Play the next song", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/previous": { | ||||
|       "post": { | ||||
|         "summary": "Play the previous song", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/shuffle/toggle": { | ||||
|       "post": { | ||||
|         "summary": "Play the previous song", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/repeat/toggle": { | ||||
|       "post": { | ||||
|         "summary": "Toggle the repeat status, toggles between \"off\" , \"single\" and \"all\"", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/player/playpause": { | ||||
|       "post": { | ||||
|         "summary": "Start playing the media if paused, or pause the media if playing", | ||||
|         "tags": ["player"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/settings/skipped-artists": { | ||||
|       "get": { | ||||
|         "summary": "get a list of artists that TIDAL Hi-Fi will skip if skipping is enabled", | ||||
|         "tags": ["settings"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "The list book.", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/StringArray" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "post": { | ||||
|         "summary": "Add new artists to the list of skipped artists", | ||||
|         "tags": ["settings"], | ||||
|         "requestBody": { | ||||
|           "required": true, | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/StringArray" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/settings/skipped-artists/delete": { | ||||
|       "post": { | ||||
|         "summary": "Remove artists from the list of skipped artists", | ||||
|         "tags": ["settings"], | ||||
|         "requestBody": { | ||||
|           "required": true, | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/StringArray" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/settings/skipped-artists/current": { | ||||
|       "post": { | ||||
|         "summary": "Add the current artist to the list of skipped artists", | ||||
|         "tags": ["settings"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "summary": "Remove the current artist from the list of skipped artists", | ||||
|         "tags": ["settings"], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/image": { | ||||
|       "get": { | ||||
|         "summary": "Get current image", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Current image", | ||||
|             "content": { | ||||
|               "image/png": { | ||||
|                 "schema": { | ||||
|                   "type": "string", | ||||
|                   "format": "binary" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           "404": { | ||||
|             "description": "Not found" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/play": { | ||||
|       "get": { | ||||
|         "summary": "Play the current media", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Action performed", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/favorite/toggle": { | ||||
|       "get": { | ||||
|         "summary": "Add the current media to your favorites, or remove it if its already added to your favorites", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/pause": { | ||||
|       "get": { | ||||
|         "summary": "Pause the current media", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/next": { | ||||
|       "get": { | ||||
|         "summary": "Play the next song", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/previous": { | ||||
|       "get": { | ||||
|         "summary": "Play the previous song", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/playpause": { | ||||
|       "get": { | ||||
|         "summary": "Toggle play/pause", | ||||
|         "tags": ["legacy"], | ||||
|         "deprecated": true, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "Ok", | ||||
|             "content": { | ||||
|               "text/plain": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/OkResponse" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "components": { | ||||
|     "schemas": { | ||||
|       "MediaInfo": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "title": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "artists": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "album": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "icon": { | ||||
|             "type": "string", | ||||
|             "format": "uri" | ||||
|           }, | ||||
|           "playingFrom": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "status": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "url": { | ||||
|             "type": "string", | ||||
|             "format": "uri" | ||||
|           }, | ||||
|           "current": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "currentInSeconds": { | ||||
|             "type": "integer" | ||||
|           }, | ||||
|           "duration": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "durationInSeconds": { | ||||
|             "type": "integer" | ||||
|           }, | ||||
|           "image": { | ||||
|             "type": "string", | ||||
|             "format": "uri" | ||||
|           }, | ||||
|           "favorite": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "player": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
|               "status": { | ||||
|                 "type": "string" | ||||
|               }, | ||||
|               "shuffle": { | ||||
|                 "type": "boolean" | ||||
|               }, | ||||
|               "repeat": { | ||||
|                 "type": "string" | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           "artist": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "example": { | ||||
|           "title": "Sample Title", | ||||
|           "artists": "Sample Artist", | ||||
|           "album": "Sample Album", | ||||
|           "icon": "/path/to/sample/icon.jpg", | ||||
|           "playingFrom": "Sample Playlist", | ||||
|           "status": "playing", | ||||
|           "url": "https://tidal.com/browse/track/sample", | ||||
|           "current": "1:23", | ||||
|           "currentInSeconds": 83, | ||||
|           "duration": "3:45", | ||||
|           "durationInSeconds": 225, | ||||
|           "image": "https://example.com/sample-image.jpg", | ||||
|           "favorite": true, | ||||
|           "player": { | ||||
|             "status": "playing", | ||||
|             "shuffle": true, | ||||
|             "repeat": "one" | ||||
|           }, | ||||
|           "artist": "Sample Artist" | ||||
|         } | ||||
|       }, | ||||
|       "OkResponse": { | ||||
|         "type": "string", | ||||
|         "example": "OK" | ||||
|       }, | ||||
|       "StringArray": { | ||||
|         "type": "array", | ||||
|         "items": { | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "example": ["Artist1", "Artist2"] | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "tags": [ | ||||
|     { | ||||
|       "name": "current", | ||||
|       "description": "The current media info API" | ||||
|     }, | ||||
|     { | ||||
|       "name": "player", | ||||
|       "description": "The player control API" | ||||
|     }, | ||||
|     { | ||||
|       "name": "settings", | ||||
|       "description": "The settings management API" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/features/flags/flags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| import { App } from "electron"; | ||||
| import { flags } from "../../constants/flags"; | ||||
| import { settings } from "../../constants/settings"; | ||||
| import { settingsStore } from "../../scripts/settings"; | ||||
| import { Logger } from "../logger"; | ||||
|  | ||||
| /** | ||||
|  * Set default Electron flags | ||||
|  */ | ||||
| export function setDefaultFlags(app: App) { | ||||
|   setFlag(app, "disable-seccomp-filter-sandbox"); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set Tidal's managed flags from the user settings | ||||
|  * @param app | ||||
|  */ | ||||
| export function setManagedFlagsFromSettings(app: App) { | ||||
|   const flagsFromSettings = settingsStore.get(settings.flags.root); | ||||
|   if (flagsFromSettings) { | ||||
|     for (const [key, value] of Object.entries(flagsFromSettings)) { | ||||
|       if (value) { | ||||
|         flags[key].forEach((flag) => { | ||||
|           setFlag(app, flag.flag, flag.value); | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set a single flag for Electron | ||||
|  * @param app app to set it on | ||||
|  * @param flag flag name | ||||
|  * @param value value to be set for the flag | ||||
|  */ | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| function setFlag(app: App, flag: string, value?: any) { | ||||
|   Logger.log(`enabling command line option ${flag} with value ${value}`); | ||||
|   app.commandLine.appendSwitch(flag, value); | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/features/idleInhibitor/idleInhibitor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | ||||
| import { PowerSaveBlocker, powerSaveBlocker } from "electron"; | ||||
| import { Logger } from "../logger"; | ||||
|  | ||||
| /** | ||||
|  * Start blocking idle/screen timeouts | ||||
|  * @param blocker optional instance of the powerSaveBlocker to use | ||||
|  * @returns id of current block | ||||
|  */ | ||||
| export const acquireInhibitor = (blocker?: PowerSaveBlocker): number => { | ||||
|   const currentBlocker = blocker ?? powerSaveBlocker; | ||||
|   const blockId = currentBlocker.start("prevent-app-suspension"); | ||||
|   Logger.log(`Started preventing app suspension with id: ${blockId}`); | ||||
|   return blockId; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Check whether there is a blocker active for the current id, if not start it. | ||||
|  * @param id id of inhibitor you want to check activity against | ||||
|  * @param blocker optional instance of the powerSaveBlocker to use | ||||
|  */ | ||||
| export const acquireInhibitorIfInactive = (id: number, blocker?: PowerSaveBlocker): number => { | ||||
|   const currentBlocker = blocker ?? powerSaveBlocker; | ||||
|   if (!isInhibitorActive(id, currentBlocker)) { | ||||
|     return acquireInhibitor(); | ||||
|   } | ||||
|  | ||||
|   return id; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * stop blocking idle/screen timeouts | ||||
|  * @param id id of inhibitor you want to check activity against | ||||
|  * @param blocker optional instance of the powerSaveBlocker to use | ||||
|  */ | ||||
| export const releaseInhibitor = (id: number, blocker?: PowerSaveBlocker) => { | ||||
|   try { | ||||
|     const currentBlocker = blocker ?? powerSaveBlocker; | ||||
|     currentBlocker.stop(id); | ||||
|     Logger.log(`Released inhibitor with id: ${id}`); | ||||
|   } catch (error) { | ||||
|     Logger.log("Releasing inhibitor failed"); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * stop blocking idle/screen timeouts if a inhibitor is active | ||||
|  * @param id id of inhibitor you want to check activity against | ||||
|  * @param blocker optional instance of the powerSaveBlocker to use | ||||
|  */ | ||||
| export const releaseInhibitorIfActive = (id: number, blocker?: PowerSaveBlocker) => { | ||||
|   const currentBlocker = blocker ?? powerSaveBlocker; | ||||
|   if (isInhibitorActive(id, currentBlocker)) { | ||||
|     releaseInhibitor(id, currentBlocker); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * check whether the inhibitor is active | ||||
|  * @param id id of inhibitor you want to check activity against | ||||
|  * @param blocker optional instance of the powerSaveBlocker to use | ||||
|  */ | ||||
| export const isInhibitorActive = (id: number, blocker?: PowerSaveBlocker) => { | ||||
|   const currentBlocker = blocker ?? powerSaveBlocker; | ||||
|   return currentBlocker.isStarted(id); | ||||
| }; | ||||
							
								
								
									
										135
									
								
								src/features/listenbrainz/listenbrainz.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,135 @@ | ||||
| import axios from "axios"; | ||||
| import Store from "electron-store"; | ||||
| import { settings } from "../../constants/settings"; | ||||
| import { MediaStatus } from "../../models/mediaStatus"; | ||||
| import { settingsStore } from "../../scripts/settings"; | ||||
| import { Logger } from "../logger"; | ||||
| import { StoreData } from "./models/storeData"; | ||||
|  | ||||
| const ListenBrainzStore = new Store({ name: "listenbrainz" }); | ||||
|  | ||||
| export const ListenBrainzConstants = { | ||||
|   oldData: "oldData", | ||||
| }; | ||||
|  | ||||
| export class ListenBrainz { | ||||
|   /** | ||||
|    * Create the object to store old information in the Store :) | ||||
|    * @param title | ||||
|    * @param artists | ||||
|    * @param duration | ||||
|    * @returns data passed along in an object + a "listenedAt" key with the current time | ||||
|    */ | ||||
|   private static constructStoreData(title: string, artists: string, duration: number): StoreData { | ||||
|     return { | ||||
|       listenedAt: Math.floor(new Date().getTime() / 1000), | ||||
|       title, | ||||
|       artists, | ||||
|       duration, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Call the ListenBrainz API and create playing now payload and scrobble old song | ||||
|    * @param title | ||||
|    * @param artists | ||||
|    * @param status | ||||
|    * @param duration | ||||
|    */ | ||||
|   public static async scrobble( | ||||
|     title: string, | ||||
|     artists: string, | ||||
|     status: string, | ||||
|     duration: number | ||||
|   ): Promise<void> { | ||||
|     try { | ||||
|       if (status === MediaStatus.paused) { | ||||
|         return; | ||||
|       } else { | ||||
|         // Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area | ||||
|         const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData; | ||||
|         const tidalUrl = | ||||
|           settingsStore.get<string, string>(settings.advanced.tidalUrl) || | ||||
|           "https://listen.tidal.com"; | ||||
|         const playing_data = { | ||||
|           listen_type: "playing_now", | ||||
|           payload: [ | ||||
|             { | ||||
|               track_metadata: { | ||||
|                 additional_info: { | ||||
|                   media_player: "Tidal Hi-Fi", | ||||
|                   submission_client: "Tidal Hi-Fi", | ||||
|                   music_service: "tidal.com", | ||||
|                   duration: duration, | ||||
|                 }, | ||||
|                 artist_name: artists, | ||||
|                 track_name: title, | ||||
|               }, | ||||
|             }, | ||||
|           ], | ||||
|         }; | ||||
|  | ||||
|         await axios.post( | ||||
|           `${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`, | ||||
|           playing_data, | ||||
|           { | ||||
|             headers: { | ||||
|               "Content-Type": "application/json", | ||||
|               Authorization: `Token ${settingsStore.get<string, string>( | ||||
|                 settings.ListenBrainz.token | ||||
|               )}`, | ||||
|             }, | ||||
|           } | ||||
|         ); | ||||
|         if (!oldData) { | ||||
|           ListenBrainzStore.set( | ||||
|             ListenBrainzConstants.oldData, | ||||
|             this.constructStoreData(title, artists, duration) | ||||
|           ); | ||||
|         } else { | ||||
|           if (oldData.title !== title) { | ||||
|             // This constructs the data required to scrobble the data after the song finishes | ||||
|             const scrobble_data = { | ||||
|               listen_type: "single", | ||||
|               payload: [ | ||||
|                 { | ||||
|                   listened_at: oldData.listenedAt, | ||||
|                   track_metadata: { | ||||
|                     additional_info: { | ||||
|                       media_player: "Tidal Hi-Fi", | ||||
|                       submission_client: "Tidal Hi-Fi", | ||||
|                       music_service: tidalUrl, | ||||
|                       duration: oldData.duration, | ||||
|                     }, | ||||
|                     artist_name: oldData.artists, | ||||
|                     track_name: oldData.title, | ||||
|                   }, | ||||
|                 }, | ||||
|               ], | ||||
|             }; | ||||
|             await axios.post( | ||||
|               `${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`, | ||||
|               scrobble_data, | ||||
|               { | ||||
|                 headers: { | ||||
|                   "Content-Type": "application/json", | ||||
|                   Authorization: `Token ${settingsStore.get<string, string>( | ||||
|                     settings.ListenBrainz.token | ||||
|                   )}`, | ||||
|                 }, | ||||
|               } | ||||
|             ); | ||||
|             ListenBrainzStore.set( | ||||
|               ListenBrainzConstants.oldData, | ||||
|               this.constructStoreData(title, artists, duration) | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } catch (error) { | ||||
|       Logger.log(JSON.stringify(error)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export { ListenBrainzStore }; | ||||
							
								
								
									
										9
									
								
								src/features/listenbrainz/models/storeData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| /** | ||||
|  * Data saved for ListenBrainz | ||||
|  */ | ||||
| export interface StoreData { | ||||
|   listenedAt: number; | ||||
|   title: string; | ||||
|   artists: string; | ||||
|   duration: number; | ||||
| } | ||||
							
								
								
									
										52
									
								
								src/features/logger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| import { IpcMain, ipcMain, IpcMainEvent, ipcRenderer } from "electron"; | ||||
| import { globalEvents } from "../constants/globalEvents"; | ||||
|  | ||||
| export class Logger { | ||||
|   /** | ||||
|    * Subscribe to watch for logs from the IPC client | ||||
|    * @param ipcMain main thread IPC client so we can subscribe to events | ||||
|    */ | ||||
|   public static watch(ipcMain: IpcMain) { | ||||
|     ipcMain.on( | ||||
|       globalEvents.log, | ||||
|       (event: IpcMainEvent | { content: string; message: string }, message) => { | ||||
|         const { content, object } = message ?? event; | ||||
|         this.logToSTDOut(content, object); | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|   /** | ||||
|    * Log content to STDOut | ||||
|    * @param content | ||||
|    * @param object js(on) object that will be prettyPrinted | ||||
|    */ | ||||
|   public static log(content: string, object: object = {}) { | ||||
|     if (ipcRenderer) { | ||||
|       ipcRenderer.send(globalEvents.log, { content, object }); | ||||
|     } else { | ||||
|       ipcMain.emit(globalEvents.log, { content, object }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Log content to STDOut and use the provided alert function to alert | ||||
|    * @param content | ||||
|    * @param object js(on) object that will be prettyPrinted | ||||
|    */ | ||||
|   // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
|   public static alert(content: string, object: any = {}, alert?: (msg: string) => void) { | ||||
|     Logger.log(content, object); | ||||
|     if (alert) { | ||||
|       alert(`${content} \n\nwith details: \n${JSON.stringify(object, null, 2)}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Log to STDOut | ||||
|    * @param content | ||||
|    * @param object | ||||
|    */ | ||||
|   private static logToSTDOut(content: string, object = {}) { | ||||
|     console.log(content, Object.keys(object).length > 0 ? JSON.stringify(object, null, 2) : ""); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/features/sharingService/sharingService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| export class SharingService { | ||||
|   /** | ||||
|    * Retrieve the universal link given a regular track link | ||||
|    * @param currentUrl | ||||
|    * @returns | ||||
|    */ | ||||
|   public static getUniversalLink(currentUrl: string): string { | ||||
|     return `${currentUrl}?u`; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/features/theming/theming.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| import fs from "fs"; | ||||
| import { settings } from "../../constants/settings"; | ||||
| import { settingsStore } from "../../scripts/settings"; | ||||
| import { Logger } from "../logger"; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| export function addCustomCss(app: any) { | ||||
|   window.addEventListener("DOMContentLoaded", () => { | ||||
|     const selectedTheme = settingsStore.get<string, string>(settings.theme); | ||||
|     if (selectedTheme !== "none") { | ||||
|       const userThemePath = `${app.getPath("userData")}/themes/${selectedTheme}`; | ||||
|       const resourcesThemePath = `${process.resourcesPath}/${selectedTheme}`; | ||||
|       const themeFile = fs.existsSync(userThemePath) ? userThemePath : resourcesThemePath; | ||||
|       fs.readFile(themeFile, "utf-8", (err, data) => { | ||||
|         if (err) { | ||||
|           Logger.alert("An error ocurred reading the theme file.", err, alert); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const themeStyle = document.createElement("style"); | ||||
|         themeStyle.innerHTML = data; | ||||
|         document.head.appendChild(themeStyle); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // read customCSS (it will override the theme) | ||||
|     const style = document.createElement("style"); | ||||
|     style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n"); | ||||
|     document.head.appendChild(style); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/features/time/parse.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| /** | ||||
|  * Convert a HH:MM:SS string (or variants such as MM:SS or SS) to plain seconds | ||||
|  * @param duration in HH:MM:SS format | ||||
|  * @returns number of seconds in duration | ||||
|  */ | ||||
| export const convertDurationToSeconds = (duration: string) => { | ||||
|   return duration | ||||
|     .split(":") | ||||
|     .reverse() | ||||
|     .map((val) => Number(val)) | ||||
|     .reduce((previous, current, index) => { | ||||
|       return index === 0 ? current : previous + current * Math.pow(60, index); | ||||
|     }, 0); | ||||
| }; | ||||
							
								
								
									
										221
									
								
								src/main.js
									
									
									
									
									
								
							
							
						
						| @@ -1,221 +0,0 @@ | ||||
| require("@electron/remote/main").initialize(); | ||||
| const { | ||||
|   app, | ||||
|   BrowserWindow, | ||||
|   components, | ||||
|   globalShortcut, | ||||
|   ipcMain, | ||||
|   protocol, | ||||
|   session, | ||||
| } = require("electron"); | ||||
| const { | ||||
|   settings, | ||||
|   store, | ||||
|   createSettingsWindow, | ||||
|   showSettingsWindow, | ||||
|   closeSettingsWindow, | ||||
|   hideSettingsWindow, | ||||
| } = require("./scripts/settings"); | ||||
| const { addTray, refreshTray } = require("./scripts/tray"); | ||||
| const { addMenu } = require("./scripts/menu"); | ||||
| const path = require("path"); | ||||
| const tidalUrl = "https://listen.tidal.com"; | ||||
| const expressModule = require("./scripts/express"); | ||||
| const mediaKeys = require("./constants/mediaKeys"); | ||||
| const mediaInfoModule = require("./scripts/mediaInfo"); | ||||
| const discordModule = require("./scripts/discord"); | ||||
| const globalEvents = require("./constants/globalEvents"); | ||||
| const flagValues = require("./constants/flags"); | ||||
|  | ||||
| let mainWindow; | ||||
| let icon = path.join(__dirname, "../assets/icon.png"); | ||||
| const PROTOCOL_PREFIX = "tidal"; | ||||
|  | ||||
| setFlags(); | ||||
|  | ||||
| function setFlags() { | ||||
|   const flags = store.get().flags; | ||||
|   if (flags) { | ||||
|     for (const [key, value] of Object.entries(flags)) { | ||||
|       if (value) { | ||||
|         flagValues[key].forEach((flag) => { | ||||
|           console.log(`enabling command line switch ${flag.flag} with value ${flag.value}`); | ||||
|           app.commandLine.appendSwitch(flag.flag, flag.value); | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Fix Display Compositor issue. | ||||
|    */ | ||||
|   app.commandLine.appendSwitch("disable-seccomp-filter-sandbox"); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Update the menuBarVisbility according to the store value | ||||
|  * | ||||
|  */ | ||||
| function syncMenuBarWithStore() { | ||||
|   const fixedMenuBar = store.get(settings.menuBar); | ||||
|  | ||||
|   mainWindow.autoHideMenuBar = !fixedMenuBar; | ||||
|   mainWindow.setMenuBarVisibility(fixedMenuBar); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Determine whether the current window is the main window | ||||
|  * if singleInstance is requested. | ||||
|  * If singleInstance isn't requested simply return true | ||||
|  * @returns true if singInstance is not requested, otherwise true/false based on whether the current window is the main window | ||||
|  */ | ||||
| function isMainInstanceOrMultipleInstancesAllowed() { | ||||
|   if (store.get(settings.singleInstance)) { | ||||
|     const gotTheLock = app.requestSingleInstanceLock(); | ||||
|  | ||||
|     if (!gotTheLock) { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| function createWindow(options = {}) { | ||||
|   // Create the browser window. | ||||
|   mainWindow = new BrowserWindow({ | ||||
|     x: options.x, | ||||
|     y: options.y, | ||||
|     width: store && store.get(settings.windowBounds.width), | ||||
|     height: store && store.get(settings.windowBounds.height), | ||||
|     icon, | ||||
|     backgroundColor: options.backgroundColor, | ||||
|     autoHideMenuBar: true, | ||||
|     webPreferences: { | ||||
|       sandbox: false, | ||||
|       preload: path.join(__dirname, "preload.js"), | ||||
|       plugins: true, | ||||
|       devTools: true, // I like tinkering, others might too | ||||
|     }, | ||||
|   }); | ||||
|   require("@electron/remote/main").enable(mainWindow.webContents); | ||||
|   registerHttpProtocols(); | ||||
|   syncMenuBarWithStore(); | ||||
|  | ||||
|   // load the Tidal website | ||||
|   mainWindow.loadURL(tidalUrl); | ||||
|  | ||||
|   if (store.get(settings.disableBackgroundThrottle)) { | ||||
|     // prevent setInterval lag | ||||
|     mainWindow.webContents.setBackgroundThrottling(false); | ||||
|   } | ||||
|  | ||||
|   // run stuff after first load | ||||
|   mainWindow.webContents.once("did-finish-load", () => {}); | ||||
|  | ||||
|   mainWindow.on("close", function (event) { | ||||
|     if (!app.isQuiting && store.get(settings.minimizeOnClose)) { | ||||
|       event.preventDefault(); | ||||
|       mainWindow.hide(); | ||||
|       refreshTray(mainWindow); | ||||
|     } | ||||
|     return false; | ||||
|   }); | ||||
|   // Emitted when the window is closed. | ||||
|   mainWindow.on("closed", function () { | ||||
|     closeSettingsWindow(); | ||||
|     app.quit(); | ||||
|   }); | ||||
|   mainWindow.on("resize", () => { | ||||
|     let { width, height } = mainWindow.getBounds(); | ||||
|  | ||||
|     store.set(settings.windowBounds.root, { width, height }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function registerHttpProtocols() { | ||||
|   protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request, _callback) => { | ||||
|     mainWindow.loadURL(`${tidalUrl}/${request.url.substring(PROTOCOL_PREFIX.length + 3)}`); | ||||
|   }); | ||||
|   if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) { | ||||
|     app.setAsDefaultProtocolClient(PROTOCOL_PREFIX); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function addGlobalShortcuts() { | ||||
|   Object.keys(mediaKeys).forEach((key) => { | ||||
|     globalShortcut.register(`${key}`, () => { | ||||
|       mainWindow.webContents.send("globalEvent", `${mediaKeys[key]}`); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // This method will be called when Electron has finished | ||||
| // initialization and is ready to create browser windows. | ||||
| // Some APIs can only be used after this event occurs. | ||||
| app.on("ready", async () => { | ||||
|   if (isMainInstanceOrMultipleInstancesAllowed()) { | ||||
|     await components.whenReady(); | ||||
|  | ||||
|     // Adblock | ||||
|     if (store.get(settings.adBlock)) { | ||||
|       const filter = { urls: ["https://listen.tidal.com/*"] }; | ||||
|       session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => { | ||||
|         if (details.url.match(/\/users\/.*\d\?country/)) callback({ cancel: true }); | ||||
|         else callback({ cancel: false }); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     createWindow(); | ||||
|     addMenu(mainWindow); | ||||
|     createSettingsWindow(); | ||||
|     addGlobalShortcuts(); | ||||
|     store.get(settings.trayIcon) && addTray(mainWindow, { icon }) && refreshTray(); | ||||
|     store.get(settings.api) && expressModule.run(mainWindow); | ||||
|     store.get(settings.enableDiscord) && discordModule.initRPC(); | ||||
|     // mainWindow.webContents.openDevTools(); | ||||
|   } else { | ||||
|     app.quit(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| app.on("activate", function () { | ||||
|   // 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) { | ||||
|     createWindow(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| app.on("browser-window-created", (_, window) => { | ||||
|   require("@electron/remote/main").enable(window.webContents); | ||||
| }); | ||||
|  | ||||
| // IPC | ||||
| ipcMain.on(globalEvents.updateInfo, (_event, arg) => { | ||||
|   mediaInfoModule.update(arg); | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.hideSettings, (_event, _arg) => { | ||||
|   hideSettingsWindow(); | ||||
| }); | ||||
| ipcMain.on(globalEvents.showSettings, (_event, _arg) => { | ||||
|   showSettingsWindow(); | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.refreshMenuBar, (_event, _arg) => { | ||||
|   syncMenuBarWithStore(); | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.storeChanged, (_event, _arg) => { | ||||
|   syncMenuBarWithStore(); | ||||
|  | ||||
|   if (store.get(settings.enableDiscord) && !discordModule.rpc) { | ||||
|     discordModule.initRPC(); | ||||
|   } else if (!store.get(settings.enableDiscord) && discordModule.rpc) { | ||||
|     discordModule.unRPC(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.error, (event, _arg) => { | ||||
|   console.log(event); | ||||
| }); | ||||
							
								
								
									
										261
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,261 @@ | ||||
| import { enable, initialize } from "@electron/remote/main"; | ||||
| import { BrowserWindow, app, components, ipcMain, session } from "electron"; | ||||
| import path from "path"; | ||||
| import { globalEvents } from "./constants/globalEvents"; | ||||
| import { settings } from "./constants/settings"; | ||||
| import { startApi } from "./features/api"; | ||||
| import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags"; | ||||
| import { | ||||
|   acquireInhibitorIfInactive, | ||||
|   releaseInhibitorIfActive, | ||||
| } from "./features/idleInhibitor/idleInhibitor"; | ||||
| import { Logger } from "./features/logger"; | ||||
| import { SharingService } from "./features/sharingService/sharingService"; | ||||
| import { MediaInfo } from "./models/mediaInfo"; | ||||
| import { MediaStatus } from "./models/mediaStatus"; | ||||
| import { initRPC, rpc, unRPC } from "./scripts/discord"; | ||||
| import { updateMediaInfo } from "./scripts/mediaInfo"; | ||||
| import { addMenu } from "./scripts/menu"; | ||||
| import { | ||||
|   closeSettingsWindow, | ||||
|   createSettingsWindow, | ||||
|   hideSettingsWindow, | ||||
|   settingsStore, | ||||
|   showSettingsWindow, | ||||
| } from "./scripts/settings"; | ||||
| import { addTray, refreshTray } from "./scripts/tray"; | ||||
| let mainInhibitorId = -1; | ||||
|  | ||||
| let mainWindow: BrowserWindow; | ||||
| const icon = path.join(__dirname, "../assets/icon.png"); | ||||
| const PROTOCOL_PREFIX = "tidal"; | ||||
| const windowPreferences = { | ||||
|   sandbox: false, | ||||
|   plugins: true, | ||||
|   devTools: true, // I like tinkering, others might too | ||||
| }; | ||||
|  | ||||
| setDefaultFlags(app); | ||||
| setManagedFlagsFromSettings(app); | ||||
|  | ||||
| const tidalUrl = | ||||
|   settingsStore.get<string, string>(settings.advanced.tidalUrl) || "https://listen.tidal.com"; | ||||
|  | ||||
| /** | ||||
|  * Update the menuBarVisibility according to the store value | ||||
|  * | ||||
|  */ | ||||
| function syncMenuBarWithStore() { | ||||
|   const fixedMenuBar = !!settingsStore.get(settings.menuBar); | ||||
|  | ||||
|   mainWindow.autoHideMenuBar = !fixedMenuBar; | ||||
|   mainWindow.setMenuBarVisibility(fixedMenuBar); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @returns true/false based on whether the current window is the main window | ||||
|  */ | ||||
| function isMainInstance() { | ||||
|   return app.requestSingleInstanceLock(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @returns true/false based on whether multiple instances are allowed | ||||
|  */ | ||||
| function isMultipleInstancesAllowed() { | ||||
|   return !settingsStore.get(settings.singleInstance); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param args the arguments passed to the app | ||||
|  * @returns the custom protocol url if it exists, otherwise null | ||||
|  */ | ||||
| function getCustomProtocolUrl(args: string[]) { | ||||
|   const customProtocolArg = args.find((arg) => arg.startsWith(PROTOCOL_PREFIX)); | ||||
|  | ||||
|   if (!customProtocolArg) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return tidalUrl + "/" + customProtocolArg.substring(PROTOCOL_PREFIX.length + 3); | ||||
| } | ||||
|  | ||||
| function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { | ||||
|   // Create the browser window. | ||||
|   mainWindow = new BrowserWindow({ | ||||
|     x: options.x, | ||||
|     y: options.y, | ||||
|     width: settingsStore?.get(settings.windowBounds.width), | ||||
|     height: settingsStore?.get(settings.windowBounds.height), | ||||
|     icon, | ||||
|     backgroundColor: options.backgroundColor, | ||||
|     autoHideMenuBar: true, | ||||
|     webPreferences: { | ||||
|       ...windowPreferences, | ||||
|       ...{ | ||||
|         preload: path.join(__dirname, "preload.js"), | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   enable(mainWindow.webContents); | ||||
|   registerHttpProtocols(); | ||||
|   syncMenuBarWithStore(); | ||||
|  | ||||
|   // find the custom protocol argument | ||||
|   const customProtocolUrl = getCustomProtocolUrl(process.argv); | ||||
|  | ||||
|   if (customProtocolUrl) { | ||||
|     // load the url received from the custom protocol | ||||
|     mainWindow.loadURL(customProtocolUrl); | ||||
|   } else { | ||||
|     // load the Tidal website | ||||
|     mainWindow.loadURL(tidalUrl); | ||||
|   } | ||||
|  | ||||
|   if (settingsStore.get(settings.disableBackgroundThrottle)) { | ||||
|     // prevent setInterval lag | ||||
|     mainWindow.webContents.setBackgroundThrottling(false); | ||||
|   } | ||||
|  | ||||
|   mainWindow.on("close", function (event: CloseEvent) { | ||||
|     if (settingsStore.get(settings.minimizeOnClose)) { | ||||
|       event.preventDefault(); | ||||
|       mainWindow.hide(); | ||||
|       refreshTray(mainWindow); | ||||
|     } | ||||
|     return false; | ||||
|   }); | ||||
|  | ||||
|   // Emitted when the window is closed. | ||||
|   mainWindow.on("closed", function () { | ||||
|     releaseInhibitorIfActive(mainInhibitorId); | ||||
|     closeSettingsWindow(); | ||||
|     app.quit(); | ||||
|   }); | ||||
|   mainWindow.on("resize", () => { | ||||
|     const { width, height } = mainWindow.getBounds(); | ||||
|     settingsStore.set(settings.windowBounds.root, { width, height }); | ||||
|   }); | ||||
|   mainWindow.webContents.setWindowOpenHandler(() => { | ||||
|     return { | ||||
|       action: "allow", | ||||
|       overrideBrowserWindowOptions: { | ||||
|         webPreferences: { | ||||
|           sandbox: false, | ||||
|           plugins: true, | ||||
|           devTools: true, // I like tinkering, others might too | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function registerHttpProtocols() { | ||||
|   if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) { | ||||
|     app.setAsDefaultProtocolClient(PROTOCOL_PREFIX); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // This method will be called when Electron has finished | ||||
| // initialization and is ready to create browser windows. | ||||
| // Some APIs can only be used after this event occurs. | ||||
| app.on("ready", async () => { | ||||
|   // check if the app is the main instance and multiple instances are not allowed | ||||
|   if (isMainInstance() && !isMultipleInstancesAllowed()) { | ||||
|     app.on("second-instance", (_, commandLine) => { | ||||
|       const customProtocolUrl = getCustomProtocolUrl(commandLine); | ||||
|  | ||||
|       if (customProtocolUrl) { | ||||
|         mainWindow.loadURL(customProtocolUrl); | ||||
|       } | ||||
|  | ||||
|       if (mainWindow) { | ||||
|         if (mainWindow.isMinimized()) mainWindow.restore(); | ||||
|         mainWindow.focus(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (isMainInstance() || isMultipleInstancesAllowed()) { | ||||
|     await components.whenReady(); | ||||
|     initialize(); | ||||
|  | ||||
|     // Adblock | ||||
|     if (settingsStore.get(settings.adBlock)) { | ||||
|       const filter = { urls: ["https://listen.tidal.com/*"] }; | ||||
|       session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => { | ||||
|         if (details.url.match(/\/users\/.*\d\?country/)) callback({ cancel: true }); | ||||
|         else callback({ cancel: false }); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     Logger.log("components ready:", components.status()); | ||||
|  | ||||
|     createWindow(); | ||||
|     addMenu(mainWindow); | ||||
|     createSettingsWindow(); | ||||
|     if (settingsStore.get(settings.trayIcon)) { | ||||
|       addTray(mainWindow, { icon }); | ||||
|       refreshTray(mainWindow); | ||||
|     } | ||||
|     settingsStore.get(settings.api) && startApi(mainWindow); | ||||
|     settingsStore.get(settings.enableDiscord) && initRPC(); | ||||
|   } else { | ||||
|     app.quit(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| app.on("activate", function () { | ||||
|   // 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) { | ||||
|     createWindow(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| app.on("browser-window-created", (_, window) => { | ||||
|   enable(window.webContents); | ||||
| }); | ||||
|  | ||||
| // IPC | ||||
| ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => { | ||||
|   updateMediaInfo(arg); | ||||
|   if (arg.status === MediaStatus.playing) { | ||||
|     mainInhibitorId = acquireInhibitorIfInactive(mainInhibitorId); | ||||
|   } else { | ||||
|     releaseInhibitorIfActive(mainInhibitorId); | ||||
|     mainInhibitorId = -1; | ||||
|   } | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.hideSettings, () => { | ||||
|   hideSettingsWindow(); | ||||
| }); | ||||
| ipcMain.on(globalEvents.showSettings, () => { | ||||
|   showSettingsWindow(); | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.refreshMenuBar, () => { | ||||
|   syncMenuBarWithStore(); | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.storeChanged, () => { | ||||
|   syncMenuBarWithStore(); | ||||
|  | ||||
|   if (settingsStore.get(settings.enableDiscord) && !rpc) { | ||||
|     initRPC(); | ||||
|   } else if (!settingsStore.get(settings.enableDiscord) && rpc) { | ||||
|     unRPC(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| ipcMain.on(globalEvents.error, (event) => { | ||||
|   console.log(event); | ||||
| }); | ||||
|  | ||||
| ipcMain.handle(globalEvents.getUniversalLink, async (event, url) => { | ||||
|   return SharingService.getUniversalLink(url); | ||||
| }); | ||||
|  | ||||
| Logger.watch(ipcMain); | ||||
							
								
								
									
										19
									
								
								src/models/mediaInfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| import { MediaPlayerInfo } from "./mediaPlayerInfo"; | ||||
| import { MediaStatus } from "./mediaStatus"; | ||||
|  | ||||
| export interface MediaInfo { | ||||
|   title: string; | ||||
|   artists: string; | ||||
|   album: string; | ||||
|   icon: string; | ||||
|   status: MediaStatus; | ||||
|   url: string; | ||||
|   playingFrom: string; | ||||
|   current: string; | ||||
|   currentInSeconds?: number; | ||||
|   duration: string; | ||||
|   durationInSeconds?: number; | ||||
|   image: string; | ||||
|   favorite: boolean; | ||||
|   player?: MediaPlayerInfo; | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/models/mediaPlayerInfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| import { RepeatState } from "./repeatState"; | ||||
| import { MediaStatus } from "./mediaStatus"; | ||||
|  | ||||
| export interface MediaPlayerInfo { | ||||
|   status: MediaStatus; | ||||
|   shuffle: boolean; | ||||
|   repeat: RepeatState; | ||||
| } | ||||
							
								
								
									
										4
									
								
								src/models/mediaStatus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| export enum MediaStatus { | ||||
|   playing = "playing", | ||||
|   paused = "paused", | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/models/repeatState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| export enum RepeatState { | ||||
|   off = "off", | ||||
|   all = "all", | ||||
|   single = "single", | ||||
| } | ||||
| Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 32 KiB | 
| @@ -1,149 +0,0 @@ | ||||
| let trayIcon, | ||||
|   minimizeOnClose, | ||||
|   mpris, | ||||
|   enableCustomHotkeys, | ||||
|   enableDiscord, | ||||
|   skipArtists, | ||||
|   notifications, | ||||
|   playBackControl, | ||||
|   api, | ||||
|   port, | ||||
|   menuBar, | ||||
|   skippedArtists, | ||||
|   adBlock, | ||||
|   disableBackgroundThrottle, | ||||
|   singleInstance, | ||||
|   disableHardwareMediaKeys, | ||||
|   gpuRasterization; | ||||
|  | ||||
| const { store, settings } = require("./../../scripts/settings"); | ||||
| const { ipcRenderer } = require("electron"); | ||||
| const globalEvents = require("./../../constants/globalEvents"); | ||||
| const remote = require("@electron/remote"); | ||||
| const { app } = remote; | ||||
| /** | ||||
|  * Sync the UI forms with the current settings | ||||
|  */ | ||||
| function refreshSettings() { | ||||
|   notifications.checked = store.get(settings.notifications); | ||||
|   playBackControl.checked = store.get(settings.playBackControl); | ||||
|   api.checked = store.get(settings.api); | ||||
|   port.value = store.get(settings.apiSettings.port); | ||||
|   menuBar.checked = store.get(settings.menuBar); | ||||
|   trayIcon.checked = store.get(settings.trayIcon); | ||||
|   mpris.checked = store.get(settings.mpris); | ||||
|   enableCustomHotkeys.checked = store.get(settings.enableCustomHotkeys); | ||||
|   enableDiscord.checked = store.get(settings.enableDiscord); | ||||
|   minimizeOnClose.checked = store.get(settings.minimizeOnClose); | ||||
|   skipArtists.checked = store.get(settings.skipArtists); | ||||
|   skippedArtists.value = store.get(settings.skippedArtists).join("\n"); | ||||
|   adBlock.checked = store.get(settings.adBlock); | ||||
|   singleInstance.checked = store.get(settings.singleInstance); | ||||
|   disableHardwareMediaKeys.checked = store.get(settings.flags.disableHardwareMediaKeys); | ||||
|   gpuRasterization.checked = store.get(settings.flags.gpuRasterization); | ||||
|   disableBackgroundThrottle.checked = store.get("disableBackgroundThrottle"); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Open an url in the default browsers | ||||
|  */ | ||||
| function openExternal(url) { | ||||
|   const { shell } = require("electron"); | ||||
|   shell.openExternal(url); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * hide the settings window | ||||
|  */ | ||||
| function hide() { | ||||
|   ipcRenderer.send(globalEvents.hideSettings); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Restart tidal-hifi after changes | ||||
|  */ | ||||
| function restart() { | ||||
|   app.relaunch(); | ||||
|   app.exit(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Bind UI components to functions after DOMContentLoaded | ||||
|  */ | ||||
| window.addEventListener("DOMContentLoaded", () => { | ||||
|   function get(id) { | ||||
|     return document.getElementById(id); | ||||
|   } | ||||
|  | ||||
|   document.getElementById("close").addEventListener("click", hide); | ||||
|   document.getElementById("restart").addEventListener("click", restart); | ||||
|   document.querySelectorAll(".external-link").forEach((elem) => | ||||
|     elem.addEventListener("click", function (event) { | ||||
|       openExternal(event.target.getAttribute("data-url")); | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
|   function addInputListener(source, key) { | ||||
|     source.addEventListener("input", function (_event, _data) { | ||||
|       if (this.value === "on") { | ||||
|         store.set(key, source.checked); | ||||
|       } else { | ||||
|         store.set(key, this.value); | ||||
|       } | ||||
|       ipcRenderer.send(globalEvents.storeChanged); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   function addTextAreaListener(source, key) { | ||||
|     source.addEventListener("input", function (_event, _data) { | ||||
|       store.set(key, source.value.split("\n")); | ||||
|       ipcRenderer.send(globalEvents.storeChanged); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   ipcRenderer.on("refreshData", () => { | ||||
|     refreshSettings(); | ||||
|   }); | ||||
|  | ||||
|   ipcRenderer.on("goToTab", (_, tab) => { | ||||
|     document.getElementById(tab).click(); | ||||
|   }); | ||||
|  | ||||
|   notifications = get("notifications"); | ||||
|   playBackControl = get("playBackControl"); | ||||
|   api = get("apiCheckbox"); | ||||
|   port = get("port"); | ||||
|   menuBar = get("menuBar"); | ||||
|   trayIcon = get("trayIcon"); | ||||
|   minimizeOnClose = get("minimizeOnClose"); | ||||
|   mpris = get("mprisCheckbox"); | ||||
|   enableCustomHotkeys = get("enableCustomHotkeys"); | ||||
|   enableDiscord = get("enableDiscord"); | ||||
|   skipArtists = get("skipArtists"); | ||||
|   skippedArtists = get("skippedArtists"); | ||||
|   adBlock = get("adBlock"); | ||||
|   disableBackgroundThrottle = get("disableBackgroundThrottle"); | ||||
|   singleInstance = get("singleInstance"); | ||||
|   disableHardwareMediaKeys = get("disableHardwareMediaKeys"); | ||||
|   gpuRasterization = get("gpuRasterization"); | ||||
|  | ||||
|   refreshSettings(); | ||||
|  | ||||
|   addInputListener(notifications, settings.notifications); | ||||
|   addInputListener(playBackControl, settings.playBackControl); | ||||
|   addInputListener(api, settings.api); | ||||
|   addInputListener(port, settings.apiSettings.port); | ||||
|   addInputListener(menuBar, settings.menuBar); | ||||
|   addInputListener(trayIcon, settings.trayIcon); | ||||
|   addInputListener(mpris, settings.mpris); | ||||
|   addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys); | ||||
|   addInputListener(enableDiscord, settings.enableDiscord); | ||||
|   addInputListener(minimizeOnClose, settings.minimizeOnClose); | ||||
|   addInputListener(skipArtists, settings.skipArtists); | ||||
|   addTextAreaListener(skippedArtists, settings.skippedArtists); | ||||
|   addInputListener(adBlock, settings.adBlock); | ||||
|   addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle); | ||||
|   addInputListener(singleInstance, settings.singleInstance); | ||||
|   addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys); | ||||
|   addInputListener(gpuRasterization, settings.flags.gpuRasterization); | ||||
| }); | ||||