├── .editorconfig ├── .github └── main.workflow ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── angular.json ├── design-template.md ├── docs ├── 3rdpartylicenses.txt ├── 404.html ├── assets │ ├── favicon.ico │ ├── forward-96.png │ ├── logo-128.png │ ├── logo-152.png │ ├── logo-256.png │ ├── logo-512.png │ ├── playlist-tracks.json │ ├── repeat-green.svg │ ├── repeat.svg │ ├── rewind-96.png │ ├── shuffle-green.svg │ ├── shuffle.svg │ ├── spotify-mtv-logo.svg │ └── user-playlists.json ├── favicon.ico ├── index.html ├── main.058ed237ac5c59377964.js ├── manifest.json ├── polyfills.5fb5131975c60703500b.js ├── runtime.b57bf819d5bdce77f1c7.js └── styles.2e8e0902f5dee2fbaee1.css ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── set-creds.js ├── src ├── DEVLOG.md ├── KNOWNISSUES.md ├── TODO.md ├── app │ ├── app-materials.module.ts │ ├── app-routes.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ ├── app.module.ts │ ├── components │ │ ├── contact-notification │ │ │ ├── contact-notification.component.ts │ │ │ ├── contact-notification.css │ │ │ └── contact-notification.html │ │ ├── current-song-meta │ │ │ ├── current-song.component.ts │ │ │ ├── current-song.css │ │ │ └── current-song.html │ │ ├── sidebar │ │ │ ├── sidebar.component.ts │ │ │ ├── sidebar.css │ │ │ └── sidebar.html │ │ └── video │ │ │ ├── video.component.ts │ │ │ ├── video.css │ │ │ └── video.html │ ├── config.development.json │ ├── config.production.json │ ├── dashboard │ │ ├── dashboard.component.ts │ │ ├── dashboard.css │ │ ├── dashboard.html │ │ └── dashboard.model.ts │ ├── env.json │ ├── fourOhFour.component.ts │ ├── login │ │ ├── login.component.ts │ │ ├── login.css │ │ └── login.html │ └── shared │ │ ├── auth.state.ts │ │ └── spotify.state.ts ├── assets │ ├── .gitkeep │ ├── favicon.ico │ ├── forward-96.png │ ├── logo-128.png │ ├── logo-152.png │ ├── logo-256.png │ ├── logo-512.png │ ├── playlist-tracks.json │ ├── repeat-green.svg │ ├── repeat.svg │ ├── rewind-96.png │ ├── shuffle-green.svg │ ├── shuffle.svg │ ├── spotify-mtv-logo.svg │ └── user-playlists.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── lib │ ├── service │ │ ├── authentication │ │ │ ├── authentication.model.ts │ │ │ └── authentication.service.ts │ │ ├── data │ │ │ └── data.service.ts │ │ ├── spotify │ │ │ ├── spotify.model.ts │ │ │ ├── spotify.service.spec.ts │ │ │ └── spotify.service.ts │ │ └── youtube │ │ │ ├── youtube.model.ts │ │ │ ├── youtube.service.spec.ts │ │ │ └── youtube.service.ts │ └── utils │ │ └── safeurl.pipe.ts ├── main.ts ├── manifest.json ├── ngsw-config.json ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Rebuild Site" { 2 | on = "push" 3 | resolves = ["Build for Github Pages"] 4 | } 5 | 6 | action "GitHub Action for npm" { 7 | uses = "actions/npm@de7a3705a9510ee12702e124482fad6af249991b" 8 | runs = "install" 9 | } 10 | 11 | action "Set Credentials" { 12 | uses = "actions/npm@de7a3705a9510ee12702e124482fad6af249991b" 13 | needs = ["GitHub Action for npm"] 14 | runs = "setcreds" 15 | secrets = ["SPOTIFYID", "YOUTUBE_API_KEY"] 16 | } 17 | 18 | action "Build for Github Pages" { 19 | uses = "actions/npm@de7a3705a9510ee12702e124482fad6af249991b" 20 | needs = ["Set Credentials"] 21 | runs = "deploy:prod" 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # service files 38 | /src/app/config.*.json 39 | 40 | # e2e 41 | /e2e/*.js 42 | /e2e/*.map 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#d04649", 4 | "activityBar.activeBorder": "#37cb34", 5 | "activityBar.background": "#d04649", 6 | "activityBar.foreground": "#e7e7e7", 7 | "activityBar.inactiveForeground": "#e7e7e799", 8 | "activityBarBadge.background": "#37cb34", 9 | "activityBarBadge.foreground": "#15202b", 10 | "editorGroup.border": "#d04649", 11 | "panel.border": "#d04649", 12 | "sideBar.border": "#d04649", 13 | "statusBar.background": "#b52e31", 14 | "statusBar.border": "#b52e31", 15 | "statusBar.foreground": "#e7e7e7", 16 | "statusBarItem.hoverBackground": "#d04649", 17 | "tab.activeBorder": "#d04649", 18 | "titleBar.activeBackground": "#b52e31", 19 | "titleBar.activeForeground": "#e7e7e7", 20 | "titleBar.border": "#b52e31", 21 | "titleBar.inactiveBackground": "#b52e3199", 22 | "titleBar.inactiveForeground": "#e7e7e799" 23 | }, 24 | "peacock.color": "#b52e31" 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | !["Logo"](./src/assets/spotify-mtv-logo.svg) 2 | 3 | # Spotify Television 4 | 5 | Web application to view Spotify playlists as Youtube playlists in one UI. 6 | 7 | ## Feedback 8 | Have some suggestions or ideas to make this app better? [Send some feedback](https://spotifytv-community.glitch.me/) 9 | 10 | ### Articles 11 | [Hypebot SpotifyTelevision](http://www.hypebot.com/hypebot/2018/03/spotify-television-turns-playlists-into-youtube-stream.html) 12 | 13 | ### Dev Dependencies for future reference: 14 | [ngx-youtube-player](https://github.com/orizens/ngx-youtube-player) 15 | 16 | ## Angular CLI Mumbo Jumbo 17 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.6.5. 18 | 19 | ### Development server 20 | 21 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 22 | 23 | ### Build 24 | 25 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 26 | 27 | ### Running unit tests 28 | 29 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 30 | 31 | ### Running end-to-end tests 32 | 33 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 34 | 35 | ### Further help 36 | 37 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 38 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "SpotifyTelevision": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico", 22 | "src/manifest.json" 23 | ], 24 | "styles": [ 25 | "src/styles.css" 26 | ], 27 | "scripts": [] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "optimization": true, 32 | "outputHashing": "all", 33 | "sourceMap": false, 34 | "extractCss": true, 35 | "namedChunks": false, 36 | "aot": true, 37 | "extractLicenses": true, 38 | "vendorChunk": false, 39 | "buildOptimizer": true, 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ] 46 | } 47 | } 48 | }, 49 | "serve": { 50 | "builder": "@angular-devkit/build-angular:dev-server", 51 | "options": { 52 | "browserTarget": "SpotifyTelevision:build" 53 | }, 54 | "configurations": { 55 | "production": { 56 | "browserTarget": "SpotifyTelevision:build:production" 57 | } 58 | } 59 | }, 60 | "extract-i18n": { 61 | "builder": "@angular-devkit/build-angular:extract-i18n", 62 | "options": { 63 | "browserTarget": "SpotifyTelevision:build" 64 | } 65 | }, 66 | "test": { 67 | "builder": "@angular-devkit/build-angular:karma", 68 | "options": { 69 | "main": "src/test.ts", 70 | "karmaConfig": "./karma.conf.js", 71 | "polyfills": "src/polyfills.ts", 72 | "tsConfig": "src/tsconfig.spec.json", 73 | "scripts": [], 74 | "styles": [ 75 | "src/styles.css" 76 | ], 77 | "assets": [ 78 | "src/assets", 79 | "src/favicon.ico", 80 | "src/manifest.json" 81 | ] 82 | } 83 | }, 84 | "lint": { 85 | "builder": "@angular-devkit/build-angular:tslint", 86 | "options": { 87 | "tsConfig": [ 88 | "src/tsconfig.app.json", 89 | "src/tsconfig.spec.json" 90 | ], 91 | "exclude": [ 92 | "**/node_modules/**" 93 | ] 94 | } 95 | } 96 | } 97 | }, 98 | "SpotifyTelevision-e2e": { 99 | "root": "", 100 | "sourceRoot": "", 101 | "projectType": "application", 102 | "architect": { 103 | "e2e": { 104 | "builder": "@angular-devkit/build-angular:protractor", 105 | "options": { 106 | "protractorConfig": "./protractor.conf.js", 107 | "devServerTarget": "SpotifyTelevision:serve" 108 | } 109 | }, 110 | "lint": { 111 | "builder": "@angular-devkit/build-angular:tslint", 112 | "options": { 113 | "tsConfig": [ 114 | "e2e/tsconfig.e2e.json" 115 | ], 116 | "exclude": [ 117 | "**/node_modules/**" 118 | ] 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "defaultProject": "SpotifyTelevision", 125 | "schematics": { 126 | "@schematics/angular:component": { 127 | "prefix": "app", 128 | "styleext": "css" 129 | }, 130 | "@schematics/angular:directive": { 131 | "prefix": "app" 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /design-template.md: -------------------------------------------------------------------------------- 1 | ## Design Template: 2 | 3 | #### Colors: 4 | [Color Template](https://coolors.co/0c0f0a-ff206e-fbff12-41ead4-ffffff)
5 | [runner up](https://coolors.co/3772ff-f038ff-ef709d-e2ef70-70e4ef)
6 | 7 | #0C0F0A 8 | #FF206E 9 | #FBFF12 10 | #41EAD4 11 | #FFFFFF 12 | 13 | 14 | #### Fonts: 15 | TODO -------------------------------------------------------------------------------- /docs/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | tslib 2 | Apache-2.0 3 | Apache License 4 | 5 | Version 2.0, January 2004 6 | 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 18 | 19 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 20 | 21 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 22 | 23 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 24 | 25 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 26 | 27 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 28 | 29 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 30 | 31 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 32 | 33 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 34 | 35 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 36 | 37 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 38 | 39 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 40 | 41 | You must cause any modified files to carry prominent notices stating that You changed the files; and 42 | 43 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 44 | 45 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 46 | 47 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 48 | 49 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 50 | 51 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 52 | 53 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 54 | 55 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 56 | 57 | END OF TERMS AND CONDITIONS 58 | 59 | 60 | rxjs 61 | Apache-2.0 62 | Apache License 63 | Version 2.0, January 2004 64 | http://www.apache.org/licenses/ 65 | 66 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 67 | 68 | 1. Definitions. 69 | 70 | "License" shall mean the terms and conditions for use, reproduction, 71 | and distribution as defined by Sections 1 through 9 of this document. 72 | 73 | "Licensor" shall mean the copyright owner or entity authorized by 74 | the copyright owner that is granting the License. 75 | 76 | "Legal Entity" shall mean the union of the acting entity and all 77 | other entities that control, are controlled by, or are under common 78 | control with that entity. For the purposes of this definition, 79 | "control" means (i) the power, direct or indirect, to cause the 80 | direction or management of such entity, whether by contract or 81 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 82 | outstanding shares, or (iii) beneficial ownership of such entity. 83 | 84 | "You" (or "Your") shall mean an individual or Legal Entity 85 | exercising permissions granted by this License. 86 | 87 | "Source" form shall mean the preferred form for making modifications, 88 | including but not limited to software source code, documentation 89 | source, and configuration files. 90 | 91 | "Object" form shall mean any form resulting from mechanical 92 | transformation or translation of a Source form, including but 93 | not limited to compiled object code, generated documentation, 94 | and conversions to other media types. 95 | 96 | "Work" shall mean the work of authorship, whether in Source or 97 | Object form, made available under the License, as indicated by a 98 | copyright notice that is included in or attached to the work 99 | (an example is provided in the Appendix below). 100 | 101 | "Derivative Works" shall mean any work, whether in Source or Object 102 | form, that is based on (or derived from) the Work and for which the 103 | editorial revisions, annotations, elaborations, or other modifications 104 | represent, as a whole, an original work of authorship. For the purposes 105 | of this License, Derivative Works shall not include works that remain 106 | separable from, or merely link (or bind by name) to the interfaces of, 107 | the Work and Derivative Works thereof. 108 | 109 | "Contribution" shall mean any work of authorship, including 110 | the original version of the Work and any modifications or additions 111 | to that Work or Derivative Works thereof, that is intentionally 112 | submitted to Licensor for inclusion in the Work by the copyright owner 113 | or by an individual or Legal Entity authorized to submit on behalf of 114 | the copyright owner. For the purposes of this definition, "submitted" 115 | means any form of electronic, verbal, or written communication sent 116 | to the Licensor or its representatives, including but not limited to 117 | communication on electronic mailing lists, source code control systems, 118 | and issue tracking systems that are managed by, or on behalf of, the 119 | Licensor for the purpose of discussing and improving the Work, but 120 | excluding communication that is conspicuously marked or otherwise 121 | designated in writing by the copyright owner as "Not a Contribution." 122 | 123 | "Contributor" shall mean Licensor and any individual or Legal Entity 124 | on behalf of whom a Contribution has been received by Licensor and 125 | subsequently incorporated within the Work. 126 | 127 | 2. Grant of Copyright License. Subject to the terms and conditions of 128 | this License, each Contributor hereby grants to You a perpetual, 129 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 130 | copyright license to reproduce, prepare Derivative Works of, 131 | publicly display, publicly perform, sublicense, and distribute the 132 | Work and such Derivative Works in Source or Object form. 133 | 134 | 3. Grant of Patent License. Subject to the terms and conditions of 135 | this License, each Contributor hereby grants to You a perpetual, 136 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 137 | (except as stated in this section) patent license to make, have made, 138 | use, offer to sell, sell, import, and otherwise transfer the Work, 139 | where such license applies only to those patent claims licensable 140 | by such Contributor that are necessarily infringed by their 141 | Contribution(s) alone or by combination of their Contribution(s) 142 | with the Work to which such Contribution(s) was submitted. If You 143 | institute patent litigation against any entity (including a 144 | cross-claim or counterclaim in a lawsuit) alleging that the Work 145 | or a Contribution incorporated within the Work constitutes direct 146 | or contributory patent infringement, then any patent licenses 147 | granted to You under this License for that Work shall terminate 148 | as of the date such litigation is filed. 149 | 150 | 4. Redistribution. You may reproduce and distribute copies of the 151 | Work or Derivative Works thereof in any medium, with or without 152 | modifications, and in Source or Object form, provided that You 153 | meet the following conditions: 154 | 155 | (a) You must give any other recipients of the Work or 156 | Derivative Works a copy of this License; and 157 | 158 | (b) You must cause any modified files to carry prominent notices 159 | stating that You changed the files; and 160 | 161 | (c) You must retain, in the Source form of any Derivative Works 162 | that You distribute, all copyright, patent, trademark, and 163 | attribution notices from the Source form of the Work, 164 | excluding those notices that do not pertain to any part of 165 | the Derivative Works; and 166 | 167 | (d) If the Work includes a "NOTICE" text file as part of its 168 | distribution, then any Derivative Works that You distribute must 169 | include a readable copy of the attribution notices contained 170 | within such NOTICE file, excluding those notices that do not 171 | pertain to any part of the Derivative Works, in at least one 172 | of the following places: within a NOTICE text file distributed 173 | as part of the Derivative Works; within the Source form or 174 | documentation, if provided along with the Derivative Works; or, 175 | within a display generated by the Derivative Works, if and 176 | wherever such third-party notices normally appear. The contents 177 | of the NOTICE file are for informational purposes only and 178 | do not modify the License. You may add Your own attribution 179 | notices within Derivative Works that You distribute, alongside 180 | or as an addendum to the NOTICE text from the Work, provided 181 | that such additional attribution notices cannot be construed 182 | as modifying the License. 183 | 184 | You may add Your own copyright statement to Your modifications and 185 | may provide additional or different license terms and conditions 186 | for use, reproduction, or distribution of Your modifications, or 187 | for any such Derivative Works as a whole, provided Your use, 188 | reproduction, and distribution of the Work otherwise complies with 189 | the conditions stated in this License. 190 | 191 | 5. Submission of Contributions. Unless You explicitly state otherwise, 192 | any Contribution intentionally submitted for inclusion in the Work 193 | by You to the Licensor shall be under the terms and conditions of 194 | this License, without any additional terms or conditions. 195 | Notwithstanding the above, nothing herein shall supersede or modify 196 | the terms of any separate license agreement you may have executed 197 | with Licensor regarding such Contributions. 198 | 199 | 6. Trademarks. This License does not grant permission to use the trade 200 | names, trademarks, service marks, or product names of the Licensor, 201 | except as required for reasonable and customary use in describing the 202 | origin of the Work and reproducing the content of the NOTICE file. 203 | 204 | 7. Disclaimer of Warranty. Unless required by applicable law or 205 | agreed to in writing, Licensor provides the Work (and each 206 | Contributor provides its Contributions) on an "AS IS" BASIS, 207 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 208 | implied, including, without limitation, any warranties or conditions 209 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 210 | PARTICULAR PURPOSE. You are solely responsible for determining the 211 | appropriateness of using or redistributing the Work and assume any 212 | risks associated with Your exercise of permissions under this License. 213 | 214 | 8. Limitation of Liability. In no event and under no legal theory, 215 | whether in tort (including negligence), contract, or otherwise, 216 | unless required by applicable law (such as deliberate and grossly 217 | negligent acts) or agreed to in writing, shall any Contributor be 218 | liable to You for damages, including any direct, indirect, special, 219 | incidental, or consequential damages of any character arising as a 220 | result of this License or out of the use or inability to use the 221 | Work (including but not limited to damages for loss of goodwill, 222 | work stoppage, computer failure or malfunction, or any and all 223 | other commercial damages or losses), even if such Contributor 224 | has been advised of the possibility of such damages. 225 | 226 | 9. Accepting Warranty or Additional Liability. While redistributing 227 | the Work or Derivative Works thereof, You may choose to offer, 228 | and charge a fee for, acceptance of support, warranty, indemnity, 229 | or other liability obligations and/or rights consistent with this 230 | License. However, in accepting such obligations, You may act only 231 | on Your own behalf and on Your sole responsibility, not on behalf 232 | of any other Contributor, and only if You agree to indemnify, 233 | defend, and hold each Contributor harmless for any liability 234 | incurred by, or claims asserted against, such Contributor by reason 235 | of your accepting any such warranty or additional liability. 236 | 237 | END OF TERMS AND CONDITIONS 238 | 239 | APPENDIX: How to apply the Apache License to your work. 240 | 241 | To apply the Apache License to your work, attach the following 242 | boilerplate notice, with the fields enclosed by brackets "[]" 243 | replaced with your own identifying information. (Don't include 244 | the brackets!) The text should be enclosed in the appropriate 245 | comment syntax for the file format. We also recommend that a 246 | file or class name and description of purpose be included on the 247 | same "printed page" as the copyright notice for easier 248 | identification within third-party archives. 249 | 250 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 251 | 252 | Licensed under the Apache License, Version 2.0 (the "License"); 253 | you may not use this file except in compliance with the License. 254 | You may obtain a copy of the License at 255 | 256 | http://www.apache.org/licenses/LICENSE-2.0 257 | 258 | Unless required by applicable law or agreed to in writing, software 259 | distributed under the License is distributed on an "AS IS" BASIS, 260 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 261 | See the License for the specific language governing permissions and 262 | limitations under the License. 263 | 264 | 265 | 266 | @angular/core 267 | MIT 268 | 269 | @angular/common 270 | MIT 271 | 272 | @angular/platform-browser 273 | MIT 274 | 275 | @angular/router 276 | MIT 277 | 278 | @angular/material 279 | MIT 280 | The MIT License 281 | 282 | Copyright (c) 2019 Google LLC. 283 | 284 | Permission is hereby granted, free of charge, to any person obtaining a copy 285 | of this software and associated documentation files (the "Software"), to deal 286 | in the Software without restriction, including without limitation the rights 287 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 288 | copies of the Software, and to permit persons to whom the Software is 289 | furnished to do so, subject to the following conditions: 290 | 291 | The above copyright notice and this permission notice shall be included in 292 | all copies or substantial portions of the Software. 293 | 294 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 295 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 296 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 297 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 298 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 299 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 300 | THE SOFTWARE. 301 | 302 | 303 | @angular/cdk 304 | MIT 305 | The MIT License 306 | 307 | Copyright (c) 2019 Google LLC. 308 | 309 | Permission is hereby granted, free of charge, to any person obtaining a copy 310 | of this software and associated documentation files (the "Software"), to deal 311 | in the Software without restriction, including without limitation the rights 312 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 313 | copies of the Software, and to permit persons to whom the Software is 314 | furnished to do so, subject to the following conditions: 315 | 316 | The above copyright notice and this permission notice shall be included in 317 | all copies or substantial portions of the Software. 318 | 319 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 320 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 321 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 322 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 323 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 324 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 325 | THE SOFTWARE. 326 | 327 | 328 | @angular/material/card 329 | 330 | @angular/animations 331 | MIT 332 | 333 | @angular/material/button 334 | 335 | @ngxs/store 336 | MIT 337 | 338 | ngx-youtube-player 339 | MIT 340 | MIT License 341 | 342 | Copyright (c) 2018 Oren Farhi 343 | 344 | Permission is hereby granted, free of charge, to any person obtaining a copy 345 | of this software and associated documentation files (the "Software"), to deal 346 | in the Software without restriction, including without limitation the rights 347 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 348 | copies of the Software, and to permit persons to whom the Software is 349 | furnished to do so, subject to the following conditions: 350 | 351 | The above copyright notice and this permission notice shall be included in all 352 | copies or substantial portions of the Software. 353 | 354 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 355 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 356 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 357 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 358 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 359 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 360 | SOFTWARE. 361 | 362 | 363 | @angular/forms 364 | MIT 365 | 366 | @angular/material/core 367 | 368 | @angular/material/list 369 | 370 | @angular/material/expansion 371 | 372 | @angular/material/tooltip 373 | 374 | core-js 375 | MIT 376 | Copyright (c) 2014-2019 Denis Pushkarev 377 | 378 | Permission is hereby granted, free of charge, to any person obtaining a copy 379 | of this software and associated documentation files (the "Software"), to deal 380 | in the Software without restriction, including without limitation the rights 381 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 382 | copies of the Software, and to permit persons to whom the Software is 383 | furnished to do so, subject to the following conditions: 384 | 385 | The above copyright notice and this permission notice shall be included in 386 | all copies or substantial portions of the Software. 387 | 388 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 389 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 390 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 391 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 392 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 393 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 394 | THE SOFTWARE. 395 | 396 | 397 | zone.js 398 | MIT 399 | The MIT License 400 | 401 | Copyright (c) 2016-2018 Google, Inc. 402 | 403 | Permission is hereby granted, free of charge, to any person obtaining a copy 404 | of this software and associated documentation files (the "Software"), to deal 405 | in the Software without restriction, including without limitation the rights 406 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 407 | copies of the Software, and to permit persons to whom the Software is 408 | furnished to do so, subject to the following conditions: 409 | 410 | The above copyright notice and this permission notice shall be included in 411 | all copies or substantial portions of the Software. 412 | 413 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 414 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 415 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 416 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 417 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 418 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 419 | THE SOFTWARE. 420 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | SpotifyTV 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/forward-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/forward-96.png -------------------------------------------------------------------------------- /docs/assets/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/logo-128.png -------------------------------------------------------------------------------- /docs/assets/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/logo-152.png -------------------------------------------------------------------------------- /docs/assets/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/logo-256.png -------------------------------------------------------------------------------- /docs/assets/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/logo-512.png -------------------------------------------------------------------------------- /docs/assets/repeat-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/assets/repeat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/assets/rewind-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/assets/rewind-96.png -------------------------------------------------------------------------------- /docs/assets/shuffle-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/assets/shuffle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 22 | 26 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/assets/spotify-mtv-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | SpotifyTV 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spotify Television", 3 | "short_name": "SpotifyTV", 4 | "description": "Watch music videos for the songs in your Spotify playlists", 5 | "icons": [{ 6 | "src": "assets/logo-128.png", 7 | "sizes": "128x128", 8 | "type": "image/png" 9 | }, { 10 | "src": "assets/logo-152.png", 11 | "sizes": "152x152", 12 | "type": "image/png" 13 | }, { 14 | "src": "assets/logo-256.png", 15 | "sizes": "256x256", 16 | "type": "image/png" 17 | }, { 18 | "src": "assets/logo-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }], 22 | "start_url": "index.html", 23 | "display": "standalone", 24 | "background_color": "#ffffff", 25 | "theme_color": "#23CF5F" 26 | } -------------------------------------------------------------------------------- /docs/runtime.b57bf819d5bdce77f1c7.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iwant-my-mtv", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "create404": "cp ./docs/index.html ./docs/404.html", 9 | "prebuild": "rm -rf docs", 10 | "build": "ng build --prod --aot --output-path docs --base-href \"https://immannino.github.io/SpotifyTelevision/\"", 11 | "postbuild": "npm run create404", 12 | "test": "ng test", 13 | "lint": "ng lint", 14 | "e2e": "ng e2e" 15 | }, 16 | "private": true, 17 | "dependencies": { 18 | "@angular-devkit/schematics": "^7.2.3", 19 | "@angular/animations": "^7.2.2", 20 | "@angular/cdk": "^7.2.2", 21 | "@angular/common": "^7.2.2", 22 | "@angular/compiler": "^7.2.2", 23 | "@angular/core": "^7.2.2", 24 | "@angular/forms": "^7.2.2", 25 | "@angular/http": "^7.2.2", 26 | "@angular/material": "^7.2.2", 27 | "@angular/platform-browser": "^7.2.2", 28 | "@angular/platform-browser-dynamic": "^7.2.2", 29 | "@angular/router": "^7.2.2", 30 | "@angular/service-worker": "^7.2.2", 31 | "@ngxs/store": "^3.3.4", 32 | "@types/youtube": "0.0.35", 33 | "core-js": "^2.6.3", 34 | "ngx-youtube-player": "6.0.0", 35 | "rxjs": "^6.3.3", 36 | "youtube": "^0.1.0", 37 | "youtube-player": "^5.5.2", 38 | "zone.js": "^0.8.29" 39 | }, 40 | "devDependencies": { 41 | "@angular/cli": "^7.2.3", 42 | "@angular/compiler-cli": "^7.2.2", 43 | "@angular/language-service": "^7.2.2", 44 | "@types/jasmine": "~3.3.8", 45 | "@types/jasminewd2": "~2.0.6", 46 | "@types/node": "~10.12.18", 47 | "codelyzer": "^4.5.0", 48 | "jasmine-core": "~3.3.0", 49 | "jasmine-spec-reporter": "~4.2.1", 50 | "karma": "~4.0.0", 51 | "karma-chrome-launcher": "~2.2.0", 52 | "karma-cli": "~2.0.0", 53 | "karma-coverage-istanbul-reporter": "^2.0.4", 54 | "karma-jasmine": "~2.0.1", 55 | "karma-jasmine-html-reporter": "^1.4.0", 56 | "protractor": "~5.4.2", 57 | "ts-node": "~8.0.2", 58 | "tslint": "~5.12.1", 59 | "typescript": "~3.2.4", 60 | "@angular-devkit/build-angular": "~0.12.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /set-creds.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const config = require('./src/app/config.development.json'); 3 | const spotifyClientid = process.env.SPOTIFYID; 4 | const youtubeKey = process.env.YOUTUBE_API_KEY; 5 | 6 | module.exports = () => { 7 | if (!spotifyClientid) throw new Error('Spotify Client ID not set in environment.'); 8 | if (!youtubeKey) throw new Error('Youtube API Key not set in environment.') 9 | 10 | config.spotify.clientid = spotifyClientid; 11 | config.youtube.apikey = youtubeKey; 12 | 13 | fs.writeFileSync('./src/app/config.development.json', JSON.stringify(config)); 14 | } -------------------------------------------------------------------------------- /src/DEVLOG.md: -------------------------------------------------------------------------------- 1 | - Date: 02-15-2018 2 | - Today I was able to rewrite the design and some other features of the application. I rewrote the Design using Material design and angular, I cached the client credientials, and I looked over some other data. 3 | - Still todo: I need to cache the local data of the user spotify data. 4 | - I also need to paginate playlists + songs of playlists 5 | - I also need to convert UI to be a responsive UI grid 6 | - I also need to make the credentials available from a service and not stored in the UI. -------------------------------------------------------------------------------- /src/KNOWNISSUES.md: -------------------------------------------------------------------------------- 1 | ### ISSUES 2 | - Update designs -------------------------------------------------------------------------------- /src/TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | - login Componeont 4 | - If not logging in through spotify, Auth using my user and display 'browse' and 'demo' playlists. 5 | 6 | - Long Term: 7 | - Styling. Still working on ideas. 8 | - Dockerize app. 9 | - Unit testing. 10 | - CICD 11 | - Make website responsive for mobile + web design 12 | - Userdata caching (into localStorage) 13 | 14 | - Finished 15 | - Name for app. 16 | - Logo design. 17 | - Auth token caching. -------------------------------------------------------------------------------- /src/app/app-materials.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { MatCommonModule, MatButtonModule, MatSidenavModule, MatListModule, MatExpansionModule, MatCardModule, MatTooltipModule } from '@angular/material'; 4 | 5 | @NgModule({ 6 | imports: [ BrowserAnimationsModule, MatCommonModule, MatButtonModule, MatSidenavModule, MatListModule, MatExpansionModule, MatCardModule, MatTooltipModule ], 7 | exports: [ BrowserAnimationsModule, MatCommonModule, MatButtonModule, MatSidenavModule, MatListModule, MatExpansionModule, MatCardModule, MatTooltipModule ], 8 | }) 9 | export class AppMaterialsModule { } -------------------------------------------------------------------------------- /src/app/app-routes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { DashboardComponent } from './dashboard/dashboard.component'; 5 | import { LoginComponent } from './login/login.component'; 6 | import { FourOhFourComponent } from './fourOhFour.component'; 7 | 8 | const appRoutes: Routes = [ 9 | { path: 'login', component: LoginComponent }, 10 | { path: 'dashboard', component: DashboardComponent }, 11 | 12 | { path: '', redirectTo: '/login', pathMatch: 'full' }, 13 | { path: '**', component: FourOhFourComponent } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [ 18 | RouterModule.forRoot( 19 | appRoutes 20 | // { enableTracing: true } // <-- debugging purposes only 21 | ) 22 | ], 23 | exports: [ 24 | RouterModule 25 | ] 26 | }) 27 | export class AppRoutingModule {} -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent { 11 | constructor(private router: Router) {} 12 | 13 | routeHome() { 14 | this.router.navigate(['/login']); 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import * as data from './config.development.json'; 3 | 4 | @Injectable() 5 | export class AppConfig { 6 | 7 | private config: any = null; 8 | 9 | constructor() { } 10 | 11 | /** 12 | * Use to get the data found in the second file (config file) 13 | */ 14 | public getConfig(key: any) { 15 | return this.config.default[key]; 16 | } 17 | 18 | /** 19 | * This method: 20 | * a) Loads "env.json" to get the current working environment (e.g.: 'production', 'development') 21 | * b) Loads "config.[env].json" to get all env's variables (e.g.: 'config.development.json') 22 | */ 23 | public load() { 24 | this.config = data; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { APP_INITIALIZER } from '@angular/core'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { AppConfig } from './app.config'; 6 | import { RouterModule, Routes } from '@angular/router'; 7 | 8 | // import { ServiceWorkerModule } from '@angular/service-worker'; 9 | import { environment } from '../environments/environment'; 10 | 11 | import { AppRoutingModule } from './app-routes.module'; 12 | import { AppMaterialsModule } from './app-materials.module'; 13 | import { YoutubePlayerModule } from 'ngx-youtube-player'; 14 | 15 | import { AppComponent } from './app.component'; 16 | import { LoginComponent } from './login/login.component'; 17 | import { DashboardComponent } from './dashboard/dashboard.component'; 18 | import { FourOhFourComponent } from './fourOhFour.component'; 19 | import { SafeUrlPipe } from '../lib/utils/safeurl.pipe'; 20 | 21 | import { AuthenticationService } from '../lib/service/authentication/authentication.service'; 22 | import { SpotifyService } from '../lib/service/spotify/spotify.service'; 23 | import { YoutubeService } from '../lib/service/youtube/youtube.service'; 24 | import { NgxsModule } from '@ngxs/store'; 25 | import { SpotifyAuthState } from './shared/auth.state'; 26 | import { SpotifyDataState } from './shared/spotify.state'; 27 | import { DataService } from '../lib/service/data/data.service'; 28 | import { VideoComponent } from './components/video/video.component'; 29 | import { SidebarComponent } from './components/sidebar/sidebar.component'; 30 | import { CurrentSongComponent } from './components/current-song-meta/current-song.component'; 31 | import {ContactNotifyComponent} from './components/contact-notification/contact-notification.component'; 32 | 33 | @NgModule({ 34 | declarations: [ 35 | AppComponent, 36 | LoginComponent, 37 | DashboardComponent, 38 | FourOhFourComponent, 39 | VideoComponent, 40 | SidebarComponent, 41 | SafeUrlPipe, 42 | CurrentSongComponent, 43 | ContactNotifyComponent 44 | ], 45 | imports: [ 46 | BrowserModule, 47 | HttpClientModule, 48 | AppRoutingModule, 49 | AppMaterialsModule, 50 | NgxsModule.forRoot([SpotifyAuthState, SpotifyDataState]), 51 | YoutubePlayerModule 52 | // environment.production ? ServiceWorkerModule.register('/ngsw-worker.js') : [] 53 | ], 54 | providers: [ 55 | YoutubeService, 56 | SpotifyService, 57 | AuthenticationService, 58 | DataService, 59 | AppConfig, 60 | { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true } 61 | ], 62 | bootstrap: [AppComponent] 63 | }) 64 | export class AppModule { } 65 | -------------------------------------------------------------------------------- /src/app/components/contact-notification/contact-notification.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from "@angular/core"; 2 | 3 | @Component({ 4 | selector: 'contact-notify', 5 | templateUrl: './contact-notification.html', 6 | styleUrls: [ './contact-notification.css' ] 7 | }) 8 | export class ContactNotifyComponent implements OnInit { 9 | showMessage: boolean = true; 10 | constructor() {} 11 | 12 | ngOnInit() {} 13 | 14 | hideMessage() { 15 | this.showMessage = false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/contact-notification/contact-notification.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: ghostwhite; 3 | color: black; 4 | display: flex; 5 | justify-content: space-evenly; 6 | flex-direction: column; 7 | font-size: 1.25rem; 8 | padding: 0.25rem; 9 | border: 1px solid blue; 10 | } 11 | 12 | 13 | .container > a { 14 | padding: 0.25rem; 15 | } 16 | 17 | .close { 18 | color: red; 19 | } 20 | 21 | .glitchLink { 22 | color: blue; 23 | } 24 | 25 | .container > a:hover { 26 | cursor: pointer; 27 | } 28 | 29 | @media screen and (min-width: 600px) { 30 | .container { 31 | flex-direction: row; 32 | width: 90%; 33 | box-shadow: 5px 5px 0px blue; 34 | margin: 0 auto; 35 | margin-top: 0.25rem; 36 | } 37 | } -------------------------------------------------------------------------------- /src/app/components/contact-notification/contact-notification.html: -------------------------------------------------------------------------------- 1 |
2 |
Psst. I'm looking for some feedback on Spotify Television.
3 | Click here for info regarding the SpotifyTV! 4 | X Close this Msg 5 |
-------------------------------------------------------------------------------- /src/app/components/current-song-meta/current-song.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { Store } from "@ngxs/store"; 3 | import { SetShuffle, SetRepeat } from "../../shared/spotify.state"; 4 | import { DataService } from "../../../lib/service/data/data.service"; 5 | import { SpotifyPlaylistTrack } from "../../../lib/service/spotify/spotify.model"; 6 | 7 | @Component({ 8 | moduleId: module.id, 9 | selector: 'current-song', 10 | templateUrl: 'current-song.html', 11 | styleUrls: ['current-song.css'] 12 | }) 13 | export class CurrentSongComponent { 14 | currentPlayingSpotifySong: SpotifyPlaylistTrack = null; 15 | playerStatus: boolean = false; 16 | isRandom: boolean = false; 17 | isRepeat: boolean = false; 18 | shuffleImgSrc: string = './assets/shuffle.svg'; 19 | repeatImgSrc: string = './assets/repeat.svg'; 20 | playText: string = 'Play'; 21 | 22 | constructor(private store: Store, private dataService: DataService) { 23 | this.dataService.currentSongSubject.subscribe((song) => { 24 | this.currentPlayingSpotifySong = song; 25 | }); 26 | 27 | this.dataService.playerStatusSubject.subscribe((status) => { 28 | this.playerStatus = status; 29 | }); 30 | 31 | this.dataService.playPauseSubject.subscribe((status) => { 32 | this.playText = status; 33 | }); 34 | } 35 | 36 | changeCurrentSong(changeVal: number) { 37 | this.dataService.toggleNextSong(changeVal); 38 | } 39 | 40 | setShuffleFlag() { 41 | if (this.isRandom) { 42 | this.shuffleImgSrc = "./assets/shuffle.svg"; 43 | } else { 44 | this.shuffleImgSrc = "./assets/shuffle-green.svg" 45 | } 46 | 47 | this.isRandom = !this.isRandom; 48 | this.store.dispatch(new SetShuffle(this.isRandom)); 49 | } 50 | 51 | setRepeatFlag() { 52 | if (this.isRepeat) { 53 | this.repeatImgSrc = "./assets/repeat.svg"; 54 | } else { 55 | this.repeatImgSrc = "./assets/repeat-green.svg" 56 | } 57 | 58 | this.isRepeat = !this.isRepeat; 59 | 60 | this.store.dispatch(new SetRepeat(this.isRepeat)); 61 | } 62 | 63 | togglePlay() { 64 | let playPauseText = ''; 65 | this.playerStatus ? playPauseText = 'Pause' : playPauseText = 'Play'; 66 | 67 | this.dataService.togglePlayPause(playPauseText); 68 | this.dataService.updatePlayerStatus(!this.playerStatus); 69 | } 70 | } -------------------------------------------------------------------------------- /src/app/components/current-song-meta/current-song.css: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | Css in this section is regarding current song play info. 3 | ************************************************************************/ 4 | 5 | @media (max-width: 599px) { 6 | .current-song { 7 | display: grid; 8 | grid-template-rows: 40px 1fr; 9 | text-align: center; 10 | margin: auto; 11 | color: white; 12 | background-color: #424242; 13 | /* width: 100vw; */ 14 | padding-right: 25px; 15 | } 16 | } 17 | 18 | @media (min-width: 600px) { 19 | .current-song { 20 | display: grid; 21 | grid-template-columns: 100px 1fr; 22 | margin: auto; 23 | color: white; 24 | background-color: #424242; 25 | max-width: 50%; 26 | } 27 | } 28 | .song-meta { 29 | margin-left: 10px; 30 | max-height: 50px; 31 | } 32 | .track-name { 33 | font-size: 16px; 34 | } 35 | .artist-name { 36 | /* font-size: 16px; */ 37 | font-weight: 400; 38 | } 39 | 40 | /************************************************************************ 41 | Css in this section is regarding the previous and next buttons for songs. 42 | ************************************************************************/ 43 | .track-buttons { 44 | display: grid; 45 | /* height: 25px; */ 46 | grid-gap: 5px; 47 | grid-template-rows: 1fr; 48 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr; 49 | text-align: center; 50 | padding-top: 5px; 51 | } 52 | 53 | img { 54 | height: 30px; 55 | width: 30px; 56 | } 57 | 58 | .icon-container { 59 | width: 100%; 60 | display:block; 61 | margin:auto; 62 | 63 | } 64 | 65 | .icon-container:hover { 66 | background-color: #e0e0e0; 67 | cursor: pointer; 68 | } 69 | 70 | .icon-container > img:hover { 71 | background-color: #e0e0e0; 72 | /* cursor: pointer; */ 73 | } 74 | 75 | .shuffle-icon { 76 | height: 20px; 77 | width: 20px; 78 | } 79 | 80 | .repeat-icon { 81 | height: 20px; 82 | width: 20px; 83 | } -------------------------------------------------------------------------------- /src/app/components/current-song-meta/current-song.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Pending Song
4 |
5 |
{{currentPlayingSpotifySong.track.name}}
6 |
{{currentPlayingSpotifySong.track.artists[0].name}}
7 |
8 |
9 |
10 | shuffle 11 |
12 |
13 | previous 14 |
15 | 16 |
17 | forward 18 |
19 |
20 | repeat 21 |
22 |
23 |
-------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { DataService } from '../../../lib/service/data/data.service'; 3 | import { UserSpotifyPlaylists, SpotifyPlaylistTracks, SpotifyPlaylistTrack } from '../../../lib/service/spotify/spotify.model'; 4 | import { Store } from '@ngxs/store'; 5 | import { SetSinglePlaylist, SetPlaylists, SetTrackIndex, SetPlayingSong, SetRepeat, SetShuffle } from '../../shared/spotify.state'; 6 | import { SpotifyService } from '../../../lib/service/spotify/spotify.service'; 7 | import { Router } from '@angular/router'; 8 | 9 | @Component({ 10 | moduleId: module.id, 11 | selector: 'sidebar', 12 | templateUrl: 'sidebar.html', 13 | styleUrls: [ 'sidebar.css' ], 14 | encapsulation: ViewEncapsulation.None, 15 | }) 16 | export class SidebarComponent { 17 | spotifyPlaylists: UserSpotifyPlaylists = null; 18 | currentSpotifyPlaylistSongs: SpotifyPlaylistTracks = null; 19 | currentPlayingSpotifySong: SpotifyPlaylistTrack = null; 20 | selectedPlaylistIndex: number = -1; 21 | isRandom: boolean = false; 22 | isRepeat: boolean = false; 23 | 24 | constructor(private store: Store, private dataService: DataService, private spotifyService: SpotifyService, private router: Router) { 25 | this.dataService.userPlaylistsSubject.subscribe((playlists) => { 26 | this.spotifyPlaylists = playlists; 27 | }); 28 | 29 | this.dataService.playlistSubject.subscribe((playlist) => { 30 | this.currentSpotifyPlaylistSongs = playlist; 31 | }); 32 | 33 | this.dataService.currentSongSubject.subscribe((song) => { 34 | this.currentPlayingSpotifySong = song; 35 | }) 36 | } 37 | 38 | expandPlaylist(index: number) { 39 | // check whether tracks have been loaded or not for this playlist 40 | this.selectedPlaylistIndex = index; 41 | 42 | if (this.spotifyPlaylists.items[index].tracks_local) { 43 | this.currentSpotifyPlaylistSongs = this.spotifyPlaylists.items[index].tracks_local; 44 | 45 | this.store.dispatch(new SetSinglePlaylist(this.currentSpotifyPlaylistSongs)); 46 | } else { 47 | this.getSpotifyPlaylistTracks(index); 48 | } 49 | } 50 | 51 | getSpotifyPlaylistTracks(index: number) { 52 | this.spotifyService.getUserPlaylistTracks(this.spotifyPlaylists.items[index].id, this.spotifyPlaylists.items[index].owner.id).subscribe((playlistTracks: SpotifyPlaylistTracks) => { 53 | 54 | // Cache local tracks 55 | this.spotifyPlaylists.items[index].tracks = playlistTracks; 56 | this.spotifyPlaylists.items[index].tracks_local = playlistTracks; 57 | 58 | // Set current list of songs in sidebar 59 | this.currentSpotifyPlaylistSongs = playlistTracks; 60 | 61 | this.store.dispatch(new SetPlaylists(this.spotifyPlaylists)).subscribe(() => { 62 | this.dataService.updateUserPlaylists(this.spotifyPlaylists); 63 | if (playlistTracks.next) this.getSpotifyPlaylistTracksPaginate(index, playlistTracks.next); 64 | }); 65 | }, error => this.handleApiError(error), () => { }); 66 | } 67 | 68 | getSpotifyPlaylistTracksPaginate(index: number, paginateUrl: string) { 69 | this.spotifyService.getUserPlaylistTracksPaginate(paginateUrl).subscribe((playlistTracks: SpotifyPlaylistTracks) => { 70 | for (let playlistTrack of playlistTracks.items) { 71 | this.spotifyPlaylists.items[index].tracks_local.items.push(playlistTrack); 72 | } 73 | 74 | this.store.dispatch(new SetPlaylists(this.spotifyPlaylists)).subscribe(() => { 75 | if (playlistTracks.next) this.getSpotifyPlaylistTracksPaginate(index, playlistTracks.next); 76 | }); 77 | }, error => this.handleApiError(error), () => { }) 78 | } 79 | 80 | handleApiError(error: any) { 81 | if (error) { 82 | switch (error.status) { 83 | case 401: 84 | this.router.navigate(['/login']); 85 | break; 86 | default: 87 | this.router.navigate(['/login']); 88 | } 89 | } 90 | } 91 | 92 | playCurrentSong(index: number) { 93 | let tempSong = this.spotifyPlaylists.items[this.selectedPlaylistIndex].tracks_local.items[index]; 94 | this.store.dispatch(new SetTrackIndex(index)); 95 | this.store.dispatch(new SetPlayingSong(tempSong)); 96 | this.dataService.updateCurrentSong(tempSong); 97 | this.currentPlayingSpotifySong = tempSong; 98 | } 99 | 100 | changeCurrentSong(changeVal: number) { 101 | this.dataService.toggleNextSong(changeVal); 102 | } 103 | 104 | shouldDisplaySidebarSongs(index: number): boolean { 105 | return (index === this.selectedPlaylistIndex && this.currentSpotifyPlaylistSongs !== null); 106 | } 107 | } -------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.css: -------------------------------------------------------------------------------- 1 | mat-list { 2 | padding-top: 0!important; 3 | } 4 | 5 | .song-list { 6 | overflow: scroll; 7 | -webkit-overflow-scrolling: touch; 8 | max-height: 30em; 9 | } 10 | 11 | .playlist-container { 12 | overflow: scroll; 13 | -webkit-overflow-scrolling: touch; 14 | height: 100vh; 15 | } 16 | 17 | /************************************************************************ 18 | Css in this section is regarding UI ViewPorts for mobile width devices. 19 | ************************************************************************/ 20 | @media (max-width: 599px) { 21 | .logo { 22 | display: none; 23 | } 24 | } 25 | 26 | /************************************************************************ 27 | Css in this section is regarding UI ViewPorts for desktop width devices. 28 | ************************************************************************/ 29 | @media (min-width: 600px) { 30 | .logo { 31 | background-color: #424242; 32 | color: white; 33 | text-align: center; 34 | padding-bottom: 10px; 35 | } 36 | 37 | .logo-tip { 38 | background-color: rgba(255, 0, 0, 0.707); 39 | } 40 | 41 | .logo:hover { 42 | cursor: pointer; 43 | } 44 | } 45 | 46 | /******************************************************************************** 47 | Css in this section is regarding random stylistic elements for main body content. 48 | ********************************************************************************/ 49 | mat-list-item:hover { 50 | background-color: #68686862; 51 | } 52 | 53 | .mat-expanded { 54 | background-color: #4242428e; 55 | } -------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/video/video.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { DataService } from '../../../lib/service/data/data.service'; 3 | import { YoutubeService } from '../../../lib/service/youtube/youtube.service'; 4 | import { SpotifyPlaylistTrack, SpotifySong } from '../../../lib/service/spotify/spotify.model'; 5 | import { Store } from '@ngxs/store'; 6 | import { SetPlayingSong, SetSinglePlaylist } from '../../shared/spotify.state'; 7 | import { Router } from '@angular/router'; 8 | 9 | @Component({ 10 | moduleId: module.id, 11 | selector: 'video-player', 12 | templateUrl: 'video.html', 13 | styleUrls: ['video.css'] 14 | }) 15 | export class VideoComponent { 16 | currentSong: SpotifyPlaylistTrack; 17 | displayYoutubePlayer: boolean = false; 18 | private id: string = 'qDuKsiwS5xw'; 19 | player: YT.Player; 20 | isRandom: boolean = false; 21 | isRepeat: boolean = false; 22 | 23 | constructor(private dataService: DataService, private youtubeService: YoutubeService, private store: Store, private router: Router) { 24 | this.dataService.currentSongSubject.subscribe((song) => { 25 | this.currentSong = song; 26 | this.checkSongCache(song); 27 | }); 28 | 29 | this.dataService.playerStatusSubject.subscribe((status) => { 30 | status ? this.player.pauseVideo() : this.player.playVideo(); 31 | }); 32 | } 33 | 34 | /** 35 | * Used to build the iframe url to be pased into an iframe. All functionality since has been replaced by youtube-player lib. 36 | * 37 | * @param youtubeVideoId 38 | */ 39 | getSingleSongYoutubeVideoUrl(youtubeVideoId: string): string { 40 | return `https://www.youtube.com/embed/${youtubeVideoId}?autoplay=1`; 41 | } 42 | 43 | checkSongCache(song: SpotifyPlaylistTrack) { 44 | song.youtubeVideoId ? this.setVideoPlayer(song) : this.getYoutubeVideoForSong(song); 45 | } 46 | 47 | getYoutubeVideoForSong(song: SpotifyPlaylistTrack) { 48 | let tempSong: SpotifySong = new SpotifySong(song.track.artists[0].name, song.track.name); 49 | 50 | this.youtubeService.searchYoutube(tempSong).subscribe((response) => { 51 | song.youtubeVideoId = response.items[0].id.videoId; 52 | let state = this.store.snapshot().spotifydata; 53 | 54 | let tempSongs = state.currentSpotifyPlaylistSongs; 55 | let index = state.trackIndex; 56 | 57 | tempSongs.items[index].youtubeVideoId = song.youtubeVideoId; 58 | 59 | this.store.dispatch(new SetPlayingSong(song)); 60 | this.store.dispatch(new SetSinglePlaylist(tempSongs)); 61 | 62 | this.setVideoPlayer(song); 63 | }, (error) => this.handleApiError(error), () => { }); 64 | } 65 | 66 | /** 67 | * Handles setting the Youtube Player as a callback from the lib factory. 68 | * 69 | * @param player The YoutubePlayer singleton. 70 | */ 71 | savePlayer(player) { 72 | this.player = player; 73 | this.displayYoutubePlayer = true; 74 | this.player.playVideo(); 75 | } 76 | 77 | setVideoPlayer(song: SpotifyPlaylistTrack) { 78 | if (this.displayYoutubePlayer) { 79 | this.setVideoPlayerSong(song.youtubeVideoId); 80 | } else { 81 | this.id = song.youtubeVideoId; 82 | this.displayYoutubePlayer = true; 83 | } 84 | } 85 | 86 | setVideoPlayerSong(videoId: string) { 87 | this.id = videoId; 88 | this.player.loadVideoById(videoId); 89 | this.player.playVideo(); 90 | } 91 | 92 | onStateChange(event) { 93 | switch (event.data) { 94 | case 0: // Status: ended 95 | this.dataService.togglePlayPause('Play'); 96 | this.dataService.toggleNextSong(1); 97 | break; 98 | case 1: // Status: playing 99 | this.dataService.togglePlayPause('Pause'); 100 | break; 101 | case 2: // Status: paused 102 | this.dataService.togglePlayPause('Play'); 103 | break; 104 | case 3: // Status: buffering 105 | case 5: // Status: video cued 106 | default: 107 | 108 | } 109 | } 110 | 111 | handleApiError(error: any) { 112 | if (error) { 113 | switch (error.status) { 114 | case 401: 115 | this.router.navigate(['/login']); 116 | break; 117 | default: 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/app/components/video/video.css: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | Css in this section is regarding UI ViewPorts for mobile width devices. 3 | ************************************************************************/ 4 | @media (max-width: 599px) { 5 | .iframe-container { 6 | position: relative; 7 | width: 100%; 8 | height: 40vh; 9 | } 10 | } 11 | 12 | /************************************************************************ 13 | Css in this section is regarding UI ViewPorts for desktop width devices. 14 | ************************************************************************/ 15 | @media (min-width: 600px) { 16 | .iframe-container { 17 | position: relative; 18 | width: 100%; 19 | height: 80vh; 20 | } 21 | } 22 | 23 | .video-container { 24 | /* height: 100%; */ 25 | width: inherit; 26 | grid-area: "video"; 27 | text-align: center; 28 | margin: 10px; 29 | } 30 | 31 | .video-default-text { 32 | color: white; 33 | } -------------------------------------------------------------------------------- /src/app/components/video/video.html: -------------------------------------------------------------------------------- 1 |
2 |

Click on a video to begin

3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /src/app/config.development.json: -------------------------------------------------------------------------------- 1 | {"spotify": {"redirect_url": "https://immannino.github.io/SpotifyTelevision/login"},"youtube": {}} -------------------------------------------------------------------------------- /src/app/config.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "spotify": { 3 | "redirect_url": "https://immannino.github.io/SpotifyTelevision/login", 4 | "clientid": "" 5 | }, 6 | "youtube": { 7 | "apikey": "" 8 | } 9 | } -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'; 3 | 4 | import { AuthenticationService } from '../../lib/service/authentication/authentication.service'; 5 | import { AuthData } from '../../lib/service/authentication/authentication.model'; 6 | import { SpotifySong, SpotifyPlaylist, SpotifyUserProfile, UserSpotifyPlaylists, SpotifyPlaylistTracks, SpotifyPlaylistTrack, SimpleSpotifyTrack } from '../../lib/service/spotify/spotify.model'; 7 | import { DashboardPlaylist, PlaylistItem } from './dashboard.model'; 8 | import { SafeUrlPipe } from '../../lib/utils/safeurl.pipe'; 9 | 10 | import { Observable } from 'rxjs'; 11 | 12 | import { SpotifyService } from '../../lib/service/spotify/spotify.service'; 13 | import { Router, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; 14 | import { EventListener } from '@angular/core/src/debug/debug_node'; 15 | import { Store } from '@ngxs/store'; 16 | import { SetProfile, SetPlaylists, SetSinglePlaylist, SetPlayingSong } from '../shared/spotify.state'; 17 | import { DataService } from '../../lib/service/data/data.service'; 18 | 19 | @Component({ 20 | selector: 'dashboard', 21 | templateUrl: './dashboard.html', 22 | styleUrls: ['./dashboard.css'] 23 | }) 24 | export class DashboardComponent { 25 | constructor(private authService: AuthenticationService, private spotifyService: SpotifyService, private router: Router, private store: Store, private dataService: DataService) { 26 | this.router.events.subscribe(event => { 27 | if (event instanceof NavigationEnd) { 28 | if (!this.store.snapshot().spotifydata.spotifyPlaylists) { 29 | this.getUserProfileInformation(); 30 | } else { 31 | this.refreshLocalStateData(); 32 | } 33 | } 34 | }); 35 | } 36 | 37 | spotifyPlaylists: UserSpotifyPlaylists = null; 38 | userProfile: SpotifyUserProfile = null; 39 | 40 | ngOnInit() { 41 | let authData = this.store.snapshot().survey; 42 | 43 | if (!authData.userAccessToken) { 44 | this.router.navigate(['/login']); 45 | } 46 | } 47 | 48 | getUserProfileInformation() { 49 | this.spotifyService.getSpotifyUserProfile().subscribe((value: SpotifyUserProfile) => { 50 | this.userProfile = value; 51 | this.store.dispatch(new SetProfile(this.userProfile)).subscribe(() => { 52 | 53 | /** 54 | * Prep the playlist variables. 55 | */ 56 | this.spotifyPlaylists = new UserSpotifyPlaylists(); 57 | this.spotifyPlaylists.items = new Array(); 58 | 59 | this.getUserPlaylists(); 60 | }); 61 | }, error => this.handleApiError(error), () => { }); 62 | } 63 | 64 | /** 65 | * Initial request for user playlists. 66 | */ 67 | getUserPlaylists() { 68 | this.spotifyService.getUserPlaylists(this.userProfile.id).subscribe((playlistData: UserSpotifyPlaylists) => { 69 | this.spotifyPlaylists = playlistData; 70 | 71 | this.store.dispatch(new SetPlaylists(this.spotifyPlaylists)).subscribe(() => { 72 | this.dataService.updateUserPlaylists(this.spotifyPlaylists); 73 | 74 | if (playlistData.next) this.userPlaylistPaginate(playlistData.next); 75 | else this.getUserLibraryTracks(); 76 | }); 77 | }, (error) => this.handleApiError(error), () => { }); 78 | } 79 | 80 | /** 81 | * Recursively make pagination calls to the spotify playlist api 82 | * while the user still has playlists to snag. 83 | * 84 | * @param paginateUrl Url for the next set of playlists. 85 | */ 86 | userPlaylistPaginate(paginateUrl: string) { 87 | this.spotifyService.getUserPlaylistPaginate(paginateUrl).subscribe((playlistData: UserSpotifyPlaylists) => { 88 | for (let playlist of playlistData.items) { 89 | this.spotifyPlaylists.items.push(playlist); 90 | } 91 | 92 | this.store.dispatch(new SetPlaylists(this.spotifyPlaylists)).subscribe(() => { 93 | this.dataService.updateUserPlaylists(this.spotifyPlaylists); 94 | 95 | if (playlistData.next) this.userPlaylistPaginate(playlistData.next); 96 | else this.getUserLibraryTracks(); 97 | }); 98 | }, error => this.handleApiError(error), () => { }) 99 | } 100 | 101 | getUserLibraryTracks() { 102 | this.spotifyService.getUserLibrarySongs().subscribe((libraryTracks: SpotifyPlaylistTracks) => { 103 | let tempLocalPlaylists: SpotifyPlaylist = new SpotifyPlaylist(); 104 | tempLocalPlaylists.name = "User Library Songs"; 105 | tempLocalPlaylists.tracks_local = libraryTracks; 106 | 107 | this.spotifyPlaylists.items.unshift(tempLocalPlaylists); 108 | this.store.dispatch(new SetPlaylists(this.spotifyPlaylists)).subscribe(() => { 109 | this.dataService.updateUserPlaylists(this.spotifyPlaylists); 110 | if (libraryTracks.next) this.getUserLibraryTracksPaginate(libraryTracks.next); 111 | }); 112 | }, (error) => this.handleApiError(error), () => { }); 113 | } 114 | 115 | getUserLibraryTracksPaginate(paginateUrl: string) { 116 | this.spotifyService.getUserLibrarySongsPaginate(paginateUrl).subscribe((libraryTracks: SpotifyPlaylistTracks) => { 117 | for (let libraryTrack of libraryTracks.items) { 118 | this.spotifyPlaylists.items[0].tracks_local.items.push(libraryTrack); 119 | } 120 | 121 | this.store.dispatch(new SetPlaylists(this.spotifyPlaylists)).subscribe(() => { 122 | this.dataService.updateUserPlaylists(this.spotifyPlaylists); 123 | if (libraryTracks.next) this.getUserLibraryTracksPaginate(libraryTracks.next); 124 | }); 125 | }, (error) => this.handleApiError(error), () => { }) 126 | } 127 | 128 | handleApiError(error: any) { 129 | if (error) { 130 | switch (error.status) { 131 | case 401: 132 | this.router.navigate(['/login']); 133 | break; 134 | default: 135 | } 136 | } 137 | } 138 | 139 | refreshLocalStateData() { 140 | let data = this.store.snapshot().spotifydata; 141 | 142 | this.userProfile = data.userProfile; 143 | this.spotifyPlaylists = data.spotifyPlaylists; 144 | } 145 | 146 | /** 147 | * Cleans up user app cache. 148 | */ 149 | ngOnDestroy() { 150 | localStorage.clear(); 151 | localStorage.setItem("auth_error", "true"); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.css: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | Css in this section is regarding UI ViewPorts for mobile width devices. 3 | ************************************************************************/ 4 | @media (max-width: 599px) { 5 | .container { 6 | grid-template-columns: 1; 7 | grid-template-rows: 1fr 1fr; 8 | background-color: #464646; 9 | background: transparent; 10 | } 11 | } 12 | 13 | /************************************************************************ 14 | Css in this section is regarding UI ViewPorts for desktop width devices. 15 | ************************************************************************/ 16 | @media (min-width: 600px) { 17 | .container { 18 | display: grid; 19 | grid-template-rows: 100vh; 20 | grid-template-columns: 1fr 250px; 21 | background: transparent; 22 | font-size: 12px; 23 | overflow: hidden; 24 | } 25 | } 26 | 27 | .transparent-gradient { 28 | z-index: -4; 29 | width: 100%; 30 | height: 100%; 31 | position: absolute; 32 | background-image: linear-gradient(transparent,#000); 33 | } 34 | 35 | .color-gradient { 36 | z-index: -5; 37 | width: 100%; 38 | height: 100%; 39 | position: absolute; 40 | background: linear-gradient(90deg, #77C9D4, #57DC90); 41 | } -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | 8 |
9 | 10 |
-------------------------------------------------------------------------------- /src/app/dashboard/dashboard.model.ts: -------------------------------------------------------------------------------- 1 | import { YoutubeThumbnail } from "../../lib/service/youtube/youtube.model"; 2 | 3 | export class DashboardPlaylist { 4 | playlistItem: Array; 5 | } 6 | 7 | export class PlaylistItem { 8 | artist: string; 9 | title: string; 10 | youtubeVideoId: string; 11 | youtubeVideoTitle: string; 12 | thumbnails: Map; 13 | } -------------------------------------------------------------------------------- /src/app/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "development" 3 | } -------------------------------------------------------------------------------- /src/app/fourOhFour.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: '

Wow there bucko, that page aint be existin.

' 5 | }) 6 | export class FourOhFourComponent {} -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AppMaterialsModule } from '../app-materials.module'; 4 | import { SpotifyService } from '../../lib/service/spotify/spotify.service'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { Router, RouterModule, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; 7 | import { SafeResourceUrl, DomSanitizer} from '@angular/platform-browser'; 8 | import { SafeUrlPipe } from '../../lib/utils/safeurl.pipe'; 9 | import { UserData } from '../../lib/service/spotify/spotify.model'; 10 | import { AppConfig } from '../app.config'; 11 | import { Timestamp } from 'rxjs'; 12 | import { Store } from '@ngxs/store'; 13 | import { SetAuth } from '../shared/auth.state'; 14 | 15 | @Component({ 16 | moduleId: module.id, 17 | selector: 'login', 18 | templateUrl: 'login.html', 19 | styleUrls: [ 'login.css' ] 20 | }) 21 | export class LoginComponent implements OnInit { 22 | constructor( 23 | private config: AppConfig, 24 | private spotifyService: SpotifyService, 25 | private router: Router, 26 | private sanitizer: DomSanitizer, 27 | private store: Store 28 | ) { 29 | this.router.events.subscribe(event => { 30 | if (event instanceof NavigationEnd ) { 31 | let data = event.urlAfterRedirects.split("#")[1]; 32 | 33 | if (data && data !== '' && !data.includes("error")) { 34 | let responseItems: string[] = data.split("&"); 35 | /** 36 | * In the future, add logic to check that client state key is valid from response; 37 | */ 38 | this.userSpotifyLogin(responseItems); 39 | } else { 40 | let currentToken: string = localStorage.getItem('auth_error'); 41 | 42 | if (currentToken && currentToken === "true") { 43 | this.errorMessagePrimaryText = "Spotify session token has expired." 44 | this.errorMessageSubText = "Please log back in."; 45 | this.hasErrorOccurred = true; 46 | } 47 | } 48 | } 49 | }); 50 | } 51 | 52 | title = 'Spotify Television'; 53 | errorMessagePrimaryText: string = ""; 54 | errorMessageSubText: string = ""; 55 | hasErrorOccurred: boolean = false; 56 | hrefUrl: SafeResourceUrl = ""; 57 | client_id: string; 58 | 59 | ngOnInit() { 60 | this.client_id = this.config.getConfig('spotify').clientid; 61 | this.generateSpotifyLoginUrl(); 62 | } 63 | 64 | userSpotifyLogin(responseItems: string[]) { 65 | let tempUserData: UserData = { 66 | userAccessToken: responseItems[0].split("=")[1], 67 | token_type: responseItems[1].split("=")[1], 68 | refreshTokenTimeout: Number(responseItems[2].split("=")[1]), 69 | state: responseItems[3].split("=")[1] 70 | }; 71 | 72 | this.store.dispatch(new SetAuth(tempUserData)).subscribe(() => { 73 | 74 | }); 75 | 76 | if (this.hasErrorOccurred) this.hasErrorOccurred = false; 77 | 78 | this.navigateToDashboard(); 79 | } 80 | 81 | generateSpotifyLoginUrl() { 82 | let clientStateKey = this.spotifyService.generateRandomString(50); 83 | // let appRedirectUrl: string = "http://localhost:4200/login"; 84 | // let appRedirectUrl: string = "http://10.0.0.101:4200/login"; 85 | let appRedirectUrl: string = this.config.getConfig('spotify').redirect_url; 86 | 87 | /** 88 | * Update scopes to appropriate values for what information I'm requesting from the user. 89 | */ 90 | let scopes: string = 'user-read-private user-read-email playlist-read-private playlist-read-collaborative user-library-read'; 91 | 92 | let url = 'https://accounts.spotify.com/authorize' + 93 | '?client_id=' + this.client_id + 94 | '&redirect_uri=' + encodeURIComponent(appRedirectUrl) + 95 | (scopes ? '&scope=' + encodeURIComponent(scopes) : '') + 96 | '&response_type=token' + '&state=' + clientStateKey; 97 | 98 | this.hrefUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); 99 | } 100 | 101 | authUser() { 102 | this.navigateToDashboard(); 103 | } 104 | 105 | navigateToDashboard() { 106 | this.router.navigate(['/dashboard']); 107 | } 108 | } -------------------------------------------------------------------------------- /src/app/login/login.css: -------------------------------------------------------------------------------- 1 | .button-container { 2 | text-align: center; 3 | color: white; 4 | background: linear-gradient(to bottom left, #77C9D4, #57DC90); 5 | box-shadow: 0 0 5px inset gray; 6 | padding-bottom: 0.5em; 7 | } 8 | 9 | .title { 10 | font-size: 1.5rem; 11 | padding: 0.5em 0; 12 | } 13 | 14 | .login-button { 15 | margin: 1em; 16 | height: 4em; 17 | width: 15em; 18 | border: none; 19 | color: white; 20 | padding-top: 10px; 21 | } 22 | 23 | #spotify { 24 | color: #1db954; 25 | /* background-color: #1db954; */ 26 | background-color: white; 27 | } 28 | 29 | #spotify:hover { 30 | color: white; 31 | background-color: #1ed760; 32 | } 33 | 34 | #continue { 35 | color: #676767; 36 | /* background-color: #676767; */ 37 | background-color: white; 38 | } 39 | 40 | #continue:hover { 41 | color: white; 42 | background-color: #1ed760; 43 | } 44 | 45 | .disabled:hover { 46 | cursor: not-allowed !important; 47 | } 48 | 49 | .error-info { 50 | margin: auto; 51 | text-align: center; 52 | width: 40%; 53 | background: red; 54 | word-wrap: break-word; 55 | color: white; 56 | margin-bottom: 8px; 57 | } 58 | 59 | .container { 60 | text-align: center; 61 | height: 100%; 62 | } 63 | 64 | h1 { 65 | width: 100%; 66 | margin:0; 67 | padding-bottom: 5px; 68 | } 69 | 70 | .container:hover { 71 | cursor: pointer; 72 | } -------------------------------------------------------------------------------- /src/app/login/login.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ title }}

3 |
4 | {{errorMessagePrimaryText}}
{{errorMessageSubText}}
5 |
6 |
Login
7 |
Choose to login using Spotify or Sign Up for a Spotify account.
8 | 9 |
10 | 11 |
-------------------------------------------------------------------------------- /src/app/shared/auth.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, Selector } from '@ngxs/store'; 2 | import { UserData } from '../../lib/service/spotify/spotify.model'; 3 | 4 | export class SetAuth { 5 | static readonly type = '[Auth] Set Auth'; 6 | constructor(public authData: UserData) {} 7 | } 8 | 9 | @State({ 10 | name: "survey", 11 | defaults: { 12 | authData: { 13 | 14 | } 15 | // spotifyAuthState: { 16 | // userAccessToken: null, 17 | // tokenTimeout: null, 18 | // tokenType: null, 19 | // state: null 20 | // } 21 | } 22 | }) 23 | export class SpotifyAuthState { 24 | @Selector() static _toggleDrinks(state: any) { 25 | return state.toggleDrinks; 26 | } 27 | 28 | @Action(SetAuth) 29 | setAuth(ctx: StateContext, action: SetAuth) { 30 | const localState = ctx.getState(); 31 | 32 | ctx.patchState({ 33 | ...localState, 34 | userAccessToken: action.authData.userAccessToken, 35 | token_type: action.authData.token_type, 36 | refreshTokenTimeout: action.authData.refreshTokenTimeout, 37 | state: action.authData.state 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /src/app/shared/spotify.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, Selector } from '@ngxs/store'; 2 | import { UserData, SpotifyUserProfile, SimpleSpotifyTrack, SpotifyPlaylistTracks, UserSpotifyPlaylists, SpotifyPlaylistTrack } from '../../lib/service/spotify/spotify.model'; 3 | 4 | export class SetPlaylists { 5 | static readonly type = '[Auth] Set Playlists'; 6 | constructor(public playlists: UserSpotifyPlaylists) {} 7 | } 8 | export class SetSinglePlaylist { 9 | static readonly type = '[Auth] Set Playlist'; 10 | constructor(public playlist: SpotifyPlaylistTracks) {} 11 | } 12 | export class SetPlayingSong { 13 | static readonly type = '[Auth] Set Song'; 14 | constructor(public song: SpotifyPlaylistTrack) {} 15 | } 16 | export class SetProfile { 17 | static readonly type = '[Auth] Set Profile'; 18 | constructor(public userProfile: SpotifyUserProfile) {} 19 | } 20 | export class SetTrackIndex { 21 | static readonly type = '[Auth] Set Track Index'; 22 | constructor(public trackIndex: number) {} 23 | } 24 | export class SetRepeat { 25 | static readonly type = '[Auth] Set Repeat Flag'; 26 | constructor(public shouldRepeatSongs: boolean) {} 27 | } 28 | export class SetShuffle { 29 | static readonly type = '[Auth] Set Shuffle Flag'; 30 | constructor(public shouldShuffleSongs: boolean) {} 31 | } 32 | export class SetPlayerStatus { 33 | static readonly type = '[Auth] Set Player Status'; 34 | constructor(public playerStatus: boolean) {} 35 | } 36 | 37 | export class SpotifyDataStateModel { 38 | spotifyPlaylists: UserSpotifyPlaylists; 39 | currentSpotifyPlaylistSongs: SpotifyPlaylistTracks; 40 | currentPlayingSpotifySong: SpotifyPlaylistTrack; 41 | userProfile: SpotifyUserProfile; 42 | trackIndex: number; 43 | shouldRepeatSongs: boolean; 44 | shouldShuffleSongs: boolean; 45 | playerStatus: boolean; 46 | } 47 | 48 | @State({ 49 | name: "spotifydata", 50 | defaults: { 51 | spotifyPlaylists: null, 52 | currentSpotifyPlaylistSongs: null, 53 | currentPlayingSpotifySong: null, 54 | userProfile: null, 55 | trackIndex: null, 56 | shouldRepeatSongs: null, 57 | shouldShuffleSongs: null, 58 | playerStatus: null 59 | } 60 | }) 61 | export class SpotifyDataState { 62 | @Selector() static _spotifyPlaylists(state: any) { 63 | return state.spotifyPlaylists; 64 | } 65 | @Selector() static _currentSpotifyPlaylistSongs(state: any) { 66 | return state.currentSpotifyPlaylistSongs; 67 | } 68 | @Selector() static _currentPlayingSpotifySong(state: any) { 69 | return state.currentPlayingSpotifySong; 70 | } 71 | @Selector() static _userProfile(state: any) { 72 | return state.userProfile; 73 | } 74 | 75 | @Action(SetTrackIndex) 76 | setTrackIndex(ctx: StateContext, action: SetTrackIndex) { 77 | const localState = ctx.getState(); 78 | 79 | ctx.patchState({ 80 | ...localState, 81 | trackIndex: action.trackIndex 82 | }); 83 | } 84 | 85 | @Action(SetProfile) 86 | setProfile(ctx: StateContext, action: SetProfile) { 87 | const localState = ctx.getState(); 88 | 89 | ctx.patchState({ 90 | ...localState, 91 | userProfile: action.userProfile 92 | }); 93 | } 94 | 95 | @Action(SetPlaylists) 96 | setPlaylists(ctx: StateContext, action: SetPlaylists) { 97 | const localState = ctx.getState(); 98 | 99 | ctx.patchState({ 100 | ...localState, 101 | spotifyPlaylists: action.playlists 102 | }); 103 | } 104 | 105 | @Action(SetSinglePlaylist) 106 | setSinglePlaylist(ctx: StateContext, action: SetSinglePlaylist) { 107 | const localState = ctx.getState(); 108 | 109 | ctx.patchState({ 110 | ...localState, 111 | currentSpotifyPlaylistSongs: action.playlist 112 | }); 113 | } 114 | 115 | @Action(SetPlayingSong) 116 | setSong(ctx: StateContext, action: SetPlayingSong) { 117 | const localState = ctx.getState(); 118 | 119 | ctx.patchState({ 120 | ...localState, 121 | currentPlayingSpotifySong: action.song 122 | }); 123 | } 124 | 125 | @Action(SetRepeat) 126 | setRepeat(ctx: StateContext, action: SetRepeat) { 127 | const localState = ctx.getState(); 128 | 129 | ctx.patchState({ 130 | ...localState, 131 | shouldRepeatSongs: action.shouldRepeatSongs 132 | }); 133 | } 134 | 135 | @Action(SetShuffle) 136 | setShuffle(ctx: StateContext, action: SetShuffle) { 137 | const localState = ctx.getState(); 138 | 139 | ctx.patchState({ 140 | ...localState, 141 | shouldShuffleSongs: action.shouldShuffleSongs 142 | }); 143 | } 144 | 145 | @Action(SetPlayerStatus) 146 | setPlayerStatus(ctx: StateContext, action: SetPlayerStatus) { 147 | const localState = ctx.getState(); 148 | 149 | ctx.patchState({ 150 | ...localState, 151 | playerStatus: action.playerStatus 152 | }) 153 | } 154 | } -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/forward-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/forward-96.png -------------------------------------------------------------------------------- /src/assets/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/logo-128.png -------------------------------------------------------------------------------- /src/assets/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/logo-152.png -------------------------------------------------------------------------------- /src/assets/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/logo-256.png -------------------------------------------------------------------------------- /src/assets/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/logo-512.png -------------------------------------------------------------------------------- /src/assets/repeat-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/repeat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/assets/rewind-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/assets/rewind-96.png -------------------------------------------------------------------------------- /src/assets/shuffle-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/assets/shuffle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 22 | 26 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/assets/spotify-mtv-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immannino/SpotifyTelevision/263ed66932136b5edf0f79ff7e6ef082ab1c68c5/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | SpotifyTV 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/service/authentication/authentication.model.ts: -------------------------------------------------------------------------------- 1 | export class AuthData { 2 | clientId: string; 3 | isAuthed: boolean; 4 | 5 | constructor(clientId: string = "") { 6 | this.clientId = clientId; 7 | } 8 | } -------------------------------------------------------------------------------- /src/lib/service/authentication/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject , Observable } from 'rxjs'; 3 | 4 | import { AuthData } from './authentication.model'; 5 | 6 | @Injectable() 7 | export class AuthenticationService { 8 | 9 | // Observable AuthData sources 10 | private authenticationAnnouncedSource = new Subject(); 11 | private localAuthConfig: AuthData = null; 12 | 13 | // Observable AuthData streams 14 | authenticationAnnounced$ = this.authenticationAnnouncedSource.asObservable(); 15 | 16 | authorizeUser(clientId: string) { 17 | let tempData = new AuthData(clientId); 18 | this.localAuthConfig = tempData; 19 | this.authenticationAnnouncedSource.next(tempData); 20 | } 21 | 22 | getAuthData(): AuthData { 23 | return this.localAuthConfig; 24 | } 25 | } -------------------------------------------------------------------------------- /src/lib/service/data/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { UserSpotifyPlaylists, SpotifyPlaylist, SpotifySong, SpotifyPlaylistTrack, SpotifyPlaylistTracks } from "../spotify/spotify.model"; 3 | import { Observable, Subject } from "rxjs"; 4 | import { Store } from "@ngxs/store"; 5 | import { SetPlaylists, SetPlayingSong, SetSinglePlaylist, SetTrackIndex, SetPlayerStatus } from "../../../app/shared/spotify.state"; 6 | 7 | @Injectable() 8 | export class DataService { 9 | public userPlaylistsSubject: Subject = new Subject(); 10 | public playlistSubject: Subject = new Subject(); 11 | public currentSongSubject: Subject = new Subject(); 12 | public playerStatusSubject: Subject = new Subject(); 13 | public playPauseSubject: Subject = new Subject(); 14 | 15 | constructor(private store: Store) { 16 | } 17 | 18 | isValidChange(change: number, index: number): boolean { 19 | //implement this logic from Dashboard Component 20 | return true; 21 | } 22 | 23 | /** 24 | * Refactor and put in Data Service 25 | * 26 | * Used by the previous and next buttons to change what song to play. 27 | * 28 | * @param changeValue 1 or -1 29 | */ 30 | getNewSong(changeValue: number): number { 31 | let state = this.store.snapshot().spotifydata; 32 | 33 | // If shuffling, then shuffle. 34 | if (state.shouldShuffleSongs) { 35 | return Math.round(Math.random() * (state.currentSpotifyPlaylistSongs.items.length - 1)); 36 | } else { 37 | 38 | // Check if we're in legal bounds of the playlist 39 | if ((state.trackIndex + changeValue) >= 0 && (state.trackIndex <= state.currentSpotifyPlaylistSongs.items.length - 1)) { 40 | 41 | // if last song in playlist, check if we're going to cycle back to the beginning 42 | if (((state.trackIndex === state.currentSpotifyPlaylistSongs.items.length - 1) && changeValue == 1 && (state.shouldRepeatSongs))) { 43 | return 0; 44 | } else { 45 | 46 | // Just play the next song in the playlist 47 | return state.trackIndex + changeValue; 48 | } 49 | } else { 50 | // If we're on the first song, and we're going to repeat songs. 51 | // Then cycle to the last song in the playlist 52 | if (state.shouldRepeatSongs && state.trackIndex == 0) { 53 | return state.currentSpotifyPlaylistSongs.items.length - 1; 54 | } 55 | } 56 | } 57 | 58 | return -1; 59 | } 60 | 61 | toggleNextSong(change: number) { 62 | let newIndex: number = this.getNewSong(change); 63 | 64 | if (newIndex > -1) { 65 | let newSong: SpotifyPlaylistTrack = this.store.snapshot().spotifydata.currentSpotifyPlaylistSongs.items[newIndex]; 66 | 67 | // Save State 68 | this.store.dispatch(new SetPlayingSong(newSong)); 69 | this.store.dispatch(new SetTrackIndex(newIndex)); 70 | 71 | // Trigger event to let video component know song was updated 72 | this.updateCurrentSong(newSong); 73 | } 74 | } 75 | 76 | togglePlayPause(change: string) { 77 | this.playPauseSubject.next(change); 78 | } 79 | 80 | updatePlayerStatus(change: boolean) { 81 | this.store.dispatch(new SetPlayerStatus(change)); 82 | 83 | this.playerStatusSubject.next(change); 84 | } 85 | 86 | updateUserPlaylists(playlists: UserSpotifyPlaylists) { 87 | this.store.dispatch(new SetPlaylists(playlists)); 88 | 89 | this.userPlaylistsSubject.next(playlists); 90 | } 91 | 92 | updateCurrentPlaylist(playlist: SpotifyPlaylistTracks) { 93 | this.store.dispatch(new SetSinglePlaylist(playlist)); 94 | 95 | this.playlistSubject.next(playlist); 96 | } 97 | 98 | updateCurrentSong(song: SpotifyPlaylistTrack) { 99 | this.store.dispatch(new SetPlayingSong(song)); 100 | 101 | this.currentSongSubject.next(song); 102 | } 103 | } -------------------------------------------------------------------------------- /src/lib/service/spotify/spotify.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Depricated: Was used for initial testing. 3 | * 4 | * Replaced by SpotifyTrack Object (simplified) 5 | */ 6 | export class SpotifySong { 7 | artist: string; 8 | song: string; 9 | 10 | constructor(artist: string = "", song: string = "") { 11 | this.artist = artist; 12 | this.song = song; 13 | } 14 | } 15 | 16 | /** 17 | * UserData for Spotify User Auth data. 18 | */ 19 | export class UserData { 20 | userAccessToken: string; 21 | refreshTokenTimeout: number; 22 | state: string; 23 | token_type: string; 24 | 25 | constructor(token: string = "", timeout: number = 0, token_type: string = "", state: string = "") { 26 | this.userAccessToken = token; 27 | this.refreshTokenTimeout = timeout; 28 | this.state = state; 29 | this.token_type = token_type; 30 | } 31 | } 32 | 33 | /** 34 | * Object representation of users Spotify Playlists 35 | */ 36 | export class UserSpotifyPlaylists { 37 | href: string; 38 | items: SpotifyPlaylist[]; 39 | limit: number; 40 | next: string; 41 | offset: number; 42 | previous: string; 43 | total: number; 44 | } 45 | 46 | /** 47 | * Object representation of a single Spotify Playlist 48 | */ 49 | export class SpotifyPlaylist { 50 | collaborative: boolean; 51 | externalUrls: string; 52 | href: string; 53 | id: string; 54 | images: string[]; 55 | name: string; 56 | owner: SpotifyPlaylistOwner; 57 | public: boolean; 58 | snapshot_id: string; 59 | tracks: SpotifyTrackReference; 60 | tracks_local: SpotifyPlaylistTracks; 61 | type: string; 62 | uri: string; 63 | } 64 | 65 | /** 66 | * Owner object. Straightforward. 67 | */ 68 | export class SpotifyPlaylistOwner { 69 | external_urls: SpotifyExternalUrl; 70 | href: string; 71 | id: string; 72 | type: string; 73 | uri: string; 74 | } 75 | 76 | /** 77 | * Spotify track reference data. 78 | */ 79 | export class SpotifyTrackReference { 80 | href: string; 81 | total: number; 82 | } 83 | 84 | /** 85 | * Straightforward. 86 | * 87 | * Honestly just to make marshalling 100%. 88 | */ 89 | export class SpotifyExternalUrl { 90 | spotify: string; 91 | } 92 | 93 | export class SpotifyPlaylistTracks { 94 | href: string; 95 | items: SpotifyPlaylistTrack[]; 96 | limit: number; 97 | next: string; 98 | offset: number; 99 | previous: string; 100 | total: number; 101 | } 102 | 103 | export class SpotifyPlaylistTrack { 104 | added_at: string; 105 | added_by: any; 106 | is_local: boolean; 107 | track: SimpleSpotifyTrack; 108 | youtubeVideoId: string; 109 | } 110 | 111 | export class SimpleSpotifyTrack { 112 | artists: SimpleSpotifyArtist[]; 113 | href: string; 114 | id: string; 115 | name: string; 116 | } 117 | 118 | export class SimpleSpotifyArtist { 119 | external_urls: SpotifyExternalUrl; 120 | href: string; 121 | id: string; 122 | name: string; 123 | type: string; 124 | uri: string; 125 | } 126 | 127 | export class SpotifyUserProfile { 128 | country: string; 129 | display_name: string; 130 | email: string; 131 | external_urls: SpotifyExternalUrl; 132 | followers: SpotifyFollowers; 133 | href: string; 134 | id: string; 135 | images: SpotifyUserImage[]; 136 | product: string; 137 | type: string; 138 | uri: string; 139 | } 140 | 141 | export class SpotifyFollowers { 142 | href: string; 143 | total: number; 144 | } 145 | 146 | export class SpotifyUserImage { 147 | height: number; 148 | url: string; 149 | width: number; 150 | } -------------------------------------------------------------------------------- /src/lib/service/spotify/spotify.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { SpotifyService } from './spotify.service'; 4 | 5 | describe('SpotifyService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [SpotifyService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([SpotifyService], (service: SpotifyService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/service/spotify/spotify.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpRequest } from '@angular/common/http'; 3 | import { Headers, RequestOptions } from '@angular/http' 4 | import { HttpErrorResponse } from '@angular/common/http'; 5 | import { Observable } from 'rxjs'; 6 | import { catchError } from 'rxjs/operators'; 7 | 8 | import { AppConfig } from '../../../app/app.config'; 9 | import { UserData, UserSpotifyPlaylists, SpotifyPlaylistTracks, SpotifyUserProfile } from './spotify.model'; 10 | import { Store } from '@ngxs/store'; 11 | 12 | @Injectable() 13 | export class SpotifyService { 14 | 15 | constructor(private config: AppConfig, private http: HttpClient, private store: Store) { } 16 | 17 | spotifyApiUrl: string = "https://api.spotify.com/v1"; 18 | currentOffset: number = 0; 19 | 20 | /** 21 | * 22 | * Just want it working, I'll get the proper design out there soon enough. 23 | * Currently getting race condition in app. 24 | */ 25 | getSpotifyUserProfile() { 26 | let headers: HttpHeaders = this.generateRequestOptions(); 27 | 28 | return this.http.get(`${this.spotifyApiUrl}/me`, { headers }); 29 | } 30 | /** 31 | * Get User Playlists: 32 | * 33 | * endpoint: /users/{user_id}/playlists 34 | */ 35 | getUserPlaylists(user_id: string) { 36 | let headers: HttpHeaders = this.generateRequestOptions(); 37 | // return this.http.get('../../../assets/user-playlists.json').map(response => response.json()); 38 | 39 | return this.http.get(`${this.spotifyApiUrl}/users/${user_id}/playlists?limit=50`, { headers }); 40 | } 41 | 42 | getUserPlaylistPaginate(url: string) { 43 | let headers: HttpHeaders = this.generateRequestOptions(); 44 | 45 | return this.http.get(url, { headers }); 46 | } 47 | 48 | /** 49 | * Get Playlist Tracks 50 | * 51 | * endpoint: /users/{user_id}/playlists/{playlist_id}/tracks 52 | */ 53 | getUserPlaylistTracks(playlistId: string, user_id: string) { 54 | let headers: HttpHeaders = this.generateRequestOptions(); 55 | return this.http.get(`${this.spotifyApiUrl}/users/${user_id}/playlists/${playlistId}/tracks`, { headers }); 56 | } 57 | 58 | getUserPlaylistTracksPaginate(url: string) { 59 | let headers: HttpHeaders = this.generateRequestOptions(); 60 | 61 | return this.http.get(url, { headers }); 62 | } 63 | 64 | getUserLibrarySongs() { 65 | let headers: HttpHeaders = this.generateRequestOptions(); 66 | return this.http.get(`${this.spotifyApiUrl}/me/tracks?limit=50`, { headers }); 67 | } 68 | 69 | getUserLibrarySongsPaginate(url: string) { 70 | let headers: HttpHeaders = this.generateRequestOptions(); 71 | 72 | return this.http.get(url, { headers }); 73 | } 74 | 75 | private generateRequestOptions() { 76 | let clientId = this.store.snapshot().survey.userAccessToken; 77 | 78 | let requestHeaders = new HttpHeaders({ 79 | 'Content-Type': 'application/json', 80 | 'Authorization': `Bearer ${clientId}` 81 | }); 82 | 83 | return requestHeaders; 84 | } 85 | 86 | generateRandomString(length: number): string { 87 | var text = ''; 88 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 89 | 90 | for (var i = 0; i < length; i++) { 91 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 92 | } 93 | 94 | return text; 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/service/youtube/youtube.model.ts: -------------------------------------------------------------------------------- 1 | export class YoutubeSearch { 2 | kind: string; 3 | etag: string; 4 | items: YoutubeSearch; 5 | id: YoutubeId; 6 | snippet: YoutubeSnippet; 7 | channelTitle: string; 8 | liveBroadcastContent: string; 9 | } 10 | 11 | export class YoutubeId { 12 | kind: string; 13 | videoId: string; 14 | channelId: string; 15 | playlistId: string; 16 | } 17 | 18 | export class YoutubeSnippet { 19 | publishedAt: Date; 20 | channelId: string; 21 | title: string; 22 | description: string; 23 | thumbnails: Map; 24 | } 25 | 26 | export class YoutubeThumbnail { 27 | url: string; 28 | width: number; 29 | height: number; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/lib/service/youtube/youtube.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { YoutubeService } from './youtube.service'; 4 | 5 | describe('SpotifyService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [YoutubeService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([YoutubeService], (service: YoutubeService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/service/youtube/youtube.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModule } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { YoutubeSearch } from './youtube.model'; 4 | import { AppConfig } from '../../../app/app.config'; 5 | import { SpotifySong } from '../spotify/spotify.model'; 6 | import { Observable } from 'rxjs'; 7 | 8 | 9 | @Injectable() 10 | export class YoutubeService { 11 | 12 | constructor(private config: AppConfig, private http: HttpClient) { } 13 | 14 | googleApiUrl: string = "https://www.googleapis.com/youtube/v3/search?"; 15 | apiKeys: string = this.config.getConfig('youtube').data; 16 | 17 | // Start each client off on a random count, 18 | // so that all users don't always use the first API Key 19 | requestCount: number = Math.round(Math.random() * (this.apiKeys.length - 1)); 20 | 21 | searchYoutube(song: SpotifySong): Observable { 22 | const apiKey = this.getApiKey(); 23 | let url = this.googleApiUrl + "q=" + song.artist + ' ' + song.song + ' Official Video'+ '&maxResults=1&part=snippet&key=' + apiKey; 24 | return this.http.get(encodeURI(url)); 25 | } 26 | 27 | // Round robin through the keys in order 28 | getApiKey() { 29 | const key = this.apiKeys[this.requestCount % this.apiKeys.length]; 30 | this.requestCount += 1; 31 | return key; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/utils/safeurl.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | import { SafeResourceUrl, DomSanitizer} from '@angular/platform-browser'; 3 | 4 | @Pipe({ 5 | name: 'sanitizeUrl', 6 | pure: false 7 | }) 8 | 9 | export class SafeUrlPipe implements PipeTransform { 10 | constructor(private domSanitizer: DomSanitizer) { 11 | } 12 | 13 | transform(value: string, args?: any): SafeResourceUrl { 14 | return this.domSanitizer.bypassSecurityTrustResourceUrl(value); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spotify Television", 3 | "short_name": "SpotifyTV", 4 | "description": "Watch music videos for the songs in your Spotify playlists", 5 | "icons": [{ 6 | "src": "assets/logo-128.png", 7 | "sizes": "128x128", 8 | "type": "image/png" 9 | }, { 10 | "src": "assets/logo-152.png", 11 | "sizes": "152x152", 12 | "type": "image/png" 13 | }, { 14 | "src": "assets/logo-256.png", 15 | "sizes": "256x256", 16 | "type": "image/png" 17 | }, { 18 | "src": "assets/logo-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }], 22 | "start_url": "index.html", 23 | "display": "standalone", 24 | "background_color": "#ffffff", 25 | "theme_color": "#23CF5F" 26 | } -------------------------------------------------------------------------------- /src/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [{ 4 | "name": "app", 5 | "installMode": "prefetch", 6 | "resources": { 7 | "files": [ 8 | "/favicon.ico", 9 | "/index.html" 10 | ], 11 | "versionedFiles": [ 12 | "/*.bundle.css", 13 | "/*.bundle.js", 14 | "/*.chunk.js" 15 | ] 16 | } 17 | }, { 18 | "name": "assets", 19 | "installMode": "lazy", 20 | "updateMode": "prefetch", 21 | "resources": { 22 | "files": [ 23 | "/assets/**" 24 | ] 25 | } 26 | }] 27 | } -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/prebuilt-themes/purple-green.css'; 3 | 4 | html { 5 | height: 100%; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto:300', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 7 | /* background-image: url("http://s3-us-west-2.amazonaws.com/designfacts/facts/df_0067.jpg"); */ 8 | /* background-repeat: repeat; */ 9 | } 10 | 11 | body { 12 | margin: 0; 13 | min-height: 100%; 14 | } 15 | /* 16 | Following spinner css courtesy of 0xD34F on codepen. <3 17 | */ 18 | .spinner { 19 | font-size: 5px; 20 | width: 18em; 21 | height: 14em; 22 | border: 2em solid black; 23 | border-left-color: black; 24 | box-sizing: border-box; 25 | position: relative; 26 | } 27 | .spinner::before { 28 | content: ""; 29 | width: 2em; 30 | height: 2em; 31 | position: absolute; 32 | left: 6em; 33 | top: 8em; 34 | background-color: black; 35 | border-radius: 50%; 36 | animation: spinner 2s infinite linear; 37 | } 38 | @keyframes spinner { 39 | 0%, 100% { 40 | transform: translate(0em, 0em); 41 | } 42 | 12% { 43 | transform: translate(6em, -6em); 44 | } 45 | 16% { 46 | transform: translate(4em, -8em); 47 | } 48 | 34% { 49 | transform: translate(-4em, 0em); 50 | } 51 | 38% { 52 | transform: translate(-6em, -2em); 53 | } 54 | 50% { 55 | transform: translate(0em, -8em); 56 | } 57 | 62% { 58 | transform: translate(6em, -2em); 59 | } 60 | 66% { 61 | transform: translate(4em, 0em); 62 | } 63 | 84% { 64 | transform: translate(-4em, -8em); 65 | } 66 | 88% { 67 | transform: translate(-6em, -6em); 68 | } 69 | } 70 | 71 | .spinner { 72 | position: fixed; 73 | top: 50%; 74 | left: 50%; 75 | /* bring your own prefixes */ 76 | transform: translate(-50%, -50%); 77 | } -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts", 14 | "polyfills.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | declare module "*.json" { 7 | const value: any; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | "app", 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | "app", 129 | "kebab-case" 130 | ], 131 | "no-output-on-prefix": true, 132 | "use-input-property-decorator": true, 133 | "use-output-property-decorator": true, 134 | "use-host-property-decorator": true, 135 | "no-input-rename": true, 136 | "no-output-rename": true, 137 | "use-life-cycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "component-class-suffix": true, 140 | "directive-class-suffix": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------