tv_renamer 4.0.2

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.
@@ -0,0 +1,646 @@
1
+ # renamer.rb
2
+ # Version 4.0.0
3
+ # Copyright 2011 Kevin Adler
4
+ # License: GPL v2
5
+
6
+ require 'rubygems'
7
+ require 'net/http'
8
+ require 'date'
9
+ require 'cgi'
10
+ require 'fileutils'
11
+ require 'nokogiri'
12
+
13
+ class VideoFile
14
+ SPLITS = [ ' ', '.' ]
15
+
16
+ attr_accessor :orig_show, :season, :episode_number, :episode_name, :extension, :filename, :date, :production_code, :format, :rename_by_date
17
+
18
+ def show
19
+ @orig_show ||= show_name_from_tokens
20
+ @show ? @show : @orig_show
21
+ end
22
+
23
+ def show=(show)
24
+ @show = show
25
+ end
26
+
27
+ def to_s
28
+ [show, @season, @episode_number, @date, @production_code, @episode_name].join(' : ')
29
+ end
30
+
31
+ def initialize(filename)
32
+ @filename = filename
33
+ @extension ||= @filename.split('.').pop
34
+ @show_pieces = Array.new
35
+
36
+ cleaned_filename = @filename.delete("[]").gsub(" - ", " ").gsub(/\([-\w]+\)/, '')
37
+
38
+ if date_match = cleaned_filename.match(/\d\d\.\d\d\.\d{4}/)
39
+ @date = Date.parse(date_match[0].gsub('.', '/')).strftime("%d %b %y")
40
+
41
+ # remove leading zeros
42
+ @date = @date[1..-1] if @date[0..0] == '0'
43
+
44
+ # remove date from filename to prevent matching parts of date as season or episode number
45
+ cleaned_filename = [date_match.pre_match, date_match.post_match].join('[date]')
46
+ end
47
+
48
+ SPLITS.each do |char|
49
+ clear_variables
50
+
51
+ pieces = remove_extension(cleaned_filename).split(char)
52
+
53
+ parse_showname(pieces) if pieces.length > 1
54
+
55
+ break if parsed_ok?
56
+ end
57
+ end
58
+
59
+ def clear_variables
60
+ @orig_show = @show = @season = @episode_name = @episode_number = @production_code = @date = nil
61
+ @show_pieces = Array.new
62
+ end
63
+
64
+ def parsed_ok?
65
+ show && ((@episode_number && @season) || (@rename_by_date && @date))
66
+ end
67
+
68
+ def parse_showname(pieces)
69
+ date = false
70
+ pieces.each do |piece|
71
+ if match = piece.match(/^[sS]([0-9]{1,2})[eE]([0-9]{1,3})$/)
72
+ @season = match[1]
73
+ @episode_number = match[2]
74
+ if(@season[0].chr == '0') then @season.delete!("0") end
75
+ if(@episode_number[0].chr == '0') then @episode_number.delete!("0") end
76
+ break
77
+ elsif match = piece.match(/^[sS]([0-9]{1,2})$/)
78
+ @season = match[1]
79
+ if(@season[0].chr == '0') then @season.delete!("0") end
80
+ if(@episode_number) then break end
81
+ elsif match = piece.match(/^[eE]([0-9]{1,3})$/)
82
+ @episode_number = match[1]
83
+ if(@episode_number[0].chr == '0') then @episode_number.delete!("0") end
84
+ if(@season) then break end
85
+ elsif match = piece.match(/^([0-9]{1,2})[xX]([0-9]{1,3})$/)
86
+ @season = match[1]
87
+ @episode_number = match[2]
88
+ if(@season[0].chr == '0') then @season.delete!("0") end
89
+ if(@episode_number[0].chr == '0') then @episode_number.delete!("0") end
90
+ break
91
+ elsif
92
+ (
93
+ (match = piece.match(/^[0-9]{3,4}$/)) and
94
+ !(
95
+ show_name_from_tokens.downcase == "the" || # Work around for "The 4400"
96
+ show_name_from_tokens.downcase == "sealab" || # Work around for "Sealab 2021"
97
+ (show_name_from_tokens.downcase == "knight rider" and match.to_s == "2008") || # Work around for "Knight Rider 2008"
98
+ (show_name_from_tokens.downcase == "90210" and match.to_s == "90210") # Work around for 90210
99
+ )
100
+ )
101
+ piece = match.to_s
102
+ if piece.length == 3
103
+ @season = piece[0].chr
104
+ @episode_number = piece[1..2]
105
+ if(@episode_number[0].chr == '0') then @episode_number.delete!("0") end
106
+ else
107
+ @season = piece[0..1]
108
+ @episode_number = piece[2..3]
109
+ if(@season[0].chr == '0') then @season.delete!("0") end
110
+ if(@episode_number[0].chr == '0') then @episode_number.delete!("0") end
111
+ end
112
+
113
+ break
114
+ elsif piece == "[date]"
115
+ date = true
116
+ else
117
+ if !date
118
+ if !@orig_show
119
+ @show_pieces.push camelize(piece)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+ private
128
+
129
+ def camelize(string)
130
+ string[0] = string[0].chr.upcase
131
+ string
132
+ end
133
+
134
+ def remove_extension(file)
135
+ file.gsub(".#{@extension}", '')
136
+ end
137
+
138
+ def show_name_from_tokens
139
+ @show_pieces.join ' '
140
+ end
141
+ end
142
+
143
+ class Renamer
144
+
145
+ def initialize(args)
146
+ @output_dir = '.'
147
+ @rename = true
148
+ i=0
149
+ while i < args.size
150
+ case args[i]
151
+ when "-i"
152
+ @ini_file = args[i+1]
153
+ if @ini_file.nil?
154
+ puts "You must enter the path to the shows.ini file"
155
+ exit
156
+ end
157
+ i += 1
158
+ when "--output-dir", "-d"
159
+ @output_dir = args[i+1]
160
+ if @output_dir.nil?
161
+ puts "You must enter a directory to renamer files to!"
162
+ exit 1
163
+ end
164
+ i += 1
165
+ when "--debug"
166
+ @debug = true
167
+ when "--no-rename", "-n"
168
+ @rename = false
169
+ when "--overwrite", "-o"
170
+ @overwrite = true
171
+ when "--verbose", "-v"
172
+ @verbose = true
173
+ end
174
+ i += 1
175
+ end
176
+
177
+ if(!@ini_file)
178
+ #check if windows or linux
179
+ if RUBY_PLATFORM['linux']
180
+ if ENV['XDG_CONFIG_HOME']
181
+ basedir = ENV['XDG_CONFIG_HOME']
182
+ else
183
+ if ENV['HOME']
184
+ basedir = File.join(ENV['HOME'], '.config')
185
+ else
186
+ puts '$XDG_CONFIG_HOME and $HOME unset, falling back to current directory'
187
+ basedir = '.'
188
+ end
189
+ end
190
+ else
191
+ basedir = ENV['HOMEDRIVE'] + ENV['HOMEPATH']
192
+ end
193
+
194
+ @ini_file = File.join(basedir, 'shows.ini')
195
+ end
196
+
197
+ begin
198
+ @ini = Ini.new(@ini_file, true)
199
+ rescue
200
+ puts "#{@ini_file} does not exist, no custom renaming available"
201
+ end
202
+ end
203
+
204
+ def run
205
+ files = Dir['*.{avi,wmv,divx,mpg,mpeg,xvid,mp4,mkv}'].sort
206
+
207
+ files.each do |file|
208
+ video = VideoFile.new(file)
209
+
210
+ video.rename_by_date = true if video.show && attribute('renamebydate', video.orig_show)
211
+
212
+ # parse the show into @show, @season, @episode_number, etc...
213
+ if !video.parsed_ok?
214
+ puts "I could not match #{file} to a naming pattern I can interpret"
215
+ next
216
+ end
217
+
218
+ rename(video)
219
+ end
220
+
221
+ # delete the cached results from epguides
222
+ Dir['*.renamer'].each do |filename|
223
+ File::delete(filename)
224
+ end
225
+
226
+ if @one_rename_failed
227
+ # if some renames succeeded, return 2
228
+ if @one_rename_succeeded
229
+ exit 2
230
+ # if no renames succeeded, return 1
231
+ else
232
+ exit 1
233
+ end
234
+ end
235
+ end
236
+
237
+ def set_attributes_from_epguides(video)
238
+ line = epguide_line(video)
239
+ return false if line.nil?
240
+
241
+ info = parse_line(line, video.format)
242
+ return false if info.nil?
243
+
244
+ season_episode = info[1]
245
+ video.production_code = info[2]
246
+ video.date = info[3]
247
+ episode_name = info[4]
248
+
249
+ if line.match("<li>")
250
+ video.episode_name = episode_name
251
+ else
252
+ doc = Nokogiri::HTML("<pre>#{line}</pre>")
253
+ links = doc.css('pre a')
254
+ if links.empty?
255
+ puts "Could not find episode name for #{video}"
256
+ else
257
+ video.episode_name = links[0].content
258
+ end
259
+ end
260
+
261
+ seasonmatch = season_episode.match(/(\d\d?)- ?(\d+)/)
262
+ video.season ||= seasonmatch[1]
263
+ video.episode_number ||= seasonmatch[2]
264
+ end
265
+
266
+ def rename(video)
267
+ return false unless set_attributes_from_epguides(video)
268
+
269
+ # if the ini exists, we set the showname to the custome name if it exists
270
+ if customname = attribute('customname', video.orig_show)
271
+ video.show = customname
272
+ end
273
+
274
+ # pad the episode with 0 if length is less than 0, we need to handle
275
+ # this better for 3+ digit episodes seasons
276
+ video.episode_number = sprintf("%02i", video.episode_number.to_i)
277
+
278
+ return false unless new_filename = generate_filename(video)
279
+
280
+ new_filename = [@output_dir, new_filename].join(File::Separator)
281
+
282
+ if !@rename or @verbose
283
+ puts "rename #{video.filename} to #{new_filename}"
284
+ end
285
+
286
+ if @rename
287
+ # if the file doesn't exist (or we allow overwrites), we rename it
288
+ if !File::exist?(new_filename) or @overwrite
289
+ begin
290
+ FileUtils.mv(video.filename, new_filename)
291
+ @one_rename_succeeded = true
292
+ puts "rename succeeded"
293
+ rescue Exception
294
+ @one_rename_failed = true
295
+ puts "rename failed"
296
+ end
297
+ # otherwise we don't overwrite it and just display a message
298
+ else
299
+ puts "Can't rename #{video.filename} to #{new_filename}!"
300
+ puts "#{video.filename} already exists!"
301
+ @one_rename_failed = true
302
+ end
303
+ end
304
+ end
305
+
306
+ def generate_filename(video)
307
+ # if the ini file exists, and they set a custom renaming mask,
308
+ # or a global one exists we need to do more custom renaming
309
+ if mask = attribute('mask', video.orig_show)
310
+ # set the filename to the mask as a base
311
+
312
+ filename = mask.dup
313
+
314
+ # substitute the patterns with the data we found
315
+ filename.gsub!("%show%", video.show)
316
+ filename.gsub!("%episode%", video.episode_name)
317
+ filename.gsub!("%season%", video.season)
318
+ filename.gsub!("%epnumber%", video.episode_number)
319
+
320
+ # if there is a custom date format, use that
321
+ # otherwise date is however epguides displays it
322
+ if !video.date.nil? && date_format = attribute('dateformat', video.orig_show)
323
+ video.date.insert(-3, "20")
324
+ video.date = Date.parse(video.date).strftime(date_format)
325
+ end
326
+
327
+ # TODO: we need to handle this better if date, code, etc.. don't exists
328
+ # right now they just end up as spaces
329
+ filename.gsub!("%date%", video.date)
330
+ filename.gsub!("%code%", video.production_code)
331
+ # if we don't have a shows.ini, or nothing specific is set, use the default pattern
332
+ else
333
+ filename = [video.show, video.season, video.episode_number, video.episode_name].join(' - ')
334
+ end
335
+
336
+ # add on the extension
337
+ filename = "#{filename}.#{video.extension}"
338
+
339
+ # replace html encoded characters
340
+ filename = CGI.unescapeHTML(filename)
341
+
342
+ # replace these illegal win32 characters with '-'
343
+ filename.gsub!(":", "-")
344
+ filename.gsub!("/", "-")
345
+
346
+ # just delete theses illegal win32 characters
347
+ filename.delete!("?\\/<>\"")
348
+
349
+ filename
350
+ end
351
+
352
+ def epguide_data(video, url)
353
+ if !File::exist?(url + ".renamer")
354
+ page = Net::HTTP.new('www.epguides.com')
355
+
356
+ response = page.get("/#{url}/")
357
+
358
+ case response
359
+ when Net::HTTPSuccess
360
+ data = response.body
361
+
362
+ File.open(url + ".renamer", "w") do |file|
363
+ file << data
364
+ end
365
+ else
366
+ if !@ini.nil?
367
+ puts "#{@ini_file} does not exist, please create this file and add an alias for #{video.show}"
368
+ return nil
369
+ end
370
+
371
+ unless @ini[video.show.downcase]
372
+ puts "Please add an alias of the epguide.com show url for \"#{video.show}\" to #{@ini_file}"
373
+ return nil
374
+ end
375
+
376
+ if @ini[video.show.downcase]["url"]
377
+ puts "The entry for \"#{video.show}\" has an invalid URL"
378
+ else
379
+ puts "The entry for \"#{video.show}\" needs a URL"
380
+ end
381
+
382
+ data = nil
383
+ end
384
+ else
385
+ File.open(url + ".renamer", "r") do |file|
386
+ data = file.read
387
+ end
388
+ end
389
+
390
+ return data
391
+ end
392
+
393
+ def attribute(attribute, show)
394
+ if show
395
+ attr = @ini[show.downcase][attribute] unless @ini.nil? or @ini[show.downcase].nil?
396
+ if attr
397
+ return attr
398
+ else
399
+ return attribute(attribute, nil)
400
+ end
401
+ else
402
+ return @ini[attribute] unless @ini.nil?
403
+ end
404
+ end
405
+
406
+ def matchstring(video, tvrage)
407
+ if video.rename_by_date && video.date
408
+ if !tvrage
409
+ video.date
410
+ else
411
+ video.date.gsub(' ', '/')
412
+ end
413
+ else
414
+ if !tvrage
415
+ matchstring = "#{video.season}-#{sprintf('%2s', video.episode_number)}"
416
+ else
417
+ matchstring = "#{video.season}-#{sprintf('%02i', video.episode_number.to_i)}"
418
+ end
419
+ end
420
+ end
421
+
422
+ # returns the line of html from epguides.com that contains the information for this episode
423
+ def epguide_line(video)
424
+ url = show_url(video)
425
+ data = epguide_data(video, url)
426
+
427
+ return nil if data.nil?
428
+
429
+ # default to TV.com pages
430
+ pattern = matchstring(video, false)
431
+
432
+ # get each line
433
+ lines = data.split(/\n|\r/)
434
+
435
+ # go through each until we find the show data
436
+ lines.each do |line|
437
+ if line =~ /((_+) )+/
438
+ video.format = line
439
+ elsif line.match(pattern)
440
+ return line
441
+ elsif line.match("this TVRage editor")
442
+ pattern = matchstring(video, true)
443
+ end
444
+ end
445
+
446
+ puts "Epguides does not have #{video.show} season: #{video.season} episode: #{video.episode_number} in its guides."
447
+ nil
448
+ end
449
+
450
+ def parse_line(line, format)
451
+ if format.nil?
452
+ if line.match("<li>")
453
+ format = "____ _______ ________ ___________ ______________________________________________"
454
+ else
455
+ format = "_____ ______ ___________ ___________ ___________________________________________"
456
+ end
457
+
458
+ puts "Couldn't find data format string from epguides page, assuming format like:"
459
+ puts format
460
+ end
461
+
462
+ # ensure format ends in exactly one space so loop gets all tokens
463
+ format = format.split(' ').join(' ') + ' '
464
+
465
+ tokens = []
466
+ prev_offset = -1
467
+ while offset = format.index(' ', prev_offset + 1) do
468
+ range = (prev_offset+1)..(offset - 1)
469
+ tokens.push line.slice(range).strip
470
+ prev_offset = offset
471
+ end
472
+
473
+ return tokens
474
+ end
475
+
476
+ def show_url(video)
477
+ # use the url specified in the ini, if set
478
+ url = attribute('url', video.show)
479
+
480
+ if url.nil?
481
+ url = video.show.split(' ').join
482
+
483
+ url = url[3, url.length] if url[0,3] == "The"
484
+ end
485
+
486
+ url
487
+ end
488
+
489
+ end # class Renamer
490
+
491
+ # Ini class - read and write ini files
492
+ # Copyright (C) 2007 Jeena Paradies
493
+ # License: GPL
494
+ # Author: Jeena Paradies (info@jeenaparadies.net)
495
+
496
+ class Ini
497
+
498
+ # :inihash is a hash which holds all ini data
499
+ # :comment is a string which holds the comments on the top of the file
500
+ attr_accessor :inihash, :comment
501
+
502
+ # Creating a new Ini object
503
+ # +path+ is a path to the ini file
504
+ # +load+ if nil restores the data if possible
505
+ # if true restores the data, if not possible raises an error
506
+ # if false does not resotre the data
507
+ def initialize(path, load=nil)
508
+ @path = path
509
+ @inihash = {}
510
+
511
+ if load or ( load.nil? and FileTest.readable_real? @path )
512
+ restore()
513
+ end
514
+ end
515
+
516
+ # Retrive the ini data for the key +key+
517
+ def [](key)
518
+ @inihash[key]
519
+ end
520
+
521
+ # Set the ini data for the key +key+
522
+ def []=(key, value)
523
+ raise TypeError, "String expected" unless key.is_a? String
524
+ raise TypeError, "String or Hash expected" unless value.is_a? String or value.is_a? Hash
525
+
526
+ @inihash[key] = value
527
+ end
528
+
529
+ # Restores the data from file into the object
530
+ def restore()
531
+ @inihash = Ini.read_from_file(@path)
532
+ @comment = Ini.read_comment_from_file(@path)
533
+ end
534
+
535
+ # Store data from the object in the file
536
+ def update()
537
+ Ini.write_to_file(@path, @inihash, @comment)
538
+ end
539
+
540
+ # Reading data from file
541
+ # +path+ is a path to the ini file
542
+ # returns a hash which represents the data from the file
543
+ def Ini.read_from_file(path)
544
+ inihash = {}
545
+ headline = nil
546
+
547
+
548
+ IO.foreach(path) do |line|
549
+
550
+ line = line.strip.split(/#/)[0]
551
+
552
+ # if line is nil, just go to the next one
553
+ next if line.nil?
554
+
555
+ # read it only if the line doesn't begin with a "=" and is long enough
556
+ unless line.length < 2 and line[0,1] == "="
557
+
558
+ # it's a headline if the line begins with a "[" and ends with a "]"
559
+ if line[0,1] == "[" and line[line.length - 1, line.length] == "]"
560
+
561
+ # get rid of the [] and unnecessary spaces
562
+ headline = line[1, line.length - 2 ].strip
563
+ inihash[headline] = {}
564
+ else
565
+
566
+ key, value = line.split(/=/, 2)
567
+
568
+ key = key.strip unless key.nil?
569
+ value = value.strip unless value.nil?
570
+
571
+ unless headline.nil?
572
+ inihash[headline][key] = value
573
+ else
574
+ inihash[key] = value unless key.nil?
575
+ end
576
+ end
577
+ end
578
+ end
579
+
580
+ inihash
581
+ end
582
+
583
+ # Reading comments from file
584
+ # +path+ is a path to the ini file
585
+ # Returns a string with comments from the beginning of the
586
+ # ini file.
587
+ def Ini.read_comment_from_file(path)
588
+ comment = ""
589
+
590
+ IO.foreach(path) do |line|
591
+ line.strip!
592
+ next if line.nil?
593
+
594
+ next unless line[0,1] == "#"
595
+
596
+ comment << "#{line[1, line.length ].strip}\n"
597
+ end
598
+
599
+ comment
600
+ end
601
+
602
+ # Writing a ini hash into a file
603
+ # +path+ is a path to the ini file
604
+ # +inihash+ is a hash representing the ini File. Default is a empty hash.
605
+ # +comment+ is a string with comments which appear on the
606
+ # top of the file. Each line will get a "#" before.
607
+ # Default is no comment.
608
+ def Ini.write_to_file(path, inihash={}, comment=nil)
609
+ raise TypeError, "String expected" unless comment.is_a? String or comment.nil?
610
+
611
+ raise TypeError, "Hash expected" unless inihash.is_a? Hash
612
+ File.open(path, "w") { |file|
613
+
614
+ unless comment.nil?
615
+ comment.each do |line|
616
+ file << "# #{line}"
617
+ end
618
+ end
619
+
620
+ file << Ini.to_s(inihash)
621
+ }
622
+ end
623
+
624
+ # Turn a hash (up to 2 levels deepness) into a ini string
625
+ # +inihash+ is a hash representing the ini File. Default is a empty hash.
626
+ # Returns a string in the ini file format.
627
+ def Ini.to_s(inihash={})
628
+ str = ""
629
+
630
+ inihash.each do |key, value|
631
+ if value.is_a? Hash
632
+ str << "[#{key.to_s}]\n"
633
+
634
+ value.each do |under_key, under_value|
635
+ str << "#{under_key.to_s}=#{under_value.to_s unless under_value.nil?}\n"
636
+ end
637
+
638
+ else
639
+ str << "#{key.to_s}=#{value.to_s unless value2.nil?}\n"
640
+ end
641
+ end
642
+
643
+ str
644
+ end
645
+
646
+ end # end Ini
data/lib/tv_renamer.rb ADDED
@@ -0,0 +1,2 @@
1
+ # require 'tv_renamer/ini'
2
+ require 'tv_renamer/renamer'