serienmover 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|