Improve Twitter/X link handling and text formatting

- Use FxTwitter API for full note tweet text (with syndication API fallback)
- Save Twitter posts based on media content:
  - Videos → embed type (proxied video)
  - Images → image type (gallery)
  - Text-only → quote type
- Add granular preview badges: 'X VIDEO', 'X GALLERY', 'X POST'
- Preserve formatting/spacing with white-space: pre-wrap for quotes and descriptions
- Rename VideoInfo to EmbedInfo for better semantic clarity
This commit is contained in:
soup 2026-01-18 00:03:30 -05:00
parent 8a046728ef
commit 4a2cb341fa
Signed by: soup
SSH key fingerprint: SHA256:GYxje8eQkJ6HZKzVWDdyOUF1TyDiprruGhE0Ym8qYDY
7 changed files with 249 additions and 137 deletions

View file

@ -96,22 +96,24 @@ type urlMetadata struct {
VideoID string
EmbedHTML string
VideoURL string // Direct video URL (for Twitter)
MediaType string // "video", "images", "text" - for Twitter only
}
func fetchURLMetadata(ctx context.Context, url string) (*urlMetadata, error) {
// Check if it's a YouTube/Vimeo/Twitter embed
videoInfo, err := embed.Detect(ctx, url)
if err == nil && videoInfo != nil {
embedInfo, err := embed.Detect(ctx, url)
if err == nil && embedInfo != nil {
return &urlMetadata{
Title: videoInfo.Title,
Description: videoInfo.Description,
ImageURL: videoInfo.ThumbnailURL,
ImageURLs: videoInfo.ThumbnailURLs,
Title: embedInfo.Title,
Description: embedInfo.Description,
ImageURL: embedInfo.ThumbnailURL,
ImageURLs: embedInfo.ThumbnailURLs,
IsEmbed: true,
Provider: string(videoInfo.Provider),
VideoID: videoInfo.VideoID,
EmbedHTML: videoInfo.EmbedHTML,
VideoURL: videoInfo.VideoURL,
Provider: string(embedInfo.Provider),
VideoID: embedInfo.VideoID,
EmbedHTML: embedInfo.EmbedHTML,
VideoURL: embedInfo.VideoURL,
MediaType: embedInfo.MediaType,
}, nil
}
@ -139,6 +141,7 @@ type previewResponse struct {
Provider string `json:"provider,omitempty"`
VideoID string `json:"videoId,omitempty"`
EmbedHTML string `json:"embedHtml,omitempty"`
MediaType string `json:"mediaType,omitempty"` // "video", "images", "text" - for Twitter
}
// HandlePreviewLink handles POST /api/preview - fetches metadata for a URL
@ -175,6 +178,7 @@ func HandlePreviewLink(rc *RequestContext, w http.ResponseWriter, r *http.Reques
Provider: meta.Provider,
VideoID: meta.VideoID,
EmbedHTML: meta.EmbedHTML,
MediaType: meta.MediaType,
})
}
@ -207,35 +211,58 @@ func HandleCreateFromLink(rc *RequestContext, w http.ResponseWriter, r *http.Req
var itemType string
var embedProvider, embedVideoID, embedHTML *string
var imageURL, videoURL string
var imageURLs []string // For multi-image tweets
if req.Provider != nil && *req.Provider != "" {
// It's an embed
itemType = "embed"
embedProvider = req.Provider
embedVideoID = req.VideoID
embedHTML = req.EmbedHTML
// Special handling for Twitter based on media content
if *req.Provider == "twitter" {
// Fetch tweet info to determine content type
videoInfo, err := embed.Detect(ctx, req.URL)
if err == nil && videoInfo != nil {
if videoInfo.VideoURL != "" {
// Tweet has video → keep as embed (proxied)
itemType = "embed"
embedProvider = req.Provider
embedVideoID = req.VideoID
embedHTML = req.EmbedHTML
videoURL = videoInfo.VideoURL
imageURL = videoInfo.ThumbnailURL
} else if len(videoInfo.ThumbnailURLs) > 0 {
// Tweet has image(s) → save as image
itemType = "image"
imageURL = videoInfo.ThumbnailURL
imageURLs = videoInfo.ThumbnailURLs
} else {
// Text-only tweet → save as quote
itemType = "quote"
// title and description already set from request
}
} else {
// If detection fails, fall back to link
itemType = "link"
}
} else {
// YouTube, Vimeo, etc. → keep as embed
itemType = "embed"
embedProvider = req.Provider
embedVideoID = req.VideoID
embedHTML = req.EmbedHTML
// Fetch thumbnail for non-Twitter embeds
if videoInfo, err := embed.Detect(ctx, req.URL); err == nil && videoInfo != nil {
imageURL = videoInfo.ThumbnailURL
videoURL = videoInfo.VideoURL
}
}
} else if req.ImageURL != nil && *req.ImageURL != "" {
// It's a link with an image
itemType = "image"
imageURL = *req.ImageURL
} else {
// Just a link (will be shown as a card)
itemType = "link"
}
// For embeds, fetch thumbnail(s) and video URL
var imageURL, videoURL string
var imageURLs []string // For multi-image tweets
if req.ImageURL != nil {
imageURL = *req.ImageURL
}
if itemType == "embed" && embedProvider != nil {
if videoInfo, err := embed.Detect(ctx, req.URL); err == nil && videoInfo != nil {
imageURL = videoInfo.ThumbnailURL
imageURLs = videoInfo.ThumbnailURLs
videoURL = videoInfo.VideoURL
}
}
// Create the item
it, err := item.QCreate(ctx, rc.DB, item.CreateParams{
Title: item.Nullable(req.Title),