p3-tv 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +46 -0
  5. data/bin/p3tv +130 -0
  6. data/bin/p3tv_json_api +56 -0
  7. data/lib/p3-tv.rb +643 -0
  8. data/p3-tv.gemspec +26 -0
  9. metadata +110 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c3d630bf9e9b81457b1d57e3baba1358c849611b
4
+ data.tar.gz: 8ff50ded8f3818818acd36aae020d9f58c0ccc51
5
+ SHA512:
6
+ metadata.gz: 56ee639f934ff4761867beaad17e76781e495be16e6f9c28365ee5b0826433ec510f30ac3b037a3906947d55fc2a7d434048332dbb8f0d63bffa5a9e50cb4dbf
7
+ data.tar.gz: 6ac0f51ef743fb84b49c1715b6c3a7f5cc0e957b59a89feea4992fcbb44c76443b16afd2cb0eeafadb8a3917076b07df552a95abb1c9931c82e1394527f9cfb4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in p3-tv.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Poul Hornsleth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ # P3TV
2
+ [![Gem Version](https://badge.fury.io/rb/p3-tv.svg)](http://badge.fury.io/rb/p3-tv)
3
+
4
+ Organize and rename your TV Shows. Automatically find links to missing shows. Includes Command-Line Utility
5
+
6
+ ## Installation
7
+ $ sudo gem install p3-tv
8
+
9
+ ## Usage
10
+
11
+ Run 'p3tv' at the command prompt and choose from the menu
12
+
13
+ $ p3tv
14
+ 1. Search for TV Series
15
+ 2. List TV Series
16
+ 3. Download Missing Episodes
17
+ 4. Catalog Downloads
18
+ 5. Manage Directories
19
+ 6. Test Mode
20
+ 7. quit
21
+ What do you want to do?
22
+
23
+
24
+ ## Development Usage
25
+
26
+ Fetch a series and get all the magnet links:
27
+ ```ruby
28
+ require 'p3-tv'
29
+
30
+ P3::TV::Settings::create_default! unless P3::TV::Settings::exists?
31
+ P3::TV::add_series!( "Black Sails" )
32
+ P3::TV::enable_test_mode!( true )
33
+
34
+ P3::TV::catalog_downloads!
35
+ P3::TV::download_missing!
36
+
37
+ ```
38
+
39
+
40
+ ## Contributing
41
+
42
+ 1. Fork it.
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
44
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
45
+ 4. Push to the branch (`git push origin my-new-feature`)
46
+ 5. Create new Pull Request
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'highline'
4
+ require 'p3-tv'
5
+ Encoding.default_external = 'UTF-8'
6
+
7
+ # Basic usage
8
+
9
+ Signal::trap("INT") do
10
+ exit
11
+ end
12
+
13
+ Signal::trap("TERM") do
14
+ exit
15
+ end
16
+
17
+
18
+ P3::TV::Settings::create_default! unless P3::TV::Settings::exists?
19
+
20
+
21
+ cli = HighLine.new
22
+
23
+ begin
24
+ settings = P3::TV::Settings.new
25
+
26
+ cli.say("\n")
27
+ cli.say("\n")
28
+ cli.say("\n")
29
+
30
+ cli.choose do | main_menu |
31
+
32
+ main_menu.prompt = "What do you want to do?"
33
+
34
+
35
+ main_menu.choice("Catalog Downloads") do
36
+ confirm = cli.ask("This command will move any episodes from your download dir to your Library. Are you sure? [Y/N] ") { |yn| yn.limit = 1, yn.validate = /[yn]/i }
37
+ P3::TV::catalog_downloads!( settings ) if confirm.downcase == 'y'
38
+ end
39
+
40
+
41
+
42
+ main_menu.choice("Download Missing Episodes") do
43
+ confirm = cli.ask("This command will open any missing magnet links. Are you sure? [Y/N]") { |yn| yn.limit = 1, yn.validate = /[yn]/i }
44
+ P3::TV::download_missing!( settings ) if confirm.downcase == 'y'
45
+ end
46
+
47
+
48
+ main_menu.choice("Manage Series") do
49
+ cli.choose do | series_menu |
50
+ series_menu.choice("Add Series") do
51
+ title = cli.ask "What series would you like to search for?"
52
+ search = P3::TV::Search.new( settings )
53
+ results = search.find_series( title )
54
+ cli.choose do | results_menu |
55
+ results_menu.prompt = "Which series would you like to add?"
56
+ results.each do | series |
57
+ results_menu.choice( series.name + ' : ' + series.network ) do
58
+ settings.add_series!( series )
59
+ settings.save!
60
+ end
61
+ end
62
+ results_menu.choice( "cancel" ) do
63
+ end
64
+ end
65
+ end
66
+
67
+ series_menu.choice("Delete Series") do
68
+
69
+ cli.choose do | delete_menu |
70
+ delete_menu.prompt = "Which series would you like to delete?"
71
+ settings[:series].each do | series |
72
+ delete_menu.choice( series[:name] + ' : ' + series[:network] ) do
73
+ confirm = cli.ask("Are you sure? [Y/N]") { |yn| yn.limit = 1, yn.validate = /[yn]/i }
74
+ settings.remove_series!( series[:id] ) if confirm.downcase == 'y'
75
+ settings.save!
76
+ end
77
+ end
78
+
79
+ delete_menu.choice( "cancel" ) do
80
+ end
81
+ end
82
+ end
83
+
84
+ series_menu.choice("List TV Series") do
85
+ settings[:series].each do | series |
86
+ cli.say( series[:name] + ' : ' + series[:network] )
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+
93
+
94
+ main_menu.choice("Manage Directories") do
95
+ cli.choose do | dir_menu |
96
+ {:library_path => 'Library', :download_path => 'Download' }.each do | k, v |
97
+ dir_menu.choice( "Update #{v} Path" ) do
98
+ path = cli.ask('Enter New Path')
99
+ P3::TV::Settings::set!( k, path )
100
+ end
101
+ dir_menu.choice( "Open #{v} Path" ) do
102
+ cmd = "open '#{settings[k]}'"
103
+ puts cmd
104
+ system( cmd )
105
+ end
106
+ end
107
+ dir_menu.choice( "cancel" ) do
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ main_menu.choice("Toggle Test Mode ( Currently #{P3::TV::test_mode_enabled? ? 'Enabled' : 'Disabled'} )") do
114
+ P3::TV::enable_test_mode!( !P3::TV::test_mode_enabled? )
115
+ end
116
+
117
+ main_menu.choice("Set TVDB API Key ( #{settings[:tvdb_api_key] or 'Not Set'} )") do
118
+ tvdb_api_key = cli.ask('Enter TVDB API Key ( http://www.thetvdb.com/?tab=register )')
119
+ P3::TV::Settings::set!(:tvdb_api_key, tvdb_api_key )
120
+ end
121
+
122
+ main_menu.choice("Reload Settings") do
123
+ puts "reloaded" # will be reloaded on next loop
124
+ end
125
+
126
+ main_menu.choice("quit") do
127
+ exit(0)
128
+ end
129
+ end
130
+ end while true
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'p3-tv'
4
+ Encoding.default_external = 'UTF-8'
5
+
6
+ P3::TV::Settings::create_default! unless P3::TV::Settings::exists?
7
+
8
+ settings = P3::TV::Settings.new
9
+
10
+ case ARGV[0]
11
+ when 'set_tvdb_api_key'
12
+ settings[:tvdb_api_key] = ARGV[1]
13
+ when 'settings'
14
+ puts settings.to_h.to_json
15
+ when 'search'
16
+ search = P3::TV::Search.new( settings )
17
+ results = search.find_series( ARGV[1] )
18
+ results = results.collect{|r| r.to_h }
19
+ puts results.to_json
20
+ when 'add_series'
21
+ search = P3::TV::Search.new( settings )
22
+ series = search.find_series_by_id( ARGV[1] )
23
+ settings.add_series!( series ) if series
24
+ when 'remove_series'
25
+ search = P3::TV::Search.new( settings )
26
+ series = search.find_series_by_id( ARGV[1] )
27
+ settings.remove_series!( series.id ) if series
28
+ when 'episode_status'
29
+ search = P3::TV::Search.new( settings )
30
+ library = P3::TV::Library.new( settings )
31
+ downloads = P3::TV::Downloads.new( settings )
32
+ files = []
33
+ settings.each_series_episode_file_status( ARGV[1], search, downloads, library ) do | episode_file |
34
+ files << episode_file.to_h
35
+ end
36
+ puts files.to_json
37
+ when 'download_missing'
38
+ P3::TV::download_missing_series!( ARGV[1], settings )
39
+ when 'catalog_and_downloads'
40
+ downloads = P3::TV::Downloads.new( settings )
41
+
42
+ if( downloads.remove_completed_torrents! > 0 )
43
+ P3::TV::catalog_downloads!( settings, downloads )
44
+ end
45
+
46
+ files = []
47
+ downloads.each_downloading_file do | episode_file |
48
+ files << episode_file.to_h
49
+ end
50
+ files.sort!{|a,b| b[:percent_done] <=> a[:percent_done] } #highest percent first
51
+ puts files.to_json
52
+ when 'catalog_downloads_for_series'
53
+ P3::TV::catalog_downloads_series!( ARGV[1], settings )
54
+ when 'catalog_downloads'
55
+ P3::TV::catalog_downloads!( settings )
56
+ end
@@ -0,0 +1,643 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'open-uri'
4
+
5
+ require 'p3-eztv'
6
+ require 'p3-tvdb'
7
+ require 'p3-transmission'
8
+
9
+ module P3
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',
20
+ :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
516
+ def initialize( settings = Settings.new )
517
+ @settings = settings
518
+ raise "tvdb api key required" unless @settings[:tvdb_api_key]
519
+ @tvdb = P3::Tvdb::Search.new( @settings[:tvdb_api_key] )
520
+ @eztv = {}
521
+ end
522
+
523
+ def find_series( title )
524
+ results = @tvdb.search( title )
525
+ results.select!{|r| r['FirstAired'] } #must have this field
526
+
527
+ #assume the more-recent show first
528
+ results.sort!{ | a,b | b['FirstAired'] <=> a['FirstAired'] }
529
+ results = results.collect{|r| find_series_by_id( r['seriesid'] ) }
530
+ return results
531
+ end
532
+
533
+ def find_series_by_id( seriesid )
534
+ return @tvdb.get_series_by_id( seriesid )
535
+ end
536
+
537
+ def find_episodes_by_seriesid( seriesid )
538
+ series = find_series_by_id( seriesid )
539
+ if( series )
540
+ series.episodes.each do | episode |
541
+ yield( episode ) if episode.season_number > 0
542
+ end
543
+ end
544
+ end
545
+
546
+ def each_episode
547
+ @settings[:series].each do | series_hash |
548
+ find_episodes_by_seriesid( series_hash[:id] ) do | episode |
549
+ yield( episode )
550
+ end
551
+ end
552
+ end
553
+
554
+ def eztv( series_name )
555
+ unless( @eztv.has_key?( series_name ) )
556
+ ez = P3::Eztv::Series.new( P3::TV::format_title( series_name ) )
557
+ ez.high_def! if @settings[:high_def]
558
+ @eztv[ series_name ] = ez
559
+ end
560
+ return @eztv[ series_name ]
561
+ end
562
+
563
+ def get_magnet_link_if_exists( episode_file )
564
+ ez = eztv( episode_file.series )
565
+ eztv_episode = ez.episode( episode_file.season, episode_file.episode )
566
+ return eztv_episode.magnet_link if eztv_episode
567
+ return nil
568
+ end
569
+ end
570
+
571
+ def self.add_series!( title )
572
+ settings = Settings.new
573
+ search = Search.new( settings )
574
+
575
+ results = search.find_series( title )
576
+
577
+ settings.add_series!( results[0] )
578
+ settings.save!
579
+ end
580
+
581
+ def self.test_mode_enabled?
582
+ settings = Settings.new
583
+ return ( settings[:verbose] and settings[:dry_run] )
584
+ end
585
+
586
+ def self.enable_test_mode!( enable )
587
+ Settings::set!( :dry_run, enable )
588
+ Settings::set!( :verbose, enable )
589
+ end
590
+
591
+
592
+ def self.catalog_file!( path, settings = Settings.new )
593
+ downloads = Downloads.new( settings )
594
+ return if settings.allowed_type?( path )
595
+ library = Library.new( settings )
596
+ episode = downloads.create_episode_from_filename( path )
597
+ library.catalog!( episode ) if episode
598
+ return nil
599
+ end
600
+
601
+ def self.catalog_downloads_series!( seriesid, settings = Settings.new )
602
+ downloads = Downloads.new( settings )
603
+ downloads.remove_completed_torrents!
604
+
605
+ library = Library.new( settings )
606
+ downloads.each_episode_file_in_series( seriesid ) do | episode_file |
607
+ library.catalog!( episode_file )
608
+ end
609
+ return nil
610
+ end
611
+
612
+ def self.catalog_downloads!( settings = Settings.new, downloads = Downloads.new( settings ) )
613
+ library = Library.new( settings )
614
+ downloads.each_episode_file do | episode_file |
615
+ library.catalog!( episode_file )
616
+ end
617
+ return nil
618
+ end
619
+
620
+ def self.download_missing_series!( seriesid, settings = Settings.new )
621
+ search = Search.new( settings )
622
+ library = Library.new( settings )
623
+ downloads = Downloads.new( settings )
624
+
625
+ settings.each_series_episode_file_status( seriesid, search, downloads, library ) do | episode_file |
626
+ downloads.download_episode_file!( episode_file )
627
+ end
628
+ end
629
+
630
+ def self.download_missing!( settings = Settings.new )
631
+ search = Search.new( settings )
632
+ library = Library.new( settings )
633
+ downloads = Downloads.new( settings )
634
+ settings[:series].each do | series |
635
+ settings.each_series_episode_file_status( series[:id], search, downloads, library ) do | episode_file |
636
+ downloads.download_episode_file!( episode_file )
637
+ end
638
+ end
639
+ end
640
+
641
+
642
+ end
643
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "p3-tv"
7
+ spec.version = "0.9.1"
8
+ spec.authors = ["Poul Hornsleth"]
9
+ spec.email = ["poulh@umich.edu"]
10
+ spec.summary = "TV Show Organizer, Renamer, and Downloader"
11
+ spec.description = "Organize and rename your TV Shows. Automatically find links to missing shows. Includes Command-Line Utility 'p3tv'"
12
+ spec.homepage = "https://github.com/poulh/p3-tv"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+
18
+
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "p3-eztv", "~> 0.0.7"
22
+ spec.add_runtime_dependency "p3-tvdb", "~> 0.9.0"
23
+ spec.add_runtime_dependency "p3-transmission", "~> 0.0.15"
24
+ spec.add_runtime_dependency "highline", "~> 1.7"
25
+
26
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: p3-tv
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.1
5
+ platform: ruby
6
+ authors:
7
+ - Poul Hornsleth
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: p3-eztv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.7
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: p3-tvdb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: p3-transmission
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.15
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.15
55
+ - !ruby/object:Gem::Dependency
56
+ name: highline
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ description: Organize and rename your TV Shows. Automatically find links to missing
70
+ shows. Includes Command-Line Utility 'p3tv'
71
+ email:
72
+ - poulh@umich.edu
73
+ executables:
74
+ - p3tv
75
+ - p3tv_json_api
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - Gemfile
80
+ - LICENSE
81
+ - README.md
82
+ - bin/p3tv
83
+ - bin/p3tv_json_api
84
+ - lib/p3-tv.rb
85
+ - p3-tv.gemspec
86
+ homepage: https://github.com/poulh/p3-tv
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.0.14.1
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: TV Show Organizer, Renamer, and Downloader
110
+ test_files: []