Add experimental support for unified_card

Closes #345
This commit is contained in:
Zed 2022-01-13 00:36:30 +01:00
parent 8a6fbe81ab
commit 111927a21c
7 changed files with 185 additions and 17 deletions

View file

@ -22,6 +22,7 @@ requires "redpool#f880f49"
requires "https://github.com/zedeus/redis#d0a0e6f" requires "https://github.com/zedeus/redis#d0a0e6f"
requires "zippy#0.7.3" requires "zippy#0.7.3"
requires "flatty#0.2.3" requires "flatty#0.2.3"
requires "jsony#1.1.3"
# Tasks # Tasks

View file

@ -0,0 +1,91 @@
import std/[options, tables, strutils, strformat, sugar]
import jsony
import ../types/unifiedcard
from ../../types import Card, CardKind, Video
from ../../utils import twimg, https
proc getImageUrl(entity: MediaEntity): string =
entity.mediaUrlHttps.dup(removePrefix(twimg), removePrefix(https))
proc parseDestination(id: string; card: UnifiedCard; result: var Card) =
let destination = card.destinationObjects[id].data
result.dest = destination.urlData.vanity
result.url = destination.urlData.url
proc parseDetails(data: ComponentData; card: UnifiedCard; result: var Card) =
data.destination.parseDestination(card, result)
result.text = data.title
if result.text.len == 0:
result.text = data.name
proc parseMediaDetails(data: ComponentData; card: UnifiedCard; result: var Card) =
data.destination.parseDestination(card, result)
result.kind = summary
result.image = card.mediaEntities[data.mediaId].getImageUrl
result.text = data.topicDetail.title
result.dest = "Topic"
proc parseAppDetails(data: ComponentData; card: UnifiedCard; result: var Card) =
let app = card.appStoreData[data.appId][0]
case app.kind
of androidApp:
result.url = "http://play.google.com/store/apps/details?id=" & app.id
of iPhoneApp, iPadApp:
result.url = "https://itunes.apple.com/app/id" & app.id
result.text = app.title
result.dest = app.category
proc parseListDetails(data: ComponentData; result: var Card) =
result.dest = &"List · {data.memberCount} Members"
proc parseCommunityDetails(data: ComponentData; result: var Card) =
result.dest = &"Community · {data.memberCount} Members"
proc parseMedia(component: Component; card: UnifiedCard; result: var Card) =
let mediaId =
if component.kind == swipeableMedia:
component.data.mediaList[0].id
else:
component.data.id
let rMedia = card.mediaEntities[mediaId]
case rMedia.kind:
of photo:
result.kind = summaryLarge
result.image = rMedia.getImageUrl
of video:
let videoInfo = rMedia.videoInfo.get
result.kind = promoVideo
result.video = some Video(
available: true,
thumb: rMedia.getImageUrl,
durationMs: videoInfo.durationMillis,
variants: videoInfo.variants
)
proc parseUnifiedCard*(json: string): Card =
let card = json.fromJson(UnifiedCard)
for component in card.componentObjects.values:
case component.kind
of details, communityDetails, twitterListDetails:
component.data.parseDetails(card, result)
of appStoreDetails:
component.data.parseAppDetails(card, result)
of mediaWithDetailsHorizontal:
component.data.parseMediaDetails(card, result)
of media, swipeableMedia:
component.parseMedia(card, result)
of buttonGroup:
discard
case component.kind
of twitterListDetails:
component.data.parseListDetails(result)
of communityDetails:
component.data.parseCommunityDetails(result)
else: discard

View file

@ -0,0 +1,79 @@
import options, tables
from ../../types import VideoType, VideoVariant
type
UnifiedCard* = object
componentObjects*: Table[string, Component]
destinationObjects*: Table[string, Destination]
mediaEntities*: Table[string, MediaEntity]
appStoreData*: Table[string, seq[AppStoreData]]
ComponentType* = enum
details
media
swipeableMedia
buttonGroup
appStoreDetails
twitterListDetails
communityDetails
mediaWithDetailsHorizontal
Component* = object
kind*: ComponentType
data*: ComponentData
ComponentData* = object
id*: string
appId*: string
mediaId*: string
destination*: string
title*: Text
subtitle*: Text
name*: Text
memberCount*: int
mediaList*: seq[MediaItem]
topicDetail*: tuple[title: Text]
MediaItem* = object
id*: string
destination*: string
Destination* = object
kind*: string
data*: tuple[urlData: UrlData]
UrlData* = object
url*: string
vanity*: string
MediaType* = enum
photo, video
MediaEntity* = object
kind*: MediaType
mediaUrlHttps*: string
videoInfo*: Option[VideoInfo]
VideoInfo* = object
durationMillis*: int
variants*: seq[VideoVariant]
AppType* = enum
androidApp, iPhoneApp, iPadApp
AppStoreData* = object
kind*: AppType
id*: string
title*: Text
category*: Text
Text = object
content: string
HasTypeField = Component | Destination | MediaEntity | AppStoreData
converter fromText*(text: Text): string = text.content
proc renameHook*(v: var HasTypeField; fieldName: var string) =
if fieldName == "type":
fieldName = "kind"

View file

