invidious/src/invidious/routes/api/v1/channels.cr
Samantaz Fox 7c1d2714e0
Comments: Add support for new format (#4576)
The new comment format is similar to the description's commandRuns.

This should fix the issues with most comments but there are still
some more changes that would need to be made like adding support for
formatting (bold, italic, underline) and channel emojis.

Fixes issue 4566
2024-04-26 23:48:15 +02:00

470 lines
13 KiB
Crystal

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 "tags", channel.tags
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, next_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", next_continuation if next_continuation
end
end
end
def self.podcasts(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
continuation = env.params.query["continuation"]?
# Use the macro defined above
channel = nil # Make the compiler happy
get_channel()
items, next_continuation = fetch_channel_podcasts(channel.ucid, channel.author, continuation)
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", next_continuation if next_continuation
end
end
end
def self.releases(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
continuation = env.params.query["continuation"]?
# Use the macro defined above
channel = nil # Make the compiler happy
get_channel()
items, next_continuation = fetch_channel_releases(channel.ucid, channel.author, continuation)
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", next_continuation if next_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.post(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
id = env.params.url["id"].to_s
ucid = env.params.query["ucid"]?
thin_mode = env.params.query["thin_mode"]?
thin_mode = thin_mode == "true"
format = env.params.query["format"]?
format ||= "json"
if ucid.nil?
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
return error_json(400, "Invalid post ID") if response["error"]?
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
else
ucid = ucid.to_s
end
begin
fetch_channel_community_post(ucid, id, locale, format, thin_mode)
rescue ex
return error_json(500, ex)
end
end
def self.post_comments(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
id = env.params.url["id"]
thin_mode = env.params.query["thin_mode"]?
thin_mode = thin_mode == "true"
format = env.params.query["format"]?
format ||= "json"
continuation = env.params.query["continuation"]?
case continuation
when nil, ""
ucid = env.params.query["ucid"]
comments = Comments.fetch_community_post_comments(ucid, id)
else
comments = YoutubeAPI.browse(continuation: continuation)
end
return Comments.parse_youtube(id, comments, format, locale, thin_mode, is_post: true)
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