junkie 0.0.6 → 0.0.7

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.
data/bin/junkie CHANGED
@@ -5,6 +5,14 @@
5
5
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
6
6
 
7
7
  require 'junkie'
8
- include Junkie
8
+ require 'logging'
9
+
10
+ Logging.appenders.stdout(
11
+ 'stdout',
12
+ :level => :debug,
13
+ :layout => Logging.layouts.pattern(:pattern => '[%d] %-5l: %m\n')
14
+ )
15
+ Logging.logger.root.appenders = Logging.appenders.stdout
9
16
 
17
+ include Junkie
10
18
  start_reactor
data/lib/junkie/errors.rb CHANGED
@@ -7,6 +7,8 @@ module Junkie
7
7
  class HTTP404Error < Exception; end
8
8
  class HTTP500Error < Exception; end
9
9
 
10
+ class InvalidResponse < Exception; end
11
+
10
12
  class InvalidConfigError < Exception; end
11
13
 
12
14
  class InvalidStateError < Exception; end
@@ -30,8 +30,8 @@ module Junkie
30
30
  protocol: 'http',
31
31
  host: 'localhost',
32
32
  port: 8000,
33
- api_user: '',
34
- api_password: ''
33
+ api_user: 'user',
34
+ api_password: 'password'
35
35
  }
36
36
 
37
37
  def initialize()
@@ -7,6 +7,8 @@ module Junkie
7
7
  class Observer
8
8
  include Log, Helper, Config
9
9
 
10
+ attr_accessor :api
11
+
10
12
  DEFAULT_CONFIG = {
11
13
  watchdog_refresh: 10, # interval the watchdog_timer is fired
12
14
  }
@@ -25,7 +27,6 @@ module Junkie
25
27
  @ready_for_new_links = true
26
28
  @watchdog_enabled = false
27
29
  @skipped_timer_at_first_complete_detection = false
28
- @should_send_it_on_channel = false
29
30
 
30
31
  @channels[:episodes].subscribe do |episode|
31
32
  next unless episode.status == :found
@@ -45,24 +46,13 @@ module Junkie
45
46
  }
46
47
 
47
48
  EM.add_periodic_timer(@config[:watchdog_refresh]) do
48
-
49
- # temporary fix for the SystemStackError
50
- if @should_send_it_on_channel
51
- log.info("Sending complete episode out on the channel")
52
- @channels[:episodes].push(@active_episode)
53
- @active_episode = nil
54
-
55
- @should_send_it_on_channel = false
56
- end
57
-
58
49
  monitor_progress if @watchdog_enabled
59
50
 
60
- in_fiber {
61
- add_next_episode_to_pyload
62
- }
51
+ in_fiber { add_next_episode_to_pyload }
63
52
  end
64
53
  end
65
54
 
55
+ private
66
56
 
67
57
  # Add next episode to Pyload which downloads the episode and extracts it
68
58
  #
@@ -99,81 +89,132 @@ module Junkie
99
89
  @ready_for_new_links
100
90
  end
101
91
 
102
- private
103
92
 
104
- # This method is called from the watchdog timer periodically
93
+ # Utility method that fetches the QueueData, validates it and returns
94
+ # the JSON Object as a real Ruby Object
105
95
  #
106
- # It monitors the download process and reacts depending on the results
96
+ # @raise [Junkie::InvalidResponse] if the response is faulty
97
+ # @return [Array] the JSON response as an object
98
+ def get_queue_data
99
+ data = @api.call(:getQueueData)
100
+
101
+ if data && data.is_a?(Array)
102
+ return data
103
+ end
104
+ raise Junkie::InvalidResponse,
105
+ "'#{data}' is not a valid response from Pyload"
106
+ end
107
+
108
+
109
+ #########################################################################
110
+ # This method is called from the watchdog timer periodically #
111
+ # #
112
+ # It monitors the download process and reacts depending on the results #
107
113
  def monitor_progress
108
114
  log.debug("Watchdog timer has been fired")
