mirror of
https://github.com/usetrmnl/plugins.git
synced 2026-08-13 14:26:58 -07:00
Merge pull request #75 from usetrmnl/open-source/youtube_analytics-20250827-182716
Open source Youtube Analytics plugin
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user