mirror of
https://github.com/zedeus/nitter.git
synced 2024-06-10 09:09:21 +00:00
Merge 401aa26464
into b62d73dbd3
This commit is contained in:
commit
8659ec6b9e
|
@ -84,8 +84,8 @@ proc parseVideo(js: JsonNode): Video =
|
|||
views: getVideoViewCount(js),
|
||||
available: true,
|
||||
title: js{"ext_alt_text"}.getStr,
|
||||
durationMs: js{"video_info", "duration_millis"}.getInt
|
||||
# playbackType: mp4
|
||||
durationMs: js{"video_info", "duration_millis"}.getInt,
|
||||
playbackType: m3u8
|
||||
)
|
||||
|
||||
with status, js{"ext_media_availability", "status"}:
|
||||
|
@ -103,6 +103,9 @@ proc parseVideo(js: JsonNode): Video =
|
|||
contentType = parseEnum[VideoType](v{"content_type"}.getStr("summary"))
|
||||
url = v{"url"}.getStr
|
||||
|
||||
if contentType == mp4:
|
||||
result.playbackType = mp4
|
||||
|
||||
result.variants.add VideoVariant(
|
||||
contentType: contentType,
|
||||
bitrate: v{"bitrate"}.getInt,
|
||||
|
|
|
@ -80,7 +80,7 @@ genPrefs:
|
|||
|
||||
Media:
|
||||
mp4Playback(checkbox, true):
|
||||
"Enable mp4 video playback (only for gifs)"
|
||||
"Enable mp4 video playback"
|
||||
|
||||
hlsPlayback(checkbox, false):
|
||||
"Enable HLS video streaming (requires JavaScript)"
|
||||
|
|
|
@ -12,7 +12,8 @@ export httpclient, os, strutils, asyncstreams, base64, re
|
|||
|
||||
const
|
||||
m3u8Mime* = "application/vnd.apple.mpegurl"
|
||||
maxAge* = "max-age=604800"
|
||||
mp4Mime* = "video/mp4"
|
||||
maxAge* = "public, max-age=604800, must-revalidate"
|
||||
|
||||
proc safeFetch*(url: string): Future[string] {.async.} =
|
||||
let client = newAsyncHttpClient()
|
||||
|
@ -20,56 +21,84 @@ proc safeFetch*(url: string): Future[string] {.async.} =
|
|||
except: discard
|
||||
finally: client.close()
|
||||
|
||||
template respond*(req: asynchttpserver.Request; headers) =
|
||||
var msg = "HTTP/1.1 200 OK\c\L"
|
||||
for k, v in headers:
|
||||
template respond*(req: asynchttpserver.Request; code: HttpCode;
|
||||
headers: seq[(string, string)]) =
|
||||
var msg = "HTTP/1.1 " & $code & "\c\L"
|
||||
for (k, v) in headers:
|
||||
msg.add(k & ": " & v & "\c\L")
|
||||
|
||||
msg.add "\c\L"
|
||||
yield req.client.send(msg)
|
||||
yield req.client.send(msg, flags={})
|
||||
|
||||
proc getContentLength(res: AsyncResponse): string =
|
||||
result = "0"
|
||||
if res.headers.hasKey("content-length"):
|
||||
result = $res.contentLength
|
||||
elif res.headers.hasKey("content-range"):
|
||||
result = res.headers["content-range"]
|
||||
result = result[result.find('/') + 1 .. ^1]
|
||||
if result == "*":
|
||||
result.setLen(0)
|
||||
|
||||
proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
|
||||
result = Http200
|
||||
|
||||
let
|
||||
request = req.getNativeReq()
|
||||
client = newAsyncHttpClient()
|
||||
hashed = $hash(url)
|
||||
|
||||
if request.headers.getOrDefault("If-None-Match") == hashed:
|
||||
return Http304
|
||||
|
||||
let c = newAsyncHttpClient(headers=newHttpHeaders({
|
||||
"accept": "*/*",
|
||||
"range": $req.headers.getOrDefault("range")
|
||||
}))
|
||||
|
||||
try:
|
||||
let res = await client.get(url)
|
||||
if res.status != "200 OK":
|
||||
var res = await c.get(url)
|
||||
if not res.status.startsWith("20"):
|
||||
return Http404
|
||||
|
||||
let hashed = $hash(url)
|
||||
if request.headers.getOrDefault("If-None-Match") == hashed:
|
||||
return Http304
|
||||
var headers = @{
|
||||
"accept-ranges": "bytes",
|
||||
"content-type": $res.headers.getOrDefault("content-type"),
|
||||
"cache-control": maxAge,
|
||||
"age": $res.headers.getOrDefault("age"),
|
||||
"date": $res.headers.getOrDefault("date"),
|
||||
"last-modified": $res.headers.getOrDefault("last-modified")
|
||||
}
|
||||
|
||||
let contentLength =
|
||||
if res.headers.hasKey("content-length"):
|
||||
res.headers["content-length", 0]
|
||||
else:
|
||||
""
|
||||
var tries = 0
|
||||
while tries <= 10 and res.headers.hasKey("transfer-encoding"):
|
||||
await sleepAsync(100 + tries * 200)
|
||||
res = await c.get(url)
|
||||
tries.inc
|
||||
|
||||
let headers = newHttpHeaders({
|
||||
"Content-Type": res.headers["content-type", 0],
|
||||
"Content-Length": contentLength,
|
||||
"Cache-Control": maxAge,
|
||||
"ETag": hashed
|
||||
})
|
||||
let contentLength = res.getContentLength
|
||||
if contentLength.len > 0:
|
||||
headers.add ("content-length", contentLength)
|
||||
|
||||
respond(request, headers)
|
||||
if res.headers.hasKey("content-range"):
|
||||
headers.add ("content-range", $res.headers.getOrDefault("content-range"))
|
||||
respond(request, Http206, headers)
|
||||
else:
|
||||
respond(request, Http200, headers)
|
||||
|
||||
var (hasValue, data) = (true, "")
|
||||
while hasValue:
|
||||
(hasValue, data) = await res.bodyStream.read()
|
||||
if hasValue:
|
||||
await request.client.send(data)
|
||||
await request.client.send(data, flags={})
|
||||
data.setLen 0
|
||||
except HttpRequestError, ProtocolError, OSError:
|
||||
except OSError: discard
|
||||
except ProtocolError, HttpRequestError:
|
||||
result = Http404
|
||||
finally:
|
||||
client.close()
|
||||
c.close()
|
||||
|
||||
template check*(code): untyped =
|
||||
template check*(c): untyped =
|
||||
let code = c
|
||||
if code != Http200:
|
||||
resp code
|
||||
else:
|
||||
|
@ -83,37 +112,27 @@ proc decoded*(req: jester.Request; index: int): string =
|
|||
if based: decode(encoded)
|
||||
else: decodeUrl(encoded)
|
||||
|
||||
proc getPicUrl*(req: jester.Request): string =
|
||||
result = decoded(req, 1)
|
||||
if "twimg.com" notin result:
|
||||
result.insert(twimg)
|
||||
if not result.startsWith(https):
|
||||
result.insert(https)
|
||||
|
||||
proc createMediaRouter*(cfg: Config) =
|
||||
router media:
|
||||
get "/pic/?":
|
||||
resp Http404
|
||||
|
||||
get re"^\/pic\/orig\/(enc)?\/?(.+)":
|
||||
var url = decoded(request, 1)
|
||||
if "twimg.com" notin url:
|
||||
url.insert(twimg)
|
||||
if not url.startsWith(https):
|
||||
url.insert(https)
|
||||
url.add("?name=orig")
|
||||
|
||||
let uri = parseUri(url)
|
||||
cond isTwitterUrl(uri) == true
|
||||
|
||||
let code = await proxyMedia(request, url)
|
||||
check code
|
||||
let url = getPicUrl(request)
|
||||
cond isTwitterUrl(parseUri(url)) == true
|
||||
check await proxyMedia(request, url & "?name=orig")
|
||||
|
||||
get re"^\/pic\/(enc)?\/?(.+)":
|
||||
var url = decoded(request, 1)
|
||||
if "twimg.com" notin url:
|
||||
url.insert(twimg)
|
||||
if not url.startsWith(https):
|
||||
url.insert(https)
|
||||
|
||||
let uri = parseUri(url)
|
||||
cond isTwitterUrl(uri) == true
|
||||
|
||||
let code = await proxyMedia(request, url)
|
||||
check code
|
||||
let url = getPicUrl(request)
|
||||
cond isTwitterUrl(parseUri(url)) == true
|
||||
check await proxyMedia(request, url)
|
||||
|
||||
get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
|
||||
let url = decoded(request, 2)
|
||||
|
@ -123,8 +142,7 @@ proc createMediaRouter*(cfg: Config) =
|
|||
resp showError("Failed to verify signature", cfg)
|
||||
|
||||
if ".mp4" in url or ".ts" in url or ".m4s" in url:
|
||||
let code = await proxyMedia(request, url)
|
||||
check code
|
||||
check await proxyMedia(request, url)
|
||||
|
||||
var content: string
|
||||
if ".vmap" in url:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import strutils, strformat, uri, tables, base64
|
||||
import nimcrypto
|
||||
import types
|
||||
|
||||
var
|
||||
hmacKey: string
|
||||
|
@ -28,6 +29,20 @@ proc setProxyEncoding*(state: bool) =
|
|||
proc getHmac*(data: string): string =
|
||||
($hmac(sha256, hmacKey, data))[0 .. 12]
|
||||
|
||||
proc getBestMp4VidVariant(video: Video): VideoVariant =
|
||||
for v in video.variants:
|
||||
if v.bitrate >= result.bitrate:
|
||||
result = v
|
||||
|
||||
proc getVidVariant*(video: Video; playbackType: VideoType): VideoVariant =
|
||||
case playbackType
|
||||
of mp4:
|
||||
return video.getBestMp4VidVariant
|
||||
of m3u8, vmap:
|
||||
for variant in video.variants:
|
||||
if variant.contentType == playbackType:
|
||||
return variant
|
||||
|
||||
proc getVidUrl*(link: string): string =
|
||||
if link.len == 0: return
|
||||
let sig = getHmac(link)
|
||||
|
|
|
@ -84,7 +84,7 @@ proc genDate*(pref, state: string): VNode =
|
|||
|
||||
proc genImg*(url: string; class=""): VNode =
|
||||
buildHtml():
|
||||
img(src=getPicUrl(url), class=class, alt="")
|
||||
img(src=getPicUrl(url), class=class, alt="", loading="lazy", decoding="async")
|
||||
|
||||
proc getTabClass*(query: Query; tab: QueryKind): string =
|
||||
if query.kind == tab: "tab-item active"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import strutils, sequtils, strformat, options, algorithm
|
||||
import strutils, sequtils, strformat, options
|
||||
import karax/[karaxdsl, vdom, vstyles]
|
||||
from jester import Request
|
||||
|
||||
|
@ -12,7 +12,7 @@ const doctype = "<!DOCTYPE html>\n"
|
|||
proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
|
||||
let url = getPicUrl(user.getUserPic("_mini"))
|
||||
buildHtml():
|
||||
img(class=(prefs.getAvatarClass & " mini"), src=url)
|
||||
img(class=(prefs.getAvatarClass & " mini"), src=url, loading="lazy")
|
||||
|
||||
proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VNode =
|
||||
buildHtml(tdiv):
|
||||
|
@ -84,34 +84,35 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): 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
|
||||
playbackType = if 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(playbackType, path)
|
||||
else:
|
||||
let canPlay = prefs.isPlaybackEnabled(playbackType)
|
||||
|
||||
if video.available and canPlay:
|
||||
let
|
||||
vars = video.variants.filterIt(it.contentType == playbackType)
|
||||
vidUrl = vars.sortedByIt(it.resolution)[^1].url
|
||||
vidUrl = video.getVidVariant(playbackType).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")
|
||||
video(src=source, poster=thumb, controls="", muted=prefs.muteVideos, preload="metadata")
|
||||
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")
|
||||
verbatim "</div>"
|
||||
else:
|
||||
img(src=thumb, loading="lazy", decoding="async")
|
||||
if not canPlay:
|
||||
renderVideoDisabled(playbackType, path)
|
||||
else:
|
||||
renderVideoUnavailable(video)
|
||||
|
||||
if container.len > 0:
|
||||
tdiv(class="card-content"):
|
||||
h2(class="card-title"): text video.title
|
||||
|
@ -144,7 +145,7 @@ proc renderPoll(poll: Poll): VNode =
|
|||
proc renderCardImage(card: Card): VNode =
|
||||
buildHtml(tdiv(class="card-image-container")):
|
||||
tdiv(class="card-image"):
|
||||
img(src=getPicUrl(card.image), alt="")
|
||||
img(src=getPicUrl(card.image), alt="", loading="lazy")
|
||||
if card.kind == player:
|
||||
tdiv(class="card-overlay"):
|
||||
tdiv(class="overlay-circle"):
|
||||
|
|
Loading…
Reference in a new issue