109
115
  in_fiber {
110
116
  catch(:break) {
111
- queue_data = @api.call(:getQueueData)
117
+ queue_data = get_queue_data
112
118
 
113
- if queue_data.empty? and @active_episode.nil?
114
- log.info("Empty Pyload queue, I cancel the watchdog timer")
115
- @watchdog_enabled = false
116
- throw :break
117
- end
119
+ # check for empty queue and disable watchdog if needed
120
+ check_for_empty_queue(queue_data)
118
121
 
119
122
  # extract package IDs and map them to current downloads
120
123
  update_package_ids(queue_data)
121
124
 
122
- if has_queue_any_failed_links?(queue_data)
123
- log.info("There are failed links in the Queue, will fix this")
124
- @api.call(:restartFailed)
125
- throw :break
126
- end
127
-
128
- # look for complete downloads
129
- pids = get_complete_downloads(queue_data)
130
-
131
- if not pids.empty?
132
- if @skipped_timer_at_first_complete_detection
133
-
134
- # post process complete active download and send it out on the
135
- # channel
136
- if pids.include? @active_episode.pid
137
- log.info("'#{@active_episode}' is extracted completely")
138
- @active_episode.pid = nil
139
- @active_episode.status = :extracted
140
-
141
- @should_send_it_on_channel = true
142
- end
143
-
144
- # remove all complete packages
145
- @api.call(:deletePackages, {pids: pids})
146
- log.info("Complete packages are removed from Pyload Queue")
147
-
148
- @skipped_timer_at_first_complete_detection = false
149
- @ready_for_new_links = true
150
- else
151
- # If a package is complete, we are sometimes so fast to remove
152
- # it, before the extracting can be started, so we will skip it
153
- # the first time
154
- log.info("Complete package detected, skip the first time")
155
- @skipped_timer_at_first_complete_detection = true
156
- end
157
- end
125
+ check_for_failed_links(queue_data)
126
+
127
+ check_for_complete_packages(queue_data)
158
128
  }
159
129
  }
130
+ rescue Junkie::InvalidResponse => e
131
+ log.error("Pyload returns InvalidResponse (#{e}), I will ignore this")
160
132
  end
161
133
 
162
134
 
163
- # Searches for failed links in the pyload queue
135
+ # Checks if the Pyload Queue is empty and disables the watchdog if
136
+ # it is really real
164
137
  #
165
138
  # @param [Array] queue_data returned from :getQueueData api method
139
+ def check_for_empty_queue(queue_data)
140
+ if queue_data.empty?
141
+
142
+ unless @active_episode.nil?
143
+ log.info("Empty queue detected but a download is set as active")
144
+ return
145
+ end
146
+
147
+ log.info("Empty Pyload queue detected, will double check")
148
+
149
+ # check twice if the queue is definitely empty
150
+ check_data = get_queue_data
151
+ if check_data.empty?
152
+ log.info("Double check is successful, I will disable the watchdog")
153
+ @watchdog_enabled = false
154
+ throw :break
155
+ else
156
+ log.info("Double check failed, Queue is not empty")
157
+ end
158
+ end
159
+ end
160
+
161
+ # Searches for failed links in the pyload queue and reacts on this
162
+ # through issuing a restartFailed call
166
163
  #
167
- # @return [Boolean] true if there are any failed links, false otherwise
168
- def has_queue_any_failed_links?(queue_data)
164
+ # @param [Array] queue_data returned from :getQueueData api method
165
+ def check_for_failed_links(queue_data)
166
+ failed = 0
169
167
  queue_data.each do |package|
170
168
  package['links'].each do |link|
171
169
  status = Pyload::Api::FILE_STATUS[link['status']]
172
- return true if status == :failed
170
+ (failed += 1) if status == :failed
173
171
  end
174
172
  end
175
173
 
