p3-tv 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/bin/p3tv_json_api +17 -0
  3. data/lib/p3-tv.rb +633 -630
  4. data/p3-tv.gemspec +1 -1
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fc7ad218f4aa40ae19eaf08703b8687286ec3b42
4
- data.tar.gz: f61c67bea5e93d64e5945c00f11c3bf60d968238
3
+ metadata.gz: 5bc737fbd16562f2e12f370e6ed1012c86701b53
4
+ data.tar.gz: 99248ef697c8fadad879b617014db6355c5804fe
5
5
  SHA512:
6
- metadata.gz: 260afe152fcc583f141e120a46b3407a919d152c273f315480b07629ce7502c3eb292b5901942d38e551ffd6091377234b4f25dd42b5a8a5601c949c1ebb3f47
7
- data.tar.gz: c1ca3a1c732b7eefeb1bddea050c7c1e9ac14faf2b0a6d31a9804394e803060e35ec8e0f77a91d906ab1ed576814f83ad553ba1bd7b60b1527018a4b44f87215
6
+ metadata.gz: 0baa51409822f068a6590fbf0381dbc046df24923877dc6b14cf73322cc733d4d7d2694c94288829cb324d8b9ba69e3224acd32b12af4c4eb1833f658afd21a5
7
+ data.tar.gz: 8441636f8830d0ac2bb7863ff511ae15377b11327fdfac403dbdf4fd2924ed5ef75bff55074a93ce08859db7bc2a8c190b41b408c0249fd1b408347260172aec
data/bin/p3tv_json_api CHANGED
@@ -12,6 +12,23 @@ when 'set_tvdb_api_key'
12
12
  settings[:tvdb_api_key] = ARGV[1]
13
13
  when 'settings'
14
14
  puts settings.to_h.to_json
15
+ when 'set'
16
+ begin
17
+ settings[ ARGV[1] ] = Integer( ARGV[2] )
18
+ rescue
19
+ begin
20
+ settings[ ARGV[1] ] = Float( ARGV[2] )
21
+ rescue
22
+ case ARGV[2]
23
+ when "true"
24
+ settings[ ARGV[1] ] = true
25
+ when "false"
26
+ settings[ ARGV[1] ] = false
27
+ else
28
+ settings[ ARGV[1] ] = ARGV[2] #string
29
+ end
30
+ end
31
+ end
15
32
  when 'search'
16
33
  search = P3::TV::Search.new( settings )
17
34
  results = search.find_series( ARGV[1] )
data/lib/p3-tv.rb CHANGED
@@ -8,641 +8,644 @@ require 'p3-transmission'
8
8
 
9
9
  module P3
10
10
  module TV
