Last active
May 17, 2021 13:55
-
-
Save kentcdodds/acfa864608d00af8a37a074cc857c657 to your computer and use it in GitHub Desktop.
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
// 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