176
- false
174
+ if failed > 0
175
+ log.info("There are failed links in the Queue, will fix this")
176
+ @api.call(:restartFailed)
177
+ throw :break
178
+ end
179
+ end
180
+
181
+
182
+ # Looks for complete packages and does some post-processing
183
+ #
184
+ # @param [Array] queue_data returned from :getQueueData api method
185
+ def check_for_complete_packages(queue_data)
186
+ pids = get_complete_downloads(queue_data)
187
+
188
+ throw :break if pids.empty?
189
+
190
+ if @skipped_timer_at_first_complete_detection
191
+
192
+ # post process complete active download and send it out on the
193
+ # channel
194
+ if pids.include? @active_episode.pid
195
+ log.info("'#{@active_episode}' has been extracted completely")
196
+ @active_episode.pid = nil
197
+ @active_episode.status = :extracted
198
+
199
+ log.info("Sending complete episode out on the channel")
200
+ @channels[:episodes].push(@active_episode)
201
+ @active_episode = nil
202
+ end
203
+
204
+ # remove all complete packages
205
+ @api.call(:deletePackages, { pids: pids })
206
+ log.info("Complete packages are removed from Pyload Queue #{ pids }")
207
+
208
+ @skipped_timer_at_first_complete_detection = false
209
+ @ready_for_new_links = true
210
+
211
+ else
212
+ # If a package is complete, we are sometimes so fast to remove
213
+ # it, before the extracting can be started, so we will skip it
214
+ # the first time
215
+ log.info("Complete package detected, skip the first time")
216
+ @skipped_timer_at_first_complete_detection = true
217
+ end
177
218
  end
178
219
 
179
220
 
@@ -189,46 +230,45 @@ module Junkie
189
230
 
190
231
  next if package['linksdone'] == 0
191
232
 
192
- # When extracting is in progress the status of the first link is set
193
- # to :extracting
194
- extracting = package['links'].select do |link|
195
- Pyload::Api::FILE_STATUS[link['status']] == :extracting
233
+ invalid_links = package['links'].reject do |e|
234
+ [:done, :skipped].include?(
235
+ Junkie::Pyload::Api::FILE_STATUS[e['status']])
196
236
  end
197
- next unless extracting.empty?
237
+ next unless invalid_links.empty?
198
238
 
199
239
  sizetotal = package['sizetotal'].to_i
200
240
  sizedone = package['sizedone'].to_i
201
241
 
202
- if sizetotal > 0 && sizedone > 0
203
-
204
- if sizetotal == sizedone
242
+ if sizetotal > 0 && sizedone > 0 && sizetotal == sizedone
205
243
  pids << package['pid']
206
- end
207
244
  end
208
245
  end
209
246
 
210
247
  pids
211
248
  end
212
249
 
250
+
213
251
  # extract package IDs and map them to current downloads
214
252
  #
215
253
  # @param [Array] queue_data returned from :getQueueData api method
216
254
  def update_package_ids(queue_data)
217
- queue_data.each do |package|
218
- pid = package['pid']
219
- next if @active_episode.pid == pid
220
255
 
221
- if @active_episode.pid == pid-1
222
- log.info("Package ID has been changed, I will correct this")
256
+ if queue_data.size == 1
257
+ pid = queue_data[0]['pid']
258
+ if @active_episode && @active_episode.pid != pid
259
+ log.info("Package ID has been changed, New PID is #{pid}")
223
260
  @active_episode.pid = pid
224
- next
225
261
  end
226
262
 
263
+ elsif queue_data.size > 1
227
264
  raise InvalidStateError,
228
- "PackageID #{pid} can't be mapped to active Download"
265
+ "There is more than 1 Package in the Queue"
229
266
  end
230
267
  end
231
268
 
269
+
270
+ # Removes existing packages from the Pyload Queue and creates a valid
271
+ # state to begin communication
232
272
  def cleanup
233
273
  log.debug("Made a cleanup of Pyload's Queue")
234
274
  @api.call(:stopAllDownloads)
@@ -1,3 +1,3 @@
1
1
  module Junkie
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
data/lib/junkie.rb CHANGED
@@ -9,14 +9,6 @@ require 'junkie/patched/sjunkieex'
9
9
  require 'junkie/pyload/api'
10
10
  require 'junkie/pyload/observer'
11
11
  require 'junkie/notification/twitter'
12
- require 'logging'
13
-
14
- Logging.appenders.stdout(
15
- 'stdout',
16
- :level => :debug,
17
- :layout => Logging.layouts.pattern(:pattern => '[%d] %-5l: %m\n')
18
- )
19
- Logging.logger.root.appenders = Logging.appenders.stdout
20
12
 
