yt 0.7.5 → 0.7.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d346ffef89d5b5f77243722480b4182d31d23a7f
4
- data.tar.gz: 1148ab3e8561f75edba5cf62dad118d0e4fe255d
3
+ metadata.gz: 21e026d255684b3c18d8291bd4eeda02ff233f0c
4
+ data.tar.gz: 9af923985d7022b16b6d31b8483af39f617015bf
5
5
  SHA512:
6
- metadata.gz: 7ab946dc91a6d0089042d33ada2939b0a8c6a2a8221360e3f9cd122c197cdf40835715d3b284b869c344de7af8260eaa305a83b76fddcff8fd29372dee876536
7
- data.tar.gz: 1c9cbb659eddc7993dbd3f5c9e8206d2bed99639bfb189bf013565d7b9884773711e45f379f2ac491e3e92e4282a0b2b8b05d1692a6c8d4315d1997e433fcabb
6
+ metadata.gz: 0e8b1696e652a865442b0b604882293a9057fe193092167793da12e7a273e615605072b3eaffd3e661f2c65471c0ad6a958bbc21c61653e4aee3f05c4ebe904d
7
+ data.tar.gz: 90b38724c6349dfd14100bb3f4bf12bf2187d704ebd3c069acd5f6bfa6dc1e2772acaf8c1306483cde36e3a2fa1beaecde069e6f0917b44af503b7cc8c2dd2e4
@@ -22,4 +22,5 @@ env:
22
22
  - secure: Ejj8tsuwyrRVmCc/R9ubKWCHWhCGpe0Dy6fc1UuPCkcMZyXq9ZC02v2obWsTQQ7epEgsCYZAO4v/gWpuv1b1huGcWdfJzMW7RCoY87cEf9HnAK0lSwGx4+/pYkEMe8y5p149C3vAR8nqczvEavN1fUq/WwPUqp+JyDP7kwFTs2Y=
23
23
  - secure: gE5kAT1R54hmS+W3YYGcUtlD8ZskvTctVR3sr+C5CUjVPdq6Ktx5Q/a6EJyAVVrhxpaCOuk3LG+VkzdQIVFUNRiDPcOulkond4HkSQDoy+IJ/wTXvUS+lIJ1ERUnWega+APrQUjH5s2WayPGZUBqWt/u8Tt9EmSUZfuKZSEXqZk=
24
24
  - secure: ZUx5v/wHW/TENg8NfFINiiMoe2D031ntDTiuIBdf88c/bMClkEtRRgomtK9RBkFonEyGEOkXxUm2SLzRf340V3eIXWQhil7ab1lcYs8X59aVS/NK/GqChH8Nia17gc3OTQ9k6rYvj4Lp60Dh9WG1cijLPd4/OvPmf6qX9uYfJMw=
25
- - secure: DumQVO01Y3Ki1skuOYOZzosDb6jS0XyG1O8Agy3mVxXGJzQE+s1z2UFz4gMpsU9o/gmiNMddp7I6+RtbZjo9hN3H7vlRRwEeB7tuUMiDyomSx1FlHcCFfPdTmhxGg8X78SErMWqNC6eReGrCTgBdIq1ho7dIu53qJNxTEFqx7eI=
25
+ - secure: DumQVO01Y3Ki1skuOYOZzosDb6jS0XyG1O8Agy3mVxXGJzQE+s1z2UFz4gMpsU9o/gmiNMddp7I6+RtbZjo9hN3H7vlRRwEeB7tuUMiDyomSx1FlHcCFfPdTmhxGg8X78SErMWqNC6eReGrCTgBdIq1ho7dIu53qJNxTEFqx7eI=
26
+ - secure: UCceSRqPe5Y8WCM85qj0i8wlB5gJRSegJZfVj5jnwyermcOc1ZucwDZwTzkYVbLfDGTqTqzqW6M322a1tWm74xeIbxuHG7fw3Waoxk4zh3tg1CmNkzeDZFnl6V10uOMT6pn5E3kfhLzqOzftJN6fTMCgYMdkwJcfdrMX+z1cxwM=
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- yt (0.7.5)
4
+ yt (0.7.6)
5
5
  activesupport
