huginn_youtube_agent 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +7 -0
- data/lib/huginn_youtube_agent/youtube_agent.rb +302 -0
- data/lib/huginn_youtube_agent.rb +4 -0
- data/spec/youtube_agent_spec.rb +13 -0
- metadata +90 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: a0f675afde0011fe1a39f68ed76f4fd7c6055b9f25292cfa834abe4d34f90e4a
         | 
| 4 | 
            +
              data.tar.gz: 9513c020a01e58e315b5758f0514dd815b952b38f621f737f7663fe5caf06c7d
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: e53c8b92f864496bd477fd4ddcdea1b25bfc44c4f2ce3188d99b76d3b4a115a14660271e5eaf9f0223ea5f16117d647dfd8aeb60645c17d1a3f76f0028741bbb
         | 
| 7 | 
            +
              data.tar.gz: afed2e6c9d90dcab885f78b2c41eeeb8ea28b2b03625fd76be49ca88dc6c32afc8c0bba1b4cb49f78ac0b735f431dc2a39189e7b42d0e579c43a47f17b3165bb
         | 
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            Copyright (c) 2023 Nicolas Germain
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
| @@ -0,0 +1,302 @@ | |
| 1 | 
            +
            module Agents
         | 
| 2 | 
            +
              class YoutubeAgent < Agent
         | 
| 3 | 
            +
                include FormConfigurable
         | 
| 4 | 
            +
                can_dry_run!
         | 
| 5 | 
            +
                no_bulk_receive!
         | 
| 6 | 
            +
                default_schedule 'every_1h'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                description do
         | 
| 9 | 
            +
                  <<-MD
         | 
| 10 | 
            +
                  The Youtube Agent interacts with Youtube API.
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  `debug` is used for verbose mode.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  `youtube_api_key` is needed for authentication.
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  `channel_id` is the id of the channel.
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  `playlist_id` is the id of the playlist.
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  `limit` is the max number of results.
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  `type` is for the wanted action like check_videos.
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  `expected_receive_period_in_days` is used to determine if the Agent is working. Set it to the maximum number of days
         | 
| 25 | 
            +
                  that you anticipate passing without this Agent receiving an incoming Event.
         | 
| 26 | 
            +
                  MD
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                event_description <<-MD
         | 
| 30 | 
            +
                  Events look like this:
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      {
         | 
| 33 | 
            +
                        "kind": "youtube#searchResult",
         | 
| 34 | 
            +
                        "etag": "Q0TmoqzRcRGCScTDXGKO1XkTQAQ",
         | 
| 35 | 
            +
                        "id": {
         | 
| 36 | 
            +
                          "kind": "youtube#video",
         | 
| 37 | 
            +
                          "videoId": "h46EvTIB1D0"
         | 
| 38 | 
            +
                        },
         | 
| 39 | 
            +
                        "snippet": {
         | 
| 40 | 
            +
                          "publishedAt": "2023-07-24T16:40:18Z",
         | 
| 41 | 
            +
                          "channelId": "UC1WMae32v_eJ8qOtLQqM26Q",
         | 
| 42 | 
            +
                          "title": "Edge Computing & 5G to Supercharge Blockchains",
         | 
| 43 | 
            +
                          "description": "Multilingual Subtitles Available! In this episode of CallistoTalks, we explore the future of blockchain systems, supercharged by ...",
         | 
| 44 | 
            +
                          "thumbnails": {
         | 
| 45 | 
            +
                            "default": {
         | 
| 46 | 
            +
                              "url": "https://i.ytimg.com/vi/h46EvTIB1D0/default.jpg",
         | 
| 47 | 
            +
                              "width": 120,
         | 
| 48 | 
            +
                              "height": 90
         | 
| 49 | 
            +
                            },
         | 
| 50 | 
            +
                            "medium": {
         | 
| 51 | 
            +
                              "url": "https://i.ytimg.com/vi/h46EvTIB1D0/mqdefault.jpg",
         | 
| 52 | 
            +
                              "width": 320,
         | 
| 53 | 
            +
                              "height": 180
         | 
| 54 | 
            +
                            },
         | 
| 55 | 
            +
                            "high": {
         | 
| 56 | 
            +
                              "url": "https://i.ytimg.com/vi/h46EvTIB1D0/hqdefault.jpg",
         | 
| 57 | 
            +
                              "width": 480,
         | 
| 58 | 
            +
                              "height": 360
         | 
| 59 | 
            +
                            }
         | 
| 60 | 
            +
                          },
         | 
| 61 | 
            +
                          "channelTitle": "Callisto Network",
         | 
| 62 | 
            +
                          "liveBroadcastContent": "none",
         | 
| 63 | 
            +
                          "publishTime": "2023-07-24T16:40:18Z"
         | 
| 64 | 
            +
                        }
         | 
| 65 | 
            +
                      }
         | 
