Created
March 28, 2024 14:50
-
-
Save cbleisch/86977ac6cddefee159a4e20f0ef9fe83 to your computer and use it in GitHub Desktop.
Script for generating text & html content from API Endpoint in Live Streams
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios from "axios"; | |
import striptags from "striptags"; | |
import { decode } from 'html-entities'; | |
import fs from 'fs'; | |
import QRCode from 'qrcode'; | |
import path from "path"; | |
import inquirer from 'inquirer'; | |
import DatePrompt from "inquirer-date-prompt"; | |
import dayjs from 'dayjs'; | |
import isYesterday from 'dayjs/plugin/isYesterday.js'; | |
import utc from 'dayjs/plugin/utc.js'; | |
import localizedFormat from 'dayjs/plugin/localizedFormat.js'; | |
import { getDevoFromAPI } from "./APIs/DevoAPI.js"; | |
import { YouTubeAPI } from "./APIs/YouTubeAPI.js"; | |
dayjs.extend(utc); | |
dayjs.extend(localizedFormat) | |
const GAPI_KEY = '#############-#########################'; | |
// name the needed directories | |
const outputDir = './outputs'; | |
const imagesDir = `${outputDir}/images`; | |
const textDir = `${outputDir}/text`; | |
const htmlDir = `${outputDir}/html`; | |
const YouTubeAPIClient = new YouTubeAPI(); | |
const qrConfig = { | |
color: { | |
dark: '#2279CB', // Blue dots | |
light: '#0000' // Transparent background | |
}, | |
width: 800, | |
height: 800 | |
} | |
// make the needed directories | |
makeDirIfNotExists(outputDir); | |
makeDirIfNotExists(textDir); | |
makeDirIfNotExists(htmlDir); | |
/** | |
* generateContent is the main function for the script. | |
* Using today's date fetch the devotional data from the devo API. | |
* Then store each devo attribute in text format, html, | |
* download any detected images, and generate a QR code to link to ODB. | |
*/ | |
async function generateContent() { | |
try { | |
console.time('get devo'); | |
inquirer.registerPrompt("date", DatePrompt); | |
let selectedDate = await propmtForDate('selectedDate', 'Select Devotional Date: '); | |
selectedDate = dayjs(selectedDate).format('MM-DD-YYYY'); | |
console.log(selectedDate); | |
// create resources for linking to most recent YouTube video | |
// The video will always be the most recent in the "Devotional Streams" playlist | |
// the dates passed in are used as the startDate and endDate for a YouTube Analytics report | |
// const endDate = dayjs(); | |
await generateYouTubeLinks(dayjs('2023-01-01', 'YYYY-MM-DD').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')); | |
const devo = await getDevoFromAPI(selectedDate); | |
let REQUIRES_YESTERDAY = false; | |
// this handy little section checks for existing devo data from yesterday | |
// if found it calls a function to move all of yesterday's devo files | |
// into corresponding yesterday folders in each html, images, and text dirs | |
if (fs.existsSync(`${textDir}/date.txt`)) { | |
dayjs.extend(isYesterday); | |
// console.log('found data from yesterday?'); | |
// TODO: ensure the data from yesterday corresponds | |
// to the day prior to the user's requested date | |
const dateFile = await fs.readFileSync(`${textDir}/date.txt`, 'utf8'); | |
const DATA_FROM_YESTERDAY_FOUND = dayjs.utc(+dateFile).isYesterday(); | |
console.log(dayjs.utc(+dateFile).format('MM-DD-YYYY'), DATA_FROM_YESTERDAY_FOUND); | |
if (DATA_FROM_YESTERDAY_FOUND) { | |
console.log("moving yesterdays data"); | |
moveYesterdaysFiles(); | |
} else { | |
REQUIRES_YESTERDAY = true; | |
} | |
REQUIRES_YESTERDAY = dayjs(+dateFile).isBefore(selectedDate) && !DATA_FROM_YESTERDAY_FOUND; | |
} else { | |
REQUIRES_YESTERDAY = true; | |
} | |
await generateFiles(selectedDate, devo); | |
if (REQUIRES_YESTERDAY) { | |
// script hasn't run before, so get yesterday's devo data from API | |
let yesterday = dayjs(selectedDate, 'MM-DD-YYYY').subtract(1, 'day').format('MM-DD-YYYY'); | |
console.log('Yesterday: ' + yesterday); | |
const yesterdayDevo = await getDevoFromAPI(yesterday); | |
await generateFiles(yesterday, yesterdayDevo, '/yesterday') | |
} | |
console.timeEnd('get devo'); | |
} catch (err) { | |
console.warn('Unable to generate devo outputs!'); | |
console.log(err); | |
} | |
} | |
/** | |
* Update the latest_stream.html file with yesterday's video embed. | |
* | |
* @param {string} startDate | |
* @param {string} endDate | |
*/ | |
async function generateYouTubeLinks(startDate, endDate) { | |
const channel = await YouTubeAPIClient.getChannel(); | |
YouTubeAPIClient.getPlaylist(undefined, 'Devotional Streams').then(async devoStreamPlaylist => { | |
let playlist = devoStreamPlaylist; | |
if (Array.isArray(devoStreamPlaylist)) { | |
playlist = devoStreamPlaylist[0]; | |
} | |
const { id: playlistId } = playlist; | |
const videos = await YouTubeAPIClient.getPlaylistItems(playlistId); | |
// sort the videos in playlist descending by pulishtedAt | |
const sortedVideos = videos.sort(function (a, b) { | |
return new Date(b.snippet.publishedAt) - new Date(a.snippet.publishedAt); | |
}); | |
const mostRecentVideoFromPlaylist = sortedVideos[0]; | |
const { videoId } = mostRecentVideoFromPlaylist.contentDetails; | |
const mostRecentVideo = await YouTubeAPIClient.getVideos(videoId); | |
const video = mostRecentVideo[0]; | |
// analytics output does not contain references to videoId | |
const theAnalytics = await YouTubeAPIClient.getAnalytics(videoId, video.snippet.channelId, startDate, endDate); | |
// make QR Code | |
const videoURL = `https://www.youtube.com/watch?v=${videoId}&list=${playlistId}`; | |
makeDirIfNotExists(imagesDir); | |
QRCode.toFile(`${imagesDir}/latest_youtube_stream.png`, videoURL, qrConfig).then(() => { console.log('Latest YouTube Stream QR Code Generated') }); | |
// make html embed file | |
fs.writeFile(`${htmlDir}/latest_stream.html`, `${formatHTML(video.player.embedHtml.replace('//www.', 'https://www.').replace(videoId, `${videoId}?&autoplay=1`))}`, () => { | |
console.log('Latest YouTube Stream Embed HTML Generated') | |
}); | |
}); | |
} | |
/** | |
* Use a devotional object to extract its attributes into individual files for each attribute | |
* @param {string} date | |
* @param {object} devo | |
* @param {string} path | |
*/ | |
async function generateFiles(date, devo, path) { | |
let theDate = dayjs(date, 'MM-DD-YYYY'); | |
const month = theDate.get('month') + 1; | |
const day = theDate.get('date'); | |
const year = theDate.get('year'); | |
const attributes = Object.keys(devo); | |
// const values = Object.values(devo); | |
// loop over each piece of devotional data | |
// and store it in html, text, or image folder | |
for (let i = 0; i < attributes.length; i++) { | |
const attribute = attributes[i]; | |
const value = `${devo[attribute]}`; | |
// remove all html tags and decode characters | |
const pureString = decode(striptags(value)); | |
// find any images and save them | |
checkForAndSaveImages(pureString, attribute, path); | |
// write the html and text files | |
const html = formatHTML(value); | |
if (path) { | |
makeDirIfNotExists(`${htmlDir}${path}`); | |
makeDirIfNotExists(`${textDir}${path}`); | |
makeDirIfNotExists(`${imagesDir}${path}`); | |
} | |
if (html) { | |
fs.writeFile(`${htmlDir}${path ? path : ''}/${attribute}.html`, `${formatHTML(value)}`, () => { | |
// console.log(`HTML Written: ${attribute}`) | |
}); | |
} | |
if (pureString) { | |
fs.writeFile(`${textDir}${path ? path : ''}/${attribute}.txt`, `${pureString}`, () => { | |
// console.log(`Text Written: ${attribute}`) | |
}); | |
} | |
} | |
fs.writeFile(`${textDir}${path ? path : ''}/date-string.txt`, `${theDate.format('LL')}`, () => { | |
console.log('Date written') | |
}); | |
createDevoQRCode(month, day, year, path); | |
} | |
/** | |
* This little function moves all of the existing output files from | |
* the html, text, and images dirs into their respective yesterday | |
* dirs for easy access on the stream | |
*/ | |
function moveYesterdaysFiles() { | |
const yesterdayHTMLDir = `${htmlDir}/yesterday`; | |
const yesterdayTextDir = `${textDir}/yesterday`; | |
const yesterdayImagesDir = `${imagesDir}/yesterday`; | |
makeDirIfNotExists(yesterdayHTMLDir); | |
makeDirIfNotExists(yesterdayTextDir); | |
makeDirIfNotExists(yesterdayImagesDir); | |
fs.readdirSync(htmlDir).forEach(file => { | |
fs.rename(`${htmlDir}/${file}`, `${yesterdayHTMLDir}/${file}`, () => { }) | |
}); | |
fs.readdirSync(textDir).forEach(file => { | |
fs.rename(`${textDir}/${file}`, `${yesterdayTextDir}/${file}`, () => { }) | |
}); | |
fs.readdirSync(imagesDir).forEach(file => { | |
fs.rename(`${imagesDir}/${file}`, `${yesterdayImagesDir}/${file}`, () => { }) | |
}); | |
} | |
/** | |
* Take a given piece of devo data and if it is a link to an image then download the image | |
* @param {string} url | |
* @param {string} attribute | |
*/ | |
async function checkForAndSaveImages(url, attribute, savePath) { | |
makeDirIfNotExists(imagesDir); | |
const imageRegex = /(?:https?|ftp):\/\/[\S]*\.(?:png|jpe?g|gif|svg|webp)(?:\?\S+=\S*(?:&\S+=\S*)*)?/g; | |
const IS_IMAGE_LINK = url.match(imageRegex); | |
if (IS_IMAGE_LINK) { | |
// get the images file name and image format type (ie png, jpg, jpeg, svg) | |
const urlArr = url.split('/'); | |
const originalFilename = urlArr[urlArr.length - 1]; | |
const fileArr = originalFilename.split('.'); | |
const fileType = fileArr[fileArr.length - 1]; | |
const CAN_BE_SAVED = attribute && fileType; | |
if (CAN_BE_SAVED) { | |
if (savePath) { | |
makeDirIfNotExists(`${imagesDir}${savePath}`); | |
} | |
const filePath = path.resolve('./', `${imagesDir}${savePath ? savePath : ''}`, `${attribute}.${fileType}`); | |
const writer = fs.createWriteStream(filePath); | |
// get the image data | |
const response = await axios.get(url, { | |
responseType: 'stream' | |
}); | |
// save the image file | |
response.data.pipe(writer); | |
} | |
} | |
} | |
/** | |
* Create and save a QR code to the images directory | |
* @param {number} month | |
* @param {number} day | |
* @param {number} year | |
* @param {string} path | |
*/ | |
async function createDevoQRCode(month, day, year, path) { | |
makeDirIfNotExists(imagesDir); | |
if (path) { | |
makeDirIfNotExists(`${imagesDir}${path}`); | |
} | |
// TODO: add tracking code to watch usage! | |
const url = `https://odb.org/US/${year}/${('0' + month).slice(-2)}/${('0' + day).slice(-2)}`; | |
// property id 357539023 | |
// stream id 4691043667 | |
// measurement id G-XKYR6QM6X5 | |
// http://www.example.com/?utm_source=exampleblog&utm_medium=referral&utm_campaign=summer-sale | |
await QRCode.toFile(`${imagesDir}${path ? path : ''}/devo_qr.png`, url, qrConfig); | |
} | |
/** | |
* Format and save the content in a valid html document. | |
* This is critical because it sets the meta charset tag to properly parse the content. | |
* @param {string} content | |
* @returns | |
*/ | |
function formatHTML(content) { | |
if (content) { | |
const head = '<html lang="en" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" data-a11y-animated-images="system" data-turbo-loaded=""><head><meta charset="utf-8"><title>Embed utf-8 html content</title></head><body>' | |
const end = '</body></html>'; | |
const final = `${head}${content}${end}`; | |
return final; | |
} | |
} | |
/** | |
* Check for a named directory and create the dir if it does not exist. | |
* @param {string} dir | |
*/ | |
function makeDirIfNotExists(dir) { | |
if (!fs.existsSync(dir)) { | |
fs.mkdirSync(dir); | |
} | |
} | |
/** | |
* Create a date prompt with Inquirer | |
* @param {string} name | |
* @param {string} message | |
* @returns { date } result from Inquirer prompt | |
*/ | |
async function propmtForDate(name, message) { | |
const startDatePrompt = { | |
type: 'date', | |
name, | |
message, | |
locale: 'en-US', | |
format: { month: "2-digit", day: "2-digit", year: "numeric", hour: undefined, minute: undefined } | |
}; | |
const result = await inquirer.prompt([startDatePrompt]); | |
return result[name]; | |
} | |
generateContent(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "stream-scripts", | |
"version": "0.0.1", | |
"description": "Script for generating text & html content from API Endpoint in Live Streams", | |
"main": "get_devo.js", | |
"type": "module", | |
"directories": { | |
"test": "test" | |
}, | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "Charlie Bleisch", | |
"license": "ISC", | |
"dependencies": { | |
"axios": "^1.3.4", | |
"babel-polyfill": "^6.26.0", | |
"branded-qr-code": "^1.3.0", | |
"dayjs": "^1.11.7", | |
"google-auth-library": "^8.7.0", | |
"googleapis": "^112.0.0", | |
"html-entities": "^2.3.3", | |
"inquirer": "^9.1.4", | |
"inquirer-date-prompt": "^3.0.0", | |
"qr-code-styling": "^1.6.0-rc.1", | |
"qr-with-logo": "^1.0.5", | |
"qrcode": "^1.5.1", | |
"qrcode-with-logos": "^1.0.3", | |
"striptags": "^3.2.0" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment