0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【rails】Youtube動画の保存について

Posted at

はじめに

現在私はポートフォリオを作成しており、Youtubeapiを導入して、データの取得はできるようになりました。なので、次にAPIのリクエスト制限を回避したいと思い、それらの動画データをデータベースに保存しようと考えました。また、レスポンスも早くなりそうだなと感じ、ユーザーの満足度も少し向上するなと考えております。

現状

Viewで動画検索をするとクエリパラメータを作成し、Youtube apiがそれらをもとに動画データを取得して、動画を表示することは以下のコードできています。

views/index.html.erb
<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 %>

videos_controller.rb
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の動画データを保存する必要があります。
スクリーンショット 2025-01-29 9.11.32.png

2つ目は、動画のフリー検索から動画を保存する処理です。これについても、筋トレ以外の動画が表示されないようにしたいので、Youtube apiから取得した動画の説明欄から、まず用意している7つのカテゴリーがあれば動画を保存し、カテゴリー名がなければ動画が保存できず、表示されないようにする必要があります。
スクリーンショット 2025-01-29 9.19.40.png

実装

1つ目の流れを実装していきます。まずはサービスクラスの用意をします。app/services/youtube_video_service.rbを作成してください。ここではAPIから取得したデータを処理して適切なカテゴリーとスタイルに紐付けする仕組みを実装します。まずは完成のコードです。

youtube_video_service.rb
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 どの種類のデータを取得するか指定する。snippetidなどがある。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_idcategory_idと同様に新規作成されたらデータが保存される仕組みです。

5 Rakeタスク実行

これまでに作成した、サービスクラスを以下のように入力して実行してください。

lib/tasks/fetch_video.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つ目の実装の方法です。先ほどのサービスクラスに追記していきます。まずは完成版(先ほどのコードも有り)を以下に記載します。

youtube_video_service.rb
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 フリー検索のリクエストを受け取る

ビューでフリー検索をして、それらの値をクエリパラメータに送り、コントローラで受け取る記述が以下になります。

videos/index.html.erb
<%= form_with url: videos_path, method: :get, local: true do |f| %>
  <%= f.text_field :query, placeholder: '検索キーワードを入力' %>
  <%= f.submit '検索'%>
<% end %>
videos_controller.rb
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 サービスクラスの追記

youtube_video_service.rb
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

動画のtitledescriptioncategory_keywordsの配列の文字があれば、それに応じたcategory_idfind_byによって取得ができます。スタイルも同じような流れですが、スタイルの場合はカテゴリーが取得できたら、スタイルに当てはまるものがなくてもデフォルトで自重になるようしています。
ここまでの流れで作成したメソッドをfetch_and_save_videos_from_youtubeメソッドでまとめている。

おわりに

ここまでの流れをChatGPTにメンター代わりになってもらい、少しずつ理解しながら進めてきました。少しずつですが、理解が進んでいますが、やはり細かい部分の勉強がもっと必要だなと感じました。このアプリが完成したらチェリー本で勉強していきたいと思います。最後まで閲覧いただきまことにありがとうございましt

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?