| 66 | 
            +
                MD
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def default_options
         | 
| 69 | 
            +
                  {
         | 
| 70 | 
            +
                    'channel_id' => '',
         | 
| 71 | 
            +
                    'playlist_id' => '',
         | 
| 72 | 
            +
                    'youtube_api_key' => '',
         | 
| 73 | 
            +
                    'limit' => '10',
         | 
| 74 | 
            +
                    'type' => 'check_channel',
         | 
| 75 | 
            +
                    'debug' => 'false',
         | 
| 76 | 
            +
                    'expected_receive_period_in_days' => '2',
         | 
| 77 | 
            +
                  }
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                form_configurable :youtube_api_key, type: :string
         | 
| 81 | 
            +
                form_configurable :channel_id, type: :string
         | 
| 82 | 
            +
                form_configurable :playlist_id, type: :string
         | 
| 83 | 
            +
                form_configurable :limit, type: :number
         | 
| 84 | 
            +
                form_configurable :debug, type: :boolean
         | 
| 85 | 
            +
                form_configurable :expected_receive_period_in_days, type: :string
         | 
| 86 | 
            +
                form_configurable :type, type: :array, values: ['check_channel', 'check_playlist', 'check_videos']
         | 
| 87 | 
            +
                def validate_options
         | 
| 88 | 
            +
                  errors.add(:base, "type has invalid value: should be 'check_channel', 'check_playlist', 'check_videos'") if interpolated['type'].present? && !%w(check_channel check_playlist check_videos).include?(interpolated['type'])
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  unless options['playlist_id'].present? || !['check_playlist'].include?(options['type'])
         | 
| 91 | 
            +
                    errors.add(:base, "playlist_id is a required field")
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  unless options['channel_id'].present? || !['check_channel' 'check_videos'].include?(options['type'])
         | 
| 95 | 
            +
                    errors.add(:base, "channel_id is a required field")
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  unless options['youtube_api_key'].present? || !['check_channel' 'check_playlist' 'check_videos'].include?(options['type'])
         | 
| 99 | 
            +
                    errors.add(:base, "youtube_api_key is a required field")
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  unless options['limit'].present? || !['check_playlist'].include?(options['type'])
         | 
| 103 | 
            +
                    errors.add(:base, "limit is a required field")
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  if options.has_key?('debug') && boolify(options['debug']).nil?
         | 
| 107 | 
            +
                    errors.add(:base, "if provided, debug must be true or false")
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
         | 
| 111 | 
            +
                    errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def working?
         | 
| 116 | 
            +
                  event_created_within?(options['expected_receive_period_in_days']) && !recent_error_logs?
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def receive(incoming_events)
         | 
| 120 | 
            +
                  incoming_events.each do |event|
         | 
| 121 | 
            +
                    interpolate_with(event) do
         | 
| 122 | 
            +
                      log event
         | 
| 123 | 
            +
                      trigger_action
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def check
         | 
| 129 | 
            +
                  trigger_action
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                private
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def log_curl_output(code,body)
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                  log "request status : #{code}"
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  if interpolated['debug'] == 'true'
         | 
| 139 | 
            +
                    log "body"
         | 
| 140 | 
            +
                    log body
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def check_channel()
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                  uri = URI.parse("https://youtube.googleapis.com/youtube/v3/channels?part=snippet%2CcontentDetails%2Cstatistics&id=#{interpolated['channel_id']}&key=#{interpolated['youtube_api_key']}")
         | 
| 148 | 
            +
                  request = Net::HTTP::Get.new(uri)
         | 
| 149 | 
            +
                  request["Accept"] = "application/json"
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  req_options = {
         | 
| 152 | 
            +
                    use_ssl: uri.scheme == "https",
         | 
| 153 | 
            +
                  }
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
         | 
| 156 | 
            +
                    http.request(request)
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  log_curl_output(response.code,response.body)
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                  payload = JSON.parse(response.body)
         | 
| 162 | 
            +
                  if !memory['last_status'] || (memory['last_status'].present? && payload != memory['last_status'])
         | 
| 163 | 
            +
                    create_event payload: payload
         | 
| 164 | 
            +
                    memory['last_status'] = payload
         | 
| 165 | 
            +
                  else
         | 
| 166 | 
            +
                    if interpolated['debug'] == 'true'
         | 
| 167 | 
            +
                      log "nothing to compare"
         | 
| 168 | 
            +
                    end
         | 
| 169 | 
            +
                  end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                def check_playlist()
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  uri = URI.parse("https://www.googleapis.com/youtube/v3/playlistItems?key=#{interpolated['youtube_api_key']}&playlistId=#{interpolated['playlist_id']}&part=snippet,contentDetails,status&maxResults=#{interpolated['limit']}")
         | 
