feat: improve text output with article preview, quote tweets, and media
Enhance the text output format to show more useful information: - Article tweets: Show 📰 indicator with title + preview text in feeds, full body when reading single tweets - Quote tweets: Display quoted content with ┌─ QT @user formatting - Media: Show 🖼️/🎬/🔄 indicators with URLs for photos/videos/GIFs Also refactors single tweet read to use shared printTweets function for consistent formatting.
This commit is contained in:
committed by
Peter Steinberger
parent
de49a66601
commit
cf78fce646
+39
-1
@@ -320,7 +320,45 @@ export function createCliContext(normalizedArgs: string[], env: NodeJS.ProcessEn
|
|||||||
}
|
}
|
||||||
for (const tweet of tweets) {
|
for (const tweet of tweets) {
|
||||||
console.log(`\n@${tweet.author.username} (${tweet.author.name}):`);
|
console.log(`\n@${tweet.author.username} (${tweet.author.name}):`);
|
||||||
console.log(tweet.text);
|
|
||||||
|
// Display tweet text, with article indicator if present
|
||||||
|
if (tweet.article) {
|
||||||
|
// For articles: show preview if text is just the title (home timeline),
|
||||||
|
// otherwise show full text (single tweet detail)
|
||||||
|
const isFullBody = tweet.text.length > tweet.article.title.length + 50;
|
||||||
|
if (isFullBody) {
|
||||||
|
console.log(`📰 ${tweet.text}`);
|
||||||
|
} else {
|
||||||
|
console.log(`📰 ${tweet.article.title}`);
|
||||||
|
if (tweet.article.previewText) {
|
||||||
|
console.log(` ${tweet.article.previewText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(tweet.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display media attachments
|
||||||
|
if (tweet.media && tweet.media.length > 0) {
|
||||||
|
for (const m of tweet.media) {
|
||||||
|
const label = m.type === 'video' ? '🎬' : m.type === 'animated_gif' ? '🔄' : '🖼️';
|
||||||
|
console.log(`${label} ${m.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display quoted tweet
|
||||||
|
if (tweet.quotedTweet) {
|
||||||
|
console.log(`┌─ QT @${tweet.quotedTweet.author.username}:`);
|
||||||
|
const qtText = tweet.quotedTweet.article ? `📰 ${tweet.quotedTweet.article.title}` : tweet.quotedTweet.text;
|
||||||
|
// Indent and truncate quoted tweet text
|
||||||
|
const maxLen = 280;
|
||||||
|
const truncated = qtText.length > maxLen ? `${qtText.slice(0, maxLen)}...` : qtText;
|
||||||
|
for (const line of truncated.split('\n').slice(0, 4)) {
|
||||||
|
console.log(`│ ${line}`);
|
||||||
|
}
|
||||||
|
console.log(`└─ https://x.com/${tweet.quotedTweet.author.username}/status/${tweet.quotedTweet.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (tweet.createdAt) {
|
if (tweet.createdAt) {
|
||||||
console.log(`${l('date')}${tweet.createdAt}`);
|
console.log(`${l('date')}${tweet.createdAt}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,7 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
|
|||||||
if (cmdOpts.json || cmdOpts.jsonFull) {
|
if (cmdOpts.json || cmdOpts.jsonFull) {
|
||||||
console.log(JSON.stringify(result.tweet, null, 2));
|
console.log(JSON.stringify(result.tweet, null, 2));
|
||||||
} else {
|
} else {
|
||||||
console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`);
|
ctx.printTweets([result.tweet], { showSeparator: false });
|
||||||
console.log(result.tweet.text);
|
|
||||||
if (result.tweet.createdAt) {
|
|
||||||
console.log(`\n${ctx.l('date')}${result.tweet.createdAt}`);
|
|
||||||
}
|
|
||||||
console.log(formatStatsLine(result.tweet, ctx.getOutput()));
|
console.log(formatStatsLine(result.tweet, ctx.getOutput()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ export interface TweetData {
|
|||||||
quotedTweet?: TweetData;
|
quotedTweet?: TweetData;
|
||||||
// Media attachments (photos, videos, GIFs)
|
// Media attachments (photos, videos, GIFs)
|
||||||
media?: TweetMedia[];
|
media?: TweetMedia[];
|
||||||
|
// Article metadata (for Twitter Articles / long-form posts)
|
||||||
|
article?: {
|
||||||
|
title: string;
|
||||||
|
previewText?: string;
|
||||||
|
};
|
||||||
// Raw GraphQL tweet result (only when includeRaw is enabled)
|
// Raw GraphQL tweet result (only when includeRaw is enabled)
|
||||||
_raw?: GraphqlTweetResult;
|
_raw?: GraphqlTweetResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,29 @@ export function extractTweetText(result: GraphqlTweetResult | undefined): string
|
|||||||
return extractArticleText(result) ?? extractNoteTweetText(result) ?? firstText(result?.legacy?.full_text);
|
return extractArticleText(result) ?? extractNoteTweetText(result) ?? firstText(result?.legacy?.full_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractArticleMetadata(
|
||||||
|
result: GraphqlTweetResult | undefined,
|
||||||
|
): { title: string; previewText?: string } | undefined {
|
||||||
|
const article = result?.article;
|
||||||
|
if (!article) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleResult = article.article_results?.result ?? article;
|
||||||
|
const title = firstText(articleResult.title, article.title);
|
||||||
|
if (!title) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// preview_text is available in home timeline responses
|
||||||
|
const previewText = firstText(
|
||||||
|
(articleResult as { preview_text?: string }).preview_text,
|
||||||
|
(article as { preview_text?: string }).preview_text,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { title, previewText };
|
||||||
|
}
|
||||||
|
|
||||||
export function extractMedia(result: GraphqlTweetResult | undefined): TweetMedia[] | undefined {
|
export function extractMedia(result: GraphqlTweetResult | undefined): TweetMedia[] | undefined {
|
||||||
// Prefer extended_entities (has video info), fall back to entities
|
// Prefer extended_entities (has video info), fall back to entities
|
||||||
const rawMedia = result?.legacy?.extended_entities?.media ?? result?.legacy?.entities?.media;
|
const rawMedia = result?.legacy?.extended_entities?.media ?? result?.legacy?.entities?.media;
|
||||||
@@ -261,6 +284,7 @@ export function mapTweetResult(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const media = extractMedia(result);
|
const media = extractMedia(result);
|
||||||
|
const article = extractArticleMetadata(result);
|
||||||
|
|
||||||
const tweetData: TweetData = {
|
const tweetData: TweetData = {
|
||||||
id: result.rest_id,
|
id: result.rest_id,
|
||||||
@@ -278,6 +302,7 @@ export function mapTweetResult(
|
|||||||
authorId: userId,
|
authorId: userId,
|
||||||
quotedTweet,
|
quotedTweet,
|
||||||
media,
|
media,
|
||||||
|
article,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeRaw) {
|
if (includeRaw) {
|
||||||
|
|||||||
Reference in New Issue
Block a user