diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c4c2f75 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "extends": ["motley"], + "rules": { + "no-console": 0, + "global-require": 0, + } +} diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 79a8b81..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,68 +0,0 @@ -parserOptions: - ecmaVersion: 6 - sourceType: "module" - -env: - es6: true - node: true - -rules: - comma-dangle: [2, "never"] - no-cond-assign: 2 - no-console: 0 - no-constant-condition: 2 - no-control-regex: 2 - no-debugger: 2 - no-dupe-keys: 2 - no-empty: 2 - no-empty-character-class: 2 - no-ex-assign: 2 - no-extra-boolean-cast: 2 - no-extra-parens: [2, "functions"] - no-extra-semi: 2 - no-func-assign: 2 - no-inner-declarations: 2 - no-invalid-regexp: 2 - no-irregular-whitespace: 2 - no-negated-in-lhs: 2 - no-obj-calls: 2 - no-regex-spaces: 2 - no-sparse-arrays: 2 - no-unreachable: 2 - use-isnan: 2 - valid-jsdoc: [1, { - requireReturn: false - }] - valid-typeof: 2 - no-unexpected-multiline: 2 - eol-last: 2 - - #Best Practices - - block-scoped-var: 2 - eqeqeq: 2 - no-eval: 2 - no-implied-eval: 2 - no-new-func: 2 - no-multi-str: 2 - no-octal: 2 - no-useless-call: 2 - no-void: 2 - radix: 2 - - #Variables - no-delete-var: 2 - no-undef: 2 - no-undefined: 2 - no-unused-vars: 2 - - #NodeJS - callback-return: 2 - handle-callback-err: 2 - no-mixed-requires: 2 - - #Stylistic - array-bracket-spacing: [2, "never"] - camelcase: 0 - no-mixed-spaces-and-tabs: 2 - keyword-spacing: 1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4f9cd7a..4ec6bb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .config.js -node_modules/ \ No newline at end of file +node_modules/ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index ba09c2e..5d4b4aa 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Ascii-shot -Get your latest Instagram shot as an ASCII version to `stdout`. +:rainbow: Get an users instagram feed as an ASCII version to `stdout`. -![ASCIIfeed](./assets/example.png) +![Ascii-shot](./assets/example.png) ## Installing @@ -12,34 +12,17 @@ Install `ascii-shot` with `npm` npm install ascii-shot -g ``` -## Usage (CLI) +## Usage -### ascii-shot -Prints your latest Instagram shot as an ASCII version to `stdout`. - -``` bash -ascii-shot ``` + Usage + $ ascii-shot -## Setting up -Instagram API is quite restrictive, so you'll need to jump through some hoops. - -1. Setup your client here: https://www.instagram.com/developer/ -2. Set your `INSTAGRAM_CLIENT_ID`, `INSTAGRAM_CLIENT_SECRET` and `INSTAGRAM_REDIRECT_URI` as environment variables. For example - -``` bash - set INSTAGRAM_CLIENT_SECRET=your_secret_here + Examples + $ ascii-shot petetnt ``` -When you run `ascii-shot`, it will automatically fetch your `access_token` and show your newest Instagram shot. If it's your first time running the script, it will most likely ask you to authoritize the app. - -After you have successfully gained your `access_token`, check the console and save it as an environment variable `INSTAGRAM_ACCESS_TOKEN` to skip further need to get it again. (The tokens, however, might expire at some point so you might need to get another later). - -## Why would I want this? -¯\\\_(ツ)_/¯ - -Like I said, the API is quite restrictive and this is pretty much all ASCII fun you can have without breaking all or some of the rules. - +And then use spacebar to fetch more images. Any other key will exit the program. -## License and acknowledgements -MIT Copyright © 2016 Pete Nykänen +## License +MIT \ No newline at end of file diff --git a/VERSIONS.md b/VERSIONS.md new file mode 100644 index 0000000..8e1358e --- /dev/null +++ b/VERSIONS.md @@ -0,0 +1,7 @@ +# 1.0.0 + +- Complete rewrite using the semi-public API `username/media`. Fixes #1. + +# 0.0.1 - 0.0.3 + +- Legacy version using authenticated API communication. \ No newline at end of file diff --git a/assets/example.png b/assets/example.png index 497035a..df438fb 100644 Binary files a/assets/example.png and b/assets/example.png differ diff --git a/bin/ascii-shot b/bin/ascii-shot old mode 100644 new mode 100755 index 72e55b1..ca46c4f --- a/bin/ascii-shot +++ b/bin/ascii-shot @@ -1,3 +1,18 @@ -#!/usr/bin/env node --harmony -'use strict'; -require("../lib"); \ No newline at end of file +#!/usr/bin/env node +const meow = require("meow"); +const cli = meow(` + Usage + $ ascii-shot + + Examples + $ ascii-shot petetnt +`); + +const username = cli.input[0]; + +if (!username) { + console.error(" Error: You must pass an username"); + cli.showHelp(); +} else { + require("../lib/ascii-shot")(username); +} diff --git a/lib/ascii-shot.js b/lib/ascii-shot.js index 1b90d40..a480d85 100644 --- a/lib/ascii-shot.js +++ b/lib/ascii-shot.js @@ -1,42 +1,151 @@ const EOL = require("os").EOL; const fetch = require("node-fetch"); const cheerio = require("cheerio"); -const ansi = require('ansi'); +const ansi = require("ansi"); const cursor = ansi(process.stdout); /** * Takes a HTML-formatted text and parses the containing ASCII image to text. + * Also writes the attached metadata to console. * @param {String} text - HTML body + * @param {object} image - Object containing related metadata. + * @returns {Promise.resolve} - Resolved promise for chaining. */ -function _parseAsciiImage(text) { - const $ = cheerio.load(text); - const imageParts = $("font").children(); - - imageParts.each((index, elem) => { - if (elem.name === "span") { - const $elem = $(elem); - cursor.red().write($elem.text()).reset(); - } +function writeInstagramShot(text, image) { + const $ = cheerio.load(text); + const imageParts = $("font").children(); - if (elem.name === "br") { - cursor.write(EOL); - } - }); + const { + likes, + date, + caption, + } = image; + + cursor.write(`${"-".repeat(52)}${EOL}`); + cursor.write(`Caption: ${caption}${EOL}`); + cursor.write(`Created at: ${date}${EOL}`); + cursor.write(`Likes: ${likes}${EOL}`); + cursor.write(`${"-".repeat(52)}${EOL}`); + + imageParts.each((index, elem) => { + if (elem.name === "span") { + const $elem = $(elem); + cursor.red().write($elem.text()).reset(); + } + + if (elem.name === "br") { + cursor.write(EOL); + } + }); + + return Promise.resolve(); } +/** + * Formats the itemsArray to caption/likes/imageurl format + * @param {Array} itemsArray - Array of instagram items + * @returns {Array} - Array of image urls + */ +const formatItemsArray = (itemsArray) => ( + itemsArray.map((item) => { + const { + images, + likes, + caption, + created_time: createdTime, + } = item; + + const { + standard_resolution: standardResolution, + low_resolution: lowResolution, + } = images; + + let url = null; + + if (lowResolution) { + url = lowResolution.url.split("?")[0]; + } else if (standardResolution) { + url = standardResolution.url.split("?")[0]; + } + + return { + likes: likes.count, + caption: caption ? caption.text : "", + date: new Date(createdTime * 1000), + url, + }; + }).sort((a, b) => a.date.getTime() - b.date.getTime()) +); + +/** + * Writes an image to the console + * @param {String} imageUrl - Url of the image to fetch + * @returns {Promise} - Res + */ +const writeImageToConsole = (image) => { + console.log(`Fetching ${image.url}...`); + return fetch(`${image.url}.html`) + .then((res) => res.text()) + .then((text) => writeInstagramShot(text, image)) + .catch((err) => { + console.error(" Error: Ressor fetching/parsing images:"); + console.error(err.message); + }); +}; + /** * Fetches the latest Instagram shot of the owner of the access_token * @param {String} token - access token */ -const AsciiShot = (token) => { - fetch(`https://api.instagram.com/v1/users/self/media/recent/?access_token=${token}`).then(res => { - return res.json(); - }).then(json => { - const image = json.data[0].images.standard_resolution.url.split("?")[0]; - return fetch(`${image}.html`); - }).then(res => { - return res.text(); - }).then(_parseAsciiImage); +const AsciiShot = (username) => { + fetch(`https://instagram.com/${username}/media`) + .then((res) => res.json()) + .then(json => { + const itemsArray = json.items; + + if (!json || !itemsArray) { + throw new Error("No items found."); + } + + let isFetching = false; + + const instagramShots = formatItemsArray(itemsArray); + + const fetchNextOrDie = (data) => { + process.stdin.setRawMode(false); + + if (isFetching) { + return; + } + + if (data && data.toString("hex") !== "20") { + process.exit(0); + } + + isFetching = true; + + writeImageToConsole(instagramShots.pop()).then(() => { + isFetching = false; + if (instagramShots.length) { + process.stdin.setRawMode(true); + process.stdin.resume(); + console.log(""); + console.log(""); + console.log("Press space to fetch the next one or any other key to exit"); + } else { + console.log(`No more images for ${username} :)`); + process.exit(0); + } + }); + }; + + fetchNextOrDie(); + process.stdin.on("data", fetchNextOrDie); + }) + .catch((err) => { + console.error(` Error: Couldn't fetch images for username ${username}`); + console.error(err.message); + }); }; module.exports = AsciiShot; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index d821517..0000000 --- a/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -require("./server"); diff --git a/lib/server.js b/lib/server.js deleted file mode 100644 index 1b13404..0000000 --- a/lib/server.js +++ /dev/null @@ -1,64 +0,0 @@ -const express = require("express"); -const app = express(); -const open = require("open"); -const fetch = require("node-fetch"); -const FormData = require("form-data"); -const AsciiShot = require("./ascii-shot"); - -var config = null; -try { - config = require("../.config"); -} catch (e) { - config = {}; -} - -const clientID = process.env.INSTAGRAM_CLIENT_ID || config.INSTAGRAM_CLIENT_ID; -const clientSecret = process.env.INSTAGRAM_CLIENT_SECRET || config.INSTAGRAM_CLIENT_SECRET; -const redirectURI = process.env.INSTAGRAM_REDIRECT_URI || config.INSTAGRAM_REDIRECT_URI; -const token = process.env.INSTAGRAM_ACCESS_TOKEN || config.INSTAGRAM_ACCESS_TOKEN; - -if (!clientID) { - throw new Error(`Missing INSTAGRAM_CLIENT_ID. See README.md on how to fix this`); -} - -if (!clientSecret) { - throw new Error(`Missing INSTAGRAM_CLIENT_SECRET. See README.md on how to fix this`); -} - -if (!redirectURI) { - throw new Error(`Missing INSTAGRAM_REDIRECT_URI. See README.md on how to fix this`); -} - -app.get(`/${redirectURI.split("/").pop()}`, (req, redirectRes) => { - if (req.query.error) { - throw new Error(req.query.error); - } - - const code = req.query.code; - const form = new FormData(); - - form.append("client_id", clientID); - form.append("client_secret", clientSecret); - form.append("grant_type", "authorization_code"); - form.append("redirect_uri", redirectURI); - form.append("code", code); - - fetch("https://api.instagram.com/oauth/access_token", { - method: "POST", - body: form - }).then(function(res) { - return res.json(); - }).then(function(json) { - console.log(`Your access_token is ${json.access_token}`); - redirectRes.status(200).json({"message": `Successfully authed, you can now close this tab`}); - AsciiShot(json.access_token); - }); -}); - -if (token) { - AsciiShot(token); -} else { - app.listen(8080, () => { - open(`https://api.instagram.com/oauth/authorize/?client_id=${clientID}&redirect_uri=${redirectURI}&response_type=code`); - }); -} diff --git a/package.json b/package.json index db9e261..1c59550 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "ascii-shot", - "version": "0.0.3", - "description": ":rainbow: Get your latest Instagram shot as an ASCII version to stdout.", - "main": "index.js", + "version": "1.0.0", + "description": ":rainbow: Get an users instagram feed as an ASCII version to stdout.", + "main": "ascii-shot.js", "scripts": { - "test": "mocha --reporter=spec" + "test": "ava" }, "repository": { "type": "git", @@ -25,13 +25,14 @@ "dependencies": { "ansi": "^0.3.1", "cheerio": "^0.20.0", - "express": "^4.13.4", "form-data": "^0.2.0", "meow": "^3.7.0", - "node-fetch": "^1.4.0", - "open": "0.0.5" + "node-fetch": "^1.4.0" }, "bin": { "ascii-shot": "bin/ascii-shot" + }, + "devDependencies": { + "eslint-config-motley": "^1.0.2" } }