Skip to content

Commit

Permalink
Add CLI tool for fun
Browse files Browse the repository at this point in the history
  • Loading branch information
diurnalist committed Nov 2, 2020
1 parent c181e5a commit 5e67543
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 56 deletions.
1 change: 1 addition & 0 deletions bin/build.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env node
import build from '../src/index.js';

await build();
32 changes: 32 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node
import chalk from 'chalk';
import program from 'commander';
import moment from 'moment-timezone';
import { table } from 'table';
import locations from '../src/config/index.js';
import { getShowtimes } from '../src/scraper.js';
import { daysFromNow } from '../src/lib/datetime.js';
import { toDisplayTime } from '../src/lib/utils.js';

program
.version(process.env.npm_package_version)
.option('-l, --location [loc]', 'Which location to display showtimes for', 'chicago')
.parse(process.argv);

const { kinos, timezone } = locations[program.location];
if (! kinos) {
throw `Invalid location: "${program.location}"`;
}

const showtimes = await getShowtimes(kinos);
const rows = [['Time', 'Kino', 'Title']].concat(showtimes
.filter(daysFromNow(0, timezone))
.filter(({ showtime }) => {
return showtime.isAfter(moment().subtract(1, 'hours'))
})
.map(({ showtime, title, location }) => {
return [toDisplayTime(showtime), chalk.cyan(location), chalk.cyanBright.bold(title)];
}));

console.log(chalk.cyan.bold(' Today'))
console.log(table(rows));
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"scripts": {
"build": "bin/build.js",
"show": "bin/cli.js",
"test": "echo \"Error: no test specified\" && exit 1",
"test-scraper": "bin/test-scraper.js"
},
Expand All @@ -21,17 +22,19 @@
"homepage": "https://github.com/diurnalist/ourkino.com#readme",
"dependencies": {
"async": "^3.2.0",
"chalk": "^4.1.0",
"cheerio": "^0.22.0",
"commander": "^6.2.0",
"date-fns": "^2.16.1",
"debug": "^4.3.0",
"esm": "^3.2.25",
"fs-extra": "^9.0.1",
"handlebars": "^4.7.6",
"node-ical": "^0.12.3",
"moment-timezone": "^0.5.31",
"node-ical": "^0.12.3",
"request": "^2.88.2",
"split": "^1.0.1"
"split": "^1.0.1",
"table": "^6.0.3"
},
"devDependencies": {
"chokidar": "^3.4.3"
Expand Down
59 changes: 7 additions & 52 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import handlebars from 'handlebars';
import path from 'path';
import { fileURLToPath } from 'url';
import locations from './config/index.js';
import { today } from './lib/datetime.js';
import { pad } from './lib/utils.js';
import { daysFromNow } from './lib/datetime.js';
import { toDisplayTime } from './lib/utils.js';
import { getShowtimes } from './scraper.js';

const log = debug('build');
Expand Down Expand Up @@ -38,46 +38,7 @@ function getTemplate() {
});
}

const locationMatches = (city) => ({ location }) => {
const locationSearch = location.toLowerCase();
return !!locations[city].find((loc) => locationSearch.indexOf(loc) >= 0);
};

function daysFromNow(days, timezone) {
const todayDatetime = today(timezone);
// Cut-off is 3am to account for midnight movies
const start = todayDatetime.clone().add(days, 'day').set('hour', 3);
const end = start.clone().add(1, 'day');

return ({ showtime }) => {
return showtime.isAfter(start) && showtime.isBefore(end);
};
}

function dedupe() {
const seenPairs = [];
const isSeen = ({ title, showtime, location }) => {
for (let pair, i = 0; pair = seenPairs[i++];) {
if (title === pair.title &&
showtime.isSame(pair.showtime) &&
location === pair.location) return true;
}
return false;
};
return ({ title, showtime, location }) => {
const pair = { title, showtime, location };
if (isSeen(pair)) {
return false;
} else {
seenPairs.push(pair);
return true;
}
};
}