@ -61,7 +61,7 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
result = result.replace(tco, https & prefs.replaceTwitter & "/t.co") result = result.replace(tco, https & prefs.replaceTwitter & "/t.co")
result = result.replace(cards, prefs.replaceTwitter & "/cards") result = result.replace(cards, prefs.replaceTwitter & "/cards")
result = result.replace(twRegex, prefs.replaceTwitter) result = result.replace(twRegex, prefs.replaceTwitter)
result = result.replace(twLinkRegex, a( result = result.replacef(twLinkRegex, a(
prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1")) prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1"))
if prefs.replaceReddit.len > 0 and ("reddit.com" in result or "redd.it" in result): if prefs.replaceReddit.len > 0 and ("reddit.com" in result or "redd.it" in result):

View file

@ -1,8 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import strutils, options, tables, times, math import strutils, options, tables, times, math
import packedjson import packedjson, packedjson/deserialiser
import packedjson / deserialiser
import types, parserutils, utils import types, parserutils, utils
import experimental/parser/unifiedcard
proc parseProfile(js: JsonNode; id=""): Profile = proc parseProfile(js: JsonNode; id=""): Profile =
if js.isNull: return if js.isNull: return
@ -102,7 +102,6 @@ proc parseGif(js: JsonNode): Gif =
proc parseVideo(js: JsonNode): Video = proc parseVideo(js: JsonNode): Video =
result = Video( result = Video(
videoId: js{"id_str"}.getStr,
thumb: js{"media_url_https"}.getImageStr, thumb: js{"media_url_https"}.getImageStr,
views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr, views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr,
available: js{"ext_media_availability", "status"}.getStr == "available", available: js{"ext_media_availability", "status"}.getStr == "available",
@ -119,7 +118,7 @@ proc parseVideo(js: JsonNode): Video =
for v in js{"video_info", "variants"}: for v in js{"video_info", "variants"}:
result.variants.add VideoVariant( result.variants.add VideoVariant(
videoType: parseEnum[VideoType](v{"content_type"}.getStr("summary")), contentType: parseEnum[VideoType](v{"content_type"}.getStr("summary")),
bitrate: v{"bitrate"}.getInt, bitrate: v{"bitrate"}.getInt,
url: v{"url"}.getStr url: v{"url"}.getStr
) )
@ -129,19 +128,17 @@ proc parsePromoVideo(js: JsonNode): Video =
thumb: js{"player_image_large"}.getImageVal, thumb: js{"player_image_large"}.getImageVal,
available: true, available: true,
durationMs: js{"content_duration_seconds"}.getStrVal("0").parseInt * 1000, durationMs: js{"content_duration_seconds"}.getStrVal("0").parseInt * 1000,
playbackType: vmap, playbackType: vmap
videoId: js{"player_content_id"}.getStrVal(js{"card_id"}.getStrVal(
js{"amplify_content_id"}.getStrVal())),
) )
var variant = VideoVariant( var variant = VideoVariant(
videoType: vmap, contentType: vmap,
url: js{"player_hls_url"}.getStrVal(js{"player_stream_url"}.getStrVal( url: js{"player_hls_url"}.getStrVal(js{"player_stream_url"}.getStrVal(
js{"amplify_url_vmap"}.getStrVal())) js{"amplify_url_vmap"}.getStrVal()))
) )
if "m3u8" in variant.url: if "m3u8" in variant.url:
variant.videoType = m3u8 variant.contentType = m3u8
result.playbackType = m3u8 result.playbackType = m3u8
result.variants.add variant result.variants.add variant
@ -154,7 +151,7 @@ proc parseBroadcast(js: JsonNode): Card =
title: js{"broadcaster_display_name"}.getStrVal, title: js{"broadcaster_display_name"}.getStrVal,
text: js{"broadcast_title"}.getStrVal, text: js{"broadcast_title"}.getStrVal,
image: image, image: image,
video: some Video(videoId: js{"broadcast_media_id"}.getStrVal, thumb: image) video: some Video(thumb: image)
) )
proc parseCard(js: JsonNode; urls: JsonNode): Card = proc parseCard(js: JsonNode; urls: JsonNode): Card =
@ -166,6 +163,9 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card =
name = js{"name"}.getStr name = js{"name"}.getStr
kind = parseEnum[CardKind](name[(name.find(":") + 1) ..< name.len], unknown) kind = parseEnum[CardKind](name[(name.find(":") + 1) ..< name.len], unknown)
if kind == unified:
return parseUnifiedCard(vals{"unified_card", "string_value"}.getStr)
result = Card( result = Card(
kind: kind, kind: kind,
url: vals.getCardUrl(kind), url: vals.getCardUrl(kind),
@ -190,7 +190,7 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card =
result.url = vals{"player_url"}.getStrVal result.url = vals{"player_url"}.getStrVal
if "youtube.com" in result.url: if "youtube.com" in result.url:
result.url = result.url.replace("/embed/", "/watch?v=") result.url = result.url.replace("/embed/", "/watch?v=")
of audiospace, unified, unknown: of audiospace, unknown:
result.title = "This card type is not supported." result.title = "This card type is not supported."
else: discard else: discard

View file

@ -70,12 +70,11 @@ type
vmap = "video/vmap" vmap = "video/vmap"
VideoVariant* = object VideoVariant* = object
videoType*: VideoType contentType*: VideoType
url*: string url*: string
bitrate*: int bitrate*: int
Video* = object Video* = object
videoId*: string
durationMs*: int durationMs*: int
url*: string url*: string
thumb*: string thumb*: string
@ -147,8 +146,6 @@ type
Card* = object Card* = object
kind*: CardKind kind*: CardKind
id*: string
query*: string
url*: string url*: string
title*: string title*: string
dest*: string dest*: string

View file

@ -97,7 +97,7 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
img(src=thumb) img(src=thumb)
renderVideoDisabled(video, path) renderVideoDisabled(video, path)
else: else:
let vid = video.variants.filterIt(it.videoType == video.playbackType) let vid = video.variants.filterIt(it.contentType == video.playbackType)
let source = getVidUrl(vid[0].url) let source = getVidUrl(vid[0].url)
case video.playbackType case video.playbackType
of mp4: of mp4: