Merge pull request #75 from usetrmnl/open-source/youtube_analytics-20250827-182716

Open source Youtube Analytics plugin
This commit is contained in:
Ryan Kulp
2025-08-27 18:28:02 -04:00
committed by GitHub
6 changed files with 515 additions and 0 deletions
+1
View File
@@ -24,6 +24,7 @@ community Recipes:
- [Tempest Weather Station](/lib/tempest_weather_station)
- [Todoist](/lib/todoist)
- [Weather (generic)](/lib/weather)
- [Youtube Analytics](/lib/youtube_analytics)
native plugin structure:
+103
View File
@@ -0,0 +1,103 @@
<div class="view view--full">
<div class="layout layout--col gap--space-between">
<div class="grid">
<div class="col--span-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--xlarge value--tnums" data-value-type="number"><%= number_with_delimiter(metrics[:views]) %></span>
<span class="label label--large">Views</span>
</div>
</div>
</div>
<div class="col--span-1">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--xlarge value--tnums">
<% total_seconds = metrics[:minutes_watched].to_i * 60 %>
<% days = total_seconds / 86400 %>
<% hours = (total_seconds % 86400) / 3600 %>
<% minutes = (total_seconds % 3600) / 60 %>
<% seconds = total_seconds % 60 %>
<% if days > 0 %>
<%= days %>+
<% time_unit = 'Days' %>
<% elsif hours > 0 %>
<%= hours %>+
<% time_unit = 'Hours' %>
<% elsif minutes > 0 %>
<%= minutes %>+
<% time_unit = 'Minutes' %>
<% else %>
<%= seconds %>
<% time_unit = 'Seconds' %>
<% end %>
</span>
<span class="label label--large"><%= time_unit %> Watched</span>
</div>
</div>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-3">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">
<% avg_view_duration = metrics[:average_view_duration].to_i %>
<% minutes = avg_view_duration / 60 %>
<% seconds = avg_view_duration % 60 %>
<%= "#{minutes}m #{seconds}s" %>
</span>
<span class="label">Average View Duration</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums" data-value-type="number"><%= number_with_delimiter(metrics[:subscribers_gained]) %></span>
<span class="label">Subscribers Gained</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums"><%= (metrics[:average_view_percentage])&.round %>%</span>
<span class="label">View Through Rate</span>
</div>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-3">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums" data-value-type="number"><%= number_with_delimiter(metrics[:likes]) %></span>
<span class="label">Likes</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums" data-value-type="number"><%= number_with_delimiter(metrics[:comments]) %></span>
<span class="label">Comments</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums" data-value-type="number"><%= number_with_delimiter(metrics[:shares]) %></span>
<span class="label">Shares</span>
</div>
</div>
</div>
</div>
<div class="title_bar">
<img class="image" src="<%= Rails.application.credentials.base_url %>/images/plugins/youtube--render.svg" />
<span class="title">YouTube</span>
<span class="instance"><%= instance_name %></span>
</div>
</div>
@@ -0,0 +1,104 @@
<div class="view view--half_horizontal">
<div class="layout gap--space-between">
<div class="grid grid--cols-3 h-full">
<div class="col gap--space-between">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums" data-value-type="number"><%= number_with_delimiter(metrics[:views]) %></span>
<span class="label">Views</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">
<% total_seconds = metrics[:minutes_watched].to_i * 60 %>
<% days = total_seconds / 86400 %>
<% hours = (total_seconds % 86400) / 3600 %>
<% minutes = (total_seconds % 3600) / 60 %>
<% seconds = total_seconds % 60 %>
<% if days > 0 %>
<%= days %>+
<% time_unit = 'Days Watched' %>
<% elsif hours > 0 %>
<%= hours %>+
<% time_unit = 'Hours Watched' %>
<% elsif minutes > 0 %>
<%= minutes %>+
<% time_unit = 'Minutes Watched' %>
<% else %>
<%= pluralize(seconds, 'second') %>
<% time_unit = 'Seconds Watched' %>
<% end %>
</span>
<span class="label"><%= time_unit %></span>
</div>
</div>
</div>
<div class="col gap--space-between">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall"><%= (metrics[:average_view_percentage])&.round %>%</span>
<span class="label label--small">View Through Rate</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall">
<% avg_view_duration = metrics[:average_view_duration].to_i %>
<% minutes = avg_view_duration / 60 %>
<% seconds = avg_view_duration % 60 %>
<%= "#{minutes}m #{seconds}s" %>
</span>
<span class="label label--small">Average View Duration</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:subscribers_gained]) %></span>
<span class="label label--small">Subscribers Gained</span>
</div>
</div>
</div>
<div class="col gap--space-between">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:likes]) %></span>
<span class="label label--small">Likes</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:comments]) %></span>
<span class="label label--small">Comments</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:shares]) %></span>
<span class="label label--small">Shares</span>
</div>
</div>
</div>
</div>
</div>
<div class="title_bar">
<img class="image" src="<%= Rails.application.credentials.base_url %>/images/plugins/youtube--render.svg" />
<span class="title">YouTube</span>
<span class="instance"><%= instance_name %></span>
</div>
</div>
@@ -0,0 +1,101 @@
<div class="view view--half_vertical">
<div class="layout layout--col gap--space-between">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--large" data-value-type="number"><%= number_with_delimiter(metrics[:views]) %></span>
<span class="label">Views</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">
<% total_seconds = metrics[:minutes_watched].to_i * 60 %>
<% days = total_seconds / 86400 %>
<% hours = (total_seconds % 86400) / 3600 %>
<% minutes = (total_seconds % 3600) / 60 %>
<% seconds = total_seconds % 60 %>
<% if days > 0 %>
<%= days %>+
<% time_unit = 'Days' %>
<% elsif hours > 0 %>
<%= hours %>+
<% time_unit = 'Hours' %>
<% elsif minutes > 0 %>
<%= minutes %>+
<% time_unit = 'Minutes' %>
<% else %>
<%= pluralize(seconds, 'second') %>
<% time_unit = 'Seconds' %>
<% end %>
</span>
<span class="label"><%= time_unit %> Watched</span>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall"><%= metrics[:average_view_percentage]&.round %>%</span>
<span class="label label--small">View Through Rate</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall">
<% avg_view_duration = metrics[:average_view_duration].to_i %>
<% minutes = avg_view_duration / 60 %>
<% seconds = avg_view_duration % 60 %>
<%= "#{minutes}m #{seconds}s" %>
</span>
<span class="label label--small">Average View Duration</span>
</div>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:subscribers_gained]) %></span>
<span class="label label--small">Subscribers Gained</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:likes]) %></span>
<span class="label label--small">Likes</span>
</div>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:comments]) %></span>
<span class="label label--small">Comments</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:shares]) %></span>
<span class="label label--small">Shares</span>
</div>
</div>
</div>
</div>
<div class="title_bar">
<img class="image" src="<%= Rails.application.credentials.base_url %>/images/plugins/youtube--render.svg" />
<span class="title">YouTube</span>
<span class="instance"><%= instance_name %></span>
</div>
</div>
@@ -0,0 +1,85 @@
<div class="view view--quadrant">
<div class="layout layout--col gap--space-between">
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--small" data-value-type="number"><%= number_with_delimiter(metrics[:views]) %></span>
<span class="label">Views</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--small">
<% total_seconds = metrics[:minutes_watched].to_i * 60 %>
<% days = total_seconds / 86400 %>
<% hours = (total_seconds % 86400) / 3600 %>
<% minutes = (total_seconds % 3600) / 60 %>
<% seconds = total_seconds % 60 %>
<% if days > 0 %>
<%= days %>+
<% time_unit = 'Days' %>
<% elsif hours > 0 %>
<%= hours %>+
<% time_unit = 'Hours' %>
<% elsif minutes > 0 %>
<%= minutes %>+
<% time_unit = 'Minutes' %>
<% else %>
<%= pluralize(seconds, 'second') %>
<% time_unit = 'Seconds' %>
<% end %>
</span>
<span class="label"><%= time_unit %> Watched</span>
</div>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall"><%= (metrics[:average_view_percentage])&.round %>%</span>
<span class="label label--small">View Through Rate</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall">
<% avg_view_duration = metrics[:average_view_duration].to_i %>
<% minutes = avg_view_duration / 60 %>
<% seconds = avg_view_duration % 60 %>
<%= "#{minutes}m #{seconds}s" %>
</span>
<span class="label label--small">Average View Duration</span>
</div>
</div>
</div>
<div class="w-full b-h-gray-5"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:subscribers_gained]) %></span>
<span class="label label--small">Subscribers Gained</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums value--xsmall" data-value-type="number"><%= number_with_delimiter(metrics[:likes]) %></span>
<span class="label label--small">Likes</span>
</div>
</div>
</div>
</div>
<div class="title_bar">
<img class="image" src="<%= Rails.application.credentials.base_url %>/images/plugins/youtube--render.svg" />
<span class="title">YouTube</span>
<span class="instance"><%= instance_name %></span>
</div>
</div>
+121
View File
@@ -0,0 +1,121 @@
module Plugins
class YoutubeAnalytics < Base
def locals
{ metrics: }
end
class << self
def redirect_url
client = Signet::OAuth2::Client.new(client_options)
client.authorization_uri.to_s
end
def fetch_access_token(code)
client = Signet::OAuth2::Client.new(client_options)
client.code = code
client.fetch_access_token!
end
def client_options
{
client_id: Rails.application.credentials.plugins[:google][:client_id],
client_secret: Rails.application.credentials.plugins[:google][:client_secret],
authorization_uri: 'https://accounts.google.com/o/oauth2/auth', # may require change to: '/oauth2/v2/auth'
token_credential_uri: 'https://accounts.google.com/o/oauth2/token', # may require change to: '/oauth2/v2/token'
access_type: 'offline',
scope: [Google::Apis::YoutubeAnalyticsV2::AUTH_YT_ANALYTICS_READONLY],
redirect_uri: "#{Rails.application.credentials.base_url}/plugin_settings/youtube_analytics/redirect",
additional_parameters: {
prompt: 'consent select_account'
}
}
end
end
private
def metrics = fetch_analytics
def fetch_analytics
retry_count = 0
begin
service = Google::Apis::YoutubeAnalyticsV2::YouTubeAnalyticsService.new
service.authorization = client
report = service.query_report(
ids: "channel==MINE",
start_date: start_date,
end_date: end_date,
metrics: "views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage,subscribersGained,likes,dislikes,comments,shares",
dimensions: "day",
sort: "day"
)
process_report(report)
rescue Google::Apis::AuthorizationError
refresh!
retry_count += 1
if retry_count <= 1 # Retry just once, otherwise retry next day.
sleep 2
retry
else
plugin_settings.refresh_in_24hr
{}
end
rescue Google::Apis::ClientError => e
handle_erroring_state(e.message)
end
end
def process_report(report)
kpis = {
views: 0,
minutes_watched: 0,
average_view_duration: 0,
average_view_percentage: 0,
subscribers_gained: 0,
likes: 0,
dislikes: 0,
comments: 0,
shares: 0
}
report.rows.each do |metrics|
kpis[:views] += metrics[1].round(2)
kpis[:minutes_watched] += metrics[2].round(2)
kpis[:average_view_duration] += metrics[3].round(2)
kpis[:average_view_percentage] += metrics[4].round(2)
kpis[:subscribers_gained] += metrics[5].round(2)
kpis[:likes] += metrics[6].round(2)
kpis[:dislikes] += metrics[7].round(2)
kpis[:comments] += metrics[8].round(2)
kpis[:shares] += metrics[9].round(2)
end
kpis[:average_view_duration] = (kpis[:average_view_duration] / settings['duration'].to_i).round(2)
kpis[:average_view_percentage] = (kpis[:average_view_percentage] / settings['duration'].to_i).to_i
kpis
end
def start_date = (Date.today - settings['duration'].to_i.days).to_s
def end_date = Date.today.to_s
def client
credentials = settings['youtube_analytics']
oauth_client = Signet::OAuth2::Client.new(Plugins::YoutubeAnalytics.client_options)
oauth_client.update!(credentials)
oauth_client
end
def refresh!
response = client.refresh!
credentials = plugin_settings.encrypted_settings
credentials['youtube_analytics']['access_token'] = response['access_token']
plugin_settings.update(encrypted_settings: credentials)
self.settings = plugin_settings.settings.merge(plugin_settings.encrypted_settings)
rescue Signet::AuthorizationError
handle_erroring_state('Signet::AuthorizationError')
end
end
end