6
6
 
7
7
  GEM
data/HISTORY.md CHANGED
@@ -13,6 +13,7 @@ v0.7 - 2014/06/18
13
13
  * Extract Reports (earnings, views) into module with macro `has_report`
14
14
  * New channel reports: comments, likes, dislikes, shares and impressions
15
15
  * Allow both normal and partnered channels to retrieve reports about views, comments, likes, dislikes, shares
16
+ * Make reports available also on Video (not just Channel)
16
17
 
17
18
  v0.6 - 2014/06/05
18
19
  -----------------
data/README.md CHANGED
@@ -119,6 +119,13 @@ channel = Yt::Channel.new id: 'UCxO1tY8h1AhOz0T4ENwmpow', auth: account
119
119
 
120
120
  channel.subscribed? #=> false
121
121
  channel.subscribe #=> true
122
+ ```
123
+
124
+ *The methods above require to be authenticated as a YouTube account (see below).*
125
+
126
+ ```ruby
127
+ account = Yt::Account.new access_token: 'ya29.1.ABCDEFGHIJ'
128
+ channel = Yt::Channel.new id: 'UCxO1tY8h1AhOz0T4ENwmpow', auth: account
122
129
 
123
130
  channel.create_playlist title: 'New playlist' #=> true
124
131
  channel.delete_playlists title: 'New playlist' #=> [true]
@@ -130,7 +137,7 @@ channel.dislikes to: 2.days.ago #=> {Tue, 27 May 2014 => 0.0, Wed, 28 May 2014 =
130
137
  channel.shares since: 7.days.ago, until: 7.days.ago #=> {Wed, 28 May 2014 => 3.0}
131
138
  ```
132
139
 
133
- *The methods above require to be authenticated as a YouTube account (see below).*
140
+ *The methods above require to be authenticated as the channel’s account (see below).*
134
141
 
135
142
  ```ruby
136
143
  content_owner = Yt::ContentOwner.new owner_name: 'CMSname', access_token: 'ya29.1.ABCDEFGHIJ'