| 176 | 
            +
                  request = Net::HTTP::Get.new(uri)
         | 
| 177 | 
            +
                  request["Accept"] = "application/json"
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  req_options = {
         | 
| 180 | 
            +
                    use_ssl: uri.scheme == "https",
         | 
| 181 | 
            +
                  }
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
         | 
| 184 | 
            +
                    http.request(request)
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  log_curl_output(response.code,response.body)
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  payload = JSON.parse(response.body)
         | 
| 190 | 
            +
                  if !memory['last_status']
         | 
| 191 | 
            +
                    payload['items'].each do |playlist|
         | 
| 192 | 
            +
                      create_event payload: playlist
         | 
| 193 | 
            +
                    end
         | 
| 194 | 
            +
                    memory['last_status'] = payload
         | 
| 195 | 
            +
                  else
         | 
| 196 | 
            +
                    if payload != memory['last_status']
         | 
| 197 | 
            +
                      if memory['last_status'] == ''
         | 
| 198 | 
            +
                      else
         | 
| 199 | 
            +
                        last_status = memory['last_status']
         | 
| 200 | 
            +
                        payload['items'].each do |playlist|
         | 
| 201 | 
            +
                          found = false
         | 
| 202 | 
            +
                          if interpolated['debug'] == 'true'
         | 
| 203 | 
            +
                            log "playlist"
         | 
| 204 | 
            +
                            log playlist
         | 
| 205 | 
            +
                          end
         | 
| 206 | 
            +
                          last_status['items'].each do |playlistbis|
         | 
| 207 | 
            +
                            if playlist == playlistbis
         | 
| 208 | 
            +
                              found = true
         | 
| 209 | 
            +
                            end
         | 
| 210 | 
            +
                            if interpolated['debug'] == 'true'
         | 
| 211 | 
            +
                              log "playlistbis"
         | 
| 212 | 
            +
                              log playlistbis
         | 
| 213 | 
            +
                              log "found is #{found}!"
         | 
| 214 | 
            +
                            end
         | 
| 215 | 
            +
                          end
         | 
| 216 | 
            +
                          if found == false
         | 
| 217 | 
            +
                            create_event payload: playlist
         | 
| 218 | 
            +
                          end
         | 
| 219 | 
            +
                        end
         | 
| 220 | 
            +
                      end
         | 
| 221 | 
            +
                      memory['last_status'] = payload
         | 
| 222 | 
            +
                    else
         | 
| 223 | 
            +
                      if interpolated['debug'] == 'true'
         | 
| 224 | 
            +
                        log "nothing to compare"
         | 
| 225 | 
            +
                      end
         | 
| 226 | 
            +
                    end
         | 
| 227 | 
            +
                  end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                end
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                def check_videos()
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  uri = URI.parse("https://www.googleapis.com/youtube/v3/search?key=#{interpolated['youtube_api_key']}&channelId=#{interpolated['channel_id']}&part=snippet,id&order=date&maxResults=#{interpolated['limit']}")
         | 
| 234 | 
            +
                  request = Net::HTTP::Get.new(uri)
         | 
| 235 | 
            +
                  request["Accept"] = "application/json"
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                  req_options = {
         | 
| 238 | 
            +
                    use_ssl: uri.scheme == "https",
         | 
| 239 | 
            +
                  }
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
         | 
| 242 | 
            +
                    http.request(request)
         | 
| 243 | 
            +
                  end
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                  log_curl_output(response.code,response.body)
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                  payload = JSON.parse(response.body)
         | 
| 248 | 
            +
                  if !memory['last_status']
         | 
| 249 | 
            +
                    payload['items'].each do |video|
         | 
| 250 | 
            +
                      create_event payload: video
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
                    memory['last_status'] = payload
         | 
| 253 | 
            +
                  else
         | 
| 254 | 
            +
                    if payload != memory['last_status']
         | 
| 255 | 
            +
                      if memory['last_status'] == ''
         | 
| 256 | 
            +
                      else
         | 
| 257 | 
            +
                        last_status = memory['last_status']
         | 
| 258 | 
            +
                        payload['items'].each do |video|
         | 
| 259 | 
            +
                          found = false
         | 
| 260 | 
            +
                          if interpolated['debug'] == 'true'
         | 
| 261 | 
            +
                            log "video"
         | 
| 262 | 
            +
                            log video
         | 
| 263 | 
            +
                          end
         | 
| 264 | 
            +
                          last_status['items'].each do |videobis|
         | 
| 265 | 
            +
                            if video == videobis
         | 
| 266 | 
            +
                              found = true
         | 
| 267 | 
            +
                            end
         | 
| 268 | 
            +
                            if interpolated['debug'] == 'true'
         | 
| 269 | 
            +
                              log "videobis"
         | 
