2019-06-24 03:14:14 +00:00
|
|
|
import xmltree, sequtils, strtabs, strutils, strformat, json
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-24 06:07:36 +00:00
|
|
|
import ./types, ./parserutils, ./formatters
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-21 00:15:46 +00:00
|
|
|
proc parsePopupProfile*(node: XmlNode): Profile =
|
2019-06-26 16:51:21 +00:00
|
|
|
let profile = node.select(".profile-card")
|
2019-06-27 19:07:29 +00:00
|
|
|
if profile == nil: return
|
2019-06-21 00:15:46 +00:00
|
|
|
|
|
|
|
result = Profile(
|
2019-06-24 00:09:32 +00:00
|
|
|
fullname: profile.getName(".fullname"),
|
|
|
|
username: profile.getUsername(".username"),
|
|
|
|
bio: profile.getBio(".bio"),
|
|
|
|
userpic: profile.getAvatar(".ProfileCard-avatarImage"),
|
|
|
|
verified: isVerified(profile),
|
|
|
|
protected: isProtected(profile),
|
|
|
|
banner: getBanner(profile)
|
2019-06-21 00:15:46 +00:00
|
|
|
)
|
2019-06-24 06:07:36 +00:00
|
|
|
|
2019-06-23 23:34:30 +00:00
|
|
|
result.getPopupStats(profile)
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-21 00:15:46 +00:00
|
|
|
proc parseIntentProfile*(profile: XmlNode): Profile =
|
|
|
|
result = Profile(
|
2019-06-24 00:09:32 +00:00
|
|
|
fullname: profile.getName("a.fn.url.alternate-context"),
|
|
|
|
username: profile.getUsername(".nickname"),
|
|
|
|
bio: profile.getBio("p.note"),
|
2019-06-26 16:51:21 +00:00
|
|
|
userpic: profile.select(".profile.summary").getAvatar("img.photo"),
|
2019-06-27 19:07:29 +00:00
|
|
|
verified: profile.select("li.verified") != nil,
|
|
|
|
protected: profile.select("li.protected") != nil,
|
2019-06-24 00:09:32 +00:00
|
|
|
banner: getBanner(profile)
|
2019-06-21 00:15:46 +00:00
|
|
|
)
|
2019-06-24 06:07:36 +00:00
|
|
|
|
2019-06-23 23:34:30 +00:00
|
|
|
result.getIntentStats(profile)
|
2019-06-21 00:15:46 +00:00
|
|
|
|
|
|
|
proc parseTweetProfile*(profile: XmlNode): Profile =
|
2019-06-20 14:16:20 +00:00
|
|
|
result = Profile(
|
2019-06-27 19:07:29 +00:00
|
|
|
fullname: profile.attr("data-name").stripText(),
|
|
|
|
username: profile.attr("data-screen-name"),
|
2019-06-23 23:34:30 +00:00
|
|
|
userpic: profile.getAvatar(".avatar"),
|
|
|
|
verified: isVerified(profile)
|
|
|
|
)
|
|
|
|
|
2019-06-24 06:07:36 +00:00
|
|
|
proc parseQuote*(quote: XmlNode): Quote =
|
|
|
|
result = Quote(
|
2019-06-27 19:07:29 +00:00
|
|
|
id: quote.attr("data-item-id"),
|
|
|
|
link: quote.attr("href"),
|
2019-06-25 02:52:38 +00:00
|
|
|
text: getQuoteText(quote)
|
2019-06-23 23:34:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
result.profile = Profile(
|
2019-06-25 00:38:18 +00:00
|
|
|
fullname: quote.selectText(".QuoteTweet-fullname").stripText(),
|
2019-06-27 19:07:29 +00:00
|
|
|
username: quote.attr("data-screen-name"),
|
2019-06-24 06:07:36 +00:00
|
|
|
verified: isVerified(quote)
|
2019-06-20 14:16:20 +00:00
|
|
|
)
|
|
|
|
|
2019-06-24 06:07:36 +00:00
|
|
|
result.getQuoteMedia(quote)
|
|
|
|
|
2019-06-26 16:51:21 +00:00
|
|
|
proc parseTweet*(node: XmlNode): Tweet =
|
|
|
|
let tweet = node.select(".tweet")
|
2019-06-27 19:07:29 +00:00
|
|
|
if tweet == nil: return Tweet()
|
2019-06-26 16:51:21 +00:00
|
|
|
|
2019-06-21 00:30:57 +00:00
|
|
|
result = Tweet(
|
2019-06-27 19:07:29 +00:00
|
|
|
id: tweet.attr("data-item-id"),
|
|
|
|
link: tweet.attr("data-permalink-path"),
|
2019-06-23 23:34:30 +00:00
|
|
|
text: getTweetText(tweet),
|
|
|
|
time: getTimestamp(tweet),
|
|
|
|
shortTime: getShortTime(tweet),
|
2019-06-27 19:07:29 +00:00
|
|
|
profile: parseTweetProfile(tweet),
|
|
|
|
pinned: "pinned" in tweet.attr("class"),
|
|
|
|
available: true
|
2019-06-21 00:30:57 +00:00
|
|
|
)
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-23 23:34:30 +00:00
|
|
|
result.getTweetStats(tweet)
|
|
|
|
result.getTweetMedia(tweet)
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-21 00:30:57 +00:00
|
|
|
let by = tweet.selectText(".js-retweet-text > a > b")
|
|
|
|
if by.len > 0:
|
2019-06-25 00:38:18 +00:00
|
|
|
result.retweetBy = some(by.stripText())
|
2019-06-27 19:07:29 +00:00
|
|
|
result.retweetId = some(tweet.attr("data-retweet-id"))
|
2019-06-21 00:30:57 +00:00
|
|
|
|
2019-06-26 16:51:21 +00:00
|
|
|
let quote = tweet.select(".QuoteTweet-innerContainer")
|
2019-06-27 19:07:29 +00:00
|
|
|
if quote != nil:
|
2019-06-24 06:07:36 +00:00
|
|
|
result.quote = some(parseQuote(quote))
|
|
|
|
|
2019-06-29 04:31:02 +00:00
|
|
|
proc parseTweets*(nodes: XmlNode): Tweets =
|
|
|
|
if nodes == nil: return
|
|
|
|
for n in nodes.filterIt(it.kind != xnText):
|
|
|
|
let class = n.attr("class").toLower()
|
|
|
|
if "tombstone" in class or "unavailable" in class:
|
|
|
|
result.add Tweet()
|
|
|
|
elif "morereplies" notin class:
|
2019-06-27 19:48:59 +00:00
|
|
|
result.add parseTweet(n)
|
2019-06-20 14:16:20 +00:00
|
|
|
|
|
|
|
proc parseConversation*(node: XmlNode): Conversation =
|
2019-06-24 03:14:14 +00:00
|
|
|
result = Conversation(
|
2019-06-26 16:51:21 +00:00
|
|
|
tweet: parseTweet(node.select(".permalink-tweet-container")),
|
2019-06-29 04:31:02 +00:00
|
|
|
before: parseTweets(node.select(".in-reply-to .stream-items"))
|
2019-06-24 03:14:14 +00:00
|
|
|
)
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-29 04:31:02 +00:00
|
|
|
let replies = node.select(".replies-to .stream-items")
|
2019-06-27 19:07:29 +00:00
|
|
|
if replies == nil: return
|
2019-06-20 14:16:20 +00:00
|
|
|
|
2019-06-26 17:59:28 +00:00
|
|
|
for reply in replies.filterIt(it.kind != xnText):
|
2019-06-29 04:31:02 +00:00
|
|
|
let class = reply.attr("class").toLower()
|
|
|
|
let thread = reply.select(".stream-items")
|
|
|
|
|
|
|
|
if "self" in class:
|
|
|
|
result.after = parseTweets(thread)
|
|
|
|
elif "lone" in class:
|
2019-06-26 17:59:28 +00:00
|
|
|
result.replies.add parseTweets(reply)
|
2019-06-29 04:31:02 +00:00
|
|
|
else:
|
|
|
|
result.replies.add parseTweets(thread)
|
2019-06-24 03:14:14 +00:00
|
|
|
|
|
|
|
proc parseVideo*(node: JsonNode): Video =
|
2019-06-29 05:45:36 +00:00
|
|
|
let
|
|
|
|
track = node{"track"}
|
|
|
|
cType = track["contentType"].to(string)
|
|
|
|
pType = track["playbackType"].to(string)
|
2019-06-25 05:37:44 +00:00
|
|
|
|
2019-06-29 05:45:36 +00:00
|
|
|
case cType
|
2019-06-25 05:37:44 +00:00
|
|
|
of "media_entity":
|
|
|
|
result = Video(
|
2019-06-29 05:45:36 +00:00
|
|
|
playbackType: if "mp4" in pType: mp4 else: m3u8,
|
|
|
|
contentId: track["contentId"].to(string),
|
|
|
|
durationMs: track["durationMs"].to(int),
|
2019-06-25 05:37:44 +00:00
|
|
|
views: track["viewCount"].to(string),
|
|
|
|
url: track["playbackUrl"].to(string),
|
|
|
|
available: track{"mediaAvailability"}["status"].to(string) == "available"
|
|
|
|
)
|
|
|
|
of "vmap":
|
|
|
|
result = Video(
|
2019-06-29 05:45:36 +00:00
|
|
|
playbackType: vmap,
|
|
|
|
durationMs: track["durationMs"].to(int),
|
|
|
|
url: track["vmapUrl"].to(string)
|
2019-06-25 05:37:44 +00:00
|
|
|
)
|
|
|
|
else:
|
2019-06-29 05:45:36 +00:00
|
|
|
echo "Can't parse video of type ", cType
|
|
|
|
|
|
|
|
result.thumb = node["posterImage"].to(string)
|