mirror of
https://github.com/zedeus/nitter.git
synced 2024-06-09 16:49:21 +00:00
0633ec2c39
m3u8 videos only work when the proxy is enabled. Further, this allows video playback without Javascript. This is only done when proxying is disabled to avoid excessive memory usage on the nitter instance that would result from loading longer videos in a single chunk.
369 lines
13 KiB
Nim
369 lines
13 KiB
Nim
# SPDX-License-Identifier: AGPL-3.0-only
|
||
import strutils, sequtils, strformat, options
|
||
import karax/[karaxdsl, vdom, vstyles]
|
||
from jester import Request
|
||
|
||
import renderutils
|
||
import ".."/[types, utils, formatters]
|
||
import general
|
||
|
||
proc getSmallPic(url: string): string =
|
||
result = url
|
||
if "?" notin url and not url.endsWith("placeholder.png"):
|
||
result &= ":small"
|
||
result = getPicUrl(result)
|
||
|
||
proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
|
||
let url = getPicUrl(user.getUserPic("_mini"))
|
||
buildHtml():
|
||
img(class=(prefs.getAvatarClass & " mini"), src=url)
|
||
|
||
proc renderHeader(tweet: Tweet; retweet: string; prefs: Prefs): VNode =
|
||
buildHtml(tdiv):
|
||
if retweet.len > 0:
|
||
tdiv(class="retweet-header"):
|
||
span: icon "retweet", retweet & " retweeted"
|
||
|
||
if tweet.pinned:
|
||
tdiv(class="pinned"):
|
||
span: icon "pin", "Pinned Tweet"
|
||
|
||
tdiv(class="tweet-header"):
|
||
a(class="tweet-avatar", href=("/" & tweet.user.username)):
|
||
var size = "_bigger"
|
||
if not prefs.autoplayGifs and tweet.user.userPic.endsWith("gif"):
|
||
size = "_400x400"
|
||
genImg(tweet.user.getUserPic(size), class=prefs.getAvatarClass)
|
||
|
||
tdiv(class="tweet-name-row"):
|
||
tdiv(class="fullname-and-username"):
|
||
linkUser(tweet.user, class="fullname")
|
||
linkUser(tweet.user, class="username")
|
||
|
||
span(class="tweet-date"):
|
||
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)
|
||
|
||
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
|
||
orig = if named: photo else: photo & "?name=orig"
|
||
small = if named: photo else: photo & "?name=small"
|
||
a(href=getPicUrl(orig), class="still-image", target="_blank"):
|
||
genImg(small)
|
||
|
||
proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
|
||
case playbackType
|
||
of mp4: prefs.mp4Playback
|
||
of m3u8, vmap: prefs.hlsPlayback
|
||
|
||
proc hasMp4Url(video: Video): bool =
|
||
video.variants.anyIt(it.contentType == mp4)
|
||
|
||
proc renderVideoDisabled(video: Video; path: string): VNode =
|
||
buildHtml(tdiv(class="video-overlay")):
|
||
case video.playbackType
|
||
of mp4:
|
||
p: text "mp4 playback disabled in preferences"
|
||
of m3u8, vmap:
|
||
buttonReferer "/enablehls", "Enable hls playback", path
|
||
|
||
proc renderVideoUnavailable(video: Video): VNode =
|
||
buildHtml(tdiv(class="video-overlay")):
|
||
case video.reason
|
||
of "dmcaed":
|
||
p: text "This media has been disabled in response to a report by the copyright owner"
|
||
else:
|
||
p: text "This media is unavailable"
|
||
|
||
proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
||
let container =
|
||
if video.description.len > 0 or video.title.len > 0: " card-container"
|
||
else: ""
|
||
let playbackType =
|
||
if not prefs.proxyVideos and video.hasMp4Url: mp4
|
||
else: video.playbackType
|
||
|
||
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)
|
||
renderVideoUnavailable(video)
|
||
elif not prefs.isPlaybackEnabled(playbackType):
|
||
img(src=thumb)
|
||
renderVideoDisabled(video, path)
|
||
else:
|
||
let vid = video.variants.filterIt(it.contentType == playbackType)
|
||
let source = if prefs.proxyVideos: getVidUrl(vid[0].url) else: vid[0].url
|
||
case playbackType
|
||
of mp4:
|
||
if prefs.muteVideos:
|
||
video(poster=thumb, controls="", muted=""):
|
||
source(src=source, `type`="video/mp4")
|
||
else:
|
||
video(poster=thumb, controls=""):
|
||
source(src=source, `type`="video/mp4")
|
||
of m3u8, vmap:
|
||
video(poster=thumb, data-url=source, data-autoload="false")
|
||
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
||
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
||
verbatim "</div>"
|
||
if container.len > 0:
|
||
tdiv(class="card-content"):
|
||
h2(class="card-title"): text video.title
|
||
if video.description.len > 0:
|
||
p(class="card-description"): text video.description
|
||
|
||
proc renderGif(gif: Gif; prefs: Prefs): VNode =
|
||
buildHtml(tdiv(class="attachments media-gif")):
|
||
tdiv(class="gallery-gif", style={maxHeight: "unset"}):
|
||
tdiv(class="attachment"):
|
||
let thumb = getSmallPic(gif.thumb)
|
||
let url = getPicUrl(gif.url)
|
||
if prefs.autoplayGifs:
|
||
video(class="gif", poster=thumb, controls="", autoplay="", muted="", loop=""):
|
||
source(src=url, `type`="video/mp4")
|
||
else:
|
||
video(class="gif", poster=thumb, controls="", muted="", loop=""):
|
||
source(src=url, `type`="video/mp4")
|
||
|
||
proc renderPoll(poll: Poll): VNode =
|
||
buildHtml(tdiv(class="poll")):
|
||
for i in 0 ..< poll.options.len:
|
||
let
|
||
leader = if poll.leader == i: " leader" else: ""
|
||
val = poll.values[i]
|
||
perc = if val > 0: val / poll.votes * 100 else: 0
|
||
percStr = (&"{perc:>3.0f}").strip(chars={'.'}) & '%'
|
||
tdiv(class=("poll-meter" & leader)):
|
||
span(class="poll-choice-bar", style={width: percStr})
|
||
span(class="poll-choice-value"): text percStr
|
||
span(class="poll-choice-option"): text poll.options[i]
|
||
span(class="poll-info"):
|
||
text insertSep($poll.votes, ',') & " votes • " & poll.status
|
||
|
||
proc renderCardImage(card: Card): VNode =
|
||
buildHtml(tdiv(class="card-image-container")):
|
||
tdiv(class="card-image"):
|
||
img(src=getPicUrl(card.image), alt="")
|
||
if card.kind == player:
|
||
tdiv(class="card-overlay"):
|
||
tdiv(class="overlay-circle"):
|
||
span(class="overlay-triangle")
|
||
|
||
proc renderCardContent(card: Card): VNode =
|
||
buildHtml(tdiv(class="card-content")):
|
||
h2(class="card-title"): text card.title
|
||
if card.text.len > 0:
|
||
p(class="card-description"): text card.text
|
||
if card.dest.len > 0:
|
||
span(class="card-destination"): text card.dest
|
||
|
||
proc renderCard(card: Card; prefs: Prefs; path: string): VNode =
|
||
const smallCards = {app, player, summary, storeLink}
|
||
let large = if card.kind notin smallCards: " large" else: ""
|
||
let url = replaceUrls(card.url, prefs)
|
||
|
||
buildHtml(tdiv(class=("card" & large))):
|
||
if card.video.isSome:
|
||
tdiv(class="card-container"):
|
||
renderVideo(get(card.video), prefs, path)
|
||
a(class="card-content-container", href=url):
|
||
renderCardContent(card)
|
||
else:
|
||
a(class="card-container", href=url):
|
||
if card.image.len > 0:
|
||
renderCardImage(card)
|
||
tdiv(class="card-content-container"):
|
||
renderCardContent(card)
|
||
|
||
func formatStat(stat: int): string =
|
||
if stat > 0: insertSep($stat, ',')
|
||
else: ""
|
||
|
||
proc renderStats(stats: TweetStats; views: string): VNode =
|
||
buildHtml(tdiv(class="tweet-stats")):
|
||
span(class="tweet-stat"): icon "comment", formatStat(stats.replies)
|
||
span(class="tweet-stat"): icon "retweet", formatStat(stats.retweets)
|
||
span(class="tweet-stat"): icon "quote", formatStat(stats.quotes)
|
||
span(class="tweet-stat"): icon "heart", formatStat(stats.likes)
|
||
if views.len > 0:
|
||
span(class="tweet-stat"): icon "play", insertSep(views, ',')
|
||
|
||
proc renderReply(tweet: Tweet): VNode =
|
||
buildHtml(tdiv(class="replying-to")):
|
||
text "Replying to "
|
||
for i, u in tweet.reply:
|
||
if i > 0: text " "
|
||
a(href=("/" & u)): text "@" & u
|
||
|
||
proc renderAttribution(user: User; prefs: Prefs): VNode =
|
||
buildHtml(a(class="attribution", href=("/" & user.username))):
|
||
renderMiniAvatar(user, prefs)
|
||
strong: text user.fullname
|
||
if user.verified:
|
||
icon "ok", class="verified-icon", title="Verified account"
|
||
|
||
proc renderMediaTags(tags: seq[User]): VNode =
|
||
buildHtml(tdiv(class="media-tag-block")):
|
||
icon "user"
|
||
for i, p in tags:
|
||
a(class="media-tag", href=("/" & p.username), title=p.username):
|
||
text p.fullname
|
||
if i < tags.high:
|
||
text ", "
|
||
|
||
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)
|
||
|
||
proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||
if not quote.available:
|
||
return buildHtml(tdiv(class="quote unavailable")):
|
||
tdiv(class="unavailable-quote"):
|
||
if quote.tombstone.len > 0:
|
||
text quote.tombstone
|
||
elif quote.text.len > 0:
|
||
text quote.text
|
||
else:
|
||
text "This tweet is unavailable"
|
||
|
||
buildHtml(tdiv(class="quote quote-big")):
|
||
a(class="quote-link", href=getLink(quote))
|
||
|
||
tdiv(class="tweet-name-row"):
|
||
tdiv(class="fullname-and-username"):
|
||
renderMiniAvatar(quote.user, prefs)
|
||
linkUser(quote.user, class="fullname")
|
||
linkUser(quote.user, class="username")
|
||
|
||
span(class="tweet-date"):
|
||
a(href=getLink(quote), title=quote.getTime):
|
||
text quote.getShortTime
|
||
|
||
if quote.reply.len > 0:
|
||
renderReply(quote)
|
||
|
||
if quote.text.len > 0:
|
||
tdiv(class="quote-text", dir="auto"):
|
||
verbatim replaceUrls(quote.text, prefs)
|
||
|
||
if quote.hasThread:
|
||
a(class="show-thread", href=getLink(quote)):
|
||
text "Show this thread"
|
||
|
||
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
||
renderQuoteMedia(quote, prefs, path)
|
||
|
||
proc renderLocation*(tweet: Tweet): string =
|
||
let (place, url) = tweet.getLocation()
|
||
if place.len == 0: return
|
||
let node = buildHtml(span(class="tweet-geo")):
|
||
text " – at "
|
||
if url.len > 1:
|
||
a(href=url): text place
|
||
else:
|
||
text place
|
||
return $node
|
||
|
||
proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
||
last=false; showThread=false; mainTweet=false; afterTweet=false): VNode =
|
||
var divClass = class
|
||
if index == -1 or last:
|
||
divClass = "thread-last " & class
|
||
|
||
if not tweet.available:
|
||
return buildHtml(tdiv(class=divClass & "unavailable timeline-item")):
|
||
tdiv(class="unavailable-box"):
|
||
if tweet.tombstone.len > 0:
|
||
text tweet.tombstone
|
||
elif tweet.text.len > 0:
|
||
text tweet.text
|
||
else:
|
||
text "This tweet is unavailable"
|
||
|
||
if tweet.quote.isSome:
|
||
renderQuote(tweet.quote.get(), prefs, path)
|
||
|
||
let fullTweet = tweet
|
||
var retweet: string
|
||
var tweet = fullTweet
|
||
if tweet.retweet.isSome:
|
||
tweet = tweet.retweet.get
|
||
retweet = fullTweet.user.fullname
|
||
|
||
buildHtml(tdiv(class=("timeline-item " & divClass))):
|
||
if not mainTweet:
|
||
a(class="tweet-link", href=getLink(tweet))
|
||
|
||
tdiv(class="tweet-body"):
|
||
var views = ""
|
||
renderHeader(tweet, retweet, prefs)
|
||
|
||
if not afterTweet and index == 0 and tweet.reply.len > 0 and
|
||
(tweet.reply.len > 1 or tweet.reply[0] != tweet.user.username):
|
||
renderReply(tweet)
|
||
|
||
var tweetClass = "tweet-content media-body"
|
||
if prefs.bidiSupport:
|
||
tweetClass &= " tweet-bidi"
|
||
|
||
tdiv(class=tweetClass, dir="auto"):
|
||
verbatim replaceUrls(tweet.text, prefs) & renderLocation(tweet)
|
||
|
||
if tweet.attribution.isSome:
|
||
renderAttribution(tweet.attribution.get(), prefs)
|
||
|
||
if tweet.card.isSome:
|
||
renderCard(tweet.card.get(), prefs, path)
|
||
|
||
if tweet.photos.len > 0:
|
||
renderAlbum(tweet)
|
||
elif tweet.video.isSome:
|
||
renderVideo(tweet.video.get(), prefs, path)
|
||
views = tweet.video.get().views
|
||
elif tweet.gif.isSome:
|
||
renderGif(tweet.gif.get(), prefs)
|
||
views = "GIF"
|
||
|
||
if tweet.poll.isSome:
|
||
renderPoll(tweet.poll.get())
|
||
|
||
if tweet.quote.isSome:
|
||
renderQuote(tweet.quote.get(), prefs, path)
|
||
|
||
if mainTweet:
|
||
p(class="tweet-published"): text &"{getTime(tweet)} · {tweet.source}"
|
||
|
||
if tweet.mediaTags.len > 0:
|
||
renderMediaTags(tweet.mediaTags)
|
||
|
||
if not prefs.hideTweetStats:
|
||
renderStats(tweet.stats, views)
|
||
|
||
if showThread:
|
||
a(class="show-thread", href=("/i/status/" & $tweet.threadId)):
|
||
text "Show this thread"
|
||
|
||
proc renderTweetEmbed*(tweet: Tweet; path: string; prefs: Prefs; cfg: Config; req: Request): VNode =
|
||
buildHtml(tdiv(class="tweet-embed")):
|
||
renderHead(prefs, cfg, req)
|
||
renderTweet(tweet, prefs, path, mainTweet=true)
|