| 270 | 
            +
                              log videobis
         | 
| 271 | 
            +
                              log "found is #{found}!"
         | 
| 272 | 
            +
                            end
         | 
| 273 | 
            +
                          end
         | 
| 274 | 
            +
                          if found == false
         | 
| 275 | 
            +
                            create_event payload: video
         | 
| 276 | 
            +
                          end
         | 
| 277 | 
            +
                        end
         | 
| 278 | 
            +
                      end
         | 
| 279 | 
            +
                      memory['last_status'] = payload
         | 
| 280 | 
            +
                    else
         | 
| 281 | 
            +
                      if interpolated['debug'] == 'true'
         | 
| 282 | 
            +
                        log "nothing to compare"
         | 
| 283 | 
            +
                      end
         | 
| 284 | 
            +
                    end
         | 
| 285 | 
            +
                  end
         | 
| 286 | 
            +
                end
         | 
| 287 | 
            +
             | 
| 288 | 
            +
                def trigger_action
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                  case interpolated['type']
         | 
| 291 | 
            +
                  when "check_channel"
         | 
| 292 | 
            +
                    check_channel()
         | 
| 293 | 
            +
                  when "check_playlist"
         | 
| 294 | 
            +
                    check_playlist()
         | 
| 295 | 
            +
                  when "check_videos"
         | 
| 296 | 
            +
                    check_videos()
         | 
| 297 | 
            +
                  else
         | 
| 298 | 
            +
                    log "Error: type has an invalid value (#{interpolated['type']})"
         | 
| 299 | 
            +
                  end
         | 
| 300 | 
            +
                end
         | 
| 301 | 
            +
              end
         | 
| 302 | 
            +
            end
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            require 'rails_helper'
         | 
| 2 | 
            +
            require 'huginn_agent/spec_helper'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe Agents::YoutubeAgent do
         | 
| 5 | 
            +
              before(:each) do
         | 
| 6 | 
            +
                @valid_options = Agents::YoutubeAgent.new.default_options
         | 
| 7 | 
            +
                @checker = Agents::YoutubeAgent.new(:name => "YoutubeAgent", :options => @valid_options)
         | 
| 8 | 
            +
                @checker.user = users(:bob)
         | 
| 9 | 
            +
                @checker.save!
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              pending "add specs here"
         | 
| 13 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,90 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: huginn_youtube_agent
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: '0.1'
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Nicolas Germain
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2023-09-17 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: bundler
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ">="
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 2.1.0
         | 
| 20 | 
            +
              type: :development
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ">="
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 2.1.0
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: rake
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 12.3.3
         | 
| 34 | 
            +
              type: :development
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: 12.3.3
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: huginn_agent
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            description: Write a longer description or delete this line.
         | 
| 56 | 
            +
            email:
         | 
| 57 | 
            +
            - ngermain@hihouhou.com
         | 
| 58 | 
            +
            executables: []
         | 
| 59 | 
            +
            extensions: []
         | 
| 60 | 
            +
            extra_rdoc_files: []
         | 
| 61 | 
            +
            files:
         | 
| 62 | 
            +
            - LICENSE.txt
         | 
| 63 | 
            +
            - lib/huginn_youtube_agent.rb
         | 
| 64 | 
            +
            - lib/huginn_youtube_agent/youtube_agent.rb
         | 
| 65 | 
            +
            - spec/youtube_agent_spec.rb
         | 
| 66 | 
            +
            homepage: https://github.com/hihouhou/huginn_youtube_agent
         | 
| 67 | 
            +
            licenses:
         | 
| 68 | 
            +
            - MIT
         | 
| 69 | 
            +
            metadata: {}
         | 
| 70 | 
            +
            post_install_message: 
         | 
| 71 | 
            +
            rdoc_options: []
         | 
| 72 | 
            +
            require_paths:
         | 
| 73 | 
            +
            - lib
         | 
| 74 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 75 | 
            +
              requirements:
         | 
| 76 | 
            +
              - - ">="
         | 
| 77 | 
            +
                - !ruby/object:Gem::Version
         | 
| 78 | 
            +
                  version: '0'
         | 
| 79 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 80 | 
            +
              requirements:
         | 
| 81 | 
            +
              - - ">="
         | 
| 82 | 
            +
                - !ruby/object:Gem::Version
         | 
| 83 | 
            +
                  version: '0'
         | 
| 84 | 
            +
            requirements: []
         | 
| 85 | 
            +
            rubygems_version: 3.3.3
         | 
| 86 | 
            +
            signing_key: 
         | 
| 87 | 
            +
            specification_version: 4
         | 
| 88 | 
            +
            summary: Write a short summary, because Rubygems requires one.
         | 
| 89 | 
            +
            test_files:
         | 
| 90 | 
            +
            - spec/youtube_agent_spec.rb
         |