function toTemplateData({ deepLink, language, location, showtime, title }) {
const hours = pad(showtime.hour());
const minutes = pad(showtime.minute());
const textSearch = [ title, location, language ]
.filter(Boolean)
.map((s) => s.toLowerCase())
Expand All @@ -89,7 +50,7 @@ function toTemplateData({ deepLink, language, location, showtime, title }) {
language,
location,
textSearch,
time: `${hours}:${minutes}`,
time: toDisplayTime(showtime),
title
};
}
Expand All @@ -105,24 +66,18 @@ async function buildLocation(name, showtimes) {
}
});

const sorted = showtimes.sort(({ showtime: a }, { showtime: b }) => {
if (a.isSame(b)) return 0;
else if (a.isBefore(b)) return -1;
else return 1;
});
const template = await getTemplate();
const indexHtml = path.join(locationDir, 'index.html');
const deduped = sorted.filter(dedupe());
const today = deduped.filter(daysFromNow(0, timezone)).map(toTemplateData);
const tomorrow = deduped.filter(daysFromNow(1, timezone)).map(toTemplateData);
const today = showtimes.filter(daysFromNow(0, timezone)).map(toTemplateData);
const tomorrow = showtimes.filter(daysFromNow(1, timezone)).map(toTemplateData);
await fs.writeFile(indexHtml, template({ today, tomorrow }));
}

export default async function build() {
const filteredLocations = Object.keys(locations).filter((name) => {
return !program.location || program.location === name;
});
const showtimes = await async.parallel(
const allShowtimes = await async.parallel(
filteredLocations.reduce((jobs, name) => {
const { kinos } = locations[name];
jobs[name] = getShowtimes.bind(null, kinos);
Expand All @@ -132,7 +87,7 @@ export default async function build() {

async function buildAllLocations() {
await async.parallel(
filteredLocations.map((name) => buildLocation.bind(null, name, showtimes[name]))
filteredLocations.map((name) => buildLocation.bind(null, name, allShowtimes[name]))
);
}

Expand Down
11 changes: 11 additions & 0 deletions src/lib/datetime.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ export function toUTC(date) {
utcDate.millisecond(localDate.millisecond());
return utcDate;
};

export function daysFromNow(days, timezone) {
const todayDatetime = today(timezone);
// Cut-off is 3am to account for midnight movies
const start = todayDatetime.clone().add(days, 'day').set('hour', 3);
const end = start.clone().add(1, 'day');

return ({ showtime }) => {
return showtime.isAfter(start) && showtime.isBefore(end);
};
}
6 changes: 6 additions & 0 deletions src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export function gcalURL(id) {
export function pad(number) {
return (number < 10 ? '0' : '') + number;
};

export function toDisplayTime(showtime) {
const hours = pad(showtime.hour());
const minutes = pad(showtime.minute());
return `${hours}:${minutes}`;
}
29 changes: 28 additions & 1 deletion src/scraper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,35 @@ import async from 'async';
// sudden bursts of traffic, even minor.
const concurrentTaskLimit = 4;

function dedupe() {
const seenPairs = [];
const isSeen = ({ title, showtime, location }) => {
for (let pair, i = 0; pair = seenPairs[i++];) {
if (title === pair.title &&
showtime.isSame(pair.showtime) &&
location === pair.location) return true;
}
return false;
};
return ({ title, showtime, location }) => {
const pair = { title, showtime, location };
if (isSeen(pair)) {
return false;
} else {
seenPairs.push(pair);
return true;
}
};
}

export async function getShowtimes(scrapers) {
const data = await async.parallelLimit(scrapers, concurrentTaskLimit);
const showtimes = data.reduce((acc, list) => acc.concat(list), []).filter(Boolean);
return showtimes;
const sorted = showtimes.sort(({ showtime: a }, { showtime: b }) => {
if (a.isSame(b)) return 0;
else if (a.isBefore(b)) return -1;
else return 1;
});
const deduped = sorted.filter(dedupe());
return deduped;
};
Loading

0 comments on commit 5e67543

Please sign in to comment.