Class: YouTube::SyncService

Inherits:
Object
  • Object
show all
Defined in:
app/services/youtube/sync_service.rb

Overview

Synchronizes YouTube video metadata with local Video records.

Two sync directions:

  1. Pull: fetch video details from YouTube for videos with youtube_id set
  2. Full: list all channel videos and match/flag unlinked ones

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(account: nil, client: nil) ⇒ SyncService

Returns a new instance of SyncService.



13
14
15
16
# File 'app/services/youtube/sync_service.rb', line 13

def initialize(account: nil, client: nil)
  @client = client || YouTube::ApiClient.new(account: )
  @logger = Rails.logger
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



11
12
13
# File 'app/services/youtube/sync_service.rb', line 11

def client
  @client
end

#loggerObject (readonly)

Returns the value of attribute logger.



11
12
13
# File 'app/services/youtube/sync_service.rb', line 11

def logger
  @logger
end

Instance Method Details

#find_unlinked_channel_videosArray<Hash>

List all videos on the connected YouTube channel and return
any that don't have a matching local Video record.

Returns:

  • (Array<Hash>)

    array of { youtube_id:, title:, published_at: }



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'app/services/youtube/sync_service.rb', line 70

def find_unlinked_channel_videos
  channel = @client.my_channel
  return [] unless channel

  channel_id = channel.id
  existing_youtube_ids = Video.where.not(youtube_id: [nil, '']).pluck(:youtube_id).to_set
  unlinked = []
  page_token = nil

  loop do
    response = @client.list_channel_videos(channel_id: channel_id, page_token: page_token)
    break if response.items.blank?

    response.items.each do |item|
      yt_id = item.id.video_id
      next if existing_youtube_ids.include?(yt_id)

      unlinked << {
        youtube_id: yt_id,
        title: item.snippet.title,
        published_at: item.snippet.published_at
      }
    end

    page_token = response.next_page_token
    break if page_token.blank?
  end

  unlinked
end

#sync_existing_videosHash

Sync metadata for all Video records that have a youtube_id.
Fetches data from YouTube in batches of 50 (API max).

Returns:

  • (Hash)

    { synced: count, errors: count, skipped: count }



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'app/services/youtube/sync_service.rb', line 21

def sync_existing_videos
  videos = Video.where.not(youtube_id: [nil, ''])
  return { synced: 0, errors: 0, skipped: 0 } if videos.none?

  stats = { synced: 0, errors: 0, skipped: 0 }

  videos.pluck(:id, :youtube_id).each_slice(50) do |batch|
    youtube_ids = batch.map(&:last)
    video_id_map = batch.to_h { |id, yt_id| [yt_id, id] }

    begin
      yt_videos = @client.get_videos(youtube_ids, parts: 'snippet,contentDetails,status')

      yt_videos.each do |yt_video|
        local_id = video_id_map[yt_video.id]
        next unless local_id

        (local_id, yt_video)
        stats[:synced] += 1
      rescue StandardError => e
        logger.error("[YouTube::SyncService] Error syncing video #{local_id}: #{e.message}")
        stats[:errors] += 1
      end
    rescue YouTube::ApiClient::ApiError => e
      logger.error("[YouTube::SyncService] Batch API error: #{e.message}")
      stats[:errors] += batch.size
    end
  end

  stats
end

#sync_video(video_id) ⇒ Video?

Sync a single video by its local ID.

Parameters:

  • video_id (Integer)

    the local Video record ID

Returns:

  • (Video, nil)

    the updated video, or nil if not found on YouTube



56
57
58
59
60
61
62
63
64
65
# File 'app/services/youtube/sync_service.rb', line 56

def sync_video(video_id)
  video = Video.find(video_id)
  return nil if video.youtube_id.blank?

  yt_video = @client.get_video(video.youtube_id)
  return nil unless yt_video

  (video.id, yt_video)
  video.reload
end