diff --git a/.gitignore b/.gitignore
index dbd2f6b..09bdaa4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,5 @@ nitter.conf
guest_accounts.json*
sessions.json*
dump.rdb
+*.bak
+/tools/*.json*
diff --git a/src/apiutils.nim b/src/apiutils.nim
index ddb5027..42664cf 100644
--- a/src/apiutils.nim
+++ b/src/apiutils.nim
@@ -19,6 +19,7 @@ proc setDisableTid*(disable: bool) =
disableTid = disable
proc setApiProxy*(url: string) =
+ apiProxy = ""
if url.len > 0:
apiProxy = url.strip(chars={'/'}) & "/"
if "http" notin apiProxy:
diff --git a/src/parser.nim b/src/parser.nim
index da0ffb4..a566fb1 100644
--- a/src/parser.nim
+++ b/src/parser.nim
@@ -140,27 +140,37 @@ proc parseVideo(js: JsonNode): Video =
result.variants = parseVideoVariants(js{"video_info", "variants"})
+proc addMedia(media: var MediaEntities; photo: Photo) =
+ media.add Media(kind: photoMedia, photo: photo)
+
+proc addMedia(media: var MediaEntities; video: Video) =
+ media.add Media(kind: videoMedia, video: video)
+
+proc addMedia(media: var MediaEntities; gif: Gif) =
+ media.add Media(kind: gifMedia, gif: gif)
+
proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) =
with jsMedia, js{"extended_entities", "media"}:
for m in jsMedia:
case m.getTypeName:
of "photo":
- result.photos.add Photo(
+ result.media.addMedia(Photo(
url: m{"media_url_https"}.getImageStr,
altText: m{"ext_alt_text"}.getStr
- )
+ ))
of "video":
- result.video = some(parseVideo(m))
+ result.media.addMedia(parseVideo(m))
with user, m{"additional_media_info", "source_user"}:
if user{"id"}.getInt > 0:
result.attribution = some(parseUser(user))
else:
result.attribution = some(parseGraphUser(user))
of "animated_gif":
- result.gif = some Gif(
+ result.media.addMedia(Gif(
url: m{"video_info", "variants"}[0]{"url"}.getImageStr,
- thumb: m{"media_url_https"}.getImageStr
- )
+ thumb: m{"media_url_https"}.getImageStr,
+ altText: m{"ext_alt_text"}.getStr
+ ))
else: discard
with url, m{"url"}:
@@ -170,29 +180,35 @@ proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) =
proc parseMediaEntities(js: JsonNode; result: var Tweet) =
with mediaEntities, js{"media_entities"}:
+ var parsedMedia: MediaEntities
for mediaEntity in mediaEntities:
with mediaInfo, mediaEntity{"media_results", "result", "media_info"}:
case mediaInfo.getTypeName
of "ApiImage":
- result.photos.add Photo(
+ parsedMedia.addMedia(Photo(
url: mediaInfo{"original_img_url"}.getImageStr,
altText: mediaInfo{"alt_text"}.getStr
- )
+ ))
of "ApiVideo":
let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"}
- result.video = some Video(
+ parsedMedia.addMedia(Video(
available: status.getStr == "Available",
thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr,
+ title: mediaInfo{"alt_text"}.getStr,
durationMs: mediaInfo{"duration_millis"}.getInt,
variants: parseVideoVariants(mediaInfo{"variants"})
- )
+ ))
of "ApiGif":
- result.gif = some Gif(
+ parsedMedia.addMedia(Gif(
url: mediaInfo{"variants"}[0]{"url"}.getImageStr,
- thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr
- )
+ thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr,
+ altText: mediaInfo{"alt_text"}.getStr
+ ))
else: discard
+ if mediaEntities.len > 0 and parsedMedia.len == mediaEntities.len:
+ result.media = parsedMedia
+
# Remove media URLs from text
with mediaList, js{"legacy", "entities", "media"}:
for url in mediaList:
@@ -348,13 +364,13 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull();
let name = jsCard{"name"}.getStr
if "poll" in name:
if "image" in name:
- result.photos.add Photo(
+ result.media.addMedia(Photo(
url: jsCard{"binding_values", "image_large"}.getImageVal
- )
+ ))
result.poll = some parsePoll(jsCard)
elif name == "amplify":
- result.video = some parsePromoVideo(jsCard{"binding_values"})
+ result.media.addMedia(parsePromoVideo(jsCard{"binding_values"}))
else:
result.card = some parseCard(jsCard, js{"entities", "urls"})
diff --git a/src/parserutils.nim b/src/parserutils.nim
index 98dd2ee..ea37468 100644
--- a/src/parserutils.nim
+++ b/src/parserutils.nim
@@ -352,9 +352,7 @@ proc expandBirdwatchEntities*(text: string; entities: JsonNode): string =
proc extractGalleryPhoto*(t: Tweet): GalleryPhoto =
let url =
- if t.photos.len > 0: t.photos[0].url
- elif t.video.isSome: get(t.video).thumb
- elif t.gif.isSome: get(t.gif).thumb
+ if t.media.len > 0: t.media[0].getThumb
elif t.card.isSome: get(t.card).image
else: ""
diff --git a/src/routes/embed.nim b/src/routes/embed.nim
index 0527d3d..bdca60f 100644
--- a/src/routes/embed.nim
+++ b/src/routes/embed.nim
@@ -11,7 +11,7 @@ proc createEmbedRouter*(cfg: Config) =
router embed:
get "/i/videos/tweet/@id":
let tweet = await getGraphTweetResult(@"id")
- if tweet == nil or tweet.video.isNone:
+ if tweet == nil or not tweet.hasVideos:
resp Http404
resp renderVideoEmbed(tweet, cfg, request)
diff --git a/src/routes/status.nim b/src/routes/status.nim
index ce093cb..f7fb1bb 100644
--- a/src/routes/status.nim
+++ b/src/routes/status.nim
@@ -44,15 +44,19 @@ proc createStatusRouter*(cfg: Config) =
desc = conv.tweet.text
var
- images = conv.tweet.photos.mapIt(it.url)
+ images = conv.tweet.getPhotos.mapIt(it.url)
video = ""
- if conv.tweet.video.isSome():
- images = @[get(conv.tweet.video).thumb]
+ let
+ firstMediaKind = if conv.tweet.media.len > 0: conv.tweet.media[0].kind
+ else: photoMedia
+
+ if firstMediaKind == videoMedia:
+ images = @[conv.tweet.media[0].getThumb]
video = getVideoEmbed(cfg, conv.tweet.id)
- elif conv.tweet.gif.isSome():
- images = @[get(conv.tweet.gif).thumb]
- video = getPicUrl(get(conv.tweet.gif).url)
+ elif firstMediaKind == gifMedia:
+ images = @[conv.tweet.media[0].getThumb]
+ video = getPicUrl(conv.tweet.media[0].gif.url)
elif conv.tweet.card.isSome():
let card = conv.tweet.card.get()
if card.image.len > 0:
diff --git a/src/sass/tweet/embed.scss b/src/sass/tweet/embed.scss
index fbdbd41..bee23d3 100644
--- a/src/sass/tweet/embed.scss
+++ b/src/sass/tweet/embed.scss
@@ -11,7 +11,7 @@
left: 0%;
}
- .video-container {
+ .gallery-video > .attachment {
max-height: unset;
}
}
diff --git a/src/sass/tweet/media.scss b/src/sass/tweet/media.scss
index 040edda..d7b443e 100644
--- a/src/sass/tweet/media.scss
+++ b/src/sass/tweet/media.scss
@@ -10,9 +10,47 @@
max-width: 533px;
pointer-events: all;
- .still-image {
- width: 100%;
- align-self: center;
+ &.mixed-row {
+ .attachment {
+ min-width: 0;
+ min-height: 0;
+ flex: 1 1 0;
+ max-height: 379.5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #101010;
+ }
+
+ .still-image,
+ .still-image img,
+ .attachment > video,
+ .attachment > img {
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ max-height: none;
+ }
+
+ .still-image {
+ display: flex;
+ align-self: stretch;
+ }
+
+ .still-image img {
+ flex-basis: auto;
+ flex-grow: 0;
+ object-fit: cover;
+ }
+
+ .attachment > video,
+ .attachment > img {
+ object-fit: cover;
+ }
+
+ .attachment > video {
+ object-fit: contain;
+ }
}
}
@@ -28,10 +66,6 @@
background-color: var(--bg_color);
align-items: center;
pointer-events: all;
-
- .image-attachment {
- width: 100%;
- }
}
.attachment {
@@ -49,7 +83,14 @@
}
}
-.gallery-gif video {
+.media-gif {
+ display: table;
+ background-color: unset;
+ width: unset;
+ max-height: unset;
+}
+
+.media-gif video {
max-height: 530px;
background-color: #101010;
}
@@ -96,22 +137,6 @@
transition-property: max-height;
}
-.image {
- display: flex;
-}
-
-// .single-image {
-// display: inline-block;
-// width: 100%;
-// max-height: 600px;
-
-// .attachments {
-// width: unset;
-// max-height: unset;
-// display: inherit;
-// }
-// }
-
.overlay-circle {
border-radius: 50%;
background-color: var(--dark_grey);
@@ -133,12 +158,6 @@
margin-left: 14px;
}
-.media-gif {
- display: table;
- background-color: unset;
- width: unset;
-}
-
.media-body {
flex: 1;
padding: 0;
diff --git a/src/sass/tweet/quote.scss b/src/sass/tweet/quote.scss
index 2f3122a..d6a75a9 100644
--- a/src/sass/tweet/quote.scss
+++ b/src/sass/tweet/quote.scss
@@ -95,7 +95,7 @@
justify-content: center;
}
- .gallery-gif .attachment {
+ .media-gif > .attachment {
display: flex;
justify-content: center;
background-color: var(--bg_color);
@@ -108,8 +108,9 @@
}
}
- .gallery-video,
- .gallery-gif {
+ .gallery-row .attachment,
+ .gallery-row .attachment > video,
+ .gallery-row .attachment > img {
max-height: 300px;
}
diff --git a/src/sass/tweet/video.scss b/src/sass/tweet/video.scss
index ba77b14..c20d348 100644
--- a/src/sass/tweet/video.scss
+++ b/src/sass/tweet/video.scss
@@ -9,22 +9,22 @@ video {
.gallery-video {
display: flex;
overflow: hidden;
-}
+
+ &.card-container {
+ flex-direction: column;
+ width: 100%;
+ }
-.gallery-video.card-container {
- flex-direction: column;
- width: 100%;
-}
+ > .attachment {
+ min-height: 80px;
+ min-width: 200px;
+ max-height: 530px;
+ margin: 0;
-.video-container {
- min-height: 80px;
- min-width: 200px;
- max-height: 530px;
- margin: 0;
-
- img {
- max-height: 100%;
- max-width: 100%;
+ img {
+ max-height: 100%;
+ max-width: 100%;
+ }
}
}
diff --git a/src/types.nim b/src/types.nim
index 56bc2b2..3f655b6 100644
--- a/src/types.nim
+++ b/src/types.nim
@@ -136,11 +136,28 @@ type
Gif* = object
url*: string
thumb*: string
+ altText*: string
Photo* = object
url*: string
altText*: string
+ MediaKind* = enum
+ photoMedia
+ videoMedia
+ gifMedia
+
+ Media* = object
+ case kind*: MediaKind
+ of photoMedia:
+ photo*: Photo
+ of videoMedia:
+ video*: Video
+ of gifMedia:
+ gif*: Gif
+
+ MediaEntities* = seq[Media]
+
GalleryPhoto* = object
url*: string
tweetId*: string
@@ -219,9 +236,7 @@ type
quote*: Option[Tweet]
card*: Option[Card]
poll*: Option[Poll]
- gif*: Option[Gif]
- video*: Option[Video]
- photos*: seq[Photo]
+ media*: MediaEntities
history*: seq[int64]
note*: string
@@ -310,3 +325,24 @@ proc contains*(thread: Chain; tweet: Tweet): bool =
proc add*(timeline: var seq[Tweets]; tweet: Tweet) =
timeline.add @[tweet]
+
+proc getPhotos*(tweet: Tweet): seq[Photo] =
+ tweet.media.filterIt(it.kind == photoMedia).mapIt(it.photo)
+
+proc getVideos*(tweet: Tweet): seq[Video] =
+ tweet.media.filterIt(it.kind == videoMedia).mapIt(it.video)
+
+proc hasPhotos*(tweet: Tweet): bool =
+ tweet.media.anyIt(it.kind == photoMedia)
+
+proc hasVideos*(tweet: Tweet): bool =
+ tweet.media.anyIt(it.kind == videoMedia)
+
+proc hasGifs*(tweet: Tweet): bool =
+ tweet.media.anyIt(it.kind == gifMedia)
+
+proc getThumb*(media: Media): string =
+ case media.kind
+ of photoMedia: media.photo.url
+ of videoMedia: media.video.thumb
+ of gifMedia: media.gif.thumb
diff --git a/src/views/embed.nim b/src/views/embed.nim
index ba49f45..62cc76f 100644
--- a/src/views/embed.nim
+++ b/src/views/embed.nim
@@ -9,14 +9,17 @@ import general, tweet
const doctype = "\n"
proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string =
- let thumb = get(tweet.video).thumb
- let vidUrl = getVideoEmbed(cfg, tweet.id)
- let prefs = Prefs(hlsPlayback: true, mp4Playback: true)
+ let
+ video = tweet.getVideos()[0]
+ thumb = video.thumb
+ vidUrl = getVideoEmbed(cfg, tweet.id)
+ prefs = Prefs(hlsPlayback: true, mp4Playback: true)
+
let node = buildHtml(html(lang="en")):
renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb]))
body:
tdiv(class="embed-video"):
- renderVideo(get(tweet.video), prefs, "")
+ renderVideo(video, prefs, "")
result = doctype & $node
diff --git a/src/views/general.nim b/src/views/general.nim
index ac087c1..9671c73 100644
--- a/src/views/general.nim
+++ b/src/views/general.nim
@@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head):
- link(rel="stylesheet", type="text/css", href="/css/style.css?v=28")
+ link(rel="stylesheet", type="text/css", href="/css/style.css?v=29")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
if theme.len > 0:
diff --git a/src/views/rss.nimf b/src/views/rss.nimf
index 2c71591..4c1a86f 100644
--- a/src/views/rss.nimf
+++ b/src/views/rss.nimf
@@ -1,29 +1,38 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
## SPDX-License-Identifier: AGPL-3.0-only
-#import strutils, xmltree, strformat, options, unicode
+#import strutils, sequtils, xmltree, strformat, options, unicode
#import ../types, ../utils, ../formatters, ../prefs
## Snowflake ID cutoff for RSS GUID format transition
## Corresponds to approximately December 14, 2025 UTC
#const guidCutoff = 2000000000000000000'i64
#
#proc getTitle(tweet: Tweet; retweet: string): string =
-#if tweet.pinned: result = "Pinned: "
-#elif retweet.len > 0: result = &"RT by @{retweet}: "
-#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: "
+#var prefix = ""
+#if tweet.pinned: prefix = "Pinned: "
+#elif retweet.len > 0: prefix = &"RT by @{retweet}: "
+#elif tweet.reply.len > 0: prefix = &"R to @{tweet.reply[0]}: "
#end if
#var text = stripHtml(tweet.text)
##if unicode.runeLen(text) > 32:
## text = unicode.runeSubStr(text, 0, 32) & "..."
##end if
-#result &= xmltree.escape(text)
-#if result.len > 0: return
+#text = xmltree.escape(text)
+#if text.len > 0:
+# result = prefix & text
+# return
#end if
-#if tweet.photos.len > 0:
-# result &= "Image"
-#elif tweet.video.isSome:
-# result &= "Video"
-#elif tweet.gif.isSome:
-# result &= "Gif"
+#if tweet.media.len > 0:
+# result = prefix
+# let firstKind = tweet.media[0].kind
+# if tweet.media.anyIt(it.kind != firstKind):
+# result &= "Media"
+# else:
+# case firstKind
+# of photoMedia: result &= "Image"
+# of videoMedia: result &= "Video"
+# of gifMedia: result &= "Gif"
+# end case
+# end if
#end if
#end proc
#
@@ -31,6 +40,26 @@
Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
#end proc
#
+#proc renderRssMedia(media: Media; tweet: Tweet; urlPrefix: string): string =
+#case media.kind
+#of photoMedia:
+# let photo = media.photo
+
+#of videoMedia:
+# let video = media.video
+
+
Video
+
+
+#of gifMedia:
+# let gif = media.gif
+# let thumb = &"{urlPrefix}{getPicUrl(gif.thumb)}"
+# let url = &"{urlPrefix}{getPicUrl(gif.url)}"
+
+#end case
+#end proc
+#
#proc getTweetsWithPinned(profile: Profile): seq[Tweets] =
#result = profile.tweets.content
#if profile.pinned.isSome and result.len > 0:
@@ -54,20 +83,10 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
#let urlPrefix = getUrlPrefix(cfg)
#let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix)
${text.replace("\n", "
\n")}