From 1ae06481d66717d343c10972dd92db65d95b99fb Mon Sep 17 00:00:00 2001 From: Lacy Morrow Date: Mon, 26 Feb 2018 19:00:32 -0800 Subject: [PATCH] v2.0.0 has a :star:Promise API:star: and :stars:more features:stars: --- .eslintrc.json | 222 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 4 +- README.md | 43 +++++--- cli.js | 98 +++++++++--------- index.js | 273 +++++++++++++++++++++++++++++++------------------ package.json | 15 ++- test.js | 72 +++++++++++-- 7 files changed, 554 insertions(+), 173 deletions(-) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..0fd6972 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,222 @@ +{ + "parserOptions": { + "ecmaVersion": 2017, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + + "env": { + "browser": true, + "es6": true, + "node": true + }, + + "plugins": [], + + "globals": { + "window": true, + "define": true, + "process": true, + "document": true, + "navigator": true + }, + + "rules": { + "accessor-pairs": "warn", + "arrow-spacing": ["error", { "before": true, "after": true }], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "camelcase": ["warn", { "properties": "always" }], + "comma-dangle": [ + "error", + { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "never" + } + ], + "comma-spacing": ["error", { "before": false, "after": true }], + "comma-style": ["error", "last"], + "constructor-super": "warn", + "curly": ["error", "multi-line"], + "dot-location": ["error", "property"], + "eol-last": "warn", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "func-call-spacing": ["error", "never"], + "generator-star-spacing": ["error", { "before": true, "after": true }], + "handle-callback-err": ["warn", "^(err|error)$"], + "indent": ["error", "tab"], + "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "new-cap": ["error", { "newIsCap": true, "capIsNew": false }], + "new-parens": "error", + "no-array-constructor": "error", + "no-caller": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-condition": ["error", { "checkLoops": false }], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-duplicate-imports": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "warn", + "no-extra-bind": "error", + "no-extra-boolean-cast": "warn", + "no-extra-parens": ["error", "functions"], + "no-fallthrough": "warn", + "no-floating-decimal": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-implied-eval": "error", + "no-inner-declarations": ["error", "functions"], + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": ["error", { "allowLoop": false, "allowSwitch": false }], + "no-lone-blocks": "error", + "no-mixed-operators": [ + "error", + { + "groups": [ + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": true + } + ], + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }], + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-symbol": "error", + "no-new-wrappers": "error", + "no-obj-calls": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-proto": "error", + "no-redeclare": "error", + "no-regex-spaces": "warn", + "no-return-assign": ["error", "except-parens"], + "no-return-await": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-tabs": "off", + "no-template-curly-in-string": "error", + "no-this-before-super": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-unexpected-multiline": "warn", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unused-expressions": [ + "error", + { + "allowShortCircuit": true, + "allowTernary": true, + "allowTaggedTemplates": true + } + ], + "no-unused-vars": [ + "error", + { "vars": "all", "args": "none", "ignoreRestSiblings": true } + ], + "no-use-before-define": [ + "error", + { "functions": true, "classes": true, "variables": true } + ], + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "object-property-newline": [ + "error", + { "allowMultiplePropertiesPerLine": true } + ], + "one-var": ["error", { "initialized": "never" }], + "operator-linebreak": [ + "error", + "after", + { "overrides": { "?": "before", ":": "before" } } + ], + "padded-blocks": [ + "error", + { "blocks": "always", "switches": "always", "classes": "always" } + ], + "prefer-promise-reject-errors": "error", + "quotes": [ + "error", + "single", + { "avoidEscape": true, "allowTemplateLiterals": true } + ], + "rest-spread-spacing": ["error", "never"], + "semi": ["warn", "never"], + "semi-spacing": ["error", { "before": false, "after": true }], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", "always"], + "space-in-parens": ["error", "always"], + "space-infix-ops": "error", + "space-unary-ops": ["error", { "words": true, "nonwords": false }], + "spaced-comment": [ + "error", + "always", + { + "line": { "markers": ["*package", "!", "/", ",", "="] }, + "block": { + "balanced": true, + "markers": [ + "*package", + "!", + ",", + ":", + "::", + "flow-include" + ], + "exceptions": ["*"] + } + } + ], + "symbol-description": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": ["error", "never"], + "unicode-bom": ["error", "never"], + "use-isnan": "error", + "valid-typeof": ["error", { "requireStringLiterals": true }], + "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], + "yield-star-spacing": ["error", "both"], + "yoda": ["error", "never"] + } +} diff --git a/.gitignore b/.gitignore index 9673c28..03eccf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .fuse* -.DS* \ No newline at end of file +.DS* + +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 2ab099e..b123a7b 100644 --- a/README.md +++ b/README.md @@ -15,25 +15,28 @@ $ npm install --save movie-art ## Usage ```js - var movieArt = require('movie-art'); +// Basic usage movieArt('Oceans Eleven', function (err, url) { console.log(url); //=> http://path/to/oceans_eleven_poster.jpg }); -movieArt('Oceans Eleven', '1960', function (err, url) { +// Usage with landscape orientation backdrop +movieArt('Oceans Eleven', {landscape: true}, function (err, url) { console.log(url); - //=> http://path/to/oceans_eleven_poster_1960.jpg + //=> http://path/to/oceans_eleven_backdrop.jpg }); -movieArt('Oceans Eleven', '1960', 'w92', function (err, url) { +// Usage with size and year options +movieArt('Oceans Eleven', {year: '1960', size: 'w92'}, function (err, url) { console.log(url); //=> http://path/to/oceans_eleven_poster_1960_small.jpg }); -movieArt('Star Trek: The Original Series', null, null,'tv', function (err, url) { +// Query television art +movieArt('Star Trek: The Original Series', {type: 'tv'}, function (err, url) { console.log(url); //=> http://path/to/star_trek_the_original_series_poster.jpg }); @@ -42,7 +45,7 @@ movieArt('Star Trek: The Original Series', null, null,'tv', function (err, url) ## API -### movieArt(movie [, year ] [, size ] [, type ], callback) +### movieArt(movie [, options] [, callback]) #### movie @@ -52,14 +55,19 @@ Type: `string` Movie to search for. +#### callback(err, url) + + +### Options + +A JavaScript object with the following properties + #### year Type: `string` Optional movie year. -#### callback(err, url) - #### size @@ -70,6 +78,7 @@ Call `movieArt(function(e){console.log(e);});` or run the CLI command with no ar *possible values at time of writing:* `w92`, `w154`, `w185`, `w342`, `w500`, `w780`, `original` + #### type Type: `string` @@ -77,7 +86,11 @@ Type: `string` The type of request: either `tv` or `movie`. Defaults to `movie`. -#### callback(err, url) +#### landscape + +Type: `boolean` + +Returns a wider, landscape orientation backdrop if true ## CLI @@ -94,11 +107,17 @@ $ npm install --global movie-art $ movie-art --help Usage - $ movie-art movie [year] [size] + $ movie-art movie [year] [size] [type] [landscape] + +Options + --year, -y Release date year + --size, -s Possible values: [w92, w154, w185, w342, w500, w780, original] + --type, -t Possible values: [tv, movie] + --landscape, -l Return wider backdrop image if true Example - $ movie-art 'Oceans Eleven' 1960 w92 - http://path/to/oceans_eleven_poster_1960_small.jpg + $ movie-art 'Oceans Eleven' --year 1960 --size w92 + //=> http://path/to/oceans_eleven_poster_1960_small.jpg ``` diff --git a/cli.js b/cli.js index afb1310..a5a2cd0 100644 --- a/cli.js +++ b/cli.js @@ -1,57 +1,55 @@ #!/usr/bin/env node -'use strict'; -var pkg = require('./package.json'); -var movieArt = require('./index'); -var movie = process.argv[2]; +'use strict' +const meow = require( 'meow' ) +const movieArt = require( './index' ) -var cb = function (err, url) { - if (err) { - console.error(err); - process.exit(1); - } - console.log(url); -} +const cli = meow( ` + Usage + $ movie-art movie [year] [size] [type] [landscape] -function help() { - console.log(pkg.description); - console.log(''); - console.log('Usage'); - console.log(' $ movie-art movie [year] [size] [type]'); - console.log(''); - console.log('Example'); - console.log(' $ movie-art \'Oceans Eleven\' 1960'); - console.log(' http://path/to/oceans_eleven_poster_1960.jpg'); - movieArt(null, null, null, cb); -} + Options + --year, -y Release date year + --size, -s Possible values: [w92, w154, w185, w342, w500, w780, original] + --type, -t Possible values: [tv, movie] + --landscape, -l Return wider backdrop image if true -if (process.argv.indexOf('-h') !== -1 || process.argv.indexOf('--help') !== -1) { - help(); - return; -} + Example + $ movie-art 'Oceans Eleven' --year 1960 + // => ... +`, { + flags: { + landscape: { + type: 'boolean', + alias: 'l' + }, + size: { + type: 'string', + alias: 's' + }, + type: { + type: 'string', + alias: 't' + }, + year: { + type: 'string', + alias: 'y' + } + } +} ) -if (process.argv.indexOf('-v') !== -1 || process.argv.indexOf('--version') !== -1) { - console.log(pkg.version); - return; +let opts = { + size: null, + type: null, + year: null, + landscape: false } -// Oh valiant if chain, I wish you weren't so useful -var argc = process.argv.length; -if (argc < 3){ - help(); -} else if (argc === 3){ - movieArt(movie, null, null, 'movie', cb); -} else if (argc === 4 && (!isNaN(parseFloat(process.argv[3])) && isFinite(process.argv[3]))){ - movieArt(movie, process.argv[3], null, 'movie', cb); -} else if (argc === 4 && ['movie', 'tv'].indexOf(process.argv[3])){ - movieArt(movie, null, null, process.argv[3], cb); -} else if (argc === 4){ - movieArt(movie, null, process.argv[3], 'movie', cb); -} else if (argc === 5 && !isNaN(parseFloat(process.argv[3])) && isFinite(process.argv[3]) && ['movie', 'tv'].indexOf(process.argv[4])){ - movieArt(movie, process.argv[3], null, process.argv[4], cb); -} else if (argc === 5 && !isNaN(parseFloat(process.argv[3])) && isFinite(process.argv[3])){ - movieArt(movie, process.argv[3], process.argv[4], 'movie', cb); -} else if (argc === 5){ - movieArt(movie, null, process.argv[3], process.argv[4], cb); -} else { - movieArt(movie, process.argv[3], process.argv[4], process.argv[5], cb); -} +if ( cli.flags.s ) opts.size = cli.flags.s +if ( cli.flags.t ) opts.type = cli.flags.t +if ( cli.flags.y ) opts.year = cli.flags.y +if ( cli.flags.l ) opts.landscape = cli.flags.l +if ( cli.input[1] ) opts.year = cli.flags.y +if ( !cli.input[0] ) cli.showHelp() + +movieArt( cli.input[0], opts ) + .then( console.log ) diff --git a/index.js b/index.js index 4bf57b7..929c3ab 100644 --- a/index.js +++ b/index.js @@ -1,104 +1,183 @@ 'use strict'; -module.exports = function (movie, year, size, type, cb) { - // We use the var `movie` instead of something generic like `query` because tv support was added later - var search = { - key: '9d2bff12ed955c7f1f74b83187f188ae', - protocol: require('https'), - cb: cb, - id: null, - year: null, - size: null, - sizes: null, - movie: movie, - options: { - host: 'api.themoviedb.org', - port: 443, - path: null - } - } - - if (typeof year === 'function') { - search.cb = year; - year = size = null; - } else if (typeof size === 'function') { - search.cb = size; - size = null; - } else if (typeof type === 'function') { - search.cb = type; - type = null; - } +( function ( root, cx ) { + + if ( typeof define === 'function' && define.amd ) { + + // AMD + define( ['fetch'], cx ) + + } else if ( typeof exports === 'object' ) { + + // Node, CommonJS-like + module.exports = cx( require( 'node-fetch' ) ) - if(type === null || (type != 'tv' && type != 'movie')) - { - type = 'movie'; - } - search.type = type; - - if (movie === null){ - getConfig(search); - } else if (typeof movie === 'function') { - search.cb = movie; - search.movie = null; - getConfig(search); - } else if (typeof movie !== 'string') { - throw new Error('Expected a string'); } else { - search.size = size; - search.year = year; - getConfig(search); + + // Browser globals (root is window) + root.movieInfo = cx( root.fetch ) + + } + +} )( this, function ( fetch ) { + + // TMDB key (public on purpose) + const key = '9d2bff12ed955c7f1f74b83187f188ae' + const base = 'https://api.themoviedb.org' + + function getConfiguration () { + + const url = + base + + encodeURI( '/3/configuration?api_key=' + key ) + + // Request + return fetch( url, { + method: 'GET' + } ) + .then( + function ( response ) { + + return response.json() + + }, + function ( error ) { + + return Promise.reject( error.message ) //= > String + + } + ) + .then( function ( json ) { + + if ( json && typeof json.status_message !== 'undefined' ) { + + return Promise.reject( new Error( 'JSON Error: ' + json.status_message ) ) + + } + + const baseURL = json.images.base_url + const sizes = json.images.poster_sizes + return { baseURL, sizes } + + } ) + } -}; - -function getConfig (search) { - var data = ''; - search.options.path = encodeURI('/3/configuration?api_key=' + search.key); - search.protocol.get(search.options, function(resp){ - resp.on('data', function(chunk){ - data += chunk; - }); - resp.on('end', function(){ - var json = JSON.parse(data); - if (typeof(json.status_message) !== 'undefined'){ - search.cb('Got error: ' + json.status_message); - } else { - search.baseURL = json.images.base_url; - search.sizes = json.images.poster_sizes; - if(search.movie === null){ - search.cb('Available sizes: ' + search.sizes); - } - getMovie(search); + + async function movieArt ( query, options, cb ) { + + // Massage inputs + if ( typeof query !== 'string' ) { + + throw new Error( 'Expected search to be a string' ) + + } else if ( typeof options === 'function' ) { + + cb = options + options = null + } - }); - }).on("error", function(e){ - search.cb('Got error: ' + e.message); - }); -} - -function getMovie(search) { - var data = ''; - search.options.path = encodeURI('/3/search/'+search.type+'?api_key=' + search.key + '&query=' + search.movie + ((search.year !== null) ? '&year='+search.year : '')); - search.protocol.get(search.options, function(resp){ - resp.on('data', function(chunk){ - data += chunk; - }); - resp.on('end', function(){ - var json = JSON.parse(data); - if (typeof(json.status_message) !== 'undefined'){ - search.cb('Got error: ' + json.status_message); - } else if (json.results.length === 0){ - search.cb('Got error: ' + 'No results found') - } else if (search.sizes.indexOf(search.size) !== -1) { - search.cb(null, encodeURI(search.baseURL + search.size + json.results[0].poster_path)); - } else { - search.cb(null, encodeURI(search.baseURL + search.sizes[search.sizes.length-1] + json.results[0].poster_path)); + + if ( typeof cb !== 'function' ) cb = null + + // Default options + const opts = Object.assign( { + year: null, + size: null, + type: 'movie', + landscape: false + }, options ) + + if ( opts.type === null || ( opts.type !== 'tv' && opts.type !== 'movie' ) ) { + + opts.type = 'movie' + } - }); - }).on("error", function(e){ - if(search.year !== null){ - search.year = null; - getMovie(search); - } else { - search.cb('Got error: ' + e.message); + + // Get configuration vars + const { baseURL, sizes } = await getConfiguration() + + // Assemble URL + const url = + base + + encodeURI( + '/3/search/' + + opts.type + + '?api_key=' + + key + + '&query=' + + query + + ( opts.year !== null ? '&year=' + opts.year : '' ) + ) + + // Request + const response = fetch( url, { + method: 'GET' + } ) + .then( + function ( response ) { + + return response.json() + + }, + function ( error ) { + + return Promise.reject( error.message ) //= > String + + } + ) + .then( function ( json ) { + + if ( json && typeof json.status_message !== 'undefined' ) { + + return Promise.reject( new Error( 'JSON Error: ' + json.status_message ) ) + + } + if ( json && json.results && json.results.length === 0 ) { + + // Retry failed search without year + if ( opts.year !== null ) { + + opts.year = null + return movieArt( query, opts, cb ) + + } else { + + return Promise.reject( new Error( 'Search Error: No results found' ) ) + + } + + } else { + + // Success + const image = opts.landscape ? json.results[0].backdrop_path : json.results[0].poster_path + if ( sizes.indexOf( opts.size ) !== -1 ) { + + return encodeURI( baseURL + opts.size + image ) + + } else { + + return encodeURI( baseURL + sizes[sizes.length - 1] + image ) + + } + + } + + } ) + .catch( error => error ) + + // Callback + if ( cb ) { + + return response.then( res => cb( null, res ), err => cb( err, null ) ) + } - }); -} + + // Promise + return response + + } + + // exposed public method + return movieArt + +} ) + diff --git a/package.json b/package.json index 654a4d4..a4304dd 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "movie-art", - "version": "1.0.7", - "description": "Get a movie poster image url in node: Oceans Eleven ➔ http://path/to/oceans_eleven_poster.jpg", + "version": "2.0.0", + "description": "Fetch art for a movie or tv show: 'Oceans Eleven' ➔ http://path/to/oceans_eleven_poster.jpg", "license": "MIT", "repository": "lacymorrow/movie-art", + "main": "index.js", + "browser": "index.js", "bin": { "movie-art": "cli.js" }, @@ -16,7 +18,7 @@ "node": ">=0.10.0" }, "scripts": { - "test": "mocha" + "test": "ava" }, "files": [ "cli.js", @@ -34,6 +36,11 @@ "path" ], "devDependencies": { - "mocha": "*" + "ava": "^0.25.0" + }, + "dependencies": { + "eslint": "^4.18.1", + "meow": "^4.0.0", + "node-fetch": "^2.0.0" } } diff --git a/test.js b/test.js index 70990ea..4bdc10f 100644 --- a/test.js +++ b/test.js @@ -1,9 +1,63 @@ -'use strict'; -var assert = require('assert'); -var movieArt = require('./index'); - -it('should return an image url', function () { - movieArt('crash', function (err, url) { - assert.strictEqual(url.indexOf('http'), 0); - }); -}); +'use strict' +import test from 'ava' +import movieArt from './index' + +test( 'returns a url ', async t => { + + t.plan( 1 ) + + const response = await movieArt( 'crash' ) + t.is( response.indexOf( 'http' ), 0, 'response is a URL' ) + +} ) + +test( 'returns a url with year ', async t => { + + t.plan( 1 ) + + const response = await movieArt( 'crash', {year: 2005} ) + t.is( response.indexOf( 'http' ), 0, 'response is a URL' ) + +} ) + +test( 'landscape returns a different url ', async t => { + + t.plan( 1 ) + + const response1 = await movieArt( 'crash' ) + const response2 = await movieArt( 'crash', { landscape: true } ) + t.not( response1, response2, 'portrait and landscape are different URLs' ) + +} ) + +test( 'returns a specific size ', async t => { + + t.plan( 1 ) + + const response = await movieArt( 'crash', { size: 'w92' } ) + t.not( response.indexOf( 'w92' ), -1, 'response is of requested size' ) + +} ) + +test( 'returns tv results ', async t => { + + t.plan( 1 ) + + const response = await movieArt( 'seinfeld', { type: 'tv' } ) + t.is( response.indexOf( 'http' ), 0, 'response is a URL' ) + +} ) + +test.cb( 'callback returns a url ', t => { + + t.plan( 1 ) + + movieArt( 'crash', ( err, res ) => { + + err && t.end( err ) + t.is( res.indexOf( 'http' ), 0, 'response is a URL' ) + t.end() + + } ) + +} )