├── .eslintignore ├── .gitignore ├── .mdlrc ├── .github ├── FUNDING.yml ├── global.png ├── example_nhl.png ├── example_nhl_2.png ├── example_nhl_3.png ├── example_nhl_4.png ├── example_nhl_5.png ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md └── workflows │ ├── changelog.yml │ └── build.yml ├── .editorconfig ├── .stylelintrc ├── jsdoc.json ├── translations ├── en.json ├── fi.json ├── fr.json └── de.json ├── MMM-NHL.css ├── .codeclimate.yml ├── .eslintrc ├── package.json ├── LICENSE ├── CHANGELOG.md ├── README.md ├── templates └── MMM-NHL.njk ├── MMM-NHL.js └── node_helper.js /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/**/* 2 | docs/ 3 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | all 2 | rules "~MD013", "~MD024", "~MD026", "~MD033" 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: fewieden 2 | custom: ['https://paypal.me/fewieden'] 3 | -------------------------------------------------------------------------------- /.github/global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewieden/MMM-NHL/HEAD/.github/global.png -------------------------------------------------------------------------------- /.github/example_nhl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewieden/MMM-NHL/HEAD/.github/example_nhl.png -------------------------------------------------------------------------------- /.github/example_nhl_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewieden/MMM-NHL/HEAD/.github/example_nhl_2.png -------------------------------------------------------------------------------- /.github/example_nhl_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewieden/MMM-NHL/HEAD/.github/example_nhl_3.png -------------------------------------------------------------------------------- /.github/example_nhl_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewieden/MMM-NHL/HEAD/.github/example_nhl_4.png -------------------------------------------------------------------------------- /.github/example_nhl_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewieden/MMM-NHL/HEAD/.github/example_nhl_5.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Platform (Hardware/OS): 2 | 3 | Node version: 4 | 5 | MagicMirror² version: 6 | 7 | Module version: 8 | 9 | Description of the issue: 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please create pull requests towards the branch `develop`. 2 | 3 | * Does the pull request solve an issue (add a reference)? 4 | * What are the features of this pr? 5 | * Add screenshots for visual changes. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [{*.json, *.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "indentation": 4, 5 | "selector-class-pattern": [ 6 | "^([a-z][a-z0-9]*|MMM-NHL)(-[a-z0-9]+)*$", 7 | {"message": "Expected class selector to be kebab-case"} 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "dictionaries": ["jsdoc"] 4 | }, 5 | "source": { 6 | "include": [ 7 | "package.json", 8 | "LICENSE", 9 | "README.md" 10 | ], 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | }, 15 | "opts": { 16 | "destination": "docs", 17 | "recurse": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thanks for contributing to this module! 4 | 5 | Please create pull requests towards the branch `develop`. 6 | 7 | To hold one code style and standard there are several linters and tools in this project configured. Make sure you fulfill the requirements. 8 | Also, there will be an automatic analysis performed once you create the pull request. 9 | -------------------------------------------------------------------------------- /translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "Home", 3 | "AWAY": "Away", 4 | "PRE_GAME": "Warm Up", 5 | "1st": "1st Period", 6 | "2nd": "2nd Period", 7 | "3rd": "3rd Period", 8 | "OVER_TIME": "OT", 9 | "SHOOTOUT": "SO", 10 | "FINAL": "Final", 11 | "FINAL_OT": "Final (OT)", 12 | "FINAL_SO": "Final (SO)", 13 | "TIME_LEFT": "{TIME} left", 14 | "POSTPONED": "PPD", 15 | "SERIES": "Series" 16 | } 17 | -------------------------------------------------------------------------------- /translations/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "Koti", 3 | "AWAY": "Vieras", 4 | "PRE_GAME": "Lämmittely", 5 | "1st": "1. Erä", 6 | "2nd": "2. Erä", 7 | "3rd": "3. Erä", 8 | "OVER_TIME": "JA", 9 | "SHOOTOUT": "VL", 10 | "FINAL": "Päättynyt", 11 | "FINAL_OT": "Päättynyt (JA)", 12 | "FINAL_SO": "Päättynyt (VL)", 13 | "TIME_LEFT": "{TIME} jäljellä", 14 | "POSTPONED": "Siirretty", 15 | "SERIES": "Sarja" 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: changelog 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: dangoslen/changelog-enforcer@v1.6.1 13 | with: 14 | changeLogPath: CHANGELOG.md 15 | skipLabels: Skip Changelog 16 | -------------------------------------------------------------------------------- /translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "Domicile", 3 | "AWAY": "Visiteur", 4 | "PRE_GAME": "Warm Up", 5 | "1st": "1ere Period", 6 | "2nd": "2nd Period", 7 | "3rd": "3eme Period", 8 | "OVER_TIME": "Prolongation", 9 | "SHOOTOUT": "Tir au but", 10 | "FINAL": "Final", 11 | "FINAL_OT": "Final (OT)", 12 | "FINAL_SO": "Final (SO)", 13 | "TIME_LEFT": "{TIME} restant", 14 | "POSTPONED": "REM", 15 | "SERIES": "Séries" 16 | } 17 | -------------------------------------------------------------------------------- /translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "Heim", 3 | "AWAY": "Auswärts", 4 | "PRE_GAME": "Aufwärmen", 5 | "1st": "1. Drittel", 6 | "2nd": "2. Drittel", 7 | "3rd": "3. Drittel", 8 | "OVER_TIME": "Verlängerung", 9 | "SHOOTOUT": "Elfmeterschießen", 10 | "FINAL": "Beendet", 11 | "FINAL_OT": "Beendet (OT)", 12 | "FINAL_SO": "Beendet (SO)", 13 | "TIME_LEFT": "{TIME} übrig", 14 | "POSTPONED": "Verlegt", 15 | "SERIES": "Serie" 16 | } 17 | -------------------------------------------------------------------------------- /MMM-NHL.css: -------------------------------------------------------------------------------- 1 | .MMM-NHL .table { 2 | border-spacing: 10px 0; 3 | border-collapse: separate; 4 | text-align: center; 5 | } 6 | 7 | .MMM-NHL .icon { 8 | width: 37px; 9 | height: 25px; 10 | } 11 | 12 | .MMM-NHL .no-color { 13 | filter: grayscale(100%); 14 | } 15 | 16 | .MMM-NHL .row { 17 | vertical-align: middle; 18 | } 19 | 20 | .MMM-NHL .live { 21 | font-size: 10px; 22 | line-height: 15px; 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | stylelint: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - javascript 9 | eslint: 10 | enabled: true 11 | channel: "eslint-7" 12 | checks: 13 | import/no-unresolved: 14 | enabled: false 15 | fixme: 16 | enabled: true 17 | markdownlint: 18 | enabled: true 19 | ratings: 20 | paths: 21 | - "**.js" 22 | - "**.css" 23 | - "**.md" 24 | exclude_paths: 25 | - "node_modules/**/*" 26 | - "docs/**/*" 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 2 | 3 | name: build 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master, develop ] 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 16.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 16.x 20 | - run: npm ci 21 | - run: npm run lint 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["esnext", "esnext/style-guide", "node", "node/style-guide"], 3 | "parserOptions": { 4 | "ecmaVersion": 2017, 5 | "sourceType": "module" 6 | }, 7 | "settings": { 8 | "import/core-modules": ["logger", "node_helper"] 9 | }, 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "rules": { 16 | "import/no-commonjs": 0, 17 | "import/no-nodejs-modules": 0, 18 | "semi": 0, 19 | "comma-dangle": 0, 20 | "indent": ["error", 4], 21 | "template-curly-spacing": 0, 22 | "no-console": 0, 23 | "curly": ["error", "all"], 24 | "array-bracket-spacing": 0, 25 | "space-before-function-paren": 0, 26 | "object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmm-nhl", 3 | "version": "2.3.1", 4 | "description": "National Hockey League Module for MagicMirror²", 5 | "scripts": { 6 | "docs": "jsdoc -c jsdoc.json .", 7 | "lint": "eslint . && stylelint **/*.css" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/fewieden/MMM-NHL.git" 12 | }, 13 | "keywords": [ 14 | "MagicMirror", 15 | "NHL", 16 | "ice hockey" 17 | ], 18 | "author": "fewieden", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/fewieden/MMM-NHL/issues" 22 | }, 23 | "homepage": "https://github.com/fewieden/MMM-NHL#readme", 24 | "devDependencies": { 25 | "eslint": "^7.32.0", 26 | "eslint-config-recommended": "^4.1.0", 27 | "jsdoc": "^3.6.10", 28 | "stylelint": "^14.8.2", 29 | "stylelint-config-standard": "^25.0.0" 30 | }, 31 | "dependencies": { 32 | "node-fetch": "^2.6.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 fewieden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # MMM-NHL Changelog 2 | 3 | ## [2.4.0] 4 | 5 | Thanks to @parnic @dannoh for their contributions. 6 | 7 | ### Fixed 8 | 9 | * Updated module to work with the new NHL API. 10 | 11 | ## [2.3.1] 12 | 13 | Thanks to @parnic @dannoh @timpeer for their contributions. 14 | 15 | ### Added 16 | 17 | * Finnish translations 18 | 19 | ### Fixed 20 | 21 | * Team logo images for the 2023-2024 season 22 | 23 | ## [2.3.0] 24 | 25 | ### Changed 26 | 27 | * Updated dependencies 28 | * Updated Github config files 29 | * Updated project config files 30 | * Uniform spelling for MagicMirror² 31 | 32 | ### Fixed 33 | 34 | * Playoff series display 35 | 36 | ## [2.2.0] 37 | 38 | MagicMirror² version >= 2.15.0 required. 39 | 40 | ### Added 41 | 42 | * Added new config option `showPlayoffSeries` to display playoff series information 43 | 44 | ### Changed 45 | 46 | * Node helper logs are now done through MagicMirror logger 47 | * Updated project config files 48 | * Updated Github config files 49 | 50 | ### Fixed 51 | 52 | * Changed Logo Urls to support all teams (specifically all-star teams) 53 | * Added support for teams with no short name when showNames is true 54 | 55 | ## [2.1.0] 56 | 57 | ### Fixed 58 | 59 | * Date queries are now set based on timezone `America/Toronto`. 60 | 61 | ### Added 62 | 63 | * Config option `rollOver` 64 | 65 | ## [2.0.0] 66 | 67 | ### Added 68 | 69 | * Nunjuck templates 70 | * French translations thanks to [matlem037](https://github.com/matlem037) 71 | * Dependency `node-fetch` 72 | * Config option `daysInPast` 73 | * Config option `daysAhead` 74 | * Config option `liveReloadInterval` thanks to [dannoh](https://github.com/dannoh). 75 | * Config option `showNames` thanks to [dannoh](https://github.com/dannoh). 76 | * Config option `showLogos` thanks to [dannoh](https://github.com/dannoh). 77 | * Support for game status `postponed` thanks to [dannoh](https://github.com/dannoh). 78 | * Github actions (linting and changelog enforcer) 79 | * JSDoc documentation 80 | 81 | ### Changed 82 | 83 | * Switched API for data feed. 84 | * Display logos from remote. 85 | * Retrieve team list from API. 86 | * ESLint recommended instead of airbnb ruleset. 87 | 88 | ### Removed 89 | 90 | * Config option `format`, instead rendering information based on locale. 91 | * Travis integration 92 | * Dependency `moment-timezone` 93 | * Dependency `request` 94 | * Local team logos 95 | 96 | ## [1.0.1] 97 | 98 | ### Added 99 | 100 | * Added new team: Vegas Golden Knights 101 | 102 | ## [1.0.0] 103 | 104 | Initial version 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-NHL [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/fewieden/MMM-NHL/master/LICENSE) ![Build status](https://github.com/fewieden/MMM-NHL/workflows/build/badge.svg) [![Code Climate](https://codeclimate.com/github/fewieden/MMM-NHL/badges/gpa.svg?style=flat)](https://codeclimate.com/github/fewieden/MMM-NHL) [![Known Vulnerabilities](https://snyk.io/test/github/fewieden/mmm-nhl/badge.svg)](https://snyk.io/test/github/fewieden/mmm-nhl) 2 | 3 | National Hockey League Module for MagicMirror² 4 | 5 | ## Examples 6 | 7 | ![](.github/example_nhl.png) ![](.github/example_nhl_2.png) ![](.github/example_nhl_3.png) ![](.github/example_nhl_4.png) ![](.github/example_nhl_5.png) 8 | 9 | ## Dependencies 10 | 11 | * An installation of [MagicMirror²](https://github.com/MichMich/MagicMirror) 12 | * npm 13 | * [node-fetch](https://www.npmjs.com/package/node-fetch) 14 | 15 | ## Installation 16 | 17 | * Clone this repo into `~/MagicMirror/modules` directory. 18 | * Configure your `~/MagicMirror/config/config.js`: 19 | 20 | ```js 21 | { 22 | module: 'MMM-NHL', 23 | position: 'top_right', 24 | config: { 25 | // Add your config options here, which have a different value than default. 26 | } 27 | } 28 | ``` 29 | 30 | * Run command `npm i --production` in `~/MagicMirror/modules/MMM-NHL` directory. 31 | 32 | ## Config Options 33 | 34 | | **Option** | **Default** | **Description** | 35 | | --- | --- | --- | 36 | | `colored` | `false` | Remove black/white filter of logos. | 37 | | `focus_on` | `false` | Display only matches with teams of this array e.g. `['VAN', 'MTL', 'BOS']`. | 38 | | `matches` | `6` | Max number of matches displaying simultaneously. | 39 | | `rotateInterval` | `20000` (20 secs) | How often should be rotated the matches in the list. | 40 | | `reloadInterval` | `1800000` (30 mins) | How often should the data be fetched. | 41 | | `daysInPast` | `1` | How many days should a game be displayed after it is finished. | 42 | | `daysAhead` | `7` | How many days should a game be displayed before it starts. | 43 | | `liveReloadInterval` | `60000 (1 min)` | How often should the data be fetched during a live game. | 44 | | `showNames` | `true` | Should team names be displayed? | 45 | | `showLogos` | `true` | Should team logos be displayed? | 46 | | `showPlayoffSeries` | `true` | Should playoff series be displayed (if in playoffs)? | 47 | | `rollOver` | `false` | Displays today's games and based on game status also yesterdays games or tomorrows games. Automatically overrides `daysInPast` and `daysAhead` to 1. | 48 | 49 | ## Global config 50 | 51 | | **Option** | **Default** | **Description** | 52 | | --- | --- | --- | 53 | | `locale` | `undefined` | By default it is using your system settings. You can specify the locale in the global MagicMirror² config. Possible values are for e.g.: `'en-US'` or `'de-DE'`. | 54 | 55 | To set a global config you have top set the value in your config.js file inside the MagicMirror² project. 56 | 57 | ![](.github/global.png) 58 | 59 | ## Developer 60 | 61 | * `npm run lint` - Lints JS and CSS files. 62 | * `npm run docs` - Generates documentation. 63 | -------------------------------------------------------------------------------- /templates/MMM-NHL.njk: -------------------------------------------------------------------------------- 1 | {% if loading %} 2 |
{{ "LOADING" | translate }}
3 | {% else %} 4 |
NHL {{ modes[season.mode] | translate }} {{ season.year }}
5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | {% for index in range(rotateIndex, maxGames) %} 20 | 21 | 46 | {% if config.showNames %} 47 | 50 | {% endif %} 51 | {% if config.showLogos %} 52 | 56 | {% endif %} 57 | 58 | 59 | 60 | {% if config.showLogos %} 61 | 65 | {% endif %} 66 | {% if config.showNames %} 67 | 70 | {% endif %} 71 | 72 | {% endfor %} 73 | 74 |
10 | {{ "HOME" | translate }} 11 | 14 | {{ "AWAY" | translate }} 15 |
22 | {% if games[index].status === "PRE" %} 23 | {{ "PRE_GAME" | translate }} 24 | {# TODO: Find out what the state postponed state is in the new API #} 25 | {% elif games[index].status === "Postponed" %} 26 | {{ "POSTPONED" | translate }} 27 | {% elif games[index].status === "FUT" %} 28 | {{ games[index] | formatStartDate }} 29 | {% elif (games[index].status === "LIVE" or games[index].status === "CRIT") and games[index].live.period %} 30 | {% if games[index].live.timeRemaining %} 31 |
{{ games[index].live.period | translate }}
32 |
33 | {{ "TIME_LEFT" | translate({TIME: games[index].live.timeRemaining}) }} 34 |
35 | {% else %} 36 | {{ games[index].live.period | translate }} 37 | {% endif %} 38 | {% else %} 39 | {% if games[index].live.period === '3rd' %} 40 | {{ "FINAL" | translate }} 41 | {% else %} 42 | {{ ("FINAL_" + games[index].live.periodType) | translate }} 43 | {% endif %} 44 | {% endif %} 45 |
48 | {{ games[index].teams.home.short if games[index].teams.home.short else games[index].teams.home.name }} 49 | 53 | 55 | {{ games[index].teams.home.score }}:{{ games[index].teams.away.score }} 62 | 64 | 68 | {{ games[index].teams.away.short if games[index].teams.away.short else games[index].teams.away.name }} 69 |
75 | {% if config.showPlayoffSeries and playoffSeries and playoffSeries.length > 0 %} 76 |
{{ "SERIES" | translate }}
77 | 78 | 79 | {% for series in playoffSeries %} 80 | 81 | {% if config.showNames %} 82 | 85 | {% endif %} 86 | {% if config.showLogos %} 87 | 91 | {% endif %} 92 | 93 | 94 | 95 | {% if config.showLogos %} 96 | 100 | {% endif %} 101 | {% if config.showNames %} 102 | 105 | {% endif %} 106 | 107 | {% endfor %} 108 | 109 |
83 | {{ series.teams.home.short }} 84 | 88 | 90 | {{ series.teams.home.score }}:{{ series.teams.away.score }} 97 | 99 | 103 | {{ series.teams.away.short }} 104 |
110 | {% endif %} 111 | {% endif %} 112 | -------------------------------------------------------------------------------- /MMM-NHL.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file MMM-NHL.js 3 | * 4 | * @author fewieden 5 | * @license MIT 6 | * 7 | * @see https://github.com/fewieden/MMM-NHL 8 | */ 9 | 10 | /* global Module Log config */ 11 | 12 | /** 13 | * @external Module 14 | * @see https://github.com/MichMich/MagicMirror/blob/master/js/module.js 15 | */ 16 | 17 | /** 18 | * @external Log 19 | * @see https://github.com/MichMich/MagicMirror/blob/master/js/logger.js 20 | */ 21 | 22 | /** 23 | * @module MMM-NHL 24 | * @description Frontend of the MagicMirror² module. 25 | * 26 | * @requires external:Module 27 | * @requires external:Log 28 | */ 29 | Module.register('MMM-NHL', { 30 | /** 31 | * @member {object.} modes - Maps mode short codes to names. 32 | */ 33 | modes: { 34 | 1: 'Pre-season', 35 | 2: 'Regular season', 36 | 3: 'Playoffs', 37 | }, 38 | 39 | /** 40 | * @member {object.} states - Maps game state short codes to translation keys. 41 | */ 42 | states: { 43 | '1st': '1ST_PERIOD', 44 | '2nd': '2ND_PERIOD', 45 | '3rd': '3RD_PERIOD', 46 | OT: 'OVER_TIME', 47 | SO: 'SHOOTOUT', 48 | SHOOTOUT: 'SHOOTOUT', 49 | FINAL: 'FINAL', 50 | 'FINAL OT': 'FINAL_OVERTIME', 51 | 'FINAL SO': 'FINAL_SHOOTOUT', 52 | PPD: 'PPD' 53 | }, 54 | 55 | /** 56 | * @member {boolean} loading - Indicates loading state of module and data. 57 | */ 58 | loading: true, 59 | /** 60 | * @member {Game[]} games - List of all games matching focus and timespan config options. 61 | */ 62 | games: [], 63 | 64 | /** 65 | * @member {Series[]} playoffSeries - List of all current playoff series. 66 | */ 67 | playoffSeries: [], 68 | 69 | /** 70 | * @member {SeasonDetails} season - Current season details e.g. year and mode. 71 | */ 72 | season: {}, 73 | /** 74 | * @member {number} rotateIndex - Current index of rotation carousel. 75 | */ 76 | rotateIndex: 0, 77 | /** 78 | * @member {?Interval} rotateInterval - Interval to update rotation index. 79 | */ 80 | rotateInterval: null, 81 | 82 | /** 83 | * @member {object} defaults - Defines the default config values. 84 | * @property {boolean} colored - Flag to show logos in color or black/white. 85 | * @property {boolean|string[]} focus_on - List of team name short codes to display games from. 86 | * @property {number} matches - Max amount of matches to display at once. 87 | * @property {number} rotateInterval - Amount of milliseconds a page of the carousel is displayed. 88 | * @property {number} reloadInterval - Amount of milliseconds between data fetching. 89 | * @property {number} liveReloadInterval - Amount of milliseconds between data fetching during a live game. 90 | * @property {number} daysInPast - Amount of days a match should be displayed after it is finished. 91 | * @property {number} daysAhead - Amount of days a match should be displayed before it starts. 92 | * @property {boolean} showNames - Flag to show team names. 93 | * @property {boolean} showLogos - Flag to show club logos. 94 | * @property {boolean} showPlayoffSeries - Flag to show playoff series status during playoffs. 95 | * @property {boolean} rollOverGames - Flag to show today's games and previous/next day based on game status. 96 | */ 97 | defaults: { 98 | colored: false, 99 | focus_on: false, 100 | matches: 6, 101 | rotateInterval: 20 * 1000, 102 | reloadInterval: 30 * 60 * 1000, 103 | liveReloadInterval: 60 * 1000, 104 | daysInPast: 1, 105 | daysAhead: 7, 106 | showNames: true, 107 | showLogos: true, 108 | showPlayoffSeries: true, 109 | rollOver: false 110 | }, 111 | 112 | /** 113 | * @function getTranslations 114 | * @description Translations for this module. 115 | * @override 116 | * 117 | * @returns {object.} Available translations for this module (key: language code, value: filepath). 118 | */ 119 | getTranslations() { 120 | return { 121 | en: 'translations/en.json', 122 | de: 'translations/de.json', 123 | fr: 'translations/fr.json', 124 | fi: 'translations/fi.json' 125 | }; 126 | }, 127 | 128 | /** 129 | * @function getStyles 130 | * @description Style dependencies for this module. 131 | * @override 132 | * 133 | * @returns {string[]} List of the style dependency filepaths. 134 | */ 135 | getStyles() { 136 | return ['font-awesome.css', 'MMM-NHL.css']; 137 | }, 138 | 139 | /** 140 | * @function getTemplate 141 | * @description Nunjuck template. 142 | * @override 143 | * 144 | * @returns {string} Path to nunjuck template. 145 | */ 146 | getTemplate() { 147 | return 'templates/MMM-NHL.njk'; 148 | }, 149 | 150 | /** 151 | * @function getTemplateData 152 | * @description Data that gets rendered in the nunjuck template. 153 | * @override 154 | * 155 | * @returns {object} Data for the nunjuck template. 156 | */ 157 | getTemplateData() { 158 | return { 159 | loading: this.loading, 160 | modes: this.modes, 161 | season: this.season, 162 | games: this.games, 163 | playoffSeries: this.playoffSeries, 164 | rotateIndex: this.rotateIndex, 165 | maxGames: Math.min(this.games.length, this.rotateIndex + this.config.matches), 166 | config: this.config 167 | }; 168 | }, 169 | 170 | /** 171 | * @function start 172 | * @description Adds nunjuck filters, overrides day config options if rollOver and sends config to node_helper. 173 | * @override 174 | * 175 | * @returns {void} 176 | */ 177 | start() { 178 | Log.info(`Starting module: ${this.name}`); 179 | this.addFilters(); 180 | 181 | if (this.config.rollOver) { 182 | this.config.daysInPast = 1; 183 | this.config.daysAhead = 1; 184 | } 185 | 186 | this.sendSocketNotification('CONFIG', { config: this.config }); 187 | }, 188 | 189 | /** 190 | * @function socketNotificationReceived 191 | * @description Handles incoming messages from node_helper. 192 | * @override 193 | * 194 | * @param {string} notification - Notification name 195 | * @param {*} payload - Detailed payload of the notification. 196 | */ 197 | socketNotificationReceived(notification, payload) { 198 | if (notification === 'SCHEDULE') { 199 | this.loading = false; 200 | this.games = payload.games; 201 | this.season = payload.season; 202 | this.setRotateInterval(); 203 | } else if (notification === 'PLAYOFFS') { 204 | this.playoffSeries = payload; 205 | this.updateDom(300); 206 | } 207 | }, 208 | 209 | /** 210 | * @function setRotateInterval 211 | * @description Sets interval if necessary which updates the rotateIndex. 212 | * 213 | * @returns {void} 214 | */ 215 | setRotateInterval() { 216 | if (!this.rotateInterval && this.games.length > this.config.matches) { 217 | this.rotateInterval = setInterval(() => { 218 | if (this.rotateIndex + this.config.matches >= this.games.length) { 219 | this.rotateIndex = 0; 220 | } else { 221 | this.rotateIndex += this.config.matches; 222 | } 223 | this.updateDom(300); 224 | }, this.config.rotateInterval); 225 | } else if (this.games.length <= this.config.matches) { 226 | clearInterval(this.rotateInterval); 227 | this.rotateIndex = 0; 228 | } 229 | 230 | this.updateDom(300); 231 | }, 232 | 233 | /** 234 | * @function addFilters 235 | * @description Adds the filter used by the nunjuck template. 236 | * 237 | * @returns {void} 238 | */ 239 | addFilters() { 240 | this.nunjucksEnvironment().addFilter('formatStartDate', game => { 241 | const now = new Date(); 242 | const inAWeek = now.setDate(now.getDate() + 7); 243 | const start = new Date(game.timestamp); 244 | 245 | if (start > inAWeek) { 246 | return new Intl.DateTimeFormat(config.locale, { 247 | month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' 248 | }).format(start); 249 | } 250 | 251 | return new Intl.DateTimeFormat(config.locale, { 252 | weekday: 'short', hour: '2-digit', minute: '2-digit' 253 | }).format(start); 254 | }); 255 | } 256 | }); 257 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file node_helper.js 3 | * 4 | * @author fewieden 5 | * @license MIT 6 | * 7 | * @see https://github.com/fewieden/MMM-NHL 8 | */ 9 | 10 | /* eslint-env node */ 11 | 12 | /** 13 | * @external node-fetch 14 | * @see https://www.npmjs.com/package/node-fetch 15 | */ 16 | const fetch = require('node-fetch'); 17 | 18 | /** 19 | * @external logger 20 | * @see https://github.com/MichMich/MagicMirror/blob/master/js/logger.js 21 | */ 22 | const Log = require('logger'); 23 | 24 | /** 25 | * @external node_helper 26 | * @see https://github.com/MichMich/MagicMirror/blob/master/js/node_helper.js 27 | */ 28 | const NodeHelper = require('node_helper'); 29 | 30 | const BASE_PLAYOFF_URL = 'https://statsapi.web.nhl.com/api/v1/tournaments/playoffs?expand=round.series'; 31 | 32 | /** 33 | * Derived team details of a game from API endpoint for easier usage. 34 | * 35 | * @typedef {object} Team 36 | * @property {number} id - Team identifier. 37 | * @property {string} abbrev - 3 letter team name. 38 | * @property {number} score - Current score of the team. 39 | */ 40 | 41 | /** 42 | * Derived game details from API endpoint for easier usage. 43 | * 44 | * @typedef {object} Game 45 | * @property {number} id - Game identifier. 46 | * @property {string} timestamp - Start date of the game in UTC timezone. 47 | * @property {string} gameDay - Game day in format YYYY-MM-DD in north american timezone. 48 | * @property {string} gameState - Contains information about the game status, e.g. OFF, LIVE, CRIT, FUT. 49 | * @property {Team} awayTeam - Contains information about the away team. 50 | * @property {Team} homeTeam - Contains information about the home team. 51 | * @property {object} periodDescriptor - Contains information about the period of play of the game. Is present on all games, past, present, and future. 52 | * @property {number} periodDescriptor.number - Period of the game e.g. 1, 2, 3, 4. 53 | * @property {string} periodDescriptor.periodType - Abbreviated description of the period type, e.g. REG, OT. 54 | */ 55 | 56 | /** 57 | * Derived game details from API endpoint for easier usage. 58 | * 59 | * @typedef {object} Series 60 | * @property {number} gameNumberOfSeries - Game identifier. 61 | * @property {number} round - Playoff round number, e.g. 1, 2, 3, 4. 62 | * @property {string} roundAbbrev - Abbreviation of round type, e.g. SCF 63 | * @property {number} topSeedTeamId - Contains the ID of the top-seeded team. 64 | * @property {number} topSeedWins - Contains the number of wins of the top-seeded team in this round. 65 | * @property {number} bottomSeedTeamId - Contains the ID of the bottom-seeded team. 66 | * @property {number} bottomSeedWins - Contains the number of wins of the bottom-seed team in this round. 67 | */ 68 | 69 | /** 70 | * Derived season details from API endpoint for easier usage. 71 | * 72 | * @typedef {object} SeasonDetails 73 | * @property {string} year - Year of the season in format yy/yy e.g. 20/21. 74 | * @property {number} mode - Mode of the season e.g. 0, 1 and 2. 75 | */ 76 | 77 | /** 78 | * @module node_helper 79 | * @description Backend for the module to query data from the API provider. 80 | * 81 | * @requires external:node-fetch 82 | * @requires external:logger 83 | * @requires external:node_helper 84 | */ 85 | module.exports = NodeHelper.create({ 86 | /** @member {string} requiresVersion - Defines the minimum version of MagicMirror² to run this node_helper. */ 87 | requiresVersion: '2.15.0', 88 | /** @member {?Game} nextGame - The next upcoming game is stored in this variable. */ 89 | nextGame: null, 90 | /** @member {Game[]} liveGames - List of all ongoing games. */ 91 | liveGames: [], 92 | /** @member {Game[]} liveStates - List of all live game states. */ 93 | liveStates: ['LIVE', 'CRIT'], 94 | 95 | /** 96 | * @function socketNotificationReceived 97 | * @description Receives socket notifications from the module. 98 | * @async 99 | * @override 100 | * 101 | * @param {string} notification - Notification name 102 | * @param {*} payload - Detailed payload of the notification. 103 | * 104 | * @returns {void} 105 | */ 106 | async socketNotificationReceived(notification, payload) { 107 | if (notification === 'CONFIG') { 108 | this.config = payload.config; 109 | 110 | await this.initTeams(); 111 | 112 | await this.updateSchedule(); 113 | setInterval(() => this.updateSchedule(), this.config.reloadInterval); 114 | setInterval(() => this.fetchOnLiveState(), this.config.liveReloadInterval); 115 | } 116 | }, 117 | 118 | /** 119 | * @function initTeams 120 | * @description Retrieves a list of all teams from the API and initializes teamMapping. 121 | * @async 122 | * 123 | * @returns {void} 124 | */ 125 | async initTeams() { 126 | if (this.teamMapping) { 127 | return; 128 | } 129 | 130 | const response = await fetch(`https://api.nhle.com/stats/rest/en/team`); 131 | 132 | if (!response.ok) { 133 | Log.error(`Initializing NHL teams failed: ${response.status} ${response.statusText}`); 134 | 135 | return; 136 | } 137 | 138 | const { data } = await response.json(); 139 | 140 | this.teamMapping = data.reduce((mapping, team) => { 141 | mapping[team.id] = { short: team.triCode, name: team.fullName }; 142 | 143 | return mapping; 144 | }, {}); 145 | }, 146 | 147 | /** 148 | * @function getScheduleDates 149 | * @description Helper function to retrieve dates in the past and future based on config options. 150 | * @async 151 | * 152 | * @returns {object} Dates in the past and future. 153 | */ 154 | getScheduleDates() { 155 | const start = new Date(); 156 | start.setDate(start.getDate() - this.config.daysInPast); 157 | 158 | const end = new Date(); 159 | end.setDate(end.getDate() + this.config.daysAhead + 1); 160 | end.setHours(0); 161 | end.setMinutes(0); 162 | end.setSeconds(0); 163 | 164 | const today = new Date(); 165 | 166 | return { 167 | startUtc: start.toISOString(), 168 | startFormatted: new Intl.DateTimeFormat('fr-ca', { timeZone: 'America/Toronto' }).format(start), 169 | endUtc: end.toISOString(), 170 | endFormatted: new Intl.DateTimeFormat('fr-ca', { timeZone: 'America/Toronto' }).format(end), 171 | todayUtc: today.toISOString(), 172 | todayFormatted: new Intl.DateTimeFormat('fr-ca', { timeZone: 'America/Toronto' }).format(today) 173 | }; 174 | }, 175 | 176 | /** 177 | * @function getRemainingGameTime 178 | * @description Helper function to retrieve remaining game time. 179 | * @async 180 | * 181 | * @returns {string?} Remaining game time. 182 | */ 183 | getRemainingGameTime(game, scores) { 184 | if (!this.liveStates.includes(game.gameState)) { 185 | return; 186 | } 187 | 188 | const score = scores.find(score => score.id === game.id); 189 | if (!score) { 190 | return; 191 | } 192 | 193 | return score?.clock?.inIntermission ? '00:00' : score?.clock?.timeRemaining; 194 | }, 195 | 196 | /** 197 | * @function hydrateRemainingTime 198 | * @description Hydrates remaining time on the games in the schedule from the scores API endpoint. 199 | * @async 200 | * 201 | * @returns {object[]} Raw games from API endpoint including remaining time. 202 | */ 203 | async hydrateRemainingTime(schedule) { 204 | const { todayFormatted } = this.getScheduleDates(); 205 | const scoresUrl = `https://api-web.nhle.com/v1/score/${todayFormatted}`; 206 | const scoresResponse = await fetch(scoresUrl); 207 | if (!scoresResponse.ok) { 208 | Log.error(`Fetching NHL scores failed: ${scoresResponse.status} ${scoresResponse.statusText}. Url: ${scoresUrl}`); 209 | 210 | return schedule; 211 | } 212 | 213 | const { games } = await scoresResponse.json(); 214 | 215 | for (const game of schedule) { 216 | game.timeRemaining = this.getRemainingGameTime(game, games); 217 | } 218 | 219 | return schedule; 220 | }, 221 | 222 | /** 223 | * @function fetchSchedule 224 | * @description Retrieves a list of games from the API with timespan based on config options. 225 | * @async 226 | * 227 | * @returns {object[]} Raw games from API endpoint. 228 | */ 229 | async fetchSchedule() { 230 | const { startFormatted, endUtc } = this.getScheduleDates(); 231 | 232 | const scheduleUrl = `https://api-web.nhle.com/v1/schedule/${startFormatted}`; 233 | const scheduleResponse = await fetch(scheduleUrl); 234 | if (!scheduleResponse.ok) { 235 | Log.error(`Fetching NHL schedule failed: ${scheduleResponse.status} ${scheduleResponse.statusText}. Url: ${scheduleUrl}`); 236 | return; 237 | } 238 | 239 | const { gameWeek } = await scheduleResponse.json(); 240 | 241 | const schedule = gameWeek.map(({ date, games }) => games.filter(game => game.startTimeUTC < endUtc).map(game => ({ ...game, gameDay: date }))).flat(); 242 | 243 | const scheduleWithRemainingTime = await this.hydrateRemainingTime(schedule); 244 | 245 | return scheduleWithRemainingTime; 246 | }, 247 | 248 | /** 249 | * @function fetchPlayoffs 250 | * @description Retrieves playoff data from the API. 251 | * @async 252 | * 253 | * @returns {object} Raw playoff data from API endpoint. 254 | */ 255 | async fetchPlayoffs() { 256 | // TODO: Find playoff endpoints in new API 257 | const response = await fetch(BASE_PLAYOFF_URL); 258 | 259 | if (!response.ok) { 260 | Log.error(`Fetching NHL playoffs failed: ${response.status} ${response.statusText}.`); 261 | return; 262 | } 263 | 264 | const playoffs = await response.json(); 265 | playoffs.rounds.sort((a, b) => a.number <= b.number ? 1 : -1); 266 | 267 | return playoffs; 268 | }, 269 | 270 | /** 271 | * @function filterGameByFocus 272 | * @description Helper function to filter games based on config option. 273 | * 274 | * @param {object} game - Raw game information. 275 | * 276 | * @returns {boolean} Should game remain in list? 277 | */ 278 | filterGameByFocus(game) { 279 | const focus = this.config.focus_on; 280 | if (!focus) { 281 | return true; 282 | } 283 | 284 | const homeTeam = this.teamMapping[game.homeTeam.id].short; 285 | const awayTeam = this.teamMapping[game.awayTeam.id].short; 286 | 287 | return focus.includes(homeTeam) || focus.includes(awayTeam); 288 | }, 289 | 290 | /** 291 | * @function filterRollOverGames 292 | * @description Helper function to filter games based on rollOver config option. 293 | * 294 | * @param {Game[]} games - List of all games. 295 | * 296 | * @returns {Game[]} List of filtered games. 297 | */ 298 | filterRollOverGames(games) { 299 | if (!this.config.rollOver) { 300 | return games; 301 | } 302 | 303 | const date = new Intl.DateTimeFormat('fr-ca', { timeZone: 'America/Toronto' }) 304 | .format(new Date()); 305 | 306 | const yesterday = games.filter(game => game.gameDay < date); 307 | const today = games.filter(game => game.gameDay === date); 308 | const tomorrow = games.filter(game => game.gameDay > date); 309 | 310 | const ongoingStates = ['OFF', 'CRIT', 'LIVE']; 311 | 312 | if (today.some(game => ongoingStates.includes(game.status))) { 313 | return [...today, ...tomorrow]; 314 | } 315 | 316 | return [...yesterday, ...today]; 317 | }, 318 | 319 | /** 320 | * @function computeSeasonDetails 321 | * @description Computes current season details (year and mode) from list of games. 322 | * 323 | * @param {object[]} schedule - List of raw games from API endpoint. 324 | * 325 | * @returns {SeasonDetails} Current season details. 326 | */ 327 | computeSeasonDetails(schedule) { 328 | const game = schedule.find(game => game.gameState !== 'OFF') || schedule[schedule.length - 1]; 329 | 330 | if (game) { 331 | return { 332 | year: `${game.season.toString().slice(2, 4)}/${game.season.toString().slice(6, 8)}`, 333 | mode: game.gameType 334 | }; 335 | } 336 | 337 | const year = new Date().getFullYear(); 338 | const currentYear = year.toString().slice(-2); 339 | const nextYear = (year + 1).toString().slice(-2); 340 | 341 | return { 342 | year: `${currentYear}/${nextYear}`, 343 | mode: 1 344 | }; 345 | }, 346 | 347 | /** 348 | * @function computePlayoffDetails 349 | * @description Computes current playoff details from list of series. 350 | * 351 | * @param {object} playoffData - List of raw series from API endpoint. 352 | * 353 | * @returns {Series[]} Current season details. 354 | */ 355 | computePlayoffDetails(playoffData) { 356 | if (!playoffData || !playoffData.rounds) { 357 | return []; 358 | } 359 | 360 | const series = []; 361 | playoffData.rounds.forEach(r => { 362 | r.series.forEach(s => { 363 | const parsed = this.parseSeries(s); 364 | if (parsed) { 365 | series.push(parsed); 366 | } 367 | }); 368 | }); 369 | 370 | return series; 371 | }, 372 | 373 | /** 374 | * @function parseTeam 375 | * @description Transforms raw team information for easier usage. 376 | * 377 | * @param {object} team - Team in raw format. 378 | * 379 | * @returns {Team} Parsed team information. 380 | */ 381 | parseTeam(team) { 382 | if (!team) { 383 | Log.error('no team given'); 384 | return {}; 385 | } 386 | 387 | return { 388 | id: team.id, 389 | name: this.teamMapping[team.id].name, 390 | short: this.teamMapping[team.id].short, 391 | score: team.score ?? 0 392 | }; 393 | }, 394 | 395 | /** 396 | * @function parsePlayoffTeam 397 | * @description Transforms raw game information for easier usage. 398 | * 399 | * @param {object} rawTeam - Raw team information. 400 | * 401 | * @param {object} game - Raw game information. 402 | * 403 | * @returns {Game} Parsed game information. 404 | */ 405 | parsePlayoffTeam(rawTeam, game) { 406 | const team = this.parseTeam(rawTeam); 407 | 408 | if (game?.seriesStatus?.topSeedTeamId === team.id) { 409 | team.score = game?.seriesStatus?.topSeedWins; 410 | } else { 411 | team.score = game?.seriesStatus?.bottomSeedWins; 412 | } 413 | 414 | return team; 415 | }, 416 | 417 | /** 418 | * @function parseGame 419 | * @description Transforms raw game information for easier usage. 420 | * 421 | * @param {object} game - Raw game information. 422 | * 423 | * @returns {Game} Parsed game information. 424 | */ 425 | parseGame(game = {}) { 426 | return { 427 | id: game.id, 428 | timestamp: game.startTimeUTC, 429 | gameDay: game.gameDay, 430 | status: game.gameState, 431 | teams: { 432 | away: this.parseTeam(game.awayTeam), 433 | home: this.parseTeam(game.homeTeam) 434 | }, 435 | live: { 436 | period: this.getNumberWithOrdinal(game.periodDescriptor.number), 437 | periodType: game.periodDescriptor.periodType, 438 | timeRemaining: game.timeRemaining, 439 | } 440 | }; 441 | }, 442 | 443 | /** 444 | * @function getNumberWithOrdinal 445 | * @description Converts a raw number into a number with appropriate English ordinal suffix. 446 | * 447 | * @param {number} n - The number to apply an ordinal suffix to. 448 | * 449 | * @returns {string} The given number with its ordinal suffix appended. 450 | */ 451 | getNumberWithOrdinal(n) { 452 | // TODO: This function seems over complicated, don't we just have 1st 2nd and 3rd? 453 | const s = ['th', 'st', 'nd', 'rd']; 454 | const v = n % 100; 455 | 456 | return n + (s[(v - 20) % 10] || s[v] || s[0]); 457 | }, 458 | 459 | /** 460 | * @function parseSeries 461 | * @description Transforms raw series information for easier usage. 462 | * 463 | * @param {object} series - Raw series information. 464 | * 465 | * @returns {Series} Parsed series information. 466 | */ 467 | parseSeries(series = {}) { 468 | if (!series.matchupTeams || series.matchupTeams.length === 0) { 469 | return null; 470 | } 471 | 472 | return { 473 | number: series.number, 474 | round: series.round.number, 475 | teams: { 476 | home: this.parsePlayoffTeam(series.matchupTeams, undefined), // TODO: Don't pass undefined to retrieve the correct score 477 | away: this.parsePlayoffTeam(series.matchupTeams, undefined), // TODO: Don't pass undefined to retrieve the correct score 478 | } 479 | } 480 | }, 481 | 482 | /** 483 | * @function setNextandLiveGames 484 | * @description Sets the next scheduled and live games from a list of games. 485 | * 486 | * @param {Game[]} games - List of games. 487 | * 488 | * @returns {void} 489 | */ 490 | setNextandLiveGames(games) { 491 | this.nextGame = games.find(game => game.status === 'FUT'); 492 | this.liveGames = games.filter(game => this.liveStates.includes(game.status)); 493 | }, 494 | 495 | /** 496 | * @function sortGamesByTimestampAndID 497 | * @description Helper function to sort games by timestamp and ID. 498 | * 499 | * @param {object} game1 - Raw game information of first game. 500 | * @param {object} game2 - Raw game information of second game. 501 | * 502 | * @returns {number} Should game be before or after in the list? 503 | */ 504 | sortGamesByTimestampAndID(game1, game2) { 505 | if (game1.startTimeUTC === game2.startTimeUTC) { 506 | return game1.id > game2.id ? 1 : -1; 507 | } 508 | 509 | return game1.startTimeUTC > game2.startTimeUTC ? 1 : -1; 510 | }, 511 | 512 | /** 513 | * @function updateSchedule 514 | * @description Retrieves new schedule from API and sends a socket notification to the module. 515 | * @async 516 | * 517 | * @returns {void} 518 | */ 519 | async updateSchedule() { 520 | const schedule = await this.fetchSchedule(); 521 | schedule.sort(this.sortGamesByTimestampAndID); 522 | const season = this.computeSeasonDetails(schedule); 523 | 524 | const focusSchedule = schedule.filter(this.filterGameByFocus.bind(this)); 525 | 526 | const games = focusSchedule.map(this.parseGame.bind(this)); 527 | 528 | const rollOverGames = this.filterRollOverGames(games); 529 | 530 | this.setNextandLiveGames(rollOverGames); 531 | this.sendSocketNotification('SCHEDULE', { games: rollOverGames, season }); 532 | 533 | if (season.mode === 3 || games.length === 0) { 534 | 535 | const playoffData = await this.fetchPlayoffs(); 536 | const playoffSeries = this.computePlayoffDetails(playoffData).filter(s => s.round >= playoffData.defaultRound); 537 | 538 | this.sendSocketNotification('PLAYOFFS', playoffSeries); 539 | } 540 | }, 541 | 542 | /** 543 | * @function fetchOnLiveState 544 | * @description If there is a live game trigger updateSchedule. 545 | * @async 546 | * 547 | * @returns {void} 548 | */ 549 | fetchOnLiveState() { 550 | const hasLiveGames = this.liveGames.length > 0; 551 | const gameAboutToStart = this.nextGame && new Date().toISOString() > this.nextGame.timestamp; 552 | 553 | if (hasLiveGames || gameAboutToStart) { 554 | return this.updateSchedule(); 555 | } 556 | } 557 | }); 558 | --------------------------------------------------------------------------------