Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Last active May 17, 2021 13:55
Show Gist options
  • Save kentcdodds/acfa864608d00af8a37a074cc857c657 to your computer and use it in GitHub Desktop.
Save kentcdodds/acfa864608d00af8a37a074cc857c657 to your computer and use it in GitHub Desktop.
// Menu: Twimage Download
// Description: Download twitter images and set their exif info based on the tweet metadata
// Shortcut: fn ctrl opt cmd t
// Author: Kent C. Dodds
// Twitter: @kentcdodds
import fs from 'fs'
import {fileURLToPath, URL} from 'url'
const exiftool = await npm('node-exiftool')
const exiftoolBin = await npm('dist-exiftool')
const fsExtra = await npm('fs-extra')
const baseOut = home('Pictures/twimages')
const token = await env('TWITTER_BEARER_TOKEN')
const twitterUrl = await arg('Twitter URL')
console.log(`Starting with ${twitterUrl}`)
const tweetId = new URL(twitterUrl).pathname.split('/').slice(-1)[0]
const params = new URLSearchParams()
params.set('ids', tweetId)
params.set('user.fields', 'username')
params.set('tweet.fields', 'author_id,created_at,geo')
params.set('media.fields', 'url')
params.set('expansions', 'author_id,attachments.media_keys,geo.place_id')
const response = await get(
`https://api.twitter.com/2/tweets?${params.toString()}`,
{
headers: {
authorization: `Bearer ${token}`,
},
},
)
const json = /** @type import('../types/twimage-download').JsonResponse */ (
response.data
)
const ep = new exiftool.ExiftoolProcess(exiftoolBin)
await ep.open()
for (const tweet of json.data) {
const {attachments, geo, id, text, created_at} = tweet
if (!attachments) throw new Error(`No attachements: ${tweet.id}`)
const author = json.includes.users.find(u => u.id === tweet.author_id)
if (!author) throw new Error(`wut? No author? ${tweet.id}`)
const link = `https://twitter.com/${author.username}/status/${id}`
const {latitude, longitude} = geo ? await getGeoCoords(geo.place_id) : {}
for (const mediaKey of attachments.media_keys) {
const media = json.includes.media.find(m => mediaKey === m.media_key)
if (!media) throw new Error(`Huh... no media found...`)
const formattedDate = formatDate(created_at)
const colonDate = formattedDate.replace(/-/g, ':')
const formattedTimestamp = formatTimestamp(created_at)
const filename = new URL(media.url).pathname.split('/').slice(-1)[0]
const filepath = path.join(
baseOut,
formattedDate.split('-').slice(0, 2).join('-'),
filename,
)
await download(media.url, filepath)
console.log(`Updating exif metadata for ${filepath}`)
await ep.writeMetadata(
filepath,
{
ImageDescription: `${text} – ${link}`,
Keywords: 'photos from tweets',
DateTimeOriginal: formattedTimestamp,
FileModifyDate: formattedTimestamp,
ModifyDate: formattedTimestamp,
CreateDate: formattedTimestamp,
...(geo
? {
GPSLatitudeRef: latitude > 0 ? 'North' : 'South',
GPSLongitudeRef: longitude > 0 ? 'East' : 'West',
GPSLatitude: latitude,
GPSLongitude: longitude,
GPSDateStamp: colonDate,
GPSDateTime: formattedTimestamp,
}
: null),
},
['overwrite_original'],
)
}
}
await ep.close()
console.log(`All done with ${twitterUrl}`)
function formatDate(t) {
const d = new Date(t)
return `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(
d.getDate(),
)}`
}
function formatTimestamp(t) {
const d = new Date(t)
const formattedDate = formatDate(t)
return `${formatDate(t)} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`
}
function padZero(n) {
return String(n).padStart(2, '0')
}
async function getGeoCoords(placeId) {
const response = await get(
`https://api.twitter.com/1.1/geo/id/${placeId}.json`,
{
headers: {
authorization: `Bearer ${token}`,
},
},
)
const [longitude, latitude] = response.data.centroid
return {latitude, longitude}
}
async function download(url, out) {
console.log(`downloading ${url} to ${out}`)
await fsExtra.ensureDir(path.dirname(out))
const writer = fs.createWriteStream(out)
const response = await get(url, {responseType: 'stream'})
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', () => resolve(out))
writer.on('error', reject)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment