p3-tv 0.9.1

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 (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: []