├── .browserslistrc ├── .github └── FUNDING.yml ├── .gitignore ├── LICENCE.txt ├── README.md ├── bootstrap-pm2.json ├── env.conf ├── front_src ├── App.vue ├── assets │ ├── fonts │ │ ├── Comfortaa-Bold.woff │ │ ├── Comfortaa-Light.woff │ │ ├── Comfortaa-Regular.woff │ │ ├── FuturaStd-ExtraBold.woff │ │ ├── FuturaStd-ExtraBoldOblique.woff │ │ ├── FuturaStd-Heavy.woff │ │ ├── FuturaStd-HeavyOblique.woff │ │ ├── FuturaStd-Light.woff │ │ ├── FuturaStd-LightOblique.woff │ │ ├── FuturaStd-Medium.woff │ │ └── FuturaStd-MediumOblique.woff │ ├── icons │ │ ├── back.svg │ │ ├── burger.svg │ │ ├── chat.svg │ │ ├── chat_off.svg │ │ ├── checkmark.svg │ │ ├── checkmark_white.svg │ │ ├── copy.svg │ │ ├── cross.svg │ │ ├── cross_white.svg │ │ ├── dashedBg.svg │ │ ├── delay.svg │ │ ├── delete.svg │ │ ├── dmca.svg │ │ ├── elite.svg │ │ ├── ext_link.svg │ │ ├── ext_link_white.svg │ │ ├── flex_left.svg │ │ ├── flex_right.svg │ │ ├── gear.svg │ │ ├── home.svg │ │ ├── home_logo.svg │ │ ├── home_logo_outlined.svg │ │ ├── infos.svg │ │ ├── kick.svg │ │ ├── king.svg │ │ ├── minus.svg │ │ ├── minus2.svg │ │ ├── multiplayer.svg │ │ ├── music.svg │ │ ├── mute.svg │ │ ├── obs.svg │ │ ├── offline.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── play_withBg.svg │ │ ├── playlist.svg │ │ ├── plus.svg │ │ ├── plus2.svg │ │ ├── refresh.svg │ │ ├── remove.svg │ │ ├── share.svg │ │ ├── show.svg │ │ ├── shrug.svg │ │ ├── solo.svg │ │ ├── song.svg │ │ ├── spotify.svg │ │ ├── spotify_color.svg │ │ ├── star.svg │ │ ├── stop.svg │ │ ├── stop_square.svg │ │ ├── twitch.svg │ │ ├── unmute.svg │ │ ├── user.svg │ │ ├── user_white.svg │ │ └── warning.svg │ ├── images │ │ ├── logo-1024x768.png │ │ ├── logo-300x200.png │ │ ├── logox100.png │ │ ├── logox24.png │ │ └── spotify.png │ └── loader │ │ ├── loader.svg │ │ ├── loader_bg.svg │ │ ├── loader_border.svg │ │ ├── loader_light.svg │ │ └── loader_white.svg ├── components │ ├── AudioPlayer.ts │ ├── AutoCompleteForm.vue │ ├── BouncingLoader.vue │ ├── Button.vue │ ├── ChatWindow.vue │ ├── CountDown.vue │ ├── ExpertModeForm.vue │ ├── ExpertModeState.vue │ ├── GameParams.vue │ ├── GroupLobbyUser.vue │ ├── GroupUserList.vue │ ├── IncrementForm.vue │ ├── InfiniteList.ts │ ├── NeedInteractionLayer.vue │ ├── NoPlaylist.vue │ ├── PlayListEntry.vue │ ├── PlaylistSelectorFooter.vue │ ├── SearchPlaylistForm.vue │ ├── SearchPlaylistResultItem.vue │ ├── SearchTrackResultItem.vue │ ├── ShareMultiplayerLink.vue │ ├── SimpleLoader.vue │ ├── Slider.vue │ ├── TimerRenderer.vue │ ├── ToggleBlock.vue │ ├── Tooltip.vue │ ├── TrackAnswerForm.vue │ ├── TrackEntry.vue │ ├── VolumeButton.vue │ └── twitch │ │ └── TwitchGameStatus.vue ├── i18n │ └── Label.ts ├── less │ ├── _includes.less │ ├── _mixins.less │ ├── _variables.less │ ├── components │ │ ├── form.less │ │ └── infinitelist.less │ ├── fonts.less │ ├── index.less │ └── reset.less ├── main.ts ├── router │ └── index.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── sock │ └── SockController.ts ├── store │ ├── Store.ts │ └── index.ts ├── twitch │ ├── IRCClient.ts │ ├── IRCevent.ts │ ├── TwitchExtensionEvent.ts │ ├── TwitchExtensionHelper.ts │ ├── TwitchMessageType.ts │ └── TwitchUtils.ts ├── utils │ ├── AnswerTester.ts │ ├── Api.ts │ ├── Beeper.ts │ ├── Config.ts │ ├── EventDispatcher.ts │ ├── SpotifyAPI.ts │ ├── StatsManager.ts │ └── Utils.ts ├── views │ ├── AlertView.vue │ ├── ChangeLog.vue │ ├── Confirm.vue │ ├── DemoConfig.vue │ ├── GameView.vue │ ├── GroupGame.vue │ ├── GroupLobby.vue │ ├── Home.vue │ ├── MixCreator.vue │ ├── OAuth.vue │ ├── PlaylistSelector.vue │ └── twitch │ │ ├── TwitchLobby.vue │ │ ├── common │ │ ├── TwitchAuth.vue │ │ ├── TwitchIntro.vue │ │ └── broadcaster │ │ │ ├── TwitchBroadcaster.vue │ │ │ └── TwitchBroadcasterControls.vue │ │ ├── extension │ │ ├── TwitchExtension.vue │ │ ├── TwitchExtensionConfiguration.vue │ │ └── viewer │ │ │ ├── TwitchViewer.vue │ │ │ ├── TwitchViewerControls.vue │ │ │ ├── TwitchViewerGame.vue │ │ │ └── TwitchViewerLeaderboard.vue │ │ └── obs │ │ └── TwitchOBS.vue └── vo │ ├── PlaylistData.ts │ ├── RoomData.ts │ ├── ScoreHistory.ts │ ├── SocketEvent.ts │ ├── TrackData.ts │ └── UserData.ts ├── logo └── logo.ai ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── img │ ├── share.png │ ├── share_img.svg │ └── share_small.png ├── index.html └── mp3 │ ├── 0d99160a29e74e74335f3bf7909260c0f2a5ca98.mp3 │ ├── 22b0ba88409ea7a8d7de70a3f0fa8a3f9a20bdfb.mp3 │ ├── 2a5b5a9977f58ae525b473455e9f2e67a9edf8d7.mp3 │ ├── 2da7ea19b35ecbfaf2dd7273e9b305a4e090bbc9.mp3 │ ├── 48a875fc1117e0c027571813c3c65b7c4fe52cfa.mp3 │ ├── 4929799672010ba499c49392f6007f3f017325a4.mp3 │ ├── 50e82c99c20ffa4223e82250605bbd8500cb3928.mp3 │ ├── 5299497db5ba226f388f3a064064cc44b2b51568.mp3 │ ├── 5fcdcfe7ef20abd006bba666b4a7dff01dd5ec21.mp3 │ ├── 645cd4b425f1d48d37656cac99d640254a8f64a9.mp3 │ ├── 75d3d091213d60d9f3ed2c0698b846177076b0d0.mp3 │ ├── 84462d8e1e4d0f9e5ccd06f0da390f65843774a2.mp3 │ ├── 8ec3a4b322c0df167ad409a668ceaa704fcbd1c0.mp3 │ ├── 98959d757d14bc4924e92e91e3d3035ce48059fc.mp3 │ ├── a0aaadd12a0a4c8d925411ed687e5aa0145b2a22.mp3 │ ├── a66864fcfd8923c6084fc2000e3086e4e1e0a657.mp3 │ ├── aa4f9186e0c3f4436bb40572a63862db80d7ef2d.mp3 │ ├── ac8375f8237f6bfd9c03cd074ac674d82f24cc8a.mp3 │ ├── b56a70770267b00ccae13c2e8c8a34ed54627d02.mp3 │ ├── c0984bf089f7e7534d6c838fd4204cc40ed87368.mp3 │ ├── cda5ee4b7028e5aaca877263844f0de5354dcdfe.mp3 │ ├── d1c143357d86d1736806ed7404b71a44feb8451d.mp3 │ ├── da2134a161f1cb34d17c2d6d7e77cc93d1c1e6f7.mp3 │ ├── dd78dafe31bb98f230372c038a126b8808f9349b.mp3 │ ├── e4ef557302eaf59468e8848415c225f24939361f.mp3 │ ├── e7eb60e9466bc3a27299ea8803aadf4fa9cf795c.mp3 │ ├── f48d5786b2115ef778856979ab8823072c0d8a7c.mp3 │ ├── fab3ba2d8224f7006e8c92b7fe1171d50265d37d.mp3 │ └── silence.mp3 ├── server_src ├── bootstrap.ts ├── controllers │ └── TwitchEBS.ts ├── server │ ├── HTTPServer.ts │ └── SocketServer.ts ├── utils │ ├── Config.ts │ ├── Logger.ts │ └── Utils.ts └── vo │ ├── PlaylistData.ts │ ├── RoomData.ts │ ├── TrackData.ts │ └── UserData.ts ├── tsconfig.json ├── tsconfig_server.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Durss] 2 | custom: ["https://paypal.me/durss"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /server 5 | /debug 6 | credentials.conf 7 | MultiBlindTest.json 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | twitch 2 | 3 | # Multi Blindtest 4 | 5 | Web app to play a multi blindtest.\ 6 | It's like a classic blind test but instead of hearing one track you hear up to 6 at the same time. 7 | 8 | When you find a track it's stopped so you can better hear the others. 9 | 10 | Check it out live at :\ 11 | https://multiblindtest.com 12 | 13 | Challenge your friends with the multiplayer mode ! 14 | 15 | 16 | 17 | ## Project setup 18 | ``` 19 | npm install 20 | ``` 21 | 22 | Install PM2 globally (will run the script as a service) : 23 | ``` 24 | npm i -g pm2 25 | ``` 26 | 27 | ### Compile front with hot-reloads for development 28 | ``` 29 | npm run front/serve 30 | ``` 31 | 32 | ### Compile front for production 33 | ``` 34 | npm run front/build 35 | ``` 36 | 37 | ### Compile server with hot-reloads for development 38 | ``` 39 | npm run server/watch 40 | ``` 41 | 42 | ### Compile server for production 43 | ``` 44 | npm run server/build 45 | ``` 46 | 47 | ### Shortcut for developpement 48 | ``` 49 | npm run dev 50 | ``` 51 | Starts front and server with hot reload.\ 52 | Node process has to be started manually. See [Starting services section](#starting-services). 53 | 54 | ### Compile server+front for production 55 | ``` 56 | npm run build 57 | ``` 58 | 59 | 60 | ### Starting services 61 | Execute this inside project folder's root 62 | ``` 63 | pm2 start bootstrap-pm2.json 64 | ``` 65 | 66 | To view process logs via PM2, execute : 67 | ``` 68 | pm2 logs --raw MultiblindTest 69 | ``` 70 | 71 | ## Start on boot (DOESN'T work on windows) 72 | First start the client as explained above. 73 | Then execute these commands: 74 | ``` 75 | pm2 save 76 | pm2 startup 77 | ``` 78 | Now, the service should automatically start on boot 79 | 80 | 81 | twitch 82 | 83 | ## Build for Twitch Extension 84 | To build the project so it works on twitch hosting services the files must all have relative PATHS. 85 | Problem is this is incompatible with the "history" mode of the router used by the actual website. 86 | Before building for twitch the following line must be uncommented on the **vue.config.js** file: 87 | ``` 88 | publicPath: './', 89 | ``` 90 | -------------------------------------------------------------------------------- /bootstrap-pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "script": "./server/bootstrap.js", 3 | "name": "MultiblindTest", 4 | "env": {}, 5 | "merge_logs": true, 6 | "vizion": true, 7 | "autorestart": true, 8 | "instance_var": "NODE_APP_INSTANCE", 9 | "pmx": true, 10 | "automation": true, 11 | "treekill": true, 12 | "username": "root", 13 | "windowsHide": true, 14 | "kill_retry_time": 100, 15 | "write": true, 16 | "ignore_watch" : ["node_modules", "public", "/"], 17 | "watch": ["server"] 18 | } -------------------------------------------------------------------------------- /env.conf: -------------------------------------------------------------------------------- 1 | dev -------------------------------------------------------------------------------- /front_src/assets/fonts/Comfortaa-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/Comfortaa-Bold.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/Comfortaa-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/Comfortaa-Light.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/Comfortaa-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/Comfortaa-Regular.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-ExtraBold.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-ExtraBoldOblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-ExtraBoldOblique.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-Heavy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-Heavy.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-HeavyOblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-HeavyOblique.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-Light.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-LightOblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-LightOblique.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-Medium.woff -------------------------------------------------------------------------------- /front_src/assets/fonts/FuturaStd-MediumOblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/fonts/FuturaStd-MediumOblique.woff -------------------------------------------------------------------------------- /front_src/assets/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | NOUN_Sign 9 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /front_src/assets/icons/chat.svg: -------------------------------------------------------------------------------- 1 | mini chat -------------------------------------------------------------------------------- /front_src/assets/icons/chat_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /front_src/assets/icons/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 03 9 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/checkmark_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /front_src/assets/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/assets/icons/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /front_src/assets/icons/cross_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /front_src/assets/icons/dashedBg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /front_src/assets/icons/delay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /front_src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 14 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /front_src/assets/icons/dmca.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/elite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /front_src/assets/icons/ext_link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/ext_link_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/flex_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/flex_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/assets/icons/infos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 14 | 15 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /front_src/assets/icons/kick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /front_src/assets/icons/king.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /front_src/assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /front_src/assets/icons/minus2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 14 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /front_src/assets/icons/multiplayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 13 | 14 | 16 | 17 | 18 | 22 | 23 | 24 | 28 | 29 | 30 | 35 | 36 | 37 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /front_src/assets/icons/music.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/mute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /front_src/assets/icons/obs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /front_src/assets/icons/offline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /front_src/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /front_src/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /front_src/assets/icons/play_withBg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /front_src/assets/icons/playlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /front_src/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /front_src/assets/icons/plus2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 12 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /front_src/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /front_src/assets/icons/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /front_src/assets/icons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/assets/icons/show.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/assets/icons/shrug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 16 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /front_src/assets/icons/solo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 13 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /front_src/assets/icons/song.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | Artboard 51 9 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/spotify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /front_src/assets/icons/spotify_color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /front_src/assets/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /front_src/assets/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /front_src/assets/icons/stop_square.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/twitch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 17 | 23 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /front_src/assets/icons/unmute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /front_src/assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/user_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /front_src/assets/images/logo-1024x768.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/images/logo-1024x768.png -------------------------------------------------------------------------------- /front_src/assets/images/logo-300x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/images/logo-300x200.png -------------------------------------------------------------------------------- /front_src/assets/images/logox100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/images/logox100.png -------------------------------------------------------------------------------- /front_src/assets/images/logox24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/images/logox24.png -------------------------------------------------------------------------------- /front_src/assets/images/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/front_src/assets/images/spotify.png -------------------------------------------------------------------------------- /front_src/assets/loader/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/assets/loader/loader_bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /front_src/assets/loader/loader_border.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /front_src/assets/loader/loader_light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/assets/loader/loader_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front_src/components/BouncingLoader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | -------------------------------------------------------------------------------- /front_src/components/ChatWindow.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 66 | 67 | -------------------------------------------------------------------------------- /front_src/components/CountDown.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 76 | 77 | -------------------------------------------------------------------------------- /front_src/components/ExpertModeForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 80 | 81 | -------------------------------------------------------------------------------- /front_src/components/ExpertModeState.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 58 | 59 | -------------------------------------------------------------------------------- /front_src/components/GameParams.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 95 | 96 | -------------------------------------------------------------------------------- /front_src/components/NeedInteractionLayer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | -------------------------------------------------------------------------------- /front_src/components/NoPlaylist.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | 38 | -------------------------------------------------------------------------------- /front_src/components/PlaylistSelectorFooter.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 70 | 71 | -------------------------------------------------------------------------------- /front_src/components/SearchPlaylistForm.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 82 | 83 | -------------------------------------------------------------------------------- /front_src/components/SearchPlaylistResultItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | 50 | -------------------------------------------------------------------------------- /front_src/components/SearchTrackResultItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | 41 | -------------------------------------------------------------------------------- /front_src/components/ShareMultiplayerLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 59 | 60 | -------------------------------------------------------------------------------- /front_src/components/SimpleLoader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | 66 | -------------------------------------------------------------------------------- /front_src/components/Slider.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | -------------------------------------------------------------------------------- /front_src/components/ToggleBlock.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 71 | 72 | -------------------------------------------------------------------------------- /front_src/less/_includes.less: -------------------------------------------------------------------------------- 1 | @import "_mixins.less"; 2 | @import "_variables.less"; -------------------------------------------------------------------------------- /front_src/less/_mixins.less: -------------------------------------------------------------------------------- 1 | .center() { 2 | top: 50%; 3 | left: 50%; 4 | transform: translate(-50%, -50%); 5 | } 6 | 7 | .blockContent { 8 | margin-top: -5px; 9 | padding: 10px; 10 | box-sizing: border-box; 11 | border-bottom-left-radius: 20px; 12 | border-bottom-right-radius: 20px; 13 | background-color: @mainColor_light_extralight; 14 | } -------------------------------------------------------------------------------- /front_src/less/_variables.less: -------------------------------------------------------------------------------- 1 | @mainColor_light: #84e0e9; 2 | @mainColor_normal: #57bfc9; 3 | @mainColor_dark: #2f9da7; 4 | @mainColor_highlight: #4391f8; 5 | @mainColor_warn: #e6bf00; 6 | @mainColor_alert: #c03131; 7 | 8 | @mainColor_light_light: lighten(@mainColor_light, 10%); 9 | @mainColor_normal_light: lighten(@mainColor_normal, 20%); 10 | @mainColor_dark_light: lighten(@mainColor_dark, 40%); 11 | @mainColor_highlight_light: lighten(@mainColor_highlight, 10%); 12 | @mainColor_warn_light: lighten(@mainColor_warn, 10%); 13 | 14 | @mainColor_light_extralight: lighten(@mainColor_light, 35%); 15 | @mainColor_normal_extralight: lighten(@mainColor_normal, 35%); 16 | @mainColor_dark_extralight: lighten(@mainColor_dark, 70%); 17 | @mainColor_highlight_extralight: lighten(@mainColor_highlight, 30%); 18 | @mainColor_warn_extralight: lighten(@mainColor_warn, 40%); -------------------------------------------------------------------------------- /front_src/less/components/form.less: -------------------------------------------------------------------------------- 1 | input, textarea, select { 2 | color: @mainColor_dark; 3 | border-radius: 20px; 4 | border: none; 5 | border-top: 1px solid @mainColor_dark; 6 | border-right: 1px solid @mainColor_dark; 7 | background-color: @mainColor_normal_extralight; 8 | font-size: 16px; 9 | padding: 10px; 10 | box-sizing: border-box; 11 | &::placeholder { 12 | font-style: italic; 13 | color: fade(#000, 25%); 14 | } 15 | &:-webkit-autofill, 16 | &:-webkit-autofill:hover, 17 | &:-webkit-autofill:active, 18 | &:-webkit-autofill:valid, 19 | &:-webkit-autofill:focus { 20 | color: @mainColor_dark; 21 | background-color: @mainColor_normal_extralight; 22 | box-shadow: none !important; 23 | } 24 | 25 | &.dark { 26 | color: @mainColor_dark; 27 | background-color: @mainColor_dark_light; 28 | } 29 | 30 | &.small { 31 | padding: 5px; 32 | } 33 | 34 | &::-webkit-scrollbar { 35 | display: none; 36 | } 37 | 38 | &:disabled { 39 | color: fade(#fff, 50%); 40 | 41 | &.dark { 42 | color: fade(@mainColor_normal, 25%); 43 | } 44 | } 45 | } 46 | 47 | textarea { 48 | min-height: 65px; 49 | } 50 | 51 | input[type="submit"], 52 | button, 53 | .button { 54 | cursor: pointer; 55 | border: none; 56 | border-radius: 20px; 57 | padding: 10px 20px; 58 | font-size: 16px; 59 | color: @mainColor_dark; 60 | font-weight: bold; 61 | font-family: "Futura"; 62 | color: #fff; 63 | background-color: @mainColor_normal; 64 | transition: color .25s, background-color .25s; 65 | padding-top: 12px; 66 | box-sizing: border-box; 67 | text-align: center; 68 | 69 | &:hover { 70 | background-color: @mainColor_normal_light; 71 | } 72 | 73 | &.dark { 74 | color: #fff; 75 | background-color: @mainColor_dark; 76 | 77 | &:hover { 78 | background-color: @mainColor_dark_light; 79 | } 80 | } 81 | 82 | &.disabled { 83 | pointer-events: none; 84 | color: fade(@mainColor_dark, 25%); 85 | // text-decoration: line-through; 86 | // font-style: italic; 87 | &.dark { 88 | color: fade(#000, 25%); 89 | background-color: fade(@mainColor_dark_extralight, 50%); 90 | } 91 | } 92 | } 93 | 94 | input[type="cancel"], 95 | button[type="cancel"] { 96 | background-color: @mainColor_light; 97 | 98 | &:hover { 99 | background-color: @mainColor_light_light; 100 | } 101 | } 102 | 103 | label { 104 | font-weight: bold; 105 | font-size: 20px; 106 | margin-bottom: 5px; 107 | } 108 | 109 | // input[type="checkbox"] { 110 | // @size: 25px; 111 | // width: @size; 112 | // height: @size; 113 | // cursor: pointer; 114 | // &:hover { 115 | // &::before { 116 | // background-color: @mainColor_highlight_extralight; 117 | // } 118 | // } 119 | // &::before { 120 | // content:""; 121 | // color: @mainColor_light_extralight; 122 | // display: block; 123 | // width: @size; 124 | // height: @size; 125 | // border-radius: 8px; 126 | // box-sizing: border-box; 127 | // background-color: @mainColor_highlight_light; 128 | // } 129 | // &:checked::before { 130 | // content:"✔"; 131 | // font-size: 20px; 132 | // padding-left: 5px; 133 | // } 134 | // } -------------------------------------------------------------------------------- /front_src/less/components/infinitelist.less: -------------------------------------------------------------------------------- 1 | .infinitelist-mainHolder { 2 | .infinitelist-itemsHolder { 3 | user-select: none; 4 | .listItem { 5 | width: 100%; 6 | } 7 | } 8 | .infinitelist-scrollbar-cursor { 9 | background-color: @mainColor_normal; 10 | } 11 | } -------------------------------------------------------------------------------- /front_src/less/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Futura"; 3 | src: url("../assets/fonts/FuturaStd-Medium.woff") format('woff'); 4 | } 5 | @font-face { 6 | font-family: "Futura"; 7 | font-style: italic; 8 | src: url("../assets/fonts/FuturaStd-MediumOblique.woff") format('woff'); 9 | } 10 | @font-face { 11 | font-family: "Futura"; 12 | font-weight: bold; 13 | src: url("../assets/fonts/FuturaStd-Heavy.woff") format('woff'); 14 | } 15 | @font-face { 16 | font-family: "Futura"; 17 | font-weight: bold; 18 | font-style: italic; 19 | src: url("../assets/fonts/FuturaStd-HeavyOblique.woff") format('woff'); 20 | } 21 | @font-face { 22 | font-family: "FuturaExtraBold"; 23 | src: url("../assets/fonts/FuturaStd-ExtraBold.woff") format('woff'); 24 | } 25 | @font-face { 26 | font-family: "FuturaExtraBold"; 27 | font-style: italic; 28 | src: url("../assets/fonts/FuturaStd-ExtraBoldOblique.woff") format('woff'); 29 | } 30 | @font-face { 31 | font-family: "FuturaLight"; 32 | src: url("../assets/fonts/FuturaStd-Light.woff") format('woff'); 33 | } 34 | @font-face { 35 | font-family: "FuturaLight"; 36 | font-style: italic; 37 | src: url("../assets/fonts/FuturaStd-LightOblique.woff") format('woff'); 38 | } 39 | 40 | // @font-face { 41 | // font-family: 'CircularPro'; 42 | // font-style: normal; 43 | // src: url("../assets/fonts/circularpro-book-web.woff2") format('woff2'); 44 | // } 45 | 46 | // @font-face { 47 | // font-family: 'CircularPro'; 48 | // font-weight: bold; 49 | // src: url("../assets/fonts/circularpro-bold-web.woff2") format('woff2'); 50 | // } -------------------------------------------------------------------------------- /front_src/less/index.less: -------------------------------------------------------------------------------- 1 | @import "_includes.less"; 2 | @import "fonts.less"; 3 | @import "reset.less"; 4 | @import "components/form.less"; 5 | @import "components/infinitelist.less"; 6 | 7 | body, html { 8 | padding: 0; 9 | margin: 0; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | font-family: "FuturaLight", Helvetica, Arial, sans-serif; 13 | font-size: 18px; 14 | color: @mainColor_normal; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | h1 { 20 | font-weight: bold; 21 | text-align: center; 22 | font-size: 40px; 23 | font-family: "Futura"; 24 | } 25 | 26 | h2 { 27 | font-family: "Futura"; 28 | font-weight: bold; 29 | font-size: 25px; 30 | border-bottom: 1px solid; 31 | padding-bottom: 5px; 32 | margin-bottom: 5px; 33 | text-align: center; 34 | padding: 10px; 35 | color: @mainColor_dark; 36 | border-top-left-radius: 20px; 37 | border-top-right-radius: 20px; 38 | background-color: @mainColor_normal_light; 39 | &.highlight { 40 | color: #fff; 41 | background-color: @mainColor_normal; 42 | } 43 | } 44 | 45 | .subtitle { 46 | font-family: "Futura"; 47 | text-align: center; 48 | font-size: 20px; 49 | } 50 | 51 | a { 52 | text-decoration: none; 53 | color: @mainColor_highlight; 54 | } 55 | 56 | .frame { 57 | border: 1px solid @mainColor_dark; 58 | border-radius: 10px; 59 | overflow: hidden; 60 | h1 { 61 | background-color: @mainColor_dark; 62 | color: #fff; 63 | padding: 10px; 64 | } 65 | } 66 | 67 | .backBt { 68 | margin: auto; 69 | position: absolute !important; 70 | left: 10px; 71 | top: 10px; 72 | width: 40px; 73 | height: 40px; 74 | } 75 | 76 | .header { 77 | font-size: 20px; 78 | margin-bottom: 20px; 79 | padding: 20px; 80 | color: @mainColor_dark; 81 | border-radius: 25px; 82 | // border-bottom-left-radius: 5px; 83 | // border-bottom-right-radius: 5px; 84 | background-color: @mainColor_normal_light; 85 | } 86 | 87 | 88 | @media only screen and (max-width: 500px) { 89 | h1 { 90 | font-size: 30px; 91 | } 92 | } 93 | 94 | 95 | //Expose color theme to JS 96 | #lessVars { 97 | .mainColor_light{ color:@mainColor_light; } 98 | .mainColor_normal{ color:@mainColor_normal; } 99 | .mainColor_dark{ color:@mainColor_dark; } 100 | .mainColor_highlight{ color:@mainColor_highlight; } 101 | .mainColor_warn{ color:@mainColor_warn; } 102 | 103 | .mainColor_light_light{ color:@mainColor_light_light; } 104 | .mainColor_normal_light{ color:@mainColor_normal_light; } 105 | .mainColor_dark_light{ color:@mainColor_dark_light; } 106 | .mainColor_highlight_light{ color:@mainColor_highlight_light; } 107 | .mainColor_warn_light{ color:@mainColor_warn_light; } 108 | 109 | .mainColor_light_extralight{ color:@mainColor_light_extralight; } 110 | .mainColor_normal_extralight{ color:@mainColor_normal_extralight; } 111 | .mainColor_dark_extralight{ color:@mainColor_dark_extralight; } 112 | .mainColor_highlight_extralight{ color:@mainColor_highlight_extralight; } 113 | .mainColor_warn_extralight{ color:@mainColor_warn_extralight; } 114 | } -------------------------------------------------------------------------------- /front_src/less/reset.less: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | font-size: 100%; 11 | font: inherit; 12 | vertical-align: baseline; 13 | } 14 | 15 | /* HTML5 display-role reset for older browsers */ 16 | 17 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 18 | display: block; 19 | } 20 | 21 | body { 22 | line-height: 1; 23 | } 24 | 25 | ol, ul { 26 | list-style: none; 27 | } 28 | 29 | blockquote, q { 30 | quotes: none; 31 | } 32 | 33 | blockquote { 34 | &:before, &:after { 35 | content: ''; 36 | content: none; 37 | } 38 | } 39 | 40 | q { 41 | &:before, &:after { 42 | content: ''; 43 | content: none; 44 | } 45 | } 46 | 47 | table { 48 | border-collapse: collapse; 49 | border-spacing: 0; 50 | } 51 | 52 | strong { 53 | font-weight: bold; 54 | } 55 | 56 | i { 57 | font-style: italic; 58 | } -------------------------------------------------------------------------------- /front_src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /front_src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /front_src/store/Store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fallback to sessionStorage if localStorage isn't available 3 | * Created : 18/10/2020 4 | */ 5 | export default class Store { 6 | 7 | private static store:Storage; 8 | 9 | constructor() { 10 | 11 | } 12 | 13 | /******************** 14 | * GETTER / SETTERS * 15 | ********************/ 16 | 17 | 18 | 19 | /****************** 20 | * PUBLIC METHODS * 21 | ******************/ 22 | public static get(key:string):string { 23 | if(!this.store) this.init(); 24 | return this.store.getItem(key); 25 | } 26 | public static set(key:string, value:string):void { 27 | if(!this.store) this.init(); 28 | this.store.setItem(key, value); 29 | } 30 | public static remove(key:string):void { 31 | if(!this.store) this.init(); 32 | this.store.removeItem(key); 33 | } 34 | public static clear():void { 35 | if(!this.store) this.init(); 36 | this.store.clear(); 37 | } 38 | 39 | 40 | 41 | /******************* 42 | * PRIVATE METHODS * 43 | *******************/ 44 | private static init():void { 45 | this.store = localStorage? localStorage : sessionStorage; 46 | } 47 | } -------------------------------------------------------------------------------- /front_src/twitch/IRCClient.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from "@/utils/EventDispatcher"; 2 | import * as tmi from "tmi.js"; 3 | import IRCEvent from "./IRCevent"; 4 | 5 | /** 6 | * Created : 19/01/2021 7 | */ 8 | export default class IRCClient extends EventDispatcher { 9 | 10 | private static _instance:IRCClient; 11 | private client:tmi.Client; 12 | private login:string; 13 | private isConnected:boolean = false; 14 | 15 | public token:string; 16 | public channel:string; 17 | 18 | constructor() { 19 | super(); 20 | } 21 | 22 | /******************** 23 | * GETTER / SETTERS * 24 | ********************/ 25 | static get instance():IRCClient { 26 | if(!IRCClient._instance) { 27 | IRCClient._instance = new IRCClient(); 28 | } 29 | return IRCClient._instance; 30 | } 31 | 32 | public get connected():boolean { 33 | return this.isConnected; 34 | } 35 | 36 | 37 | 38 | /****************** 39 | * PUBLIC METHODS * 40 | ******************/ 41 | public initialize(login:string, token:string):Promise { 42 | return new Promise((resolve, reject) => { 43 | this.login = login; 44 | this.token = token; 45 | 46 | this.client = new tmi.Client({ 47 | options: { debug: false }, 48 | connection: { reconnect: true }, 49 | channels: [ login ], 50 | identity: { 51 | username: login, 52 | password: "oauth:"+token 53 | }, 54 | }); 55 | 56 | this.client.on("join", (channel, user, test)=> { 57 | this.channel = channel; 58 | if(user == this.login) { 59 | // this.client.action(this.login, "SingsNote MultiBlindtest connected");//TODO localize 60 | this.isConnected = true; 61 | // console.log(reason); 62 | resolve(); 63 | } 64 | }); 65 | 66 | //@ts-ignore dirty system event listener because i foudn no other 67 | //way to capture a connexion error... 68 | this.client.on("_promiseJoin", (message:string)=> { 69 | if(message && message.toLowerCase().indexOf("no response") > -1) { 70 | console.log("IRCClient :: Connection failed"); 71 | reject(); 72 | } 73 | }); 74 | this.client.on("disconnected", (message:string)=> { 75 | console.log("IRCClient :: Disconnected"); 76 | if(!this.isConnected) { 77 | reject(); 78 | } 79 | this.isConnected = false; 80 | }); 81 | 82 | // this.client.on('raw_message', (messageCloned, message) => { 83 | // console.log("################## ON RAW ##################"); 84 | // console.log(messageCloned); 85 | // console.log(message); 86 | // }) 87 | this.client.on('message', (channel, tags, message, self) => { 88 | // console.log("################## ON MESSAGE ##################"); 89 | // console.log(channel); 90 | // console.log(tags); 91 | // console.log(message); 92 | // console.log(self); 93 | // console.log(`${tags.username}: ${message}`); 94 | // if(!self) { 95 | // this.client.say(channel, "Yoooo"); 96 | // } 97 | this.dispatchEvent(new IRCEvent(IRCEvent.MESSAGE, message, tags, channel, self)); 98 | }); 99 | 100 | this.client.connect(); 101 | }) 102 | } 103 | 104 | public deleteMessage(id:string):void { 105 | this.client.deletemessage(this.channel, id); 106 | } 107 | 108 | public sendMessage(id:string):void { 109 | this.client.action(this.login, id); 110 | } 111 | 112 | 113 | 114 | /******************* 115 | * PRIVATE METHODS * 116 | *******************/ 117 | } 118 | 119 | 120 | 121 | export declare module IRCTypes { 122 | 123 | export interface Badges { 124 | broadcaster: string; 125 | } 126 | 127 | export interface Tag { 128 | "badge-info"?: any; 129 | "badges": Badges; 130 | "client-nonce": string; 131 | "color": string; 132 | "display-name": string; 133 | "emotes"?: any; 134 | "flags"?: any; 135 | "id": string; 136 | "mod": boolean; 137 | "room-id": string; 138 | "subscriber": boolean; 139 | "tmi-sent-ts": string; 140 | "turbo": boolean; 141 | "user-id": string; 142 | "user-type"?: any; 143 | "emotes-raw"?: any; 144 | "badge-info-raw"?: any; 145 | "badges-raw": string; 146 | "username": string; 147 | "message-type": string; 148 | } 149 | 150 | } 151 | 152 | -------------------------------------------------------------------------------- /front_src/twitch/IRCevent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@/utils/EventDispatcher'; 2 | import {IRCTypes} from './IRCClient'; 3 | 4 | /** 5 | * Created : 07/12/2020 6 | */ 7 | export default class IRCEvent extends Event { 8 | 9 | public static MESSAGE:string = "MESSAGE"; 10 | 11 | constructor(type:string, public message:any, public tags:IRCTypes.Tag, public channel:string, public self:boolean) { 12 | super(type, null); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /front_src/twitch/TwitchExtensionEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@/utils/EventDispatcher'; 2 | 3 | /** 4 | * Created : 07/12/2020 5 | */ 6 | export default class TwitchExtensionEvent extends Event { 7 | 8 | public static MESSAGE:string = "MESSAGE"; 9 | public static AUTHORIZED:string = "AUTHORIZED"; 10 | public static CONTEXT:string = "CONTEXT"; 11 | 12 | constructor(type:string, public data:any) { 13 | super(type, null); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /front_src/twitch/TwitchMessageType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created : 24/01/2021 3 | */ 4 | export default class TwitchMessageType { 5 | 6 | public static PLAYLISTS:string = "PLAYLISTS"; 7 | public static ROUND_STATE:string = "ROUND_STATE"; 8 | public static CHANGE_VOLUME:string = "CHANGE_VOLUME"; 9 | public static LEADERBOARD:string = "LEADERBOARD"; 10 | public static BROADCASTER_CONTROL:string = "BROADCASTER_CONTROL"; 11 | public static SET_ZOOM_LEVEL:string = "SET_ZOOM_LEVEL"; 12 | public static SET_TRACK_PLAY_STATE:string = "SET_TRACK_PLAY_STATE"; 13 | 14 | } -------------------------------------------------------------------------------- /front_src/twitch/TwitchUtils.ts: -------------------------------------------------------------------------------- 1 | import Config from "@/utils/Config"; 2 | 3 | /** 4 | * Created : 19/01/2021 5 | */ 6 | export default class TwitchUtils { 7 | public static requestDCF():Promise { 8 | return new Promise(async (resolve, reject) => { 9 | const url = new URL("https://id.twitch.tv/oauth2/device"); 10 | url.searchParams.append("client_id", Config.TWITCH_CLIENT_ID); 11 | url.searchParams.append("scopes", "chat:edit chat:read"); 12 | const res = await fetch(url.toString(), {method:"POST"}); 13 | const json = await res.json(); 14 | window.open(json.verification_uri, "_blank"); 15 | 16 | const interval = setInterval(async () => { 17 | const url = new URL("https://id.twitch.tv/oauth2/token"); 18 | url.searchParams.append("client_id", Config.TWITCH_CLIENT_ID); 19 | url.searchParams.append("scopes", "chat:edit chat:read"); 20 | url.searchParams.append("device_code", json.device_code); 21 | url.searchParams.append("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); 22 | const resLoc = await fetch(url.toString(), {method:"POST"}); 23 | const jsonLoc = await resLoc.json(); 24 | if(jsonLoc.access_token) { 25 | clearInterval(interval); 26 | resolve(jsonLoc as TwitchAuthToken); 27 | } 28 | }, json.interval * 1000); 29 | }); 30 | } 31 | 32 | public static refreshToken(refresh_token:string):Promise { 33 | return new Promise(async (resolve, reject) => { 34 | const url = new URL("https://id.twitch.tv/oauth2/token"); 35 | url.searchParams.append("client_id", Config.TWITCH_CLIENT_ID); 36 | url.searchParams.append("scopes", "chat:edit chat:read"); 37 | url.searchParams.append("refresh_token", refresh_token); 38 | url.searchParams.append("grant_type", "refresh_token"); 39 | const res = await fetch(url.toString(), {method:"POST"}); 40 | const json = await res.json(); 41 | resolve(json); 42 | }); 43 | } 44 | 45 | public static validateToken(oAuthToken:string):Promise<{expires_in:number, client_id:string, login:string, user_id:string; scopes:string[]}> { 46 | return new Promise((resolve, reject) => { 47 | let headers:any = { 48 | "Authorization":"OAuth "+oAuthToken 49 | }; 50 | var options = { 51 | method: "GET", 52 | headers: headers, 53 | }; 54 | fetch("https://id.twitch.tv/oauth2/validate", options) 55 | .then((result) => { 56 | if(result.status == 200) { 57 | result.json().then((json)=> { 58 | resolve(json) 59 | }); 60 | }else{ 61 | resolve(null); 62 | } 63 | }); 64 | }); 65 | } 66 | 67 | public static broadcastToExtension(channelId:string, token:string, clientId:string):Promise { 68 | return new Promise((resolve, reject)=> { 69 | let headers:any = { 70 | "Authorization":"Bearer "+token, 71 | "Client-Id": clientId, 72 | "Content-Type": "application/json", 73 | }; 74 | var options = { 75 | method: "POST", 76 | headers: headers, 77 | }; 78 | 79 | let body = JSON.stringify({ 80 | 81 | }) 82 | fetch("https://api.twitch.tv/extensions/message/"+channelId, options) 83 | .then((result) => { 84 | if(result.status == 200) { 85 | result.json().then((json)=> { 86 | resolve(json) 87 | }); 88 | }else{ 89 | resolve(null); 90 | } 91 | }); 92 | resolve(true); 93 | }) 94 | } 95 | } 96 | 97 | export interface TwitchAuthToken { 98 | access_token: string, 99 | expires_in: number, 100 | refresh_token: string, 101 | scope: string[], 102 | token_type: "bearer" 103 | } -------------------------------------------------------------------------------- /front_src/utils/Api.ts: -------------------------------------------------------------------------------- 1 | import Config from '@/utils/Config'; 2 | 3 | export default class Api { 4 | 5 | public static get(endpoint: string, params: any = null, headers: any = null, setDefaultheader:boolean = true, autoPrefixApiPath:boolean = true): Promise { 6 | return this._call(endpoint, params, headers, 'GET', setDefaultheader, autoPrefixApiPath); 7 | } 8 | public static post(endpoint: string, params: any = null, headers: any = null, setDefaultheader:boolean = true, autoPrefixApiPath:boolean = true): Promise { 9 | return this._call(endpoint, params, headers, 'POST', setDefaultheader, autoPrefixApiPath); 10 | } 11 | public static put(endpoint: string, params: any = null, headers: any = null, setDefaultheader:boolean = true, autoPrefixApiPath:boolean = true): Promise { 12 | return this._call(endpoint, params, headers, 'PUT', setDefaultheader, autoPrefixApiPath); 13 | } 14 | public static patch(endpoint: string, params: any = null, headers: any = null, setDefaultheader:boolean = true, autoPrefixApiPath:boolean = true): Promise { 15 | return this._call(endpoint, params, headers, 'PATCH', setDefaultheader, autoPrefixApiPath); 16 | } 17 | public static delete(endpoint: string, params: any = null, headers: any = null, setDefaultheader:boolean = true, autoPrefixApiPath:boolean = true): Promise { 18 | return this._call(endpoint, params, headers, 'DELETE', setDefaultheader, autoPrefixApiPath); 19 | } 20 | 21 | protected static _call(endpoint: string, params: any = null, headers: any = null, verb: string = 'GET', setDefaultheader:boolean = true, autoPrefixApiPath:boolean = true): Promise { 22 | let _headers:any = {}; 23 | if(setDefaultheader) { 24 | _headers["Content-Type"] = "application/json"; 25 | } 26 | if (headers) { 27 | for(let key in headers) { 28 | _headers[key] = headers[key]; 29 | } 30 | } 31 | var options = { 32 | method: verb, 33 | headers: _headers 34 | }; 35 | 36 | let url = endpoint; 37 | if(autoPrefixApiPath) url = Config.API_PATH + "/" + url; 38 | if (verb == 'GET' && params != null) { 39 | let chunks = []; 40 | for (let key in params) { 41 | chunks.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); 42 | } 43 | url += (url.indexOf("?") == -1? "?" : "") + chunks.join("&"); 44 | params = null; 45 | } 46 | // let url = "//" + document.location.hostname + ":" + Config.API_PORT + '/api/' + api_version + "/" + endpoint; 47 | params = (params && _headers["Content-Type"] == "application/json" && typeof params != "string") ? JSON.stringify(params) : params; 48 | return this._sendRequest(url, options, params); 49 | } 50 | 51 | 52 | 53 | protected static _sendRequest(url:string, options: any, bodyParams?: string): Promise { 54 | return new Promise((resolve, reject) => { 55 | if(bodyParams) { 56 | options.body = bodyParams; 57 | } 58 | fetch(url, options) 59 | .then((result) => { 60 | if(result.status == 200) { 61 | result.text().then((text)=> { 62 | let json:any; 63 | try { 64 | json = JSON.parse(text); 65 | }catch(error) { 66 | //JSON parse failed, return error 67 | reject({code:"unknown", message:text.replace(//gi, ">")}); 68 | return; 69 | } 70 | if(json.success === false) { 71 | //Status 200 but JSON says there's an error 72 | reject(json); 73 | }else if(json.data){ 74 | resolve(json.data); 75 | }else{ 76 | resolve(json); 77 | } 78 | }).catch(_=> { 79 | reject({code:"unknown", message:"Unable to decode query result"}); 80 | }); 81 | }else{ 82 | result.text().then((text)=> { 83 | let json:any; 84 | try { 85 | json = JSON.parse(text); 86 | }catch(error) { 87 | //JSON parse failed, return error 88 | reject({code:"unknown", message:text.replace(//gi, ">")}); 89 | return; 90 | } 91 | reject(json); 92 | }).catch(_=> { 93 | reject({code:"unknown", message:"Unable to decode query result"}); 94 | }); 95 | } 96 | }).catch((error) => { 97 | reject(error); 98 | }); 99 | }); 100 | } 101 | } -------------------------------------------------------------------------------- /front_src/utils/Beeper.ts: -------------------------------------------------------------------------------- 1 | import Utils from './Utils'; 2 | 3 | export default class Beeper { 4 | 5 | private static _instance:Beeper; 6 | 7 | private audioCtx:AudioContext; 8 | private version:number = 0; 9 | 10 | constructor() { 11 | this.initialize(); 12 | } 13 | 14 | 15 | 16 | /******************** 17 | * GETTER / SETTERS * 18 | ********************/ 19 | 20 | /** 21 | * Gets the singleton's reference 22 | */ 23 | public static get instance():Beeper { 24 | if(!this._instance) this._instance = new Beeper(); 25 | return this._instance; 26 | } 27 | 28 | public get ready():boolean { 29 | return this.audioCtx != null; 30 | } 31 | 32 | 33 | 34 | /****************** 35 | * PUBLIC METHODS * 36 | ******************/ 37 | /** 38 | * Plays a sequence of beeps 39 | * @param patern d:duration, d:frequency, v:volume, t:type, p:pause 40 | * @param volumeOverride 41 | */ 42 | public async beepPatern(patern:{d:number, f?:number, v?:number, t?:string, p?:number}[], volumeOverride?:number):Promise { 43 | let v = this.version 44 | for (let i = 0; i < patern.length; i++) { 45 | const p = patern[i]; 46 | if(typeof volumeOverride == "number") p.v = volumeOverride; 47 | await this.beep(p.d, p.f, p.v, p.t, v); 48 | if(p.p) { 49 | await Utils.promisedTimeout(p.p); 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Stops all pending beeps 56 | */ 57 | public stopAll():void { 58 | this.version ++; 59 | } 60 | 61 | /** 62 | * Make a beep 63 | */ 64 | public beep(duration:number, frequency:number=440, volume:number=1, type:any="sine", v?:number):Promise { 65 | if(v != undefined && v < this.version) return Promise.resolve();//when stopAll() is called, version is incremented so we can ignore beep() called from setTimeouts 66 | 67 | return new Promise((resolve, reject) => { 68 | let oscillator; 69 | try { 70 | oscillator = this.audioCtx.createOscillator(); 71 | }catch(error) { 72 | console.log("BEEP FAILED",error) 73 | return; 74 | } 75 | let gainNode = this.audioCtx.createGain(); 76 | 77 | oscillator.connect(gainNode); 78 | gainNode.connect(this.audioCtx.destination); 79 | 80 | if (typeof volume == "number"){gainNode.gain.value = volume;} 81 | if (frequency){oscillator.frequency.value = frequency;} 82 | if (type){oscillator.type = type;} 83 | oscillator.onended = _=> resolve(); 84 | 85 | // gainNode.gain.value = 0;//TODO comment that debug 86 | 87 | oscillator.start(this.audioCtx.currentTime); 88 | oscillator.stop(this.audioCtx.currentTime + ((duration || 500) / 1000)); 89 | }) 90 | }; 91 | 92 | /** 93 | * Check if we can autoplay sound 94 | */ 95 | public checkAutoPlayRights():boolean { 96 | try { 97 | this.audioCtx.createOscillator(); 98 | }catch(err) { 99 | return false; 100 | } 101 | return true; 102 | } 103 | 104 | 105 | 106 | /******************* 107 | * PRIVATE METHODS * 108 | *******************/ 109 | /** 110 | * Initializes the class 111 | */ 112 | private initialize():void { 113 | //@ts-ignore 114 | this.audioCtx = new (window.AudioContext || window.webkitAudioContext || window.audioContext); 115 | // document.addEventListener("mousedown", _=> { 116 | // if(!this.audioCtx) return; 117 | // //@ts-ignore 118 | // this.audioCtx = new (window.AudioContext || window.webkitAudioContext || window.audioContext); 119 | // }) 120 | } 121 | } -------------------------------------------------------------------------------- /front_src/utils/Config.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Created by Durss 4 | */ 5 | export default class Config { 6 | 7 | private static _ENV_NAME: EnvName; 8 | 9 | public static MAX_TRACK_COUNT:number = 6; 10 | public static STORAGE_VERSION:number = 2; 11 | public static TWITCH_EXT_URL:string = "https://dashboard.twitch.tv/extensions/u4auavhba5b6brrtvjyjeqzhyz841b-1.0.0"; 12 | 13 | public static init():void { 14 | let prod = document.location.port == ""; 15 | 16 | if(prod) this._ENV_NAME = "prod"; 17 | else this._ENV_NAME = "dev"; 18 | } 19 | 20 | public static get IS_PROD():boolean { 21 | return this._ENV_NAME == "prod"; 22 | } 23 | 24 | public static get SERVER_PORT(): number { 25 | return this.getEnvData({ 26 | dev: 3004, 27 | prod: document.location.port, 28 | }); 29 | } 30 | 31 | public static get SOCKET_PATH():string{ 32 | // if(document.location.href.indexOf("twitch") > -1){ 33 | // return "https://multiblindtest.com/sock"; 34 | // }else 35 | if(this.IS_PROD) { 36 | return "/sock"; 37 | }else{ 38 | return window.location.origin.replace(/(.*):[0-9]+/gi, "$1")+":"+this.SERVER_PORT+"/sock"; 39 | } 40 | }; 41 | 42 | public static get TWITCH_CLIENT_ID():string { 43 | return this.getEnvData({ 44 | dev: "p8xijblr8h21rbtuveb871f0w9yvd2", 45 | prod: "p8xijblr8h21rbtuveb871f0w9yvd2", 46 | }); 47 | } 48 | 49 | public static get SPOTIFY_CLIENT_ID():string { 50 | return this.getEnvData({ 51 | dev: "944d2c2ba14745f588a370a93e56833c", 52 | prod: "944d2c2ba14745f588a370a93e56833c", 53 | }); 54 | } 55 | 56 | public static get UA():string { 57 | return this.getEnvData({ 58 | dev: "UA-156712572-1", 59 | prod: "UA-156712572-1", 60 | }); 61 | } 62 | 63 | public static get API_PATH(): string { 64 | return this.getEnvData({ 65 | dev: "http://localhost:"+this.SERVER_PORT+"/api", 66 | prod:"/api", 67 | }); 68 | } 69 | 70 | public static get BASE_URL(): string { 71 | return this.getEnvData({ 72 | dev: document.location.origin, 73 | prod:"https://multiblindtest.com", 74 | }); 75 | } 76 | 77 | 78 | /** 79 | * Extract a data from an hasmap depending on the current environment. 80 | * @param map 81 | * @returns {any} 82 | */ 83 | private static getEnvData(map: any): any { 84 | //Get the data from hashmap 85 | if (map[this._ENV_NAME]) return map[this._ENV_NAME]; 86 | return map[Object.keys(map)[0]]; 87 | } 88 | } 89 | 90 | type EnvName = "dev" | "preprod" | "prod" | "standalone"; -------------------------------------------------------------------------------- /front_src/utils/EventDispatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * EventDispatcher (TypeScript) 3 | * - Simple extendable event dispatching class 4 | * 5 | * @version 0.1.5 6 | * @author John Vrbanac 7 | * @license MIT License 8 | **/ 9 | export class Event { 10 | private _type:string; 11 | private _target:any; 12 | 13 | constructor(type:string, targetObj:any) { 14 | this._type = type; 15 | this._target = targetObj; 16 | } 17 | 18 | public getTarget():any { 19 | return this._target; 20 | } 21 | 22 | public getType():string { 23 | return this._type; 24 | } 25 | } 26 | 27 | export class EventDispatcher { 28 | private _listeners:any[]; 29 | constructor() { 30 | this._listeners = []; 31 | } 32 | 33 | public hasEventListener(type:string, listener:Function):Boolean { 34 | var exists:Boolean = false; 35 | for (var i = 0; i < this._listeners.length; i++) { 36 | if (this._listeners[i].type === type && this._listeners[i].listener === listener) { 37 | exists = true; 38 | } 39 | } 40 | 41 | return exists; 42 | } 43 | 44 | public addEventListener (typeStr:string, listenerFunc:Function):void { 45 | if (this.hasEventListener(typeStr, listenerFunc)) { 46 | return; 47 | } 48 | 49 | this._listeners.push({type: typeStr, listener: listenerFunc}); 50 | } 51 | 52 | public removeEventListener (typeStr:string, listenerFunc:Function):void { 53 | for (var i = 0; i < this._listeners.length; i++) { 54 | if (this._listeners[i].type === typeStr && this._listeners[i].listener === listenerFunc) { 55 | this._listeners.splice(i, 1); 56 | } 57 | } 58 | } 59 | 60 | public dispatchEvent (evt:Event) { 61 | for (var i = 0; i < this._listeners.length; i++) { 62 | if (this._listeners[i].type === evt.getType()) { 63 | this._listeners[i].listener.call(this, evt); 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /front_src/utils/SpotifyAPI.ts: -------------------------------------------------------------------------------- 1 | import Config from './Config'; 2 | import { Route } from 'vue-router'; 3 | import Store from '@/store/Store'; 4 | 5 | export default class SpotifyAPI { 6 | 7 | private static _instance: SpotifyAPI; 8 | 9 | private access_token: string = null; 10 | 11 | constructor() { 12 | this.initialize(); 13 | } 14 | 15 | 16 | 17 | /******************** 18 | * GETTER / SETTERS * 19 | ********************/ 20 | 21 | /** 22 | * Gets the singleton's reference 23 | */ 24 | public static get instance(): SpotifyAPI { 25 | if (!this._instance) this._instance = new SpotifyAPI(); 26 | return this._instance; 27 | } 28 | 29 | public get hasAccessToken():boolean { 30 | return this.access_token != null; 31 | } 32 | 33 | 34 | 35 | /****************** 36 | * PUBLIC METHODS * 37 | ******************/ 38 | /** 39 | * Call a spotify endpoint 40 | */ 41 | public async call(endpoint: string, params?: any, autoAuth:boolean = true): Promise { 42 | let url = "https://api.spotify.com/"+endpoint+"?access_token=" + this.access_token; 43 | 44 | if(params) { 45 | var query = Object.keys(params) 46 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) 47 | .join('&'); 48 | url += "&" + query 49 | } 50 | 51 | let headers = new Headers(); 52 | let options = { 53 | method: "GET", 54 | headers 55 | }; 56 | let result = await fetch(url, options); 57 | if(result.status == 401) { 58 | Store.set("redirect", document.location.href);//will allow to redirect the user to the current page after oauth result 59 | if(autoAuth) { 60 | this.authenticate(); 61 | } 62 | return Promise.reject(); 63 | } 64 | if(result.status == 429) { 65 | //When reaching the max rate limit of spotify API this status is returned 66 | //along with a "retry-after" header containing the number of seconds to 67 | //wait before executing a new request 68 | return new Promise((resolve, reject) => { 69 | //Wait for the requested amount of time and reissue the query 70 | setTimeout(async ()=> { 71 | let res = await this.call(endpoint, params); 72 | resolve(res); 73 | }, parseInt(result.headers.get("retry-after")) * 1000+500); 74 | }) 75 | } 76 | if(result.status == 200) { 77 | return await result.json(); 78 | }else{ 79 | return Promise.reject(); 80 | } 81 | } 82 | 83 | /** 84 | * Register the current access token for spotify api calls 85 | */ 86 | public setToken(token:string):void { 87 | this.access_token = token; 88 | Store.set("spotify_access_token", token); 89 | } 90 | 91 | /** 92 | * Starts OAuth process for user authentication 93 | */ 94 | public authenticate():void { 95 | document.location.href = this.getAuthUrl(); 96 | } 97 | 98 | /** 99 | * Get OAuth url 100 | */ 101 | public getAuthUrl():string { 102 | let url = document.location.protocol+"//"+document.location.host+"/oauth"; 103 | let redir = encodeURIComponent(url); 104 | let clientID = Config.SPOTIFY_CLIENT_ID; 105 | let scopes = encodeURIComponent("playlist-read-private playlist-read-collaborative"); 106 | return "https://accounts.spotify.com/authorize?client_id="+clientID+"&scope="+scopes+"&redirect_uri="+redir+"&response_type=token"; 107 | } 108 | 109 | /** 110 | * Restart an OAuth process if the access token expired 111 | */ 112 | public refreshTokenIfNecessary(redirTo:Route):Promise { 113 | return new Promise((resolve, reject) => { 114 | if(this.isTokenExpired()) { 115 | if(redirTo) { 116 | let redirUrl = window.location.protocol+"//"+window.location.host+redirTo.path; 117 | Store.set("redirect", redirUrl); 118 | } 119 | this.authenticate(); 120 | reject(); 121 | }else{ 122 | resolve(); 123 | } 124 | }) 125 | } 126 | 127 | /** 128 | * Check if token expired 129 | */ 130 | public isTokenExpired():boolean { 131 | let minutesBeforeExpiration = 2; 132 | let expirationDate = parseInt(Store.get("expirationDate")); 133 | return !expirationDate || isNaN(expirationDate) || (new Date().getTime() + minutesBeforeExpiration * 60 * 1000) > expirationDate; 134 | 135 | } 136 | 137 | 138 | 139 | /******************* 140 | * PRIVATE METHODS * 141 | *******************/ 142 | /** 143 | * Initializes the class 144 | */ 145 | private initialize(): void { 146 | if(Store.get("spotify_access_token")) { 147 | this.access_token = Store.get("spotify_access_token"); 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /front_src/utils/StatsManager.ts: -------------------------------------------------------------------------------- 1 | import ua from 'universal-analytics'; 2 | import Config from './Config'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import Store from '@/store/Store'; 5 | 6 | export default class StatsManager { 7 | 8 | private static _instance: StatsManager; 9 | 10 | private _visitor: ua.Visitor; 11 | 12 | constructor() { 13 | this.initialize(); 14 | } 15 | 16 | 17 | 18 | /******************** 19 | * GETTER / SETTERS * 20 | ********************/ 21 | 22 | /** 23 | * Gets the singleton's reference 24 | */ 25 | public static get instance(): StatsManager { 26 | if (!this._instance) this._instance = new StatsManager(); 27 | return this._instance; 28 | } 29 | 30 | /** 31 | * Gets the singleton's reference 32 | */ 33 | public set clientId(value:string) { 34 | Store.set("uid", value); 35 | this._visitor.set("uid", value); 36 | } 37 | 38 | 39 | 40 | /****************** 41 | * PUBLIC METHODS * 42 | ******************/ 43 | /** 44 | * Sends a pageview3 45 | * 46 | * @param path path 47 | * @param title page title 48 | */ 49 | public pageView(path: string, title?: string): void { 50 | if(!Config.IS_PROD) return; 51 | 52 | let data: any = {}; 53 | data.dp = path 54 | if (title) data.dt = title; 55 | this._visitor.pageview(data).send(); 56 | } 57 | 58 | /** 59 | * Sends an event 60 | * 61 | * @param cat category 62 | * @param act action 63 | * @param label label 64 | * @param value value 65 | * @param path path 66 | */ 67 | public event(cat: string, act:string, label?:string, value?:any, path?:string): void { 68 | if(!Config.IS_PROD) return; 69 | 70 | let data: any = {}; 71 | data.ec = cat; 72 | data.ea = act; 73 | data.el = label; 74 | data.ev = value; 75 | data.dp = path; 76 | this._visitor.event(data).send(); 77 | } 78 | 79 | 80 | 81 | /******************* 82 | * PRIVATE METHODS * 83 | *******************/ 84 | /** 85 | * Initializes the class 86 | */ 87 | private initialize(): void { 88 | let cid = uuidv4(); 89 | if(!Store.get("cid")) { 90 | Store.set("cid", cid); 91 | }else{ 92 | cid = Store.get("cid"); 93 | } 94 | 95 | this._visitor = ua(Config.UA, cid); 96 | 97 | if(Store.get("uid")) { 98 | this.clientId = Store.get("uid"); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /front_src/views/AlertView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | 45 | -------------------------------------------------------------------------------- /front_src/views/ChangeLog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 | -------------------------------------------------------------------------------- /front_src/views/DemoConfig.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | 40 | -------------------------------------------------------------------------------- /front_src/views/OAuth.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 57 | 58 | -------------------------------------------------------------------------------- /front_src/views/twitch/common/TwitchIntro.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 42 | 43 | -------------------------------------------------------------------------------- /front_src/views/twitch/common/broadcaster/TwitchBroadcaster.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 43 | 44 | -------------------------------------------------------------------------------- /front_src/views/twitch/extension/TwitchExtension.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 109 | 110 | -------------------------------------------------------------------------------- /front_src/views/twitch/extension/TwitchExtensionConfiguration.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | 37 | -------------------------------------------------------------------------------- /front_src/views/twitch/extension/viewer/TwitchViewer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 69 | 70 | -------------------------------------------------------------------------------- /front_src/views/twitch/obs/TwitchOBS.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 100 | 101 | -------------------------------------------------------------------------------- /front_src/vo/PlaylistData.ts: -------------------------------------------------------------------------------- 1 | import TrackData from './TrackData'; 2 | 3 | export default interface PlaylistData { 4 | id: string; 5 | name: string; 6 | cover: string; 7 | owner: string; 8 | tracks?: TrackData[]; 9 | searchOrigin?: boolean; 10 | processingTracks?: boolean; 11 | } -------------------------------------------------------------------------------- /front_src/vo/RoomData.ts: -------------------------------------------------------------------------------- 1 | import UserData from "./UserData"; 2 | import PlaylistData from './PlaylistData'; 3 | import TrackData from './TrackData'; 4 | import ScoreHistory from './ScoreHistory'; 5 | 6 | export default interface RoomData { 7 | id:string; 8 | creator:string; 9 | users:UserData[]; 10 | playlists:PlaylistData[]; 11 | tracksCount:number; 12 | gamesCount:number; 13 | gameDuration:number; 14 | gameStepIndex:number; 15 | acceptAlbum:boolean; 16 | currentTracks?:TrackData[]; 17 | expertMode:string[]; 18 | scoreHistory:ScoreHistory[]; 19 | } -------------------------------------------------------------------------------- /front_src/vo/ScoreHistory.ts: -------------------------------------------------------------------------------- 1 | export default interface ScoreHistory { 2 | trackId:string; 3 | guesserId:string; 4 | score:number; 5 | guesserName?:string;//Only used for twitch mode to reduce data transfer with pears 6 | } -------------------------------------------------------------------------------- /front_src/vo/SocketEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@/utils/EventDispatcher'; 2 | import { SOCK_ACTIONS } from '@/sock/SockController'; 3 | 4 | export default class SocketEvent extends Event { 5 | 6 | 7 | private _data:any; 8 | 9 | constructor(type:SOCK_ACTIONS, data:any) { 10 | super(type, null); 11 | this._data = data; 12 | } 13 | 14 | 15 | 16 | /******************** 17 | * GETTER / SETTERS * 18 | ********************/ 19 | public get data():any { return this._data; } 20 | 21 | 22 | 23 | /****************** 24 | * PUBLIC METHODS * 25 | ******************/ 26 | 27 | 28 | 29 | /******************* 30 | * PRIVATE METHODS * 31 | *******************/ 32 | } -------------------------------------------------------------------------------- /front_src/vo/TrackData.ts: -------------------------------------------------------------------------------- 1 | import UserData from './UserData'; 2 | 3 | export default interface TrackData { 4 | id: string; 5 | name: string; 6 | artist: string; 7 | album: string; 8 | audioPath: string; 9 | enabled?:boolean; 10 | isPlaying?:boolean; 11 | picture?:string; 12 | loadFail?:boolean; 13 | guessedBy?:UserData[];//For multiplayer mode 14 | pendingAcceptation?:boolean;//Used for twitch mode when "multiple winners" is enabled 15 | highlight?:boolean;//Used in twitch mode when replaying one specific song to highlight it on the stream 16 | score?:number//Used to make track scoring easier to manage 17 | } -------------------------------------------------------------------------------- /front_src/vo/UserData.ts: -------------------------------------------------------------------------------- 1 | export default interface UserData { 2 | name:string; 3 | id:string; 4 | offline:boolean; 5 | score:number; 6 | handicap:number; 7 | pass?:boolean; 8 | } -------------------------------------------------------------------------------- /logo/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/logo/logo.ai -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiblindtest", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently --kill-others \"npm run front/serve\" \"npm run server/watch\"", 7 | "build": "npm run front/build && npm run server/build", 8 | "front/serve": "vue-cli-service serve", 9 | "front/build": "vue-cli-service build", 10 | "server/watch": "tsc -w -p ./tsconfig_server.json", 11 | "server/build": "tsc -p ./tsconfig_server.json", 12 | "server/start": "pm2 start bootstrap-pm2.json" 13 | }, 14 | "dependencies": { 15 | "@types/node-fetch": "^2.5.10", 16 | "browserify-zlib": "^0.2.0", 17 | "connect-history-api-fallback": "^1.6.0", 18 | "express": "^4.17.3", 19 | "express-serve-static-core": "^0.1.1", 20 | "glob": "^7.1.6", 21 | "http": "0.0.0", 22 | "jsonwebtoken": "^8.5.1", 23 | "node-fetch": "^2.6.7", 24 | "node-polyfill-webpack-plugin": "^4.0.0", 25 | "uuid": "^8.3.2", 26 | "vue": "^2.6.12", 27 | "vue-class-component": "^7.2.6", 28 | "vue-property-decorator": "^9.1.2", 29 | "vue-router": "^3.5.1", 30 | "vuex": "^3.6.2" 31 | }, 32 | "devDependencies": { 33 | "@types/jsonwebtoken": "^8.5.1", 34 | "@types/sockjs": "^0.3.32", 35 | "@types/tmi.js": "^1.7.1", 36 | "@types/universal-analytics": "^0.4.4", 37 | "@vue/cli-plugin-router": "^5.0.8", 38 | "@vue/cli-plugin-typescript": "^5.0.8", 39 | "@vue/cli-plugin-vuex": "^5.0.8", 40 | "@vue/cli-service": "^5.0.8", 41 | "concurrently": "^6.0.1", 42 | "fuse.js": "^6.4.6", 43 | "gsap": "^3.6.1", 44 | "html-webpack-plugin": "^5.3.1", 45 | "less": "^4.1.1", 46 | "less-loader": "^7.2.1", 47 | "process": "^0.11.10", 48 | "sockjs-client": "^1.6.1", 49 | "tmi.js": "^1.7.5", 50 | "typescript": "^5.7.2", 51 | "universal-analytics": "^0.4.23", 52 | "vite": "^6.0.1", 53 | "vite-plugin-package-version": "^1.1.0", 54 | "vue-i18n": "^8.24.2", 55 | "vue-template-compiler": "^2.6.12" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/favicon.png -------------------------------------------------------------------------------- /public/img/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/img/share.png -------------------------------------------------------------------------------- /public/img/share_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/img/share_small.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Multi Blindtest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/mp3/0d99160a29e74e74335f3bf7909260c0f2a5ca98.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/0d99160a29e74e74335f3bf7909260c0f2a5ca98.mp3 -------------------------------------------------------------------------------- /public/mp3/22b0ba88409ea7a8d7de70a3f0fa8a3f9a20bdfb.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/22b0ba88409ea7a8d7de70a3f0fa8a3f9a20bdfb.mp3 -------------------------------------------------------------------------------- /public/mp3/2a5b5a9977f58ae525b473455e9f2e67a9edf8d7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/2a5b5a9977f58ae525b473455e9f2e67a9edf8d7.mp3 -------------------------------------------------------------------------------- /public/mp3/2da7ea19b35ecbfaf2dd7273e9b305a4e090bbc9.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/2da7ea19b35ecbfaf2dd7273e9b305a4e090bbc9.mp3 -------------------------------------------------------------------------------- /public/mp3/48a875fc1117e0c027571813c3c65b7c4fe52cfa.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/48a875fc1117e0c027571813c3c65b7c4fe52cfa.mp3 -------------------------------------------------------------------------------- /public/mp3/4929799672010ba499c49392f6007f3f017325a4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/4929799672010ba499c49392f6007f3f017325a4.mp3 -------------------------------------------------------------------------------- /public/mp3/50e82c99c20ffa4223e82250605bbd8500cb3928.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/50e82c99c20ffa4223e82250605bbd8500cb3928.mp3 -------------------------------------------------------------------------------- /public/mp3/5299497db5ba226f388f3a064064cc44b2b51568.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/5299497db5ba226f388f3a064064cc44b2b51568.mp3 -------------------------------------------------------------------------------- /public/mp3/5fcdcfe7ef20abd006bba666b4a7dff01dd5ec21.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/5fcdcfe7ef20abd006bba666b4a7dff01dd5ec21.mp3 -------------------------------------------------------------------------------- /public/mp3/645cd4b425f1d48d37656cac99d640254a8f64a9.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/645cd4b425f1d48d37656cac99d640254a8f64a9.mp3 -------------------------------------------------------------------------------- /public/mp3/75d3d091213d60d9f3ed2c0698b846177076b0d0.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/75d3d091213d60d9f3ed2c0698b846177076b0d0.mp3 -------------------------------------------------------------------------------- /public/mp3/84462d8e1e4d0f9e5ccd06f0da390f65843774a2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/84462d8e1e4d0f9e5ccd06f0da390f65843774a2.mp3 -------------------------------------------------------------------------------- /public/mp3/8ec3a4b322c0df167ad409a668ceaa704fcbd1c0.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/8ec3a4b322c0df167ad409a668ceaa704fcbd1c0.mp3 -------------------------------------------------------------------------------- /public/mp3/98959d757d14bc4924e92e91e3d3035ce48059fc.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/98959d757d14bc4924e92e91e3d3035ce48059fc.mp3 -------------------------------------------------------------------------------- /public/mp3/a0aaadd12a0a4c8d925411ed687e5aa0145b2a22.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/a0aaadd12a0a4c8d925411ed687e5aa0145b2a22.mp3 -------------------------------------------------------------------------------- /public/mp3/a66864fcfd8923c6084fc2000e3086e4e1e0a657.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/a66864fcfd8923c6084fc2000e3086e4e1e0a657.mp3 -------------------------------------------------------------------------------- /public/mp3/aa4f9186e0c3f4436bb40572a63862db80d7ef2d.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/aa4f9186e0c3f4436bb40572a63862db80d7ef2d.mp3 -------------------------------------------------------------------------------- /public/mp3/ac8375f8237f6bfd9c03cd074ac674d82f24cc8a.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/ac8375f8237f6bfd9c03cd074ac674d82f24cc8a.mp3 -------------------------------------------------------------------------------- /public/mp3/b56a70770267b00ccae13c2e8c8a34ed54627d02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/b56a70770267b00ccae13c2e8c8a34ed54627d02.mp3 -------------------------------------------------------------------------------- /public/mp3/c0984bf089f7e7534d6c838fd4204cc40ed87368.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/c0984bf089f7e7534d6c838fd4204cc40ed87368.mp3 -------------------------------------------------------------------------------- /public/mp3/cda5ee4b7028e5aaca877263844f0de5354dcdfe.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/cda5ee4b7028e5aaca877263844f0de5354dcdfe.mp3 -------------------------------------------------------------------------------- /public/mp3/d1c143357d86d1736806ed7404b71a44feb8451d.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/d1c143357d86d1736806ed7404b71a44feb8451d.mp3 -------------------------------------------------------------------------------- /public/mp3/da2134a161f1cb34d17c2d6d7e77cc93d1c1e6f7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/da2134a161f1cb34d17c2d6d7e77cc93d1c1e6f7.mp3 -------------------------------------------------------------------------------- /public/mp3/dd78dafe31bb98f230372c038a126b8808f9349b.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/dd78dafe31bb98f230372c038a126b8808f9349b.mp3 -------------------------------------------------------------------------------- /public/mp3/e4ef557302eaf59468e8848415c225f24939361f.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/e4ef557302eaf59468e8848415c225f24939361f.mp3 -------------------------------------------------------------------------------- /public/mp3/e7eb60e9466bc3a27299ea8803aadf4fa9cf795c.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/e7eb60e9466bc3a27299ea8803aadf4fa9cf795c.mp3 -------------------------------------------------------------------------------- /public/mp3/f48d5786b2115ef778856979ab8823072c0d8a7c.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/f48d5786b2115ef778856979ab8823072c0d8a7c.mp3 -------------------------------------------------------------------------------- /public/mp3/fab3ba2d8224f7006e8c92b7fe1171d50265d37d.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/fab3ba2d8224f7006e8c92b7fe1171d50265d37d.mp3 -------------------------------------------------------------------------------- /public/mp3/silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Durss/multiblindtest/44e38009dea558f4285964afa8a94625e148d635/public/mp3/silence.mp3 -------------------------------------------------------------------------------- /server_src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import HTTPServer from "./server/HTTPServer"; 2 | import Config from "./utils/Config"; 3 | 4 | new HTTPServer(Config.SERVER_PORT); 5 | -------------------------------------------------------------------------------- /server_src/utils/Config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import Logger, { LogStyle } from "../utils/Logger"; 3 | /** 4 | * Created by Durss 5 | */ 6 | export default class Config { 7 | 8 | private static _ENV_NAME: EnvName; 9 | private static _CONF_PATH: string = "env.conf"; 10 | private static _PRIVATE_CREDENTIALS: any; 11 | 12 | public static get SECRET(): string { 13 | return this._PRIVATE_CREDENTIALS["SECRET"]; 14 | } 15 | public static get EXTSECRET(): string { 16 | return this._PRIVATE_CREDENTIALS["EXTSECRET"]; 17 | } 18 | public static get OWNERID(): string { 19 | return this._PRIVATE_CREDENTIALS["OWNERID"]; 20 | } 21 | public static get CLIENTID(): string { 22 | return this._PRIVATE_CREDENTIALS["CLIENTID"]; 23 | } 24 | public static get EXTVERSION(): string { 25 | return this._PRIVATE_CREDENTIALS["EXTVERSION"]; 26 | } 27 | public static get SMS_KEY(): string { 28 | return this._PRIVATE_CREDENTIALS["SMS_KEY"]; 29 | } 30 | public static get SMS_USER(): string { 31 | return this._PRIVATE_CREDENTIALS["SMS_USER"]; 32 | } 33 | 34 | public static async loadPrivateCredentials():Promise { 35 | let url = "credentials.conf"; 36 | let creds; 37 | if (fs.existsSync(url)) { 38 | creds = fs.readFileSync(url); 39 | } else { 40 | url = "./server_src/" + url; 41 | if (fs.existsSync(url)) { 42 | creds = fs.readFileSync(url); 43 | } 44 | } 45 | if (creds) { 46 | this._PRIVATE_CREDENTIALS = {}; 47 | let chunks = creds.toString().replace(/(\r|\n){2,}/gi, "\r").split(/\r|\n/gi); 48 | for (let i = 0; i < chunks.length; i++) { 49 | const [id, value] = chunks[i].split(";"); 50 | // this[id] = value.replace(/"/gi, ""); 51 | this._PRIVATE_CREDENTIALS[id] = value.replace(/"/gi, ""); 52 | } 53 | 54 | } else { 55 | Logger.error("MISSING FILE \"credentials.conf\" contianing Twitch credentials"); 56 | Logger.error("The file has been created, please fill the missing key values"); 57 | fs.writeFileSync("credentials.conf", `SECRET;xxx 58 | EXTSECRET;xxx 59 | CLIENTID;xxx 60 | OWNERID;xxx 61 | EXTVERSION;xxx 62 | SMS_KEY;xxx 63 | SMS_USER;xxx`); 64 | } 65 | } 66 | 67 | 68 | public static get LOGS_ENABLED(): boolean { 69 | return this.getEnvData({ 70 | dev: true, 71 | prod: false, 72 | }); 73 | } 74 | 75 | public static get SERVER_PORT(): number { 76 | return this.getEnvData({ 77 | dev: 3004, 78 | prod: 3004, 79 | }); 80 | } 81 | 82 | public static get PUBLIC_PATH(): string { 83 | return this.getEnvData({ 84 | dev: "./dist", 85 | prod: "./public", 86 | }); 87 | } 88 | 89 | public static get SERVER_NAME(): string { 90 | return this.getEnvData({ 91 | dev: "", 92 | prod: "", 93 | }); 94 | } 95 | 96 | 97 | /** 98 | * Extract a data from an hasmap depending on the current environment. 99 | * @param map 100 | * @returns {any} 101 | */ 102 | private static getEnvData(map: any): any { 103 | //Grab env name the first time 104 | if (!this._ENV_NAME) { 105 | if (fs.existsSync(this._CONF_PATH)) { 106 | let content: string = fs.readFileSync(this._CONF_PATH, "utf8"); 107 | this._ENV_NAME = content; 108 | let str: String = " :: Current environment \"" + content + "\" :: "; 109 | let head: string = str.replace(/./g, " "); 110 | console.log("\n"); 111 | console.log(LogStyle.BgGreen + head + LogStyle.Reset); 112 | console.log(LogStyle.Bright + LogStyle.BgGreen + LogStyle.FgWhite + str + LogStyle.Reset); 113 | console.log(LogStyle.BgGreen + head + LogStyle.Reset); 114 | console.log("\n"); 115 | 116 | } else { 117 | this._ENV_NAME = "dev"; 118 | fs.writeFileSync(this._CONF_PATH, this._ENV_NAME); 119 | let str: String = " /!\\ Missing file \"./" + this._CONF_PATH + "\" /!\\ "; 120 | let head: string = str.replace(/./g, " "); 121 | console.log("\n"); 122 | console.log(LogStyle.BgRed + head + LogStyle.Reset); 123 | console.log(LogStyle.Bright + LogStyle.BgRed + LogStyle.FgWhite + str + LogStyle.Reset); 124 | console.log(LogStyle.BgRed + head + LogStyle.Reset); 125 | console.log("\n"); 126 | console.log("Creating env.conf file autmatically and set it to \"standalone\"\n\n"); 127 | } 128 | } 129 | 130 | //Get the data from hashmap 131 | if (map[this._ENV_NAME]) return map[this._ENV_NAME]; 132 | return map[Object.keys(map)[0]]; 133 | } 134 | } 135 | 136 | type EnvName = "dev" | "preprod" | "prod"; -------------------------------------------------------------------------------- /server_src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import Config from "./Config"; 2 | /** 3 | * Created by Durss on 16/03/2017 4 | */ 5 | export default class Logger { 6 | 7 | constructor() { 8 | } 9 | 10 | /******************** 11 | * GETTER / SETTERS * 12 | ********************/ 13 | 14 | 15 | 16 | /****************** 17 | * PUBLIC METHODS * 18 | ******************/ 19 | public static log(message:any, ...more):void { 20 | if(!Config.LOGS_ENABLED) return; 21 | 22 | let chunks:string[] = [message]; 23 | chunks = chunks.concat(more); 24 | 25 | this.doLog(chunks.join(" ")); 26 | } 27 | 28 | public static simpleLog(message:any, ...more):void { 29 | if(!Config.LOGS_ENABLED) return; 30 | 31 | let chunks:string[] = [message]; 32 | chunks = chunks.concat(more); 33 | console.log(" "+LogStyle.Reset+chunks.join(" ")+LogStyle.Reset); 34 | } 35 | 36 | public static info(message:any, ...more):void { 37 | if(!Config.LOGS_ENABLED) return; 38 | 39 | let chunks:string[] = [message]; 40 | chunks = chunks.concat(more); 41 | 42 | this.doLog(LogStyle.FgCyan+chunks.join(" ")); 43 | } 44 | 45 | public static warn(message:any, ...more):void { 46 | let chunks:string[] = [message]; 47 | chunks = chunks.concat(more); 48 | 49 | this.doLog(LogStyle.FgYellow+chunks.join(" ")); 50 | } 51 | 52 | public static error(message:any, ...more):void { 53 | let chunks:string[] = [message]; 54 | chunks = chunks.concat(more); 55 | 56 | this.doLog(LogStyle.FgRed+chunks.join(" ")); 57 | } 58 | 59 | public static success(message:any, ...more):void { 60 | let chunks:string[] = [message]; 61 | chunks = chunks.concat(more); 62 | 63 | this.doLog(LogStyle.FgGreen+chunks.join(" ")); 64 | } 65 | 66 | public static faded(message:any, ...more):void { 67 | let chunks:string[] = [message]; 68 | chunks = chunks.concat(more); 69 | 70 | this.doLog(LogStyle.Dim+chunks.join(" ")); 71 | } 72 | 73 | 74 | 75 | /******************* 76 | * PRIVATE METHODS * 77 | *******************/ 78 | private static convertDate(inputFormat:Date):string { 79 | function pad(s) { return (s < 10) ? '0' + s : s; } 80 | function pad2(s) { return (s < 10) ? '00' + s : (s < 100) ? '0' + s : s; } 81 | let d = new Date(inputFormat); 82 | return [pad(d.getDate()), pad(d.getMonth()+1), d.getFullYear()].join('/')+ " " + [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':')+":"+pad2(d.getMilliseconds()); 83 | } 84 | 85 | private static doLog(mess:string):void { 86 | console.log(LogStyle.Underscore+LogStyle.FgBlack+LogStyle.BgWhite+"["+this.convertDate(new Date())+"]"+LogStyle.Reset+" : "+mess+LogStyle.Reset); 87 | } 88 | } 89 | 90 | export class LogStyle { 91 | static Reset = "\x1b[0m"; 92 | static Bright = "\x1b[1m"; 93 | static Dim = "\x1b[2m"; 94 | static Underscore = "\x1b[4m"; 95 | static Blink = "\x1b[5m"; 96 | static Reverse = "\x1b[7m"; 97 | static Hidden = "\x1b[8m"; 98 | 99 | static FgBlack = "\x1b[30m"; 100 | static FgRed = "\x1b[31m"; 101 | static FgGreen = "\x1b[32m"; 102 | static FgYellow = "\x1b[33m"; 103 | static FgBlue = "\x1b[34m"; 104 | static FgMagenta = "\x1b[35m"; 105 | static FgCyan = "\x1b[36m"; 106 | static FgWhite = "\x1b[37m"; 107 | 108 | static BgBlack = "\x1b[40m"; 109 | static BgRed = "\x1b[41m"; 110 | static BgGreen = "\x1b[42m"; 111 | static BgYellow = "\x1b[43m"; 112 | static BgBlue = "\x1b[44m"; 113 | static BgMagenta = "\x1b[45m"; 114 | static BgCyan = "\x1b[46m"; 115 | static BgWhite = "\x1b[47m"; 116 | } -------------------------------------------------------------------------------- /server_src/vo/PlaylistData.ts: -------------------------------------------------------------------------------- 1 | export default interface PlaylistData { 2 | id: string; 3 | name: string; 4 | cover: string; 5 | owner: string; 6 | } -------------------------------------------------------------------------------- /server_src/vo/RoomData.ts: -------------------------------------------------------------------------------- 1 | import UserData from "./UserData"; 2 | import PlaylistData from "./PlaylistData"; 3 | import TrackData from "./TrackData"; 4 | 5 | export default interface RoomData { 6 | id:string; 7 | creator:string; 8 | users:UserData[]; 9 | playlists:PlaylistData[]; 10 | tracksCount:number; 11 | gamesCount:number; 12 | gameStepIndex:number; 13 | currentTracks?:TrackData[]; 14 | expertMode:string[]; 15 | scoreHistory:{trackId:string, guesserId:string, score:number}[]; 16 | } -------------------------------------------------------------------------------- /server_src/vo/TrackData.ts: -------------------------------------------------------------------------------- 1 | import UserData from "./UserData"; 2 | 3 | export default interface TrackData { 4 | id: string; 5 | name: string; 6 | artist: string; 7 | audioPath: string; 8 | enabled?:boolean; 9 | isPlaying?:boolean; 10 | picture?:string; 11 | guessedBy?:UserData; 12 | score?:number; 13 | } -------------------------------------------------------------------------------- /server_src/vo/UserData.ts: -------------------------------------------------------------------------------- 1 | export default interface UserData { 2 | name:string; 3 | id:string; 4 | offline:boolean; 5 | score:number; 6 | handicap:number; 7 | pass?:boolean; 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "strictNullChecks": false, 12 | "allowSyntheticDefaultImports": true, 13 | "strictPropertyInitialization": false, 14 | "noImplicitAny": false, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "types": [ 18 | "webpack-env" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "front_src/*" 23 | ], 24 | "*": ["node_modules/*"] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "front_src/**/*.ts", 35 | "front_src/**/*.tsx", 36 | "front_src/**/*.vue", 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig_server.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "noImplicitThis": false, 8 | "strictNullChecks": false, 9 | "importHelpers": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": false, 12 | "baseUrl": ".", 13 | "outDir": "./server", 14 | "skipLibCheck": true, 15 | "paths": { 16 | "@/*": [ 17 | "server_src/*" 18 | ] 19 | }, 20 | "lib": [ 21 | "esnext", 22 | "dom", 23 | "dom.iterable", 24 | "scripthost" 25 | ] 26 | }, 27 | "include": [ 28 | "server_src/**/*.ts", 29 | "server_src/**/*.tsx" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const htmlwp = require('html-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 4 | module.exports = { 5 | //Uncomment this line before compiling twitch extension 6 | // publicPath: './', 7 | 8 | //MPA definitions 9 | // pages: { 10 | // game: { 11 | // entry:'./front_src/main.ts', 12 | // template:'./public/index.html', 13 | // filename:'game.html', 14 | // }, 15 | // twitchExtension: { 16 | // entry:'./front_src/main_twitch.ts', 17 | // template:'./public/index_twitch.html', 18 | // filename:'index_twitch.html', 19 | // } 20 | // }, 21 | 22 | // chainWebpack: (config) => { 23 | // config.plugin('html') 24 | // // .use(htmlwp)//Enable this to make MPA compiling properly 25 | // .tap(args => { 26 | // if(args && args.length > 0) { 27 | // args[0].minify = false 28 | // } 29 | // return args 30 | // }) 31 | // }, 32 | 33 | configureWebpack: { 34 | plugins: [ 35 | new NodePolyfillPlugin(), 36 | new webpack.ProvidePlugin({ 37 | process: 'process/browser', 38 | Buffer: ['buffer', 'Buffer'] 39 | }), 40 | ], 41 | resolve: { 42 | alias: { 43 | '@': __dirname + '/front_src' 44 | }, 45 | fallback: { 46 | "assert": require.resolve("assert/"), 47 | "util": require.resolve("util/"), 48 | "url": require.resolve("url/"), 49 | "crypto": require.resolve("crypto-browserify"), 50 | "querystring": require.resolve("querystring-es3"), 51 | "stream": require.resolve("stream-browserify"), 52 | "http": require.resolve("stream-http"), 53 | "https": require.resolve("https-browserify"), 54 | "os": require.resolve("os-browserify/browser"), 55 | "path": require.resolve("path-browserify"), 56 | "zlib": require.resolve("browserify-zlib"), 57 | "net": false, 58 | "tls": false, 59 | "fs": false, 60 | "buffer": require.resolve("buffer/"), 61 | "process": require.resolve("process/browser"), 62 | } 63 | }, 64 | entry: { 65 | app: './front_src/main.ts' 66 | }, 67 | optimization: { 68 | minimize: false,//Avoids minifying the index which would break share meta for whatsapp 69 | } 70 | }, 71 | css: { 72 | loaderOptions: { 73 | less: { 74 | additionalData: `@import (reference) "@/less/index.less";@import (reference) "@/less/_includes.less";` 75 | } 76 | } 77 | } 78 | } --------------------------------------------------------------------------------