Overhaul extractors.cr to use modules
This commit is contained in:
parent
3dea670091
commit
142317c2be
@ -3,47 +3,34 @@
|
|||||||
|
|
||||||
# Tuple of Parsers/Extractors so we can easily cycle through them.
|
# Tuple of Parsers/Extractors so we can easily cycle through them.
|
||||||
private ITEM_CONTAINER_EXTRACTOR = {
|
private ITEM_CONTAINER_EXTRACTOR = {
|
||||||
YoutubeTabsExtractor.new,
|
Extractors::YouTubeTabs,
|
||||||
SearchResultsExtractor.new,
|
Extractors::SearchResults,
|
||||||
ContinuationExtractor.new,
|
Extractors::Continuation,
|
||||||
}
|
}
|
||||||
|
|
||||||
private ITEM_PARSERS = {
|
private ITEM_PARSERS = {
|
||||||
VideoParser.new,
|
Parsers::VideoRendererParser,
|
||||||
ChannelParser.new,
|
Parsers::ChannelRendererParser,
|
||||||
GridPlaylistParser.new,
|
Parsers::GridPlaylistRendererParser,
|
||||||
PlaylistParser.new,
|
Parsers::PlaylistRendererParser,
|
||||||
CategoryParser.new,
|
Parsers::CategoryRendererParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AuthorFallback
|
record AuthorFallback, name : String? = nil, id : String? = nil
|
||||||
property name, id
|
|
||||||
|
|
||||||
def initialize(@name : String? = nil, @id : String? = nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# The following are the parsers for parsing raw item data into neatly packaged structs.
|
# The following are the parsers for parsing raw item data into neatly packaged structs.
|
||||||
# They're accessed through the process() method which validates the given data as applicable
|
# They're accessed through the process() method which validates the given data as applicable
|
||||||
# to their specific struct and then use the internal parse() method to assemble the struct
|
# to their specific struct and then use the internal parse() method to assemble the struct
|
||||||
# specific to their category.
|
# specific to their category.
|
||||||
private abstract struct ItemParser
|
private module Parsers
|
||||||
# Base type for all item parsers.
|
module VideoRendererParser
|
||||||
def process(item : JSON::Any, author_fallback : AuthorFallback)
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
end
|
|
||||||
|
|
||||||
private def parse(item_contents : JSON::Any, author_fallback : AuthorFallback)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private struct VideoParser < ItemParser
|
|
||||||
def process(item, author_fallback)
|
|
||||||
if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
|
if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
|
||||||
return self.parse(item_contents, author_fallback)
|
return self.parse(item_contents, author_fallback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
video_id = item_contents["videoId"].as_s
|
video_id = item_contents["videoId"].as_s
|
||||||
title = item_contents["title"].try { |t| t["simpleText"]?.try &.as_s || t["runs"]?.try &.as_a.map(&.["text"].as_s).join("") } || ""
|
title = item_contents["title"].try { |t| t["simpleText"]?.try &.as_s || t["runs"]?.try &.as_a.map(&.["text"].as_s).join("") } || ""
|
||||||
|
|
||||||
@ -92,16 +79,16 @@ private struct VideoParser < ItemParser
|
|||||||
premiere_timestamp: premiere_timestamp,
|
premiere_timestamp: premiere_timestamp,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private struct ChannelParser < ItemParser
|
module ChannelRendererParser
|
||||||
def process(item, author_fallback)
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
|
if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
|
||||||
return self.parse(item_contents, author_fallback)
|
return self.parse(item_contents, author_fallback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
author = item_contents["title"]["simpleText"]?.try &.as_s || author_fallback.name || ""
|
author = item_contents["title"]["simpleText"]?.try &.as_s || author_fallback.name || ""
|
||||||
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id || ""
|
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id || ""
|
||||||
|
|
||||||
@ -123,16 +110,16 @@ private struct ChannelParser < ItemParser
|
|||||||
auto_generated: auto_generated,
|
auto_generated: auto_generated,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private struct GridPlaylistParser < ItemParser
|
module GridPlaylistRendererParser
|
||||||
def process(item, author_fallback)
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
if item_contents = item["gridPlaylistRenderer"]?
|
if item_contents = item["gridPlaylistRenderer"]?
|
||||||
return self.parse(item_contents, author_fallback)
|
return self.parse(item_contents, author_fallback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
title = item_contents["title"]["runs"].as_a[0]?.try &.["text"].as_s || ""
|
title = item_contents["title"]["runs"].as_a[0]?.try &.["text"].as_s || ""
|
||||||
plid = item_contents["playlistId"]?.try &.as_s || ""
|
plid = item_contents["playlistId"]?.try &.as_s || ""
|
||||||
|
|
||||||
@ -149,16 +136,16 @@ private struct GridPlaylistParser < ItemParser
|
|||||||
thumbnail: playlist_thumbnail,
|
thumbnail: playlist_thumbnail,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private struct PlaylistParser < ItemParser
|
module PlaylistRendererParser
|
||||||
def process(item, author_fallback)
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
if item_contents = item["playlistRenderer"]?
|
if item_contents = item["playlistRenderer"]?
|
||||||
return self.parse(item_contents, author_fallback)
|
return self.parse(item_contents, author_fallback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
title = item_contents["title"]["simpleText"]?.try &.as_s || ""
|
title = item_contents["title"]["simpleText"]?.try &.as_s || ""
|
||||||
plid = item_contents["playlistId"]?.try &.as_s || ""
|
plid = item_contents["playlistId"]?.try &.as_s || ""
|
||||||
|
|
||||||
@ -193,16 +180,16 @@ private struct PlaylistParser < ItemParser
|
|||||||
thumbnail: playlist_thumbnail,
|
thumbnail: playlist_thumbnail,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private struct CategoryParser < ItemParser
|
module CategoryRendererParser
|
||||||
def process(item, author_fallback)
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
if item_contents = item["shelfRenderer"]?
|
if item_contents = item["shelfRenderer"]?
|
||||||
return self.parse(item_contents, author_fallback)
|
return self.parse(item_contents, author_fallback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
# Title extraction is a bit complicated. There are two possible routes for it
|
# Title extraction is a bit complicated. There are two possible routes for it
|
||||||
# as well as times when the title attribute just isn't sent by YT.
|
# as well as times when the title attribute just isn't sent by YT.
|
||||||
title_container = item_contents["title"]? || ""
|
title_container = item_contents["title"]? || ""
|
||||||
@ -255,6 +242,7 @@ private struct CategoryParser < ItemParser
|
|||||||
badges: badges,
|
badges: badges,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# The following are the extractors for extracting an array of items from
|
# The following are the extractors for extracting an array of items from
|
||||||
@ -262,22 +250,15 @@ end
|
|||||||
# a structure we can more easily use via the parsers above. Their internals are
|
# a structure we can more easily use via the parsers above. Their internals are
|
||||||
# identical to the item parsers.
|
# identical to the item parsers.
|
||||||
|
|
||||||
private abstract struct ItemsContainerExtractor
|
private module Extractors
|
||||||
def process(item : Hash(String, JSON::Any))
|
module YouTubeTabs
|
||||||
end
|
def self.process(initial_data : Hash(String, JSON::Any))
|
||||||
|
|
||||||
private def extract(target : JSON::Any)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private struct YoutubeTabsExtractor < ItemsContainerExtractor
|
|
||||||
def process(initial_data)
|
|
||||||
if target = initial_data["twoColumnBrowseResultsRenderer"]?
|
if target = initial_data["twoColumnBrowseResultsRenderer"]?
|
||||||
self.extract(target)
|
self.extract(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def extract(target)
|
private def self.extract(target)
|
||||||
raw_items = [] of JSON::Any
|
raw_items = [] of JSON::Any
|
||||||
selected_tab = extract_selected_tab(target["tabs"])
|
selected_tab = extract_selected_tab(target["tabs"])
|
||||||
content = selected_tab["content"]
|
content = selected_tab["content"]
|
||||||
@ -302,16 +283,16 @@ private struct YoutubeTabsExtractor < ItemsContainerExtractor
|
|||||||
|
|
||||||
return raw_items
|
return raw_items
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private struct SearchResultsExtractor < ItemsContainerExtractor
|
module SearchResults
|
||||||
def process(initial_data)
|
def self.process(initial_data : Hash(String, JSON::Any))
|
||||||
if target = initial_data["twoColumnSearchResultsRenderer"]?
|
if target = initial_data["twoColumnSearchResultsRenderer"]?
|
||||||
self.extract(target)
|
self.extract(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def extract(target)
|
private def self.extract(target)
|
||||||
raw_items = [] of Array(JSON::Any)
|
raw_items = [] of Array(JSON::Any)
|
||||||
content = target["primaryContents"]
|
content = target["primaryContents"]
|
||||||
renderer = content["sectionListRenderer"]["contents"].as_a.each do |node|
|
renderer = content["sectionListRenderer"]["contents"].as_a.each do |node|
|
||||||
@ -324,10 +305,10 @@ private struct SearchResultsExtractor < ItemsContainerExtractor
|
|||||||
|
|
||||||
return raw_items
|
return raw_items
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private struct ContinuationExtractor < ItemsContainerExtractor
|
module Continuation
|
||||||
def process(initial_data)
|
def self.process(initial_data : Hash(String, JSON::Any))
|
||||||
if target = initial_data["continuationContents"]?
|
if target = initial_data["continuationContents"]?
|
||||||
self.extract(target)
|
self.extract(target)
|
||||||
elsif target = initial_data["appendContinuationItemsAction"]?
|
elsif target = initial_data["appendContinuationItemsAction"]?
|
||||||
@ -335,7 +316,7 @@ private struct ContinuationExtractor < ItemsContainerExtractor
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def extract(target)
|
private def self.extract(target)
|
||||||
raw_items = [] of JSON::Any
|
raw_items = [] of JSON::Any
|
||||||
if content = target["gridContinuation"]?
|
if content = target["gridContinuation"]?
|
||||||
raw_items = content["items"].as_a
|
raw_items = content["items"].as_a
|
||||||
@ -345,6 +326,7 @@ private struct ContinuationExtractor < ItemsContainerExtractor
|
|||||||
|
|
||||||
return raw_items
|
return raw_items
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Parses an item from Youtube's JSON response into a more usable structure.
|
# Parses an item from Youtube's JSON response into a more usable structure.
|
||||||
|
Loading…
Reference in New Issue
Block a user