diff --git a/src/fetch.ts b/src/fetch.ts index c1796be..26462ac 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -63,7 +63,7 @@ export const twitterFetch = async ( while (apiAttempts < API_ATTEMPTS) { /* Generate a random CSRF token, Twitter just cares that header and cookie match, - REST can use shorter csrf tokens (32 bytes) but graphql prefers 160 bytes */ + REST can use shorter csrf tokens (32 bytes) but graphql uses a 160 byte one. */ const csrfToken = crypto.randomUUID().replace(/-/g, ''); const headers: Record = { diff --git a/src/helpers/card.ts b/src/helpers/card.ts index 8df5c89..973ae58 100644 --- a/src/helpers/card.ts +++ b/src/helpers/card.ts @@ -3,7 +3,11 @@ import { calculateTimeLeftString } from './pollTime'; /* Renders card for polls and non-Twitter video embeds (i.e. YouTube) */ export const renderCard = ( card: GraphQLTwitterStatus['card'] -): { poll?: APIPoll; external_media?: APIExternalMedia } => { +): { + poll?: APIPoll; + external_media?: APIExternalMedia; + media?: { videos: TweetMedia[]; photos: TweetMedia[] }; +} => { if (!Array.isArray(card.legacy?.binding_values)) { return {}; } @@ -58,5 +62,37 @@ export const renderCard = ( }; } + if (binding_values.unified_card?.string_value) { + try { + const card = JSON.parse(binding_values.unified_card.string_value); + const mediaEntities = card?.media_entities as Record; + + if (mediaEntities) { + const media = { + videos: [] as TweetMedia[], + photos: [] as TweetMedia[] + }; + Object.keys(mediaEntities).forEach(key => { + const mediaItem = mediaEntities[key]; + switch (mediaItem.type) { + case 'photo': + media.photos.push(mediaItem); + break; + case 'animated_gif': + case 'video': + media.videos.push(mediaItem); + break; + } + }); + + console.log('media', media); + + return { media: media }; + } + } catch (e) { + console.error('Failed to parse unified card JSON', e); + } + } + return {}; }; diff --git a/src/helpers/media.ts b/src/helpers/media.ts index b1e4b84..85198c4 100644 --- a/src/helpers/media.ts +++ b/src/helpers/media.ts @@ -20,7 +20,8 @@ export const processMedia = (media: TweetMedia): APIPhoto | APIVideo | null => { width: media.original_info?.width, height: media.original_info?.height, format: bestVariant?.content_type || '', - type: media.type === 'animated_gif' ? 'gif' : 'video' + type: media.type === 'animated_gif' ? 'gif' : 'video', + variants: media.video_info?.variants ?? [] }; } return null; diff --git a/src/providers/twitter/processor.ts b/src/providers/twitter/processor.ts index 7db0ebf..597601b 100644 --- a/src/providers/twitter/processor.ts +++ b/src/providers/twitter/processor.ts @@ -230,6 +230,33 @@ export const buildAPITwitterStatus = async ( if (card.poll) { apiStatus.poll = card.poll; } + /* TODO: Right now, we push them after native photos and videos but should we prepend them instead? */ + if (card.media) { + if (card.media.videos) { + card.media.videos.forEach(video => { + const mediaObject = processMedia(video) as APIVideo; + if (mediaObject) { + apiStatus.media.all = apiStatus.media?.all ?? []; + apiStatus.media?.all?.push(mediaObject); + apiStatus.media.videos = apiStatus.media?.videos ?? []; + apiStatus.media.videos?.push(mediaObject); + } + }); + } + if (card.media.photos) { + card.media.photos.forEach(photo => { + const mediaObject = processMedia(photo) as APIPhoto; + if (mediaObject) { + apiStatus.media.all = apiStatus.media?.all ?? []; + apiStatus.media?.all?.push(mediaObject); + apiStatus.media.photos = apiStatus.media?.photos ?? []; + apiStatus.media.photos?.push(mediaObject); + } + }); + } + + + } } else { /* Determine if the status contains a YouTube link (either youtube.com or youtu.be) so we can include it */ const youtubeIdRegex = /(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([^\s&]+)/; diff --git a/src/types/types.d.ts b/src/types/types.d.ts index f7dc79f..273919e 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -101,6 +101,7 @@ interface APIVideo extends APIMedia { thumbnail_url: string; format: string; duration: number; + variants: TweetMediaFormat[]; } interface APIMosaicPhoto extends APIMedia { diff --git a/src/types/vendor/twitter.d.ts b/src/types/vendor/twitter.d.ts index b2bcd97..7949862 100644 --- a/src/types/vendor/twitter.d.ts +++ b/src/types/vendor/twitter.d.ts @@ -420,7 +420,8 @@ type GraphQLTwitterStatus = { | 'last_updated_datetime_utc' | 'duration_minutes' | 'api' - | 'card_url'; + | 'card_url' + | 'unified_card'; value: | { string_value: string; // "Option text"