Add thumbnail and banners to channel page
This commit is contained in:
		
							
								
								
									
										155
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								src/invidious.cr
									
									
									
									
									
								
							@@ -2504,7 +2504,7 @@ get "/feed/channel/:ucid" do |env|
 | 
			
		||||
  ucid = env.params.url["ucid"]
 | 
			
		||||
 | 
			
		||||
  begin
 | 
			
		||||
    author, ucid, auto_generated = get_about_info(ucid, locale)
 | 
			
		||||
    channel = get_about_info(ucid, locale)
 | 
			
		||||
  rescue ex
 | 
			
		||||
    error_message = ex.message
 | 
			
		||||
    env.response.status_code = 500
 | 
			
		||||
@@ -2512,7 +2512,7 @@ get "/feed/channel/:ucid" do |env|
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  client = make_client(YT_URL)
 | 
			
		||||
  rss = client.get("/feeds/videos.xml?channel_id=#{ucid}").body
 | 
			
		||||
  rss = client.get("/feeds/videos.xml?channel_id=#{channel.ucid}").body
 | 
			
		||||
  rss = XML.parse_html(rss)
 | 
			
		||||
 | 
			
		||||
  videos = [] of SearchVideo
 | 
			
		||||
@@ -2552,18 +2552,18 @@ get "/feed/channel/:ucid" do |env|
 | 
			
		||||
      "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
 | 
			
		||||
      "xml:lang": "en-US") do
 | 
			
		||||
      xml.element("link", rel: "self", href: "#{host_url}#{env.request.resource}")
 | 
			
		||||
      xml.element("id") { xml.text "yt:channel:#{ucid}" }
 | 
			
		||||
      xml.element("yt:channelId") { xml.text ucid }
 | 
			
		||||
      xml.element("title") { xml.text author }
 | 
			
		||||
      xml.element("link", rel: "alternate", href: "#{host_url}/channel/#{ucid}")
 | 
			
		||||
      xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
 | 
			
		||||
      xml.element("yt:channelId") { xml.text channel.ucid }
 | 
			
		||||
      xml.element("title") { xml.text channel.author }
 | 
			
		||||
      xml.element("link", rel: "alternate", href: "#{host_url}/channel/#{channel.ucid}")
 | 
			
		||||
 | 
			
		||||
      xml.element("author") do
 | 
			
		||||
        xml.element("name") { xml.text author }
 | 
			
		||||
        xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" }
 | 
			
		||||
        xml.element("name") { xml.text channel.author }
 | 
			
		||||
        xml.element("uri") { xml.text "#{host_url}/channel/#{channel.ucid}" }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      videos.each do |video|
 | 
			
		||||
        video.to_xml(host_url, auto_generated, xml)
 | 
			
		||||
        video.to_xml(host_url, channel.auto_generated, xml)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
@@ -2888,22 +2888,18 @@ get "/channel/:ucid" do |env|
 | 
			
		||||
  sort_by = env.params.query["sort_by"]?.try &.downcase
 | 
			
		||||
 | 
			
		||||
  begin
 | 
			
		||||
    author, ucid, auto_generated, sub_count = get_about_info(ucid, locale)
 | 
			
		||||
    channel = get_about_info(ucid, locale)
 | 
			
		||||
  rescue ex
 | 
			
		||||
    error_message = ex.message
 | 
			
		||||
    env.response.status_code = 500
 | 
			
		||||
    next templated "error"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if !auto_generated
 | 
			
		||||
    env.set "search", "channel:#{ucid} "
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if auto_generated
 | 
			
		||||
  if channel.auto_generated
 | 
			
		||||
    sort_options = {"last", "oldest", "newest"}
 | 
			
		||||
    sort_by ||= "last"
 | 
			
		||||
 | 
			
		||||
    items, continuation = fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
 | 
			
		||||
    items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
 | 
			
		||||
    items.uniq! do |item|
 | 
			
		||||
      if item.responds_to?(:title)
 | 
			
		||||
        item.title
 | 
			
		||||
