This commit is contained in:
Zed 2023-11-01 16:18:43 +00:00 committed by GitHub
commit 8659ec6b9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 71 deletions

View File

@ -84,8 +84,8 @@ proc parseVideo(js: JsonNode): Video =
views: getVideoViewCount(js), views: getVideoViewCount(js),
available: true, available: true,
title: js{"ext_alt_text"}.getStr, title: js{"ext_alt_text"}.getStr,
durationMs: js{"video_info", "duration_millis"}.getInt durationMs: js{"video_info", "duration_millis"}.getInt,
# playbackType: mp4 playbackType: m3u8
) )
with status, js{"ext_media_availability", "status"}: with status, js{"ext_media_availability", "status"}:
@ -103,6 +103,9 @@ proc parseVideo(js: JsonNode): Video =
contentType = parseEnum[VideoType](v{"content_type"}.getStr("summary")) contentType = parseEnum[VideoType](v{"content_type"}.getStr("summary"))
url = v{"url"}.getStr url = v{"url"}.getStr
if contentType == mp4:
result.playbackType = mp4
result.variants.add VideoVariant( result.variants.add VideoVariant(
contentType: contentType, contentType: contentType,
bitrate: v{"bitrate"}.getInt, bitrate: v{"bitrate"}.getInt,

View File

@ -80,7 +80,7 @@ genPrefs:
Media: Media:
mp4Playback(checkbox, true): mp4Playback(checkbox, true):
"Enable mp4 video playback (only for gifs)" "Enable mp4 video playback"
hlsPlayback(checkbox, false): hlsPlayback(checkbox, false):
"Enable HLS video streaming (requires JavaScript)" "Enable HLS video streaming (requires JavaScript)"

View File

@ -12,7 +12,8 @@ export httpclient, os, strutils, asyncstreams, base64, re
const const
m3u8Mime* = "application/vnd.apple.mpegurl" 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.} = proc safeFetch*(url: string): Future[string] {.async.} =
let client = newAsyncHttpClient() let client = newAsyncHttpClient()
@ -20,56 +21,84 @@ proc safeFetch*(url: string): Future[string] {.async.} =
except: discard except: discard
finally: client.close() finally: client.close()
template respond*(req: asynchttpserver.Request; headers) = template respond*(req: asynchttpserver.Request; code: HttpCode;
var msg = "HTTP/1.1 200 OK\c\L" headers: seq[(string, string)]) =
for k, v in headers: var msg = "HTTP/1.1 " & $code & "\c\L"
for (k, v) in headers:
msg.add(k & ": " & v & "\c\L") msg.add(k & ": " & v & "\c\L")
msg.add "\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.} = proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
result = Http200 result = Http200
let let
request = req.getNativeReq() 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: try:
let res = await client.get(url) var res = await c.get(url)
if res.status != "200 OK": if not res.status.startsWith("20"):
return Http404 return Http404
let hashed = $hash(url) var headers = @{
if request.headers.getOrDefault("If-None-Match") == hashed: "accept-ranges": "bytes",
return Http304 "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 = var tries = 0
if res.headers.hasKey("content-length"): while tries <= 10 and res.headers.hasKey("transfer-encoding"):
res.headers["content-length", 0] await sleepAsync(100 + tries * 200)
else: res = await c.get(url)
"" tries.inc
let headers = newHttpHeaders({ let contentLength = res.getContentLength
"Content-Type": res.headers["content-type", 0], if contentLength.len > 0:
"Content-Length": contentLength, headers.add ("content-length", contentLength)
"Cache-Control": maxAge,
"ETag": hashed
})
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, "") var (hasValue, data) = (true, "")
while hasValue: while hasValue:
(hasValue, data) = await res.bodyStream.read() (hasValue, data) = await res.bodyStream.read()
if hasValue: if hasValue:
await request.client.send(data) await request.client.send(data, flags={})
data.setLen 0 data.setLen 0
except HttpRequestError, ProtocolError, OSError: except OSError: discard
except ProtocolError, HttpRequestError:
result = Http404 result = Http404
finally: finally:
client.close() c.close()
template check*(code): untyped = template check*(c): untyped =
let code = c
if code != Http200: if code != Http200:
resp code resp code
else: else:
@ -83,37 +112,27 @@ proc decoded*(req: jester.Request; index: int): string =
if based: decode(encoded) if based: decode(encoded)
else: decodeUrl(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) = proc createMediaRouter*(cfg: Config) =
router media: router media:
get "/pic/?": get "/pic/?":
resp Http404 resp Http404
get re"^\/pic\/orig\/(enc)?\/?(.+)": get re"^\/pic\/orig\/(enc)?\/?(.+)":
var url = decoded(request, 1) let url = getPicUrl(request)
if "twimg.com" notin url: cond isTwitterUrl(parseUri(url)) == true
url.insert(twimg) check await proxyMedia(request, url & "?name=orig")
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
get re"^\/pic\/(enc)?\/?(.+)": get re"^\/pic\/(enc)?\/?(.+)":
var url = decoded(request, 1) let url = getPicUrl(request)
if "twimg.com" notin url: cond isTwitterUrl(parseUri(url)) == true
url.insert(twimg) check await proxyMedia(request, url)
if not url.startsWith(https):
url.insert(https)
let uri = parseUri(url)
cond isTwitterUrl(uri) == true
let code = await proxyMedia(request, url)
check code
get re"^\/video\/(enc)?\/?(.+)\/(.+)$": get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
let url = decoded(request, 2) let url = decoded(request, 2)
@ -123,8 +142,7 @@ proc createMediaRouter*(cfg: Config) =
resp showError("Failed to verify signature", cfg) resp showError("Failed to verify signature", cfg)
if ".mp4" in url or ".ts" in url or ".m4s" in url: if ".mp4" in url or ".ts" in url or ".m4s" in url:
let code = await proxyMedia(request, url) check await proxyMedia(request, url)
check code
var content: string var content: string
if ".vmap" in url: if ".vmap" in url:

View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import strutils, strformat, uri, tables, base64 import strutils, strformat, uri, tables, base64
import nimcrypto import nimcrypto
import types
var var
hmacKey: string hmacKey: string
@ -28,6 +29,20 @@ proc setProxyEncoding*(state: bool) =
proc getHmac*(data: string): string = proc getHmac*(data: string): string =
($hmac(sha256, hmacKey, data))[0 .. 12] ($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 = proc getVidUrl*(link: string): string =
if link.len == 0: return if link.len == 0: return
let sig = getHmac(link) let sig = getHmac(link)

View File

@ -84,7 +84,7 @@ proc genDate*(pref, state: string): VNode =
proc genImg*(url: string; class=""): VNode = proc genImg*(url: string; class=""): VNode =
buildHtml(): 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 = proc getTabClass*(query: Query; tab: QueryKind): string =
if query.kind == tab: "tab-item active" if query.kind == tab: "tab-item active"

View File

@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import strutils, sequtils, strformat, options, algorithm import strutils, sequtils, strformat, options
import karax/[karaxdsl, vdom, vstyles] import karax/[karaxdsl, vdom, vstyles]
from jester import Request from jester import Request
@ -12,7 +12,7 @@ const doctype = "<!DOCTYPE html>\n"
proc renderMiniAvatar(user: User; prefs: Prefs): VNode = proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
let url = getPicUrl(user.getUserPic("_mini")) let url = getPicUrl(user.getUserPic("_mini"))
buildHtml(): 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 = proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VNode =
buildHtml(tdiv): buildHtml(tdiv):
@ -84,34 +84,35 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
let let
container = if video.description.len == 0 and video.title.len == 0: "" container = if video.description.len == 0 and video.title.len == 0: ""
else: " card-container" else: " card-container"
playbackType = if not prefs.proxyVideos and video.hasMp4Url: mp4 playbackType = if prefs.proxyVideos and video.hasMp4Url: mp4
else: video.playbackType else: video.playbackType
buildHtml(tdiv(class="attachments card")): buildHtml(tdiv(class="attachments card")):
tdiv(class="gallery-video" & container): tdiv(class="gallery-video" & container):
tdiv(class="attachment video-container"): tdiv(class="attachment video-container"):
let thumb = getSmallPic(video.thumb) let thumb = getSmallPic(video.thumb)
if not video.available: let canPlay = prefs.isPlaybackEnabled(playbackType)
img(src=thumb)
renderVideoUnavailable(video) if video.available and canPlay:
elif not prefs.isPlaybackEnabled(playbackType):
img(src=thumb)
renderVideoDisabled(playbackType, path)
else:
let let
vars = video.variants.filterIt(it.contentType == playbackType) vidUrl = video.getVidVariant(playbackType).url
vidUrl = vars.sortedByIt(it.resolution)[^1].url
source = if prefs.proxyVideos: getVidUrl(vidUrl) source = if prefs.proxyVideos: getVidUrl(vidUrl)
else: vidUrl else: vidUrl
case playbackType case playbackType
of mp4: of mp4:
video(poster=thumb, controls="", muted=prefs.muteVideos): video(src=source, poster=thumb, controls="", muted=prefs.muteVideos, preload="metadata")
source(src=source, `type`="video/mp4")
of m3u8, vmap: of m3u8, vmap:
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")
verbatim "</div>" verbatim "</div>"
else:
img(src=thumb, loading="lazy", decoding="async")
if not canPlay:
renderVideoDisabled(playbackType, path)
else:
renderVideoUnavailable(video)
if container.len > 0: if container.len > 0:
tdiv(class="card-content"): tdiv(class="card-content"):
h2(class="card-title"): text video.title h2(class="card-title"): text video.title
@ -144,7 +145,7 @@ proc renderPoll(poll: Poll): VNode =
proc renderCardImage(card: Card): VNode = proc renderCardImage(card: Card): VNode =
buildHtml(tdiv(class="card-image-container")): buildHtml(tdiv(class="card-image-container")):
tdiv(class="card-image"): tdiv(class="card-image"):
img(src=getPicUrl(card.image), alt="") img(src=getPicUrl(card.image), alt="", loading="lazy")
if card.kind == player: if card.kind == player:
tdiv(class="card-overlay"): tdiv(class="card-overlay"):
tdiv(class="overlay-circle"): tdiv(class="overlay-circle"):