twimage-download

Add to Kit.app+

Download twitter images and set their exif info based on the tweet metadata

by Kent C. Dodds
// Menu: Twimage Download
// Description: Download twitter images and set their exif info based on the tweet metadata
// Shortcut: command option control 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 response = await get(
`https://api.twitter.com/1.1/statuses/show/${tweetId}.json?include_entities=true`,
{
headers: {
authorization: `Bearer ${token}`,
},
},
)
const tweet = response.data
const {
geo,
id,
text,
created_at,
extended_entities: {media: medias},
} = tweet
const [latitude, longitude] = geo?.coordinates || []
const ep = new exiftool.ExiftoolProcess(exiftoolBin)
await ep.open()
for (const media of medias) {
let url
if (media.type === 'photo') {
url = media.media_url_https
} else if (media.type === 'video') {
let best = {bitrate: 0}
for (const variant of media.video_info.variants) {
if (variant.bitrate > best.bitrate) best = variant
}
url = best.url
} else {
throw new Error(`Unknown media type for ${twitterUrl}: ${media.type}`)
}
if (!url) throw new Error(`Huh... no media url found for ${twitterUrl}`)
const formattedDate = formatDate(created_at)
const colonDate = formattedDate.replace(/-/g, ':')
const formattedTimestamp = formatTimestamp(created_at)
const filename = new URL(url).pathname.split('/').slice(-1)[0]
const filepath = path.join(
baseOut,
formattedDate.split('-').slice(0, 2).join('-'),
filename,
)
await download(url, filepath)
await ep.writeMetadata(
filepath,
{
ImageDescription: `${text}${twitterUrl}`,
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()
notify(`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)
})
}