mirror of
https://github.com/zedeus/nitter
synced 2026-09-05 22:59:31 +00:00
parent
4bf3df94f8
commit
35a929c415
15 changed files with 305 additions and 175 deletions
|
|
@ -9,14 +9,17 @@ import general, tweet
|
|||
const doctype = "<!DOCTYPE html>\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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<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] =
|
||||
#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)
|
||||
<p>${text.replace("\n", "<br>\n")}</p>
|
||||
#if tweet.photos.len > 0:
|
||||
# for photo in tweet.photos:
|
||||
<img src="${urlPrefix}${getPicUrl(photo.url)}" style="max-width:250px;" />
|
||||
#if tweet.media.len > 0:
|
||||
# for media in tweet.media:
|
||||
${renderRssMedia(media, tweet, urlPrefix)}
|
||||
# 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:
|
||||
# let card = tweet.card.get()
|
||||
# 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):
|
||||
text tweet.getShortTime
|
||||
|
||||
proc renderAlbum(tweet: Tweet): VNode =
|
||||
let
|
||||
groups = if tweet.photos.len < 3: @[tweet.photos]
|
||||
else: tweet.photos.distribute(2)
|
||||
proc renderAltText(altText: string): VNode =
|
||||
buildHtml(p(class="alt-text")):
|
||||
text "ALT " & altText
|
||||
|
||||
buildHtml(tdiv(class="attachments")):
|
||||
for i, photos in groups:
|
||||
let margin = if i > 0: ".25em" else: ""
|
||||
tdiv(class="gallery-row", style={marginTop: margin}):
|
||||
for photo in photos:
|
||||
tdiv(class="attachment image"):
|
||||
let
|
||||
named = "name=" in photo.url
|
||||
small = if named: photo.url else: photo.url & smallWebp
|
||||
a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"):
|
||||
genImg(small, alt=photo.altText)
|
||||
if photo.altText.len > 0:
|
||||
p(class="alt-text"): text "ALT " & photo.altText
|
||||
proc renderPhotoAttachment(photo: Photo): VNode =
|
||||
buildHtml(tdiv(class="attachment")):
|
||||
let
|
||||
named = "name=" in photo.url
|
||||
small = if named: photo.url else: photo.url & smallWebp
|
||||
a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"):
|
||||
genImg(small, alt=photo.altText)
|
||||
if photo.altText.len > 0:
|
||||
renderAltText(photo.altText)
|
||||
|
||||
proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
|
||||
case playbackType
|
||||
|
|
@ -65,7 +60,7 @@ proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
|
|||
proc hasMp4Url(video: Video): bool =
|
||||
video.variants.anyIt(it.contentType == mp4)
|
||||
|
||||
proc renderVideoDisabled(playbackType: VideoType; path: string): VNode =
|
||||
proc renderVideoDisabled(playbackType: VideoType; path=""): VNode =
|
||||
buildHtml(tdiv(class="video-overlay")):
|
||||
case playbackType
|
||||
of mp4:
|
||||
|
|
@ -81,52 +76,97 @@ proc renderVideoUnavailable(video: Video): VNode =
|
|||
else:
|
||||
p: text "This media is unavailable"
|
||||
|
||||
proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
||||
proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""): VNode =
|
||||
let
|
||||
container = if video.description.len == 0 and video.title.len == 0: ""
|
||||
else: " card-container"
|
||||
playbackType = if not prefs.proxyVideos and video.hasMp4Url: mp4
|
||||
else: video.playbackType
|
||||
playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4
|
||||
else: videoData.playbackType
|
||||
thumb = getSmallPic(videoData.thumb)
|
||||
|
||||
buildHtml(tdiv(class="attachment")):
|
||||
if not videoData.available:
|
||||
img(src=thumb, loading="lazy")
|
||||
renderVideoUnavailable(videoData)
|
||||
elif not prefs.isPlaybackEnabled(playbackType):
|
||||
img(src=thumb, loading="lazy")
|
||||
renderVideoDisabled(playbackType, path)
|
||||
else:
|
||||
let
|
||||
vars = videoData.variants.filterIt(it.contentType == playbackType)
|
||||
vidUrl = vars.sortedByIt(it.resolution)[^1].url
|
||||
source = if prefs.proxyVideos: getVidUrl(vidUrl)
|
||||
else: vidUrl
|
||||
case playbackType
|
||||
of mp4:
|
||||
video(poster=thumb, controls="", muted=prefs.muteVideos):
|
||||
source(src=source, `type`="video/mp4")
|
||||
of m3u8, vmap:
|
||||
video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos)
|
||||
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
||||
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
||||
tdiv(class="overlay-duration"): text getDuration(videoData)
|
||||
verbatim "</div>"
|
||||
|
||||
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" & container):
|
||||
tdiv(class="attachment video-container"):
|
||||
let thumb = getSmallPic(video.thumb)
|
||||
if not video.available:
|
||||
img(src=thumb, loading="lazy")
|
||||
renderVideoUnavailable(video)
|
||||
elif not prefs.isPlaybackEnabled(playbackType):
|
||||
img(src=thumb, loading="lazy")
|
||||
renderVideoDisabled(playbackType, path)
|
||||
else:
|
||||
let
|
||||
vars = video.variants.filterIt(it.contentType == playbackType)
|
||||
vidUrl = vars.sortedByIt(it.resolution)[^1].url
|
||||
source = if prefs.proxyVideos: getVidUrl(vidUrl)
|
||||
else: vidUrl
|
||||
case playbackType
|
||||
of mp4:
|
||||
video(poster=thumb, controls="", muted=prefs.muteVideos):
|
||||
source(src=source, `type`="video/mp4")
|
||||
of m3u8, vmap:
|
||||
video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos)
|
||||
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
||||
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
||||
tdiv(class="overlay-duration"): text getDuration(video)
|
||||
verbatim "</div>"
|
||||
if container.len > 0:
|
||||
tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))):
|
||||
renderVideoAttachment(video, prefs, path)
|
||||
if hasCardContent:
|
||||
tdiv(class="card-content"):
|
||||
h2(class="card-title"): text video.title
|
||||
if video.description.len > 0:
|
||||
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 =
|
||||
buildHtml(tdiv(class="attachments media-gif")):
|
||||
tdiv(class="gallery-gif", style={maxHeight: "unset"}):
|
||||
tdiv(class="attachment"):
|
||||
video(class="gif", poster=getSmallPic(gif.thumb), autoplay=prefs.autoplayGifs,
|
||||
controls="", muted="", loop=""):
|
||||
source(src=getPicUrl(gif.url), `type`="video/mp4")
|
||||
renderGifAttachment(gif, prefs)
|
||||
|
||||
proc renderMedia(media: seq[Media]; prefs: Prefs; path: string): VNode =
|
||||
if media.len == 0:
|
||||
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 =
|
||||
buildHtml(tdiv(class="poll")):
|
||||
|
|
@ -219,12 +259,7 @@ proc renderLatestPost(username: string; id: int64): VNode =
|
|||
|
||||
proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
buildHtml(tdiv(class="quote-media-container")):
|
||||
if quote.photos.len > 0:
|
||||
renderAlbum(quote)
|
||||
elif quote.video.isSome:
|
||||
renderVideo(quote.video.get(), prefs, path)
|
||||
elif quote.gif.isSome:
|
||||
renderGif(quote.gif.get(), prefs)
|
||||
renderMedia(quote.media, prefs, path)
|
||||
|
||||
proc renderCommunityNote(note: string; prefs: Prefs): VNode =
|
||||
buildHtml(tdiv(class="community-note")):
|
||||
|
|
@ -266,7 +301,7 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
|||
tdiv(class="quote-text", dir="auto"):
|
||||
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)
|
||||
|
||||
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:
|
||||
renderCard(tweet.card.get(), prefs, path)
|
||||
|
||||
if tweet.photos.len > 0:
|
||||
renderAlbum(tweet)
|
||||
elif tweet.video.isSome:
|
||||
renderVideo(tweet.video.get(), prefs, path)
|
||||
elif tweet.gif.isSome:
|
||||
renderGif(tweet.gif.get(), prefs)
|
||||
if tweet.media.len > 0:
|
||||
renderMedia(tweet.media, prefs, path)
|
||||
|
||||
if tweet.poll.isSome:
|
||||
renderPoll(tweet.poll.get())
|
||||
|
|
|
|||
Loading…
Reference in a new issue