@@ -2918,8 +2914,10 @@ get "/channel/:ucid" do |env|
 | 
			
		||||
    sort_options = {"newest", "oldest", "popular"}
 | 
			
		||||
    sort_by ||= "newest"
 | 
			
		||||
 | 
			
		||||
    items, count = get_60_videos(ucid, page, auto_generated, sort_by)
 | 
			
		||||
    items, count = get_60_videos(channel.ucid, page, channel.auto_generated, sort_by)
 | 
			
		||||
    items.select! { |item| !item.paid }
 | 
			
		||||
 | 
			
		||||
    env.set "search", "channel:#{channel.ucid} "
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  templated "channel"
 | 
			
		||||
@@ -2958,18 +2956,18 @@ get "/channel/:ucid/playlists" do |env|
 | 
			
		||||
  sort_by ||= "last"
 | 
			
		||||
 | 
			
		||||
  begin
 | 
			
		||||
    author, ucid, auto_generated, sub_count = get_about_info(ucid, locale)
 | 
			
		||||
    channel = get_about_info(ucid, locale)
 | 
			
		||||
  rescue ex
 | 
			
		||||
    error_message = ex.message
 | 
			
		||||
    env.response.status_code = 500
 | 
			
		||||
    next templated "error"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if auto_generated
 | 
			
		||||
    next env.redirect "/channel/#{ucid}"
 | 
			
		||||
  if channel.auto_generated
 | 
			
		||||
    next env.redirect "/channel/#{channel.ucid}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  items, continuation = fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
 | 
			
		||||
  items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
 | 
			
		||||
  items.select! { |item| item.is_a?(SearchPlaylist) && !item.videos.empty? }
 | 
			
		||||
  items = items.map { |item| item.as(SearchPlaylist) }
 | 
			
		||||
  items.each { |item| item.author = "" }
 | 
			
		||||
@@ -3539,7 +3537,7 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
  sort_by ||= "newest"
 | 
			
		||||
 | 
			
		||||
  begin
 | 
			
		||||
    author, ucid, auto_generated = get_about_info(ucid, locale)
 | 
			
		||||
    channel = get_about_info(ucid, locale)
 | 
			
		||||
  rescue ex
 | 
			
		||||
    error_message = {"error" => ex.message}.to_json
 | 
			
		||||
    env.response.status_code = 500
 | 
			
		||||
@@ -3547,12 +3545,12 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  page = 1
 | 
			
		||||
  if auto_generated
 | 
			
		||||
  if channel.auto_generated
 | 
			
		||||
    videos = [] of SearchVideo
 | 
			
		||||
    count = 0
 | 
			
		||||
  else
 | 
			
		||||
    begin
 | 
			
		||||
      videos, count = get_60_videos(ucid, page, auto_generated, sort_by)
 | 
			
		||||
      videos, count = get_60_videos(channel.ucid, page, channel.auto_generated, sort_by)
 | 
			
		||||
    rescue ex
 | 
			
		||||
      error_message = {"error" => ex.message}.to_json
 | 
			
		||||
      env.response.status_code = 500
 | 
			
		||||
