Skip to content

Commit

Permalink
Add YouTube iCalendar endpoint. It should make it possible to subscri…
Browse files Browse the repository at this point in the history
…be to channels in calendar applications and get notified on upcoming live streams, and easily find recent videos.
  • Loading branch information
stefansundin committed Apr 17, 2020
1 parent e02c0f7 commit 3604d00
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 0 deletions.
52 changes: 52 additions & 0 deletions app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,58 @@
end
end

get "/youtube/:channel_id/:username.ics" do
@channel_id = params[:channel_id]
@username = params[:username]

# The API is really inconsistent in listing scheduled live streams, but the RSS endpoint seems to consistently list them, so experiment with using that
response = HTTP.get("https://www.youtube.com/feeds/videos.xml?channel_id=#{@channel_id}")
raise(GoogleError, response) if !response.success?
doc = Nokogiri::XML(response.body)
ids = doc.xpath("//yt:videoId").map(&:text)

response = Google.get("/youtube/v3/videos", query: { part: "snippet,liveStreamingDetails,contentDetails", id: ids.join(",") })
raise(GoogleError, response) if !response.success?
@data = response.json["items"]

if params.has_key?(:eventType)
eventTypes = params[:eventType].split(",")
eventType_completed = eventTypes.include?("completed")
eventType_live = eventTypes.include?("live")
eventType_upcoming = eventTypes.include?("upcoming")
@data.select! do |v|
v.has_key?("liveStreamingDetails") && (
(eventType_completed && v["liveStreamingDetails"].has_key?("actualEndTime")) ||
(eventType_live && v["liveStreamingDetails"].has_key?("actualStartTime") && !v["liveStreamingDetails"].has_key?("actualEndTime")) ||
(eventType_upcoming && v["liveStreamingDetails"].has_key?("scheduledStartTime") && !v["liveStreamingDetails"].has_key?("actualStartTime"))
)
end
end

if params.has_key?(:q)
@query = params[:q]
q = @query.downcase
@data.select! { |v| v["snippet"]["title"].downcase.include?(q) }
@title = "\"#{@query}\" from #{@username}"
else
@title = "#{@username} on YouTube"
end

@data.sort_by! do |video|
if video.has_key?("liveStreamingDetails")
Time.parse(video["liveStreamingDetails"]["actualStartTime"] || video["liveStreamingDetails"]["scheduledStartTime"])
else
Time.parse(video["snippet"]["publishedAt"])
end
end.reverse!

@data.map do |video|
video["snippet"]["description"].grep_urls
end.flatten.tap { |urls| URL.resolve(urls) }

erb :"youtube.ics"
end

get "/youtube/:channel_id/:username" do
@channel_id = params[:channel_id]
playlist_id = "UU" + @channel_id[2..]
Expand Down
1 change: 1 addition & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

ENV["APP_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["APP_VERSION"] ||= ENV["HEROKU_RELEASE_VERSION"] || "unknown"

require "bundler/setup"
Bundler.require(:default, ENV["APP_ENV"])
Expand Down
55 changes: 55 additions & 0 deletions views/youtube.ics.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<%- content_type :ics -%>
BEGIN:VCALENDAR
PRODID:-//RSSBox//RSSBox <%= ENV["APP_VERSION"] %>//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:<%= @title %>
X-WR-CALDESC:https://www.youtube.com/channel/<%= @channel_id %><%= Addressable::URI.new(path: "/search", query: "query=#{@query}").normalize.to_s if @query %>
<%-
@data.each do |video|
duration = video["contentDetails"]["duration"].parse_pt
title = video["snippet"]["title"].to_line
url = "https://www.youtube.com/watch?v=#{video["id"]}"
description = if duration > 0
"Duration: #{duration.to_duration}"
end
published_at = Time.parse(video["snippet"]["publishedAt"])

if video.has_key?("liveStreamingDetails")
dtstart = Time.parse(video["liveStreamingDetails"]["actualStartTime"] || video["liveStreamingDetails"]["scheduledStartTime"])
dtend = if video["liveStreamingDetails"]["actualEndTime"]
Time.parse(video["liveStreamingDetails"]["actualEndTime"])
else
[Time.now, dtstart].max + 7200
end
if video["liveStreamingDetails"].has_key?("actualEndTime")
elsif video["liveStreamingDetails"].has_key?("actualStartTime")
title += " (started)"
elsif video["liveStreamingDetails"].has_key?("scheduledStartTime")
title += " (scheduled)"
end
else
dtstart = published_at
dtend = published_at + duration
end

dtstart = dtstart.strftime("%Y%m%dT%H%M%SZ")
dtend = dtend.strftime("%Y%m%dT%H%M%SZ")
published_at = published_at.strftime("%Y%m%dT%H%M%SZ")
-%>
BEGIN:VEVENT
UID:video-<%= video["id"] %>@youtube.com
DTSTART:<%= dtstart %>
DTEND:<%= dtend %>
DTSTAMP:<%= published_at %>
CREATED:<%= published_at %>
LAST-MODIFIED:<%= published_at %>
SUMMARY:<%= title %>
LOCATION:<%= url %>
DESCRIPTION:<%= description %>
END:VEVENT

<%- end -%>
END:VCALENDAR

0 comments on commit 3604d00

Please sign in to comment.