@@ -156,6 +163,7 @@ Use [Yt::Video](http://rubydoc.info/github/Fullscreen/yt/master/Yt/Models/Video)
156
163
  * update the attributes of a video
157
164
  * access the annotations of a video
158
165
  * like and dislike a video
166
+ * retrieve the daily earnings, views, comments, likes, dislikes, shares and impressions of a channel
159
167
 
160
168
  ```ruby
161
169
  video = Yt::Video.new id: 'MESycYJytkU'
@@ -195,11 +203,35 @@ video.like #=> true
195
203
  *The methods above require to be authenticated as a YouTube account (see below).*
196
204
 
197
205
  ```ruby
206
+ account = Yt::Account.new access_token: 'ya29.1.ABCDEFGHIJ'
207
+ video = Yt::Video.new id: 'MESycYJytkU', auth: account
208
+
198
209
  video.update title: 'A title', description: 'A description', tags: ['a tag'], categoryId: '21'
210
+
211
+ video.views since: 7.days.ago #=> {Wed, 28 May 2014 => 12.0, Thu, 29 May 2014 => 3.0, …}
212
+ video.comments until: 2.days.ago #=> {Wed, 28 May 2014 => 9.0, Thu, 29 May 2014 => 4.0, …}
213
+ video.likes from: 8.days.ago #=> {Tue, 27 May 2014 => 7.0, Wed, 28 May 2014 => 0.0, …}
214
+ video.dislikes to: 2.days.ago #=> {Tue, 27 May 2014 => 0.0, Wed, 28 May 2014 => 1.0, …}
215
+ video.shares since: 7.days.ago, until: 7.days.ago #=> {Wed, 28 May 2014 => 3.0}
199
216
  ```
200
217
 
201
218
  *The methods above require to be authenticated as the video’s owner (see below).*
202
219
 
220
+ ```ruby
221
+ content_owner = Yt::ContentOwner.new owner_name: 'CMSname', access_token: 'ya29.1.ABCDEFGHIJ'
222
+ video = Yt::Video.new id: 'MESycYJytkU', auth: content_owner
223
+
224
+ video.earnings_on 5.days.ago #=> 12.23
225
+ video.views since: 7.days.ago #=> {Wed, 28 May 2014 => 12.0, Thu, 29 May 2014 => 3.0, …}
226
+ video.comments until: 2.days.ago #=> {Wed, 28 May 2014 => 9.0, Thu, 29 May 2014 => 4.0, …}
227
+ video.likes from: 8.days.ago #=> {Tue, 27 May 2014 => 7.0, Wed, 28 May 2014 => 0.0, …}
228
+ video.dislikes to: 2.days.ago #=> {Tue, 27 May 2014 => 0.0, Wed, 28 May 2014 => 1.0, …}
229
+ video.shares since: 7.days.ago, until: 7.days.ago #=> {Wed, 28 May 2014 => 3.0}
230
+ video.impressions_on 5.days.ago #=> 157.0
231
+ ```
232
+
233
+ *The methods above require to be authenticated as the video’s content owner (see below).*
234
+
203
235
  Yt::Playlist
204
236
  ------------
205
237
 
@@ -439,7 +471,7 @@ To install on your system, run
439
471
 
440
472
  To use inside a bundled Ruby project, add this line to the Gemfile:
441
473
 
442
- gem 'yt', '~> 0.7.5'
474
+ gem 'yt', '~> 0.7.6'
443
475
 
444
476
  Since the gem follows [Semantic Versioning](http://semver.org),
445
477
  indicating the full version in your Gemfile (~> *major*.*minor*.*patch*)
@@ -1,10 +1,14 @@
1
1
  require 'yt/models/resource'
2
+ require 'yt/modules/reports'
2
3
 
3
4
  module Yt
4
5
  module Models
5
6
  # Provides methods to interact with YouTube videos.
6
7
  # @see https://developers.google.com/youtube/v3/docs/videos
7
8
  class Video < Resource
9
+ # Includes the +:has_report+ method to access YouTube Analytics reports.
10
+ extend Modules::Reports
11
+
8
12
  delegate :tags, :channel_id, :channel_title, :category_id,
9
13
  :live_broadcast_content, to: :snippet
10
14
 
@@ -22,6 +26,27 @@ module Yt
22
26
  # @return [Yt::Collections::Annotations] the video’s annotations.
23
27
  has_many :annotations
24
28
 
29
+ # @macro has_report
30
+ has_report :earnings
31
+
32
+ # @macro has_report
33
+ has_report :views
34
+
35
+ # @macro has_report
36
+ has_report :comments
37
+
38
+ # @macro has_report
39
+ has_report :likes
40
+
41
+ # @macro has_report
42
+ has_report :dislikes
43
+
44
+ # @macro has_report
45
+ has_report :shares
46
+
47
+ # @macro has_report
48
+ has_report :impressions
49
+
25
50
  # @!attribute [r] statistics_set
26
51
  # @return [Yt::Models::StatisticsSet] the statistics for the video.
27
52
  has_one :statistics_set
@@ -94,6 +119,21 @@ module Yt
94
119
  !liked?
95
120
  end
96
121
 
122
+ # @private
123
+ # Tells `has_reports` to retrieve the reports from YouTube Analytics API
124
+ # either as a Channel or as a Content Owner.
125
+ # @see https://developers.google.com/youtube/analytics/v1/reports
126
+ def reports_params
127
+ {}.tap do |params|
128
+ if auth.owner_name
129
+ params['ids'] = "contentOwner==#{auth.owner_name}"
130
+ else
131
+ params['ids'] = "channel==#{channel_id}"
132
+ end
133
+ params['filters'] = "video==#{id}"
134
+ end
135
+ end
136
+
97
137
  private
98
138
 
99
139
  # @return [Hash] the parameters to submit to YouTube to update a video.
@@ -1,3 +1,3 @@
1
1
  module Yt
2
- VERSION = '0.7.5'
2
+ VERSION = '0.7.6'
3
3
  end
@@ -73,7 +73,7 @@ describe Yt::Channel, :device_app do
73
73
 
74
74
  it 'returns valid reports for channel-related metrics' do
75
75
  # Some reports are only available to Content Owners.
76
- # See content ownere test for more details about what the methods return.
76
+ # See content owner test for more details about what the methods return.
77
77
  expect{channel.views}.not_to raise_error
78
78
  expect{channel.comments}.not_to raise_error
79
79
  expect{channel.likes}.not_to raise_error
@@ -58,7 +58,7 @@ describe Yt::Video, :device_app do
58
58
  it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
59
59
  end
60
60
 
61
- context 'given one of my own videos that I want to update' do
61
+ context 'given one of my own videos' do
62
62
  let(:id) { $account.videos.first.id }
63
63
 
64
64
  describe 'updates the attributes that I specify explicitly' do
@@ -78,5 +78,26 @@ describe Yt::Video, :device_app do
78
78
  it { expect{video.update attrs}.not_to change{video.category_id} }
79
79
  it { expect{video.update attrs}.not_to change{video.privacy_status} }
80
80
  end
81
+
82
+
83
+ it 'returns valid reports for channel-related metrics' do
84
+ # Some reports are only available to Content Owners.
85
+ # See content ownere test for more details about what the methods return.
86
+ expect{video.views}.not_to raise_error
87
+ expect{video.comments}.not_to raise_error
88
+ expect{video.likes}.not_to raise_error
89
+ expect{video.dislikes}.not_to raise_error
90
+ expect{video.shares}.not_to raise_error
91
+ expect{video.earnings}.to raise_error Yt::Errors::Unauthorized
92
+ expect{video.impressions}.to raise_error Yt::Errors::Unauthorized
93
+
94
+ expect{video.views_on 3.days.ago}.not_to raise_error
95
+ expect{video.comments_on 3.days.ago}.not_to raise_error
96
+ expect{video.likes_on 3.days.ago}.not_to raise_error
97
+ expect{video.dislikes_on 3.days.ago}.not_to raise_error
98
+ expect{video.shares_on 3.days.ago}.not_to raise_error
99
+ expect{video.earnings_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
100
+ expect{video.impressions_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
101
+ end
81
102
  end
82
103
  end
@@ -0,0 +1,237 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+ require 'yt/models/video'
4
+
5
+ describe Yt::Video, :partner do
6
+ subject(:video) { Yt::Video.new id: id, auth: $content_owner }
7
+
8
+ context 'given a video of a partnered channel', :partner do
9
+ context 'managed by the authenticated Content Owner' do
10
+ let(:id) { ENV['YT_TEST_VIDEO_CHANNEL_ID'] }
11
+
12
+ describe 'earnings can be retrieved for a specific day' do
13
+ context 'in which the video made any money' do
14
+ let(:earnings) {video.earnings_on 5.days.ago}
15
+ it { expect(earnings).to be_a Float }
16
+ end
17
+
18
+ context 'in the future' do
19
+ let(:earnings) { video.earnings_on 5.days.from_now}
20
+ it { expect(earnings).to be_nil }
21
+ end
22
+ end
23
+
24
+ describe 'earnings can be retrieved for a range of days' do
25
+ let(:date) { 4.days.ago }
26
+
27
+ specify 'with a given start (:since option)' do
28
+ expect(video.earnings(since: date).keys.min).to eq date.to_date
29
+ end
30
+
31
+ specify 'with a given end (:until option)' do
32
+ expect(video.earnings(until: date).keys.max).to eq date.to_date
33
+ end
34
+
35
+ specify 'with a given start (:from option)' do
36
+ expect(video.earnings(from: date).keys.min).to eq date.to_date
37
+ end
38
+
39
+ specify 'with a given end (:to option)' do
40
+ expect(video.earnings(to: date).keys.max).to eq date.to_date
41
+ end
42
+ end
43
+
44
+ describe 'views can be retrieved for a specific day' do
45
+ context 'in which the video was partnered' do
46
+ let(:views) { video.views_on 5.days.ago}
47
+ it { expect(views).to be_a Float }
48
+ end
49
+
50
+ context 'in which the video was not partnered' do
51
+ let(:views) { video.views_on 20.years.ago}
52
+ it { expect(views).to be_nil }
53
+ end
54
+ end
55
+
56
+ describe 'views can be retrieved for a range of days' do
57
+ let(:date) { 4.days.ago }
58
+
59
+ specify 'with a given start (:since option)' do
60
+ expect(video.views(since: date).keys.min).to eq date.to_date
61
+ end
62
+
63
+ specify 'with a given end (:until option)' do
64
+ expect(video.views(until: date).keys.max).to eq date.to_date
65
+ end
66
+
67
+ specify 'with a given start (:from option)' do
68
+ expect(video.views(from: date).keys.min).to eq date.to_date
69
+ end
70
+
71
+ specify 'with a given end (:to option)' do
72
+ expect(video.views(to: date).keys.max).to eq date.to_date
73
+ end
74
+ end
75
+
76
+ describe 'comments can be retrieved for a specific day' do
77
+ context 'in which the video was partnered' do
78
+ let(:comments) { video.comments_on 5.days.ago}
79
+ it { expect(comments).to be_a Float }
80
+ end
81
+
82
+ context 'in which the video was not partnered' do
83
+ let(:comments) { video.comments_on 20.years.ago}
84
+ it { expect(comments).to be_nil }
85
+ end
86
+ end
87
+
88
+ describe 'comments can be retrieved for a range of days' do
89
+ let(:date) { 4.days.ago }
90
+
91
+ specify 'with a given start (:since option)' do
92
+ expect(video.comments(since: date).keys.min).to eq date.to_date
93
+ end
94
+
95
+ specify 'with a given end (:until option)' do
96
+ expect(video.comments(until: date).keys.max).to eq date.to_date
97
+ end
98
+
99
+ specify 'with a given start (:from option)' do
100
+ expect(video.comments(from: date).keys.min).to eq date.to_date
101
+ end
102
+
103
+ specify 'with a given end (:to option)' do
104
+ expect(video.comments(to: date).keys.max).to eq date.to_date
105
+ end
106
+ end
107
+
108
+ describe 'likes can be retrieved for a specific day' do
109
+ context 'in which the video was partnered' do
110
+ let(:likes) { video.likes_on 5.days.ago}
111
+ it { expect(likes).to be_a Float }
112
+ end
113
+
114
+ context 'in which the video was not partnered' do
115
+ let(:likes) { video.likes_on 20.years.ago}
116
+ it { expect(likes).to be_nil }
117
+ end
118
+ end
119
+
120
+ describe 'likes can be retrieved for a range of days' do
121
+ let(:date) { 4.days.ago }
122
+
123
+ specify 'with a given start (:since option)' do
124
+ expect(video.likes(since: date).keys.min).to eq date.to_date
125
+ end
126
+
127
+ specify 'with a given end (:until option)' do
128
+ expect(video.likes(until: date).keys.max).to eq date.to_date
129
+ end
130
+
131
+ specify 'with a given start (:from option)' do
132
+ expect(video.likes(from: date).keys.min).to eq date.to_date
133
+ end
134
+
135
+ specify 'with a given end (:to option)' do
136
+ expect(video.likes(to: date).keys.max).to eq date.to_date
137
+ end
138
+ end
139
+
140
+ describe 'dislikes can be retrieved for a specific day' do
141
+ context 'in which the video was partnered' do
142
+ let(:dislikes) { video.dislikes_on 5.days.ago}
143
+ it { expect(dislikes).to be_a Float }
144
+ end
145
+
146
+ context 'in which the video was not partnered' do
147
+ let(:dislikes) { video.dislikes_on 20.years.ago}
148
+ it { expect(dislikes).to be_nil }
149
+ end
150
+ end
151
+
152
+ describe 'dislikes can be retrieved for a range of days' do
153
+ let(:date) { 4.days.ago }
154
+
155
+ specify 'with a given start (:since option)' do
156
+ expect(video.dislikes(since: date).keys.min).to eq date.to_date
157
+ end
158
+
159
+ specify 'with a given end (:until option)' do
160
+ expect(video.dislikes(until: date).keys.max).to eq date.to_date
161
+ end
162
+
163
+ specify 'with a given start (:from option)' do
164
+ expect(video.dislikes(from: date).keys.min).to eq date.to_date
165
+ end
166
+
167
+ specify 'with a given end (:to option)' do
168
+ expect(video.dislikes(to: date).keys.max).to eq date.to_date
169
+ end
170
+ end
171
+
172
+ describe 'shares can be retrieved for a specific day' do
173
+ context 'in which the video was partnered' do
174
+ let(:shares) { video.shares_on 5.days.ago}
175
+ it { expect(shares).to be_a Float }
176
+ end
177
+
178
+ context 'in which the video was not partnered' do
179
+ let(:shares) { video.shares_on 20.years.ago}
180
+ it { expect(shares).to be_nil }
181
+ end
182
+ end
183
+
184
+ describe 'shares can be retrieved for a range of days' do
185
+ let(:date) { 4.days.ago }
186
+
187
+ specify 'with a given start (:since option)' do
188
+ expect(video.shares(since: date).keys.min).to eq date.to_date
189
+ end
190
+
191
+ specify 'with a given end (:until option)' do
192
+ expect(video.shares(until: date).keys.max).to eq date.to_date
193
+ end
194
+
195
+ specify 'with a given start (:from option)' do
196
+ expect(video.shares(from: date).keys.min).to eq date.to_date
197
+ end
198
+
199
+ specify 'with a given end (:to option)' do
200
+ expect(video.shares(to: date).keys.max).to eq date.to_date
201
+ end
202
+ end
203
+
204
+ describe 'impressions can be retrieved for a specific day' do
205
+ context 'in which the video was partnered' do
206
+ let(:impressions) { video.impressions_on 5.days.ago}
207
+ it { expect(impressions).to be_a Float }
208
+ end
209
+
210
+ context 'in which the video was not partnered' do
211
+ let(:impressions) { video.impressions_on 20.years.ago}
212
+ it { expect(impressions).to be_nil }
213
+ end
214
+ end
215
+
216
+ describe 'impressions can be retrieved for a range of days' do
217
+ let(:date) { 4.days.ago }
218
+
219
+ specify 'with a given start (:since option)' do
220
+ expect(video.impressions(since: date).keys.min).to eq date.to_date
221
+ end
222
+
223
+ specify 'with a given end (:until option)' do
224
+ expect(video.impressions(until: date).keys.max).to eq date.to_date
225
+ end
226
+
227
+ specify 'with a given start (:from option)' do
228
+ expect(video.impressions(from: date).keys.min).to eq date.to_date
229
+ end
230
+
231
+ specify 'with a given end (:to option)' do
232
+ expect(video.impressions(to: date).keys.max).to eq date.to_date
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.5
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-26 00:00:00.000000000 Z
11
+ date: 2014-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -211,6 +211,7 @@ files:
211
211
  - spec/requests/as_account/video_spec.rb
212
212
  - spec/requests/as_content_owner/channel_spec.rb
213
213
  - spec/requests/as_content_owner/content_owner_spec.rb
214
+ - spec/requests/as_content_owner/video_spec.rb
214
215
  - spec/requests/as_server_app/channel_spec.rb
215
216
  - spec/requests/as_server_app/playlist_item_spec.rb
216
217
  - spec/requests/as_server_app/playlist_spec.rb
@@ -287,6 +288,7 @@ test_files:
287
288
  - spec/requests/as_account/video_spec.rb
288
289
  - spec/requests/as_content_owner/channel_spec.rb
289
290
  - spec/requests/as_content_owner/content_owner_spec.rb
291
+ - spec/requests/as_content_owner/video_spec.rb
290
292
  - spec/requests/as_server_app/channel_spec.rb
291
293
  - spec/requests/as_server_app/playlist_item_spec.rb
292
294
  - spec/requests/as_server_app/playlist_spec.rb