21
13
  module Junkie
22
14
 
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+
3
+ module Junkie::Pyload::Observer::TestData
4
+ def self.get_fixture
5
+ json = <<-eos
6
+ [
7
+ {
8
+ "name": "onetreehill_sl_s09e04",
9
+ "links": [
10
+ {
11
+ "status": 12,
12
+ "format_size": "102.00 MiB",
13
+ "name": "onetreehill_sl_s09e04.part1.rar",
14
+ "plugin": "UploadedTo",
15
+ "url": "http:\/\/uploaded.net\/file\/kigscay8",
16
+ "packageID": 11,
17
+ "fid": 827,
18
+ "error": "",
19
+ "statusmsg": "downloade",
20
+ "order": 0,
21
+ "size": 106954752
22
+ },
23
+ {
24
+ "status": 12,
25
+ "format_size": "102.00 MiB",
26
+ "name": "onetreehill_sl_s09e04.part2.rar",
27
+ "plugin": "UploadedTo",
28
+ "url": "http:\/\/uploaded.net\/file\/pm6m7v3d",
29
+ "packageID": 11,
30
+ "fid": 828,
31
+ "error": "",
32
+ "statusmsg": "downloade",
33
+ "order": 1,
34
+ "size": 106954752
35
+ },
36
+ {
37
+ "status": 12,
38
+ "format_size": "102.00 MiB",
39
+ "name": "onetreehill_sl_s09e04.part3.rar",
40
+ "plugin": "UploadedTo",
41
+ "url": "http:\/\/uploaded.net\/file\/3tzo52uh",
42
+ "packageID": 11,
43
+ "fid": 829,
44
+ "error": "",
45
+ "statusmsg": "downloade",
46
+ "order": 2,
47
+ "size": 106954752
48
+ },
49
+ {
50
+ "status": 2,
51
+ "format_size": "102.00 MiB",
52
+ "name": "onetreehill_sl_s09e04.part4.rar",
53
+ "plugin": "UploadedTo",
54
+ "url": "http:\/\/uploaded.net\/file\/0e367cl3\/onetreehill_sl_s09e04.part4.rar",
55
+ "packageID": 11,
56
+ "fid": 830,
57
+ "error": "",
58
+ "statusmsg": "online",
59
+ "order": 3,
60
+ "size": 106954752
61
+ },
62
+ {
63
+ "status": 2,
64
+ "format_size": "78.96 MiB",
65
+ "name": "onetreehill_sl_s09e04.part5.rar",
66
+ "plugin": "UploadedTo",
67
+ "url": "http:\/\/uploaded.net\/file\/9iuhwfjk\/onetreehill_sl_s09e04.part5.rar",
68
+ "packageID": 11,
69
+ "fid": 831,
70
+ "error": "",
71
+ "statusmsg": "online",
72
+ "order": 4,
73
+ "size": 82795518
74
+ }
75
+ ],
76
+ "dest": 1,
77
+ "pid": 11,
78
+ "site": "",
79
+ "linksdone": 0,
80
+ "fids": null,
81
+ "sizetotal": 510614526,
82
+ "sizedone": 0,
83
+ "linkstotal": null,
84
+ "folder": "onetreehill_sl_s09e04",
85
+ "password": "",
86
+ "order": 0
87
+ }
88
+ ]
89
+ eos
90
+
91
+ JSON.load(json)
92
+ end
93
+
94
+ end
@@ -0,0 +1,216 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/fixtures/pyload_queue_data')
3
+ require 'yaml'
4
+
5
+ describe Junkie::Pyload::Observer do
6
+ include EMHelper
7
+
8
+ # remove the Config module behaviour
9
+ before(:each) do
10
+ Junkie::Config.stub!(:get_config) do |clas|
11
+ clas.class::DEFAULT_CONFIG
12
+ end
13
+
14
+ stub_const("Junkie::CONFIG_FILE", '/dev/null')
15
+ end
16
+
17
+ let(:channels) { {episodes: EM::Channel.new} }
18
+ let(:epi_channel) { channels[:episodes]}
19
+ let(:episode) do
20
+ epi = Junkie::Episode.new( "One Tree Hill", "http://local.host/12345",
21
+ "One.Tree.Hill.S09E01.Testepisode")
22
+ epi.status = :found
23
+ epi
24
+ end
25
+
26
+ context "monitor the pyload progress" do
27
+
28
+ let(:observer) do
29
+ ob = Junkie::Pyload::Observer.new(channels)
30
+ ob.api.stub(:call).and_return(nil)
31
+ ob.instance_variable_set(:@found_episodes, [episode])
32
+ ob.instance_variable_set(:@ready_for_new_links, true)
33
+ ob.send(:add_next_episode_to_pyload)
34
+
35
+ episode.pid = 10
36
+ ob
37
+ end
38
+
39
+ context "#get_queue_data" do
40
+
41
+ it "should throw InvalidResponse on empty string" do
42
+ observer.api.should_receive(:call).and_return("")
43
+ expect {
44
+ observer.send(:get_queue_data)
45
+ }.to raise_error(Junkie::InvalidResponse)
46
+ end
47
+
48
+ it "should throw InvalidResponse on empty response" do
49
+ observer.api.should_receive(:call).and_return(nil)
50
+ expect {
51
+ observer.send(:get_queue_data)
52
+ }.to raise_error(Junkie::InvalidResponse)
53
+ end
54
+
55
+ it "should return the valid response" do
56
+ response = [ {pid: 13} ]
57
+ observer.api.should_receive(:call).and_return(response)
58
+ observer.send(:get_queue_data).should eq response
59
+ end
60
+ end
61
+
62
+ context "#monitor_progress" do
63
+
64
+ let(:response) do
65
+ Junkie::Pyload::Observer::TestData::get_fixture
66
+ end
67
+
68
+ it "should catch InvalidResponse raised from #get_queue_data" do
69
+ observer.api.should_receive(:call).and_return("")
70
+
71
+ expect {
72
+ observer.send(:monitor_progress)
73
+ }.to_not raise_error(Junkie::InvalidResponse)
74
+ end
75
+
76
+ context "#check_for_empty_queue" do
77
+
78
+ it "should be an active watchdog timer" do
79
+ observer.instance_variable_get(:@watchdog_enabled).should be true
80
+ end
81
+
82
+ it "should disable the watchdog if there isn't an active download" do
83
+ observer.instance_variable_set(:@active_episode, nil)
84
+ # it double checks for emptiness
85
+ observer.should_receive(:get_queue_data).and_return([])
86
+
87
+ expect {
88
+ observer.send(:check_for_empty_queue, [])
89
+ }.to throw_symbol(:break)
90
+
91
+ observer.instance_variable_get(:@watchdog_enabled).should be false
92
+ end
93
+
94
+ it "shouldn't disable the watchdog if double check fails" do
95
+ observer.instance_variable_set(:@active_episode, nil)
96
+ observer.should_receive(:get_queue_data).and_return(response)
97
+
98
+ observer.send(:check_for_empty_queue, [])
99
+ observer.instance_variable_get(:@watchdog_enabled).should be true
100
+ end
101
+
102
+ it "shouldn't disable the watchdog if there is an active download" do
103
+ observer.send(:check_for_empty_queue, [])
104
+ observer.instance_variable_get(:@watchdog_enabled).should be true
105
+ end
106
+ end
107
+
108
+ context "#update_package_ids" do
109
+
110
+ it "should update the package id" do
111
+ observer.send(:update_package_ids, response)
112
+ episode.pid.should eq 11
113
+ end
114
+
115
+ it "should update the pid also if it is not incremented by 1" do
116
+ response[0]['pid'] = 12
117
+
118
+ expect {
119
+ observer.send(:update_package_ids, response)
120
+ }.to_not raise_error(Junkie::InvalidStateError)
121
+ episode.pid.should eq 12
122
+ end
123
+ end
124
+
125
+ context "#react_on_failed_links" do
126
+
127
+ let(:failed_id){ Junkie::Pyload::Api::FILE_STATUS.key(:failed) }
128
+
129
+ it "should call restartFailed on the Api if there are failed links" do
130
+ response[0]['links'][0]['status'] = failed_id
131
+
132
+ observer.api.should_receive(:call).with(:restartFailed)
133
+ expect {
134
+ observer.send(:check_for_failed_links, response)
135
+ }.to throw_symbol(:break)
136
+ end
137
+ end
138
+
139
+ context "#check_for_complete_packages" do
140
+
141
+ let(:file_status){ Junkie::Pyload::Api::FILE_STATUS }
142
+ let(:complete_response) do
143
+ copy = response.clone
144
+ new_links = copy[0]['links'].map do |e|
145
+ e['status'] = file_status.key(:done)
146
+ e
147
+ end
148
+ copy[0]['links'] = new_links
149
+ copy[0]['linksdone'] = new_links.size
150
+ copy[0]['sizedone'] = 12345
151
+ copy[0]['sizetotal'] = 12345
152
+ copy[0]['pid'] = 12
153
+ copy
154
+ end
155
+
156
+ context "#get_complete_downloads" do
157
+ it "should return pids where alle links are done and size is right" do
158
+ pids = observer.send(:get_complete_downloads, complete_response)
159
+ pids.should include(12)
160
+ end
161
+
162
+ it "should treat :skipped and :done links as complete" do
163
+ complete_response[0]['links'][1]['status'] = file_status.key(:skipped)
164
+ complete_response[0]['links'][3]['status'] = file_status.key(:skipped)
165
+
166
+ pids = observer.send(:get_complete_downloads, complete_response)
167
+ pids.should include(12)
168
+ end
169
+
170
+ it "should take file status more seriously than sizedone" do
171
+ complete_response[0]['links'][1]['status'] = file_status.key(:failed)
172
+
173
+ pids = observer.send(:get_complete_downloads, complete_response)
174
+ pids.should_not include(12)
175
+ end
176
+ end
177
+
178
+
179
+ context "detected complete package the first time" do
180
+
181
+ it "should set a flag if a complete package has been detected" do
182
+ observer.send(:check_for_complete_packages, complete_response)
183
+ observer.instance_variable_get(
184
+ :@skipped_timer_at_first_complete_detection).should eq true
185
+ end
186
+ end
187
+
188
+ context "detected complete package second time" do
189
+
190
+ it "should remove the complete packages from Queue" do
191
+ observer.api.should_receive(:call).with(:deletePackages, {pids: [12]})
192
+ observer.instance_variable_set(
193
+ :@skipped_timer_at_first_complete_detection, true)
194
+
195
+ em {
196
+ observer.send(:check_for_complete_packages, complete_response)
197
+ }
198
+ end
199
+
200
+ it "should send out the active episode out on the channel" do
201
+ observer.instance_variable_set(
202
+ :@skipped_timer_at_first_complete_detection, true)
203
+ episode.pid = 12
204
+
205
+ observer.send(:check_for_complete_packages, complete_response)
206
+ observer.instance_variable_get(:@active_episode).should be_nil
207
+ observer.send(:is_ready?).should be true
208
+ end
209
+
210
+ end
211
+ end
212
+
213
+ end
214
+ end
215
+ end
216
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: junkie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-11 00:00:00.000000000 Z
12
+ date: 2012-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
@@ -202,7 +202,9 @@ files:
202
202
  - lib/junkie/version.rb
203
203
  - spec/config_spec.rb
204
204
  - spec/environment_spec.rb
205
+ - spec/fixtures/pyload_queue_data.rb
205
206
  - spec/pyload_api_spec.rb
207
+ - spec/pyload_observer_spec.rb
206
208
  - spec/spec_helper.rb
207
209
  homepage: https://github.com/pboehm/junkie
208
210
  licenses: []
@@ -231,6 +233,8 @@ summary: TV series managament tool that does all the work you have with your ser
231
233
  test_files:
232
234
  - spec/config_spec.rb
233
235
  - spec/environment_spec.rb
236
+ - spec/fixtures/pyload_queue_data.rb
234
237
  - spec/pyload_api_spec.rb
238
+ - spec/pyload_observer_spec.rb
235
239
  - spec/spec_helper.rb
236
240
  has_rdoc: