Skip to content

Commit 64cfd22

Browse files
committed
Add support for subscribing to channels via PubSubHubbub
1 parent 17cf077 commit 64cfd22

5 files changed

Lines changed: 110 additions & 14 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
3+
psql invidious -c "ALTER TABLE channels ADD COLUMN subscribed bool;"
4+
psql invidious -c "UPDATE channels SET subscribed = false;"

src/invidious.cr

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ require "./invidious/helpers/*"
2828
require "./invidious/*"
2929

3030
CONFIG = Config.from_yaml(File.read("config/config.yml"))
31-
HMAC_KEY = CONFIG.hmac_key || Random::Secure.random_bytes(32)
31+
HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32)
3232

3333
config = CONFIG
3434
logger = Invidious::LogHandler.new
@@ -88,6 +88,7 @@ PG_DB = DB.open PG_URL
8888
YT_URL = URI.parse("https://www.youtube.com")
8989
REDDIT_URL = URI.parse("https://www.reddit.com")
9090
LOGIN_URL = URI.parse("https://accounts.google.com")
91+
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
9192
TEXTCAPTCHA_URL = URI.parse("http://textcaptcha.com/omarroth@hotmail.com.json")
9293
CURRENT_COMMIT = `git rev-list HEAD --max-count=1 --abbrev-commit`.strip
9394
CURRENT_VERSION = `git describe --tags $(git rev-list --tags --max-count=1)`.strip
@@ -115,6 +116,8 @@ refresh_channels(PG_DB, logger, config.channel_threads, config.full_refresh)
115116

116117
refresh_feeds(PG_DB, logger, config.feed_threads)
117118

119+
subscribe_to_feeds(PG_DB, logger, HMAC_KEY, config)
120+
118121
config.video_threads.times do |i|
119122
spawn do
120123
refresh_videos(PG_DB, logger)
@@ -2314,6 +2317,58 @@ get "/feed/playlist/:plid" do |env|
23142317
document
23152318
end
23162319

2320+
# Add support for subscribing to channels via PubSubHubbub
2321+
2322+
get "/feed/webhook" do |env|
2323+
mode = env.params.query["hub.mode"]
2324+
topic = env.params.query["hub.topic"]
2325+
challenge = env.params.query["hub.challenge"]
2326+
lease_seconds = env.params.query["hub.lease_seconds"]
2327+
verify_token = env.params.query["hub.verify_token"]
2328+
2329+
time, signature = verify_token.split(":")
2330+
2331+
if Time.now.to_unix - time.to_i > 600
2332+
halt env, status_code: 400
2333+
end
2334+
2335+
if OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, time) != signature
2336+
halt env, status_code: 400
2337+
end
2338+
2339+
ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]
2340+
PG_DB.exec("UPDATE channels SET subscribed = true WHERE ucid = $1", ucid)
2341+
2342+
halt env, status_code: 200, response: challenge
2343+
end
2344+
2345+
post "/feed/webhook" do |env|
2346+
body = env.request.body.not_nil!.gets_to_end
2347+
signature = env.request.headers["X-Hub-Signature"].lchop("sha1=")
2348+
2349+
if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body)
2350+
halt env, status_code: 200
2351+
end
2352+
2353+
rss = XML.parse_html(body)
2354+
rss.xpath_nodes("//feed/entry").each do |entry|
2355+
id = entry.xpath_node("videoid").not_nil!.content
2356+
2357+
video = get_video(id, PG_DB, proxies)
2358+
video = ChannelVideo.new(id, video.title, video.published, Time.now, video.ucid, video.author, video.length_seconds)
2359+
2360+
PG_DB.exec("UPDATE users SET notifications = notifications || $1 \
2361+
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, video.ucid)
2362+
2363+
video_array = video.to_a
2364+
args = arg_array(video_array)
2365+
2366+
PG_DB.exec("INSERT INTO channel_videos VALUES (#{args}) \
2367+
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
2368+
updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array)
2369+
end
2370+
end
2371+
23172372
# Channels
23182373

23192374
# YouTube appears to let users set a "brand" URL that

src/invidious/channels.cr

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
class InvidiousChannel
22
add_mapping({
3-
id: String,
4-
author: String,
5-
updated: Time,
6-
deleted: Bool,
3+
id: String,
4+
author: String,
5+
updated: Time,
6+
deleted: Bool,
7+
subscribed: {type: Bool, default: false},
78
})
89
end
910

@@ -15,10 +16,7 @@ class ChannelVideo
1516
updated: Time,
1617
ucid: String,
1718
author: String,
18-
length_seconds: {
19-
type: Int32,
20-
default: 0,
21-
},
19+
length_seconds: {type: Int32, default: 0},
2220
})
2321
end
2422

@@ -188,11 +186,29 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
188186
db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{ids.map { |id| %("#{id}") }.join(",")}}') AND ucid = $1", ucid)
189187
end
190188

191-
channel = InvidiousChannel.new(ucid, author, Time.now, false)
189+
channel = InvidiousChannel.new(ucid, author, Time.now, false, false)
192190

193191
return channel
194192
end
195193

194+
def subscribe_pubsub(ucid, key, config)
195+
client = make_client(PUBSUB_URL)
196+
time = Time.now.to_unix.to_s
197+
198+
host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
199+
200+
body = {
201+
"hub.callback" => "#{host_url}/feed/webhook",
202+
"hub.topic" => "https://www.youtube.com/feeds/videos.xml?channel_id=#{ucid}",
203+
"hub.verify" => "async",
204+
"hub.mode" => "subscribe",
205+
"hub.verify_token" => "#{time}:#{OpenSSL::HMAC.hexdigest(:sha1, key, time)}",
206+
"hub.secret" => key.to_s,
207+
}
208+
209+
return client.post("/subscribe", form: body)
210+
end
211+
196212
def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
197213
client = make_client(YT_URL)
198214

src/invidious/helpers/helpers.cr

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ user: String,
1111
port: Int32,
1212
dbname: String,
1313
),
14-
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
15-
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
16-
hmac_key: String?, # HMAC signing key for CSRF tokens
17-
domain: String?, # Domain to be used for links to resources on the site where an absolute URL is required
14+
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
15+
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
16+
hmac_key: String?, # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
17+
domain: String?, # Domain to be used for links to resources on the site where an absolute URL is required
18+
use_pubsub_feeds: {type: Bool, default: false}, # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
1819
default_home: {type: String, default: "Top"},
1920
feed_menu: {type: Array(String), default: ["Popular", "Top", "Trending"]},
2021
top_enabled: {type: Bool, default: true},

src/invidious/jobs.cr

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,26 @@ def refresh_feeds(db, logger, max_threads = 1)
153153
max_channel.send(max_threads)
154154
end
155155

156+
def subscribe_to_feeds(db, logger, key, config)
157+
if config.use_pubsub_feeds
158+
spawn do
159+
loop do
160+
db.query_all("SELECT ucid FROM channels WHERE subscribed = false") do |rs|
161+
ucid = rs.read(String)
162+
response = subscribe_pubsub(ucid, key, config)
163+
164+
if response.status_code >= 400
165+
logger.write("#{ucid} : #{response.body}\n")
166+
end
167+
end
168+
169+
sleep 1.minute
170+
Fiber.yield
171+
end
172+
end
173+
end
174+
end
175+
156176
def pull_top_videos(config, db)
157177
loop do
158178
begin

0 commit comments

Comments
 (0)