Skip to content

Commit

Permalink
Merge branch 'master' into retrieve-asset-metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
jcohenho committed Jun 24, 2019
2 parents 55320a0 + 65489d6 commit 78b6de4
Show file tree
Hide file tree
Showing 24 changed files with 389 additions and 76 deletions.
5 changes: 4 additions & 1 deletion .rspec
@@ -1,3 +1,6 @@
--format documentation
--color
--fail-fast
--tag ~rate_limited
--tag ~flaky
--tag ~extended_permissions
--exclude_pattern spec/requests/as_content_owner/*_spec.rb
6 changes: 2 additions & 4 deletions .travis.yml
Expand Up @@ -3,7 +3,5 @@ notifications:
email: true
matrix:
include:
- rvm: 1.9.3
gemfile: gemfiles/Gemfile.activesupport-3.x
- rvm: 2.0.0
gemfile: gemfiles/Gemfile.activesupport-4.x
- rvm: 2.6.3
gemfile: gemfiles/Gemfile.activesupport-4.x
8 changes: 7 additions & 1 deletion CHANGELOG.md
Expand Up @@ -6,8 +6,14 @@ For more information about changelogs, check
[Keep a Changelog](http://keepachangelog.com) and
[Vandamme](http://tech-angels.github.io/vandamme).

## unreleased
## Unreleased

* [FEATURE] Add `ownership_effective` method to access asset ownership ("effective") via the asset object.
* [FEATURE] List content owners of others with `content_owner.content_owners`

## 0.32.3 - 2019-03-15

* [ENHANCEMENT] Add `Yt::URL` to get id, kind, and its resource (channel, video, playlist)
* [BUGFIX] Fix `subscription.insert` by adding a parameter
* [FEATURE] Add `file_name` attribute to `Yt::FileDetail` model

Expand Down
54 changes: 26 additions & 28 deletions README.md
Expand Up @@ -266,6 +266,11 @@ asset = content_owner.assets.where(id: 'A969176766549462', fetch_metadata: 'effe
asset.metadata_effective.title #=> "Neu la anh" (different due to ownership conflicts)
```

```ruby
asset = content_owner.assets.where(id: 'A125058570526569', fetch_ownership: 'effective').first
asset.ownership_effective.general_owners.first.owner # => "XOuN81q-MeEUVrsiZeK1lQ"
```

* to search for an asset

```ruby
Expand Down Expand Up @@ -492,23 +497,6 @@ end
so use the approach that you prefer.
If a variable is set in both places, then `Yt.configure` takes precedence.

Why you should use Yt…
======================

… and not [youtube_it](https://github.com/kylejginavan/youtube_it)?
Because youtube_it does not support YouTube API V3, and the YouTube API V2 has
been [officially deprecated as of March 4, 2014](https://developers.google.com/youtube/2.0/developers_guide_protocol_audience).
If you need help upgrading your code, check [YOUTUBE_IT.md](https://github.com/Fullscreen/yt/blob/master/YOUTUBE_IT.md),
a step-by-step comparison between youtube_it and Yt to make upgrade easier.

… and not [Google Api Client](https://github.com/google/google-api-ruby-client)?
Because Google Api Client is poorly coded, poorly documented and adds many
dependencies, bloating the size of your project.

… and not your own code? Because Yt is fully tested, well documented,
has few dependencies and helps you forget about the burden of dealing with
Google API!

How to test
===========

Expand All @@ -517,15 +505,6 @@ Yt comes with two different sets of tests:
1. tests in `spec/models`, `spec/collections` and `spec/errors` **do not hit** the YouTube API
1. tests in `spec/requests` **hit** the YouTube API and require authentication

The reason why some tests actually hit the YouTube API is because they are
meant to really integrate Yt with YouTube. YouTube API is not exactly
*the most reliable* API out there, so we need to make sure that the responses
match the documentation.

You don’t have to run all the tests every time you change code.
Travis CI is already set up to do this for when whenever you push a branch
or create a pull request for this project.

To only run tests against models, collections and errors (which do not hit the API), type:

```bash
Expand All @@ -539,8 +518,26 @@ rspec
```

This will fail unless you have set up a test YouTube application and some
tests YouTube accounts to hit the API. Once again, you probably don’t need
this, since Travis CI already takes care of running this kind of tests.
tests YouTube accounts (with appropriate fixture data) to hit the API.
Furthermore, tests that require authentication are divided into three
roles, which correspond to each directory in `spec/requests`:

* Account-based tests, which require a valid refresh token along with
the application-level credentials the refresh token was created with
(`YT_TEST_DEVICE_REFRESH_TOKEN`, `YT_TEST_DEVICE_CLIENT_ID`, and
`YT_TEST_DEVICE_CLIENT_SECRET` respectively).
* Server application tests, which use a server API key
(`YT_TEST_SERVER_API_KEY).
* Tests that excercise YouTube's partner functionality. This requires an
a partner channel id (`YT_TEST_CONTENT_OWNER_NAME`), a refresh token
that's authenticated with that channel
(`YT_TEST_CONTENT_OWNER_REFRESH_TOKEN`), and the corresponding
application (`YT_TEST_PARTNER_CLIENT_ID` and
(`YT_TEST_PARTNER_CLIENT_SECRET`).

The refresh tokens need to be generated with the `youtube`,
`yt-analytics` and `userinfo.profile` permissions in order for tests to
pass.

How to release new versions
===========================
Expand Down Expand Up @@ -568,3 +565,4 @@ the [YouTube Analytics API](https://developers.google.com/youtube/analytics).
If you find that a method is missing, fork the project, add the missing code,
write the appropriate tests, then submit a pull request, and it will gladly
be merged!

1 change: 1 addition & 0 deletions lib/yt.rb
Expand Up @@ -13,6 +13,7 @@
require 'yt/models/comment_thread'
require 'yt/models/ownership'
require 'yt/models/advertising_options_set'
require 'yt/models/url'

# An object-oriented Ruby client for YouTube.
# Helps creating applications that need to interact with YouTube objects.
Expand Down
8 changes: 6 additions & 2 deletions lib/yt/collections/content_owners.rb
Expand Up @@ -25,8 +25,12 @@ def list_params
end

def content_owners_params
{fetch_mine: true}
if @where_params.blank?
{fetch_mine: true}
else
apply_where_params! on_behalf_of_content_owner: @parent.owner_name
end
end
end
end
end
end
4 changes: 4 additions & 0 deletions lib/yt/models/asset.rb
Expand Up @@ -35,6 +35,10 @@ def metadata_effective
@metadata_effective ||= Yt::Models::AssetMetadata.new data: @data.fetch('metadataEffective', {})
end

def ownership_effective
@ownership_effective ||= Yt::Models::Ownership.new data: @data.fetch('ownershipEffective', {})
end

# Soft-deletes the asset.
# @note YouTube API does not provide a +delete+ method for the Asset
# resource, but only an +update+ method. Updating the +status+ of a
Expand Down
99 changes: 99 additions & 0 deletions lib/yt/models/url.rb
@@ -0,0 +1,99 @@
require 'yt/models/video'
require 'yt/models/playlist'
require 'yt/models/channel'

module Yt
module Models
# Provides methods to identify YouTube resources from names or URLs.
# @see https://developers.google.com/youtube/v3/docs
# @example Identify a YouTube video from its short URL:
# url = Yt::URL.new 'youtu.be/kawaiiguy'
# url.id # => 'UC4lU5YG9QDgs0X2jdnt7cdQ'
# url.resource # => #<Yt::Channel @id=UC4lU5YG9QDgs0X2jdnt7cdQ>
class URL
# @param [String] text the name or URL of a YouTube resource (in any form).
def initialize(text)
@text = text.to_s.strip
@match = find_pattern_match
end

# @return [Symbol] the kind of YouTube resource matching the URL.
# Possible values are: +:playlist+, +:video+, +:channel+, and +:unknown:.
def kind
@match[:kind]
end

# @return [<String, nil>] the ID of the YouTube resource matching the URL.
def id
@match['id'] ||= fetch_id
end

# @return [<Yt::Channel>] the resource associated with the URL
def resource(options = {})
@resource ||= case kind
when :channel then Yt::Channel
when :video then Yt::Video
when :playlist then Yt::Playlist
else raise Yt::Errors::NoItems
end.new options.merge(id: id)
end

# @return [Array<Regexp>] patterns matching URLs of YouTube playlists.
PLAYLIST_PATTERNS = [
%r{^(?:https?://)?(?:www\.)?youtube\.com/playlist/?\?list=(?<id>[a-zA-Z0-9_-]+)},
]

# @return [Array<Regexp>] patterns matching URLs of YouTube videos.
VIDEO_PATTERNS = [
%r{^(?:https?://)?(?:www\.)?youtube\.com/watch\?v=(?<id>[a-zA-Z0-9_-]{11})},
%r{^(?:https?://)?(?:www\.)?youtu\.be/(?<id>[a-zA-Z0-9_-]{11})},
%r{^(?:https?://)?(?:www\.)?youtube\.com/embed/(?<id>[a-zA-Z0-9_-]{11})},
%r{^(?:https?://)?(?:www\.)?youtube\.com/v/(?<id>[a-zA-Z0-9_-]{11})},
]

# @return [Array<Regexp>] patterns matching URLs of YouTube channels.
CHANNEL_PATTERNS = [
%r{^(?:https?://)?(?:www\.)?youtube\.com/channel/(?<id>UC[a-zA-Z0-9_-]{22})},
%r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>c/|user/)?(?<name>[a-zA-Z0-9_-]+)}
]

private

def find_pattern_match
patterns.find(-> {{kind: :unknown}}) do |kind, regex|
if data = @text.match(regex)
# Note: With Ruby 2.4, the following is data.named_captures
break data.names.zip(data.captures).to_h.merge kind: kind
end
end
end

def patterns
# @note: :channel *must* be the last since one of its regex eats the
# remaining patterns. In short, don't change the following order.
Enumerator.new do |patterns|
VIDEO_PATTERNS.each {|regex| patterns << [:video, regex]}
PLAYLIST_PATTERNS.each {|regex| patterns << [:playlist, regex]}
CHANNEL_PATTERNS.each {|regex| patterns << [:channel, regex]}
end
end

def fetch_id
response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}")
end
if response.is_a?(Net::HTTPRedirection)
response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
http.request Net::HTTP::Get.new(response['location'])
end
end
regex = %r{<meta itemprop="channelId" content="(?<id>UC[a-zA-Z0-9_-]{22})">}
if data = response.body.match(regex)
data[:id]
else
raise Yt::Errors::NoItems
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/yt/version.rb
@@ -1,3 +1,3 @@
module Yt
VERSION = '0.32.2'
VERSION = '0.32.3'
end
14 changes: 13 additions & 1 deletion spec/models/asset_spec.rb
Expand Up @@ -17,4 +17,16 @@
it { expect(asset.type).to eq 'web' }
end
end
end

describe '#ownership_effective' do
context 'given fetching a asset returns an ownershipEffective' do
let(:data) {
{"ownershipEffective"=>{"kind"=>"youtubePartner#rightsOwnership",
"general"=>[{"ratio"=>100.0, "owner"=>"XOuN81q-MeEUVrsiZeK1lQ", "type"=>"exclude"}]}}
}
it { expect(asset.ownership_effective).to be_a Yt::Ownership }
it { expect(asset.ownership_effective.general_owners.first).to be_a Yt::RightOwner }
it { expect(asset.ownership_effective.general_owners.first.owner).to eq "XOuN81q-MeEUVrsiZeK1lQ" }
end
end
end
78 changes: 78 additions & 0 deletions spec/models/url_spec.rb
@@ -0,0 +1,78 @@
require 'spec_helper'
require 'yt/models/url'

describe Yt::URL do
subject(:url) { Yt::URL.new text }

context 'given a YouTube playlist URL' do
let(:text) { "https://www.youtube.com/playlist?list=#{id}" }

describe 'works with existing playlists' do
let(:id) { 'LLxO1tY8h1AhOz0T4ENwmpow' }
it {expect(url.id).to eq id }
end

describe 'works with unknown playlists' do
let(:id) { 'PL12--not-a-playlist' }
it {expect(url.id).to eq id }
end
end

context 'given a YouTube video URL' do
let(:text) { "https://www.youtube.com/watch?v=#{id}" }

describe 'works with existing videos' do
let(:id) { 'gknzFj_0vvY' }
it {expect(url.id).to eq id }
end

describe 'works with unknown videos' do
let(:id) { 'abc123abc12' }
it {expect(url.id).to eq id }
end
end

context 'given a YouTube channel URL in the ID form' do
let(:text) { "https://www.youtube.com/channel/#{id}" }

describe 'works with existing channels' do
let(:id) { 'UC4lU5YG9QDgs0X2jdnt7cdQ' }
it {expect(url.id).to eq id }
end

describe 'works with unknown channels' do
let(:id) { 'UC-not-an-actual-channel' }
it {expect(url.id).to eq id }
end
end

context 'given an existing YouTube channel' do
let(:text) { 'youtube.com/channel/UCxO1tY8h1AhOz0T4ENwmpow' }
it {expect(url.kind).to eq :channel }
end

context 'given an existing YouTube video' do
let(:text) { 'youtube.com/watch?v=gknzFj_0vvY' }
it {expect(url.kind).to eq :video }
end

context 'given an existing YouTube playlist' do
let(:text) { 'youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow' }
it {expect(url.kind).to eq :playlist }
end

context 'given an unknown YouTube channel URL' do
let(:text) { 'youtube.com/channel/UC-too-short-to-be-an-id' }
it {expect(url.kind).to eq :channel }
end

context 'given an unknown YouTube video URL' do
let(:text) { 'youtu.be/not-an-id' }
it {expect(url.kind).to eq :unknown }
end

context 'given an unknown text' do
let(:text) { 'not-really-anything---' }
it {expect(url.kind).to eq :unknown }
end
end
7 changes: 1 addition & 6 deletions spec/requests/as_account/account_spec.rb
Expand Up @@ -3,7 +3,7 @@
require 'yt/models/account'

describe Yt::Account, :device_app do
describe 'can create playlists' do
describe 'can create playlists', rate_limited: true do
let(:params) { {title: 'Test Yt playlist', privacy_status: 'unlisted'} }
before { @playlist = $account.create_playlist params }
it { expect(@playlist).to be_a Yt::Playlist }
Expand All @@ -26,11 +26,6 @@
uploads = related_playlists.select{|p| p.title.starts_with? 'Uploads'}
expect(uploads).not_to be_empty
end

specify 'includes private playlists (such as History)' do
history = related_playlists.select{|p| p.title == 'History'}
expect(history).not_to be_empty
end
end

describe '.videos' do
Expand Down
2 changes: 1 addition & 1 deletion spec/requests/as_account/channel_spec.rb
Expand Up @@ -180,7 +180,7 @@
expect(channel.subscriptions.size).to be
end

describe 'playlists can be deleted' do
describe 'playlists can be deleted', rate_limited: true do
let(:title) { "Yt Test Delete All Playlists #{rand}" }
before { $account.create_playlist params }

Expand Down

0 comments on commit 78b6de4

Please sign in to comment.