Add Twitter video support and use sql.Null[T] for nullable columns
- Store Twitter video URLs in embed_video_url column instead of downloading - Play videos directly from Twitter's CDN in <video> element - Fix Twitter API parsing (content_type/url fields) - Strip t.co URLs from tweet text descriptions - Use sql.Null[T] generic type for nullable DB columns - Add Nullable[T] and Ptr[T] helper functions - Add play indicator overlay for video items in grid - Add migration for embed_video_url column
This commit is contained in:
parent
cdcc5b5293
commit
2887d9c430
8 changed files with 226 additions and 120 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -27,12 +28,14 @@ type VideoInfo struct {
|
|||
Description string
|
||||
ThumbnailURL string
|
||||
EmbedHTML string
|
||||
VideoURL string // Direct video URL (for Twitter videos)
|
||||
}
|
||||
|
||||
var (
|
||||
youtubeRegex = regexp.MustCompile(`(?:youtube\.com/(?:watch\?v=|embed/|v/|shorts/)|youtu\.be/)([a-zA-Z0-9_-]{11})`)
|
||||
vimeoRegex = regexp.MustCompile(`(?:vimeo\.com/(?:video/)?|player\.vimeo\.com/video/)(\d+)`)
|
||||
twitterRegex = regexp.MustCompile(`(?:twitter\.com|x\.com)/([^/]+)/status/(\d+)`)
|
||||
tcoRegex = regexp.MustCompile(`\s*https://t\.co/\S+`)
|
||||
)
|
||||
|
||||
// Detect checks if a URL is a YouTube, Vimeo, or Twitter/X post and returns its info.
|
||||
|
|
@ -121,6 +124,13 @@ type twitterSyndicationResponse struct {
|
|||
MediaDetails []struct {
|
||||
MediaURLHTTPS string `json:"media_url_https"`
|
||||
Type string `json:"type"`
|
||||
VideoInfo struct {
|
||||
Variants []struct {
|
||||
ContentType string `json:"content_type"`
|
||||
URL string `json:"url"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
} `json:"variants"`
|
||||
} `json:"video_info"`
|
||||
} `json:"mediaDetails"`
|
||||
Video struct {
|
||||
Poster string `json:"poster"`
|
||||
|
|
@ -152,13 +162,26 @@ func fetchTwitter(ctx context.Context, tweetID string, originalURL string) (*Vid
|
|||
return nil, fmt.Errorf("twitter syndication decode: %w", err)
|
||||
}
|
||||
|
||||
// Find thumbnail - prefer photos, then video poster
|
||||
var thumbnailURL string
|
||||
// Find thumbnail and video URL from media
|
||||
var thumbnailURL, videoURL string
|
||||
if len(tweet.Photos) > 0 {
|
||||
thumbnailURL = tweet.Photos[0].URL
|
||||
} else if len(tweet.MediaDetails) > 0 {
|
||||
thumbnailURL = tweet.MediaDetails[0].MediaURLHTTPS
|
||||
} else if tweet.Video.Poster != "" {
|
||||
media := tweet.MediaDetails[0]
|
||||
thumbnailURL = media.MediaURLHTTPS
|
||||
|
||||
// Extract video URL - find highest bitrate MP4
|
||||
if media.Type == "video" || media.Type == "animated_gif" {
|
||||
var bestBitrate int
|
||||
for _, v := range media.VideoInfo.Variants {
|
||||
if v.ContentType == "video/mp4" && v.Bitrate >= bestBitrate {
|
||||
bestBitrate = v.Bitrate
|
||||
videoURL = v.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if thumbnailURL == "" && tweet.Video.Poster != "" {
|
||||
thumbnailURL = tweet.Video.Poster
|
||||
}
|
||||
|
||||
|
|
@ -173,12 +196,17 @@ func fetchTwitter(ctx context.Context, tweetID string, originalURL string) (*Vid
|
|||
title = fmt.Sprintf("%s (@%s)", tweet.User.Name, tweet.User.ScreenName)
|
||||
}
|
||||
|
||||
// Clean up tweet text - remove trailing t.co URLs
|
||||
description := tcoRegex.ReplaceAllString(tweet.Text, "")
|
||||
description = strings.TrimSpace(description)
|
||||
|
||||
return &VideoInfo{
|
||||
Provider: ProviderTwitter,
|
||||
VideoID: tweetID,
|
||||
Title: title,
|
||||
Description: tweet.Text,
|
||||
Description: description,
|
||||
ThumbnailURL: thumbnailURL,
|
||||
VideoURL: videoURL,
|
||||
EmbedHTML: embedHTML,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue