serienmover 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +9 -1
- data/bin/serienmover +207 -0
- data/lib/serienmover/episode.rb +39 -0
- data/lib/serienmover/series_store.rb +239 -0
- data/lib/serienmover/version.rb +1 -1
- data/lib/serienmover.rb +5 -2
- data/spec/episode_spec.rb +51 -0
- data/spec/serienmover_spec.rb +13 -7
- data/spec/series_store_spec.rb +110 -0
- data/spec/spec_helper.rb +87 -0
- data/spec/spec_testdata.rb +62 -0
- metadata +13 -3
data/Rakefile
CHANGED
data/bin/serienmover
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- ruby -*-
|
3
|
+
# encoding: UTF-8
|
4
|
+
|
5
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
6
|
+
|
7
|
+
require 'serienmover'
|
8
|
+
require 'serienrenamer'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'hashconfig'
|
11
|
+
require 'optparse'
|
12
|
+
require 'highline/import'
|
13
|
+
require "highline/system_extensions"
|
14
|
+
include HighLine::SystemExtensions
|
15
|
+
|
16
|
+
# create program configuration dirs/files
|
17
|
+
CONFIG_DIR = File.join( File.expand_path("~"), ".serienmover" )
|
18
|
+
CONFIG_FILE = File.join( CONFIG_DIR, "config.yml" )
|
19
|
+
FileUtils.mkdir(CONFIG_DIR) unless File.directory?(CONFIG_DIR)
|
20
|
+
|
21
|
+
###
|
22
|
+
# configuration
|
23
|
+
STANDARD_CONFIG = {
|
24
|
+
:default_directory => File.join(File.expand_path("~"), "Downloads"),
|
25
|
+
:series_directories => [],
|
26
|
+
:read_episode_info => false,
|
27
|
+
:store_path => '',
|
28
|
+
:byte_count_for_md5 => 2048,
|
29
|
+
:collective_directory => '',
|
30
|
+
}
|
31
|
+
|
32
|
+
config = STANDARD_CONFIG.merge_with_serialized(CONFIG_FILE)
|
33
|
+
|
34
|
+
###
|
35
|
+
# option definition and handling
|
36
|
+
options = {}
|
37
|
+
OptionParser.new do |opts|
|
38
|
+
opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [DIR]"
|
39
|
+
|
40
|
+
opts.separator("")
|
41
|
+
opts.separator("Tool that moves series episodes into a specific")
|
42
|
+
opts.separator("directory structure.")
|
43
|
+
opts.separator("")
|
44
|
+
opts.separator(" Options:")
|
45
|
+
|
46
|
+
opts.on( "-s", "--seriesdir=DIR", String,
|
47
|
+
"Directory that contains series data (multiple allowed)") do |dirs|
|
48
|
+
dirs = [ dirs ] if dirs.is_a? String
|
49
|
+
|
50
|
+
dirs.each do |d|
|
51
|
+
if File.directory?(d)
|
52
|
+
config[:series_directories] << d
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on( "-i", "--ignore-seriesinfo",
|
58
|
+
"do not use the information from the infostore") do |opt|
|
59
|
+
config[:read_episode_info] = false
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on( "-v", "--version",
|
63
|
+
"Outputs the version number.") do |opt|
|
64
|
+
puts Serienmover::VERSION
|
65
|
+
exit
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.separator("")
|
69
|
+
opts.separator(" Arguments:")
|
70
|
+
opts.separator(" DIR The path that includes the episodes")
|
71
|
+
opts.separator(" defaults to ~/Downloads")
|
72
|
+
opts.separator("")
|
73
|
+
|
74
|
+
end.parse!
|
75
|
+
|
76
|
+
###
|
77
|
+
# change into DIR
|
78
|
+
episode_directory = ARGV.pop || config[:default_directory]
|
79
|
+
|
80
|
+
fail "'#{episode_directory}' does not exist or is not a directory" unless
|
81
|
+
Dir.exists?(episode_directory)
|
82
|
+
|
83
|
+
Dir.chdir(episode_directory)
|
84
|
+
|
85
|
+
###
|
86
|
+
# instantiate the series_store
|
87
|
+
store = Serienmover::SeriesStore.new(config[:series_directories])
|
88
|
+
|
89
|
+
###
|
90
|
+
# instantiate information store
|
91
|
+
info_store = Serienrenamer::InformationStore.new(
|
92
|
+
config[:store_path], config[:byte_count_for_md5])
|
93
|
+
|
94
|
+
###
|
95
|
+
# iterate through all episode files
|
96
|
+
episode_actions = []
|
97
|
+
|
98
|
+
Dir.new('.').to_a.sort.each do |file|
|
99
|
+
|
100
|
+
next if file.match(/^\./)
|
101
|
+
next unless File.file? file
|
102
|
+
next unless Serienrenamer::Episode.determine_video_file(file)
|
103
|
+
|
104
|
+
p file
|
105
|
+
|
106
|
+
episode = Serienrenamer::Episode.new(file)
|
107
|
+
|
108
|
+
# get seriesname from the informationstore which is used by
|
109
|
+
# serienrenamer to store the seriesname when it renames files
|
110
|
+
md5 = episode.md5sum(config[:byte_count_for_md5])
|
111
|
+
series = info_store.episode_hash[md5]
|
112
|
+
|
113
|
+
options = {}
|
114
|
+
if config[:read_episode_info] && series && series.match(/\w+/)
|
115
|
+
options[:series] = series
|
116
|
+
end
|
117
|
+
|
118
|
+
targets = store.find_suitable_target(episode, options)
|
119
|
+
selected_target = nil
|
120
|
+
|
121
|
+
###
|
122
|
+
# process the targets
|
123
|
+
case targets.size
|
124
|
+
when 0
|
125
|
+
puts "No suitable target found\n"
|
126
|
+
next
|
127
|
+
when 1
|
128
|
+
selected_target = targets[0]
|
129
|
+
else
|
130
|
+
|
131
|
+
begin
|
132
|
+
puts "Available targets:"
|
133
|
+
choose do |menu|
|
134
|
+
menu.prompt = "Choose the right target: "
|
135
|
+
|
136
|
+
targets.each do |t|
|
137
|
+
menu.choice t.series do lambda { selected_target = t }.call end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
rescue Interrupt
|
141
|
+
puts ""
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
if selected_target
|
147
|
+
puts ">> '%s'" % selected_target
|
148
|
+
episode.target = selected_target
|
149
|
+
|
150
|
+
###
|
151
|
+
# ask for the action (copy/move)
|
152
|
+
print "What should be done ( [c]opy (*) , [m]ove ): "
|
153
|
+
char = get_character
|
154
|
+
print char.chr unless char.chr.match(/\r/)
|
155
|
+
|
156
|
+
unless char.chr.match(/[kcmv\r]/i)
|
157
|
+
puts "\nwill be skipped ...\n\n"
|
158
|
+
next
|
159
|
+
end
|
160
|
+
|
161
|
+
if char.chr.match(/[kc\r]/i)
|
162
|
+
episode.set_action(copy: true)
|
163
|
+
print " ... copy"
|
164
|
+
else
|
165
|
+
episode.set_action(move: true)
|
166
|
+
print " ... move"
|
167
|
+
end
|
168
|
+
|
169
|
+
###
|
170
|
+
# save the episode and set the target as used
|
171
|
+
store.set_target_to_used(episode, selected_target)
|
172
|
+
|
173
|
+
episode_actions << episode
|
174
|
+
end
|
175
|
+
|
176
|
+
puts "\n\n"
|
177
|
+
end
|
178
|
+
|
179
|
+
exit if episode_actions.empty?
|
180
|
+
|
181
|
+
####
|
182
|
+
# Process the actions on the episodes
|
183
|
+
print "Start processing the episodes ? [yJ]"
|
184
|
+
char = get_character
|
185
|
+
print char.chr
|
186
|
+
|
187
|
+
unless char.chr.match(/[jy\r]/i)
|
188
|
+
puts "\nwill exit ...\n\n"
|
189
|
+
exit
|
190
|
+
end
|
191
|
+
|
192
|
+
puts "\nEpisodes will be processed now"
|
193
|
+
episode_actions.each do |episode|
|
194
|
+
puts "%s '%s' to '%s'" % [episode.action.capitalize, episode, episode.target]
|
195
|
+
|
196
|
+
episode.process_action
|
197
|
+
|
198
|
+
# move copied file into a collective_directory if needed
|
199
|
+
if config[:collective_directory] &&
|
200
|
+
File.directory?(config[:collective_directory]) &&
|
201
|
+
episode.action.match(/copy/i)
|
202
|
+
|
203
|
+
remote_file = File.join(config[:collective_directory],
|
204
|
+
File.basename(episode.episodepath))
|
205
|
+
FileUtils.mv(episode.episodepath, remote_file)
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'serienrenamer'
|
2
|
+
|
3
|
+
class ::Serienrenamer::Episode
|
4
|
+
attr_accessor :target, :action
|
5
|
+
|
6
|
+
# Public: Sets the action that should be processed on this episode
|
7
|
+
#
|
8
|
+
# options - generale options (default: {copy: false, move: false})
|
9
|
+
# :copy - if true than the file will be copied (higher priority)
|
10
|
+
# :move - if true than the file wille be moved
|
11
|
+
def set_action(options = {})
|
12
|
+
opt = {move: false, copy: false}.merge(options)
|
13
|
+
|
14
|
+
if opt[:copy]
|
15
|
+
@action = 'copy'
|
16
|
+
elsif opt[:move]
|
17
|
+
@action = 'move'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Move/Copy the file to the target directory
|
22
|
+
def process_action
|
23
|
+
raise ArgumentError, "target needed" unless self.target
|
24
|
+
raise ArgumentError, "action needed" unless
|
25
|
+
self.action.match(/(move|copy)/i)
|
26
|
+
|
27
|
+
FileUtils.mkdir_p(self.target.targetdir) unless
|
28
|
+
File.directory?(self.target.targetdir)
|
29
|
+
|
30
|
+
remote_file = File.join(self.target.targetdir,
|
31
|
+
File.basename(self.episodepath))
|
32
|
+
|
33
|
+
if self.action.match(/copy/i)
|
34
|
+
FileUtils.cp(self.episodepath, self.target.targetdir)
|
35
|
+
elsif self.action.match(/move/i)
|
36
|
+
FileUtils.mv(self.episodepath, remote_file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'find'
|
2
|
+
require 'serienrenamer'
|
3
|
+
|
4
|
+
module Serienmover
|
5
|
+
|
6
|
+
# Public: holds information from the series directories and is able to
|
7
|
+
# search for possible targets for an supplied episode
|
8
|
+
class SeriesStore
|
9
|
+
|
10
|
+
attr_reader :series_data, :series_dirs
|
11
|
+
alias_method :series, :series_data
|
12
|
+
|
13
|
+
# Public: Initialize a SeriesStore
|
14
|
+
#
|
15
|
+
# series_directories - array of Strings that contains the
|
16
|
+
# series_directories
|
17
|
+
def initialize(series_directories)
|
18
|
+
|
19
|
+
@series_data = {}
|
20
|
+
@series_dirs = []
|
21
|
+
|
22
|
+
series_directories.each do |series_directory|
|
23
|
+
next unless File.directory? series_directory
|
24
|
+
@series_dirs << series_directory
|
25
|
+
|
26
|
+
Dir.new(series_directory).each do |series|
|
27
|
+
next if series.match(/^\.*$/)
|
28
|
+
|
29
|
+
seriesdir = File.join(series_directory, series)
|
30
|
+
|
31
|
+
episodes = {}
|
32
|
+
Find.find(seriesdir) do |file|
|
33
|
+
next unless File.file?(file)
|
34
|
+
|
35
|
+
infos = Serienrenamer::Episode.extract_episode_information(
|
36
|
+
File.basename(file))
|
37
|
+
|
38
|
+
if infos
|
39
|
+
index = SeriesStore.build_index(infos[:season], infos[:episode])
|
40
|
+
episodes[index] = file
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
series_data[series] = episodes
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: finds a suitable target in the store
|
50
|
+
#
|
51
|
+
# episode - Serienrenamer::Episode instance
|
52
|
+
# options - optional arguments (default: {):}
|
53
|
+
# :series - if this option is set than it returns only targets
|
54
|
+
# where the series matches the supplied arguments
|
55
|
+
#
|
56
|
+
# Returns an array of EpisodeTargets
|
57
|
+
def find_suitable_target(episode, options = {})
|
58
|
+
raise ArgumentError, "Serienrenamer::Episode needed" unless
|
59
|
+
episode.is_a? Serienrenamer::Episode
|
60
|
+
|
61
|
+
targets = []
|
62
|
+
current = SeriesStore.build_index(episode.season, episode.episode)
|
63
|
+
before = SeriesStore.build_index(episode.season, episode.episode-1)
|
64
|
+
|
65
|
+
series_data.each do |series, episodes|
|
66
|
+
|
67
|
+
# restrict series to the supplied seriesname and skip otherwise
|
68
|
+
if options.include?(:series) && options[:series].match(/\w+/)
|
69
|
+
next unless SeriesStore.does_match_series?(series, options[:series])
|
70
|
+
end
|
71
|
+
|
72
|
+
target_directory = nil
|
73
|
+
|
74
|
+
if episode.episode <= 1
|
75
|
+
# find possible targets for a new season
|
76
|
+
current_season = episodes.select do |e|
|
77
|
+
! e.match(/^#{episode.season.to_s}_/).nil?
|
78
|
+
end
|
79
|
+
before_season = episodes.select do |e|
|
80
|
+
! e.match(/^#{(episode.season-1).to_s}_/).nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
if current_season.empty? && ! before_season.empty?
|
84
|
+
season_before_dir = File.dirname(before_season.values[0])
|
85
|
+
|
86
|
+
dir_part = File.basename(season_before_dir)
|
87
|
+
dir_part.gsub!(/#{(episode.season-1).to_s }/, episode.season.to_s)
|
88
|
+
|
89
|
+
# let 'Staffel 010' be 'Staffel 10' as a side effect of
|
90
|
+
# the regex before
|
91
|
+
dir_part.gsub!(/010/, '10')
|
92
|
+
|
93
|
+
target_directory =
|
94
|
+
File.join( File.dirname(season_before_dir), dir_part )
|
95
|
+
end
|
96
|
+
|
97
|
+
elsif episodes.include?(before) && ! episodes.include?(current)
|
98
|
+
# look for the episode before the current which
|
99
|
+
# should be not existant and which hash no seasons afterwards
|
100
|
+
|
101
|
+
# check for seasons that follows the current
|
102
|
+
episodes_of_following_seasons =
|
103
|
+
episodes.select { |e| e.match(/#{(episode.season + 1).to_s}_/) }
|
104
|
+
|
105
|
+
if episodes_of_following_seasons.empty?
|
106
|
+
target_directory = File.dirname(episodes[before])
|
107
|
+
end
|
108
|
+
|
109
|
+
elsif ! episodes.include?(before) && ! episodes.include?(current)
|
110
|
+
# look for episodes that are released out of order so that
|
111
|
+
# S05E10 is released before S05E07, for which we are searching
|
112
|
+
# for a target
|
113
|
+
episode.episode.upto(50).each do |e|
|
114
|
+
index = SeriesStore.build_index(episode.season, e)
|
115
|
+
if episodes.include?(index)
|
116
|
+
target_directory = File.dirname(episodes[index])
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# build up target instance
|
122
|
+
if target_directory
|
123
|
+
target = EpisodeTarget.new(series, target_directory)
|
124
|
+
targets << target
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
targets
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Public: set the supplied target as used so that it is not
|
133
|
+
# available for future searches
|
134
|
+
#
|
135
|
+
# episode - Serienrenamer::Episode instcnace
|
136
|
+
# target - EpisodeTarget instance
|
137
|
+
#
|
138
|
+
# Returns nothing
|
139
|
+
def set_target_to_used(episode, target)
|
140
|
+
raise ArgumentError, "Serienrenamer::Episode needed" unless
|
141
|
+
episode.is_a? Serienrenamer::Episode
|
142
|
+
raise ArgumentError, "Serienmover::EpisodeTarget needed" unless
|
143
|
+
target.is_a? Serienmover::EpisodeTarget
|
144
|
+
|
145
|
+
index = SeriesStore.build_index(episode.season, episode.episode)
|
146
|
+
@series_data[target.series][index] =
|
147
|
+
File.join(target.targetdir, episode.to_s)
|
148
|
+
end
|
149
|
+
|
150
|
+
class << self
|
151
|
+
|
152
|
+
# Public: tries to match the suppplied seriesname pattern
|
153
|
+
# agains the series
|
154
|
+
#
|
155
|
+
# seriesname - the seriesname that comes from the series_directory
|
156
|
+
# series_pattern - the series_name that has to be checked
|
157
|
+
# agains the seriesname
|
158
|
+
#
|
159
|
+
# Returns true if it matches otherwise false
|
160
|
+
def does_match_series?(seriesname, series_pattern)
|
161
|
+
|
162
|
+
if seriesname.match(/#{series_pattern}/i)
|
163
|
+
# if pattern matches the series directly
|
164
|
+
return true
|
165
|
+
|
166
|
+
else
|
167
|
+
# start with a pattern that includes all words from
|
168
|
+
# series_pattern and if this does not match, it cuts
|
169
|
+
# off the first word and tries to match again
|
170
|
+
#
|
171
|
+
# if the pattern contains one word and if this
|
172
|
+
# still not match, the last word is splitted
|
173
|
+
# characterwise, so that:
|
174
|
+
# crmi ==> Criminal Minds
|
175
|
+
name_words = series_pattern.split(/ /)
|
176
|
+
word_splitted = false
|
177
|
+
|
178
|
+
while ! name_words.empty?
|
179
|
+
|
180
|
+
pattern = name_words.join('.*')
|
181
|
+
return true if seriesname.match(/#{pattern}/i)
|
182
|
+
|
183
|
+
# split characterwise if last word does not match
|
184
|
+
if name_words.length == 1 && ! word_splitted
|
185
|
+
name_words = pattern.split(//)
|
186
|
+
word_splitted = true
|
187
|
+
next
|
188
|
+
end
|
189
|
+
|
190
|
+
# if last word was splitted and does not match than break
|
191
|
+
# and return empty resultset
|
192
|
+
break if word_splitted
|
193
|
+
|
194
|
+
name_words.delete_at(0)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
false
|
199
|
+
end
|
200
|
+
|
201
|
+
# Public: builds up an index 'd_d' where the d's are the supplied args
|
202
|
+
#
|
203
|
+
# season - Number that is the first part of the 'd_d'
|
204
|
+
# season - Number that is the second part of 'd_d'
|
205
|
+
#
|
206
|
+
# Examples
|
207
|
+
#
|
208
|
+
# build_index('09', '12')
|
209
|
+
# # => '9_12'
|
210
|
+
#
|
211
|
+
# Returns the index
|
212
|
+
def build_index(season, episode)
|
213
|
+
return '%d_%d' % [ season.to_i, episode.to_i ]
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Public: holds information about suitable target directories
|
220
|
+
#
|
221
|
+
# Examples
|
222
|
+
#
|
223
|
+
# target = EpisodeTarget.new('Chuck', '/path/to/series')
|
224
|
+
# target.series
|
225
|
+
# # => "Chuck"
|
226
|
+
#
|
227
|
+
class EpisodeTarget
|
228
|
+
attr_accessor :series, :targetdir
|
229
|
+
|
230
|
+
def initialize(seriesname, targetdir)
|
231
|
+
@series = seriesname
|
232
|
+
@targetdir = targetdir
|
233
|
+
end
|
234
|
+
|
235
|
+
def to_s
|
236
|
+
@series
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
data/lib/serienmover/version.rb
CHANGED
data/lib/serienmover.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
require 'serienrenamer'
|
3
|
+
|
4
|
+
describe Serienrenamer::Episode do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@episodes = TestData.create
|
8
|
+
|
9
|
+
@store = Serienmover::SeriesStore.new([ TestHelper::SERIES_STORAGE_DIR ])
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
TestData.clean
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should copy the episode properly" do
|
17
|
+
episode = @episodes[:crmi]
|
18
|
+
|
19
|
+
results = @store.find_suitable_target(episode, series: "Criminal Minds")
|
20
|
+
results.size.should be == 1
|
21
|
+
results[0].series.should eq "Criminal Minds"
|
22
|
+
|
23
|
+
target = results[0]
|
24
|
+
episode.target = target
|
25
|
+
episode.set_action(copy: true)
|
26
|
+
episode.process_action
|
27
|
+
|
28
|
+
File.file?(episode.episodepath).should be_true
|
29
|
+
|
30
|
+
remote_path = File.join(target.targetdir, File.basename(episode.episodepath))
|
31
|
+
File.file?(remote_path).should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should move the episode properly" do
|
35
|
+
episode = @episodes[:crmi]
|
36
|
+
|
37
|
+
results = @store.find_suitable_target(episode, series: "Criminal Minds")
|
38
|
+
results.size.should be == 1
|
39
|
+
results[0].series.should eq "Criminal Minds"
|
40
|
+
|
41
|
+
target = results[0]
|
42
|
+
episode.target = target
|
43
|
+
episode.set_action(move: true)
|
44
|
+
episode.process_action
|
45
|
+
|
46
|
+
File.file?(episode.episodepath).should_not be_true
|
47
|
+
|
48
|
+
remote_path = File.join(target.targetdir, File.basename(episode.episodepath))
|
49
|
+
File.file?(remote_path).should be_true
|
50
|
+
end
|
51
|
+
end
|
data/spec/serienmover_spec.rb
CHANGED
@@ -1,17 +1,23 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
require File.dirname(__FILE__) + '/../lib/serienmover'
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
require 'serienrenamer'
|
4
3
|
|
5
4
|
describe Serienmover do
|
6
5
|
|
7
|
-
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@episodes = TestData.create
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:each) do
|
12
|
+
TestData.clean
|
8
13
|
end
|
9
14
|
|
10
|
-
|
15
|
+
it "should build up Episode instances successfully" do
|
16
|
+
@episodes[:tbbt].should_not be_nil
|
11
17
|
end
|
12
18
|
|
13
|
-
it "should
|
14
|
-
|
19
|
+
it "should extract the right information from the files" do
|
20
|
+
@episodes[:crmi].episode.should eq 4
|
15
21
|
end
|
16
22
|
end
|
17
23
|
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe Serienmover::SeriesStore do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@episodes = TestData.create
|
7
|
+
|
8
|
+
@store = Serienmover::SeriesStore.new([ TestHelper::SERIES_STORAGE_DIR ])
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:each) do
|
12
|
+
TestData.clean
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should be possible to instantiate the store" do
|
16
|
+
@store.should_not be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should process the series directories" do
|
20
|
+
@store.series_dirs.size > 0
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should build up a hash for series with all episodes" do
|
24
|
+
@store.series["Chuck"].size > 0
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return the right target for 'Chuck'" do
|
28
|
+
episode = @episodes[:chuck]
|
29
|
+
results = @store.find_suitable_target(episode)
|
30
|
+
results.select { |t| t.series == "Chuck" }.size.should be >= 1
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return the right target for 'Criminal Minds'" do
|
34
|
+
episode = @episodes[:crmi]
|
35
|
+
results = @store.find_suitable_target(episode)
|
36
|
+
results.select { |t| t.series == "Criminal Minds" }.size.should be >= 1
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should only return the Criminal Minds target if a name is supplied" do
|
40
|
+
episode = @episodes[:crmi]
|
41
|
+
|
42
|
+
results = @store.find_suitable_target(episode, series: "Criminal Minds")
|
43
|
+
results.size.should be == 1
|
44
|
+
results[0].series.should eq "Criminal Minds"
|
45
|
+
|
46
|
+
results = @store.find_suitable_target(episode, series: "crmi")
|
47
|
+
results.size.should be == 1
|
48
|
+
results[0].series.should eq "Criminal Minds"
|
49
|
+
|
50
|
+
results = @store.find_suitable_target(episode, series: "sof criminal minds")
|
51
|
+
results.size.should be == 1
|
52
|
+
results[0].series.should eq "Criminal Minds"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return the right target for 'The Big Bang Theory'" do
|
56
|
+
episode = @episodes[:tbbt]
|
57
|
+
results = @store.find_suitable_target(episode)
|
58
|
+
results.select { |t| t.series == "The Big Bang Theory" }.size.should be >= 1
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
it "should only return the Criminal Minds target if a name is supplied" do
|
63
|
+
episode = @episodes[:tbbt]
|
64
|
+
|
65
|
+
results = @store.find_suitable_target(episode, series: "sof tbbt")
|
66
|
+
results.size.should be == 1
|
67
|
+
results[0].series.should eq "The Big Bang Theory"
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return the right target for 'Dr House' (first epi of season)" do
|
71
|
+
episode = @episodes[:drhou]
|
72
|
+
results = @store.find_suitable_target(episode)
|
73
|
+
results.select { |t| t.series == "Dr House" }.size.should be >= 1
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should build up the right path for an episode of a new season" do
|
77
|
+
episode = @episodes[:drhou]
|
78
|
+
results = @store.find_suitable_target(episode, series: 'Dr House')
|
79
|
+
results.size.should == 1
|
80
|
+
|
81
|
+
results[0].targetdir.should match(/House.*Staffel.05/i)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should build up the right path for an episode S10E01" do
|
85
|
+
episode = @episodes[:spook]
|
86
|
+
results = @store.find_suitable_target(episode, series: 'Spooks')
|
87
|
+
results.size.should == 1
|
88
|
+
|
89
|
+
results[0].targetdir.should match(/Spooks.*Staffel.10/i)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should exclude season of series where more season exists" do
|
93
|
+
episode = @episodes[:numbe]
|
94
|
+
results = @store.find_suitable_target(episode)
|
95
|
+
results.size.should == 1
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should set target to used and skipped this target in future searches" do
|
99
|
+
episode = @episodes[:crmi]
|
100
|
+
results = @store.find_suitable_target(episode)
|
101
|
+
results.size.should be == 2
|
102
|
+
|
103
|
+
crmi_target = results.select { |t| t.series == "Criminal Minds" }[0]
|
104
|
+
@store.set_target_to_used(episode, crmi_target)
|
105
|
+
|
106
|
+
episode = @episodes[:seap]
|
107
|
+
less_results = @store.find_suitable_target(episode)
|
108
|
+
less_results.size.should be 1
|
109
|
+
end
|
110
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,92 @@
|
|
1
1
|
require 'bundler/setup'
|
2
2
|
require 'fileutils'
|
3
|
+
require 'rspec'
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + '/../lib/serienmover'
|
6
|
+
require File.join(File.dirname(__FILE__), "spec_testdata.rb")
|
3
7
|
|
4
8
|
RSpec.configure do |config|
|
5
9
|
end
|
10
|
+
|
11
|
+
class TestHelper
|
12
|
+
|
13
|
+
TESTFILE_DIRECTORY = File.join(File.dirname(__FILE__), 'testfiles')
|
14
|
+
SERIES_STORAGE_DIR = File.join(TESTFILE_DIRECTORY, 'series')
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
# create the supplied Files in the testfiles directory
|
19
|
+
def create_test_files(files)
|
20
|
+
_create_directories
|
21
|
+
|
22
|
+
files.each do |f|
|
23
|
+
FileUtils.touch File.join(TESTFILE_DIRECTORY, f)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# this method creates a directory structure for a given
|
28
|
+
# series with the opportunity to exclude some episodes
|
29
|
+
def create_series(seriesname, options={})
|
30
|
+
default = { :from => '1_0', :to => '1_0',
|
31
|
+
:max => 30, :exclude => [] }
|
32
|
+
options = default.merge(options)
|
33
|
+
|
34
|
+
series_dir = File.join(SERIES_STORAGE_DIR, seriesname)
|
35
|
+
_create_dir(series_dir)
|
36
|
+
|
37
|
+
from = _split_entry(options[:from])
|
38
|
+
to = _split_entry(options[:to])
|
39
|
+
|
40
|
+
for season in from[0]..to[0]
|
41
|
+
|
42
|
+
season_dir = File.join(series_dir, "Staffel %02d" % season)
|
43
|
+
_create_dir(season_dir)
|
44
|
+
|
45
|
+
episodes = (season == to[0]) ? to[1] : options[:max]
|
46
|
+
|
47
|
+
for episode in 1..episodes.to_i
|
48
|
+
|
49
|
+
# check for excludes
|
50
|
+
definition = "%d_%d" % [ season, episode ]
|
51
|
+
next if options[:exclude].include? definition
|
52
|
+
|
53
|
+
# build and create file
|
54
|
+
file = "S%02dE%02d - Epi%02d.avi" % [ season, episode,episode ]
|
55
|
+
episode_file = File.join(season_dir, file)
|
56
|
+
_create_file(episode_file)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# returns the absolute path to the given file
|
64
|
+
def path(element)
|
65
|
+
File.absolute_path(File.join(TESTFILE_DIRECTORY, element))
|
66
|
+
end
|
67
|
+
|
68
|
+
# remove testfile directory
|
69
|
+
def clean
|
70
|
+
if File.directory?(TESTFILE_DIRECTORY)
|
71
|
+
FileUtils.remove_dir(TESTFILE_DIRECTORY)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def _split_entry(definition)
|
76
|
+
definition.split(/_/)
|
77
|
+
end
|
78
|
+
|
79
|
+
def _create_directories
|
80
|
+
_create_dir TESTFILE_DIRECTORY
|
81
|
+
_create_dir SERIES_STORAGE_DIR
|
82
|
+
end
|
83
|
+
|
84
|
+
def _create_dir(dir)
|
85
|
+
FileUtils.mkdir(dir) unless File.directory?(dir)
|
86
|
+
end
|
87
|
+
|
88
|
+
def _create_file(file)
|
89
|
+
FileUtils.touch(file) unless File.file?(file)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
class TestData
|
4
|
+
|
5
|
+
EPISODES = {
|
6
|
+
:chuck => { :filename => "S04E11 - Pilot.avi",
|
7
|
+
:series => "Chuck",
|
8
|
+
:data => { :from => '1_1', :to => '4_10' }
|
9
|
+
},
|
10
|
+
:tbbt => { :filename => "S05E06 - Pilot.avi",
|
11
|
+
:series => "The Big Bang Theory",
|
12
|
+
:data => { :from => '1_1', :to => '5_9',
|
13
|
+
:exclude => ['5_5', '5_6', '5_7', '5_8', ] }
|
14
|
+
},
|
15
|
+
:crmi => { :filename => "S01E04 - Pilot.avi",
|
16
|
+
:series => "Criminal Minds",
|
17
|
+
:data => { :from => '1_1', :to => '1_3' }
|
18
|
+
},
|
19
|
+
:seap => { :filename => "S01E04 - Pilot.avi",
|
20
|
+
:series => "Sea Patrol",
|
21
|
+
:data => { :from => '1_1', :to => '1_3' }
|
22
|
+
},
|
23
|
+
:drhou => { :filename => "S05E01 - First Episode.avi",
|
24
|
+
:series => "Dr House",
|
25
|
+
:data => { :from => '1_1', :to => '4_20' }
|
26
|
+
},
|
27
|
+
:spook => { :filename => "S10E01 - First Episode.avi",
|
28
|
+
:series => "Spooks",
|
29
|
+
:data => { :from => '1_1', :to => '9_20' }
|
30
|
+
},
|
31
|
+
:numbe => { :filename => "S04E31 - High Episode.avi",
|
32
|
+
:series => "Numb3rs",
|
33
|
+
:data => { :from => '1_1', :to => '4_30' }
|
34
|
+
},
|
35
|
+
}
|
36
|
+
|
37
|
+
# create test data
|
38
|
+
def self.create
|
39
|
+
episodes = Hash.new
|
40
|
+
|
41
|
+
EPISODES.each do |key,value|
|
42
|
+
TestHelper.create_test_files([ value[:filename] ])
|
43
|
+
TestHelper.create_series(value[:series], value[:data])
|
44
|
+
|
45
|
+
path = TestHelper.path(value[:filename])
|
46
|
+
episode = Serienrenamer::Episode.new(path)
|
47
|
+
|
48
|
+
episodes[key] = episode
|
49
|
+
end
|
50
|
+
return episodes
|
51
|
+
end
|
52
|
+
|
53
|
+
# remove files
|
54
|
+
def self.clean
|
55
|
+
TestHelper.clean
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.get(key)
|
59
|
+
EPISODES[key]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serienmover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-04-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: serienrenamer
|
@@ -30,7 +30,8 @@ dependencies:
|
|
30
30
|
description: Tool that moves your episodes into a specific directory structure
|
31
31
|
email:
|
32
32
|
- philipp@i77i.de
|
33
|
-
executables:
|
33
|
+
executables:
|
34
|
+
- serienmover
|
34
35
|
extensions: []
|
35
36
|
extra_rdoc_files: []
|
36
37
|
files:
|
@@ -40,11 +41,17 @@ files:
|
|
40
41
|
- LICENSE
|
41
42
|
- README.md
|
42
43
|
- Rakefile
|
44
|
+
- bin/serienmover
|
43
45
|
- lib/serienmover.rb
|
46
|
+
- lib/serienmover/episode.rb
|
47
|
+
- lib/serienmover/series_store.rb
|
44
48
|
- lib/serienmover/version.rb
|
45
49
|
- serienmover.gemspec
|
50
|
+
- spec/episode_spec.rb
|
46
51
|
- spec/serienmover_spec.rb
|
52
|
+
- spec/series_store_spec.rb
|
47
53
|
- spec/spec_helper.rb
|
54
|
+
- spec/spec_testdata.rb
|
48
55
|
homepage: http://github.com/pboehm/serienmover
|
49
56
|
licenses: []
|
50
57
|
post_install_message:
|
@@ -70,5 +77,8 @@ signing_key:
|
|
70
77
|
specification_version: 3
|
71
78
|
summary: Tool that moves your episodes into a specific directory structure
|
72
79
|
test_files:
|
80
|
+
- spec/episode_spec.rb
|
73
81
|
- spec/serienmover_spec.rb
|
82
|
+
- spec/series_store_spec.rb
|
74
83
|
- spec/spec_helper.rb
|
84
|
+
- spec/spec_testdata.rb
|