├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── index.js ├── package.json ├── test ├── tv.js └── tv.mobil.js └── wercker.yml /.eslintrc: -------------------------------------------------------------------------------- 1 | # vi:syntax=json 2 | { 3 | "extends": "airbnb-base", 4 | "env": { 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "script", 11 | "ecmaFeatures": { 12 | "modules": false 13 | } 14 | }, 15 | "rules": { 16 | "strict": [2, "global"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hans Kristian Flaatten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NRK APIs for Node.JS 2 | 3 | [![Build status](https://img.shields.io/wercker/ci/55f0586d21e2917016104fd1.svg "Build status")](https://app.wercker.com/project/bykey/7235be533901a41e049d3bdc727ea66b) 4 | [![Codacy grade](https://img.shields.io/codacy/grade/df020582dccc4158af9a4c6f702dc8e2.svg "Codacy grade")](https://www.codacy.com/app/starefossen/node-nrk) 5 | [![Codacy coverage](https://img.shields.io/codacy/coverage/df020582dccc4158af9a4c6f702dc8e2.svg "Codacy coverage")](https://www.codacy.com/app/starefossen/node-nrk) 6 | [![NPM downloads](https://img.shields.io/npm/dm/nrk.svg "NPM downloads")](https://www.npmjs.com/package/nrk) 7 | [![NPM version](https://img.shields.io/npm/v/nrk.svg "NPM version")](https://www.npmjs.com/package/nrk) 8 | [![Node version](https://img.shields.io/node/v/nrk.svg "Node version")](https://www.npmjs.com/package/nrk) 9 | [![Dependency status](https://img.shields.io/david/Starefossen/node-nrk.svg "Dependency status")](https://david-dm.org/Starefossen/node-nrk) 10 | 11 | Node.JS wrapper for various (undocumented) API from the Norwegian Broadcast 12 | Corporation (NRK). Since NRK does not publicly document any of their APIs, this 13 | effort is by analyzing source code of various NRK.no sites, GitHub and other 14 | search engines, as well as analyzing traffic from various NRK applications. 15 | 16 | ## Requirements 17 | 18 | * Node.JS >= v4.0.0 19 | 20 | ## Install 21 | 22 | ``` 23 | $ npm install nrk --save 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | const nrk = require('nrk'); 30 | ``` 31 | 32 | ### Environment 33 | 34 | * `NRK_TV_API` (default: `https://tv.nrk.no`) 35 | * `NRK_TV_MOBIL` (default: `undefined`) 36 | * `NRK_PS_API` (default: `http://v8.psapi.nrk.no`) 37 | 38 | ### TV APIs 39 | 40 | API endpoints used by https://tv.nrk.no. Some API endpoints documentation can be 41 | found [here](http://v8.psapi.nrk.no/Help). 42 | 43 | * `nrk.tv.autocomplete()` 44 | * `nrk.tv.programs()` 45 | * `nrk.tv.program()` 46 | * `nrk.tv.series()` 47 | 48 | ### TV APIs (mobile) 49 | 50 | API endpoints used by the NRK TV app for iOS and Android. 51 | 52 | * `nrk.tv.mobil.categories()` 53 | * `nrk.tv.mobil.programs()` 54 | * `nrk.tv.mobil.series()` 55 | * `nrk.tv.mobil.search()` 56 | 57 | ## Legal 58 | 59 | NRK is a registered trademark of the Norwegian Broadcast Corporation which is 60 | not affiliated with this product. Content from NRK APIs may be copyrighted. 61 | 62 | ## [MIT Licensed](https://github.com/Starefossen/node-nrk/blob/master/LICENSE) 63 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | node: 5 | image: node:argon 6 | working_dir: /usr/src/app 7 | volumes: 8 | - ".:/usr/src/app" 9 | environment: 10 | - NODE_ENV=development 11 | - NPM_CONFIG_LOGLEVEL=silent 12 | - NPM_CONFIG_PROGRESS=false 13 | - NPM_CONFIG_SPIN=false 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsonist = require('jsonist'); 4 | 5 | const nrkTvApi = process.env.NRK_TV_API || 'https://tv.nrk.no'; 6 | const nrkPsApi = process.env.NRK_PS_API || 'http://v8.psapi.nrk.no'; 7 | const nrkTvMobilApi = process.env.NRK_TV_MOBIL || 'https://tvapi.nrk.no/v1'; 8 | 9 | module.exports.headers = function headers(agent) { 10 | if (agent === 'app') { 11 | return { 12 | headers: { 13 | 'User-Agent': 'NRK%20TV/43 CFNetwork/711.5.6 Darwin/14.0.0', 14 | accept: '*/*', 15 | 'app-version-ios': '43', 16 | 'Accept-Language': 'en-us', 17 | }, 18 | }; 19 | } 20 | 21 | return { 22 | headers: { 23 | 'user-agent': [ 24 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X)', 25 | 'AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0', 26 | 'Mobile/10A5376e Safari/8536.25', 27 | ].join(' '), 28 | accept: 'application/json, text/javascript, */*; q=0.01', 29 | 'x-requested-with': 'XMLHttpRequest', 30 | }, 31 | }; 32 | }; 33 | 34 | module.exports.tv = {}; 35 | module.exports.tv.mobil = {}; 36 | 37 | /** 38 | * Get TV categories 39 | * 40 | * @param cb - callback function (err, data, res) 41 | * 42 | * @return Array of categories 43 | */ 44 | module.exports.tv.mobil.categories = function categories(cb) { 45 | const url = `${nrkTvMobilApi}/categories`; 46 | jsonist.get(url, module.exports.headers('app'), cb); 47 | }; 48 | 49 | /** 50 | * Get one ore more TV program 51 | * 52 | * @param pid - program id 53 | * @param cb - callback function (err, data, res) 54 | * 55 | * @return Array of programs 56 | */ 57 | module.exports.tv.mobil.programs = function programs(pid, cb) { 58 | let url; 59 | 60 | if (!pid) { 61 | url = `${nrkTvMobilApi}/programs`; 62 | } else { 63 | url = `${nrkTvMobilApi}/programs/${pid}`; 64 | } 65 | 66 | jsonist.get(url, module.exports.headers('app'), cb); 67 | }; 68 | 69 | /** 70 | * Get one ore more TV series 71 | * 72 | * @param sid - series id 73 | * @param cb - callback function (err, data, res) 74 | * 75 | * @return Array of series 76 | */ 77 | module.exports.tv.mobil.series = function series(sid, cb) { 78 | let url; 79 | 80 | if (!sid) { 81 | url = `${nrkTvMobilApi}/series`; 82 | } else { 83 | url = `${nrkTvMobilApi}/series/${sid}`; 84 | } 85 | 86 | jsonist.get(url, module.exports.headers('app'), cb); 87 | }; 88 | 89 | /** 90 | * Search for TV programs 91 | * 92 | * @param str - string to search for 93 | * @param cb - callback function (err, data, res) 94 | * 95 | * @return Array of matching programs 96 | */ 97 | module.exports.tv.mobil.search = function search(str, cb) { 98 | const url = `${nrkTvMobilApi}/search/${encodeURIComponent(str)}`; 99 | jsonist.get(url, module.exports.headers('app'), cb); 100 | }; 101 | 102 | /** 103 | * Get programs matching string (autocomplete) 104 | * 105 | * @param str - string to autcomplete 106 | * @param cb - callback function (err, data, res) 107 | * 108 | * @return Array of matching programs 109 | */ 110 | module.exports.tv.autocomplete = function autocomplete(str, cb) { 111 | const url = `${nrkTvApi}/autocomplete?query=${str}`; 112 | jsonist.get(url, module.exports.headers(), cb); 113 | }; 114 | 115 | /** 116 | * Get all programs starting with a given letter 117 | * 118 | * @param letter - letter to filter programs ("a"…"z" + "0-9") 119 | * @param category - optionaly category name 120 | * @param cb - callback function (err, data, res) 121 | * 122 | * @return Array of matching programs 123 | */ 124 | module.exports.tv.programs = function programs(letter, category, cb) { 125 | let url; 126 | 127 | if (category) { 128 | url = `${nrkTvApi}/programmer/${category}/${letter}`; 129 | } else { 130 | url = `${nrkTvApi}/programmer/${letter}`; 131 | } 132 | 133 | jsonist.get(url, module.exports.headers(), cb); 134 | }; 135 | 136 | /** 137 | * Get program / episode details by ID 138 | * 139 | * @param pid - program id 140 | * @param cb - callback function (err, data, res) 141 | * 142 | * @return Object with details 143 | */ 144 | module.exports.tv.program = function program(pid, cb) { 145 | const url = `${nrkPsApi}/mediaelement/${pid}`; 146 | jsonist.get(url, module.exports.headers(), cb); 147 | }; 148 | 149 | /** 150 | * Get series details by ID 151 | * 152 | * @param sid - series id 153 | * @param cb - callback function (err, data, res) 154 | * 155 | * @return Object with details 156 | */ 157 | module.exports.tv.series = function series(sid, cb) { 158 | const url = `${nrkPsApi}/series/${sid}/latestornextepisode/mediaelement`; 159 | jsonist.get(url, module.exports.headers(), cb); 160 | }; 161 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nrk", 3 | "version": null, 4 | "description": "Node.JS wrapper of various (undocumented) NRK.no (Norwegian Broadcast Corporation) APIs", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js", 8 | "test" 9 | ], 10 | "scripts": { 11 | "codacy-coverage": "codacy-coverage", 12 | "cover": "istanbul cover --report lcovonly ./node_modules/.bin/_mocha -- test", 13 | "lint": "eslint index.js test.js", 14 | "nsp": "nsp check", 15 | "semantic-release": "semantic-release", 16 | "test": "mocha -bc --check-leaks -R tap test", 17 | "test:watch": "mocha -wbc --check-leaks -R progress test", 18 | "greenkeeper-postpublish": "greenkeeper-postpublish" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Starefossen/node-nrk.git" 23 | }, 24 | "keywords": [ 25 | "NRK", 26 | "NRK.no", 27 | "API", 28 | "TV" 29 | ], 30 | "author": "Hans Kristian Flaatten ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/Starefossen/node-nrk/issues" 34 | }, 35 | "homepage": "https://github.com/Starefossen/node-nrk#readme", 36 | "dependencies": { 37 | "jsonist": "^1.3.0" 38 | }, 39 | "devDependencies": { 40 | "codacy-coverage": "^2.0.0", 41 | "eslint": "^3.3.0", 42 | "eslint-config-airbnb-base": "^5.0.2", 43 | "eslint-plugin-import": "^1.13.0", 44 | "greenkeeper-postpublish": "^1.0.0", 45 | "istanbul": "^0.4.3", 46 | "joi": "^9.0.0", 47 | "mocha": "^3.0.0", 48 | "nsp": "^2.4.0", 49 | "semantic-release": "^4.3.5" 50 | }, 51 | "engines": { 52 | "node": ">=4.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/tv.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Joi = require('joi'); 5 | const nrk = require('../'); 6 | 7 | describe('autocomplete()', function describe() { 8 | it('returns autocomplete results', function it(done) { 9 | this.timeout(10000); 10 | 11 | nrk.tv.autocomplete('Side', function autocompleteCb(err, data, resp) { 12 | assert.ifError(err); 13 | assert.equal(resp.statusCode, 200); 14 | 15 | const schema = Joi.object().keys({ 16 | searchTerm: Joi.string(), 17 | success: Joi.boolean(), 18 | errorMessage: Joi.string().allow(null), 19 | exception: Joi.string().allow(null), 20 | result: Joi.array().items(Joi.object().keys({ 21 | fields: Joi.string().allow(null), 22 | _source: Joi.object().keys({ 23 | seriesId: Joi.string().allow(null), 24 | seasonId: Joi.string().allow(null), 25 | seasonNumber: Joi.number().integer().allow(null), 26 | episodeNumber: Joi.number().integer().allow(null), 27 | seriesTitle: Joi.string(), 28 | seasonDisplayType: Joi.number().integer(), 29 | episodeNumberOrDate: Joi.string().allow(null), 30 | episodeNumberWithTotal: Joi.string().allow(null), 31 | searchHitDisplayTitle: Joi.string(), 32 | episodeTitle: Joi.string(), 33 | hideInSearchResults: Joi.boolean(), 34 | aldersgrense: Joi.number().integer(), 35 | hosts: Joi.array().allow(null), 36 | seasons: Joi.array().allow(null), 37 | duration: Joi.number().integer(), 38 | mentionedList: Joi.array().allow(null), 39 | originalTitle: Joi.string().allow(null), 40 | otherTitles: Joi.array().allow(null), 41 | indexPoints: Joi.array().allow(null), 42 | programUrlMetadata: Joi.string().allow(null), 43 | usageRights: Joi.object().keys({ 44 | isGeoBlocked: Joi.boolean(), 45 | availableFrom: Joi.string(), 46 | availableTo: Joi.string(), 47 | hasRightsNow: Joi.boolean(), 48 | }), 49 | airDate: Joi.string().allow(null), 50 | id: Joi.string(), 51 | title: Joi.string(), 52 | description: Joi.string().allow(''), 53 | url: Joi.string(), 54 | category: Joi.string().allow(null), 55 | hasRights: Joi.boolean(), 56 | sourceMedium: Joi.string().allow(null), 57 | subjects: Joi.array().allow(null), 58 | image: Joi.object(), 59 | contributors: Joi.array().allow(null), 60 | availableInSuper: Joi.boolean(), 61 | }), 62 | _index: Joi.string(), 63 | _score: Joi.number(), 64 | score: Joi.number(), 65 | _type: Joi.string(), 66 | _version: Joi.string().allow(null), 67 | _id: Joi.string(), 68 | sort: Joi.string().allow(null), 69 | highlight: Joi.string().allow(null), 70 | highlights: Joi.object(), 71 | _explanation: Joi.string().allow(null), 72 | })), 73 | }); 74 | 75 | Joi.validate(data, schema, done); 76 | }); 77 | }); 78 | }); 79 | 80 | describe('program()', function describe() { 81 | it('returns data for program', function it(done) { 82 | this.timeout(10000); 83 | 84 | nrk.tv.program('NNFA41014515', function programCb(err, data, resp) { 85 | assert.ifError(err); 86 | assert.equal(resp.statusCode, 200); 87 | 88 | const schema = Joi.object().keys({ 89 | _links: Joi.object(), 90 | id: Joi.string(), 91 | title: Joi.string(), 92 | description: Joi.string(), 93 | mediaElementType: Joi.string(), 94 | mediaType: Joi.string(), 95 | image: Joi.object(), 96 | images: Joi.object(), 97 | mediaUrl: Joi.string(), 98 | mediaAssets: Joi.array().items(Joi.object().keys({ 99 | url: Joi.string(), 100 | duration: Joi.string(), 101 | carrierId: Joi.string(), 102 | webVttSubtitlesUrl: Joi.string().allow(null), 103 | timedTextSubtitlesUrl: Joi.string().allow(null), 104 | bufferDuration: Joi.number().allow(null), 105 | })), 106 | bitrateInfo: Joi.object().keys({ 107 | startIndex: Joi.number().integer(), 108 | maxIndex: Joi.number().integer(), 109 | }), 110 | playerType: Joi.string(), 111 | flashPlayerVersion: Joi.string(), 112 | flashPluginVersion: Joi.string(), 113 | isAvailable: Joi.boolean(), 114 | messageType: Joi.string(), 115 | mediaAnalytics: Joi.object(), 116 | scoresStatistics: Joi.object(), 117 | convivaStatistics: Joi.object(), 118 | messageId: Joi.string().allow(null), 119 | isLive: Joi.boolean(), 120 | usageRights: Joi.object().keys({ 121 | isGeoBlocked: Joi.boolean(), 122 | availableFrom: Joi.string(), 123 | availableTo: Joi.string(), 124 | hasRightsNow: Joi.boolean(), 125 | }), 126 | akamaiBeacon: Joi.string(), 127 | liveBufferStartTime: Joi.string().allow(null), 128 | fullTitle: Joi.string(), 129 | mainTitle: Joi.string(), 130 | legalAge: Joi.string(), 131 | relativeOriginUrl: Joi.string(), 132 | duration: Joi.string(), 133 | shortIndexPoints: Joi.array(), 134 | hasSubtitles: Joi.boolean(), 135 | subtitlesDefaultOn: Joi.boolean(), 136 | subtitlesUrlPath: Joi.string().allow(null), 137 | seriesId: Joi.string(), 138 | seriesTitle: Joi.string(), 139 | episodeNumberOrDate: Joi.string(), 140 | externalEmbeddingAllowed: Joi.boolean(), 141 | startNextEpisode: Joi.number().integer(), 142 | }); 143 | 144 | Joi.validate(data, schema, done); 145 | }); 146 | }); 147 | }); 148 | 149 | describe('series()', function describe() { 150 | it('returns data for series', function it(done) { 151 | this.timeout(10000); 152 | 153 | nrk.tv.series('side-om-side', function seriesCb(err, data, resp) { 154 | assert.ifError(err); 155 | assert.equal(resp.statusCode, 200); 156 | 157 | done(); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/tv.mobil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Joi = require('joi'); 5 | const nrk = require('../'); 6 | 7 | before(function before(done) { 8 | this.timeout(10000); 9 | 10 | nrk.tv.mobil.categories(done); 11 | }); 12 | 13 | describe('categories()', function describe() { 14 | it('returns list of categories', function it(done) { 15 | this.timeout(10000); 16 | 17 | nrk.tv.mobil.categories(function categoriesCb(err, data, resp) { 18 | assert.ifError(err); 19 | 20 | assert.equal(resp.statusCode, 200); 21 | 22 | const schema = Joi.array().items(Joi.object().keys({ 23 | categoryId: Joi.string().example('dokumentar'), 24 | displayValue: Joi.string().example('Dokumentar'), 25 | availableFilters: Joi.array().items(Joi.string().valid( 26 | ['POPULAR', 'RECOMMENDED', 'RECENT'] 27 | )), 28 | })); 29 | 30 | Joi.validate(data, schema, done); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('programs()', function describe() { 36 | it('returns list of programs', function it(done) { 37 | this.timeout(10000); 38 | 39 | nrk.tv.mobil.programs(null, function programsCb(err, data, resp) { 40 | assert.ifError(err); 41 | assert.equal(resp.statusCode, 200); 42 | 43 | const schema = Joi.array().items(Joi.object().keys({ 44 | title: Joi.string().example('Diktaturet'), 45 | description: Joi.string().allow(''), 46 | episodeNumberOrDate: Joi.string().example('7:8'), 47 | mediaUrl: Joi.string(), 48 | programId: Joi.string().example('mynt14000715'), 49 | seriesId: Joi.string().example('diktaturet'), 50 | seriesImageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 51 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 52 | plugImageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 53 | source: Joi.string().example('plugs'), 54 | category: Joi.object().keys({ 55 | categoryId: Joi.string().example('underholdning'), 56 | displayValue: Joi.string().example('Underholdning'), 57 | priority: Joi.number().integer(), 58 | }), 59 | seasonId: Joi.number().integer(), 60 | promoText: Joi.string(), 61 | usageRights: Joi.object().keys({ 62 | availableFrom: Joi.number().integer(), 63 | availableTo: Joi.number().integer(), 64 | hasNoRights: Joi.boolean(), 65 | geoblocked: Joi.boolean(), 66 | }), 67 | liveProgramMetadata: Joi.object().keys({ 68 | startTime: Joi.number().integer(), 69 | endTime: Joi.number().integer(), 70 | nextProgram: Joi.string(), 71 | }), 72 | plugTitle: Joi.string(), 73 | legalAge: Joi.string().valid(['A', '6', '9', '12', '15', '18']), 74 | legalAgeDisplayValue: Joi.string(), 75 | labelText: Joi.string(), 76 | channelId: Joi.string(), 77 | parts: Joi.array().items(Joi.object()), 78 | epgEntries: Joi.array().items(Joi.object()), 79 | size: Joi.string().valid('small', 'medium', 'large', 'random'), 80 | applicationType: Joi.string().example('bigscreen'), 81 | duration: Joi.number().integer(), 82 | isAvailable: Joi.boolean(), 83 | })); 84 | 85 | Joi.validate(data, schema, done); 86 | }); 87 | }); 88 | 89 | it('returns single program', function it(done) { 90 | this.timeout(10000); 91 | 92 | nrk.tv.mobil.programs('mynt14000715', function programsCb(err, data, resp) { 93 | assert.ifError(err); 94 | assert.equal(resp.statusCode, 200); 95 | 96 | const schema = Joi.object().keys({ 97 | title: Joi.string().example('Diktaturet'), 98 | description: Joi.string().allow(''), 99 | episodeNumberOrDate: Joi.string().example('7:8'), 100 | mediaUrl: Joi.string(), 101 | programId: Joi.string().example('mynt14000715'), 102 | seriesId: Joi.string().example('diktaturet'), 103 | seriesImageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 104 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 105 | category: Joi.object().keys({ 106 | categoryId: Joi.string().example('underholdning'), 107 | displayValue: Joi.string().example('Underholdning'), 108 | priority: Joi.number().integer(), 109 | }), 110 | seasonId: Joi.number().integer(), 111 | parts: Joi.array().items(Joi.object()), 112 | usageRights: Joi.object().keys({ 113 | availableFrom: Joi.number().integer(), 114 | availableTo: Joi.number().integer(), 115 | hasNoRights: Joi.boolean(), 116 | geoblocked: Joi.boolean(), 117 | }), 118 | legalAge: Joi.string().valid(['A', '6', '9', '12', '15', '18']), 119 | legalAgeDisplayValue: Joi.string(), 120 | numberOfAvailableEpisodes: Joi.number().integer(), 121 | epgEntries: Joi.array(), 122 | duration: Joi.number().integer(), 123 | isAvailable: Joi.boolean(), 124 | fullTitle: Joi.string().example('Diktaturet 7:8'), 125 | shortIndexPoints: Joi.array().items(Joi.object().keys({ 126 | title: Joi.string().example('Velkommen til Diktaturet'), 127 | startPoint: Joi.number().integer(), 128 | partId: Joi.number().integer(), 129 | part: Joi.number().integer(), 130 | })), 131 | hasSubtitles: Joi.boolean(), 132 | hasReview: Joi.boolean(), 133 | startNextEpisode: Joi.number(), 134 | series: Joi.object().keys({ 135 | seriesId: Joi.string().example('diktaturet'), 136 | title: Joi.string().example('Diktaturet'), 137 | description: Joi.string(), 138 | seasonIds: Joi.array().items(Joi.object().keys({ 139 | id: Joi.number().integer(), 140 | name: Joi.string().example('Sesong 1'), 141 | })), 142 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 143 | category: Joi.object().keys({ 144 | categoryId: Joi.string().example('underholdning'), 145 | displayValue: Joi.string().example('Underholdning'), 146 | priority: Joi.number().integer(), 147 | }), 148 | programs: Joi.array().items(Joi.object().keys({ 149 | title: Joi.string().example('Diktaturet'), 150 | description: Joi.string(), 151 | episodeNumberOrDate: Joi.string().example('7:8'), 152 | programId: Joi.string().example('mynt14000715'), 153 | seriesId: Joi.string().example('diktaturet'), 154 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 155 | category: Joi.object().keys({ 156 | categoryId: Joi.string().example('underholdning'), 157 | displayValue: Joi.string().example('Underholdning'), 158 | priority: Joi.number().integer(), 159 | }), 160 | seasonId: Joi.number().integer(), 161 | parts: Joi.array().items(Joi.object()), 162 | usageRights: Joi.object().keys({ 163 | availableFrom: Joi.number().integer(), 164 | availableTo: Joi.number().integer(), 165 | hasNoRights: Joi.boolean(), 166 | geoblocked: Joi.boolean(), 167 | }), 168 | legalAge: Joi.string().valid(['A', '6', '9', '12', '15', '18']), 169 | legalAgeDisplayValue: Joi.string(), 170 | duration: Joi.number().integer(), 171 | isAvailable: Joi.boolean(), 172 | })), 173 | }), 174 | contributors: Joi.array(), 175 | relativeOriginUrl: Joi.string(), 176 | more: Joi.array().label('Related Shows'), 177 | statistics: Joi.object(), 178 | }); 179 | 180 | Joi.validate(data, schema, done); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('series()', function describe() { 186 | it('returns single series', function it(done) { 187 | this.timeout(10000); 188 | 189 | nrk.tv.mobil.series('solsikkesmuget-19', function seriesCb(err, data, resp) { 190 | assert.ifError(err); 191 | assert.equal(resp.statusCode, 200); 192 | 193 | const schema = Joi.object().keys({ 194 | seriesId: Joi.string().example('solsikkesmuget-19'), 195 | title: Joi.string().example('Solsikkesmuget 19'), 196 | description: Joi.string(), 197 | seasonIds: Joi.array().items(Joi.object().keys({ 198 | id: Joi.number().integer().example(35359), 199 | name: Joi.string().example('Sesong 1'), 200 | })), 201 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 202 | category: Joi.object().keys({ 203 | categoryId: Joi.string().example('barn'), 204 | displayValue: Joi.string().example('Barn'), 205 | }), 206 | programs: Joi.array().items(Joi.object().keys({ 207 | title: Joi.string().example('Solsikkesmuget 19'), 208 | description: Joi.string(), 209 | episodeNumberOrDate: Joi.string().example('24:24'), 210 | programId: Joi.string().example('msui12008313'), 211 | seriesId: Joi.string().example('solsikkesmuget-19'), 212 | parts: Joi.array().items(Joi.object()), 213 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 214 | category: Joi.object().keys({ 215 | categoryId: Joi.string().example('barn'), 216 | displayValue: Joi.string().example('Barn'), 217 | }), 218 | seasonId: Joi.number().integer().example(35359), 219 | usageRights: Joi.object().keys({ 220 | availableFrom: Joi.number().integer(), 221 | availableTo: Joi.number().integer(), 222 | hasNoRights: Joi.boolean(), 223 | geoblocked: Joi.boolean(), 224 | }).allow(null), 225 | legalAge: Joi.string().valid(['A', '6', '9', '12', '15', '18']), 226 | legalAgeDisplayValue: Joi.string(), 227 | duration: Joi.number().allow(null), 228 | isAvailable: Joi.boolean(), 229 | })), 230 | }); 231 | 232 | Joi.validate(data, schema, done); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('search()', function describe() { 238 | it('returns matching list of programs', function it(done) { 239 | this.timeout(10000); 240 | 241 | nrk.tv.mobil.search('Side om side', function searchCb(err, data, resp) { 242 | assert.ifError(err); 243 | assert.equal(resp.statusCode, 200); 244 | 245 | const schema = Joi.object().keys({ 246 | metaData: Joi.object(), 247 | hits: Joi.array().items(Joi.object().keys({ 248 | type: Joi.string().valid('serie', 'episode', 'program'), 249 | hit: Joi.object().keys({ 250 | objectId: Joi.string().example('side-om-side'), 251 | title: Joi.string().example('Side om side'), 252 | description: Joi.string(), 253 | imageId: Joi.string().regex(/^[a-zA-Z0-9_-]+$/), 254 | seasons: Joi.any().allow(null), 255 | category: Joi.object(), 256 | duration: Joi.number().allow(null), 257 | expires: Joi.any().allow(null), 258 | seriesId: Joi.string().allow(null).example('side-om-side'), 259 | seasonNumber: Joi.string().allow(null), 260 | seriesTitle: Joi.string().allow(null), 261 | episodeNumberOrDate: Joi.string().allow(null).example('7:8'), 262 | episodeTitle: Joi.string().allow(null), 263 | aldersgrense: Joi.number().integer().allow(null), 264 | usageRights: Joi.object().keys({ 265 | availableFrom: Joi.number().integer(), 266 | availableTo: Joi.number().integer(), 267 | hasNoRights: Joi.boolean(), 268 | geoblocked: Joi.boolean(), 269 | }).allow(null), 270 | programId: Joi.string().allow(null).example('mynt14000715'), 271 | }), 272 | highlights: Joi.array().allow(null), 273 | })), 274 | }); 275 | 276 | Joi.validate(data, schema, done); 277 | }); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: node:argon 2 | 3 | build: 4 | steps: 5 | - script: 6 | name: echo nodejs information 7 | code: | 8 | echo "node version $(node -v) running" 9 | echo "npm version $(npm -v) running" 10 | 11 | - npm-install 12 | 13 | - script: 14 | name: lint 15 | code: npm run lint 16 | 17 | - npm-test 18 | 19 | - script: 20 | name: test coverage 21 | code: | 22 | npm run cover 23 | cat ./coverage/lcov.info | npm run codacy-coverage 24 | 25 | - script: 26 | name: node security project 27 | code: | 28 | npm run nsp 29 | 30 | after-steps: 31 | - turistforeningen/slack-notifier: 32 | url: $SLACK_WEBHOOK_URL 33 | 34 | deploy: 35 | steps: 36 | # Rebuild node_modules to fix broken symlinks 37 | # https://github.com/wercker/docs/issues/310 38 | - script: 39 | name: npm rebuild 40 | code: npm rebuild 41 | 42 | - script: 43 | name: semantic release pre 44 | code: npm run semantic-release -- pre 45 | 46 | - turistforeningen/npm-publish 47 | 48 | - script: 49 | name: greenkeeper postpublish hook 50 | code: npm run greenkeeper-postpublish 51 | 52 | - script: 53 | name: semantic release post 54 | code: npm run semantic-release -- post 55 | 56 | after-steps: 57 | - turistforeningen/slack-notifier: 58 | url: $SLACK_WEBHOOK_URL 59 | --------------------------------------------------------------------------------