module Invidious::Routes::API::V1::Channels # Macro to avoid duplicating some code below # This sets the `channel` variable, or handles Exceptions. private macro get_channel begin channel = get_about_info(ucid, locale) rescue ex : ChannelRedirect env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) rescue ex : NotFoundException return error_json(404, ex) rescue ex return error_json(500, ex) end end def self.home(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] env.response.content_type = "application/json" # Use the private macro defined above. channel = nil # Make the compiler happy get_channel() # Retrieve "sort by" setting from URL parameters sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" begin videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by) rescue ex return error_json(500, ex) end JSON.build do |json| # TODO: Refactor into `to_json` for InvidiousChannel json.object do json.field "author", channel.author json.field "authorId", channel.ucid json.field "authorUrl", channel.author_url json.field "authorBanners" do json.array do if channel.banner qualities = { {width: 2560, height: 424}, {width: 2120, height: 351}, {width: 1060, height: 175}, } qualities.each do |quality| json.object do json.field "url", channel.banner.not_nil!.gsub("=w1060-", "=w#{quality[:width]}-") json.field "width", quality[:width] json.field "height", quality[:height] end end json.object do json.field "url", channel.banner.not_nil!.split("=w1060-")[0] json.field "width", 512 json.field "height", 288 end end end end json.field "authorThumbnails" do json.array do qualities = {32, 48, 76, 100, 176, 512} qualities.each do |quality| json.object do json.field "url", channel.author_thumbnail.gsub(/=s\d+/, "=s#{quality}") json.field "width", quality json.field "height", quality end end end end json.field "subCount", channel.sub_count json.field "totalViews", channel.total_views json.field "joined", channel.joined.to_unix 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", channel.allowed_regions json.field "tabs", channel.tabs json.field "authorVerified", channel.verified json.field "latestVideos" do json.array do videos.each do |video| video.to_json(locale, json) end end end json.field "relatedChannels" do json.array do # Fetch related channels begin related_channels, _ = fetch_related_channels(channel) rescue ex related_channels = [] of SearchChannel end related_channels.each do |related_channel| related_channel.to_json(locale, json) end end end # relatedChannels end end end def self.latest(env) # Remove parameters that could affect this endpoint's behavior env.params.query.delete("sort_by") if env.params.query.has_key?("sort_by") env.params.query.delete("continuation") if env.params.query.has_key?("continuation") return self.videos(env) end def self.videos(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] env.response.content_type = "application/json" # Use the private macro defined above. channel = nil # Make the compiler happy get_channel() # Retrieve some URL parameters sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" continuation = env.params.query["continuation"]? begin videos, next_continuation = Channel::Tabs.get_60_videos( channel, continuation: continuation, sort_by: sort_by ) rescue ex return error_json(500, ex) end return JSON.build do |json| json.object do json.field "videos" do json.array do videos.each &.to_json(locale, json) end end json.field "continuation", next_continuation if next_continuation end end end def self.shorts(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] env.response.content_type = "application/json" # Use the private macro defined above. channel = nil # Make the compiler happy get_channel() # Retrieve continuation from URL parameters continuation = env.params.query["continuation"]? begin videos, next_continuation = Channel::Tabs.get_shorts( channel, continuation: continuation ) rescue ex return error_json(500, ex) end return JSON.build do |json| json.object do json.field "videos" do json.array do videos.each &.to_json(locale, json) end end json.field "continuation", next_continuation if next_continuation end end end def self.streams(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] env.response.content_type = "application/json" # Use the private macro defined above. channel = nil # Make the compiler happy get_channel() # Retrieve continuation from URL parameters continuation = env.params.query["continuation"]? begin videos, next_continuation = Channel::Tabs.get_60_livestreams( channel, continuation: continuation ) rescue ex return error_json(500, ex) end return JSON.build do |json| json.object do json.field "videos" do json.array do videos.each &.to_json(locale, json) end end json.field "continuation", next_continuation if next_continuation end end end def self.playlists(env) locale = env.get("preferences").as(Preferences).locale env.response.content_type = "application/json" ucid = env.params.url["ucid"] continuation = env.params.query["continuation"]? sort_by = env.params.query["sort"]?.try &.downcase || env.params.query["sort_by"]?.try &.downcase || "last" # Use the macro defined above channel = nil # Make the compiler happy get_channel() items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) JSON.build do |json| json.object do json.field "playlists" do json.array do items.each do |item| item.to_json(locale, json) if item.is_a?(SearchPlaylist) end end end json.field "continuation", continuation end end end def self.community(env) locale = env.get("preferences").as(Preferences).locale env.response.content_type = "application/json" ucid = env.params.url["ucid"] thin_mode = env.params.query["thin_mode"]? thin_mode = thin_mode == "true" format = env.params.query["format"]? format ||= "json" continuation = env.params.query["continuation"]? # sort_by = env.params.query["sort_by"]?.try &.downcase begin fetch_channel_community(ucid, continuation, locale, format, thin_mode) rescue ex return error_json(500, ex) end end def self.channels(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] env.response.content_type = "application/json" # Use the macro defined above channel = nil # Make the compiler happy get_channel() continuation = env.params.query["continuation"]? begin items, next_continuation = fetch_related_channels(channel, continuation) rescue ex return error_json(500, ex) end JSON.build do |json| json.object do json.field "relatedChannels" do json.array do items.each &.to_json(locale, json) end end json.field "continuation", next_continuation if next_continuation end end end def self.search(env) locale = env.get("preferences").as(Preferences).locale region = env.params.query["region"]? env.response.content_type = "application/json" query = Invidious::Search::Query.new(env.params.query, :channel, region) # Required because we can't (yet) pass multiple parameter to the # `Search::Query` initializer (in this case, an URL segment) query.channel = env.params.url["ucid"] begin search_results = query.process rescue ex return error_json(400, ex) end JSON.build do |json| json.array do search_results.each do |item| item.to_json(locale, json) end end end end # 301 redirect from /api/v1/channels/comments/:ucid # and /api/v1/channels/:ucid/comments to new /api/v1/channels/:ucid/community and # corresponding equivalent URL structure of the other one. def self.channel_comments_redirect(env) env.response.content_type = "application/json" ucid = env.params.url["ucid"] env.response.headers["Location"] = "/api/v1/channels/#{ucid}/community?#{env.params.query}" env.response.status_code = 301 return end end