@@ -3560,65 +3558,12 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  client = make_client(YT_URL)
 | 
			
		||||
  channel_html = client.get("/channel/#{ucid}/about?disable_polymer=1").body
 | 
			
		||||
  channel_html = XML.parse_html(channel_html)
 | 
			
		||||
  banner = channel_html.xpath_node(%q(//div[@id="gh-banner"]/style)).not_nil!.content
 | 
			
		||||
  banner = "https:" + banner.match(/background-image: url\((?<url>[^)]+)\)/).not_nil!["url"]
 | 
			
		||||
 | 
			
		||||
  author = channel_html.xpath_node(%q(//a[contains(@class, "branded-page-header-title-link")])).not_nil!.content
 | 
			
		||||
  author_url = channel_html.xpath_node(%q(//a[@class="channel-header-profile-image-container spf-link"])).not_nil!["href"]
 | 
			
		||||
  author_thumbnail = channel_html.xpath_node(%q(//img[@class="channel-header-profile-image"])).not_nil!["src"]
 | 
			
		||||
  description_html = channel_html.xpath_node(%q(//div[contains(@class,"about-description")])).try &.to_s || ""
 | 
			
		||||
 | 
			
		||||
  paid = channel_html.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
 | 
			
		||||
  is_family_friendly = channel_html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
 | 
			
		||||
  allowed_regions = channel_html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
 | 
			
		||||
 | 
			
		||||
  related_channels = channel_html.xpath_nodes(%q(//div[contains(@class, "branded-page-related-channels")]/ul/li))
 | 
			
		||||
  related_channels = related_channels.map do |node|
 | 
			
		||||
    related_id = node["data-external-id"]?
 | 
			
		||||
    related_id ||= ""
 | 
			
		||||
 | 
			
		||||
    anchor = node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
 | 
			
		||||
    related_title = anchor.try &.["title"]
 | 
			
		||||
    related_title ||= ""
 | 
			
		||||
 | 
			
		||||
    related_author_url = anchor.try &.["href"]
 | 
			
		||||
    related_author_url ||= ""
 | 
			
		||||
 | 
			
		||||
    related_author_thumbnail = node.xpath_node(%q(.//img)).try &.["data-thumb"]
 | 
			
		||||
    related_author_thumbnail ||= ""
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      id:               related_id,
 | 
			
		||||
      author:           related_title,
 | 
			
		||||
      author_url:       related_author_url,
 | 
			
		||||
      author_thumbnail: related_author_thumbnail,
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  total_views = 0_i64
 | 
			
		||||
  sub_count = 0_i64
 | 
			
		||||
  joined = Time.unix(0)
 | 
			
		||||
  metadata = channel_html.xpath_nodes(%q(//span[@class="about-stat"]))
 | 
			
		||||
  metadata.each do |item|
 | 
			
		||||
    case item.content
 | 
			
		||||
    when .includes? "views"
 | 
			
		||||
      total_views = item.content.gsub(/\D/, "").to_i64
 | 
			
		||||
    when .includes? "subscribers"
 | 
			
		||||
      sub_count = item.content.delete("subscribers").gsub(/\D/, "").to_i64
 | 
			
		||||
    when .includes? "Joined"
 | 
			
		||||
      joined = Time.parse(item.content.lchop("Joined "), "%b %-d, %Y", Time::Location.local)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  channel_info = JSON.build do |json|
 | 
			
		||||
  JSON.build do |json|
 | 
			
		||||
    # TODO: Refactor into `to_json` for InvidiousChannel
 | 
			
		||||
    json.object do
 | 
			
		||||
      json.field "author", author
 | 
			
		||||
      json.field "authorId", ucid
 | 
			
		||||
      json.field "authorUrl", author_url
 | 
			
		||||
      json.field "author", channel.author
 | 
			
		||||
      json.field "authorId", channel.ucid
 | 
			
		||||
      json.field "authorUrl", channel.author_url
 | 
			
		||||
 | 
			
		||||
      json.field "authorBanners" do
 | 
			
		||||
        json.array do
 | 
			
		||||
@@ -3629,14 +3574,14 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
          }
 | 
			
		||||
          qualities.each do |quality|
 | 
			
		||||
            json.object do
 | 
			
		||||
              json.field "url", banner.gsub("=w1060", "=w#{quality[:width]}")
 | 
			
		||||
              json.field "url", channel.banner.gsub("=w1060", "=w#{quality[:width]}")
 | 
			
		||||
              json.field "width", quality[:width]
 | 
			
		||||
              json.field "height", quality[:height]
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          json.object do
 | 
			
		||||
            json.field "url", banner.rchop("=w1060-fcrop64=1,00005a57ffffa5a8-nd-c0xffffffff-rj-k-no")
 | 
			
		||||
            json.field "url", channel.banner.rchop("=w1060-fcrop64=1,00005a57ffffa5a8-nd-c0xffffffff-rj-k-no")
 | 
			
		||||
            json.field "width", 512
 | 
			
		||||
            json.field "height", 288
 | 
			
		||||
          end
 | 
			
		||||
@@ -3649,7 +3594,7 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
 | 
			
		||||
          qualities.each do |quality|
 | 
			
		||||
            json.object do
 | 
			
		||||
              json.field "url", author_thumbnail.gsub("/s100-", "/s#{quality}-")
 | 
			
		||||
              json.field "url", channel.author_thumbnail.gsub("/s100-", "/s#{quality}-")
 | 
			
		||||
              json.field "width", quality
 | 
			
		||||
              json.field "height", quality
 | 
			
		||||
            end
 | 
			
		||||
@@ -3657,17 +3602,17 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      json.field "subCount", sub_count
 | 
			
		||||
      json.field "totalViews", total_views
 | 
			
		||||
      json.field "joined", joined.to_unix
 | 
			
		||||
      json.field "paid", paid
 | 
			
		||||
      json.field "subCount", channel.sub_count
 | 
			
		||||
      json.field "totalViews", channel.total_views
 | 
			
		||||
      json.field "joined", channel.joined.to_unix
 | 
			
		||||
      json.field "paid", channel.paid
 | 
			
		||||
 | 
			
		||||
      json.field "autoGenerated", auto_generated
 | 
			
		||||
      json.field "isFamilyFriendly", is_family_friendly
 | 
			
		||||
      json.field "description", html_to_content(description_html)
 | 
			
		||||
      json.field "descriptionHtml", description_html
 | 
			
		||||
      json.field "autoGenerated", channel.auto_generated
 | 
			
		||||
      json.field "isFamilyFriendly", channel.is_family_friendly
 | 
			
		||||
      json.field "description", html_to_content(channel.description_html)
 | 
			
		||||
      json.field "descriptionHtml", channel.description_html
 | 
			
		||||
 | 
			
		||||
      json.field "allowedRegions", allowed_regions
 | 
			
		||||
      json.field "allowedRegions", channel.allowed_regions
 | 
			
		||||
 | 
			
		||||
      json.field "latestVideos" do
 | 
			
		||||
        json.array do
 | 
			
		||||
@@ -3679,11 +3624,11 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
 | 
			
		||||
      json.field "relatedChannels" do
 | 
			
		||||
        json.array do
 | 
			
		||||
          related_channels.each do |related_channel|
 | 
			
		||||
          channel.related_channels.each do |related_channel|
 | 
			
		||||
            json.object do
 | 
			
		||||
              json.field "author", related_channel[:author]
 | 
			
		||||
              json.field "authorId", related_channel[:id]
 | 
			
		||||
              json.field "authorUrl", related_channel[:author_url]
 | 
			
		||||
              json.field "author", related_channel.author
 | 
			
		||||
              json.field "authorId", related_channel.ucid
 | 
			
		||||
              json.field "authorUrl", related_channel.author_url
 | 
			
		||||
 | 
			
		||||
              json.field "authorThumbnails" do
 | 
			
		||||
                json.array do
 | 
			
		||||
@@ -3691,7 +3636,7 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
 | 
			
		||||
                  qualities.each do |quality|
 | 
			
		||||
                    json.object do
 | 
			
		||||
                      json.field "url", related_channel[:author_thumbnail].gsub("=s48-", "=s#{quality}-")
 | 
			
		||||
                      json.field "url", related_channel.author_thumbnail.gsub("=s48-", "=s#{quality}-")
 | 
			
		||||
                      json.field "width", quality
 | 
			
		||||
                      json.field "height", quality
 | 
			
		||||
                    end
 | 
			
		||||
@@ -3704,8 +3649,6 @@ get "/api/v1/channels/:ucid" do |env|
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  channel_info
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
{"/api/v1/channels/:ucid/videos", "/api/v1/channels/videos/:ucid"}.each do |route|
 | 
			
		||||
@@ -3722,7 +3665,7 @@ end
 | 
			
		||||
    sort_by ||= "newest"
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      author, ucid, auto_generated = get_about_info(ucid, locale)
 | 
			
		||||
      channel = get_about_info(ucid, locale)
 | 
			
		||||
    rescue ex
 | 
			
		||||
      error_message = {"error" => ex.message}.to_json
 | 
			
		||||
      env.response.status_code = 500
 | 
			
		||||
@@ -3730,7 +3673,7 @@ end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      videos, count = get_60_videos(ucid, page, auto_generated, sort_by)
 | 
			
		||||
      videos, count = get_60_videos(channel.ucid, page, channel.auto_generated, sort_by)
 | 
			
		||||
    rescue ex
 | 
			
		||||
      error_message = {"error" => ex.message}.to_json
 | 
			
		||||
      env.response.status_code = 500
 | 
			
		||||
@@ -3786,16 +3729,16 @@ end
 | 
			
		||||
    sort_by ||= "last"
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      author, ucid, auto_generated = get_about_info(ucid, locale)
 | 
			
		||||
      channel = get_about_info(ucid, locale)
 | 
			
		||||
    rescue ex
 | 
			
		||||
      error_message = {"error" => ex.message}.to_json
 | 
			
		||||
      env.response.status_code = 500
 | 
			
		||||
      next error_message
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    items, continuation = fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
 | 
			
		||||
    items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
 | 
			
		||||
 | 
			
		||||
    response = JSON.build do |json|
 | 
			
		||||
    JSON.build do |json|
 | 
			
		||||
      json.object do
 | 
			
		||||
        json.field "playlists" do
 | 
			
		||||
          json.array do
 | 
			
		||||
@@ -3810,8 +3753,6 @@ end
 | 
			
		||||
        json.field "continuation", continuation
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    response
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,35 @@ struct ChannelVideo
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
struct AboutRelatedChannel
 | 
			
		||||
  db_mapping({
 | 
			
		||||
    ucid:             String,
 | 
			
		||||
    author:           String,
 | 
			
		||||
    author_url:       String,
 | 
			
		||||
    author_thumbnail: String,
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
# TODO: Refactor into either SearchChannel or InvidiousChannel
 | 
			
		||||
struct AboutChannel
 | 
			
		||||
  db_mapping({
 | 
			
		||||
    ucid:               String,
 | 
			
		||||
    author:             String,
 | 
			
		||||
    auto_generated:     Bool,
 | 
			
		||||
    author_url:         String,
 | 
			
		||||
    author_thumbnail:   String,
 | 
			
		||||
    banner:             String,
 | 
			
		||||
    description_html:   String,
 | 
			
		||||
    paid:               Bool,
 | 
			
		||||
    total_views:        Int64,
 | 
			
		||||
    sub_count:          Int64,
 | 
			
		||||
    joined:             Time,
 | 
			
		||||
    is_family_friendly: Bool,
 | 
			
		||||
    allowed_regions:    Array(String),
 | 
			
		||||
    related_channels:   Array(AboutRelatedChannel),
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10)
 | 
			
		||||
  finished_channel = Channel(String | Nil).new
 | 
			
		||||
 | 
			
		||||
@@ -617,8 +646,59 @@ def get_about_info(ucid, locale)
 | 
			
		||||
  sub_count ||= 0
 | 
			
		||||
 | 
			
		||||
  author = about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).not_nil!.content
 | 
			
		||||
  author_url = about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).not_nil!["href"]
 | 
			
		||||
  author_thumbnail = about.xpath_node(%q(//img[@class="channel-header-profile-image"])).not_nil!["src"]
 | 
			
		||||
 | 
			
		||||
  ucid = about.xpath_node(%q(//meta[@itemprop="channelId"])).not_nil!["content"]
 | 
			
		||||
 | 
			
		||||
  banner = about.xpath_node(%q(//div[@id="gh-banner"]/style)).not_nil!.content
 | 
			
		||||
  banner = "https:" + banner.match(/background-image: url\((?<url>[^)]+)\)/).not_nil!["url"]
 | 
			
		||||
 | 
			
		||||
  description_html = about.xpath_node(%q(//div[contains(@class,"about-description")])).try &.to_s || ""
 | 
			
		||||
 | 
			
		||||
  paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
 | 
			
		||||
  is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
 | 
			
		||||
  allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
 | 
			
		||||
 | 
			
		||||
  related_channels = about.xpath_nodes(%q(//div[contains(@class, "branded-page-related-channels")]/ul/li))
 | 
			
		||||
  related_channels = related_channels.map do |node|
 | 
			
		||||
    related_id = node["data-external-id"]?
 | 
			
		||||
    related_id ||= ""
 | 
			
		||||
 | 
			
		||||
    anchor = node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
 | 
			
		||||
    related_title = anchor.try &.["title"]
 | 
			
		||||
    related_title ||= ""
 | 
			
		||||
 | 
			
		||||
    related_author_url = anchor.try &.["href"]
 | 
			
		||||
    related_author_url ||= ""
 | 
			
		||||
 | 
			
		||||
    related_author_thumbnail = node.xpath_node(%q(.//img)).try &.["data-thumb"]
 | 
			
		||||
    related_author_thumbnail ||= ""
 | 
			
		||||
 | 
			
		||||
    AboutRelatedChannel.new(
 | 
			
		||||
      ucid: related_id,
 | 
			
		||||
      author: related_title,
 | 
			
		||||
      author_url: related_author_url,
 | 
			
		||||
      author_thumbnail: related_author_thumbnail,
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  total_views = 0_i64
 | 
			
		||||
  sub_count = 0_i64
 | 
			
		||||
 | 
			
		||||
  joined = Time.unix(0)
 | 
			
		||||
  metadata = about.xpath_nodes(%q(//span[@class="about-stat"]))
 | 
			
		||||
  metadata.each do |item|
 | 
			
		||||
    case item.content
 | 
			
		||||
    when .includes? "views"
 | 
			
		||||
      total_views = item.content.gsub(/\D/, "").to_i64
 | 
			
		||||
    when .includes? "subscribers"
 | 
			
		||||
      sub_count = item.content.delete("subscribers").gsub(/\D/, "").to_i64
 | 
			
		||||
    when .includes? "Joined"
 | 
			
		||||
      joined = Time.parse(item.content.lchop("Joined "), "%b %-d, %Y", Time::Location.local)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Auto-generated channels
 | 
			
		||||
  # https://support.google.com/youtube/answer/2579942
 | 
			
		||||
  auto_generated = false
 | 
			
		||||
@@ -627,7 +707,22 @@ def get_about_info(ucid, locale)
 | 
			
		||||
    auto_generated = true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  return {author, ucid, auto_generated, sub_count}
 | 
			
		||||
  return AboutChannel.new(
 | 
			
		||||
    ucid: ucid,
 | 
			
		||||
    author: author,
 | 
			
		||||
    auto_generated: auto_generated,
 | 
			
		||||
    author_url: author_url,
 | 
			
		||||
    author_thumbnail: author_thumbnail,
 | 
			
		||||
    banner: banner,
 | 
			
		||||
    description_html: description_html,
 | 
			
		||||
    paid: paid,
 | 
			
		||||
    total_views: total_views,
 | 
			
		||||
    sub_count: sub_count,
 | 
			
		||||
    joined: joined,
 | 
			
		||||
    is_family_friendly: is_family_friendly,
 | 
			
		||||
    allowed_regions: allowed_regions,
 | 
			
		||||
    related_channels: related_channels
 | 
			
		||||
  )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def get_60_videos(ucid, page, auto_generated, sort_by = "newest")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,50 @@
 | 
			
		||||
<% content_for "header" do %>
 | 
			
		||||
<title><%= author %> - Invidious</title>
 | 
			
		||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
 | 
			
		||||
<title><%= channel.author %> - Invidious</title>
 | 
			
		||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= channel.ucid %>" />
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<div class="h-box">
 | 
			
		||||
    <img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.gsub("=w1060", "=w1280")).full_path %>">
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="h-box">
 | 
			
		||||
    <hr>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="pure-g h-box">
 | 
			
		||||
    <div class="pure-u-2-3">
 | 
			
		||||
        <h3><%= author %></h3>
 | 
			
		||||
        <div class="channel-profile">
 | 
			
		||||
            <img src="/ggpht<%= URI.parse(channel.author_thumbnail.gsub("s48", "s64")).full_path %>">
 | 
			
		||||
            <span><%= channel.author %></span>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="pure-u-1-3" style="text-align:right">
 | 
			
		||||
        <h3>
 | 
			
		||||
            <a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
 | 
			
		||||
            <a href="/feed/channel/<%= channel.ucid %>"><i class="icon ion-logo-rss"></i></a>
 | 
			
		||||
        </h3>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="h-box">
 | 
			
		||||
    <% sub_count_text = number_to_short_text(sub_count) %>
 | 
			
		||||
    <% ucid = channel.ucid %>
 | 
			
		||||
    <% author = channel.author %>
 | 
			
		||||
    <% sub_count_text = number_to_short_text(channel.sub_count) %>
 | 
			
		||||
    <%= rendered "components/subscribe_widget" %>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="pure-g h-box">
 | 
			
		||||
    <div class="pure-u-1-3">
 | 
			
		||||
        <a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
 | 
			
		||||
        <% if !auto_generated %>
 | 
			
		||||
        <a href="https://www.youtube.com/channel/<%= channel.ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
 | 
			
		||||
        <% if !channel.auto_generated %>
 | 
			
		||||
            <div class="pure-u-1 pure-md-1-3">
 | 
			
		||||
                <b><%= translate(locale, "Videos") %></b>
 | 
			
		||||
            </div>
 | 
			
		||||
        <% end %>
 | 
			
		||||
        <div class="pure-u-1 pure-md-1-3">
 | 
			
		||||
            <% if auto_generated %>
 | 
			
		||||
            <% if channel.auto_generated %>
 | 
			
		||||
                <b><%= translate(locale, "Playlists") %></b>
 | 
			
		||||
            <% else %>
 | 
			
		||||
                <a href="/channel/<%= ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
 | 
			
		||||
                <a href="/channel/<%= channel.ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
 | 
			
		||||
            <% end %>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -43,7 +56,7 @@
 | 
			
		||||
                    <% if sort_by == sort %>
 | 
			
		||||
                        <b><%= translate(locale, sort) %></b>
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                        <a href="/channel/<%= ucid %>?page=<%= page %>&sort_by=<%= sort %>">
 | 
			
		||||
                        <a href="/channel/<%= channel.ucid %>?page=<%= page %>&sort_by=<%= sort %>">
 | 
			
		||||
                            <%= translate(locale, sort) %>
 | 
			
		||||
                        </a>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
@@ -68,7 +81,7 @@
 | 
			
		||||
<div class="pure-g h-box">
 | 
			
		||||
    <div class="pure-u-1 pure-u-lg-1-5">
 | 
			
		||||
        <% if page > 1 %>
 | 
			
		||||
            <a href="/channel/<%= ucid %>?page=<%= page - 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>">
 | 
			
		||||
            <a href="/channel/<%= channel.ucid %>?page=<%= page - 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>">
 | 
			
		||||
                <%= translate(locale, "Previous page") %>
 | 
			
		||||
            </a>
 | 
			
		||||
        <% end %>
 | 
			
		||||
@@ -76,7 +89,7 @@
 | 
			
		||||
    <div class="pure-u-1 pure-u-lg-3-5"></div>
 | 
			
		||||
    <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
 | 
			
		||||
        <% if count == 60 %>
 | 
			
		||||
            <a href="/channel/<%= ucid %>?page=<%= page + 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>">
 | 
			
		||||
            <a href="/channel/<%= channel.ucid %>?page=<%= page + 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>">
 | 
			
		||||
                <%= translate(locale, "Next page") %>
 | 
			
		||||
            </a>
 | 
			
		||||
        <% end %>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,35 @@
 | 
			
		||||
<% content_for "header" do %>
 | 
			
		||||
<title><%= author %> - Invidious</title>
 | 
			
		||||
<title><%= channel.author %> - Invidious</title>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<div class="pure-g h-box">
 | 
			
		||||
    <div class="pure-u-2-3">
 | 
			
		||||
        <h3><%= author %></h3>
 | 
			
		||||
        <h3><%= channel.author %></h3>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="pure-u-1-3" style="text-align:right">
 | 
			
		||||
        <h3>
 | 
			
		||||
            <a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
 | 
			
		||||
            <a href="/feed/channel/<%= channel.ucid %>"><i class="icon ion-logo-rss"></i></a>
 | 
			
		||||
        </h3>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="h-box">
 | 
			
		||||
    <% sub_count_text = number_to_short_text(sub_count) %>
 | 
			
		||||
    <% ucid = channel.ucid %>
 | 
			
		||||
    <% author = channel.author %>
 | 
			
		||||
    <% sub_count_text = number_to_short_text(channel.sub_count) %>
 | 
			
		||||
    <%= rendered "components/subscribe_widget" %>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="pure-g h-box">
 | 
			
		||||
    <div class="pure-g pure-u-1-3">
 | 
			
		||||
        <div class="pure-u-1 pure-md-1-3">
 | 
			
		||||
            <a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
 | 
			
		||||
            <a href="https://www.youtube.com/channel/<%= channel.ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pure-u-1 pure-md-1-3">
 | 
			
		||||
            <a href="/channel/<%= ucid %>"><%= translate(locale, "Videos") %></a>
 | 
			
		||||
            <a href="/channel/<%= channel.ucid %>"><%= translate(locale, "Videos") %></a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pure-u-1 pure-md-1-3">
 | 
			
		||||
            <% if !auto_generated %>
 | 
			
		||||
            <% if !channel.auto_generated %>
 | 
			
		||||
                <b><%= translate(locale, "Playlists") %></b>
 | 
			
		||||
            <% end %>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -40,7 +42,7 @@
 | 
			
		||||
                    <% if sort_by == sort %>
 | 
			
		||||
                        <b><%= translate(locale, sort) %></b>
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                        <a href="/channel/<%= ucid %>/playlists?sort_by=<%= sort %>">
 | 
			
		||||
                        <a href="/channel/<%= channel.ucid %>/playlists?sort_by=<%= sort %>">
 | 
			
		||||
                            <%= translate(locale, sort) %>
 | 
			
		||||
                        </a>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
@@ -66,7 +68,7 @@
 | 
			
		||||
    <div class="pure-u-1 pure-u-md-4-5"></div>
 | 
			
		||||
    <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
 | 
			
		||||
        <% if items.size >= 28 %>
 | 
			
		||||
            <a href="/channel/<%= ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= sort_by %><% end %>">
 | 
			
		||||
            <a href="/channel/<%= channel.ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= sort_by %><% end %>">
 | 
			
		||||
                <%= translate(locale, "Next page") %>
 | 
			
		||||
            </a>
 | 
			
		||||
        <% end %>
 | 
			
		||||
 
 | 
			
		||||
@@ -83,9 +83,9 @@ var video_data = {
 | 
			
		||||
<div class="pure-g">
 | 
			
		||||
    <div class="pure-u-1 pure-u-lg-1-5">
 | 
			
		||||
        <div class="h-box">
 | 
			
		||||
            <p>
 | 
			
		||||
            <span>
 | 
			
		||||
                <a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch on YouTube") %></a>
 | 
			
		||||
            </p>
 | 
			
		||||
            </span>
 | 
			
		||||
            <p>
 | 
			
		||||
                <% if params.annotations %>
 | 
			
		||||
                    <a href="/watch?<%= env.params.query %>&iv_load_policy=3">
 | 
			
		||||
@@ -165,11 +165,12 @@ var video_data = {
 | 
			
		||||
 | 
			
		||||
    <div class="pure-u-1 <% if params.related_videos || plid %>pure-u-lg-3-5<% else %>pure-u-md-4-5<% end %>">
 | 
			
		||||
        <div class="h-box">
 | 
			
		||||
            <p>
 | 
			
		||||
                <a href="/channel/<%= video.ucid %>">
 | 
			
		||||
                    <h3><%= video.author %></h3>
 | 
			
		||||
                </a>
 | 
			
		||||
            </p>
 | 
			
		||||
            <a href="/channel/<%= video.ucid %>">
 | 
			
		||||
                <div class="channel-profile">
 | 
			
		||||
                    <img src="/ggpht<%= URI.parse(video.author_thumbnail.gsub("s48", "s64")).full_path %>">
 | 
			
		||||
                    <span><%= video.author %></span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <% ucid = video.ucid %>
 | 
			
		||||
            <% author = video.author %>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user