11
-
12
- class Settings
13
- attr_accessor :path
14
- DEPRECATED_PATH = File::expand_path( "~/.p3tv" )
15
- DEFAULT_PATH = File::expand_path( "~/.p3tv/p3tv" )
16
- EPISODES_JSON = 'episodes.json'
17
- DEFAULTS = {
18
- :library_path => '~/Movies',
19
- :download_path => '~/Downloads',
11
+ class Settings
12
+ attr_accessor :path
13
+ DEPRECATED_PATH = File::expand_path( "~/.p3tv" )
14
+ DEFAULT_PATH = File::expand_path( "~/.p3tv/p3tv" )
15
+ EPISODES_JSON = 'episodes.json'
16
+ DEFAULTS = {
17
+ :library_path => '~/Movies',
18
+ :download_path => '~/Downloads',
20
19
  :create_p3tv_dir => true,
21
- :delete_duplicate_downloads => false,
22
- :overwrite_duplicates => false,
23
- :allowed_types => ['.avi', '.mkv', '.mp4'],
24
- :language => 'en',
25
- :subtitles => ['.srt'],
26
- :high_def => true,
27
- :verbose => false,
28
- :dry_run => false,
29
- :series => []
30
- }
31
-
32
- def self.exists?( path = DEFAULT_PATH )
33
- if( File::directory?( DEPRECATED_PATH ) )
34
- return File::exists?( path )
35
- else
36
- if( File::exists?( DEPRECATED_PATH ) )
37
- puts "please move your settings file #{DEPRECATED_PATH} to #{DEFAULT_PATH}"
38
- return false
39
- end
40
- end
41
- end
42
-
43
- def self.create_default!( path = DEFAULT_PATH )
44
- raise "a settings file already exists. please delete #{path} first" if exists?( path )
45
-
46
- FileUtils::mkdir_p( File::dirname( path ) )
47
- settings = Settings.new( path )
48
- DEFAULTS.each do | key, value |
49
- settings[ key ] = value
50
- end
51
- settings.save!
52
- end
53
-
54
- def self.set!( key, value, path = DEFAULT_PATH )
55
- settings = Settings.new( path )
56
- settings[ key ] = value
57
- settings.save!
58
- end
59
-
60
- def initialize( path = DEFAULT_PATH )
61
- @path = path
62
- @values = {}
63
- @episodes = {}
64
- return unless File::exists?( @path )
65
-
66
- FileUtils::mkdir_p( File::dirname( @path ) )
67
-
68
- f = File::open( @path, 'r' )
69
- @values = JSON::parse( f.read, :symbolize_names => true )
70
- f.close
71
-
72
- self[:library_path] = File::join( self[:library_path], "P3TV" ) if self[:create_p3tv_dir ]
73
- self[:library_path] = File::expand_path( self[:library_path ] )
74
- self[:download_path] = File::expand_path( self[:download_path ] )
75
- self[:series].uniq!
76
-
77
- if( self[:overwrite_duplicates] and self[:delete_duplicate_downloads] )
78
- raise "you cannot have 'overwrite_duplicates' and 'delete_duplicate_downloads' both set to true"
79
- end
80
-
81
- end
82
-
83
- def to_h
84
- return @values
85
- end
86
-
87
- def []( key )
88
- return @values[ key ]
89
- end
90
-
91
- def []=( key, value )
92
- @values[ key ] = value
93
- self.save!
94
- end
95
-
96
- def allowed_type?( path )
97
- return ( self[:allowed_types].include?( File::extname( path ) ) or self[:subtitles].include?( File::extname( path ) ) )
98
- end
99
-
100
- def get_series( seriesid )
101
- return self[:series].detect{|s| s[:id] == seriesid }
102
- end
103
-
104
- def download_url!( url, path )
105
- # http://stackoverflow.com/questions/2515931/how-can-i-download-a-file-from-a-url-and-save-it-in-rails
106
- return path if File::exists?( path )
107
- begin
108
- download = open( url )
109
- IO.copy_stream( download, path )
110
- rescue => e
111
- return ""
112
- end
113
- return path
114
- end
115
-
116
- def download_banners!( banners, path )
117
- FileUtils::mkdir_p( File::dirname( path ) )
118
- return if banners.empty?
119
- banner = banners.detect{|b| b.url.length }
120
- return "" unless banner
121
-
122
- return download_url!( banner.url, path )
123
- end
124
-
125
- def episodes( seriesid )
126
- unless @episodes.has_key?( seriesid )
127
- f = File::open( File::join( series_dir( seriesid ), EPISODES_JSON ) )
128
- @episodes[ seriesid ] = JSON::parse( f.read, :symbolize_names => true )
129
- end
130
- return @episodes[ seriesid ]
131
- end
132
-
133
- def each_series_episode_file_status( seriesid, search, downloads, library )
134
- today = Date::today.to_s
135
-
136
- series_hash = self[:series].detect{|s| s[:id] == seriesid}
137
- return unless series_hash
138
-
139
- episodes( seriesid ).each do | episode_hash |
140
- next if episode_hash[:season_number] == 0
141
- ep_file = P3::TV::EpisodeFile.new
142
- ep_file.series_id = episode_hash[:id]
143
- ep_file.series = series_hash[:name]
144
- ep_file.season = episode_hash[:season_number]
145
- ep_file.episode = episode_hash[:number]
146
- ep_file.title = episode_hash[:name]
147
- ep_file.air_date = episode_hash[:air_date]
148
- ep_file.thumbnail = episode_hash[:thumb_path]
149
-
150
- if( ( ep_file.air_date == nil ) or ( ep_file.air_date > today ) )
151
- ep_file.percent_done = 0
152
- ep_file.status = :upcoming
153
- ep_file.path = ''
154
- elsif( library.exists?( ep_file ) )
155
- ep_file.percent_done = 1
156
- ep_file.status = :cataloged
157
- ep_file.path = library.episode_path( ep_file )
158
- elsif( download_path = downloads.get_path_if_exists( ep_file ) )
159
- ep_file.percent_done = 1
160
- ep_file.status = :downloaded
161
- ep_file.path = download_path
162
- elsif( torrent = downloads.get_torrent_if_exists( ep_file ) )
163
- ep_file.percent_done = torrent['percentDone']
164
- ep_file.status = :downloading
165
- ep_file.path = ''
166
- elsif( magnet_link = search.get_magnet_link_if_exists( ep_file ) )
167
- ep_file.percent_done = 0
168
- ep_file.status = :available
169
- ep_file.path = magnet_link
170
- else
171
- ep_file.percent_done = 0
172
- ep_file.status = :missing
173
- ep_file.path = ''
174
- end
175
- yield( ep_file )
176
- end
177
- end
178
-
179
-
180
- def series_dir( seriesid )
181
- return File::join( File::dirname( @path ), 'series', seriesid )
182
- end
183
-
184
- def add_series!( series )
185
- hash = series.to_h
186
- hash[:banners] = {}
187
- meta_path = series_dir( hash[:id] )
188
- hash[:banners][:poster] = download_banners!( series.posters( self[:language] ), File::join( meta_path, 'poster.jpg' ) )
189
- hash[:banners][:banner] = download_banners!( series.series_banners( self[:language] ), File::join( meta_path, 'banner.jpg' ) )
190
-
191
- episodes = []
192
- series.episodes.each do |episode|
193
- episode_hash = episode.to_h
194
- episode_hash[:thumb_path] = download_url!( episode_hash[:thumb], File::join( meta_path, "#{episode.id}.jpg" ) )
195
- episodes << episode_hash
196
- end
197
- f = File::open( File::join( meta_path, EPISODES_JSON ), 'w' )
198
- f.puts JSON::pretty_generate( episodes )
199
- f.close()
200
-
201
- remove_series!( hash[:id] )
202
- self[:series] << hash
203
- leading_the = /^The /
204
- self[:series].sort!{|a,b| a[:name].gsub(leading_the,'') <=> b[:name].gsub(leading_the,'') }
205
- self.save!
206
- end
207
-
208
- def remove_series!( seriesid )
209
- self[:series].reject!{|s| s[:id] == seriesid }
210
- self.save!
211
- end
212
-
213
- def save!
214
- f = File::open( @path, 'w' )
215
- f.puts( JSON::pretty_generate( @values ) )
216
- f.close
217
- end
218
- end
219
-
220
- class EpisodeFile
221
- attr_accessor :series_id, :series, :season, :episode, :title, :air_date, :path, :status, :percent_done, :thumbnail
222
- attr_writer :type
223
-
224
- def type
225
- unless @type
226
- if( @path )
227
- ext = File::extname( @path )
228
- @type = ext unless ext.empty?
229
- end
230
- end
231
- return @type
232
- end
233
-
234
- def to_json(*a)
235
- return to_h.to_json(*a)
236
- end
237
-
238
- def to_h
239
- return { :series_id => series_id,
240
- :series => series,
241
- :season => season,
242
- :episode => episode,
243
- :title => title,
244
- :air_date => air_date.to_s,
245
- :path => path,
246
- :status => status,
247
- :percent_done => percent_done,
248
- :thumbnail => thumbnail
249
- }
250
- end
251
-
252
- def <=>( other )
253
- if( self.series == other.series )
254
- if( self.season == other.season )
255
- return self.episode <=> other.episode
256
- else
257
- return self.season <=> other.season
258
- end
259
- else
260
- return self.series <=> other.series
261
- end
262
- end
263
-
264
- def to_s
265
- return to_h.to_s
266
- end
267
- end
268
-
269
- class Library
270
-
271
- def initialize( settings = Settings.new )
272
- @settings = settings
273
- end
274
-
275
- def exists?( episode )
276
- Dir::glob( episode_glob( episode ) ).each do | path |
277
- return true if File::exists?( path )
278
- end
279
- return false
280
- end
281
-
282
- def format_season( episode )
283
- return episode.season.to_s.rjust( 2, '0' )
284
- end
285
-
286
- def format_episode( episode )
287
- return episode.episode.to_s.rjust( 2, '0' )
288
- end
289
-
290
-
291
- def episode_glob( episode )
292
- formatted_title = P3::TV::format_title( episode.series )
293
- return File::join( @settings[:library_path],
294
- formatted_title,
295
- "Season #{format_season( episode )}",
296
- "#{formatted_title} S#{format_season( episode )}E#{format_episode( episode )}" + ( episode.type or '.*' )
297
- )
298
- end
299
-
300
- def episode_path( episode )
301
- glob = episode_glob( episode )
302
- if( episode.type )
303
- return glob # this will NOT end in .*
304
- else
305
- Dir::glob( glob ).each do | path |
306
- return path
307
- end
308
- end
309
- end
310
-
311
- def catalog!( episode )
312
- cataloged_path = episode_path( episode )
313
- cataloged_dir = File::dirname( cataloged_path )
314
-
315
- unless File::exists?( cataloged_dir )
316
- FileUtils::mkdir_p( cataloged_dir, { :noop => @settings[:dry_run], :verbose => @settings[:verbose] } )
317
- end
318
-
319
- if( !File::exists?( cataloged_path ) or @settings[:overwrite_duplicates] )
320
- FileUtils::move( episode.path, cataloged_path, { :noop => @settings[:dry_run], :verbose => @settings[:verbose], :force => true } )
321
- elsif( @settings[:delete_duplicate_downloads] )
322
- FileUtils::remove( episode.path, { :noop => @settings[:dry_run], :verbose => @settings[:verbose] } )
323
- else
324
- STDERR.puts "file exists. doing nothing: #{cataloged_path}" if @settings[:verbose]
325
- end
326
- end
327
- end
328
-
329
- class Downloads
330
-
331
- REGEX = [ /[sS](\d{1,2})[eE](\d{1,2})/, #s1e2, s01e02, S1E02, S01E2
332
- /(\d{1,2})x(\d{1,2})/ #1x2, 01x2, 1x02, 01x02
333
- ]
334
-
335
- def initialize( settings = Settings.new )
336
- @settings = settings
337
- @transmission = nil
338
- @paths = nil
339
- @torrents = nil
340
- end
341
-
342
- def path_match_series( path, series_name )
343
- return unless( P3::TV::path_contains_series?( path, series_name ) )
344
- REGEX.each do | regex |
345
- match_data = path.match( regex )
346
- if( match_data )
347
- yield( match_data )
348
- return
349
- end
350
- end
351
- end
352
-
353
- def path_match( path )
354
- @settings[:series].each do | series |
355
- path_match_series( path, series[:name] ) do | match_data |
356
- yield( series, match_data )
357
- return
358
- end
359
- end
360
- end
361
-
362
- def create_episode_from_filename_series( path, seriesid, series_name )
363
- e = nil
364
- path_match_series( path, series_name ) do | match_data |
365
- e = EpisodeFile.new
366
- e.series_id = seriesid
367
- e.series = series_name
368
- e.season = match_data[1].to_i
369
- e.episode = match_data[ 2 ].to_i
370
- e.path = path
371
- end
372
- return e
373
- end
374
-
375
- def create_episode_from_filename( path )
376
- e = nil
377
- path_match( path ) do | series, match_data |
378
- e = EpisodeFile.new
379
- e.series_id = series[:id]
380
- e.series = series[:name]
381
- e.season = match_data[1].to_i
382
- e.episode = match_data[ 2 ].to_i
383
- e.path = path
384
- end
385
- return e
386
- end
387
-
388
- def each_episode_file_in_series( seriesid )
389
- series = @settings.get_series( seriesid )
390
- if( series )
391
- episode_files = paths().collect{|path| create_episode_from_filename_series( path, series[:id], series[:name] ) }
392
- episode_files.each do | episode_file |
393
- yield( episode_file ) if episode_file
394
- end
395
- end
396
- end
397
-
398
- def each_episode_file
399
- episode_files = paths().collect{|path| create_episode_from_filename( path ) }
400
- episode_files.each do | episode_file |
401
- if episode_file
402
- episode_file.status = :downloaded
403
- episode_file.percent_done = 1
404
- yield( episode_file )
405
- end
406
- end
407
-
408
- end
409
-
410
- def each_downloading_file
411
- torrents().each do |torrent|
412
- episode_file = create_episode_from_filename( torrent['name'] )
413
- if( episode_file )
414
- episode_file.status = :downloading
415
- episode_file.percent_done = torrent['percentDone']
416
- yield( episode_file )
417
- end
418
- end
419
- end
420
-
421
- def download!( path )
422
- transmission().create( path ) if transmission()
423
- end
424
-
425
- def download_episode_file!( episode_file )
426
- if( episode_file.status == :available )
427
- magnet_link = episode_file.path
428
- puts magnet_link if @settings[:verbose]
429
- unless @settings[:dry_run]
430
- download!( magnet_link )
431
- end
432
- end
433
- end
434
-
435
- def paths
436
- return @paths if @paths
437
- glob = File::join( @settings[:download_path], '**/*' )
438
- @paths = Dir::glob( glob )
439
- @paths = @paths.select{|p| @settings.allowed_type?( p ) }
440
- return @paths
441
- end
442
-
443
- def transmission
444
- unless @transmission
445
- unless( @settings[:transmission] == nil )
446
- @transmission = P3::Transmission::Client.new(@settings[:transmission])
447
- end
448
- end
449
-
450
- return @transmission
451
- end
452
-
453
- def torrents
454
- @torrents = [] unless transmission()
455
- unless @torrents
456
- @torrents = transmission().all
457
- end
458
-
459
- return @torrents
460
- end
461
-
462
- def remove_completed_torrents!
463
- count = 0
464
- torrents().each do | torrent |
465
- count += 1
466
- transmission().remove( torrent['id'] ) if torrent['percentDone'] == 1
467
- end
468
-
469
- torrents().reject!{ | torrent | torrent['percentDone'] == 1 }
470
- return count
471
- end
472
-
473
- def get_path_if_exists( episode_file )
474
- episode_files = paths().collect{|p| create_episode_from_filename_series( p, episode_file.series_id, episode_file.series ) }
475
- episode_files.select!{|ef| ef }
476
- episode_files.each do | dn_ep | #download_episode_file
477
- if( 0 == ( episode_file <=> dn_ep ) )
478
- return dn_ep.path
479
- end
480
- end
481
- return nil
482
- end
483
-
484
- def get_torrent_if_exists( episode_file )
485
- torrents().each do | torrent |
486
- name = torrent['name']
487
- torrent_episode = create_episode_from_filename_series( name, episode_file.series_id, episode_file.series )
488
- if( torrent_episode )
489
- if( 0 == ( episode_file <=> torrent_episode ) )
490
- return torrent
491
- end
492
- end
493
- end
494
- return nil
495
- end
496
-
497
- end
498
-
499
- def self.format_title( title )
500
- #strip non alphanumeric characters and extra whitespace
501
- rval = title.gsub(/[^0-9a-z ]/i, '').gsub(/[ ]+/,' ').strip
502
- return rval
503
- end
504
-
505
- def self.path_contains_series?( path, title )
506
- formatted_title = P3::TV::format_title( title )
507
- if path.scan( /#{formatted_title}/i ).empty? #case insensative
508
- if path.scan( /#{formatted_title.gsub(' ','.')}/i ).empty? # Titles.With.Periods.Instead.Of.Spaces
509
- return false
510
- end
511
- end
512
- return true
513
- end
514
-
515
- class Search
20
+ :delete_duplicate_downloads => false,
21
+ :overwrite_duplicates => false,
22
+ :allowed_types => ['.avi', '.mkv', '.mp4'],
23
+ :language => 'en',
24
+ :subtitles => ['.srt'],
25
+ :high_def => true,
26
+ :verbose => false,
27
+ :dry_run => false,
28
+ :series => []
29
+ }
30
+
31
+ def self.exists?( path = DEFAULT_PATH )
32
+ if( File::directory?( DEPRECATED_PATH ) )
33
+ return File::exists?( path )
34
+ else
35
+ if( File::exists?( DEPRECATED_PATH ) )
36
+ puts "please move your settings file #{DEPRECATED_PATH} to #{DEFAULT_PATH}"
37
+ return false
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.create_default!( path = DEFAULT_PATH )
43
+ raise "a settings file already exists. please delete #{path} first" if exists?( path )
44
+
45
+ FileUtils::mkdir_p( File::dirname( path ) )
46
+ settings = Settings.new( path )
47
+ DEFAULTS.each do | key, value |
48
+ settings[ key ] = value
49
+ end
50
+ settings.save!
51
+ end
52
+
53
+ def self.set!( key, value, path = DEFAULT_PATH )
54
+ settings = Settings.new( path )
55
+ settings[ key ] = value
56
+ settings.save!
57
+ end
58
+
59
+ def initialize( path = DEFAULT_PATH )
60
+ @path = path
61
+ @values = {}
62
+ @episodes = {}
63
+ return unless File::exists?( @path )
64
+
65
+ FileUtils::mkdir_p( File::dirname( @path ) )
66
+
67
+ f = File::open( @path, 'r' )
68
+ @values = JSON::parse( f.read, :symbolize_names => true )
69
+ f.close
70
+
71
+ self[:library_path] = File::join( self[:library_path], "P3TV" ) if self[:create_p3tv_dir ]
72
+ self[:library_path] = File::expand_path( self[:library_path ] )
73
+ self[:download_path] = File::expand_path( self[:download_path ] )
74
+ self[:series].uniq!
75
+
76
+ if( self[:overwrite_duplicates] and self[:delete_duplicate_downloads] )
77
+ raise "you cannot have 'overwrite_duplicates' and 'delete_duplicate_downloads' both set to true"
78
+ end
79
+
80
+ end
81
+
82
+ def to_h
83
+ return @values
84
+ end
85
+
86
+ def []( key )
87
+ return @values[ key.to_sym ]
88
+ end
89
+
90
+ def []=( key, value )
91
+ @values[ key.to_sym ] = value
92
+ self.save!
93
+ end
94
+
95
+ def allowed_type?( path )
96
+ return ( self[:allowed_types].include?( File::extname( path ) ) or self[:subtitles].include?( File::extname( path ) ) )
97
+ end
98
+
99
+ def get_series( seriesid )
100
+ return self[:series].detect{|s| s[:id] == seriesid }
101
+ end
102
+
103
+ def download_url!( url, path )
104
+ # http://stackoverflow.com/questions/2515931/how-can-i-download-a-file-from-a-url-and-save-it-in-rails
105
+ return path if File::exists?( path )
106
+ begin
107
+ download = open( url )
108
+ IO.copy_stream( download, path )
109
+ rescue => e
110
+ return ""
111
+ end
112
+ return path
113
+ end
114
+
115
+ def download_banners!( banners, path )
116
+ FileUtils::mkdir_p( File::dirname( path ) )
117
+ return if banners.empty?
118
+ banner = banners.detect{|b| b.url.length }
119
+ return "" unless banner
120
+
121
+ return download_url!( banner.url, path )
122
+ end
123
+
124
+ def episodes( seriesid )
125
+ unless @episodes.has_key?( seriesid )
126
+ f = File::open( File::join( series_dir( seriesid ), EPISODES_JSON ) )
127
+ @episodes[ seriesid ] = JSON::parse( f.read, :symbolize_names => true )
128
+ end
129
+ return @episodes[ seriesid ]
130
+ end
131
+
132
+ def each_series_episode_file_status( seriesid, search, downloads, library )
133
+ today = Date::today.to_s
134
+
135
+ series_hash = self[:series].detect{|s| s[:id] == seriesid}
136
+ return unless series_hash
137
+
138
+ episodes( seriesid ).each do | episode_hash |
139
+ next if episode_hash[:season_number] == 0
140
+ ep_file = P3::TV::EpisodeFile.new
141
+ ep_file.series_id = episode_hash[:id]
142
+ ep_file.series = series_hash[:name]
143
+ ep_file.season = episode_hash[:season_number]
144
+ ep_file.episode = episode_hash[:number]
145
+ ep_file.title = episode_hash[:name]
146
+ ep_file.air_date = episode_hash[:air_date]
147
+ ep_file.thumbnail = episode_hash[:thumb_path]
148
+
149
+ if( ( ep_file.air_date == nil ) or ( ep_file.air_date > today ) )
150
+ ep_file.percent_done = 0
151
+ ep_file.status = :upcoming
152
+ ep_file.path = ''
153
+ elsif( library.exists?( ep_file ) )
154
+ ep_file.percent_done = 1
155
+ ep_file.status = :cataloged
156
+ ep_file.path = library.episode_path( ep_file )
157
+ elsif( download_path = downloads.get_path_if_exists( ep_file ) )
158
+ ep_file.percent_done = 1
159
+ ep_file.status = :downloaded
160
+ ep_file.path = download_path
161
+ elsif( torrent = downloads.get_torrent_if_exists( ep_file ) )
162
+ ep_file.percent_done = torrent['percentDone']
163
+ ep_file.status = :downloading
164
+ ep_file.path = ''
165
+ elsif( magnet_link = search.get_magnet_link_if_exists( ep_file ) )
166
+ ep_file.percent_done = 0
167
+ ep_file.status = :available
168
+ ep_file.path = magnet_link
169
+ else
170
+ ep_file.percent_done = 0
171
+ ep_file.status = :missing
172
+ ep_file.path = ''
173
+ end
174
+ yield( ep_file )
175
+ end
176
+ end
177
+
178
+
179
+ def series_dir( seriesid )
180
+ return File::join( File::dirname( @path ), 'series', seriesid )
181
+ end
182
+
183
+ def add_series!( series )
184
+ hash = series.to_h
185
+ hash[:banners] = {}
186
+ meta_path = series_dir( hash[:id] )
187
+ hash[:banners][:poster] = download_banners!( series.posters( self[:language] ), File::join( meta_path, 'poster.jpg' ) )
188
+ hash[:banners][:banner] = download_banners!( series.series_banners( self[:language] ), File::join( meta_path, 'banner.jpg' ) )
189
+
190
+ episodes = []
191
+ series.episodes.each do |episode|
192
+ episode_hash = episode.to_h
193
+ episode_hash[:thumb_path] = download_url!( episode_hash[:thumb], File::join( meta_path, "#{episode.id}.jpg" ) )
194
+ episodes << episode_hash
195
+ end
196
+ f = File::open( File::join( meta_path, EPISODES_JSON ), 'w' )
197
+ f.puts JSON::pretty_generate( episodes )
198
+ f.close()
199
+
200
+ remove_series!( hash[:id] )
201
+ self[:series] << hash
202
+ leading_the = /^The /
203
+ self[:series].sort!{|a,b| a[:name].gsub(leading_the,'') <=> b[:name].gsub(leading_the,'') }
204
+ self.save!
205
+ end
206
+
207
+ def remove_series!( seriesid )
208
+ self[:series].reject!{|s| s[:id] == seriesid }
209
+ self.save!
210
+ end
211
+
212
+ def save!
213
+ f = File::open( @path, 'w' )
214
+ f.puts( JSON::pretty_generate( @values ) )
215
+ f.close
216
+ end
217
+ end
218
+
219
+ class EpisodeFile
220
+ attr_accessor :series_id, :series, :season, :episode, :title, :air_date, :path, :status, :percent_done, :thumbnail
221
+ attr_writer :type
222
+
223
+ def type
224
+ unless @type
225
+ if( @path )
226
+ ext = File::extname( @path )
227
+ @type = ext unless ext.empty?
228
+ end
229
+ end
230
+ return @type
231
+ end
232
+
233
+ def to_json(*a)
234
+ return to_h.to_json(*a)
235
+ end
236
+
237
+ def to_h
238
+ return { :series_id => series_id,
239
+ :series => series,
240
+ :season => season,
241
+ :episode => episode,
242
+ :title => title,
243
+ :air_date => air_date.to_s,
244
+ :path => path,
245
+ :status => status,
246
+ :percent_done => percent_done,
247
+ :thumbnail => thumbnail
248
+ }
249
+ end
250
+
251
+ def <=>( other )
252
+ if( self.series == other.series )
253
+ if( self.season == other.season )
254
+ return self.episode <=> other.episode
255
+ else
256
+ return self.season <=> other.season
257
+ end
258
+ else
259
+ return self.series <=> other.series
260
+ end
261
+ end
262
+
263
+ def to_s
264
+ return to_h.to_s
265
+ end
266
+ end
267
+
268
+ class Library
269
+
270
+ def initialize( settings = Settings.new )
271
+ @settings = settings
272
+ end
273
+
274
+ def exists?( episode )
275
+ Dir::glob( episode_glob( episode ) ).each do | path |
276
+ return true if File::exists?( path )
277
+ end
278
+ return false
279
+ end
280
+
281
+ def format_season( episode )
282
+ return episode.season.to_s.rjust( 2, '0' )
283
+ end
284
+
285
+ def format_episode( episode )
286
+ return episode.episode.to_s.rjust( 2, '0' )
287
+ end
288
+
289
+
290
+ def episode_glob( episode )
291
+ formatted_title = P3::TV::format_title( episode.series )
292
+ return File::join( @settings[:library_path],
293
+ formatted_title,
294
+ "Season #{format_season( episode )}",
295
+ "#{formatted_title} S#{format_season( episode )}E#{format_episode( episode )}" + ( episode.type or '.*' )
296
+ )
297
+ end
298
+
299
+ def episode_path( episode )
300
+ glob = episode_glob( episode )
301
+ if( episode.type )
302
+ return glob # this will NOT end in .*
303
+ else
304
+ Dir::glob( glob ).each do | path |
305
+ return path
306
+ end
307
+ end
308
+ end
309
+
310
+ def catalog!( episode )
311
+ cataloged_path = episode_path( episode )
312
+ cataloged_dir = File::dirname( cataloged_path )
313
+
314
+ unless File::exists?( cataloged_dir )
315
+ FileUtils::mkdir_p( cataloged_dir, { :noop => @settings[:dry_run], :verbose => @settings[:verbose] } )
316
+ end
317
+
318
+ if( !File::exists?( cataloged_path ) or @settings[:overwrite_duplicates] )
319
+ FileUtils::move( episode.path, cataloged_path, { :noop => @settings[:dry_run], :verbose => @settings[:verbose], :force => true } )
320
+ elsif( @settings[:delete_duplicate_downloads] )
321
+ FileUtils::remove( episode.path, { :noop => @settings[:dry_run], :verbose => @settings[:verbose] } )
322
+ else
323
+ STDERR.puts "file exists. doing nothing: #{cataloged_path}" if @settings[:verbose]
324
+ end
325
+ end
326
+ end
327
+
328
+ class Downloads
329
+
330
+ REGEX = [ /[sS](\d{1,2})[eE](\d{1,2})/, #s1e2, s01e02, S1E02, S01E2
331
+ /(\d{1,2})x(\d{1,2})/ #1x2, 01x2, 1x02, 01x02
332
+ ]
333
+
334
+ def initialize( settings = Settings.new )
335
+ @settings = settings
336
+ @transmission = nil
337
+ @paths = nil
338
+ @torrents = nil
339
+ end
340
+
341
+ def path_match_series( path, series_name )
342
+ return unless( P3::TV::path_contains_series?( path, series_name ) )
343
+ REGEX.each do | regex |
344
+ match_data = path.match( regex )
345
+ if( match_data )
346
+ yield( match_data )
347
+ return
348
+ end
349
+ end
350
+ end
351
+
352
+ def path_match( path )
353
+ @settings[:series].each do | series |
354
+ path_match_series( path, series[:name] ) do | match_data |
355
+ yield( series, match_data )
356
+ return
357
+ end
358
+ end
359
+ end
360
+
361
+ def create_episode_from_filename_series( path, seriesid, series_name )
362
+ e = nil
363
+ path_match_series( path, series_name ) do | match_data |
364
+ e = EpisodeFile.new
365
+ e.series_id = seriesid
366
+ e.series = series_name
367
+ e.season = match_data[1].to_i
368
+ e.episode = match_data[ 2 ].to_i
369
+ e.path = path
370
+ end
371
+ return e
372
+ end
373
+
374
+ def create_episode_from_filename( path )
375
+ e = nil
376
+ path_match( path ) do | series, match_data |
377
+ e = EpisodeFile.new
378
+ e.series_id = series[:id]
379
+ e.series = series[:name]
380
+ e.season = match_data[1].to_i
381
+ e.episode = match_data[ 2 ].to_i
382
+ e.path = path
383
+ end
384
+ return e
385
+ end
386
+
387
+ def each_episode_file_in_series( seriesid )
388
+ series = @settings.get_series( seriesid )
389
+ if( series )
390
+ episode_files = paths().collect{|path| create_episode_from_filename_series( path, series[:id], series[:name] ) }
391
+ episode_files.each do | episode_file |
392
+ yield( episode_file ) if episode_file
393
+ end
394
+ end
395
+ end
396
+
397
+ def each_episode_file
398
+ episode_files = paths().collect{|path| create_episode_from_filename( path ) }
399
+ episode_files.each do | episode_file |
400
+ if episode_file
401
+ episode_file.status = :downloaded
402
+ episode_file.percent_done = 1
403
+ yield( episode_file )
404
+ end
405
+ end
406
+
407
+ end
408
+
409
+ def each_downloading_file
410
+ torrents().each do |torrent|
411
+ episode_file = create_episode_from_filename( torrent['name'] )
412
+ if( episode_file )
413
+ episode_file.status = :downloading
414
+ episode_file.percent_done = torrent['percentDone']
415
+ yield( episode_file )
416
+ end
417
+ end
418
+ end
419
+
420
+ def download!( path )
421
+ transmission().create( path ) if transmission()
422
+ end
423
+
424
+ def download_episode_file!( episode_file )
425
+ if( episode_file.status == :available )
426
+ magnet_link = episode_file.path
427
+ puts magnet_link if @settings[:verbose]
428
+ unless @settings[:dry_run]
429
+ download!( magnet_link )
430
+ end
431
+ end
432
+ end
433
+
434
+ def paths
435
+ return @paths if @paths
436
+ glob = File::join( @settings[:download_path], '**/*' )
437
+ @paths = Dir::glob( glob )
438
+ @paths = @paths.select{|p| @settings.allowed_type?( p ) }
439
+ return @paths
440
+ end
441
+
442
+ def transmission
443
+
444
+ unless @transmission
445
+ config = {}
446
+ [:transmission_username, :transmission_password, :transmission_host, :transmission_port ].each do | transmission_key |
447
+ return @transmission unless @settings[ transmission_key ]
448
+ key = transmission_key.to_s.gsub('transmission_','').to_sym
449
+ config[ key ] = @settings[ transmission_key ]
450
+ end
451
+
452
+ @transmission = P3::Transmission::Client.new( config )
453
+ end
454
+
455
+ return @transmission
456
+ end
457
+
458
+ def torrents
459
+ @torrents = [] unless transmission()
460
+ unless @torrents
461
+ @torrents = transmission().all
462
+ end
463
+
464
+ return @torrents
465
+ end
466
+
467
+ def remove_completed_torrents!
468
+ count = 0
469
+ torrents().each do | torrent |
470
+ count += 1
471
+ transmission().remove( torrent['id'] ) if torrent['percentDone'] == 1
472
+ end
473
+
474
+ torrents().reject!{ | torrent | torrent['percentDone'] == 1 }
475
+ return count
476
+ end
477
+
478
+ def get_path_if_exists( episode_file )
479
+ episode_files = paths().collect{|p| create_episode_from_filename_series( p, episode_file.series_id, episode_file.series ) }
480
+ episode_files.select!{|ef| ef }
481
+ episode_files.each do | dn_ep | #download_episode_file
482
+ if( 0 == ( episode_file <=> dn_ep ) )
483
+ return dn_ep.path
484
+ end
485
+ end
486
+ return nil
487
+ end
488
+
489
+ def get_torrent_if_exists( episode_file )
490
+ torrents().each do | torrent |
491
+ name = torrent['name']
492
+ torrent_episode = create_episode_from_filename_series( name, episode_file.series_id, episode_file.series )
493
+ if( torrent_episode )
494
+ if( 0 == ( episode_file <=> torrent_episode ) )
495
+ return torrent
496
+ end
497
+ end
498
+ end
499
+ return nil
500
+ end
501
+
502
+ end
503
+
504
+ def self.format_title( title )
505
+ #strip non alphanumeric characters and extra whitespace
506
+ rval = title.gsub(/[^0-9a-z ]/i, '').gsub(/[ ]+/,' ').strip
507
+ return rval
508
+ end
509
+
510
+ def self.path_contains_series?( path, title )
511
+ formatted_title = P3::TV::format_title( title )
512
+ if path.scan( /#{formatted_title}/i ).empty? #case insensative
513
+ if path.scan( /#{formatted_title.gsub(' ','.')}/i ).empty? # Titles.With.Periods.Instead.Of.Spaces
514
+ return false
515
+ end
516
+ end
517
+ return true
518
+ end
519
+
520
+ class Search
516
521
  # please don't steal my key. it is very easy to get one for your
517
522
  # app here: http://thetvdb.com/index.php?tab=apiregister
518
523
  # they won't spam you and its free
519
524
  TVDB_API_KEY = '70BE8DAF03F45D4A'
520
-
521
- def initialize( settings = Settings.new )
522
- @settings = settings
523
-
524
- @tvdb = P3::Tvdb::Search.new( TVDB_API_KEY )
525
- @eztv = {}
526
- end
527
-
528
- def find_series( title )
529
- results = @tvdb.search( title )
530
- results.select!{|r| r['FirstAired'] } #must have this field
531
-
532
- #assume the more-recent show first
533
- results.sort!{ | a,b | b['FirstAired'] <=> a['FirstAired'] }
534
- results = results.collect{|r| find_series_by_id( r['seriesid'] ) }
535
- return results
536
- end
537
-
538
- def find_series_by_id( seriesid )
539
- return @tvdb.get_series_by_id( seriesid )
540
- end
541
-
542
- def find_episodes_by_seriesid( seriesid )
543
- series = find_series_by_id( seriesid )
544
- if( series )
545
- series.episodes.each do | episode |
546
- yield( episode ) if episode.season_number > 0
547
- end
548
- end
549
- end
550
-
551
- def each_episode
552
- @settings[:series].each do | series_hash |
553
- find_episodes_by_seriesid( series_hash[:id] ) do | episode |
554
- yield( episode )
555
- end
556
- end
557
- end
558
-
559
- def eztv( series_name )
560
- unless( @eztv.has_key?( series_name ) )
561
- ez = P3::Eztv::Series.new( P3::TV::format_title( series_name ) )
562
- ez.high_def! if @settings[:high_def]
563
- @eztv[ series_name ] = ez
564
- end
565
- return @eztv[ series_name ]
566
- end
567
-
568
- def get_magnet_link_if_exists( episode_file )
569
- ez = eztv( episode_file.series )
570
- eztv_episode = ez.episode( episode_file.season, episode_file.episode )
571
- return eztv_episode.magnet_link if eztv_episode
572
- return nil
573
- end
574
- end
575
-
576
- def self.add_series!( title )
577
- settings = Settings.new
578
- search = Search.new( settings )
579
-
580
- results = search.find_series( title )
581
-
582
- settings.add_series!( results[0] )
583
- settings.save!
584
- end
585
-
586
- def self.test_mode_enabled?
587
- settings = Settings.new
588
- return ( settings[:verbose] and settings[:dry_run] )
589
- end
590
-
591
- def self.enable_test_mode!( enable )
592
- Settings::set!( :dry_run, enable )
593
- Settings::set!( :verbose, enable )
594
- end
595
-
596
-
597
- def self.catalog_file!( path, settings = Settings.new )
598
- downloads = Downloads.new( settings )
599
- return if settings.allowed_type?( path )
600
- library = Library.new( settings )
601
- episode = downloads.create_episode_from_filename( path )
602
- library.catalog!( episode ) if episode
603
- return nil
604
- end
605
-
606
- def self.catalog_downloads_series!( seriesid, settings = Settings.new )
607
- downloads = Downloads.new( settings )
608
- downloads.remove_completed_torrents!
609
-
610
- library = Library.new( settings )
611
- downloads.each_episode_file_in_series( seriesid ) do | episode_file |
612
- library.catalog!( episode_file )
613
- end
614
- return nil
615
- end
616
-
617
- def self.catalog_downloads!( settings = Settings.new, downloads = Downloads.new( settings ) )
618
- library = Library.new( settings )
619
- downloads.each_episode_file do | episode_file |
620
- library.catalog!( episode_file )
621
- end
622
- return nil
623
- end
624
-
625
- def self.download_missing_series!( seriesid, settings = Settings.new )
626
- search = Search.new( settings )
627
- library = Library.new( settings )
628
- downloads = Downloads.new( settings )
629
-
630
- settings.each_series_episode_file_status( seriesid, search, downloads, library ) do | episode_file |
631
- downloads.download_episode_file!( episode_file )
632
- end
633
- end
634
-
635
- def self.download_missing!( settings = Settings.new )
636
- search = Search.new( settings )
637
- library = Library.new( settings )
638
- downloads = Downloads.new( settings )
639
- settings[:series].each do | series |
640
- settings.each_series_episode_file_status( series[:id], search, downloads, library ) do | episode_file |
641
- downloads.download_episode_file!( episode_file )
642
- end
643
- end
644
- end
645
-
646
525
 
526
+ def initialize( settings = Settings.new )
527
+ @settings = settings
528
+
529
+ @tvdb = P3::Tvdb::Search.new( TVDB_API_KEY )
530
+ @eztv = {}
531
+ end
532
+
533
+ def find_series( title )
534
+ results = @tvdb.search( title )
535
+ results.select!{|r| r['FirstAired'] } #must have this field
536
+
537
+ #assume the more-recent show first
538
+ results.sort!{ | a,b | b['FirstAired'] <=> a['FirstAired'] }
539
+ results = results.collect{|r| find_series_by_id( r['seriesid'] ) }
540
+ return results
541
+ end
542
+
543
+ def find_series_by_id( seriesid )
544
+ return @tvdb.get_series_by_id( seriesid )
545
+ end
546
+
547
+ def find_episodes_by_seriesid( seriesid )
548
+ series = find_series_by_id( seriesid )
549
+ if( series )
550
+ series.episodes.each do | episode |
551
+ yield( episode ) if episode.season_number > 0
552
+ end
553
+ end
554
+ end
555
+
556
+ def each_episode
557
+ @settings[:series].each do | series_hash |
558
+ find_episodes_by_seriesid( series_hash[:id] ) do | episode |
559
+ yield( episode )
560
+ end
561
+ end
562
+ end
563
+
564
+ def eztv( series_name )
565
+ unless( @eztv.has_key?( series_name ) )
566
+ ez = P3::Eztv::Series.new( P3::TV::format_title( series_name ) )
567
+ ez.high_def! if @settings[:high_def]
568
+ @eztv[ series_name ] = ez
569
+ end
570
+ return @eztv[ series_name ]
571
+ end
572
+
573
+ def get_magnet_link_if_exists( episode_file )
574
+ ez = eztv( episode_file.series )
575
+ eztv_episode = ez.episode( episode_file.season, episode_file.episode )
576
+ return eztv_episode.magnet_link if eztv_episode
577
+ return nil
578
+ end
579
+ end
580
+
581
+ def self.add_series!( title )
582
+ settings = Settings.new
583
+ search = Search.new( settings )
584
+
585
+ results = search.find_series( title )
586
+
587
+ settings.add_series!( results[0] )
588
+ settings.save!
589
+ end
590
+
591
+ def self.test_mode_enabled?
592
+ settings = Settings.new
593
+ return ( settings[:verbose] and settings[:dry_run] )
594
+ end
595
+
596
+ def self.enable_test_mode!( enable )
597
+ Settings::set!( :dry_run, enable )
598
+ Settings::set!( :verbose, enable )
599
+ end
600
+
601
+
602
+ def self.catalog_file!( path, settings = Settings.new )
603
+ downloads = Downloads.new( settings )
604
+ return if settings.allowed_type?( path )
605
+ library = Library.new( settings )
606
+ episode = downloads.create_episode_from_filename( path )
607
+ library.catalog!( episode ) if episode
608
+ return nil
609
+ end
610
+
611
+ def self.catalog_downloads_series!( seriesid, settings = Settings.new )
612
+ downloads = Downloads.new( settings )
613
+ downloads.remove_completed_torrents!
614
+
615
+ library = Library.new( settings )
616
+ downloads.each_episode_file_in_series( seriesid ) do | episode_file |
617
+ library.catalog!( episode_file )
618
+ end
619
+ return nil
620
+ end
621
+
622
+ def self.catalog_downloads!( settings = Settings.new, downloads = Downloads.new( settings ) )
623
+ library = Library.new( settings )
624
+ downloads.each_episode_file do | episode_file |
625
+ library.catalog!( episode_file )
626
+ end
627
+ return nil
628
+ end
629
+
630
+ def self.download_missing_series!( seriesid, settings = Settings.new )
631
+ search = Search.new( settings )
632
+ library = Library.new( settings )
633
+ downloads = Downloads.new( settings )
634
+
635
+ settings.each_series_episode_file_status( seriesid, search, downloads, library ) do | episode_file |
636
+ downloads.download_episode_file!( episode_file )
637
+ end
638
+ end
639
+
640
+ def self.download_missing!( settings = Settings.new )
641
+ search = Search.new( settings )
642
+ library = Library.new( settings )
643
+ downloads = Downloads.new( settings )
644
+ settings[:series].each do | series |
645
+ settings.each_series_episode_file_status( series[:id], search, downloads, library ) do | episode_file |
646
+ downloads.download_episode_file!( episode_file )
647
+ end
648
+ end
649
+ end
647
650
  end
648
651
  end