diff --git a/lib/yt.rb b/lib/yt.rb index 5502ee23..1c2cae93 100644 --- a/lib/yt.rb +++ b/lib/yt.rb @@ -5,6 +5,7 @@ require 'yt/models/claim' require 'yt/models/claim_history' require 'yt/models/content_owner' +require 'yt/models/live_broadcast' require 'yt/models/match_policy' require 'yt/models/playlist' require 'yt/models/playlist_item' diff --git a/lib/yt/actions/base.rb b/lib/yt/actions/base.rb index 10d4d9a9..9edde4c6 100644 --- a/lib/yt/actions/base.rb +++ b/lib/yt/actions/base.rb @@ -22,9 +22,11 @@ def underscore(value) # @see https://support.google.com/youtube/answer/57404?hl=en def sanitize_brackets!(source) case source - when String then source.gsub('<', '‹').gsub('>', '›') - when Array then source.map{|string| sanitize_brackets! string} - when Hash then source.each{|k,v| source[k] = sanitize_brackets! v} + when String then source.gsub('<', '‹').gsub('>', '›') + when Array then source.map{|string| sanitize_brackets! string} + when Hash then source.each{|k,v| source[k] = sanitize_brackets! v} + # Don't sanitize other types of objects + else source end end end diff --git a/lib/yt/collections/live_broadcasts.rb b/lib/yt/collections/live_broadcasts.rb new file mode 100644 index 00000000..1c2ee07f --- /dev/null +++ b/lib/yt/collections/live_broadcasts.rb @@ -0,0 +1,45 @@ +require 'yt/collections/base' +require 'yt/models/video' + +module Yt + module Collections + # Provides methods to interact with a collection of YouTube live broadcasts. + # + # Resources with live broadcasts are: {Yt::Models::Channel channels} and + # {Yt::Models::Account accounts}. + class LiveBroadcasts < Resources + + private + + # @return [Hash] the parameters to submit to YouTube to list live broadcasts. + # @see https://developers.google.com/youtube/v3/docs/liveBroadcasts/list + def list_params + super.tap{|params| params[:params] = live_broadcasts_params} + end + + def live_broadcasts_params + if @where_params.blank? + {broadcastType: "all", mine: true} + else + apply_where_params! on_behalf_of_content_owner: @parent.owner_name + end + end + + def attributes_for_new_item(data) + {}.tap do |attributes| + attributes[:id] = data['id'] + attributes[:snippet] = data['snippet'] + attributes[:status] = data['status'] + attributes[:content_details] = data['contentDetails'] + end + end + + def insert_parts + snippet = {keys: [:title, :description, :scheduled_start_time, :scheduled_end_time, :default_language], sanitize_brackets: true} + status = {keys: [:privacy_status, :self_declared_made_for_kids]} + {snippet: snippet, status: status} + end + end + + end +end diff --git a/lib/yt/models/account.rb b/lib/yt/models/account.rb index 6149ef26..2f7c26d9 100644 --- a/lib/yt/models/account.rb +++ b/lib/yt/models/account.rb @@ -126,6 +126,27 @@ def create_playlist(params = {}) playlists.insert params end + # Creates a live broadcast in the account’s channel. + # @return [Yt::Models::LiveBroadcast] the newly created broadcast. + # @param [Hash] params the attributes of the broadcast. + # @option params [String] :title The new broadcast’s title. + # Cannot have more than 100 characters. Can include the characters + # < and >, which are replaced to ‹ › in order to be accepted by YouTube. + # @option params [String] :description The new broadcast’s description. + # Cannot have more than 5000 bytes. Can include the characters + # < and >, which are replaced to ‹ › in order to be accepted by YouTube. + # @option params [Time] :scheduledStartTime The broadcast’s scheduled start time. + # Must be in the future and is mandatory. + # @option params [Time] :scheduledEndTime The broadcast’s scheduled end time. + # Must be in the future. + # @option params [String] :privacy_status The new broadcast’s privacy + # status. Must be one of: private, unlisted, public. + # @example Create a broadcast titled "My favorites". + # account.create_live_broadcast title: 'My favorites' + def create_live_broadcast(params = {}) + live_broadcasts.insert params + end + # @!method delete_playlists(attributes = {}) # Deletes the account’s playlists matching all the given attributes. # @return [Array] whether each playlist matching the given @@ -174,6 +195,10 @@ def create_playlist(params = {}) # account is subscribed to. delegate :subscribed_channels, to: :channel + # @!attribute [r] live_broadcasts + # @return [Yt::Collections::LiveBroadcasts] the live broadcasts owned by the account. + delegate :live_broadcasts, to: :channel + # @!attribute [r] videos # @return [Yt::Collections::Videos] the videos owned by the account. has_many :videos diff --git a/lib/yt/models/channel.rb b/lib/yt/models/channel.rb index 3c853c8c..16ed39ae 100644 --- a/lib/yt/models/channel.rb +++ b/lib/yt/models/channel.rb @@ -134,6 +134,10 @@ def unsubscribe! # @return [Yt::Collections::ChannelSections] the channel’s channel sections. has_many :channel_sections + # @!attribute [r] live_broadcasts + # @return [Yt::Collections::LiveBroadcasts] the channel’s live broadcasts. + has_many :live_broadcasts + ### ANALYTICS ### # @macro reports diff --git a/lib/yt/models/live_broadcast.rb b/lib/yt/models/live_broadcast.rb new file mode 100644 index 00000000..7bab0352 --- /dev/null +++ b/lib/yt/models/live_broadcast.rb @@ -0,0 +1,84 @@ +require 'yt/models/base' + +module Yt + module Models + # Provides methods to interact with YouTube Content ID live broadcasts. + # @see https://developers.google.com/youtube/v3/live/docs/liveBroadcasts + class LiveBroadcast < Resource + + ### SNIPPET ### + + # @!attribute [r] title + # @return [String] the broadcast’s title. + delegate :title, to: :snippet + + # @!attribute [r] description + # @return [String] the broadcast’s description. + delegate :description, to: :snippet + + # @!attribute [r] scheduledStartTime + # @return [Time] the broadcast’s scheduled start time. + delegate :scheduled_start_time, to: :snippet + + # @!attribute [r] scheduledEndTime + # @return [Time] the broadcast’s scheduled end time. + delegate :scheduled_end_time, to: :snippet + + # @!attribute [r] published_at + # @return [Time] the date and time that the broadcast was published. + delegate :published_at, to: :snippet + + # @return [String] the ID that YouTube assigns and uses to uniquely + # identify the live_broadcast. + has_attribute :id + + ### PRIVATE API ### + + # @see https://developers.google.com/youtube/v3/docs/liveBroadcasts/update + def update_parts + snippet_keys = [:title, :description, :scheduled_start_time, :scheduled_end_time] + snippet = {keys: snippet_keys, sanitize_brackets: true} + status_keys = [:privacy_status, :recording_status, + :publish_at, :self_declared_made_for_kids] + {snippet: snippet, status: {keys: status_keys}} + end + + ### ACTIONS (UPLOAD, UPDATE, DELETE) ### + + # Deletes the live broadcast. + # @return [Boolean] whether the live broadcast does not exist anymore. + # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} is not an + # authenticated Yt::Account with permissions to delete the live broadcast. + def delete(options = {}) + do_delete {@id = nil} + !exists? + end + + # Updates the attributes of a broadcast. + # @return [Boolean] whether the broadcast was successfully updated. + # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} is not an + # authenticated Yt::Account with permissions to update the broadcast. + # @param [Hash] attributes the attributes to update. + # @option attributes [String] :title The new broadcast’s title. + # Cannot have more than 100 characters. Can include the characters + # < and >, which are replaced to ‹ › in order to be accepted by YouTube. + # @option attributes [String] :description The new broadcast’s description. + # Cannot have more than 5000 bytes. Can include the characters + # < and >, which are replaced to ‹ › in order to be accepted by YouTube. + # @option attributes [String] :privacy_status The new broadcast’s privacy + # status. Must be one of: private, unscheduled, public. + # @example Update title and description of a broadcast. + # broadcast.update title: 'New title', description: 'New description' + # @example Update status of a broadcast. + # broadcast.update privacy_status: 'public' + def update(attributes = {}) + super + end + + # @private + def exists? + !@id.nil? + end + end + end +end diff --git a/lib/yt/models/snippet.rb b/lib/yt/models/snippet.rb index 8dbef40f..35ce9ef2 100644 --- a/lib/yt/models/snippet.rb +++ b/lib/yt/models/snippet.rb @@ -10,6 +10,7 @@ module Models # @see https://developers.google.com/youtube/v3/docs/playlistItems#resource # @see https://developers.google.com/youtube/v3/docs/commentThreads#resource # @see https://developers.google.com/youtube/v3/docs/comments#resource + # @see https://developers.google.com/youtube/v3/live/docs/liveBroadcasts#resource class Snippet < Base attr_reader :data @@ -38,6 +39,8 @@ def initialize(options = {}) has_attribute :parent_id has_attribute :like_count, type: Integer has_attribute :updated_at, type: Time + has_attribute :scheduled_start_time, type: Time + has_attribute :scheduled_end_time, type: Time has_attribute :last_updated, type: Time has_attribute :language diff --git a/spec/collections/live_broadcasts_spec.rb b/spec/collections/live_broadcasts_spec.rb new file mode 100644 index 00000000..1991ec3b --- /dev/null +++ b/spec/collections/live_broadcasts_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'yt/collections/live_broadcasts' +require 'yt/models/live_broadcast' + +describe Yt::Collections::LiveBroadcasts do + subject(:collection) { Yt::Collections::LiveBroadcasts.new } + before { expect(collection).to behave } + + describe '#insert' do + let(:live_broadcast) { Yt::LiveBroadcast.new } + # TODO: separate stubs to show options translate into do_insert params + let(:behave) { receive(:do_insert).and_return live_broadcast } + + it { expect(collection.insert).to eq live_broadcast } + end +end diff --git a/spec/models/live_broadcast_spec.rb b/spec/models/live_broadcast_spec.rb new file mode 100644 index 00000000..3b6a6360 --- /dev/null +++ b/spec/models/live_broadcast_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' +require 'yt/models/live_broadcast' + +describe Yt::LiveBroadcast do + subject(:broadcast) { Yt::LiveBroadcast.new attrs } + + + describe '#title' do + context 'given a snippet with a title' do + let(:attrs) { {snippet: {"title"=>"Fullscreen"}} } + it { expect(broadcast.title).to eq 'Fullscreen' } + end + + context 'given a snippet without a title' do + let(:attrs) { {snippet: {}} } + it { expect(broadcast.title).to eq '' } + end + end + + describe '#published_at' do + context 'given a snippet with a timestamp' do # always returned by YouTube + let(:attrs) { {snippet: {"publishedAt"=>"2014-04-22T19:14:49.000Z"}} } + it { expect(broadcast.published_at.year).to be 2014 } + end + end + + describe '#description' do + context 'given a snippet with a description' do + let(:attrs) { {snippet: {"description"=>"The first media company for the connected generation."}} } + it { expect(broadcast.description).to eq 'The first media company for the connected generation.' } + end + + context 'given a snippet without a description' do + let(:attrs) { {snippet: {}} } + it { expect(broadcast.description).to eq '' } + end + end + + describe '#exists?' do + context 'given a broadcast with an id' do + let(:attrs) { {id: 'PLSWYkYzOr'} } + it { expect(broadcast).to exist } + end + + context 'given a broadcast without an id' do + let(:attrs) { {} } + it { expect(broadcast).not_to exist } + end + end + + describe '#snippet' do + context 'given fetching a broadcast returns a snippet' do + let(:attrs) { {snippet: {"title"=>"Fullscreen"}} } + it { expect(broadcast.snippet).to be_a Yt::Snippet } + end + end + + describe '#status' do + context 'given fetching a broadcast returns a status' do + let(:attrs) { {status: {"privacyStatus"=>"public"}} } + it { expect(broadcast.status).to be_a Yt::Status } + end + end + + describe '#update' do + let(:attrs) { {id: 'PLSWYkYzOr', snippet: {'title'=>'old'}, status: {"privacyStatus"=>"public"}} } + before { expect(broadcast).to receive(:do_update).and_yield 'snippet'=>{'title'=>'new'} } + + it { expect(broadcast.update title: 'new').to be true } + it { expect{broadcast.update title: 'new'}.to change{broadcast.title} } + end + + describe '#delete' do + let(:attrs) { {id: 'PLSWYkYzOr'} } + + context 'given an existing broadcast' do + before { expect(broadcast).to receive(:do_delete).and_yield } + + it { expect(broadcast.delete).to be true } + it { expect{broadcast.delete}.to change{broadcast.exists?} } + end + end +end