mirror of
https://github.com/zedeus/nitter.git
synced 2026-04-16 02:32:14 -04:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@ nitter.conf
|
|||||||
guest_accounts.json*
|
guest_accounts.json*
|
||||||
sessions.json*
|
sessions.json*
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
*.bak
|
||||||
|
/tools/*.json*
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ proc setDisableTid*(disable: bool) =
|
|||||||
disableTid = disable
|
disableTid = disable
|
||||||
|
|
||||||
proc setApiProxy*(url: string) =
|
proc setApiProxy*(url: string) =
|
||||||
|
apiProxy = ""
|
||||||
if url.len > 0:
|
if url.len > 0:
|
||||||
apiProxy = url.strip(chars={'/'}) & "/"
|
apiProxy = url.strip(chars={'/'}) & "/"
|
||||||
if "http" notin apiProxy:
|
if "http" notin apiProxy:
|
||||||
|
|||||||
@@ -140,27 +140,37 @@ proc parseVideo(js: JsonNode): Video =
|
|||||||
|
|
||||||
result.variants = parseVideoVariants(js{"video_info", "variants"})
|
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) =
|
proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) =
|
||||||
with jsMedia, js{"extended_entities", "media"}:
|
with jsMedia, js{"extended_entities", "media"}:
|
||||||
for m in jsMedia:
|
for m in jsMedia:
|
||||||
case m.getTypeName:
|
case m.getTypeName:
|
||||||
of "photo":
|
of "photo":
|
||||||
result.photos.add Photo(
|
result.media.addMedia(Photo(
|
||||||
url: m{"media_url_https"}.getImageStr,
|
url: m{"media_url_https"}.getImageStr,
|
||||||
altText: m{"ext_alt_text"}.getStr
|
altText: m{"ext_alt_text"}.getStr
|
||||||
)
|
))
|
||||||
of "video":
|
of "video":
|
||||||
result.video = some(parseVideo(m))
|
result.media.addMedia(parseVideo(m))
|
||||||
with user, m{"additional_media_info", "source_user"}:
|
with user, m{"additional_media_info", "source_user"}:
|
||||||
if user{"id"}.getInt > 0:
|
if user{"id"}.getInt > 0:
|
||||||
result.attribution = some(parseUser(user))
|
result.attribution = some(parseUser(user))
|
||||||
else:
|
else:
|
||||||
result.attribution = some(parseGraphUser(user))
|
result.attribution = some(parseGraphUser(user))
|
||||||
of "animated_gif":
|
of "animated_gif":
|
||||||
result.gif = some Gif(
|
result.media.addMedia(Gif(
|
||||||
url: m{"video_info", "variants"}[0]{"url"}.getImageStr,
|
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
|
else: discard
|
||||||
|
|
||||||
with url, m{"url"}:
|
with url, m{"url"}:
|
||||||
@@ -170,29 +180,35 @@ proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) =
|
|||||||
|
|
||||||
proc parseMediaEntities(js: JsonNode; result: var Tweet) =
|
proc parseMediaEntities(js: JsonNode; result: var Tweet) =
|
||||||
with mediaEntities, js{"media_entities"}:
|
with mediaEntities, js{"media_entities"}:
|
||||||
|
var parsedMedia: MediaEntities
|
||||||
for mediaEntity in mediaEntities:
|
for mediaEntity in mediaEntities:
|
||||||
with mediaInfo, mediaEntity{"media_results", "result", "media_info"}:
|
with mediaInfo, mediaEntity{"media_results", "result", "media_info"}:
|
||||||
case mediaInfo.getTypeName
|
case mediaInfo.getTypeName
|
||||||
of "ApiImage":
|
of "ApiImage":
|
||||||
result.photos.add Photo(
|
parsedMedia.addMedia(Photo(
|
||||||
url: mediaInfo{"original_img_url"}.getImageStr,
|
url: mediaInfo{"original_img_url"}.getImageStr,
|
||||||
altText: mediaInfo{"alt_text"}.getStr
|
altText: mediaInfo{"alt_text"}.getStr
|
||||||
)
|
))
|
||||||
of "ApiVideo":
|
of "ApiVideo":
|
||||||
let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"}
|
let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"}
|
||||||
result.video = some Video(
|
parsedMedia.addMedia(Video(
|
||||||
available: status.getStr == "Available",
|
available: status.getStr == "Available",
|
||||||
thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr,
|
thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr,
|
||||||
|
title: mediaInfo{"alt_text"}.getStr,
|
||||||
durationMs: mediaInfo{"duration_millis"}.getInt,
|
durationMs: mediaInfo{"duration_millis"}.getInt,
|
||||||
variants: parseVideoVariants(mediaInfo{"variants"})
|
variants: parseVideoVariants(mediaInfo{"variants"})
|
||||||
)
|
))
|
||||||
of "ApiGif":
|
of "ApiGif":
|
||||||
result.gif = some Gif(
|
parsedMedia.addMedia(Gif(
|
||||||
url: mediaInfo{"variants"}[0]{"url"}.getImageStr,
|
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
|
else: discard
|
||||||
|
|
||||||
|
if mediaEntities.len > 0 and parsedMedia.len == mediaEntities.len:
|
||||||
|
result.media = parsedMedia
|
||||||
|
|
||||||
# Remove media URLs from text
|
# Remove media URLs from text
|
||||||
with mediaList, js{"legacy", "entities", "media"}:
|
with mediaList, js{"legacy", "entities", "media"}:
|
||||||
for url in mediaList:
|
for url in mediaList:
|
||||||
@@ -348,13 +364,13 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull();
|
|||||||
let name = jsCard{"name"}.getStr
|
let name = jsCard{"name"}.getStr
|
||||||
if "poll" in name:
|
if "poll" in name:
|
||||||
if "image" in name:
|
if "image" in name:
|
||||||
result.photos.add Photo(
|
result.media.addMedia(Photo(
|
||||||
url: jsCard{"binding_values", "image_large"}.getImageVal
|
url: jsCard{"binding_values", "image_large"}.getImageVal
|
||||||
)
|
))
|
||||||
|
|
||||||
result.poll = some parsePoll(jsCard)
|
result.poll = some parsePoll(jsCard)
|
||||||
elif name == "amplify":
|
elif name == "amplify":
|
||||||
result.video = some parsePromoVideo(jsCard{"binding_values"})
|
result.media.addMedia(parsePromoVideo(jsCard{"binding_values"}))
|
||||||
else:
|
else:
|
||||||
result.card = some parseCard(jsCard, js{"entities", "urls"})
|
result.card = some parseCard(jsCard, js{"entities", "urls"})
|
||||||
|
|
||||||
|
|||||||
@@ -352,9 +352,7 @@ proc expandBirdwatchEntities*(text: string; entities: JsonNode): string =
|
|||||||
|
|
||||||
proc extractGalleryPhoto*(t: Tweet): GalleryPhoto =
|
proc extractGalleryPhoto*(t: Tweet): GalleryPhoto =
|
||||||
let url =
|
let url =
|
||||||
if t.photos.len > 0: t.photos[0].url
|
if t.media.len > 0: t.media[0].getThumb
|
||||||
elif t.video.isSome: get(t.video).thumb
|
|
||||||
elif t.gif.isSome: get(t.gif).thumb
|
|
||||||
elif t.card.isSome: get(t.card).image
|
elif t.card.isSome: get(t.card).image
|
||||||
else: ""
|
else: ""
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ proc createEmbedRouter*(cfg: Config) =
|
|||||||
router embed:
|
router embed:
|
||||||
get "/i/videos/tweet/@id":
|
get "/i/videos/tweet/@id":
|
||||||
let tweet = await getGraphTweetResult(@"id")
|
let tweet = await getGraphTweetResult(@"id")
|
||||||
if tweet == nil or tweet.video.isNone:
|
if tweet == nil or not tweet.hasVideos:
|
||||||
resp Http404
|
resp Http404
|
||||||
|
|
||||||
resp renderVideoEmbed(tweet, cfg, request)
|
resp renderVideoEmbed(tweet, cfg, request)
|
||||||
|
|||||||
@@ -44,15 +44,19 @@ proc createStatusRouter*(cfg: Config) =
|
|||||||
desc = conv.tweet.text
|
desc = conv.tweet.text
|
||||||
|
|
||||||
var
|
var
|
||||||
images = conv.tweet.photos.mapIt(it.url)
|
images = conv.tweet.getPhotos.mapIt(it.url)
|
||||||
video = ""
|
video = ""
|
||||||
|
|
||||||
if conv.tweet.video.isSome():
|
let
|
||||||
images = @[get(conv.tweet.video).thumb]
|
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)
|
video = getVideoEmbed(cfg, conv.tweet.id)
|
||||||
elif conv.tweet.gif.isSome():
|
elif firstMediaKind == gifMedia:
|
||||||
images = @[get(conv.tweet.gif).thumb]
|
images = @[conv.tweet.media[0].getThumb]
|
||||||
video = getPicUrl(get(conv.tweet.gif).url)
|
video = getPicUrl(conv.tweet.media[0].gif.url)
|
||||||
elif conv.tweet.card.isSome():
|
elif conv.tweet.card.isSome():
|
||||||
let card = conv.tweet.card.get()
|
let card = conv.tweet.card.get()
|
||||||
if card.image.len > 0:
|
if card.image.len > 0:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
left: 0%;
|
left: 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.gallery-video > .attachment {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,47 @@
|
|||||||
max-width: 533px;
|
max-width: 533px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
.still-image {
|
&.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%;
|
width: 100%;
|
||||||
align-self: center;
|
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);
|
background-color: var(--bg_color);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
.image-attachment {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment {
|
.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;
|
max-height: 530px;
|
||||||
background-color: #101010;
|
background-color: #101010;
|
||||||
}
|
}
|
||||||
@@ -96,22 +137,6 @@
|
|||||||
transition-property: max-height;
|
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 {
|
.overlay-circle {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--dark_grey);
|
background-color: var(--dark_grey);
|
||||||
@@ -133,12 +158,6 @@
|
|||||||
margin-left: 14px;
|
margin-left: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-gif {
|
|
||||||
display: table;
|
|
||||||
background-color: unset;
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-body {
|
.media-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-gif .attachment {
|
.media-gif > .attachment {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--bg_color);
|
background-color: var(--bg_color);
|
||||||
@@ -108,8 +108,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-video,
|
.gallery-row .attachment,
|
||||||
.gallery-gif {
|
.gallery-row .attachment > video,
|
||||||
|
.gallery-row .attachment > img {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ video {
|
|||||||
.gallery-video {
|
.gallery-video {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-video.card-container {
|
&.card-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
> .attachment {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-height: 530px;
|
max-height: 530px;
|
||||||
@@ -27,6 +26,7 @@ video {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.video-overlay {
|
.video-overlay {
|
||||||
@include play-button;
|
@include play-button;
|
||||||
|
|||||||
@@ -136,11 +136,28 @@ type
|
|||||||
Gif* = object
|
Gif* = object
|
||||||
url*: string
|
url*: string
|
||||||
thumb*: string
|
thumb*: string
|
||||||
|
altText*: string
|
||||||
|
|
||||||
Photo* = object
|
Photo* = object
|
||||||
url*: string
|
url*: string
|
||||||
altText*: 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
|
GalleryPhoto* = object
|
||||||
url*: string
|
url*: string
|
||||||
tweetId*: string
|
tweetId*: string
|
||||||
@@ -219,9 +236,7 @@ type
|
|||||||
quote*: Option[Tweet]
|
quote*: Option[Tweet]
|
||||||
card*: Option[Card]
|
card*: Option[Card]
|
||||||
poll*: Option[Poll]
|
poll*: Option[Poll]
|
||||||
gif*: Option[Gif]
|
media*: MediaEntities
|
||||||
video*: Option[Video]
|
|
||||||
photos*: seq[Photo]
|
|
||||||
history*: seq[int64]
|
history*: seq[int64]
|
||||||
note*: string
|
note*: string
|
||||||
|
|
||||||
@@ -310,3 +325,24 @@ proc contains*(thread: Chain; tweet: Tweet): bool =
|
|||||||
|
|
||||||
proc add*(timeline: var seq[Tweets]; tweet: Tweet) =
|
proc add*(timeline: var seq[Tweets]; tweet: Tweet) =
|
||||||
timeline.add @[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
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ import general, tweet
|
|||||||
const doctype = "<!DOCTYPE html>\n"
|
const doctype = "<!DOCTYPE html>\n"
|
||||||
|
|
||||||
proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string =
|
proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string =
|
||||||
let thumb = get(tweet.video).thumb
|
let
|
||||||
let vidUrl = getVideoEmbed(cfg, tweet.id)
|
video = tweet.getVideos()[0]
|
||||||
let prefs = Prefs(hlsPlayback: true, mp4Playback: true)
|
thumb = video.thumb
|
||||||
|
vidUrl = getVideoEmbed(cfg, tweet.id)
|
||||||
|
prefs = Prefs(hlsPlayback: true, mp4Playback: true)
|
||||||
|
|
||||||
let node = buildHtml(html(lang="en")):
|
let node = buildHtml(html(lang="en")):
|
||||||
renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb]))
|
renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb]))
|
||||||
|
|
||||||
body:
|
body:
|
||||||
tdiv(class="embed-video"):
|
tdiv(class="embed-video"):
|
||||||
renderVideo(get(tweet.video), prefs, "")
|
renderVideo(video, prefs, "")
|
||||||
|
|
||||||
result = doctype & $node
|
result = doctype & $node
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
|||||||
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
||||||
|
|
||||||
buildHtml(head):
|
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")
|
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
||||||
|
|
||||||
if theme.len > 0:
|
if theme.len > 0:
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
#? stdtmpl(subsChar = '$', metaChar = '#')
|
#? stdtmpl(subsChar = '$', metaChar = '#')
|
||||||
## SPDX-License-Identifier: AGPL-3.0-only
|
## 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
|
#import ../types, ../utils, ../formatters, ../prefs
|
||||||
## Snowflake ID cutoff for RSS GUID format transition
|
## Snowflake ID cutoff for RSS GUID format transition
|
||||||
## Corresponds to approximately December 14, 2025 UTC
|
## Corresponds to approximately December 14, 2025 UTC
|
||||||
#const guidCutoff = 2000000000000000000'i64
|
#const guidCutoff = 2000000000000000000'i64
|
||||||
#
|
#
|
||||||
#proc getTitle(tweet: Tweet; retweet: string): string =
|
#proc getTitle(tweet: Tweet; retweet: string): string =
|
||||||
#if tweet.pinned: result = "Pinned: "
|
#var prefix = ""
|
||||||
#elif retweet.len > 0: result = &"RT by @{retweet}: "
|
#if tweet.pinned: prefix = "Pinned: "
|
||||||
#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: "
|
#elif retweet.len > 0: prefix = &"RT by @{retweet}: "
|
||||||
|
#elif tweet.reply.len > 0: prefix = &"R to @{tweet.reply[0]}: "
|
||||||
#end if
|
#end if
|
||||||
#var text = stripHtml(tweet.text)
|
#var text = stripHtml(tweet.text)
|
||||||
##if unicode.runeLen(text) > 32:
|
##if unicode.runeLen(text) > 32:
|
||||||
## text = unicode.runeSubStr(text, 0, 32) & "..."
|
## text = unicode.runeSubStr(text, 0, 32) & "..."
|
||||||
##end if
|
##end if
|
||||||
#result &= xmltree.escape(text)
|
#text = xmltree.escape(text)
|
||||||
#if result.len > 0: return
|
#if text.len > 0:
|
||||||
|
# result = prefix & text
|
||||||
|
# return
|
||||||
|
#end if
|
||||||
|
#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
|
||||||
#if tweet.photos.len > 0:
|
|
||||||
# result &= "Image"
|
|
||||||
#elif tweet.video.isSome:
|
|
||||||
# result &= "Video"
|
|
||||||
#elif tweet.gif.isSome:
|
|
||||||
# result &= "Gif"
|
|
||||||
#end if
|
#end if
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
@@ -31,6 +40,26 @@
|
|||||||
Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
|
Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
|
#proc renderRssMedia(media: Media; tweet: Tweet; urlPrefix: string): string =
|
||||||
|
#case media.kind
|
||||||
|
#of photoMedia:
|
||||||
|
# let photo = media.photo
|
||||||
|
<img src="${urlPrefix}${getPicUrl(photo.url)}" style="max-width:250px;" />
|
||||||
|
#of videoMedia:
|
||||||
|
# let video = media.video
|
||||||
|
<a href="${urlPrefix}${tweet.getLink}">
|
||||||
|
<br>Video<br>
|
||||||
|
<img src="${urlPrefix}${getPicUrl(video.thumb)}" style="max-width:250px;" />
|
||||||
|
</a>
|
||||||
|
#of gifMedia:
|
||||||
|
# let gif = media.gif
|
||||||
|
# let thumb = &"{urlPrefix}{getPicUrl(gif.thumb)}"
|
||||||
|
# let url = &"{urlPrefix}{getPicUrl(gif.url)}"
|
||||||
|
<video poster="${thumb}" autoplay muted loop style="max-width:250px;">
|
||||||
|
<source src="${url}" type="video/mp4"></video>
|
||||||
|
#end case
|
||||||
|
#end proc
|
||||||
|
#
|
||||||
#proc getTweetsWithPinned(profile: Profile): seq[Tweets] =
|
#proc getTweetsWithPinned(profile: Profile): seq[Tweets] =
|
||||||
#result = profile.tweets.content
|
#result = profile.tweets.content
|
||||||
#if profile.pinned.isSome and result.len > 0:
|
#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 urlPrefix = getUrlPrefix(cfg)
|
||||||
#let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix)
|
#let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix)
|
||||||
<p>${text.replace("\n", "<br>\n")}</p>
|
<p>${text.replace("\n", "<br>\n")}</p>
|
||||||
#if tweet.photos.len > 0:
|
#if tweet.media.len > 0:
|
||||||
# for photo in tweet.photos:
|
# for media in tweet.media:
|
||||||
<img src="${urlPrefix}${getPicUrl(photo.url)}" style="max-width:250px;" />
|
${renderRssMedia(media, tweet, urlPrefix)}
|
||||||
# end for
|
# end for
|
||||||
#elif tweet.video.isSome:
|
|
||||||
<a href="${urlPrefix}${tweet.getLink}">
|
|
||||||
<br>Video<br>
|
|
||||||
<img src="${urlPrefix}${getPicUrl(get(tweet.video).thumb)}" style="max-width:250px;" />
|
|
||||||
</a>
|
|
||||||
#elif tweet.gif.isSome:
|
|
||||||
# let thumb = &"{urlPrefix}{getPicUrl(get(tweet.gif).thumb)}"
|
|
||||||
# let url = &"{urlPrefix}{getPicUrl(get(tweet.gif).url)}"
|
|
||||||
<video poster="${thumb}" autoplay muted loop style="max-width:250px;">
|
|
||||||
<source src="${url}" type="video/mp4"></video>
|
|
||||||
#elif tweet.card.isSome:
|
#elif tweet.card.isSome:
|
||||||
# let card = tweet.card.get()
|
# let card = tweet.card.get()
|
||||||
# if card.image.len > 0:
|
# if card.image.len > 0:
|
||||||
|
|||||||
@@ -38,24 +38,19 @@ proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VN
|
|||||||
a(href=getLink(tweet), title=tweet.getTime):
|
a(href=getLink(tweet), title=tweet.getTime):
|
||||||
text tweet.getShortTime
|
text tweet.getShortTime
|
||||||
|
|
||||||
proc renderAlbum(tweet: Tweet): VNode =
|
proc renderAltText(altText: string): VNode =
|
||||||
let
|
buildHtml(p(class="alt-text")):
|
||||||
groups = if tweet.photos.len < 3: @[tweet.photos]
|
text "ALT " & altText
|
||||||
else: tweet.photos.distribute(2)
|
|
||||||
|
|
||||||
buildHtml(tdiv(class="attachments")):
|
proc renderPhotoAttachment(photo: Photo): VNode =
|
||||||
for i, photos in groups:
|
buildHtml(tdiv(class="attachment")):
|
||||||
let margin = if i > 0: ".25em" else: ""
|
|
||||||
tdiv(class="gallery-row", style={marginTop: margin}):
|
|
||||||
for photo in photos:
|
|
||||||
tdiv(class="attachment image"):
|
|
||||||
let
|
let
|
||||||
named = "name=" in photo.url
|
named = "name=" in photo.url
|
||||||
small = if named: photo.url else: photo.url & smallWebp
|
small = if named: photo.url else: photo.url & smallWebp
|
||||||
a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"):
|
a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"):
|
||||||
genImg(small, alt=photo.altText)
|
genImg(small, alt=photo.altText)
|
||||||
if photo.altText.len > 0:
|
if photo.altText.len > 0:
|
||||||
p(class="alt-text"): text "ALT " & photo.altText
|
renderAltText(photo.altText)
|
||||||
|
|
||||||
proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
|
proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
|
||||||
case playbackType
|
case playbackType
|
||||||
@@ -65,7 +60,7 @@ proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
|
|||||||
proc hasMp4Url(video: Video): bool =
|
proc hasMp4Url(video: Video): bool =
|
||||||
video.variants.anyIt(it.contentType == mp4)
|
video.variants.anyIt(it.contentType == mp4)
|
||||||
|
|
||||||
proc renderVideoDisabled(playbackType: VideoType; path: string): VNode =
|
proc renderVideoDisabled(playbackType: VideoType; path=""): VNode =
|
||||||
buildHtml(tdiv(class="video-overlay")):
|
buildHtml(tdiv(class="video-overlay")):
|
||||||
case playbackType
|
case playbackType
|
||||||
of mp4:
|
of mp4:
|
||||||
@@ -81,26 +76,22 @@ proc renderVideoUnavailable(video: Video): VNode =
|
|||||||
else:
|
else:
|
||||||
p: text "This media is unavailable"
|
p: text "This media is unavailable"
|
||||||
|
|
||||||
proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""): VNode =
|
||||||
let
|
let
|
||||||
container = if video.description.len == 0 and video.title.len == 0: ""
|
playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4
|
||||||
else: " card-container"
|
else: videoData.playbackType
|
||||||
playbackType = if not prefs.proxyVideos and video.hasMp4Url: mp4
|
thumb = getSmallPic(videoData.thumb)
|
||||||
else: video.playbackType
|
|
||||||
|
|
||||||
buildHtml(tdiv(class="attachments card")):
|
buildHtml(tdiv(class="attachment")):
|
||||||
tdiv(class="gallery-video" & container):
|
if not videoData.available:
|
||||||
tdiv(class="attachment video-container"):
|
|
||||||
let thumb = getSmallPic(video.thumb)
|
|
||||||
if not video.available:
|
|
||||||
img(src=thumb, loading="lazy")
|
img(src=thumb, loading="lazy")
|
||||||
renderVideoUnavailable(video)
|
renderVideoUnavailable(videoData)
|
||||||
elif not prefs.isPlaybackEnabled(playbackType):
|
elif not prefs.isPlaybackEnabled(playbackType):
|
||||||
img(src=thumb, loading="lazy")
|
img(src=thumb, loading="lazy")
|
||||||
renderVideoDisabled(playbackType, path)
|
renderVideoDisabled(playbackType, path)
|
||||||
else:
|
else:
|
||||||
let
|
let
|
||||||
vars = video.variants.filterIt(it.contentType == playbackType)
|
vars = videoData.variants.filterIt(it.contentType == playbackType)
|
||||||
vidUrl = vars.sortedByIt(it.resolution)[^1].url
|
vidUrl = vars.sortedByIt(it.resolution)[^1].url
|
||||||
source = if prefs.proxyVideos: getVidUrl(vidUrl)
|
source = if prefs.proxyVideos: getVidUrl(vidUrl)
|
||||||
else: vidUrl
|
else: vidUrl
|
||||||
@@ -112,21 +103,70 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
|||||||
video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos)
|
video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos)
|
||||||
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
||||||
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
||||||
tdiv(class="overlay-duration"): text getDuration(video)
|
tdiv(class="overlay-duration"): text getDuration(videoData)
|
||||||
verbatim "</div>"
|
verbatim "</div>"
|
||||||
if container.len > 0:
|
|
||||||
|
proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
||||||
|
let hasCardContent = video.description.len > 0 or video.title.len > 0
|
||||||
|
|
||||||
|
buildHtml(tdiv(class="attachments card")):
|
||||||
|
tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))):
|
||||||
|
renderVideoAttachment(video, prefs, path)
|
||||||
|
if hasCardContent:
|
||||||
tdiv(class="card-content"):
|
tdiv(class="card-content"):
|
||||||
h2(class="card-title"): text video.title
|
h2(class="card-title"): text video.title
|
||||||
if video.description.len > 0:
|
if video.description.len > 0:
|
||||||
p(class="card-description"): text video.description
|
p(class="card-description"): text video.description
|
||||||
|
|
||||||
|
proc renderGifAttachment(gif: Gif; prefs: Prefs): VNode =
|
||||||
|
let thumb = getSmallPic(gif.thumb)
|
||||||
|
|
||||||
|
buildHtml(tdiv(class="attachment")):
|
||||||
|
if not prefs.mp4Playback:
|
||||||
|
img(src=thumb, loading="lazy")
|
||||||
|
renderVideoDisabled(mp4)
|
||||||
|
elif prefs.autoplayGifs:
|
||||||
|
video(class="gif", poster=thumb, autoplay="", muted="", loop=""):
|
||||||
|
source(src=getPicUrl(gif.url), `type`="video/mp4")
|
||||||
|
else:
|
||||||
|
video(class="gif", poster=thumb, controls="", muted="", loop=""):
|
||||||
|
source(src=getPicUrl(gif.url), `type`="video/mp4")
|
||||||
|
if gif.altText.len > 0:
|
||||||
|
renderAltText(gif.altText)
|
||||||
|
|
||||||
proc renderGif(gif: Gif; prefs: Prefs): VNode =
|
proc renderGif(gif: Gif; prefs: Prefs): VNode =
|
||||||
buildHtml(tdiv(class="attachments media-gif")):
|
buildHtml(tdiv(class="attachments media-gif")):
|
||||||
tdiv(class="gallery-gif", style={maxHeight: "unset"}):
|
renderGifAttachment(gif, prefs)
|
||||||
tdiv(class="attachment"):
|
|
||||||
video(class="gif", poster=getSmallPic(gif.thumb), autoplay=prefs.autoplayGifs,
|
proc renderMedia(media: seq[Media]; prefs: Prefs; path: string): VNode =
|
||||||
controls="", muted="", loop=""):
|
if media.len == 0:
|
||||||
source(src=getPicUrl(gif.url), `type`="video/mp4")
|
return nil
|
||||||
|
|
||||||
|
if media.len == 1:
|
||||||
|
let item = media[0]
|
||||||
|
if item.kind == videoMedia:
|
||||||
|
return renderVideo(item.video, prefs, path)
|
||||||
|
if item.kind == gifMedia:
|
||||||
|
return renderGif(item.gif, prefs)
|
||||||
|
|
||||||
|
let
|
||||||
|
groups = if media.len < 3: @[media]
|
||||||
|
else: media.distribute(2)
|
||||||
|
|
||||||
|
buildHtml(tdiv(class="attachments")):
|
||||||
|
for i, mediaGroup in groups:
|
||||||
|
let margin = if i > 0: ".25em" else: ""
|
||||||
|
let rowClass = "gallery-row" &
|
||||||
|
(if mediaGroup.allIt(it.kind == photoMedia): "" else: " mixed-row")
|
||||||
|
tdiv(class=rowClass, style={marginTop: margin}):
|
||||||
|
for mediaItem in mediaGroup:
|
||||||
|
case mediaItem.kind
|
||||||
|
of photoMedia:
|
||||||
|
renderPhotoAttachment(mediaItem.photo)
|
||||||
|
of videoMedia:
|
||||||
|
renderVideoAttachment(mediaItem.video, prefs, path)
|
||||||
|
of gifMedia:
|
||||||
|
renderGifAttachment(mediaItem.gif, prefs)
|
||||||
|
|
||||||
proc renderPoll(poll: Poll): VNode =
|
proc renderPoll(poll: Poll): VNode =
|
||||||
buildHtml(tdiv(class="poll")):
|
buildHtml(tdiv(class="poll")):
|
||||||
@@ -219,12 +259,7 @@ proc renderLatestPost(username: string; id: int64): VNode =
|
|||||||
|
|
||||||
proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||||
buildHtml(tdiv(class="quote-media-container")):
|
buildHtml(tdiv(class="quote-media-container")):
|
||||||
if quote.photos.len > 0:
|
renderMedia(quote.media, prefs, path)
|
||||||
renderAlbum(quote)
|
|
||||||
elif quote.video.isSome:
|
|
||||||
renderVideo(quote.video.get(), prefs, path)
|
|
||||||
elif quote.gif.isSome:
|
|
||||||
renderGif(quote.gif.get(), prefs)
|
|
||||||
|
|
||||||
proc renderCommunityNote(note: string; prefs: Prefs): VNode =
|
proc renderCommunityNote(note: string; prefs: Prefs): VNode =
|
||||||
buildHtml(tdiv(class="community-note")):
|
buildHtml(tdiv(class="community-note")):
|
||||||
@@ -266,7 +301,7 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
|||||||
tdiv(class="quote-text", dir="auto"):
|
tdiv(class="quote-text", dir="auto"):
|
||||||
verbatim replaceUrls(quote.text, prefs)
|
verbatim replaceUrls(quote.text, prefs)
|
||||||
|
|
||||||
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
if quote.media.len > 0:
|
||||||
renderQuoteMedia(quote, prefs, path)
|
renderQuoteMedia(quote, prefs, path)
|
||||||
|
|
||||||
if quote.note.len > 0 and not prefs.hideCommunityNotes:
|
if quote.note.len > 0 and not prefs.hideCommunityNotes:
|
||||||
@@ -344,12 +379,8 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
|||||||
if tweet.card.isSome and tweet.card.get().kind != hidden:
|
if tweet.card.isSome and tweet.card.get().kind != hidden:
|
||||||
renderCard(tweet.card.get(), prefs, path)
|
renderCard(tweet.card.get(), prefs, path)
|
||||||
|
|
||||||
if tweet.photos.len > 0:
|
if tweet.media.len > 0:
|
||||||
renderAlbum(tweet)
|
renderMedia(tweet.media, prefs, path)
|
||||||
elif tweet.video.isSome:
|
|
||||||
renderVideo(tweet.video.get(), prefs, path)
|
|
||||||
elif tweet.gif.isSome:
|
|
||||||
renderGif(tweet.gif.get(), prefs)
|
|
||||||
|
|
||||||
if tweet.poll.isSome:
|
if tweet.poll.isSome:
|
||||||
renderPoll(tweet.poll.get())
|
renderPoll(tweet.poll.get())
|
||||||
|
|||||||
Reference in New Issue
Block a user