Compare commits
	
		
			244 Commits
		
	
	
		
			d81912db0c
			...
			controller
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ff4c51234b | |||
|  | c4ee6b51b9 | ||
| 45fe336598 | |||
|  | fe9f50aaf5 | ||
| 755be0ee30 | |||
|  | 9d736b2bd9 | ||
| f4d4b1a1df | |||
|  | 0c27c815f5 | ||
| 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 | 
							
								
								
									
										7
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -9,6 +9,7 @@ on: | |||||||
|     branches-ignore: |     branches-ignore: | ||||||
|       - master |       - master | ||||||
|       - develop |       - develop | ||||||
|  |   workflow_dispatch: | ||||||
| jobs: | jobs: | ||||||
|   build_on_linux: |   build_on_linux: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| @@ -20,7 +21,7 @@ jobs: | |||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 19 |           node-version: 22.4 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|  |  | ||||||
| @@ -30,7 +31,7 @@ jobs: | |||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 19 |           node-version: 22.4 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|  |  | ||||||
| @@ -40,6 +41,6 @@ jobs: | |||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 19 |           node-version: 22.4 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,6 +8,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - master |       - master | ||||||
|  |   workflow_dispatch: | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   build_on_linux: |   build_on_linux: | ||||||
| @@ -20,7 +21,7 @@ jobs: | |||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 19 |           node-version: 22.4 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|       - uses: actions/upload-artifact@master |       - uses: actions/upload-artifact@master | ||||||
| @@ -34,7 +35,7 @@ jobs: | |||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 19 |           node-version: 22.4 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|       - uses: actions/upload-artifact@master |       - uses: actions/upload-artifact@master | ||||||
| @@ -48,7 +49,7 @@ jobs: | |||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 19 |           node-version: 22.4 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|       - uses: actions/upload-artifact@master |       - uses: actions/upload-artifact@master | ||||||
|   | |||||||
| @@ -5,6 +5,9 @@ | |||||||
|   "extends": [ |   "extends": [ | ||||||
|     "stylelint-config-standard-scss" |     "stylelint-config-standard-scss" | ||||||
|   ], |   ], | ||||||
|  |   "ignoreFiles": [ | ||||||
|  |     "src/themes/**.scss" | ||||||
|  |   ], | ||||||
|   "rules": { |   "rules": { | ||||||
|     "prettier/prettier": true, |     "prettier/prettier": true, | ||||||
|     "scss/at-extend-no-missing-placeholder": null, |     "scss/at-extend-no-missing-placeholder": 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 | ||||||
							
								
								
									
										16
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,14 +1,26 @@ | |||||||
| { | { | ||||||
|   "cSpell.words": [ |   "cSpell.words": [ | ||||||
|  |     "Brainz", | ||||||
|  |     "Castlabs", | ||||||
|  |     "Fi's", | ||||||
|     "flac", |     "flac", | ||||||
|  |     "Flatpak", | ||||||
|     "geqnfr", |     "geqnfr", | ||||||
|     "hifi", |     "hifi", | ||||||
|  |     "libnotify", | ||||||
|  |     "listenbrainz", | ||||||
|     "playpause", |     "playpause", | ||||||
|  |     "prs", | ||||||
|     "rescrobbler", |     "rescrobbler", | ||||||
|     "Songwhip", |     "scrobble", | ||||||
|  |     "scrobbling", | ||||||
|     "trackid", |     "trackid", | ||||||
|     "tracklist", |     "tracklist", | ||||||
|     "widevine", |     "widevine", | ||||||
|     "xesam" |     "xesam" | ||||||
|   ] |   ], | ||||||
|  |   "sonarlint.connectedMode.project": { | ||||||
|  |     "connectionId": "public-sonarcloud", | ||||||
|  |     "projectKey": "Mastermindzh_tidal-hifi" | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										159
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -4,6 +4,157 @@ 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/), | 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). | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||||
|  |  | ||||||
|  | ## [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 | ## 5.4.0 | ||||||
|  |  | ||||||
| - Removed Windows builds (from publishes) as they don't work anymore. | - Removed Windows builds (from publishes) as they don't work anymore. | ||||||
| @@ -18,7 +169,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
|  |  | ||||||
| - SPKChaosPhoenix updated the beautiful Tokyo Night theme: | - SPKChaosPhoenix updated the beautiful Tokyo Night theme: | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## 5.2.0 | ## 5.2.0 | ||||||
|  |  | ||||||
| @@ -75,7 +226,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
| - New settings window by BlueManCZ | - New settings window by BlueManCZ | ||||||
| - Fixed the desktop files in electron-builder | - Fixed the desktop files in electron-builder | ||||||
|   - icon is set to new static path based on Arch/Debian |   - 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 | ## 4.1.2 | ||||||
|  |  | ||||||
| @@ -123,7 +274,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
|  |  | ||||||
| - Updated to Electron 15 | - Updated to Electron 15 | ||||||
| - Fixed the develop "build-unpacked" command | - 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) | - Added setting to disable HardwareMediaKeyHandling (defaults to false) | ||||||
|  |  | ||||||
| ## 2.8.2 | ## 2.8.2 | ||||||
| @@ -161,7 +312,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
|  |  | ||||||
| ## 2.5.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) | - Updated dependencies (including electron itself) | ||||||
|  |  | ||||||
| ### known issues | ### known issues | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,20 +1,20 @@ | |||||||
| # 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) |  [](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 | ## Table of Contents | ||||||
|  |  | ||||||
| <!-- toc --> | <!-- toc --> | ||||||
|  |  | ||||||
| - [Tidal-hifi](#tidal-hifi) | - [TIDAL Hi-Fi (Max quality)](#tidal-hi-fi-max-quality) | ||||||
|   - [Table of Contents](#table-of-contents) |   - [Table of Contents](#table-of-contents) | ||||||
|   - [Features](#features) |   - [Features](#features) | ||||||
|   - [Contributions](#contributions) |   - [Contributions](#contributions) | ||||||
|   - [Why did I create tidal-hifi?](#why-did-i-create-tidal-hifi) |   - [Why did I create TIDAL Hi-Fi?](#why-did-i-create-tidal-hi-fi) | ||||||
|     - [Why not extend existing projects?](#why-not-extend-existing-projects) |     - [Why not extend existing projects?](#why-not-extend-existing-projects) | ||||||
|   - [Installation](#installation) |   - [Installation](#installation) | ||||||
|     - [Dependencies](#dependencies) |     - [Dependencies](#dependencies) | ||||||
| @@ -26,8 +26,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect | |||||||
|     - [Using source](#using-source) |     - [Using source](#using-source) | ||||||
|   - [Integrations](#integrations) |   - [Integrations](#integrations) | ||||||
|   - [Known bugs](#known-bugs) |   - [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) |     - [DRM not working on Windows (error S6007)](#drm-not-working-on-windows-error-s6007) | ||||||
|       - [DRM not working on Windows](#drm-not-working-on-windows) |  | ||||||
|   - [Special thanks to](#special-thanks-to) |   - [Special thanks to](#special-thanks-to) | ||||||
|   - [Donations](#donations) |   - [Donations](#donations) | ||||||
|   - [Images](#images) |   - [Images](#images) | ||||||
| @@ -38,28 +37,33 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect | |||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
|  |  | ||||||
| - HiFi playback | - HiFi playback (High & Max settings) | ||||||
| - Notifications | - Notifications | ||||||
| - Custom [theming](./docs/theming.md) | - Custom [theming](./docs/theming.md) | ||||||
| - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) | - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) | ||||||
| - Songwhip.com integration (hotkey `ctrl + w`) | - Better icons thanks to [Papirus-icon-theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/) | ||||||
| - 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/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`) | - [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)) | - 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 | ## Contributions | ||||||
|  |  | ||||||
| To contribute you can use the standard GitHub features (issues, prs, etc) or join the discord server to talk with like-minded individuals. | 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) | -  [Join the Discord server](https://discord.gg/yhNwf4v4He) | ||||||
|  |  | ||||||
| ## Why did I create tidal-hifi? | ## Why did I create TIDAL Hi-Fi? | ||||||
|  |  | ||||||
| I moved from Spotify over to Tidal and found Linux support to be lacking. | 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. | 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? | ### Why not extend existing projects? | ||||||
|  |  | ||||||
| @@ -100,10 +104,10 @@ To install with `snap` you need to download the pre-packaged snap-package from t | |||||||
|  |  | ||||||
| ### Arch Linux | ### 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 | ```sh | ||||||
| trizen tidal-hifi-bin | trizen tidal-hifi-git | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Flatpak | ### Flatpak | ||||||
| @@ -126,40 +130,35 @@ nix-env -iA nixpkgs.tidal-hifi | |||||||
|  |  | ||||||
| To install and work with the code on this project follow these steps: | 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) | - `git clone https://github.com/Mastermindzh/tidal-hifi.git` | ||||||
| - cd tidal-hifi | - `cd tidal-hifi` | ||||||
| - npm install | - `npm install` | ||||||
| - npm start | - `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 | ## 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. | You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| It currently includes: | Integrations with other projects that are not included natively: | ||||||
|  |  | ||||||
| - MPRIS - MPRIS media player controls/status |  | ||||||
| - Discord - Shows what you're listening to on Discord. |  | ||||||
|  |  | ||||||
| Not included: |  | ||||||
|  |  | ||||||
| - [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit) | - [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). | Most Windows users run into DRM issues when trying to use TIDAL Hi-Fi. | ||||||
| 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. |  | ||||||
|  |  | ||||||
| #### DRM not working on Windows |  | ||||||
|  |  | ||||||
| Most Windows users run into DRM issues when trying to use Tidal-hifi. |  | ||||||
| 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. | 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. | ||||||
|  |  | ||||||
|  | Until then you'll have to use the official app unfortunately. | ||||||
|  |  | ||||||
| ## Special thanks to | ## Special thanks to | ||||||
|  |  | ||||||
| - [Castlabs](https://castlabs.com/) | - [Castlabs](https://castlabs.com/) | ||||||
|   | |||||||
							
								
								
									
										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 | 
| Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 4.4 KiB | 
| Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.2 KiB | 
| Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 1.5 KiB | 
| Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 9.0 KiB | 
| Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 2.2 KiB | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 2.6 KiB | 
| @@ -1,7 +1,7 @@ | |||||||
| appId: com.rickvanlieshout.tidal-hifi | appId: com.rickvanlieshout.tidal-hifi | ||||||
| electronVersion: 24.1.2 | electronVersion: 28.1.1 | ||||||
| electronDownload: | electronDownload: | ||||||
|   version: 24.1.2+wvcus |   version: 28.1.1+wvcus | ||||||
|   mirror: https://github.com/castlabs/electron-releases/releases/download/v |   mirror: https://github.com/castlabs/electron-releases/releases/download/v | ||||||
| snap: | snap: | ||||||
|   plugs: |   plugs: | ||||||
| @@ -11,10 +11,16 @@ extraResources: | |||||||
|   - "themes/**" |   - "themes/**" | ||||||
| linux: | linux: | ||||||
|   category: AudioVideo |   category: AudioVideo | ||||||
|   icon: assets/icons |   icon: build/icons | ||||||
|   target: |   target: | ||||||
|     - dir |     - dir | ||||||
|   executableName: tidal-hifi |   executableName: tidal-hifi | ||||||
|  |   executableArgs: | ||||||
|  |     [ | ||||||
|  |       "--enable-features=UseOzonePlatform", | ||||||
|  |       "--ozone-platform-hint=auto", | ||||||
|  |       "--enable-features=WaylandWindowDecorations", | ||||||
|  |     ] | ||||||
|   desktop: |   desktop: | ||||||
|     Encoding: UTF-8 |     Encoding: UTF-8 | ||||||
|     Name: TIDAL Hi-Fi |     Name: TIDAL Hi-Fi | ||||||
|   | |||||||
| 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-menu.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 262 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/new-about.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 41 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/swagger.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 88 KiB | 
| @@ -1,10 +1,10 @@ | |||||||
| # Theming tidal-hifi | # Theming TIDAL Hi-Fi | ||||||
|  |  | ||||||
| ## Table of contents | ## Table of contents | ||||||
|  |  | ||||||
| <!-- toc --> | <!-- toc --> | ||||||
|  |  | ||||||
| - [Theming tidal-hifi](#theming-tidal-hifi) | - [Theming TIDAL Hi-Fi](#theming-TIDAL Hi-Fi) | ||||||
|   - [Table of contents](#table-of-contents) |   - [Table of contents](#table-of-contents) | ||||||
|   - [Custom CSS](#custom-css) |   - [Custom CSS](#custom-css) | ||||||
|   - [config](#config) |   - [config](#config) | ||||||
| @@ -12,7 +12,7 @@ | |||||||
|  |  | ||||||
| <!-- tocstop --> | <!-- tocstop --> | ||||||
|  |  | ||||||
| By default tidal-hifi comes with a few themes. | 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. | You can select these in the settings window under the theming tab as shown below. | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										5079
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										60
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,15 +1,17 @@ | |||||||
| { | { | ||||||
|   "name": "tidal-hifi", |   "name": "tidal-hifi", | ||||||
|   "version": "5.4.0", |   "version": "5.17.0", | ||||||
|   "description": "Tidal on Electron with widevine(hifi) support", |   "description": "Tidal on Electron with widevine(hifi) support", | ||||||
|   "main": "ts-dist/main.js", |   "main": "ts-dist/main.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "electron --inspect=0.0.0.0:5858 .", |     "start": "electron --inspect=0.0.0.0:5858 .", | ||||||
|     "watchStart": "nodemon dist -x \"npm run start\"", |     "watchStart": "nodemon dist -x \"npm run start\"", | ||||||
|     "compile": "tsc && npm run sass-and-copy", |     "compile": "tsc && npm run sass-and-copy", | ||||||
|     "watch": "tsc-watch --onSuccess \"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-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", |     "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", |     "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": "npm run builder -- -c ./build/electron-builder.yml", | ||||||
|     "build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml", |     "build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml", | ||||||
| @@ -38,38 +40,44 @@ | |||||||
|   "homepage": "https://github.com/Mastermindzh/tidal-hifi", |   "homepage": "https://github.com/Mastermindzh/tidal-hifi", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@electron/remote": "^2.0.10", |     "@electron/remote": "^2.1.2", | ||||||
|     "axios": "^1.4.0", |     "@types/swagger-jsdoc": "^6.0.4", | ||||||
|     "discord-rpc": "^4.0.1", |     "@xhayper/discord-rpc": "^1.2.0", | ||||||
|     "electron-store": "^8.1.0", |     "axios": "^1.7.9", | ||||||
|     "express": "^4.18.2", |     "cors": "^2.8.5", | ||||||
|     "hotkeys-js": "^3.11.2", |     "electron-store": "^8.2.0", | ||||||
|  |     "express": "^4.21.2", | ||||||
|  |     "hotkeys-js": "^3.13.9", | ||||||
|     "mpris-service": "^2.1.2", |     "mpris-service": "^2.1.2", | ||||||
|     "request": "^2.88.2", |     "request": "^2.88.2", | ||||||
|     "sass": "^1.64.1" |     "sass": "^1.79.4", | ||||||
|  |     "swagger-ui-express": "^5.0.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@mastermindzh/prettier-config": "^1.0.0", |     "@mastermindzh/prettier-config": "^1.0.0", | ||||||
|     "@types/discord-rpc": "^4.0.5", |     "@types/cors": "^2.8.17", | ||||||
|     "@types/express": "^4.17.17", |     "@types/express": "^4.17.21", | ||||||
|     "@types/node": "^20.4.4", |     "@types/node": "^20.14.10", | ||||||
|     "@types/request": "^2.48.8", |     "@types/request": "^2.48.12", | ||||||
|     "@typescript-eslint/eslint-plugin": "^6.1.0", |     "@types/swagger-ui-express": "^4.1.6", | ||||||
|     "@typescript-eslint/parser": "^6.1.0", |     "@typescript-eslint/eslint-plugin": "^7.16.0", | ||||||
|  |     "@typescript-eslint/parser": "^7.15.0", | ||||||
|     "copyfiles": "^2.4.1", |     "copyfiles": "^2.4.1", | ||||||
|     "electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus", |     "electron": "git+https://github.com/castlabs/electron-releases#v31.1.0+wvcus", | ||||||
|     "electron-builder": "^24.4.0", |     "electron-builder": "~24.9.4", | ||||||
|     "eslint": "^8.45.0", |     "eslint": "^8.57.0", | ||||||
|     "js-yaml": "^4.1.0", |     "js-yaml": "^4.1.0", | ||||||
|     "markdown-toc": "^1.2.0", |     "markdown-toc": "^1.2.0", | ||||||
|     "nodemon": "^3.0.1", |     "nodemon": "^3.1.4", | ||||||
|     "prettier": "^3.0.0", |     "prettier": "^3.3.2", | ||||||
|     "stylelint": "^15.10.2", |     "stylelint": "^16.6.1", | ||||||
|     "stylelint-config-standard": "^34.0.0", |     "stylelint-config-standard": "^36.0.1", | ||||||
|     "stylelint-config-standard-scss": "^10.0.0", |     "stylelint-config-standard-scss": "^13.1.0", | ||||||
|     "stylelint-prettier": "^4.0.0", |     "stylelint-prettier": "^5.0.0", | ||||||
|     "tsc-watch": "^6.0.4", |     "swagger-jsdoc": "^6.2.8", | ||||||
|     "typescript": "^5.1.6" |     "ts-node": "^10.9.2", | ||||||
|  |     "tsc-watch": "^6.2.0", | ||||||
|  |     "typescript": "^5.5.3" | ||||||
|   }, |   }, | ||||||
|   "prettier": "@mastermindzh/prettier-config" |   "prettier": "@mastermindzh/prettier-config" | ||||||
| } | } | ||||||
							
								
								
									
										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 | ||||||
| @@ -17,18 +17,22 @@ | |||||||
|     shuffle: '*[data-test="shuffle"]', |     shuffle: '*[data-test="shuffle"]', | ||||||
|     repeat: '*[data-test="repeat"]', |     repeat: '*[data-test="repeat"]', | ||||||
|     account: '*[data-test^="profile-image-button"]', |     account: '*[data-test^="profile-image-button"]', | ||||||
|  |     settings: '*[data-test^="sidebar-menu-button"]', | ||||||
|  |     openSettings: '*[data-test^="open-settings"]', | ||||||
|     media: '*[data-test="current-media-imagery"]', |     media: '*[data-test="current-media-imagery"]', | ||||||
|     image: "img", |     image: "img", | ||||||
|     current: '*[data-test="current-time"]', |     current: '*[data-test="current-time"]', | ||||||
|     duration: '*[data-test="duration"]', |     duration: '*[class^=playbackControlsContainer] *[data-test="duration"]', | ||||||
|     bar: '*[data-test="progress-bar"]', |     bar: '*[data-test="progress-bar"]', | ||||||
|     footer: "#footerPlayer", |     footer: "#footerPlayer", | ||||||
|     mediaItem: "[data-type='mediaItem']", |     mediaItem: "[data-type='mediaItem']", | ||||||
|     album_header_title: '.header-details [data-test="title"]', |     album_header_title: '*[class^="playingFrom"] span:nth-child(2)', | ||||||
|  |     playingFrom: '*[class^="playingFrom"] span:nth-child(2)', | ||||||
|     currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", |     currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", | ||||||
|     album_name_cell: '[class^="album"]', |     album_name_cell: '[class^="album"]', | ||||||
|     tracklist_row: '[data-test="tracklist-row"]', |     tracklist_row: '[data-test="tracklist-row"]', | ||||||
|     volume: '*[data-test="volume"]', |     volume: '*[data-test="volume"]', | ||||||
|  |     favorite: '*[data-test="footer-favorite-button"]', | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   let results = []; |   let results = []; | ||||||
| @@ -1,4 +1,9 @@ | |||||||
| export const flags: { [key: string]: { flag: string; value?: string }[] } = { | export const flags: { [key: string]: { flag: string; value?: string }[] } = { | ||||||
|   gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }], |   gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }], | ||||||
|   disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }], |   disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }], | ||||||
|  |   enableWaylandSupport: [ | ||||||
|  |     { flag: "enable-features", value: "UseOzonePlatform" }, | ||||||
|  |     { flag: "ozone-platform-hint", value: "auto" }, | ||||||
|  |     { flag: "enable-features", value: "WaylandWindowDecorations" }, | ||||||
|  |   ], | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -10,6 +10,9 @@ export const globalEvents = { | |||||||
|   showSettings: "showSettings", |   showSettings: "showSettings", | ||||||
|   storeChanged: "storeChanged", |   storeChanged: "storeChanged", | ||||||
|   error: "error", |   error: "error", | ||||||
|   whip: "whip", |   getUniversalLink: "getUniversalLink", | ||||||
|   log: "log", |   log: "log", | ||||||
|  |   toggleFavorite: "toggleFavorite", | ||||||
|  |   toggleShuffle: "toggleShuffle", | ||||||
|  |   toggleRepeat: "toggleRepeat", | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -10,26 +10,42 @@ | |||||||
|  */ |  */ | ||||||
| export const settings = { | export const settings = { | ||||||
|   adBlock: "adBlock", |   adBlock: "adBlock", | ||||||
|  |   advanced: { | ||||||
|  |     root: "advanced", | ||||||
|  |     tidalUrl: "advanced.tidalUrl", | ||||||
|  |   }, | ||||||
|   api: "api", |   api: "api", | ||||||
|   apiSettings: { |   apiSettings: { | ||||||
|     root: "apiSettings", |     root: "apiSettings", | ||||||
|     port: "apiSettings.port", |     port: "apiSettings.port", | ||||||
|  |     hostname: "apiSettings.hostname", | ||||||
|   }, |   }, | ||||||
|   customCSS: "customCSS", |   customCSS: "customCSS", | ||||||
|   disableBackgroundThrottle: "disableBackgroundThrottle", |   disableBackgroundThrottle: "disableBackgroundThrottle", | ||||||
|   disableHardwareMediaKeys: "disableHardwareMediaKeys", |   disableHardwareMediaKeys: "disableHardwareMediaKeys", | ||||||
|   enableCustomHotkeys: "enableCustomHotkeys", |   enableCustomHotkeys: "enableCustomHotkeys", | ||||||
|   enableDiscord: "enableDiscord", |   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: { |   ListenBrainz: { | ||||||
|     root: "ListenBrainz", |     root: "ListenBrainz", | ||||||
|     enabled: "ListenBrainz.enabled", |     enabled: "ListenBrainz.enabled", | ||||||
|     api: "ListenBrainz.api", |     api: "ListenBrainz.api", | ||||||
|     token: "ListenBrainz.token", |     token: "ListenBrainz.token", | ||||||
|  |     delay: "ListenBrainz.delay", | ||||||
|   }, |   }, | ||||||
|   flags: { |   flags: { | ||||||
|     root: "flags", |     root: "flags", | ||||||
|     disableHardwareMediaKeys: "flags.disableHardwareMediaKeys", |     disableHardwareMediaKeys: "flags.disableHardwareMediaKeys", | ||||||
|     gpuRasterization: "flags.gpuRasterization", |     gpuRasterization: "flags.gpuRasterization", | ||||||
|  |     enableWaylandSupport: "flags.enableWaylandSupport", | ||||||
|   }, |   }, | ||||||
|   menuBar: "menuBar", |   menuBar: "menuBar", | ||||||
|   minimizeOnClose: "minimizeOnClose", |   minimizeOnClose: "minimizeOnClose", | ||||||
| @@ -39,6 +55,7 @@ export const settings = { | |||||||
|   singleInstance: "singleInstance", |   singleInstance: "singleInstance", | ||||||
|   skipArtists: "skipArtists", |   skipArtists: "skipArtists", | ||||||
|   skippedArtists: "skippedArtists", |   skippedArtists: "skippedArtists", | ||||||
|  |   staticWindowTitle: "staticWindowTitle", | ||||||
|   theme: "theme", |   theme: "theme", | ||||||
|   trayIcon: "trayIcon", |   trayIcon: "trayIcon", | ||||||
|   updateFrequency: "updateFrequency", |   updateFrequency: "updateFrequency", | ||||||
|   | |||||||
| @@ -1,4 +0,0 @@ | |||||||
| export const statuses = { |  | ||||||
|   playing: "playing", |  | ||||||
|   paused: "paused", |  | ||||||
| }; |  | ||||||
| @@ -1,3 +1,3 @@ | |||||||
| export default { | export default { | ||||||
|   name: "tidal-hifi", |   name: "TIDAL Hi-Fi", | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										123
									
								
								src/features/api/features/current.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,123 @@ | |||||||
|  | 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); | ||||||
|  | }; | ||||||
							
								
								
									
										42
									
								
								src/features/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | |||||||
|  | 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); | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										582
									
								
								src/features/api/swagger.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,582 @@ | |||||||
|  | { | ||||||
|  |   "openapi": "3.1.0", | ||||||
|  |   "info": { | ||||||
|  |     "title": "TIDAL Hi-Fi API", | ||||||
|  |     "version": "5.17.0", | ||||||
|  |     "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); | ||||||
|  | }; | ||||||
| @@ -1,26 +1,56 @@ | |||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
| import { settingsStore } from "../../scripts/settings"; | import Store from "electron-store"; | ||||||
| import { settings } from "../../constants/settings"; | import { settings } from "../../constants/settings"; | ||||||
| import { MediaStatus } from "../../models/mediaStatus"; | import { MediaStatus } from "../../models/mediaStatus"; | ||||||
| import Store from "electron-store"; | import { settingsStore } from "../../scripts/settings"; | ||||||
|  | import { Logger } from "../logger"; | ||||||
|  | import { StoreData } from "./models/storeData"; | ||||||
|  |  | ||||||
| const ListenBrainzStore = new Store({name: "listenbrainz"}); | const ListenBrainzStore = new Store({ name: "listenbrainz" }); | ||||||
|  |  | ||||||
|  | export const ListenBrainzConstants = { | ||||||
|  |   oldData: "oldData", | ||||||
|  | }; | ||||||
|  |  | ||||||
| export class ListenBrainz { | export class ListenBrainz { | ||||||
|   /** |   /** | ||||||
|    * Call the ListenBrainz API and create playing now payload |    * 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 title | ||||||
|    * @param artists |    * @param artists | ||||||
|    * @param status |    * @param status | ||||||
|    * @param duration |    * @param duration | ||||||
|    */ |    */ | ||||||
|   public static async scrobble(title: string, artists: string, status: string, duration: number): Promise<any> { |   public static async scrobble( | ||||||
|  |     title: string, | ||||||
|  |     artists: string, | ||||||
|  |     status: string, | ||||||
|  |     duration: number | ||||||
|  |   ): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       if (status == MediaStatus.paused) { |       if (status === MediaStatus.paused) { | ||||||
|         return false; |         return; | ||||||
|       } else { |       } else { | ||||||
|         // Fetches the OldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area |         // Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area | ||||||
|         const OldData = ListenBrainzStore.get("OldData") as string[]; |         const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData; | ||||||
|  |         const tidalUrl = | ||||||
|  |           settingsStore.get<string, string>(settings.advanced.tidalUrl) || | ||||||
|  |           "https://listen.tidal.com"; | ||||||
|         const playing_data = { |         const playing_data = { | ||||||
|           listen_type: "playing_now", |           listen_type: "playing_now", | ||||||
|           payload: [ |           payload: [ | ||||||
| @@ -34,50 +64,72 @@ export class ListenBrainz { | |||||||
|                 }, |                 }, | ||||||
|                 artist_name: artists, |                 artist_name: artists, | ||||||
|                 track_name: title, |                 track_name: title, | ||||||
|                     } |               }, | ||||||
|                 } |             }, | ||||||
|             ] |           ], | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         await axios.post(`${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, playing_data, { |         await axios.post( | ||||||
|             headers:{ |           `${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`, | ||||||
|  |           playing_data, | ||||||
|  |           { | ||||||
|  |             headers: { | ||||||
|               "Content-Type": "application/json", |               "Content-Type": "application/json", | ||||||
|                 "Authorization": `Token ${settingsStore.get(settings.ListenBrainz.token)}` |               Authorization: `Token ${settingsStore.get<string, string>( | ||||||
|  |                 settings.ListenBrainz.token | ||||||
|  |               )}`, | ||||||
|  |             }, | ||||||
|           } |           } | ||||||
|         }); |         ); | ||||||
|         if (!OldData) { |         if (!oldData) { | ||||||
|             ListenBrainzStore.set("OldData", [Math.floor(new Date().getTime() / 1000), title, artists, duration]); |           ListenBrainzStore.set( | ||||||
|         } else if (OldData[1] != title) { |             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 |             // This constructs the data required to scrobble the data after the song finishes | ||||||
|             const scrobble_data = { |             const scrobble_data = { | ||||||
|               listen_type: "single", |               listen_type: "single", | ||||||
|               payload: [ |               payload: [ | ||||||
|                 { |                 { | ||||||
|                         listened_at: OldData[0], |                   listened_at: oldData.listenedAt, | ||||||
|                   track_metadata: { |                   track_metadata: { | ||||||
|                     additional_info: { |                     additional_info: { | ||||||
|                       media_player: "Tidal Hi-Fi", |                       media_player: "Tidal Hi-Fi", | ||||||
|                       submission_client: "Tidal Hi-Fi", |                       submission_client: "Tidal Hi-Fi", | ||||||
|                                 music_service: "listen.tidal.com", |                       music_service: tidalUrl, | ||||||
|                                 duration: OldData[3], |                       duration: oldData.duration, | ||||||
|                     }, |                     }, | ||||||
|                             artist_name: OldData[2], |                     artist_name: oldData.artists, | ||||||
|                             track_name: OldData[1], |                     track_name: oldData.title, | ||||||
|                         } |                   }, | ||||||
|                     } |                 }, | ||||||
|                 ] |               ], | ||||||
|             }; |             }; | ||||||
|             await axios.post(`${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, scrobble_data, { |             await axios.post( | ||||||
|                 headers:{ |               `${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`, | ||||||
|  |               scrobble_data, | ||||||
|  |               { | ||||||
|  |                 headers: { | ||||||
|                   "Content-Type": "application/json", |                   "Content-Type": "application/json", | ||||||
|                     "Authorization": `Token ${settingsStore.get(settings.ListenBrainz.token)}` |                   Authorization: `Token ${settingsStore.get<string, string>( | ||||||
|  |                     settings.ListenBrainz.token | ||||||
|  |                   )}`, | ||||||
|  |                 }, | ||||||
|  |               } | ||||||
|  |             ); | ||||||
|  |             ListenBrainzStore.set( | ||||||
|  |               ListenBrainzConstants.oldData, | ||||||
|  |               this.constructStoreData(title, artists, duration) | ||||||
|  |             ); | ||||||
|           } |           } | ||||||
|             }); |  | ||||||
|             ListenBrainzStore.set("OldData", [Math.floor(new Date().getTime() / 1000), title, artists, duration]); |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.log(JSON.stringify(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; | ||||||
|  | } | ||||||
| @@ -1,33 +1,52 @@ | |||||||
| import { IpcMain, IpcRenderer } from "electron"; | import { IpcMain, ipcMain, IpcMainEvent, ipcRenderer } from "electron"; | ||||||
| import { globalEvents } from "../constants/globalEvents"; | import { globalEvents } from "../constants/globalEvents"; | ||||||
|  |  | ||||||
| export class Logger { | export class Logger { | ||||||
|   /** |  | ||||||
|    * |  | ||||||
|    * @param ipcRenderer renderer IPC client so we can send messages to the main thread |  | ||||||
|    */ |  | ||||||
|   constructor(private ipcRenderer: IpcRenderer) {} |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Subscribe to watch for logs from the IPC client |    * Subscribe to watch for logs from the IPC client | ||||||
|    * @param ipcMain main thread IPC client so we can subscribe to events |    * @param ipcMain main thread IPC client so we can subscribe to events | ||||||
|    */ |    */ | ||||||
|   public static watch(ipcMain: IpcMain) { |   public static watch(ipcMain: IpcMain) { | ||||||
|     ipcMain.on(globalEvents.log, (event, content, object) => { |     ipcMain.on( | ||||||
|       console.log(content, JSON.stringify(object, null, 2)); |       globalEvents.log, | ||||||
|     }); |       (event: IpcMainEvent | { content: string; message: string }, message) => { | ||||||
|  |         const { content, object } = message ?? event; | ||||||
|  |         this.logToSTDOut(content, object); | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Log content to STDOut |    * Log content to STDOut | ||||||
|    * @param content |    * @param content | ||||||
|    * @param object js(on) object that will be prettyPrinted |    * @param object js(on) object that will be prettyPrinted | ||||||
|    */ |    */ | ||||||
|   public log(content: string, object: object = {}) { |   public static log(content: string, object: object = {}) { | ||||||
|     if (this.ipcRenderer) { |     if (ipcRenderer) { | ||||||
|       this.ipcRenderer.send(globalEvents.log, { content, object }); |       ipcRenderer.send(globalEvents.log, { content, object }); | ||||||
|     } else { |     } else { | ||||||
|       console.log(`${content} \n ${JSON.stringify(object, null, 2)}`); |       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`; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| import { ServiceLinks } from "./ServiceLinks"; |  | ||||||
|  |  | ||||||
| export interface Artist { |  | ||||||
|   type: string; |  | ||||||
|   id: number; |  | ||||||
|   path: string; |  | ||||||
|   name: string; |  | ||||||
|   sourceUrl: string; |  | ||||||
|   sourceCountry: string; |  | ||||||
|   url: string; |  | ||||||
|   image: string; |  | ||||||
|   createdAt: string; |  | ||||||
|   updatedAt: string; |  | ||||||
|   refreshedAt: string; |  | ||||||
|   serviceIds: { [key: string]: string }; |  | ||||||
|   orchardId: string; |  | ||||||
|   spotifyId: string; |  | ||||||
|   links: { [key: string]: ServiceLinks[] }; |  | ||||||
|   linksCountries: string[]; |  | ||||||
|   description: string; |  | ||||||
| } |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| export interface ServiceLinks { |  | ||||||
|   link: string; |  | ||||||
|   countries: string[]; |  | ||||||
| } |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| import { Artist } from "./Artist"; |  | ||||||
| import { ServiceLinks } from "./ServiceLinks"; |  | ||||||
|  |  | ||||||
| export interface WhippedResult { |  | ||||||
|   status: string; |  | ||||||
|   data: { |  | ||||||
|     item: { |  | ||||||
|       type: string; |  | ||||||
|       id: number; |  | ||||||
|       path: string; |  | ||||||
|       name: string; |  | ||||||
|       url: string; |  | ||||||
|       sourceUrl: string; |  | ||||||
|       sourceCountry: string; |  | ||||||
|       releaseDate: string; |  | ||||||
|       createdAt: string; |  | ||||||
|       updatedAt: string; |  | ||||||
|       refreshedAt: string; |  | ||||||
|       image: string; |  | ||||||
|       isrc: string; |  | ||||||
|       isExplicit: boolean; |  | ||||||
|       links: { [key: string]: ServiceLinks[] }; |  | ||||||
|       linksCountries: string[]; |  | ||||||
|       artists: Artist[]; |  | ||||||
|     }; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| import { WhippedResult } from "./models/whip"; |  | ||||||
| import axios from "axios"; |  | ||||||
|  |  | ||||||
| export class Songwhip { |  | ||||||
|   /** |  | ||||||
|    * Call the songwhip API and create a shareable songwhip page |  | ||||||
|    * @param currentUrl |  | ||||||
|    * @returns |  | ||||||
|    */ |  | ||||||
|   public static async whip(currentUrl: string): Promise<WhippedResult> { |  | ||||||
|     try { |  | ||||||
|       const response = await axios.post("https://songwhip.com/api/songwhip/create", { |  | ||||||
|         url: currentUrl, |  | ||||||
|         // doesn't actually matter.. returns everything the same way anyway |  | ||||||
|         country: "NL", |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       return response.data; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.log(JSON.stringify(error)); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Transform a songwhip response into a shareable url |  | ||||||
|    * @param response |  | ||||||
|    * @returns |  | ||||||
|    */ |  | ||||||
|   public static getWhipUrl(response: WhippedResult) { |  | ||||||
|     return `https://songwhip.com${response.data.item.url}`; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										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); | ||||||
|  | }; | ||||||
							
								
								
									
										169
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						| @@ -1,64 +1,49 @@ | |||||||
| import { enable, initialize } from "@electron/remote/main"; | import { enable, initialize } from "@electron/remote/main"; | ||||||
| import { | import { BrowserWindow, app, components, ipcMain, session } from "electron"; | ||||||
|   BrowserWindow, |  | ||||||
|   app, |  | ||||||
|   components, |  | ||||||
|   globalShortcut, |  | ||||||
|   ipcMain, |  | ||||||
|   protocol, |  | ||||||
|   session, |  | ||||||
| } from "electron"; |  | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import { flags } from "./constants/flags"; |  | ||||||
| import { globalEvents } from "./constants/globalEvents"; | import { globalEvents } from "./constants/globalEvents"; | ||||||
| import { mediaKeys } from "./constants/mediaKeys"; | 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 { initRPC, rpc, unRPC } from "./scripts/discord"; | ||||||
| import { startExpress } from "./scripts/express"; |  | ||||||
| import { updateMediaInfo } from "./scripts/mediaInfo"; | import { updateMediaInfo } from "./scripts/mediaInfo"; | ||||||
| import { addMenu } from "./scripts/menu"; | import { addMenu } from "./scripts/menu"; | ||||||
| import { | import { | ||||||
|   closeSettingsWindow, |   closeSettingsWindow, | ||||||
|   createSettingsWindow, |   createSettingsWindow, | ||||||
|   hideSettingsWindow, |   hideSettingsWindow, | ||||||
|   showSettingsWindow, |  | ||||||
|   settingsStore, |   settingsStore, | ||||||
|  |   showSettingsWindow, | ||||||
| } from "./scripts/settings"; | } from "./scripts/settings"; | ||||||
| import { settings } from "./constants/settings"; |  | ||||||
| import { addTray, refreshTray } from "./scripts/tray"; | import { addTray, refreshTray } from "./scripts/tray"; | ||||||
| import { MediaInfo } from "./models/mediaInfo"; | let mainInhibitorId = -1; | ||||||
| import { Songwhip } from "./features/songwhip/songwhip"; |  | ||||||
| import { Logger } from "./features/logger"; |  | ||||||
| const tidalUrl = "https://listen.tidal.com"; |  | ||||||
|  |  | ||||||
| initialize(); | initialize(); | ||||||
|  |  | ||||||
| let mainWindow: BrowserWindow; | let mainWindow: BrowserWindow; | ||||||
| const icon = path.join(__dirname, "../assets/icon.png"); | const icon = path.join(__dirname, "../assets/icon.png"); | ||||||
| const PROTOCOL_PREFIX = "tidal"; | const PROTOCOL_PREFIX = "tidal"; | ||||||
|  | const windowPreferences = { | ||||||
|  |   sandbox: false, | ||||||
|  |   plugins: true, | ||||||
|  |   devTools: true, // I like tinkering, others might too | ||||||
|  | }; | ||||||
|  |  | ||||||
| setFlags(); | setDefaultFlags(app); | ||||||
|  | setManagedFlagsFromSettings(app); | ||||||
|  |  | ||||||
| function setFlags() { | const tidalUrl = | ||||||
|   const flagsFromSettings = settingsStore.get(settings.flags.root); |   settingsStore.get<string, string>(settings.advanced.tidalUrl) || "https://listen.tidal.com"; | ||||||
|   if (flagsFromSettings) { |  | ||||||
|     for (const [key, value] of Object.entries(flags)) { |  | ||||||
|       if (value) { |  | ||||||
|         flags[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 |  * Update the menuBarVisibility according to the store value | ||||||
|  * |  * | ||||||
|  */ |  */ | ||||||
| function syncMenuBarWithStore() { | function syncMenuBarWithStore() { | ||||||
| @@ -69,20 +54,31 @@ function syncMenuBarWithStore() { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Determine whether the current window is the main window |  * @returns true/false based on 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() { | function isMainInstance() { | ||||||
|   if (settingsStore.get(settings.singleInstance)) { |   return app.requestSingleInstanceLock(); | ||||||
|     const gotTheLock = app.requestSingleInstanceLock(); | } | ||||||
|  |  | ||||||
|     if (!gotTheLock) { | /** | ||||||
|       return false; |  * @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 true; |   return tidalUrl + "/" + customProtocolArg.substring(PROTOCOL_PREFIX.length + 3); | ||||||
| } | } | ||||||
|  |  | ||||||
| function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { | function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { | ||||||
| @@ -90,24 +86,32 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { | |||||||
|   mainWindow = new BrowserWindow({ |   mainWindow = new BrowserWindow({ | ||||||
|     x: options.x, |     x: options.x, | ||||||
|     y: options.y, |     y: options.y, | ||||||
|     width: settingsStore && settingsStore.get(settings.windowBounds.width), |     width: settingsStore?.get(settings.windowBounds.width), | ||||||
|     height: settingsStore && settingsStore.get(settings.windowBounds.height), |     height: settingsStore?.get(settings.windowBounds.height), | ||||||
|     icon, |     icon, | ||||||
|     backgroundColor: options.backgroundColor, |     backgroundColor: options.backgroundColor, | ||||||
|     autoHideMenuBar: true, |     autoHideMenuBar: true, | ||||||
|     webPreferences: { |     webPreferences: { | ||||||
|       sandbox: false, |       ...windowPreferences, | ||||||
|  |       ...{ | ||||||
|         preload: path.join(__dirname, "preload.js"), |         preload: path.join(__dirname, "preload.js"), | ||||||
|       plugins: true, |       }, | ||||||
|       devTools: true, // I like tinkering, others might too |  | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|   enable(mainWindow.webContents); |   enable(mainWindow.webContents); | ||||||
|   registerHttpProtocols(); |   registerHttpProtocols(); | ||||||
|   syncMenuBarWithStore(); |   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 |     // load the Tidal website | ||||||
|     mainWindow.loadURL(tidalUrl); |     mainWindow.loadURL(tidalUrl); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (settingsStore.get(settings.disableBackgroundThrottle)) { |   if (settingsStore.get(settings.disableBackgroundThrottle)) { | ||||||
|     // prevent setInterval lag |     // prevent setInterval lag | ||||||
| @@ -124,6 +128,7 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { | |||||||
|   }); |   }); | ||||||
|   // Emitted when the window is closed. |   // Emitted when the window is closed. | ||||||
|   mainWindow.on("closed", function () { |   mainWindow.on("closed", function () { | ||||||
|  |     releaseInhibitorIfActive(mainInhibitorId); | ||||||
|     closeSettingsWindow(); |     closeSettingsWindow(); | ||||||
|     app.quit(); |     app.quit(); | ||||||
|   }); |   }); | ||||||
| @@ -131,30 +136,47 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { | |||||||
|     const { width, height } = mainWindow.getBounds(); |     const { width, height } = mainWindow.getBounds(); | ||||||
|     settingsStore.set(settings.windowBounds.root, { width, height }); |     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() { | function registerHttpProtocols() { | ||||||
|   protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request) => { |  | ||||||
|     mainWindow.loadURL(`${tidalUrl}/${request.url.substring(PROTOCOL_PREFIX.length + 3)}`); |  | ||||||
|   }); |  | ||||||
|   if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) { |   if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) { | ||||||
|     app.setAsDefaultProtocolClient(PROTOCOL_PREFIX); |     app.setAsDefaultProtocolClient(PROTOCOL_PREFIX); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function addGlobalShortcuts() { |  | ||||||
|   Object.keys(mediaKeys).forEach((key) => { |  | ||||||
|     globalShortcut.register(`${key}`, () => { |  | ||||||
|       mainWindow.webContents.send("globalEvent", `${(mediaKeys as any)[key]}`); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // This method will be called when Electron has finished | // This method will be called when Electron has finished | ||||||
| // initialization and is ready to create browser windows. | // initialization and is ready to create browser windows. | ||||||
| // Some APIs can only be used after this event occurs. | // Some APIs can only be used after this event occurs. | ||||||
| app.on("ready", async () => { | app.on("ready", async () => { | ||||||
|   if (isMainInstanceOrMultipleInstancesAllowed()) { |   // 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(); |     await components.whenReady(); | ||||||
|  |  | ||||||
|     // Adblock |     // Adblock | ||||||
| @@ -169,12 +191,11 @@ app.on("ready", async () => { | |||||||
|     createWindow(); |     createWindow(); | ||||||
|     addMenu(mainWindow); |     addMenu(mainWindow); | ||||||
|     createSettingsWindow(); |     createSettingsWindow(); | ||||||
|     addGlobalShortcuts(); |  | ||||||
|     if (settingsStore.get(settings.trayIcon)) { |     if (settingsStore.get(settings.trayIcon)) { | ||||||
|       addTray(mainWindow, { icon }); |       addTray(mainWindow, { icon }); | ||||||
|       refreshTray(mainWindow); |       refreshTray(mainWindow); | ||||||
|     } |     } | ||||||
|     settingsStore.get(settings.api) && startExpress(mainWindow); |     settingsStore.get(settings.api) && startApi(mainWindow); | ||||||
|     settingsStore.get(settings.enableDiscord) && initRPC(); |     settingsStore.get(settings.enableDiscord) && initRPC(); | ||||||
|   } else { |   } else { | ||||||
|     app.quit(); |     app.quit(); | ||||||
| @@ -196,6 +217,12 @@ app.on("browser-window-created", (_, window) => { | |||||||
| // IPC | // IPC | ||||||
| ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => { | ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => { | ||||||
|   updateMediaInfo(arg); |   updateMediaInfo(arg); | ||||||
|  |   if (arg.status === MediaStatus.playing) { | ||||||
|  |     mainInhibitorId = acquireInhibitorIfInactive(mainInhibitorId); | ||||||
|  |   } else { | ||||||
|  |     releaseInhibitorIfActive(mainInhibitorId); | ||||||
|  |     mainInhibitorId = -1; | ||||||
|  |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| ipcMain.on(globalEvents.hideSettings, () => { | ipcMain.on(globalEvents.hideSettings, () => { | ||||||
| @@ -223,8 +250,8 @@ ipcMain.on(globalEvents.error, (event) => { | |||||||
|   console.log(event); |   console.log(event); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| ipcMain.handle(globalEvents.whip, async (event, url) => { | ipcMain.handle(globalEvents.getUniversalLink, async (event, url) => { | ||||||
|   return await Songwhip.whip(url); |   return SharingService.getUniversalLink(url); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| Logger.watch(ipcMain); | Logger.watch(ipcMain); | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { MediaPlayerInfo } from "./mediaPlayerInfo"; | ||||||
| import { MediaStatus } from "./mediaStatus"; | import { MediaStatus } from "./mediaStatus"; | ||||||
|  |  | ||||||
| export interface MediaInfo { | export interface MediaInfo { | ||||||
| @@ -7,7 +8,12 @@ export interface MediaInfo { | |||||||
|   icon: string; |   icon: string; | ||||||
|   status: MediaStatus; |   status: MediaStatus; | ||||||
|   url: string; |   url: string; | ||||||
|  |   playingFrom: string; | ||||||
|   current: string; |   current: string; | ||||||
|  |   currentInSeconds?: number; | ||||||
|   duration: string; |   duration: string; | ||||||
|  |   durationInSeconds?: number; | ||||||
|   image: string; |   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; | ||||||
|  | } | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| export interface Options { |  | ||||||
|   title: string; |  | ||||||
|   artists: string; |  | ||||||
|   album: string; |  | ||||||
|   status: string; |  | ||||||
|   url: string; |  | ||||||
|   current: string; |  | ||||||
|   duration: string; |  | ||||||
|   "app-name": string; |  | ||||||
|   image: string; |  | ||||||
|   icon: string; |  | ||||||
| } |  | ||||||
							
								
								
									
										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 | 
| @@ -3,17 +3,40 @@ import { ipcRenderer, shell } from "electron"; | |||||||
| import fs from "fs"; | import fs from "fs"; | ||||||
| import { globalEvents } from "../../constants/globalEvents"; | import { globalEvents } from "../../constants/globalEvents"; | ||||||
| import { settings } from "../../constants/settings"; | import { settings } from "../../constants/settings"; | ||||||
|  | import { Logger } from "../../features/logger"; | ||||||
|  | import { addCustomCss } from "../../features/theming/theming"; | ||||||
| import { settingsStore } from "./../../scripts/settings"; | import { settingsStore } from "./../../scripts/settings"; | ||||||
| import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming"; | import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming"; | ||||||
|  |  | ||||||
|  | // All switches on the settings screen that show additional options based on their state | ||||||
|  | const switchesWithSettings = { | ||||||
|  |   listenBrainz: { | ||||||
|  |     switch: "enableListenBrainz", | ||||||
|  |     classToHide: "listenbrainz__options", | ||||||
|  |     settingsKey: settings.ListenBrainz.enabled, | ||||||
|  |   }, | ||||||
|  |   discord: { | ||||||
|  |     switch: "enableDiscord", | ||||||
|  |     classToHide: "discord_options", | ||||||
|  |     settingsKey: settings.enableDiscord, | ||||||
|  |   }, | ||||||
|  |   discord_show_song: { | ||||||
|  |     switch: "discord_show_song", | ||||||
|  |     classToHide: "discord_show_song_options", | ||||||
|  |     settingsKey: settings.discord.showSong, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
| let adBlock: HTMLInputElement, | let adBlock: HTMLInputElement, | ||||||
|   api: HTMLInputElement, |   api: HTMLInputElement, | ||||||
|  |   channel: HTMLSelectElement, | ||||||
|   customCSS: HTMLInputElement, |   customCSS: HTMLInputElement, | ||||||
|   disableBackgroundThrottle: HTMLInputElement, |   disableBackgroundThrottle: HTMLInputElement, | ||||||
|   disableHardwareMediaKeys: HTMLInputElement, |   disableHardwareMediaKeys: HTMLInputElement, | ||||||
|   enableCustomHotkeys: HTMLInputElement, |   enableCustomHotkeys: HTMLInputElement, | ||||||
|   enableDiscord: HTMLInputElement, |   enableDiscord: HTMLInputElement, | ||||||
|   gpuRasterization: HTMLInputElement, |   gpuRasterization: HTMLInputElement, | ||||||
|  |   hostname: HTMLInputElement, | ||||||
|   menuBar: HTMLInputElement, |   menuBar: HTMLInputElement, | ||||||
|   minimizeOnClose: HTMLInputElement, |   minimizeOnClose: HTMLInputElement, | ||||||
|   mpris: HTMLInputElement, |   mpris: HTMLInputElement, | ||||||
| @@ -23,12 +46,24 @@ let adBlock: HTMLInputElement, | |||||||
|   singleInstance: HTMLInputElement, |   singleInstance: HTMLInputElement, | ||||||
|   skipArtists: HTMLInputElement, |   skipArtists: HTMLInputElement, | ||||||
|   skippedArtists: HTMLInputElement, |   skippedArtists: HTMLInputElement, | ||||||
|  |   staticWindowTitle: HTMLInputElement, | ||||||
|   theme: HTMLSelectElement, |   theme: HTMLSelectElement, | ||||||
|   trayIcon: HTMLInputElement, |   trayIcon: HTMLInputElement, | ||||||
|   updateFrequency: HTMLInputElement, |   updateFrequency: HTMLInputElement, | ||||||
|   enableListenBrainz: HTMLInputElement, |   enableListenBrainz: HTMLInputElement, | ||||||
|   ListenBrainzAPI: HTMLInputElement, |   ListenBrainzAPI: HTMLInputElement, | ||||||
|   ListenBrainzToken: HTMLInputElement; |   ListenBrainzToken: HTMLInputElement, | ||||||
|  |   listenbrainz_delay: HTMLInputElement, | ||||||
|  |   enableWaylandSupport: HTMLInputElement, | ||||||
|  |   discord_details_prefix: HTMLInputElement, | ||||||
|  |   discord_include_timestamps: HTMLInputElement, | ||||||
|  |   discord_button_text: HTMLInputElement, | ||||||
|  |   discord_show_song: HTMLInputElement, | ||||||
|  |   discord_show_idle: HTMLInputElement, | ||||||
|  |   discord_idle_text: HTMLInputElement, | ||||||
|  |   discord_using_text: HTMLInputElement; | ||||||
|  |  | ||||||
|  | addCustomCss(app); | ||||||
|  |  | ||||||
| function getThemeFiles() { | function getThemeFiles() { | ||||||
|   const selectElement = document.getElementById("themesList") as HTMLSelectElement; |   const selectElement = document.getElementById("themesList") as HTMLSelectElement; | ||||||
| @@ -57,6 +92,7 @@ function handleFileUploads() { | |||||||
|   const fileMessage = document.getElementById("file-message"); |   const fileMessage = document.getElementById("file-message"); | ||||||
|   fileMessage.innerText = "or drag and drop files here"; |   fileMessage.innerText = "or drag and drop files here"; | ||||||
|  |  | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|   document.getElementById("theme-files").addEventListener("change", function (e: any) { |   document.getElementById("theme-files").addEventListener("change", function (e: any) { | ||||||
|     Array.from(e.target.files).forEach((file: File) => { |     Array.from(e.target.files).forEach((file: File) => { | ||||||
|       const destination = `${app.getPath("userData")}/themes/${file.name}`; |       const destination = `${app.getPath("userData")}/themes/${file.name}`; | ||||||
| @@ -67,18 +103,36 @@ function handleFileUploads() { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * hide or unhide an element | ||||||
|  |  * @param checked | ||||||
|  |  * @param toggleOptions | ||||||
|  |  */ | ||||||
|  | function setElementHidden( | ||||||
|  |   checked: boolean, | ||||||
|  |   toggleOptions: { switch: string; classToHide: string } | ||||||
|  | ) { | ||||||
|  |   const element = document.getElementById(toggleOptions.classToHide); | ||||||
|  |  | ||||||
|  |   checked ? element.classList.remove("hidden") : element.classList.add("hidden"); | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Sync the UI forms with the current settings |  * Sync the UI forms with the current settings | ||||||
|  */ |  */ | ||||||
| function refreshSettings() { | function refreshSettings() { | ||||||
|  |   try { | ||||||
|     adBlock.checked = settingsStore.get(settings.adBlock); |     adBlock.checked = settingsStore.get(settings.adBlock); | ||||||
|     api.checked = settingsStore.get(settings.api); |     api.checked = settingsStore.get(settings.api); | ||||||
|  |     channel.value = settingsStore.get(settings.advanced.tidalUrl); | ||||||
|     customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n"); |     customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n"); | ||||||
|     disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle); |     disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle); | ||||||
|     disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys); |     disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys); | ||||||
|     enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys); |     enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys); | ||||||
|     enableDiscord.checked = settingsStore.get(settings.enableDiscord); |     enableDiscord.checked = settingsStore.get(settings.enableDiscord); | ||||||
|  |     enableWaylandSupport.checked = settingsStore.get(settings.flags.enableWaylandSupport); | ||||||
|     gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization); |     gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization); | ||||||
|  |     hostname.value = settingsStore.get(settings.apiSettings.hostname); | ||||||
|     menuBar.checked = settingsStore.get(settings.menuBar); |     menuBar.checked = settingsStore.get(settings.menuBar); | ||||||
|     minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose); |     minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose); | ||||||
|     mpris.checked = settingsStore.get(settings.mpris); |     mpris.checked = settingsStore.get(settings.mpris); | ||||||
| @@ -87,13 +141,30 @@ function refreshSettings() { | |||||||
|     port.value = settingsStore.get(settings.apiSettings.port); |     port.value = settingsStore.get(settings.apiSettings.port); | ||||||
|     singleInstance.checked = settingsStore.get(settings.singleInstance); |     singleInstance.checked = settingsStore.get(settings.singleInstance); | ||||||
|     skipArtists.checked = settingsStore.get(settings.skipArtists); |     skipArtists.checked = settingsStore.get(settings.skipArtists); | ||||||
|   theme.value = settingsStore.get(settings.theme); |  | ||||||
|     skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n"); |     skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n"); | ||||||
|  |     staticWindowTitle.checked = settingsStore.get(settings.staticWindowTitle); | ||||||
|  |     theme.value = settingsStore.get(settings.theme); | ||||||
|     trayIcon.checked = settingsStore.get(settings.trayIcon); |     trayIcon.checked = settingsStore.get(settings.trayIcon); | ||||||
|     updateFrequency.value = settingsStore.get(settings.updateFrequency); |     updateFrequency.value = settingsStore.get(settings.updateFrequency); | ||||||
|     enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled); |     enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled); | ||||||
|     ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api); |     ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api); | ||||||
|     ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token); |     ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token); | ||||||
|  |     listenbrainz_delay.value = settingsStore.get(settings.ListenBrainz.delay); | ||||||
|  |     discord_details_prefix.value = settingsStore.get(settings.discord.detailsPrefix); | ||||||
|  |     discord_include_timestamps.checked = settingsStore.get(settings.discord.includeTimestamps); | ||||||
|  |     discord_button_text.value = settingsStore.get(settings.discord.buttonText); | ||||||
|  |     discord_show_song.checked = settingsStore.get(settings.discord.showSong); | ||||||
|  |     discord_show_idle.checked = settingsStore.get(settings.discord.showIdle); | ||||||
|  |     discord_idle_text.value = settingsStore.get(settings.discord.idleText); | ||||||
|  |     discord_using_text.value = settingsStore.get(settings.discord.usingText); | ||||||
|  |  | ||||||
|  |     // set state of all switches with additional settings | ||||||
|  |     Object.values(switchesWithSettings).forEach((settingSwitch) => { | ||||||
|  |       setElementHidden(settingsStore.get(settingSwitch.settingsKey), settingSwitch); | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     Logger.log("Refreshing settings failed.", error); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -110,14 +181,6 @@ function hide() { | |||||||
|   ipcRenderer.send(globalEvents.hideSettings); |   ipcRenderer.send(globalEvents.hideSettings); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Restart tidal-hifi after changes |  | ||||||
|  */ |  | ||||||
| function restart() { |  | ||||||
|   app.relaunch(); |  | ||||||
|   app.exit(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Bind UI components to functions after DOMContentLoaded |  * Bind UI components to functions after DOMContentLoaded | ||||||
|  */ |  */ | ||||||
| @@ -130,20 +193,29 @@ window.addEventListener("DOMContentLoaded", () => { | |||||||
|   handleFileUploads(); |   handleFileUploads(); | ||||||
|  |  | ||||||
|   document.getElementById("close").addEventListener("click", hide); |   document.getElementById("close").addEventListener("click", hide); | ||||||
|   document.getElementById("restart").addEventListener("click", restart); |  | ||||||
|   document.querySelectorAll(".external-link").forEach((elem) => |   document.querySelectorAll(".external-link").forEach((elem) => | ||||||
|     elem.addEventListener("click", function (event) { |     elem.addEventListener("click", function (event) { | ||||||
|       openExternal((event.target as HTMLElement).getAttribute("data-url")); |       openExternal((event.target as HTMLElement).getAttribute("data-url")); | ||||||
|     }) |     }) | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   function addInputListener(source: HTMLInputElement, key: string) { |   function addInputListener( | ||||||
|  |     source: HTMLInputElement, | ||||||
|  |     key: string, | ||||||
|  |     toggleOptions?: { switch: string; classToHide: string } | ||||||
|  |   ) { | ||||||
|     source.addEventListener("input", () => { |     source.addEventListener("input", () => { | ||||||
|       if (source.value === "on") { |       if (source.value === "on") { | ||||||
|         settingsStore.set(key, source.checked); |         settingsStore.set(key, source.checked); | ||||||
|       } else { |       } else { | ||||||
|         settingsStore.set(key, source.value); |         settingsStore.set(key, source.value); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       if (toggleOptions) { | ||||||
|  |         if (source.value === "on" && source.id === toggleOptions.switch) { | ||||||
|  |           setElementHidden(source.checked, toggleOptions); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       ipcRenderer.send(globalEvents.storeChanged); |       ipcRenderer.send(globalEvents.storeChanged); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -172,12 +244,15 @@ window.addEventListener("DOMContentLoaded", () => { | |||||||
|  |  | ||||||
|   adBlock = get("adBlock"); |   adBlock = get("adBlock"); | ||||||
|   api = get("apiCheckbox"); |   api = get("apiCheckbox"); | ||||||
|  |   channel = get<HTMLSelectElement>("channel"); | ||||||
|   customCSS = get("customCSS"); |   customCSS = get("customCSS"); | ||||||
|   disableBackgroundThrottle = get("disableBackgroundThrottle"); |   disableBackgroundThrottle = get("disableBackgroundThrottle"); | ||||||
|   disableHardwareMediaKeys = get("disableHardwareMediaKeys"); |   disableHardwareMediaKeys = get("disableHardwareMediaKeys"); | ||||||
|   enableCustomHotkeys = get("enableCustomHotkeys"); |   enableCustomHotkeys = get("enableCustomHotkeys"); | ||||||
|   enableDiscord = get("enableDiscord"); |   enableDiscord = get("enableDiscord"); | ||||||
|  |   enableWaylandSupport = get("enableWaylandSupport"); | ||||||
|   gpuRasterization = get("gpuRasterization"); |   gpuRasterization = get("gpuRasterization"); | ||||||
|  |   hostname = get("hostname"); | ||||||
|   menuBar = get("menuBar"); |   menuBar = get("menuBar"); | ||||||
|   minimizeOnClose = get("minimizeOnClose"); |   minimizeOnClose = get("minimizeOnClose"); | ||||||
|   mpris = get("mprisCheckbox"); |   mpris = get("mprisCheckbox"); | ||||||
| @@ -188,22 +263,33 @@ window.addEventListener("DOMContentLoaded", () => { | |||||||
|   trayIcon = get("trayIcon"); |   trayIcon = get("trayIcon"); | ||||||
|   skipArtists = get("skipArtists"); |   skipArtists = get("skipArtists"); | ||||||
|   skippedArtists = get("skippedArtists"); |   skippedArtists = get("skippedArtists"); | ||||||
|  |   staticWindowTitle = get("staticWindowTitle"); | ||||||
|   singleInstance = get("singleInstance"); |   singleInstance = get("singleInstance"); | ||||||
|   updateFrequency = get("updateFrequency"); |   updateFrequency = get("updateFrequency"); | ||||||
|   enableListenBrainz = get("enableListenBrainz"); |   enableListenBrainz = get("enableListenBrainz"); | ||||||
|   ListenBrainzAPI = get("ListenBrainzAPI"); |   ListenBrainzAPI = get("ListenBrainzAPI"); | ||||||
|   ListenBrainzToken = get("ListenBrainzToken"); |   ListenBrainzToken = get("ListenBrainzToken"); | ||||||
|  |   discord_details_prefix = get("discord_details_prefix"); | ||||||
|  |   discord_include_timestamps = get("discord_include_timestamps"); | ||||||
|  |   listenbrainz_delay = get("listenbrainz_delay"); | ||||||
|  |   discord_button_text = get("discord_button_text"); | ||||||
|  |   discord_show_song = get("discord_show_song"); | ||||||
|  |   discord_show_idle = get("discord_show_idle"); | ||||||
|  |   discord_using_text = get("discord_using_text"); | ||||||
|  |   discord_idle_text = get("discord_idle_text"); | ||||||
|  |  | ||||||
|   refreshSettings(); |   refreshSettings(); | ||||||
|  |  | ||||||
|   addInputListener(adBlock, settings.adBlock); |   addInputListener(adBlock, settings.adBlock); | ||||||
|   addInputListener(api, settings.api); |   addInputListener(api, settings.api); | ||||||
|  |   addSelectListener(channel, settings.advanced.tidalUrl); | ||||||
|   addTextAreaListener(customCSS, settings.customCSS); |   addTextAreaListener(customCSS, settings.customCSS); | ||||||
|   addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle); |   addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle); | ||||||
|   addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys); |   addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys); | ||||||
|   addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys); |   addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys); | ||||||
|   addInputListener(enableDiscord, settings.enableDiscord); |   addInputListener(enableDiscord, settings.enableDiscord, switchesWithSettings.discord); | ||||||
|  |   addInputListener(enableWaylandSupport, settings.flags.enableWaylandSupport); | ||||||
|   addInputListener(gpuRasterization, settings.flags.gpuRasterization); |   addInputListener(gpuRasterization, settings.flags.gpuRasterization); | ||||||
|  |   addInputListener(hostname, settings.apiSettings.hostname); | ||||||
|   addInputListener(menuBar, settings.menuBar); |   addInputListener(menuBar, settings.menuBar); | ||||||
|   addInputListener(minimizeOnClose, settings.minimizeOnClose); |   addInputListener(minimizeOnClose, settings.minimizeOnClose); | ||||||
|   addInputListener(mpris, settings.mpris); |   addInputListener(mpris, settings.mpris); | ||||||
| @@ -212,11 +298,28 @@ window.addEventListener("DOMContentLoaded", () => { | |||||||
|   addInputListener(port, settings.apiSettings.port); |   addInputListener(port, settings.apiSettings.port); | ||||||
|   addInputListener(skipArtists, settings.skipArtists); |   addInputListener(skipArtists, settings.skipArtists); | ||||||
|   addTextAreaListener(skippedArtists, settings.skippedArtists); |   addTextAreaListener(skippedArtists, settings.skippedArtists); | ||||||
|  |   addInputListener(staticWindowTitle, settings.staticWindowTitle); | ||||||
|   addInputListener(singleInstance, settings.singleInstance); |   addInputListener(singleInstance, settings.singleInstance); | ||||||
|   addSelectListener(theme, settings.theme); |   addSelectListener(theme, settings.theme); | ||||||
|   addInputListener(trayIcon, settings.trayIcon); |   addInputListener(trayIcon, settings.trayIcon); | ||||||
|   addInputListener(updateFrequency, settings.updateFrequency); |   addInputListener(updateFrequency, settings.updateFrequency); | ||||||
|   addInputListener(enableListenBrainz, settings.ListenBrainz.enabled); |   addInputListener( | ||||||
|   addTextAreaListener(ListenBrainzAPI, settings.ListenBrainz.api); |     enableListenBrainz, | ||||||
|   addTextAreaListener(ListenBrainzToken, settings.ListenBrainz.token); |     settings.ListenBrainz.enabled, | ||||||
|  |     switchesWithSettings.listenBrainz | ||||||
|  |   ); | ||||||
|  |   addInputListener(ListenBrainzAPI, settings.ListenBrainz.api); | ||||||
|  |   addInputListener(ListenBrainzToken, settings.ListenBrainz.token); | ||||||
|  |   addInputListener(listenbrainz_delay, settings.ListenBrainz.delay); | ||||||
|  |   addInputListener(discord_details_prefix, settings.discord.detailsPrefix); | ||||||
|  |   addInputListener(discord_include_timestamps, settings.discord.includeTimestamps); | ||||||
|  |   addInputListener(discord_button_text, settings.discord.buttonText); | ||||||
|  |   addInputListener( | ||||||
|  |     discord_show_song, | ||||||
|  |     settings.discord.showSong, | ||||||
|  |     switchesWithSettings.discord_show_song | ||||||
|  |   ); | ||||||
|  |   addInputListener(discord_show_idle, settings.discord.showIdle); | ||||||
|  |   addInputListener(discord_idle_text, settings.discord.idleText); | ||||||
|  |   addInputListener(discord_using_text, settings.discord.usingText); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|   <meta http-equiv="X-UA-Compatible" content="ie=edge" /> |   <meta http-equiv="X-UA-Compatible" content="ie=edge" /> | ||||||
|   <link rel="stylesheet" href="./settings.css" /> |   <link rel="stylesheet" href="./settings.css" /> | ||||||
|  |   <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> | ||||||
| </head> | </head> | ||||||
|  |  | ||||||
| <body class="settings-window"> | <body class="settings-window"> | ||||||
| @@ -48,7 +49,7 @@ | |||||||
|             <div class="group__option"> |             <div class="group__option"> | ||||||
|               <div class="group__description"> |               <div class="group__description"> | ||||||
|                 <h4>Notifications</h4> |                 <h4>Notifications</h4> | ||||||
|                 <p>Show a notification when a new song starts.</p> |                 <p>Show a notification when new media starts.</p> | ||||||
|               </div> |               </div> | ||||||
|               <label class="switch"> |               <label class="switch"> | ||||||
|                 <input id="notifications" type="checkbox" /> |                 <input id="notifications" type="checkbox" /> | ||||||
| @@ -105,6 +106,16 @@ | |||||||
|                 <span class="switch__slider"></span> |                 <span class="switch__slider"></span> | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="group__option"> | ||||||
|  |               <div class="group__description"> | ||||||
|  |                 <h4>Static Window Title</h4> | ||||||
|  |                 <p>Makes the window title "TIDAL Hi-Fi" instead of changing to the currently playing song.</p> | ||||||
|  |               </div> | ||||||
|  |               <label class="switch"> | ||||||
|  |                 <input id="staticWindowTitle" type="checkbox" /> | ||||||
|  |                 <span class="switch__slider"></span> | ||||||
|  |               </label> | ||||||
|  |             </div> | ||||||
|             <div class="group__option"> |             <div class="group__option"> | ||||||
|               <div class="group__description"> |               <div class="group__description"> | ||||||
|                 <h4>Minimize on Close</h4> |                 <h4>Minimize on Close</h4> | ||||||
| @@ -146,7 +157,7 @@ | |||||||
|             <p class="group__title">API</p> |             <p class="group__title">API</p> | ||||||
|             <div class="group__description"> |             <div class="group__description"> | ||||||
|               <p> |               <p> | ||||||
|                 TIDAL Hi-Fi has a built-in web API to allow users to get current song information. |                 TIDAL Hi-Fi has a built-in web API to allow users to get current media information. | ||||||
|                 You can optionally enable playback control as well. |                 You can optionally enable playback control as well. | ||||||
|               </p> |               </p> | ||||||
|             </div> |             </div> | ||||||
| @@ -166,6 +177,16 @@ | |||||||
|                 <input id="port" type="number" class="text-input" name="port" /> |                 <input id="port" type="number" class="text-input" name="port" /> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="group__option"> | ||||||
|  |               <div class="group__description"> | ||||||
|  |                 <h4>API hostname</h4> | ||||||
|  |                 <p>By default (127.0.0.1) only local apps can interface with the API. <br /> | ||||||
|  |                   Change to 0.0.0.0 to allow <strong>anyone</strong> to interact with it. <br /> | ||||||
|  |                   Other options are available | ||||||
|  |                 </p> | ||||||
|  |                 <input id="hostname" type="text" class="text-input" name="hostname" /> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|             <div class="group__option"> |             <div class="group__option"> | ||||||
|               <div class="group__description"> |               <div class="group__description"> | ||||||
|                 <h4>Playback control</h4> |                 <h4>Playback control</h4> | ||||||
| @@ -201,6 +222,9 @@ | |||||||
|                 <span class="switch__slider"></span> |                 <span class="switch__slider"></span> | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div class="group"> | ||||||
|  |             <p class="group__title">Discord</p> | ||||||
|             <div class="group__option"> |             <div class="group__option"> | ||||||
|               <div class="group__description"> |               <div class="group__description"> | ||||||
|                 <h4>Discord RPC</h4> |                 <h4>Discord RPC</h4> | ||||||
| @@ -211,9 +235,83 @@ | |||||||
|                 <span class="switch__slider"></span> |                 <span class="switch__slider"></span> | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|  |             <div id="discord_options"> | ||||||
|  |  | ||||||
|  |               <div class="group__option" class="hidden"> | ||||||
|  |                 <div class="group__description"> | ||||||
|  |                   <h4>Show Idle Text</h4> | ||||||
|  |                   <p>Should the idle text be shown when idle?</p> | ||||||
|  |                 </div> | ||||||
|  |                 <label class="switch"> | ||||||
|  |                   <input id="discord_show_idle" type="checkbox" /> | ||||||
|  |                   <span class="switch__slider"></span> | ||||||
|  |                 </label> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |               <div class="group__option" class="hidden"> | ||||||
|  |                 <div class="group__description"> | ||||||
|  |                   <h4>Idle Text</h4> | ||||||
|  |                   <p>The text displayed on Discord's rich presence while idling in the app.</p> | ||||||
|  |                   <input id="discord_idle_text" type="text" class="text-input" name="discord_idle_text" /> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |               <div class="group__option" class="hidden"> | ||||||
|  |                 <div class="group__description"> | ||||||
|  |                   <h4>Using Tidal Text</h4> | ||||||
|  |                   <p>The text displayed on Discord's rich presence while "showSong" is turned off</p> | ||||||
|  |                   <input id="discord_using_text" type="text" class="text-input" name="discord_using_text" /> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |               <div class="group__option" class="hidden"> | ||||||
|  |                 <div class="group__description"> | ||||||
|  |                   <h4>Show media</h4> | ||||||
|  |                   <p>Show the current media in the Discord client</p> | ||||||
|  |                 </div> | ||||||
|  |                 <label class="switch"> | ||||||
|  |                   <input id="discord_show_song" type="checkbox" /> | ||||||
|  |                   <span class="switch__slider"></span> | ||||||
|  |                 </label> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |               <div id="discord_show_song_options" class="hidden"> | ||||||
|  |  | ||||||
|  |                 <div class="group__option" class="hidden"> | ||||||
|  |                   <div class="group__description"> | ||||||
|  |                     <h4>Include timestamps</h4> | ||||||
|  |                     <p>Show current/end playtime in the Discord client</p> | ||||||
|  |                   </div> | ||||||
|  |                   <label class="switch"> | ||||||
|  |                     <input id="discord_include_timestamps" type="checkbox" /> | ||||||
|  |                     <span class="switch__slider"></span> | ||||||
|  |                   </label> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="group__option" class="hidden"> | ||||||
|  |                   <div class="group__description"> | ||||||
|  |                     <h4>Details prefix</h4> | ||||||
|  |                     <p>Prefix for the "details" field of Discord's rich presence.</p> | ||||||
|  |                     <input id="discord_details_prefix" type="text" class="text-input" name="discord_details_prefix" /> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|                 <div class="group__option"> |                 <div class="group__option"> | ||||||
|                   <div class="group__description"> |                   <div class="group__description"> | ||||||
|                 <h4>ListenBrainz</h4> |                     <h4>Button text</h4> | ||||||
|  |                     <p>Text to display on the button below the media information.</p> | ||||||
|  |                     <input id="discord_button_text" type="text" class="text-input" name="discord_button_text" /> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div class="group"> | ||||||
|  |             <p class="group__title">ListenBrainz</p> | ||||||
|  |             <div class="group__option"> | ||||||
|  |               <div class="group__description"> | ||||||
|  |                 <h4>Enable ListenBrainz</h4> | ||||||
|                 <p>Scrobble your listens directly to ListenBrainz.</p> |                 <p>Scrobble your listens directly to ListenBrainz.</p> | ||||||
|               </div> |               </div> | ||||||
|               <label class="switch"> |               <label class="switch"> | ||||||
| @@ -221,20 +319,28 @@ | |||||||
|                 <span class="switch__slider"></span> |                 <span class="switch__slider"></span> | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|  |             <div id="listenbrainz__options" class="hidden"> | ||||||
|               <div class="group__option"> |               <div class="group__option"> | ||||||
|                 <div class="group__description"> |                 <div class="group__description"> | ||||||
|                   <h4>ListenBrainz API Url</h4> |                   <h4>ListenBrainz API Url</h4> | ||||||
|                   <p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p> |                   <p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p> | ||||||
|  |                   <input id="ListenBrainzAPI" type="text" class="text-input" name="ListenBrainzAPI" /> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             <textarea id="ListenBrainzAPI" class="textarea" cols="1" rows="1" spellcheck="false"></textarea> |  | ||||||
|               <div class="group__option"> |               <div class="group__option"> | ||||||
|                 <div class="group__description"> |                 <div class="group__description"> | ||||||
|                   <h4>ListenBrainz User Token</h4> |                   <h4>ListenBrainz User Token</h4> | ||||||
|                   <p>Provide the user token you can get from the settings page.</p> |                   <p>Provide the user token you can get from the settings page.</p> | ||||||
|  |                   <input id="ListenBrainzToken" type="text" class="text-input" name="ListenBrainzToken" /> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             <textarea id="ListenBrainzToken" class="textarea" cols="1" rows="1" spellcheck="false"></textarea> |             </div> | ||||||
|  |             <div class="group__description"> | ||||||
|  |               <h4>ScrobbleDelay</h4> | ||||||
|  |               <p>The delay (in ms) to send a listen to ListenBrainz. Prevents spamming the API when you fast forward | ||||||
|  |                 immediately</p> | ||||||
|  |               <input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" /> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </section> |         </section> | ||||||
|  |  | ||||||
| @@ -245,7 +351,7 @@ | |||||||
|               <div class="group__description"> |               <div class="group__description"> | ||||||
|                 <h4>Update frequency</h4> |                 <h4>Update frequency</h4> | ||||||
|                 <p> |                 <p> | ||||||
|                   The amount of time, in milliseconds, that tidal-hifi will refresh its playback info by scraping the |                   The amount of time, in milliseconds, that TIDAL Hi-Fi will refresh its playback info by scraping the | ||||||
|                   website. |                   website. | ||||||
|                   The default of 500 seems to work in more cases but if you are fine with a bit more resource usage you |                   The default of 500 seems to work in more cases but if you are fine with a bit more resource usage you | ||||||
|                   can decrease it as well. |                   can decrease it as well. | ||||||
| @@ -253,6 +359,21 @@ | |||||||
|                 <input id="updateFrequency" type="number" class="text-input" name="updateFrequency" /> |                 <input id="updateFrequency" type="number" class="text-input" name="updateFrequency" /> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  |             <div class="group__option"> | ||||||
|  |               <div class="group__description"> | ||||||
|  |                 <h4>Tidal channel / URL</h4> | ||||||
|  |                 <p> | ||||||
|  |                   Which URL Tidal Hi-Fi should use. | ||||||
|  |                   <strong>note! Beta might break at any time</strong> | ||||||
|  |                 </p> | ||||||
|  |                 <select class="select-input" id="channel" name="channel"> | ||||||
|  |                   <option value="https://listen.tidal.com">Stable (listen.tidal.com)</option> | ||||||
|  |                   <option value="https://listen.stage.tidal.com">Staging (listen.stage.tidal.com)</option> | ||||||
|  |                 </select> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|           <div class="group"> |           <div class="group"> | ||||||
|             <p class="group__title">Flags</p> |             <p class="group__title">Flags</p> | ||||||
|             <div class="group__option"> |             <div class="group__option"> | ||||||
| @@ -290,6 +411,18 @@ | |||||||
|                 <span class="switch__slider"></span> |                 <span class="switch__slider"></span> | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="group__option"> | ||||||
|  |               <div class="group__description"> | ||||||
|  |                 <h4>Wayland support</h4> | ||||||
|  |                 <p> | ||||||
|  |                   Adds a couple of Electron flags to help TIDAL Hi-Fi run smoothly on the Wayland window system. | ||||||
|  |                 </p> | ||||||
|  |               </div> | ||||||
|  |               <label class="switch"> | ||||||
|  |                 <input id="enableWaylandSupport" type="checkbox" /> | ||||||
|  |                 <span class="switch__slider"></span> | ||||||
|  |               </label> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </section> |         </section> | ||||||
|  |  | ||||||
| @@ -342,21 +475,26 @@ | |||||||
|  |  | ||||||
|         <section id="about-section" class="tabs__section about-section"> |         <section id="about-section" class="tabs__section about-section"> | ||||||
|           <img alt="tidal icon" class="about-section__icon" src="./icon.png" /> |           <img alt="tidal icon" class="about-section__icon" src="./icon.png" /> | ||||||
|           <p class="about-section__text"> |           <h4>TIDAL Hi-Fi</h4> | ||||||
|             <a class="external-link" data-url="https://github.com/Mastermindzh/tidal-hifi">TIDAL Hi-Fi</a> |           <div class="about-section__version"> | ||||||
|             is made by |             <a target="_blank" rel="noopener" | ||||||
|             <a class="external-link" data-url="https://www.rickvanlieshout.com"> |               href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.17.0">5.17.0</a> | ||||||
|               Rick van Lieshout</a>. <br />It uses |           </div> | ||||||
|             <a class="external-link" data-url="https://castlabs.com/">Castlabs'</a> |           <div class="about-section__links"> | ||||||
|             version of Electron for widevine support. |             <a target="_blank" rel="noopener" href="https://github.com/mastermindzh/tidal-hifi/" | ||||||
|           </p> |               class="about-section__button">Github | ||||||
|  |               <i class="fa fa-external-link"></i></a> | ||||||
|  |             <a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/issues" | ||||||
|  |               class="about-section__button">Report an issue <i class="fa fa-external-link"></i></a> | ||||||
|  |             <a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors" | ||||||
|  |               class="about-section__button">Contributors <i class="fa fa-external-link"></i></a> | ||||||
|  |           </div> | ||||||
|         </section> |         </section> | ||||||
|  |  | ||||||
|         <footer class="footer"> |         <footer class="footer"> | ||||||
|           <p class="footer__note"> |           <p class="footer__note"> | ||||||
|             Some settings may require a restart of TIDAL Hi-Fi. To do so, click the button below: |             <strong>Note</strong>: some settings may require a restart of TIDAL Hi-Fi. | ||||||
|           </p> |           </p> | ||||||
|           <button class="footer__button" id="restart">Restart TIDAL Hi-Fi</button> |  | ||||||
|         </footer> |         </footer> | ||||||
|       </div> |       </div> | ||||||
|     </main> |     </main> | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ $tidal-grey: #72777f; | |||||||
| $tidal-grey-darker: #404248; | $tidal-grey-darker: #404248; | ||||||
| $tidal-grey-darker-focus: #55585f; | $tidal-grey-darker-focus: #55585f; | ||||||
| $tidal-grey-darkest: #242528; | $tidal-grey-darkest: #242528; | ||||||
|  | $tidal-grey-darkest-focus: #2e2f33; | ||||||
|  |  | ||||||
| // --- Fonts --- | // --- Fonts --- | ||||||
|  |  | ||||||
| @@ -309,26 +310,79 @@ html { | |||||||
| } | } | ||||||
|  |  | ||||||
| .about-section { | .about-section { | ||||||
|   padding-top: 120px; |   padding-top: 40px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
|  |  | ||||||
|   &__icon { |   &__icon { | ||||||
|     display: inline-block; |     display: inline-block; | ||||||
|     width: 100px; |     width: 200px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &__text { |   &__text { | ||||||
|     display: block; |     display: block; | ||||||
|     max-width: 350px; |     max-width: 500px; | ||||||
|     margin: 20px auto 0; |     margin: -15px auto 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // --- Footer --- |   &__table { | ||||||
|  |     width: 120px; | ||||||
|  |     margin: 0 auto; | ||||||
|  |  | ||||||
|  |     td { | ||||||
|  |       text-align: left; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &__version { | ||||||
|  |     margin: -10px 0 30px; | ||||||
|  |  | ||||||
|  |     a { | ||||||
|  |       background-color: $tidal-grey-darker; | ||||||
|  |       border: none; | ||||||
|  |       color: $tidal-blue; | ||||||
|  |       padding: 8px 20px; | ||||||
|  |       font-weight: bold; | ||||||
|  |       text-align: center; | ||||||
|  |       text-decoration: none; | ||||||
|  |       display: inline-block; | ||||||
|  |       border-radius: 100px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &__links { | ||||||
|  |     width: 300px; | ||||||
|  |     margin: 0 auto; | ||||||
|  |  | ||||||
|  |     a { | ||||||
|  |       border-radius: 10px; | ||||||
|  |       border: none; | ||||||
|  |       color: $white; | ||||||
|  |       padding: 10px 10px 10px 20px; | ||||||
|  |       margin: 8px; | ||||||
|  |       text-align: left; | ||||||
|  |       font-size: 16px; | ||||||
|  |       line-height: 30px; | ||||||
|  |       display: flex; | ||||||
|  |       text-decoration: none; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       background-color: $tidal-grey-darkest; | ||||||
|  |  | ||||||
|  |       i { | ||||||
|  |         color: $white; | ||||||
|  |         line-height: 30px; | ||||||
|  |         font-size: 18px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         background-color: $tidal-grey-darkest-focus; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // --- Footer --- | ||||||
| .footer { | .footer { | ||||||
|   position: sticky; |   position: sticky; | ||||||
|   top: calc(100% - 120px); |  | ||||||
|   height: 100px; |   height: 100px; | ||||||
|   padding-top: 20px; |   padding-top: 20px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
| @@ -361,17 +415,17 @@ html { | |||||||
| } | } | ||||||
|  |  | ||||||
| // file upload | // file upload | ||||||
|  |  | ||||||
| .file-drop-area { | .file-drop-area { | ||||||
|   position: relative; |   position: relative; | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   max-width: 100%; |   max-width: 100%; | ||||||
|   padding: 25px 0 25px 0px; |   padding: 25px 0; | ||||||
|   border: 1px dashed $tidal-grey; |   border: 1px dashed $tidal-grey; | ||||||
|   border-radius: 3px; |   border-radius: 3px; | ||||||
|   transition: 0.2s; |   transition: 0.2s; | ||||||
|  |  | ||||||
|   &.is-active { |   &.is-active { | ||||||
|     background-color: $black; |     background-color: $black; | ||||||
|   } |   } | ||||||
| @@ -409,6 +463,7 @@ html { | |||||||
|   width: 100%; |   width: 100%; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   opacity: 0; |   opacity: 0; | ||||||
|  |  | ||||||
|   &:focus { |   &:focus { | ||||||
|     outline: none; |     outline: none; | ||||||
|   } |   } | ||||||
| @@ -443,3 +498,7 @@ html { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .hidden { | ||||||
|  |   display: none !important; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import fs from "fs"; | import fs from "fs"; | ||||||
|  | import { Logger } from "../../features/logger"; | ||||||
|  |  | ||||||
| const cssFilter = (file: string) => file.endsWith(".css"); | const cssFilter = (file: string) => file.endsWith(".css"); | ||||||
| const sort = (a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase()); | const sort = (a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase()); | ||||||
| @@ -36,7 +37,7 @@ export const getThemeListFromDirectory = (directory: string): string[] => { | |||||||
|     makeUserThemesDirectory(directory); |     makeUserThemesDirectory(directory); | ||||||
|     return fs.readdirSync(directory).filter(cssFilter).sort(sort); |     return fs.readdirSync(directory).filter(cssFilter).sort(sort); | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     console.error(err); |     Logger.log(`Failed to get files from ${directory}`, err); | ||||||
|     return []; |     return []; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| @@ -49,6 +50,6 @@ export const makeUserThemesDirectory = (directory: string) => { | |||||||
|   try { |   try { | ||||||
|     fs.mkdirSync(directory, { recursive: true }); |     fs.mkdirSync(directory, { recursive: true }); | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     console.error(err); |     Logger.log(`Failed to make user theme directory: ${directory}`, err); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										408
									
								
								src/preload.ts
									
									
									
									
									
								
							
							
						
						| @@ -1,23 +1,39 @@ | |||||||
| import { app, dialog, Notification } from "@electron/remote"; | import { app, dialog, Notification } from "@electron/remote"; | ||||||
| import { clipboard, ipcRenderer } from "electron"; | import { clipboard, ipcRenderer } from "electron"; | ||||||
| import fs from "fs"; |  | ||||||
| import Player from "mpris-service"; | import Player from "mpris-service"; | ||||||
| import { globalEvents } from "./constants/globalEvents"; | import { globalEvents } from "./constants/globalEvents"; | ||||||
| import { settings } from "./constants/settings"; | import { settings } from "./constants/settings"; | ||||||
| import { statuses } from "./constants/statuses"; | import { | ||||||
| import { Songwhip } from "./features/songwhip/songwhip"; |   ListenBrainz, | ||||||
| import { ListenBrainz } from "./features/listenbrainz/listenbrainz"; |   ListenBrainzConstants, | ||||||
| import { Options } from "./models/options"; |   ListenBrainzStore, | ||||||
|  | } from "./features/listenbrainz/listenbrainz"; | ||||||
|  | import { StoreData } from "./features/listenbrainz/models/storeData"; | ||||||
|  | import { Logger } from "./features/logger"; | ||||||
|  | import { SharingService } from "./features/sharingService/sharingService"; | ||||||
|  | import { addCustomCss } from "./features/theming/theming"; | ||||||
|  | import { convertDurationToSeconds } from "./features/time/parse"; | ||||||
|  | import { MediaInfo } from "./models/mediaInfo"; | ||||||
|  | import { MediaStatus } from "./models/mediaStatus"; | ||||||
|  | import { RepeatState } from "./models/repeatState"; | ||||||
| import { downloadFile } from "./scripts/download"; | import { downloadFile } from "./scripts/download"; | ||||||
| import { addHotkey } from "./scripts/hotkeys"; | import { addHotkey } from "./scripts/hotkeys"; | ||||||
|  | import { ObjectToDotNotation } from "./scripts/objectUtilities"; | ||||||
| import { settingsStore } from "./scripts/settings"; | import { settingsStore } from "./scripts/settings"; | ||||||
| import { setTitle } from "./scripts/window-functions"; | import { setTitle } from "./scripts/window-functions"; | ||||||
|  |  | ||||||
| const notificationPath = `${app.getPath("userData")}/notification.jpg`; | const notificationPath = `${app.getPath("userData")}/notification.jpg`; | ||||||
| const appName = "Tidal Hifi"; |  | ||||||
| let currentSong = ""; | let currentSong = ""; | ||||||
| let player: Player; | let player: Player; | ||||||
| let currentPlayStatus = statuses.paused; | let currentPlayStatus = MediaStatus.paused; | ||||||
|  | let currentListenBrainzDelayId: ReturnType<typeof setTimeout>; | ||||||
|  | let scrobbleWaitingForDelay = false; | ||||||
|  |  | ||||||
|  | let currentlyPlaying = MediaStatus.paused; | ||||||
|  | let currentRepeatState: RepeatState = RepeatState.off; | ||||||
|  | let currentShuffleState = false; | ||||||
|  | let currentMediaInfo: MediaInfo; | ||||||
|  | let currentNotification: Electron.Notification; | ||||||
|  |  | ||||||
| const elements = { | const elements = { | ||||||
|   play: '*[data-test="play"]', |   play: '*[data-test="play"]', | ||||||
| @@ -32,20 +48,24 @@ const elements = { | |||||||
|   search: '[class^="searchField"]', |   search: '[class^="searchField"]', | ||||||
|   shuffle: '*[data-test="shuffle"]', |   shuffle: '*[data-test="shuffle"]', | ||||||
|   repeat: '*[data-test="repeat"]', |   repeat: '*[data-test="repeat"]', | ||||||
|   account: '*[class^="profileOptions"]', |   account: '*[data-test^="profile-image-button"]', | ||||||
|   settings: '*[data-test^="open-settings"]', |   settings: '*[data-test^="sidebar-menu-button"]', | ||||||
|  |   openSettings: '*[data-test^="open-settings"]', | ||||||
|   media: '*[data-test="current-media-imagery"]', |   media: '*[data-test="current-media-imagery"]', | ||||||
|   image: "img", |   image: "img", | ||||||
|   current: '*[data-test="current-time"]', |   current: '*[data-test="current-time"]', | ||||||
|   duration: '*[data-test="duration"]', |   duration: '*[class^=playbackControlsContainer] *[data-test="duration"]', | ||||||
|   bar: '*[data-test="progress-bar"]', |   bar: '*[data-test="progress-bar"]', | ||||||
|   footer: "#footerPlayer", |   footer: "#footerPlayer", | ||||||
|   mediaItem: "[data-type='mediaItem']", |   mediaItem: "[data-type='mediaItem']", | ||||||
|   album_header_title: '.header-details [data-test="title"]', |   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']", |   currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", | ||||||
|   album_name_cell: '[class^="album"]', |   album_name_cell: '[class^="album"]', | ||||||
|   tracklist_row: '[data-test="tracklist-row"]', |   tracklist_row: '[data-test="tracklist-row"]', | ||||||
|   volume: '*[data-test="volume"]', |   volume: '*[data-test="volume"]', | ||||||
|  |   favorite: '*[data-test="footer-favorite-button"]', | ||||||
|   /** |   /** | ||||||
|    * Get an element from the dom |    * Get an element from the dom | ||||||
|    * @param {*} key key in elements object to fetch |    * @param {*} key key in elements object to fetch | ||||||
| @@ -55,7 +75,7 @@ const elements = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get the icon of the current song |    * Get the icon of the current media | ||||||
|    */ |    */ | ||||||
|   getSongIcon: function () { |   getSongIcon: function () { | ||||||
|     const figure = this.get("media"); |     const figure = this.get("media"); | ||||||
| @@ -71,7 +91,7 @@ const elements = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * returns an array of all artists in the current song |    * returns an array of all artists in the current media | ||||||
|    * @returns {Array} artists |    * @returns {Array} artists | ||||||
|    */ |    */ | ||||||
|   getArtistsArray: function () { |   getArtistsArray: function () { | ||||||
| @@ -106,7 +126,7 @@ const elements = { | |||||||
|       window.location.href.includes("/playlist/") || |       window.location.href.includes("/playlist/") || | ||||||
|       window.location.href.includes("/mix/") |       window.location.href.includes("/mix/") | ||||||
|     ) { |     ) { | ||||||
|       if (currentPlayStatus === statuses.playing) { |       if (currentPlayStatus === MediaStatus.playing) { | ||||||
|         // find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell. |         // find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell. | ||||||
|         // document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent |         // document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent | ||||||
|         const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem); |         const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem); | ||||||
| @@ -116,6 +136,12 @@ const elements = { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // see whether we're on the queue page and get it from there | ||||||
|  |     const queueAlbumName = elements.getText("queue_album"); | ||||||
|  |     if (queueAlbumName) { | ||||||
|  |       return queueAlbumName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return ""; |     return ""; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| @@ -123,6 +149,10 @@ const elements = { | |||||||
|     return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false |     return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   isFavorite: function () { | ||||||
|  |     return this.get("favorite").getAttribute("aria-checked") === "true"; | ||||||
|  |   }, | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Shorthand function to get the text of a dom element |    * Shorthand function to get the text of a dom element | ||||||
|    * @param {*} key key in elements object to fetch |    * @param {*} key key in elements object to fetch | ||||||
| @@ -150,36 +180,12 @@ const elements = { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| function addCustomCss() { |  | ||||||
|   window.addEventListener("DOMContentLoaded", () => { |  | ||||||
|     const selectedTheme = settingsStore.get(settings.theme); |  | ||||||
|     if (selectedTheme !== "none") { |  | ||||||
|       const themeFile = `${process.resourcesPath}/${selectedTheme}`; |  | ||||||
|       fs.readFile(themeFile, "utf-8", (err, data) => { |  | ||||||
|         if (err) { |  | ||||||
|           alert("An error ocurred reading the theme file."); |  | ||||||
|           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); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the update frequency from the store |  * Get the update frequency from the store | ||||||
|  * make sure it returns a number, if not use the default |  * make sure it returns a number, if not use the default | ||||||
|  */ |  */ | ||||||
| function getUpdateFrequency() { | function getUpdateFrequency() { | ||||||
|   const storeValue = settingsStore.get(settings.updateFrequency) as number; |   const storeValue = settingsStore.get<string, number>(settings.updateFrequency); | ||||||
|   const defaultValue = 500; |   const defaultValue = 500; | ||||||
|  |  | ||||||
|   if (!isNaN(storeValue)) { |   if (!isNaN(storeValue)) { | ||||||
| @@ -190,7 +196,7 @@ function getUpdateFrequency() { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Play or pause the current song |  * Play or pause the current media | ||||||
|  */ |  */ | ||||||
| function playPause() { | function playPause() { | ||||||
|   const play = elements.get("play"); |   const play = elements.get("play"); | ||||||
| @@ -202,6 +208,11 @@ function playPause() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Clears the old listenbrainz data on launch | ||||||
|  |  */ | ||||||
|  | ListenBrainzStore.clear(); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Add hotkeys for when tidal is focused |  * Add hotkeys for when tidal is focused | ||||||
|  * Reflects the desktop hotkeys found on: |  * Reflects the desktop hotkeys found on: | ||||||
| @@ -210,15 +221,19 @@ function playPause() { | |||||||
| function addHotKeys() { | function addHotKeys() { | ||||||
|   if (settingsStore.get(settings.enableCustomHotkeys)) { |   if (settingsStore.get(settings.enableCustomHotkeys)) { | ||||||
|     addHotkey("Control+p", function () { |     addHotkey("Control+p", function () { | ||||||
|       elements.click("account"); |  | ||||||
|       setTimeout(() => { |  | ||||||
|       elements.click("settings"); |       elements.click("settings"); | ||||||
|  |       setTimeout(() => { | ||||||
|  |         elements.click("openSettings"); | ||||||
|       }, 100); |       }, 100); | ||||||
|     }); |     }); | ||||||
|     addHotkey("Control+l", function () { |     addHotkey("Control+l", function () { | ||||||
|       handleLogout(); |       handleLogout(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     addHotkey("Control+a", function () { | ||||||
|  |       elements.click("favorite"); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     addHotkey("Control+h", function () { |     addHotkey("Control+h", function () { | ||||||
|       elements.click("home"); |       elements.click("home"); | ||||||
|     }); |     }); | ||||||
| @@ -240,11 +255,10 @@ function addHotKeys() { | |||||||
|       elements.click("repeat"); |       elements.click("repeat"); | ||||||
|     }); |     }); | ||||||
|     addHotkey("control+w", async function () { |     addHotkey("control+w", async function () { | ||||||
|       const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL()); |       const url = SharingService.getUniversalLink(getTrackURL()); | ||||||
|       const url = Songwhip.getWhipUrl(result); |  | ||||||
|       clipboard.writeText(url); |       clipboard.writeText(url); | ||||||
|       new Notification({ |       new Notification({ | ||||||
|         title: `Successfully whipped: `, |         title: `Universal link generated: `, | ||||||
|         body: `URL copied to clipboard: ${url}`, |         body: `URL copied to clipboard: ${url}`, | ||||||
|       }).show(); |       }).show(); | ||||||
|     }); |     }); | ||||||
| @@ -275,7 +289,7 @@ function handleLogout() { | |||||||
|       defaultId: 2, |       defaultId: 2, | ||||||
|     }) |     }) | ||||||
|     .then((result: { response: number }) => { |     .then((result: { response: number }) => { | ||||||
|       if (logoutOptions.indexOf("Yes, please") == result.response) { |       if (logoutOptions.indexOf("Yes, please") === result.response) { | ||||||
|         for (let i = 0; i < window.localStorage.length; i++) { |         for (let i = 0; i < window.localStorage.length; i++) { | ||||||
|           const key = window.localStorage.key(i); |           const key = window.localStorage.key(i); | ||||||
|           if (key.startsWith("_TIDAL_activeSession")) { |           if (key.startsWith("_TIDAL_activeSession")) { | ||||||
| @@ -303,6 +317,8 @@ function addIPCEventListeners() { | |||||||
|     ipcRenderer.on("globalEvent", (_event, args) => { |     ipcRenderer.on("globalEvent", (_event, args) => { | ||||||
|       switch (args) { |       switch (args) { | ||||||
|         case globalEvents.playPause: |         case globalEvents.playPause: | ||||||
|  |         case globalEvents.play: | ||||||
|  |         case globalEvents.pause: | ||||||
|           playPause(); |           playPause(); | ||||||
|           break; |           break; | ||||||
|         case globalEvents.next: |         case globalEvents.next: | ||||||
| @@ -311,11 +327,16 @@ function addIPCEventListeners() { | |||||||
|         case globalEvents.previous: |         case globalEvents.previous: | ||||||
|           elements.click("previous"); |           elements.click("previous"); | ||||||
|           break; |           break; | ||||||
|         case globalEvents.play: |         case globalEvents.toggleFavorite: | ||||||
|           elements.click("play"); |           elements.click("favorite"); | ||||||
|           break; |           break; | ||||||
|         case globalEvents.pause: |         case globalEvents.toggleShuffle: | ||||||
|           elements.click("pause"); |           elements.click("shuffle"); | ||||||
|  |           break; | ||||||
|  |         case globalEvents.toggleRepeat: | ||||||
|  |           elements.click("repeat"); | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|           break; |           break; | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
| @@ -331,13 +352,30 @@ function getCurrentlyPlayingStatus() { | |||||||
|  |  | ||||||
|   // if pause button is visible tidal is playing |   // if pause button is visible tidal is playing | ||||||
|   if (pause) { |   if (pause) { | ||||||
|     status = statuses.playing; |     status = MediaStatus.playing; | ||||||
|   } else { |   } else { | ||||||
|     status = statuses.paused; |     status = MediaStatus.paused; | ||||||
|   } |   } | ||||||
|   return status; |   return status; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function getCurrentShuffleState() { | ||||||
|  |   const shuffle = elements.get("shuffle"); | ||||||
|  |   return shuffle?.getAttribute("aria-checked") === "true"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCurrentRepeatState() { | ||||||
|  |   const repeat = elements.get("repeat"); | ||||||
|  |   switch (repeat?.getAttribute("data-type")) { | ||||||
|  |     case "button__repeatAll": | ||||||
|  |       return RepeatState.all; | ||||||
|  |     case "button__repeatSingle": | ||||||
|  |       return RepeatState.single; | ||||||
|  |     default: | ||||||
|  |       return RepeatState.off; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Convert the duration from MM:SS to seconds |  * Convert the duration from MM:SS to seconds | ||||||
|  * @param {*} duration |  * @param {*} duration | ||||||
| @@ -350,27 +388,137 @@ function convertDuration(duration: string) { | |||||||
| /** | /** | ||||||
|  * Update Tidal-hifi's media info |  * Update Tidal-hifi's media info | ||||||
|  * |  * | ||||||
|  * @param {*} options |  * @param {*} mediaInfo | ||||||
|  */ |  */ | ||||||
| function updateMediaInfo(options: Options, notify: boolean) { | function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) { | ||||||
|   if (options) { |   if (mediaInfo) { | ||||||
|     ipcRenderer.send(globalEvents.updateInfo, options); |     currentMediaInfo = mediaInfo; | ||||||
|     if (settingsStore.get(settings.notifications) && notify) { |     ipcRenderer.send(globalEvents.updateInfo, mediaInfo); | ||||||
|       new Notification({ title: options.title, body: options.artists, icon: options.icon }).show(); |     updateMpris(mediaInfo); | ||||||
|  |     updateListenBrainz(mediaInfo); | ||||||
|  |     if (notify) { | ||||||
|  |       sendNotification(mediaInfo); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * send a desktop notification if enabled in settings | ||||||
|  |  * @param mediaInfo | ||||||
|  |  * @param notify Whether to notify | ||||||
|  |  */ | ||||||
|  | async function sendNotification(mediaInfo: MediaInfo) { | ||||||
|  |   if (settingsStore.get(settings.notifications)) { | ||||||
|  |     if (currentNotification) { | ||||||
|  |       currentNotification.close(); | ||||||
|  |     } | ||||||
|  |     currentNotification = new Notification({ | ||||||
|  |       title: mediaInfo.title, | ||||||
|  |       body: mediaInfo.artists, | ||||||
|  |       icon: mediaInfo.icon, | ||||||
|  |     }); | ||||||
|  |     currentNotification.show(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addMPRIS() { | ||||||
|  |   if (process.platform === "linux" && settingsStore.get(settings.mpris)) { | ||||||
|  |     try { | ||||||
|  |       player = Player({ | ||||||
|  |         name: "tidal-hifi", | ||||||
|  |         identity: "tidal-hifi", | ||||||
|  |         supportedUriSchemes: ["file"], | ||||||
|  |         supportedMimeTypes: [ | ||||||
|  |           "audio/mpeg", | ||||||
|  |           "audio/flac", | ||||||
|  |           "audio/x-flac", | ||||||
|  |           "application/ogg", | ||||||
|  |           "audio/wav", | ||||||
|  |         ], | ||||||
|  |         supportedInterfaces: ["player"], | ||||||
|  |         desktopEntry: "tidal-hifi", | ||||||
|  |       }); | ||||||
|  |       // Events | ||||||
|  |       const events = { | ||||||
|  |         next: "next", | ||||||
|  |         previous: "previous", | ||||||
|  |         pause: "pause", | ||||||
|  |         playpause: "playpause", | ||||||
|  |         stop: "stop", | ||||||
|  |         play: "play", | ||||||
|  |         loopStatus: "repeat", | ||||||
|  |         shuffle: "shuffle", | ||||||
|  |         seek: "seek", | ||||||
|  |       } as { [key: string]: string }; | ||||||
|  |       Object.keys(events).forEach(function (eventName) { | ||||||
|  |         player.on(eventName, function () { | ||||||
|  |           const eventValue = events[eventName]; | ||||||
|  |           switch (events[eventValue]) { | ||||||
|  |             case events.playpause: | ||||||
|  |               playPause(); | ||||||
|  |               break; | ||||||
|  |             default: | ||||||
|  |               elements.click(eventValue); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |       // Override get position function | ||||||
|  |       player.getPosition = function () { | ||||||
|  |         return convertDuration(elements.getText("current")) * 1000 * 1000; | ||||||
|  |       }; | ||||||
|  |       player.on("quit", function () { | ||||||
|  |         app.quit(); | ||||||
|  |       }); | ||||||
|  |     } catch (exception) { | ||||||
|  |       Logger.log("MPRIS player api not working", exception); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function updateMpris(mediaInfo: MediaInfo) { | ||||||
|   if (player) { |   if (player) { | ||||||
|     player.metadata = { |     player.metadata = { | ||||||
|       ...player.metadata, |       ...player.metadata, | ||||||
|       ...{ |       ...{ | ||||||
|           "xesam:title": options.title, |         "xesam:title": mediaInfo.title, | ||||||
|           "xesam:artist": [options.artists], |         "xesam:artist": [mediaInfo.artists], | ||||||
|           "xesam:album": options.album, |         "xesam:album": mediaInfo.album, | ||||||
|           "mpris:artUrl": options.image, |         "mpris:artUrl": mediaInfo.image, | ||||||
|           "mpris:length": convertDuration(options.duration) * 1000 * 1000, |         "mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000, | ||||||
|         "mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(), |         "mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(), | ||||||
|       }, |       }, | ||||||
|  |       ...ObjectToDotNotation(mediaInfo, "custom:"), | ||||||
|     }; |     }; | ||||||
|       player.playbackStatus = options.status == statuses.paused ? "Paused" : "Playing"; |     player.playbackStatus = mediaInfo.status === MediaStatus.paused ? "Paused" : "Playing"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Update the listenbrainz service with new data based on a few conditions | ||||||
|  |  */ | ||||||
|  | function updateListenBrainz(mediaInfo: MediaInfo) { | ||||||
|  |   if (settingsStore.get(settings.ListenBrainz.enabled)) { | ||||||
|  |     const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData; | ||||||
|  |     if ( | ||||||
|  |       (!oldData && mediaInfo.status === MediaStatus.playing) || | ||||||
|  |       (oldData && oldData.title !== mediaInfo.title) | ||||||
|  |     ) { | ||||||
|  |       if (!scrobbleWaitingForDelay) { | ||||||
|  |         scrobbleWaitingForDelay = true; | ||||||
|  |         clearTimeout(currentListenBrainzDelayId); | ||||||
|  |         currentListenBrainzDelayId = setTimeout( | ||||||
|  |           () => { | ||||||
|  |             ListenBrainz.scrobble( | ||||||
|  |               mediaInfo.title, | ||||||
|  |               mediaInfo.artists, | ||||||
|  |               mediaInfo.status, | ||||||
|  |               convertDuration(mediaInfo.duration) | ||||||
|  |             ); | ||||||
|  |             scrobbleWaitingForDelay = false; | ||||||
|  |           }, | ||||||
|  |           settingsStore.get(settings.ListenBrainz.delay) ?? 0 | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -394,23 +542,6 @@ function getTrackID() { | |||||||
|   return window.location; |   return window.location; | ||||||
| } | } | ||||||
|  |  | ||||||
| function updateMediaSession(options: Options) { |  | ||||||
|   if ("mediaSession" in navigator) { |  | ||||||
|     navigator.mediaSession.metadata = new MediaMetadata({ |  | ||||||
|       title: options.title, |  | ||||||
|       artist: options.artists, |  | ||||||
|       album: options.album, |  | ||||||
|       artwork: [ |  | ||||||
|         { |  | ||||||
|           src: options.icon, |  | ||||||
|           sizes: "640x640", |  | ||||||
|           type: "image/png", |  | ||||||
|         }, |  | ||||||
|       ], |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Watch for song changes and update title + notify |  * Watch for song changes and update title + notify | ||||||
|  */ |  */ | ||||||
| @@ -418,30 +549,55 @@ setInterval(function () { | |||||||
|   const title = elements.getText("title"); |   const title = elements.getText("title"); | ||||||
|   const artistsArray = elements.getArtistsArray(); |   const artistsArray = elements.getArtistsArray(); | ||||||
|   const artistsString = elements.getArtistsString(artistsArray); |   const artistsString = elements.getArtistsString(artistsArray); | ||||||
|   skipArtistsIfFoundInSkippedArtistsList(artistsArray); |  | ||||||
|  |  | ||||||
|   const album = elements.getAlbumName(); |  | ||||||
|   const current = elements.getText("current"); |  | ||||||
|   const duration = elements.getText("duration"); |  | ||||||
|   const songDashArtistTitle = `${title} - ${artistsString}`; |   const songDashArtistTitle = `${title} - ${artistsString}`; | ||||||
|  |   const staticTitle = "TIDAL Hi-Fi"; | ||||||
|  |   const titleOrArtistsChanged = currentSong !== songDashArtistTitle; | ||||||
|  |   const current = elements.getText("current"); | ||||||
|   const currentStatus = getCurrentlyPlayingStatus(); |   const currentStatus = getCurrentlyPlayingStatus(); | ||||||
|   const options = { |   const shuffleState = getCurrentShuffleState(); | ||||||
|  |   const repeatState = getCurrentRepeatState(); | ||||||
|  |  | ||||||
|  |   const playStateChanged = currentStatus != currentlyPlaying; | ||||||
|  |   const shuffleStateChanged = shuffleState != currentShuffleState; | ||||||
|  |   const repeatStateChanged = repeatState != currentRepeatState; | ||||||
|  |  | ||||||
|  |   const hasStateChanged = playStateChanged || shuffleStateChanged || repeatStateChanged; | ||||||
|  |  | ||||||
|  |   // update info if song changed or was just paused/resumed | ||||||
|  |   if (titleOrArtistsChanged || hasStateChanged) { | ||||||
|  |     if (playStateChanged) currentlyPlaying = currentStatus; | ||||||
|  |     if (shuffleStateChanged) currentShuffleState = shuffleState; | ||||||
|  |     if (repeatStateChanged) currentRepeatState = repeatState; | ||||||
|  |  | ||||||
|  |     skipArtistsIfFoundInSkippedArtistsList(artistsArray); | ||||||
|  |     const album = elements.getAlbumName(); | ||||||
|  |     const duration = elements.getText("duration"); | ||||||
|  |     const options: MediaInfo = { | ||||||
|       title, |       title, | ||||||
|       artists: artistsString, |       artists: artistsString, | ||||||
|       album: album, |       album: album, | ||||||
|  |       playingFrom: elements.getText("playing_from"), | ||||||
|       status: currentStatus, |       status: currentStatus, | ||||||
|       url: getTrackURL(), |       url: getTrackURL(), | ||||||
|       current, |       current, | ||||||
|  |       currentInSeconds: convertDurationToSeconds(current), | ||||||
|       duration, |       duration, | ||||||
|     "app-name": appName, |       durationInSeconds: convertDurationToSeconds(duration), | ||||||
|       image: "", |       image: "", | ||||||
|       icon: "", |       icon: "", | ||||||
|  |       favorite: elements.isFavorite(), | ||||||
|  |  | ||||||
|  |       player: { | ||||||
|  |         status: currentStatus, | ||||||
|  |         shuffle: shuffleState, | ||||||
|  |         repeat: repeatState, | ||||||
|  |       }, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|   const titleOrArtistsChanged = currentSong !== songDashArtistTitle; |  | ||||||
|  |  | ||||||
|     // update title, url and play info with new info |     // update title, url and play info with new info | ||||||
|   setTitle(songDashArtistTitle); |     settingsStore.get(settings.staticWindowTitle) | ||||||
|  |       ? setTitle(staticTitle) | ||||||
|  |       : setTitle(songDashArtistTitle); | ||||||
|     getTrackURL(); |     getTrackURL(); | ||||||
|     currentSong = songDashArtistTitle; |     currentSong = songDashArtistTitle; | ||||||
|     currentPlayStatus = currentStatus; |     currentPlayStatus = currentStatus; | ||||||
| @@ -467,13 +623,14 @@ setInterval(function () { | |||||||
|       } |       } | ||||||
|     }).then(() => { |     }).then(() => { | ||||||
|       updateMediaInfo(options, titleOrArtistsChanged); |       updateMediaInfo(options, titleOrArtistsChanged); | ||||||
|     if (titleOrArtistsChanged) { |  | ||||||
|       updateMediaSession(options); |  | ||||||
|       if (settingsStore.get(settings.ListenBrainz.enabled)) { |  | ||||||
|         ListenBrainz.scrobble(options.title, options.artists, options.status, convertDuration(options.duration)); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     }); |     }); | ||||||
|  |   } else { | ||||||
|  |     // just update the time | ||||||
|  |     updateMediaInfo( | ||||||
|  |       { ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } }, | ||||||
|  |       false | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * automatically skip a song if the artists are found in the list of artists to skip |    * automatically skip a song if the artists are found in the list of artists to skip | ||||||
| @@ -494,61 +651,8 @@ setInterval(function () { | |||||||
|   } |   } | ||||||
| }, getUpdateFrequency()); | }, getUpdateFrequency()); | ||||||
|  |  | ||||||
| if (process.platform === "linux" && settingsStore.get(settings.mpris)) { | addMPRIS(); | ||||||
|   try { | addCustomCss(app); | ||||||
|     player = Player({ |  | ||||||
|       name: "tidal-hifi", |  | ||||||
|       identity: "tidal-hifi", |  | ||||||
|       supportedUriSchemes: ["file"], |  | ||||||
|       supportedMimeTypes: [ |  | ||||||
|         "audio/mpeg", |  | ||||||
|         "audio/flac", |  | ||||||
|         "audio/x-flac", |  | ||||||
|         "application/ogg", |  | ||||||
|         "audio/wav", |  | ||||||
|       ], |  | ||||||
|       supportedInterfaces: ["player"], |  | ||||||
|       desktopEntry: "tidal-hifi", |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // Events |  | ||||||
|     const events = { |  | ||||||
|       next: "next", |  | ||||||
|       previous: "previous", |  | ||||||
|       pause: "pause", |  | ||||||
|       playpause: "playpause", |  | ||||||
|       stop: "stop", |  | ||||||
|       play: "play", |  | ||||||
|       loopStatus: "repeat", |  | ||||||
|       shuffle: "shuffle", |  | ||||||
|       seek: "seek", |  | ||||||
|     } as { [key: string]: string }; |  | ||||||
|     Object.keys(events).forEach(function (eventName) { |  | ||||||
|       player.on(eventName, function () { |  | ||||||
|         const eventValue = events[eventName]; |  | ||||||
|         switch (events[eventValue]) { |  | ||||||
|           case events.playpause: |  | ||||||
|             playPause(); |  | ||||||
|             break; |  | ||||||
|  |  | ||||||
|           default: |  | ||||||
|             elements.click(eventValue); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     // Override get position function |  | ||||||
|     player.getPosition = function () { |  | ||||||
|       return convertDuration(elements.getText("current")) * 1000 * 1000; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     player.on("quit", function () { |  | ||||||
|       app.quit(); |  | ||||||
|     }); |  | ||||||
|   } catch (exception) { |  | ||||||
|     console.log("player api not working"); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| addCustomCss(); |  | ||||||
| addHotKeys(); | addHotKeys(); | ||||||
| addIPCEventListeners(); | addIPCEventListeners(); | ||||||
| addFullScreenListeners(); | addFullScreenListeners(); | ||||||
|   | |||||||
| @@ -1,78 +1,142 @@ | |||||||
| import { Client } from "discord-rpc"; | import { Client, SetActivity } from "@xhayper/discord-rpc"; | ||||||
| import { app, ipcMain } from "electron"; | import { app, ipcMain } from "electron"; | ||||||
| import { globalEvents } from "../constants/globalEvents"; | import { globalEvents } from "../constants/globalEvents"; | ||||||
|  | import { settings } from "../constants/settings"; | ||||||
|  | import { Logger } from "../features/logger"; | ||||||
|  | import { convertDurationToSeconds } from "../features/time/parse"; | ||||||
| import { MediaStatus } from "../models/mediaStatus"; | import { MediaStatus } from "../models/mediaStatus"; | ||||||
| import { mediaInfo } from "./mediaInfo"; | import { mediaInfo } from "./mediaInfo"; | ||||||
|  | import { settingsStore } from "./settings"; | ||||||
|  |  | ||||||
| const clientId = "833617820704440341"; | const clientId = "833617820704440341"; | ||||||
|  |  | ||||||
| function timeToSeconds(timeArray: string[]) { |  | ||||||
|   const minutes = parseInt(timeArray[0]) * 1; |  | ||||||
|   const seconds = minutes * 60 + parseInt(timeArray[1]) * 1; |  | ||||||
|   return seconds; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export let rpc: Client; | export let rpc: Client; | ||||||
|  |  | ||||||
|  | const ACTIVITY_LISTENING = 2; | ||||||
|  | const MAX_RETRIES = 5; | ||||||
|  | const RETRY_DELAY = 10000; | ||||||
|  |  | ||||||
| const observer = () => { | const observer = () => { | ||||||
|   if (mediaInfo.status == MediaStatus.paused && rpc) { |   if (rpc) { | ||||||
|     rpc.setActivity(idleStatus); |     updateActivity(); | ||||||
|   } else if (rpc) { |   } | ||||||
|     const currentSeconds = timeToSeconds(mediaInfo.current.split(":")); | }; | ||||||
|     const durationSeconds = timeToSeconds(mediaInfo.duration.split(":")); |  | ||||||
|     const date = new Date(); | const defaultPresence = { | ||||||
|     const now = (date.getTime() / 1000) | 0; |   largeImageKey: "tidal-hifi-icon", | ||||||
|     const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds)); |   largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`, | ||||||
|     if (mediaInfo.url) { |   instance: false, | ||||||
|       rpc.setActivity({ |   type: ACTIVITY_LISTENING | ||||||
|         ...idleStatus, | }; | ||||||
|         ...{ |  | ||||||
|           details: `Listening to ${mediaInfo.title}`, | const updateActivity = () => { | ||||||
|           state: mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)", |   const showIdle = settingsStore.get<string, boolean>(settings.discord.showIdle) ?? true; | ||||||
|           startTimestamp: now, |   if (mediaInfo.status === MediaStatus.paused && !showIdle) { | ||||||
|           endTimestamp: remaining, |     rpc.user?.clearActivity(); | ||||||
|           largeImageKey: mediaInfo.image, |  | ||||||
|           largeImageText: mediaInfo.album ? mediaInfo.album : `${idleStatus.largeImageText}`, |  | ||||||
|           buttons: [{ label: "Play on Tidal", url: mediaInfo.url }], |  | ||||||
|         }, |  | ||||||
|       }); |  | ||||||
|   } else { |   } else { | ||||||
|       rpc.setActivity({ |     rpc.user?.setActivity(getActivity()); | ||||||
|         ...idleStatus, |   } | ||||||
|         ...{ | }; | ||||||
|           details: `Watching ${mediaInfo.title}`, |  | ||||||
|           state: mediaInfo.artists, | const getActivity = (): SetActivity => { | ||||||
|           startTimestamp: now, |   const presence: SetActivity = { ...defaultPresence }; | ||||||
|           endTimestamp: remaining, |  | ||||||
|         }, |   if (mediaInfo.status === MediaStatus.paused) { | ||||||
|       }); |     presence.details = | ||||||
|  |       settingsStore.get<string, string>(settings.discord.idleText) ?? "Browsing Tidal"; | ||||||
|  |   } else { | ||||||
|  |     const showSong = settingsStore.get<string, boolean>(settings.discord.showSong) ?? false; | ||||||
|  |     if (showSong) { | ||||||
|  |       const { includeTimestamps, detailsPrefix, buttonText } = getFromStore(); | ||||||
|  |       includeTimeStamps(includeTimestamps); | ||||||
|  |       setPresenceFromMediaInfo(detailsPrefix, buttonText); | ||||||
|  |     } else { | ||||||
|  |       presence.details = | ||||||
|  |         settingsStore.get<string, string>(settings.discord.usingText) ?? "Playing media on TIDAL"; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return presence; | ||||||
|  |  | ||||||
|  |   function getFromStore() { | ||||||
|  |     const includeTimestamps = | ||||||
|  |       settingsStore.get<string, boolean>(settings.discord.includeTimestamps) ?? true; | ||||||
|  |     const detailsPrefix = | ||||||
|  |       settingsStore.get<string, string>(settings.discord.detailsPrefix) ?? "Listening to "; | ||||||
|  |     const buttonText = | ||||||
|  |       settingsStore.get<string, string>(settings.discord.buttonText) ?? "Play on TIDAL"; | ||||||
|  |  | ||||||
|  |     return { includeTimestamps, detailsPrefix, buttonText }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Pad a string using spaces to at least 2 characters | ||||||
|  |    * @param input string to pad with 2 characters | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   function pad(input: string): string { | ||||||
|  |     return input.padEnd(2, " "); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) { | ||||||
|  |     // discord requires a minimum of 2 characters | ||||||
|  |     const title = pad(mediaInfo.title); | ||||||
|  |     const album = pad(mediaInfo.album); | ||||||
|  |     const artists = pad(mediaInfo.artists); | ||||||
|  |  | ||||||
|  |     if (mediaInfo.url) { | ||||||
|  |       presence.details = `${detailsPrefix}${title}`; | ||||||
|  |       presence.state = artists ? artists : "unknown artist(s)"; | ||||||
|  |       presence.largeImageKey = mediaInfo.image; | ||||||
|  |       if (album) { | ||||||
|  |         presence.largeImageText = album; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       presence.buttons = [{ label: buttonText, url: mediaInfo.url }]; | ||||||
|  |     } else { | ||||||
|  |       presence.details = `Watching ${title}`; | ||||||
|  |       presence.state = artists; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function includeTimeStamps(includeTimestamps: boolean) { | ||||||
|  |     if (includeTimestamps) { | ||||||
|  |       const currentSeconds = convertDurationToSeconds(mediaInfo.current); | ||||||
|  |       const durationSeconds = convertDurationToSeconds(mediaInfo.duration); | ||||||
|  |       const date = new Date(); | ||||||
|  |       const now = Math.floor(date.getTime() / 1000); | ||||||
|  |       presence.startTimestamp = now - currentSeconds; | ||||||
|  |       presence.endTimestamp = presence.startTimestamp + durationSeconds; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const idleStatus = { | /** | ||||||
|   details: `Browsing Tidal`, |  * Try to login to RPC and retry if it errors | ||||||
|   largeImageKey: "tidal-hifi-icon", |  * @param retryCount Max retry count | ||||||
|   largeImageText: `Tidal HiFi ${app.getVersion()}`, |  */ | ||||||
|   instance: false, | const connectWithRetry = async (retryCount = 0) => { | ||||||
|  |   try { | ||||||
|  |     await rpc.login(); | ||||||
|  |     Logger.log('Connected to Discord'); | ||||||
|  |     rpc.on("ready", updateActivity); | ||||||
|  |     Object.values(globalEvents).forEach(event => ipcMain.on(event, observer)); | ||||||
|  |   } catch (error) { | ||||||
|  |     if (retryCount < MAX_RETRIES) { | ||||||
|  |       Logger.log(`Failed to connect to Discord, retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`); | ||||||
|  |       setTimeout(() => connectWithRetry(retryCount + 1), RETRY_DELAY); | ||||||
|  |     } else { | ||||||
|  |       Logger.log('Failed to connect to Discord after maximum retry attempts'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Set up the discord rpc and listen on globalEvents.updateInfo |  * Set up the discord rpc and listen on globalEvents.updateInfo | ||||||
|  */ |  */ | ||||||
| export const initRPC = () => { | export const initRPC = () => { | ||||||
|   rpc = new Client({ transport: "ipc" }); |   rpc = new Client({ transport: {type: "ipc"}, clientId }); | ||||||
|   rpc.login({ clientId }).then( |   connectWithRetry(); | ||||||
|     () => { |  | ||||||
|       rpc.on("ready", () => { |  | ||||||
|         rpc.setActivity(idleStatus); |  | ||||||
|       }); |  | ||||||
|       ipcMain.on(globalEvents.updateInfo, observer); |  | ||||||
|     }, |  | ||||||
|     () => { |  | ||||||
|       console.error("Can't connect to Discord, is it running?"); |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -80,7 +144,7 @@ export const initRPC = () => { | |||||||
|  */ |  */ | ||||||
| export const unRPC = () => { | export const unRPC = () => { | ||||||
|   if (rpc) { |   if (rpc) { | ||||||
|     rpc.clearActivity(); |     rpc.user?.clearActivity(); | ||||||
|     rpc.destroy(); |     rpc.destroy(); | ||||||
|     rpc = null; |     rpc = null; | ||||||
|     ipcMain.removeListener(globalEvents.updateInfo, observer); |     ipcMain.removeListener(globalEvents.updateInfo, observer); | ||||||
|   | |||||||
| @@ -1,66 +0,0 @@ | |||||||
| import { BrowserWindow, dialog } from "electron"; |  | ||||||
| import express, { Response } from "express"; |  | ||||||
| import fs from "fs"; |  | ||||||
| import { globalEvents } from "./../constants/globalEvents"; |  | ||||||
| import { statuses } from "./../constants/statuses"; |  | ||||||
| import { mediaInfo } from "./mediaInfo"; |  | ||||||
| import { settingsStore } from "./settings"; |  | ||||||
| import { settings } from "../constants/settings"; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Function to enable tidal-hifi's express api |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| // expressModule.run = function (mainWindow) |  | ||||||
| export const startExpress = (mainWindow: BrowserWindow) => { |  | ||||||
|   /** |  | ||||||
|    * 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); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const expressApp = express(); |  | ||||||
|   expressApp.get("/", (req, res) => res.send("Hello World!")); |  | ||||||
|   expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists })); |  | ||||||
|   expressApp.get("/image", (req, res) => { |  | ||||||
|     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"); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (settingsStore.get(settings.playBackControl)) { |  | ||||||
|     expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play)); |  | ||||||
|     expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause)); |  | ||||||
|     expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next)); |  | ||||||
|     expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous)); |  | ||||||
|     expressApp.get("/playpause", (req, res) => { |  | ||||||
|       if (mediaInfo.status == statuses.playing) { |  | ||||||
|         handleGlobalEvent(res, globalEvents.pause); |  | ||||||
|       } else { |  | ||||||
|         handleGlobalEvent(res, globalEvents.play); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const port = settingsStore.get<string, number>(settings.apiSettings.port); |  | ||||||
|  |  | ||||||
|   const expressInstance = expressApp.listen(port, "127.0.0.1"); |  | ||||||
|   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); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| @@ -1,35 +1,45 @@ | |||||||
| import { MediaInfo } from "../models/mediaInfo"; | import { MediaInfo } from "../models/mediaInfo"; | ||||||
| import { statuses } from "./../constants/statuses"; | import { MediaStatus } from "../models/mediaStatus"; | ||||||
|  | import { RepeatState } from "../models/repeatState"; | ||||||
|  |  | ||||||
| export const mediaInfo = { | const defaultInfo: MediaInfo = { | ||||||
|   title: "", |   title: "", | ||||||
|   artists: "", |   artists: "", | ||||||
|   album: "", |   album: "", | ||||||
|   icon: "", |   icon: "", | ||||||
|   status: statuses.paused, |   playingFrom: "", | ||||||
|  |   status: MediaStatus.paused, | ||||||
|   url: "", |   url: "", | ||||||
|   current: "", |   current: "", | ||||||
|  |   currentInSeconds: 0, | ||||||
|   duration: "", |   duration: "", | ||||||
|  |   durationInSeconds: 0, | ||||||
|   image: "tidal-hifi-icon", |   image: "tidal-hifi-icon", | ||||||
|  |   favorite: false, | ||||||
|  |  | ||||||
|  |   player: { | ||||||
|  |     status: MediaStatus.paused, | ||||||
|  |     shuffle: false, | ||||||
|  |     repeat: RepeatState.off, | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export let mediaInfo: MediaInfo = { ...defaultInfo }; | ||||||
|  |  | ||||||
| export const updateMediaInfo = (arg: MediaInfo) => { | export const updateMediaInfo = (arg: MediaInfo) => { | ||||||
|   mediaInfo.title = propOrDefault(arg.title); |   mediaInfo = { ...defaultInfo, ...arg }; | ||||||
|   mediaInfo.artists = propOrDefault(arg.artists); |   mediaInfo.url = toUniversalUrl(mediaInfo.url); | ||||||
|   mediaInfo.album = propOrDefault(arg.album); |  | ||||||
|   mediaInfo.icon = propOrDefault(arg.icon); |  | ||||||
|   mediaInfo.url = propOrDefault(arg.url); |  | ||||||
|   mediaInfo.status = propOrDefault(arg.status); |  | ||||||
|   mediaInfo.current = propOrDefault(arg.current); |  | ||||||
|   mediaInfo.duration = propOrDefault(arg.duration); |  | ||||||
|   mediaInfo.image = propOrDefault(arg.image); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Return the property or a default value |  * Append the universal link syntax (?u) to any url | ||||||
|  * @param {*} prop property to check |  * @param url url to append the universal link syntax to | ||||||
|  * @param {*} defaultValue defaults to "" |  * @returns url with `?u` appended, or the original value of url if falsy | ||||||
|  */ |  */ | ||||||
| function propOrDefault(prop: string, defaultValue = "") { | function toUniversalUrl(url: string) { | ||||||
|   return prop ? prop : defaultValue; |   if (url) { | ||||||
|  |     const queryParamsSet = url.indexOf("?"); | ||||||
|  |     return queryParamsSet > -1 ? `${url}&u` : `${url}?u`; | ||||||
|  |   } | ||||||
|  |   return url; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,6 +11,23 @@ const settingsMenuEntry = { | |||||||
|   accelerator: "Control+=", |   accelerator: "Control+=", | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const tidalMagazineEntry = { | ||||||
|  |   label: "Magazine", | ||||||
|  |   click() { | ||||||
|  |     const magazineWindow = new BrowserWindow({ | ||||||
|  |       autoHideMenuBar: true, | ||||||
|  |       webPreferences: { | ||||||
|  |         sandbox: false, | ||||||
|  |         plugins: true, | ||||||
|  |         devTools: true, // I like tinkering, others might too | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     magazineWindow.loadURL("https://tidal.com/magazine/"); | ||||||
|  |     magazineWindow.show(); | ||||||
|  |   }, | ||||||
|  |   accelerator: "Control+M", | ||||||
|  | }; | ||||||
|  |  | ||||||
| const quitMenuEntry = { | const quitMenuEntry = { | ||||||
|   label: "Quit", |   label: "Quit", | ||||||
|   click() { |   click() { | ||||||
| @@ -33,7 +50,6 @@ export const getMenu = function (mainWindow: BrowserWindow) { | |||||||
|           { |           { | ||||||
|             label: name, |             label: name, | ||||||
|             submenu: [ |             submenu: [ | ||||||
|               { role: "about" }, |  | ||||||
|               settingsMenuEntry, |               settingsMenuEntry, | ||||||
|               { type: "separator" }, |               { type: "separator" }, | ||||||
|               { role: "services" }, |               { role: "services" }, | ||||||
| @@ -42,6 +58,7 @@ export const getMenu = function (mainWindow: BrowserWindow) { | |||||||
|               { role: "hideothers" }, |               { role: "hideothers" }, | ||||||
|               { role: "unhide" }, |               { role: "unhide" }, | ||||||
|               { type: "separator" }, |               { type: "separator" }, | ||||||
|  |               tidalMagazineEntry, | ||||||
|               quitMenuEntry, |               quitMenuEntry, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
| @@ -49,7 +66,7 @@ export const getMenu = function (mainWindow: BrowserWindow) { | |||||||
|       : []), |       : []), | ||||||
|     { |     { | ||||||
|       label: "File", |       label: "File", | ||||||
|       submenu: [settingsMenuEntry, isMac ? { role: "close" } : quitMenuEntry], |       submenu: [settingsMenuEntry, tidalMagazineEntry, isMac ? { role: "close" } : quitMenuEntry], | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       label: "Edit", |       label: "Edit", | ||||||
| @@ -101,13 +118,8 @@ export const getMenu = function (mainWindow: BrowserWindow) { | |||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|     settingsMenuEntry, |     settingsMenuEntry, | ||||||
|     { |  | ||||||
|       label: "About", |  | ||||||
|       click() { |  | ||||||
|         showSettingsWindow("about"); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     toggleWindow, |     toggleWindow, | ||||||
|  |     tidalMagazineEntry, | ||||||
|     quitMenuEntry, |     quitMenuEntry, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								src/scripts/objectUtilities.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|  | export const ObjectToDotNotation = (obj: any, prefix: string = "", target: any = {}) => { | ||||||
|  |   Object.keys(obj).forEach((key: string) => { | ||||||
|  |     if (typeof obj[key] === "object" && obj[key] !== null) { | ||||||
|  |       ObjectToDotNotation(obj[key], prefix + key + ".", target); | ||||||
|  |     } else { | ||||||
|  |       const dotLocation = prefix + key; | ||||||
|  |       target[dotLocation] = obj[key]; | ||||||
|  |       return target; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   return target; | ||||||
|  | }; | ||||||
| @@ -1,31 +1,65 @@ | |||||||
| import Store from "electron-store"; | import Store from "electron-store"; | ||||||
|  |  | ||||||
| import { settings } from "../constants/settings"; | import { BrowserWindow, shell } from "electron"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import { BrowserWindow } from "electron"; | import { settings } from "../constants/settings"; | ||||||
|  |  | ||||||
| let settingsWindow: BrowserWindow; | let settingsWindow: BrowserWindow; | ||||||
|  | /** | ||||||
|  |  * Build a migration step for several settings. | ||||||
|  |  * All settings will be checked and set to the default if non-existent. | ||||||
|  |  * @param version | ||||||
|  |  * @param migrationStore | ||||||
|  |  * @param options | ||||||
|  |  */ | ||||||
|  | const buildMigration = ( | ||||||
|  |   version: string, | ||||||
|  |   migrationStore: { get: (str: string) => string; set: (str: string, val: unknown) => void }, | ||||||
|  |   options: Array<{ key: string; value: unknown }> | ||||||
|  | ) => { | ||||||
|  |   console.log(`running migrations for ${version}`); | ||||||
|  |   options.forEach(({ key, value }) => { | ||||||
|  |     const valueToSet = migrationStore.get(key) ?? value; | ||||||
|  |     console.log(`  - setting ${key} to ${value}`); | ||||||
|  |     migrationStore.set(key, valueToSet); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const settingsStore = new Store({ | export const settingsStore = new Store({ | ||||||
|   defaults: { |   defaults: { | ||||||
|     adBlock: false, |     adBlock: false, | ||||||
|  |     advanced: { | ||||||
|  |       tidalUrl: "https://listen.tidal.com", | ||||||
|  |     }, | ||||||
|     api: true, |     api: true, | ||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       port: 47836, |       port: 47836, | ||||||
|  |       hostname: "127.0.0.1", | ||||||
|     }, |     }, | ||||||
|     customCSS: [], |     customCSS: [], | ||||||
|     disableBackgroundThrottle: true, |     disableBackgroundThrottle: true, | ||||||
|     disableHardwareMediaKeys: false, |     disableHardwareMediaKeys: false, | ||||||
|     enableCustomHotkeys: false, |     enableCustomHotkeys: false, | ||||||
|     enableDiscord: false, |     enableDiscord: false, | ||||||
|  |     discord: { | ||||||
|  |       showSong: true, | ||||||
|  |       showIdle: true, | ||||||
|  |       idleText: "Browsing Tidal", | ||||||
|  |       usingText: "Playing media on TIDAL", | ||||||
|  |       includeTimestamps: true, | ||||||
|  |       detailsPrefix: "Listening to ", | ||||||
|  |       buttonText: "Play on Tidal", | ||||||
|  |     }, | ||||||
|     ListenBrainz: { |     ListenBrainz: { | ||||||
|       enabled: false, |       enabled: false, | ||||||
|       api: "https://api.listenbrainz.org", |       api: "https://api.listenbrainz.org", | ||||||
|       token: "", |       token: "", | ||||||
|  |       delay: 5000, | ||||||
|     }, |     }, | ||||||
|     flags: { |     flags: { | ||||||
|       gpuRasterization: true, |  | ||||||
|       disableHardwareMediaKeys: false, |       disableHardwareMediaKeys: false, | ||||||
|  |       enableWaylandSupport: true, | ||||||
|  |       gpuRasterization: true, | ||||||
|     }, |     }, | ||||||
|     menuBar: true, |     menuBar: true, | ||||||
|     minimizeOnClose: false, |     minimizeOnClose: false, | ||||||
| @@ -35,6 +69,7 @@ export const settingsStore = new Store({ | |||||||
|     singleInstance: true, |     singleInstance: true, | ||||||
|     skipArtists: false, |     skipArtists: false, | ||||||
|     skippedArtists: [""], |     skippedArtists: [""], | ||||||
|  |     staticWindowTitle: false, | ||||||
|     theme: "none", |     theme: "none", | ||||||
|     trayIcon: true, |     trayIcon: true, | ||||||
|     updateFrequency: 500, |     updateFrequency: 500, | ||||||
| @@ -48,6 +83,43 @@ export const settingsStore = new Store({ | |||||||
|         migrationStore.get("disableHardwareMediaKeys") ?? false |         migrationStore.get("disableHardwareMediaKeys") ?? false | ||||||
|       ); |       ); | ||||||
|     }, |     }, | ||||||
|  |     "5.7.0": (migrationStore) => { | ||||||
|  |       console.log("running migrations for 5.7.0"); | ||||||
|  |       migrationStore.set( | ||||||
|  |         settings.ListenBrainz.delay, | ||||||
|  |         migrationStore.get(settings.ListenBrainz.delay) ?? 5000 | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     "5.8.0": (migrationStore) => { | ||||||
|  |       console.log("running migrations for 5.8.0"); | ||||||
|  |       migrationStore.set( | ||||||
|  |         settings.discord.includeTimestamps, | ||||||
|  |         migrationStore.get(settings.discord.includeTimestamps) ?? true | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     "5.9.0": (migrationStore) => { | ||||||
|  |       buildMigration("5.9.0", migrationStore, [ | ||||||
|  |         { key: settings.discord.showSong, value: "true" }, | ||||||
|  |         { key: settings.discord.idleText, value: "Browsing Tidal" }, | ||||||
|  |         { | ||||||
|  |           key: settings.discord.usingText, | ||||||
|  |           value: "Playing media on TIDAL", | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }, | ||||||
|  |     "5.14.0": (migrationStore) => { | ||||||
|  |       buildMigration("5.14.0", migrationStore, [ | ||||||
|  |         { key: settings.apiSettings.hostname, value: "127.0.0.1" }, | ||||||
|  |       ]); | ||||||
|  |     }, | ||||||
|  |     "5.15.0": (migrationStore) => { | ||||||
|  |       buildMigration("5.15.0", migrationStore, [ | ||||||
|  |         { key: settings.advanced.tidalUrl, value: "https://listen.tidal.com" }, | ||||||
|  |       ]); | ||||||
|  |     }, | ||||||
|  |     "5.16.0": (migrationStore) => { | ||||||
|  |       buildMigration("5.16.0", migrationStore, [{ key: settings.discord.showIdle, value: "true" }]); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -58,8 +130,8 @@ const settingsModule = { | |||||||
|  |  | ||||||
| export const createSettingsWindow = function () { | export const createSettingsWindow = function () { | ||||||
|   settingsWindow = new BrowserWindow({ |   settingsWindow = new BrowserWindow({ | ||||||
|     width: 700, |     width: 650, | ||||||
|     height: 600, |     height: 700, | ||||||
|     resizable: true, |     resizable: true, | ||||||
|     show: false, |     show: false, | ||||||
|     transparent: true, |     transparent: true, | ||||||
| @@ -81,10 +153,18 @@ export const createSettingsWindow = function () { | |||||||
|  |  | ||||||
|   settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`); |   settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`); | ||||||
|  |  | ||||||
|  |   settingsWindow.webContents.setWindowOpenHandler(({ url }) => { | ||||||
|  |     shell.openExternal(url); | ||||||
|  |     return { action: "deny" }; | ||||||
|  |   }); | ||||||
|   settingsModule.settingsWindow = settingsWindow; |   settingsModule.settingsWindow = settingsWindow; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const showSettingsWindow = function (tab = "general") { | export const showSettingsWindow = function (tab = "general") { | ||||||
|  |   if (!settingsWindow) { | ||||||
|  |     console.log("Settings window is not initialized. Attempting to create it."); | ||||||
|  |     createSettingsWindow(); | ||||||
|  |   } | ||||||
|   settingsWindow.webContents.send("goToTab", tab); |   settingsWindow.webContents.send("goToTab", tab); | ||||||
|  |  | ||||||
|   // refresh data just before showing the window |   // refresh data just before showing the window | ||||||
| @@ -98,3 +178,25 @@ export const hideSettingsWindow = function () { | |||||||
| export const closeSettingsWindow = function () { | export const closeSettingsWindow = function () { | ||||||
|   settingsWindow = null; |   settingsWindow = null; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * add artists to the list of skipped artists | ||||||
|  |  * @param artists list of artists to append | ||||||
|  |  */ | ||||||
|  | export const addSkippedArtists = (artists: string[]) => { | ||||||
|  |   const { skippedArtists } = settings; | ||||||
|  |   const previousStoreValue = settingsStore.get<string, string[]>(skippedArtists); | ||||||
|  |   settingsStore.set(skippedArtists, Array.from(new Set([...previousStoreValue, ...artists]))); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Remove artists from the list of skipped artists | ||||||
|  |  * @param artists list of artists to remove | ||||||
|  |  */ | ||||||
|  | export const removeSkippedArtists = (artists: string[]) => { | ||||||
|  |   const { skippedArtists } = settings; | ||||||
|  |   const previousStoreValue = settingsStore.get<string, string[]>(skippedArtists); | ||||||
|  |   const filteredArtists = previousStoreValue.filter((value) => ![...artists].includes(value)); | ||||||
|  |  | ||||||
|  |   settingsStore.set(skippedArtists, filteredArtists); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -74,7 +74,7 @@ button.feedBell--kvAbD { | |||||||
| .container--PFTHk { | .container--PFTHk { | ||||||
|   background-color: var(--right-queue-background); |   background-color: var(--right-queue-background); | ||||||
| } | } | ||||||
| .container--cl4MJ{ | .container--cl4MJ { | ||||||
|   background-color: var(--search-background); |   background-color: var(--search-background); | ||||||
| } | } | ||||||
| .searchFieldHighlighted--Fitvs { | .searchFieldHighlighted--Fitvs { | ||||||
| @@ -83,3 +83,122 @@ button.feedBell--kvAbD { | |||||||
| .searchField--EGBSq { | .searchField--EGBSq { | ||||||
|   background-color: var(--search-background); |   background-color: var(--search-background); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Settings window styling | ||||||
|  |  | ||||||
|  | .settings-window { | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  | .settings-window__wrapper { | ||||||
|  |   background: var(--main-background); | ||||||
|  |   box-shadow: inset 0 0 2px 0 var(--main-feed-button-background); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .settings-window__close-button:hover { | ||||||
|  |   background: var(--main-feed-button-background); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .settings input:checked + label { | ||||||
|  |   border-bottom: 2px solid var(--player-control-active-button); | ||||||
|  |   color: var(--player-control-active-button); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .tabs::-webkit-scrollbar-thumb { | ||||||
|  |   background-color: #404248; | ||||||
|  |   box-shadow: inset 0 0 10px 2px var(--search-background); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .group { | ||||||
|  |   border-bottom: 1px solid #333; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .group__description p { | ||||||
|  |   color: var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  | .group__description .text-input { | ||||||
|  |   border-bottom: solid 1px #333; | ||||||
|  |   color: var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  | .group__description .text-input:focus { | ||||||
|  |   border-color: var(--player-control-active-button); | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .switch input:checked + .switch__slider { | ||||||
|  |   background-color: var(--player-control-active-button); | ||||||
|  | } | ||||||
|  | .switch input:checked + .switch__slider::before { | ||||||
|  |   background-color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  | .switch input:focus + .switch__slider { | ||||||
|  |   box-shadow: inset 0 0 0 1px var(--player-control-active-button); | ||||||
|  | } | ||||||
|  | .switch__slider { | ||||||
|  |   background-color: var(--search-background); | ||||||
|  | } | ||||||
|  | .switch__slider::before { | ||||||
|  |   background-color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .textarea { | ||||||
|  |   background: var(--search-background); | ||||||
|  |   color: var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  | .textarea:focus { | ||||||
|  |   border-color: var(--player-control-active-button); | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .about-section__version a { | ||||||
|  |   background-color: #404248; | ||||||
|  |   color: var(--player-control-active-button); | ||||||
|  | } | ||||||
|  | .about-section__links a { | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  |  | ||||||
|  |   background-color: var(--search-background); | ||||||
|  | } | ||||||
|  | .about-section__links a i { | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  | .about-section__links a:hover { | ||||||
|  |   background-color: var(--player-control-favorite); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .footer__note { | ||||||
|  |   color: var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  | .footer__button { | ||||||
|  |   background: #404248; | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  | .footer__button:hover { | ||||||
|  |   background: #55585f; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .file-drop-area { | ||||||
|  |   border: 1px dashed var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  | .file-drop-area.is-active { | ||||||
|  |   background-color: #17171a; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .file-btn { | ||||||
|  |   background-color: #17171a; | ||||||
|  |   border: 1px solid var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .select-input { | ||||||
|  |   border-bottom: solid 1px #333; | ||||||
|  |   color: var(--sidebar-menu-top-text); | ||||||
|  | } | ||||||
|  | .select-input:focus { | ||||||
|  |   border-color: var(--player-control-active-button); | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|  | .select-input option { | ||||||
|  |   background-color: var(--search-background); | ||||||
|  | } | ||||||
|  | .select-input option:disabled { | ||||||
|  |   color: var(--sidebar-menu-playlist-text); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|     "sourceMap": true, |     "sourceMap": true, | ||||||
|     "allowJs": true, |     "allowJs": true, | ||||||
|     "outDir": "ts-dist", |     "outDir": "ts-dist", | ||||||
|  |     "resolveJsonModule": true, | ||||||
|     "esModuleInterop": true, |     "esModuleInterop": true, | ||||||
|     "baseUrl": ".", |     "baseUrl": ".", | ||||||
|     "paths": { |     "paths": { | ||||||
|   | |||||||