p3-tv 0.9.2 → 0.10.0

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.
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