はじめに
現在私はポートフォリオを作成しており、Youtubeapiを導入して、データの取得はできるようになりました。なので、次にAPIのリクエスト制限を回避したいと思い、それらの動画データをデータベースに保存しようと考えました。また、レスポンスも早くなりそうだなと感じ、ユーザーの満足度も少し向上するなと考えております。
現状
Viewで動画検索をするとクエリパラメータ
を作成し、Youtube apiがそれらをもとに動画データを取得して、動画を表示することは以下のコードできています。
<h1>YouTube動画検索</h1>
<%= form_with url: videos_path, method: :get, local: true do |f| %>
<%= f.text_field :query, placeholder: '検索キーワードを入力' %>
<%= f.submit '検索' %>
<% end %>
<% if @videos.present? %>
<h2>検索結果</h2>
<div>
<% @videos.each do |video| %>
<div>
<div>
<iframe width="100%" height="200" src="https://www.youtube.com/embed/<%= video[:channel_id] %>" frameborder="0" allowfullscreen></iframe>
<div>
<h5><%= video[:channel_title] %></h5>
<p><%= video[:title] %></p>
<% if current_user %>
<%= render 'shared/btn', video: video %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<p>動画が見つかりませんでした。</p>
<% end %>
def index
youtube = Rails.application.config.youtube_service
if params[:query].present?
search_query = params[:query]
@videos = search_youtube_videos(search_query, youtube)
end
end
private
def search_youtube_videos(query, youtube)
response = youtube.list_searches(
'snippet',
q: query,
max_results: 1
)
response.items.map do |item|
{
title: item.snippet.title,
channel_id: item.id.video_id,
channel_title: item.snippet.channel_title
}
end
end
現状では、コントローラでAPIデータを取得し、データの処理をしていますが、これは以下の理由によりあまり良くないそうです。
- コントローラが肥大化しやすい
- コードの再利用性が低い
そのため、動画のデータを保存する役割と現在のAPIデータを取得・処理する役割をサービスクラスを活用した設計にしたいと思います。
サービスクラス
アプリケーションの具体的な処理やロジックを実装するための専用クラスで、今回だとYoutube apiの処理を行うために必要です。
流れ
今回行いたい処理は大きく2つです。
1つ目は、自分の筋トレスタイルを選択して、カテゴリーの画像を押すとそれらのクエリパラメータを取得することができ、それらに当てはまる動画をデータベースから表示する処理です。「筋トレのスタイル」に応じた「筋トレ部位」の動画を保存しておく必要があります。例えば「筋トレのスタイル」がジムの場合、「筋トレの部位」は合計で7つ用意しているので、それぞれ2つずつの動画を保存するとジムだけでも14の動画データを保存する必要があります。
2つ目は、動画のフリー検索から動画を保存する処理です。これについても、筋トレ以外の動画が表示されないようにしたいので、Youtube api
から取得した動画の説明欄から、まず用意している7つのカテゴリーがあれば動画を保存し、カテゴリー名がなければ動画が保存できず、表示されないようにする必要があります。
実装
1つ目の流れを実装していきます。まずはサービスクラスの用意をします。app/services/youtube_video_service.rb
を作成してください。ここではAPIから取得したデータを処理して適切なカテゴリーとスタイルに紐付けする仕組みを実装します。まずは完成のコードです。
class YoutubeVideoService
def initialize(youtube = Rails.application.config.youtube_service)
@youtube = youtube
end
def fetch_and_save_videos_for_category_and_style(category_name, style_name)
category = Category.find_by(name: category_name)
training_style = TrainingStyle.find_by(name: style_name)
return unless category && training_style
videos = fetch_videos_from_youtube("#{style_name} #{category_name}")
save_videos_to_database(category, training_style, videos)
end
private
def fetch_videos_from_youtube(query)
response = @youtube.list_searches(
'snippet',
q: "筋トレ #{query}",
max_results: 2
)
response.items.map do |item|
{
title: item.snippet.title,
video_id: item.id.video_id,
channel_title: item.snippet.channel_title
}
end
end
def save_videos_to_database(category, training_style, videos)
videos.each do |video_information|
category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
video.title = video_information[:title]
video.channel_title = video_information[:channel_title_]
video.training_style = training_style
end
end
end
end
1 クラス定義と初期化メソッド
ここから少しずつ解説をしていきます。
class YoutubeVideoService
def initialize(youtube = Rails.application.config.youtube_service)
@youtube = youtube
end
上記の記述はクラス定義と初期化メソッドを行なっています。まずYoutubeVideoServiceというサービスクラスをつけ、これらが動画の取得と保存を行います。initialize
メソッドを活用することで、特別な設定をすることなくAPIクライアントが使用できるようになっています。それらを@youtube
に格納することで、再利用しやすくしています。
2 メインメソッド
def fetch_and_save_videos_for_category_and_style(category_name, style_name)
category = Category.find_by(name: category_name)
training_style = TrainingStyle.find_by(name: style_name)
return unless category && training_style
videos = fetch_videos_from_youtube("#{style_name} #{category_name}")
save_videos_to_database(category, training_style, videos)
end
上記の部分がメインメソッドになります。fetch_videos_from_youtube
メソッドとsave_videos_to_database
メソッドは後で詳しく説明をします。
このメソッドでは、特定のカテゴリーとスタイルに応じたYoutube動画を取得し、それらをデータベースに保存するメソッドです。
category = Category.find_by(name: category_name)
training_style = TrainingStyle.find_by(name: style_name)
まず、Category.find_by
によりカテゴリーテーブルから指定されたレコードの取得を行います。どのレコードを取得するかは(name: category_name)
や(name: style_name)
の部分で判断しています。(name: category_name)を例にして以下に説明をまとめます。
コード | 役割 |
---|---|
name | categoriesテーブルのカラム名 |
category_name | メソッドに渡された引数 |
メインメソッドを活用する際に、例えば、Category.all.each do |category|
でcategory.name
とすることで、メソッドの引数にnameカラムの要素を1つずつ取得することができ、name
と一致するそれぞれのnameカラムのレコードを取得していくことができる。
3 Youtube apiからデータの取得
def fetch_videos_from_youtube(query)
response = @youtube.list_searches(
'snippet',
q: "筋トレ #{query}",
max_results: 2
)
response.items.map do |item|
{
title: item.snippet.title,
video_id: item.id.video_id,
channel_title: item.snippet.channel_title
}
end
end
こちらは@youtube.list_searches
で動画のデータを検索してくれるメソッドです。YouTube Data API
に対して検索クエリを送信し、その結果をresponse
変数に格納しています。以下はAPIリクエストのプロパティ一覧です。
プロパティ | 役割 |
---|---|
part | どの種類のデータを取得するか指定する。snippet 、id などがある。snippet はタイトル・チャンネル情報・説明文などのデータが取得できる |
q | 検索クエリであり、キーワードが入る |
maxResults | 取得する動画の最大件数で、指定できる値は1〜50
|
type | 検索するデータの種類を指定する。partとの違いは、そもそも動画を取得したいのか、チャンネルの情報を取得したいのかなど、検索範囲を指定している。partはデータの種類なので、typeで指定された範囲でデータを取得する |
order | 検索結果の並び順を指定する。date (新しい順)、rating (評価の高い順)、viewCount (再生回数の多い順)、relevance (関連性が高い順)などがある |
videoDuration | 動画の長さでフィルタリングをする。short (4分未満)、medium (4分以上20分未満)、long (20分以上)などがある |
それぞれ格納したデータをmap
を活用して、タイトルと動画IDとチャンネル名を含むハッシュの形で成形しています。
4 データベースへの保存メソッド
def save_videos_to_database(category, training_style, videos)
videos.each do |video_information|
category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
video.title = video_information[:title]
video.channel_title = video_information[:channel_title]
video.training_style = training_style
end
end
end
動画情報を1件ずつ取り出すために、each
文を活用します。
category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
上記の部分のcategory.videos
はcategoryに関連付けられたvideoテーブルを対象に検索または新規作成を行います。つまり、categoryが「胸筋」の場合、胸筋のcategory_id
が1なので、それらのデータを検索し、新規作成されたらcategory_id
に1がデータ保存される仕組みです。
find_or_create_by
はデータがあれば何もせず、なければデータを保存する役割です。それらを調べるのに、videosテーブルのchannel_idカラムをもとに、今回のデータが重複していないか調べ、重複していなかったらデータが保存される仕組みです。また、このchannel_id
はcategory_id
と同様に新規作成されたらデータが保存される仕組みです。
5 Rakeタスク実行
これまでに作成した、サービスクラスを以下のように入力して実行してください。
namespace :fetch_video do
desc 'カテゴリーごとのYoutube動画データを取得して保存'
task fetch_video: :environment do
styles = %w[ジム 自重 ダンベル]
Category.all.each do |category|
styles.each do |style|
YoutubeVideoService.new.fetch_and_save_videos_for_category_and_style(category.name, style)
puts "動画データを保存しました: #{category.name}の#{style}"
end
end
end
end
これを記述できたら、あとはrakeタスクを実行してください。
docker-compose exec web bundle exec rake fetch_video:fetch_video
以上で、1つ目にやりたい「筋トレのスタイル」に応じた「筋トレ部位」の動画を保存する実装です。
次に、2つ目の実装の方法です。先ほどのサービスクラスに追記していきます。まずは完成版(先ほどのコードも有り)を以下に記載します。
class YoutubeVideoService
def initialize(youtube = Rails.application.config.youtube_service)
@youtube = youtube
end
def fetch_and_save_videos_for_category_and_style(category_name, style_name)
category = Category.find_by(name: category_name)
training_style = TrainingStyle.find_by(name: style_name)
return unless category && training_style
videos = fetch_videos_from_youtube("#{style_name} #{category_name}")
save_videos_to_database(category, training_style, videos)
end
def fetch_and_save_videos_from_youtube(query)
videos = fetch_videos_from_youtube(query)
classify_and_save_videos(videos)
end
private
def fetch_videos_from_youtube(query)
response = @youtube.list_searches(
'snippet',
q: "筋トレ #{query}",
max_results: 2
)
response.items.map do |item|
{
title: item.snippet.title,
video_id: item.id.video_id,
channel_title: item.snippet.channel_title,
description: item.snippet.description
}
end
end
def classify_and_save_videos(videos)
categorized_videos = []
videos.each do |video_information|
category = determine_category(video_information[:title], video_information[:description])
training_style = determine_training_style(video_information[:title], video_information[:description])
next unless category # カテゴリが特定できなければスキップ
video = category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
video.title = video_information[:title]
video.channel_title = video_information[:channel_title]
video.training_style = training_style
end
categorized_videos << video
end
categorized_videos
end
def determine_category(title, description)
category_keywords = %w[胸筋 背筋 肩 腹筋 脚 上腕三頭筋 上腕二頭筋]
matched_keyword = category_keywords.find { |keyword| title.include?(keyword) || description.include?(keyword) }
matched_keyword ? Category.find_by(name: matched_keyword) : nil
end
def determine_training_style(title, description)
if title.include?("ジム") || description.include?("ジム")
TrainingStyle.find_by(name: "ジム")
elsif title.include?("ダンベル") || description.include?("ダンベル")
TrainingStyle.find_by(name: "ダンベル")
else
TrainingStyle.find_by(name: "自重") # デフォルトは自重
end
end
def save_videos_to_database(category, training_style, videos)
videos.each do |video_information|
category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
video.title = video_information[:title]
video.channel_title = video_information[:channel_title_]
video.training_style = training_style
end
end
end
end
以下に詳細の説明を行っていきます。
1 フリー検索のリクエストを受け取る
ビューでフリー検索をして、それらの値をクエリパラメータに送り、コントローラで受け取る記述が以下になります。
<%= form_with url: videos_path, method: :get, local: true do |f| %>
<%= f.text_field :query, placeholder: '検索キーワードを入力' %>
<%= f.submit '検索'%>
<% end %>
if params[:query].present?
search_query = params[:query]
@videos = YoutubeVideoService.new.fetch_and_save_videos_from_youtube(search_query)
else
@videos = []
end
これらができたら、実際にサービスクラスのfetch_and_save_videos_from_youtube
メソッドの詳細を説明していきます。
2 サービスクラスの追記
def fetch_and_save_videos_from_youtube(query)
videos = fetch_videos_from_youtube(query)
classify_and_save_videos(videos)
end
private
def fetch_videos_from_youtube(query) #もともと作成しているメソッド
response = @youtube.list_searches(
'snippet',
q: "筋トレ #{query}",
max_results: 2
)
response.items.map do |item|
{
title: item.snippet.title,
video_id: item.id.video_id,
channel_title: item.snippet.channel_title,
description: item.snippet.description #この部分を追記
}
end
end
def classify_and_save_videos(videos)
categorized_videos = []
videos.each do |video_information|
category = determine_category(video_information[:title], video_information[:description])
training_style = determine_training_style(video_information[:title], video_information[:description])
next unless category # カテゴリが特定できなければスキップ
video = category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
video.title = video_information[:title]
video.channel_title = video_information[:channel_title]
video.training_style = training_style
end
categorized_videos << video
end
categorized_videos
end
def determine_category(title, description)
category_keywords = %w[胸筋 背筋 肩 腹筋 脚 上腕三頭筋 上腕二頭筋]
matched_keyword = category_keywords.find { |keyword| title.include?(keyword) || description.include?(keyword) }
matched_keyword ? Category.find_by(name: matched_keyword) : nil
end
def determine_training_style(title, description)
if title.include?("ジム") || description.include?("ジム")
TrainingStyle.find_by(name: "ジム")
elsif title.include?("ダンベル") || description.include?("ダンベル")
TrainingStyle.find_by(name: "ダンベル")
else
TrainingStyle.find_by(name: "自重") # デフォルトは自重
end
end
まずはclassify_and_save_videos
メソッドについて説明していきます。
def classify_and_save_videos(videos)
categorized_videos = []
videos.each do |video_information|
category = determine_category(video_information[:title], video_information[:description])
training_style = determine_training_style(video_information[:title], video_information[:description])
next unless category # カテゴリが特定できなければスキップ
video = category.videos.find_or_create_by(channel_id: video_information[:video_id]) do |video|
video.title = video_information[:title]
video.channel_title = video_information[:channel_title]
video.training_style = training_style
end
categorized_videos << video
end
まずこのメソッドの役割は取得した動画のデータをカテゴリーとスタイルし、データベースに保存するものです。さらにその後に、保存したデータをすぐに表示等の処理で使用するため、動画のリストを一度categorized_videos
にためて、その後のコントローラやビューで使用できるようにするのに、categorized_videos
が必要です。
その他の処理については、1つ目の実践と重なる部分が多いため省略します。
次にカテゴリーやスタイルの分類の処理です。
def determine_category(title, description)
category_keywords = %w[胸筋 背筋 肩 腹筋 脚 上腕三頭筋 上腕二頭筋]
matched_keyword = category_keywords.find { |keyword| title.include?(keyword) || description.include?(keyword) }
matched_keyword ? Category.find_by(name: matched_keyword) : nil
end
def determine_training_style(title, description)
if title.include?("ジム") || description.include?("ジム")
TrainingStyle.find_by(name: "ジム")
elsif title.include?("ダンベル") || description.include?("ダンベル")
TrainingStyle.find_by(name: "ダンベル")
else
TrainingStyle.find_by(name: "自重") # デフォルトは自重
end
end
動画のtitle
、description
にcategory_keywords
の配列の文字があれば、それに応じたcategory_id
がfind_by
によって取得ができます。スタイルも同じような流れですが、スタイルの場合はカテゴリーが取得できたら、スタイルに当てはまるものがなくてもデフォルトで自重になるようしています。
ここまでの流れで作成したメソッドをfetch_and_save_videos_from_youtube
メソッドでまとめている。
おわりに
ここまでの流れをChatGPTにメンター代わりになってもらい、少しずつ理解しながら進めてきました。少しずつですが、理解が進んでいますが、やはり細かい部分の勉強がもっと必要だなと感じました。このアプリが完成したらチェリー本で勉強していきたいと思います。最後まで閲覧いただきまことにありがとうございましt