media-organizer 0.1.5 → 0.1.6
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.
- checksums.yaml +7 -0
- data/lib/filescanner.rb +97 -68
- data/lib/media-organizer.rb +2 -9
- data/lib/metadata.rb +19 -0
- data/lib/renamer.rb +188 -204
- data/lib/scrapers/image.rb +41 -42
- data/lib/scrapers/music.rb +96 -99
- metadata +16 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2b335d2088f9101cc6029772adee22c6700a5172
|
4
|
+
data.tar.gz: 04066471b4bc4c5a435ad382573caff3de4268a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f466fc9895d1ecfc1e42f66a194d5123210876ee04f7846c100d4a8d23f4bb65fc2e1d3160e1caa05642b57f8a715653f6710d56d8d1b2c3d35d4d92e815a5ef
|
7
|
+
data.tar.gz: b6f2cb8be7177a70e0304bd64240f03541cc886bd9ef44f8b57333f9785f616af3d95c23a8a0cef12da3991e6330016ffb08a6102671f6f2c964333342154034
|
data/lib/filescanner.rb
CHANGED
@@ -1,115 +1,144 @@
|
|
1
1
|
#
|
2
|
-
#filescanner.rb: defines the class Filescanner, which scans directory trees for media files
|
2
|
+
# filescanner.rb: defines the class Filescanner, which scans directory trees for media files
|
3
3
|
#
|
4
4
|
|
5
5
|
require 'scrapers/image.rb'
|
6
6
|
require 'scrapers/music.rb'
|
7
7
|
|
8
|
-
|
9
8
|
module MediaOrganizer
|
10
|
-
|
11
|
-
|
12
|
-
#FileScanner: scans a file system for images and/or music files, providing a list of file URIs.
|
13
|
-
#Output of FileScanner.open() returns a list of paths that can then be consumed by MediaOrganizer::Renamer.
|
14
|
-
#
|
15
|
-
#===Example
|
16
|
-
#
|
17
|
-
#Filescanner.open("/path/to/files/", {:image => true, :music => true})
|
18
|
-
#
|
19
|
-
#This will return a list of all music and image files contained in /path/to/files/ and it's
|
20
9
|
class Filescanner
|
21
|
-
|
22
|
-
|
23
|
-
|
10
|
+
include Image
|
11
|
+
include Music
|
24
12
|
|
25
13
|
attr_reader :root_nodes
|
26
14
|
attr_accessor :source_list
|
27
15
|
|
28
|
-
|
29
|
-
def initialize()
|
16
|
+
def initialize
|
30
17
|
@root_nodes = []
|
31
18
|
@source_list = []
|
32
19
|
end
|
33
20
|
|
34
21
|
#
|
35
|
-
#Filescanner.open(String, {}): scans directory tree for media files,
|
22
|
+
# Filescanner.open(String, {}): scans directory tree for media files,
|
23
|
+
# starting at String specified in first argument.
|
36
24
|
#
|
37
|
-
|
38
|
-
|
25
|
+
#==Inputs
|
26
|
+
#===Required
|
27
|
+
# (1) String: String containing the URI of the top of the directory tree to scan
|
39
28
|
#
|
40
|
-
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
29
|
+
#===Optional
|
30
|
+
# (2) Arguments Hash:
|
31
|
+
# *:mode => (:single) -- if set to :single, only the given URI will be scanned.
|
32
|
+
# Subdirectories will be ignored.
|
33
|
+
# *:music => (true, false) -- if true, music files will be included in the scan.
|
34
|
+
# Set to false to exclude music files. Defaults to true
|
35
|
+
# *:image => (true, false) -- if true, image files will be included in the scan.
|
36
|
+
# Set to false to exclude image files. Defaults to true
|
44
37
|
#
|
45
|
-
|
46
|
-
#Returns array of strings, where each string is a file URI for a music or image file.
|
38
|
+
#==Outputs
|
39
|
+
# Returns array of strings, where each string is a file URI for a music or image file.
|
47
40
|
#
|
48
41
|
#
|
49
|
-
|
50
|
-
#Filescanner.open("/absolute/path/for/top/of/directory/tree")
|
42
|
+
#==Usage Example
|
43
|
+
# Filescanner.open("/absolute/path/for/top/of/directory/tree")
|
51
44
|
#
|
52
|
-
def open(uri =
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
include_images = true unless args[:image] == false
|
58
|
-
include_music = true unless args[:music] == false
|
59
|
-
files = []
|
60
|
-
if args[:mode] == :single
|
61
|
-
files = Dir.glob("#{uri}/*")
|
62
|
-
else
|
63
|
-
files = Dir.glob("#{uri}/**/*")
|
64
|
-
end
|
65
|
-
|
66
|
-
#add all files found to @source_list, if they are music files
|
45
|
+
def open(uri = '', args = {})
|
46
|
+
include_images = true unless args[:image] == false
|
47
|
+
include_music = true unless args[:music] == false
|
48
|
+
|
49
|
+
files = load_files(uri, args[:mode])
|
67
50
|
files.each do |f|
|
68
|
-
if (Music.
|
51
|
+
if (Music.music?(f) && include_music) || (Image.image?(f) && include_images)
|
69
52
|
@source_list << f
|
70
53
|
end
|
71
54
|
end
|
72
55
|
|
73
56
|
return @source_list
|
74
57
|
|
75
|
-
rescue
|
76
|
-
puts e.message
|
77
|
-
puts e.backtrace.inspect
|
58
|
+
rescue StandardError => e
|
59
|
+
puts "Warning: #{e.message}"
|
78
60
|
return false
|
79
61
|
end
|
80
62
|
|
81
|
-
#
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
rescue FileNotFoundError => e
|
88
|
-
puts e.message
|
89
|
-
puts e.backtrace.inspect
|
63
|
+
# alternative run mode. Add multiple "root" directories to scan at once
|
64
|
+
def add_root(dir_uri)
|
65
|
+
validate_uri(dir_uri)
|
66
|
+
@root_nodes << dir_uri
|
67
|
+
rescue StandardError => e
|
68
|
+
puts "Warning: #{e.message}"
|
90
69
|
return false
|
91
70
|
end
|
92
71
|
|
93
|
-
#
|
72
|
+
# <<(): synonym for add_root(). Also a deformed emoticon.
|
73
|
+
# def << (dir_uri) add_root(dir_uri) end
|
74
|
+
|
75
|
+
#
|
76
|
+
# multiscan(): scans multiple directories added to @root_nodes using the add_root() method.
|
94
77
|
#
|
95
|
-
|
78
|
+
#==Inputs
|
79
|
+
#===Required
|
80
|
+
# none
|
96
81
|
#
|
97
|
-
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
82
|
+
#===Optional
|
83
|
+
# (1) Arguments Hash:
|
84
|
+
# *:mode => (:single, :multiple)
|
85
|
+
# *:music => (true, false) -- if true, music files will be included in the scan.
|
86
|
+
# Set to false to exclude music files. Defaults to true
|
87
|
+
# *:image => (true, false) -- if true, image files will be included in the scan.
|
88
|
+
# Set to false to exclude image files. Defaults to true
|
101
89
|
#
|
102
|
-
|
103
|
-
#
|
90
|
+
#==Outputs
|
91
|
+
# Array of strings, where each string is a file URI for a music or image file.
|
104
92
|
#
|
105
93
|
def multiscan(args = {})
|
106
94
|
@root_nodes.each do |uri|
|
107
95
|
open(uri, args)
|
108
96
|
end
|
109
|
-
|
97
|
+
@source_list
|
110
98
|
end
|
111
|
-
|
112
|
-
end
|
113
99
|
|
114
|
-
|
100
|
+
private
|
115
101
|
|
102
|
+
# validate_uri(): support method for validating a file system URI.
|
103
|
+
#
|
104
|
+
#==Inputs
|
105
|
+
#===Required
|
106
|
+
# (1) String: String containing the URI to validate.
|
107
|
+
#
|
108
|
+
#===Optional
|
109
|
+
# none
|
110
|
+
#==Outputs
|
111
|
+
# Returns true if the URI is valid. Raises a StandardError exception if it is not.
|
112
|
+
#
|
113
|
+
def validate_uri(uri)
|
114
|
+
unless !uri.nil? && uri.is_a?(String) && (File.directory?(uri) || File.exist?(uri))
|
115
|
+
raise StandardError, "Directory given (#{uri}) could not be accessed."
|
116
|
+
end
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# load_files(): given a URI and optional mode, scans and loads every file in
|
122
|
+
# the specifid folder or tree.
|
123
|
+
#
|
124
|
+
#==Inputs
|
125
|
+
#===Required
|
126
|
+
# (1) String: String containing the URI to validate.
|
127
|
+
#
|
128
|
+
#===Optional
|
129
|
+
# (1) Arguments Hash:
|
130
|
+
# *:mode => (:single, :multiple) - :single loads a folder, :multiple loads a full tree.
|
131
|
+
#
|
132
|
+
#==Outputs
|
133
|
+
# Array of strings, where each string is a file URI for a music or image file.
|
134
|
+
#
|
135
|
+
def load_files(uri, mode)
|
136
|
+
validate_uri(uri)
|
137
|
+
if !mode.nil? && mode == :single
|
138
|
+
Dir.glob("#{uri}/*")
|
139
|
+
else
|
140
|
+
Dir.glob("#{uri}/**/*")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/media-organizer.rb
CHANGED
@@ -1,15 +1,8 @@
|
|
1
|
-
#media-renamer.rb
|
1
|
+
# media-renamer.rb
|
2
2
|
|
3
3
|
require 'renamer.rb'
|
4
4
|
require 'filescanner.rb'
|
5
5
|
|
6
|
-
#MediaOrganizer: namespace container for MediaOrganizer sub-classes:
|
7
|
-
# *Renamer
|
8
|
-
# *Filescanner
|
9
|
-
# *Image
|
10
|
-
# *Music
|
11
6
|
module MediaOrganizer
|
12
|
-
|
13
|
-
|
7
|
+
VERSION = '0.1.5'.freeze
|
14
8
|
end
|
15
|
-
|
data/lib/metadata.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# metadata.rb: contains definition of the Metadata module, which provides
|
2
|
+
# a set of methods for analyzing metadata across multiple files, such as:
|
3
|
+
# *Metadata.completeness(arr, fields): for each metadata field provided in the "fields" argument, returns a completeness score.
|
4
|
+
# **E.g. {date_time: .95} means 95% of the files in arr have data in the "date_time" field
|
5
|
+
# *Metadata.fields(arr): returns the full list of fields available, aggregated across all files in arr
|
6
|
+
|
7
|
+
require 'scrapers/music.rb'
|
8
|
+
require 'scrapers/image.rb'
|
9
|
+
|
10
|
+
module Metadata
|
11
|
+
def self.completeness(arr = [], fields = [])
|
12
|
+
fields_count = {}
|
13
|
+
fields.each do |i|
|
14
|
+
fields_count[i] = 0
|
15
|
+
end
|
16
|
+
arr.each do |file|
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/renamer.rb
CHANGED
@@ -1,209 +1,193 @@
|
|
1
|
-
#renamer.rb: main codebase for the media-renamer gem.
|
2
|
-
#Currently configured to only rename JPG and TIF files (using EXIFR to extract metadata)
|
3
|
-
|
1
|
+
# renamer.rb: main codebase for the media-renamer gem.
|
2
|
+
# Currently configured to only rename JPG and TIF files (using EXIFR to extract metadata)
|
3
|
+
# next major release will include support
|
4
4
|
require 'scrapers/image.rb'
|
5
5
|
require 'scrapers/music.rb'
|
6
6
|
|
7
7
|
module MediaOrganizer
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
def setScheme(input_arr = [])
|
195
|
-
clean_scheme = []
|
196
|
-
input_arr.each do |i|
|
197
|
-
if i.is_a?(String) || i.is_a?(Symbol)
|
198
|
-
clean_scheme << i
|
199
|
-
end
|
200
|
-
end
|
201
|
-
return clean_scheme
|
202
|
-
end
|
203
|
-
|
204
|
-
def subHazardousChars(str = "")
|
205
|
-
return str.gsub(DISALLOWED_CHARACTERS, @subchar)
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
8
|
+
class FileNotValidError < StandardError; end
|
9
|
+
class InvalidArgumentError < StandardError; end
|
10
|
+
class UnsupportedFileTypeError < StandardError; end
|
11
|
+
class RenameFailedError < StandardError; end
|
12
|
+
|
13
|
+
# Renamer: primary class to use for renaming files. Allows renaming of a given list of files to a user-defined scheme based on each file's metadata.
|
14
|
+
#
|
15
|
+
#===Key Methods
|
16
|
+
# *naming_scheme()
|
17
|
+
# *generate()
|
18
|
+
# *overwrite()
|
19
|
+
#
|
20
|
+
#===Example Usage
|
21
|
+
#
|
22
|
+
# old_uris = ['./test/data/hs-2003-24-a-full_tif.tif']
|
23
|
+
#
|
24
|
+
# scheme = ["Test-", :date_time]
|
25
|
+
#
|
26
|
+
# r = Renamer.new()
|
27
|
+
#
|
28
|
+
# r.set_naming_scheme(scheme)
|
29
|
+
#
|
30
|
+
# new_uris = r.generate(old_uris)
|
31
|
+
#
|
32
|
+
# r.overwrite(new_uris) #new filename: "./test/data/Test-2003-09-03 12_52_43 -0400.tif")
|
33
|
+
#
|
34
|
+
class Renamer
|
35
|
+
DISALLOWED_CHARACTERS = /[\\:\?\*<>\|"\/]/ # Characters that are not allowed in file names by many file systems. Replaced with @subchar character.
|
36
|
+
|
37
|
+
attr_reader :naming_scheme # Array of strings and literals used to construct filenames. Set thruough naming_scheme as opposed to typical/default accessor.
|
38
|
+
attr_accessor :subchar # Character with which to substitute disallowed characters
|
39
|
+
|
40
|
+
def initialize(_args = {})
|
41
|
+
@naming_scheme = ['Renamed-default-']
|
42
|
+
@subchar = '_'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Renamer.naming_scheme(): sets the naming scheme for the generate method.
|
46
|
+
#
|
47
|
+
#===Inputs
|
48
|
+
# 1. Array containing strings and symbols.
|
49
|
+
#
|
50
|
+
#===Outputs
|
51
|
+
# None (sets instance variable @naming_scheme)
|
52
|
+
#
|
53
|
+
#===Example
|
54
|
+
# naming_scheme(["Vacation_Photos_", :date_taken]).
|
55
|
+
# This will rename files into a format like "Vacation_Photos_2014_05_22.png" based on the file's date_taken metadata field.
|
56
|
+
def set_naming_scheme(arr = [])
|
57
|
+
@naming_scheme = set_scheme(arr)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Renamer.generate(): Creates a hash mapping the original filenames to the new (renamed) filenames
|
61
|
+
#
|
62
|
+
#===Inputs
|
63
|
+
# 1. List of URIs in the form of an array
|
64
|
+
# 2. Optional hash of arguments.
|
65
|
+
# *:scheme - array of strings and symbols specifying file naming convention
|
66
|
+
#
|
67
|
+
#===Outputs
|
68
|
+
# Hash of "file name pairs." old_file => new_file
|
69
|
+
def generate(uri_list = [], args = {})
|
70
|
+
scheme = if !args[:scheme].nil? && args[:scheme].is_a?(Array) && !args[:scheme].empty?
|
71
|
+
set_scheme(args[:scheme])
|
72
|
+
else
|
73
|
+
@naming_scheme
|
74
|
+
end
|
75
|
+
raise InvalidArgumentError unless !uri_list.nil? && uri_list.is_a?(Array)
|
76
|
+
|
77
|
+
filename_pairs = {}
|
78
|
+
uri_list.each do |i|
|
79
|
+
new_string = handle_file(i, scheme)
|
80
|
+
# If this is a valid file path, add it to the filename_pairs
|
81
|
+
# puts "New file rename added: #{new_string}"
|
82
|
+
filename_pairs[i] = new_string if !new_string.nil? && new_string != ''
|
83
|
+
end
|
84
|
+
|
85
|
+
return filename_pairs
|
86
|
+
|
87
|
+
rescue InvalidArgumentError => arg_e
|
88
|
+
puts arg_e
|
89
|
+
puts 'Invalid arguments provided. Expected: uri_list = [], args = {}'
|
90
|
+
rescue => e
|
91
|
+
puts e.message
|
92
|
+
puts e.backtrace.inspect
|
93
|
+
end
|
94
|
+
|
95
|
+
# Renamer.overwrite(): Writes new file names based upon mapping provided in hash argument. NOTE: this will create changes to file names!
|
96
|
+
#
|
97
|
+
#===Inputs
|
98
|
+
# 1. Hash containing mappings between old filenames (full URI) and new filenames (full URI). Example: {"/path/to/oldfile.jpg" => "/path/to/newfile.jpg"}
|
99
|
+
#
|
100
|
+
#===Outputs
|
101
|
+
# none (file names are overwritten)
|
102
|
+
def overwrite(renames_hash = {})
|
103
|
+
renames_hash.each do |old_name, new_name|
|
104
|
+
begin
|
105
|
+
# error/integrity checking on old_name and new_name
|
106
|
+
raise FileNotValidError, "Could not access specified source file: #{i}." unless old_name.is_a?(String) && File.exist?(old_name)
|
107
|
+
raise FileNotValidError, 'New file name provided is not a string' unless new_name.is_a?(String)
|
108
|
+
|
109
|
+
# puts (File.dirname(File.absolute_path(old_name)) + "/" + new_name) #Comment this line out unless testing
|
110
|
+
File.rename(File.absolute_path(old_name), File.dirname(File.absolute_path(old_name)) + '/' + new_name)
|
111
|
+
|
112
|
+
# check that renamed File.exist? - Commented out because this currently does not work.
|
113
|
+
# unless new_name.is_a?(String) && File.exist?(new_name)
|
114
|
+
# raise RenameFailedError, "Could not successfuly rename file: #{old_name} => #{new_name}. Invalid URI or file does not exist."
|
115
|
+
# end
|
116
|
+
rescue => e
|
117
|
+
puts "Ignoring rename for #{old_name} => #{new_name}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Routes metadata scrape based on file type (currently relies on extension - future version should use MIME)
|
123
|
+
# currently assumes file was checked for validity in calling code.
|
124
|
+
#
|
125
|
+
#===Inputs
|
126
|
+
# String containing full file URI (path and filename)
|
127
|
+
#
|
128
|
+
#===Outputs
|
129
|
+
# Returns hash of metadata for file, or nil if none/error.
|
130
|
+
def get_metadata(file)
|
131
|
+
# LOAD EXIF DATA
|
132
|
+
case File.extname(file).downcase
|
133
|
+
when '.jpg'
|
134
|
+
Image.get_jpeg_data(file)
|
135
|
+
when '.tif'
|
136
|
+
Image.get_tiff_data(file)
|
137
|
+
when '.mp3', '.wav', '.flac', '.aiff', '.ogg', '.m4a', '.asf'
|
138
|
+
Music.get_music_data(file)
|
139
|
+
else
|
140
|
+
raise UnsupportedFileTypeError, "Error processing #{file}"
|
141
|
+
end
|
142
|
+
rescue UnsupportedFileTypeError => e
|
143
|
+
puts "Could not process file: Extension #{File.extname(file)} is not supported."
|
144
|
+
puts e.backtrace.inspect
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def handle_file(file, scheme)
|
150
|
+
# Check file is real
|
151
|
+
unless file.is_a?(String) && File.exist?(file)
|
152
|
+
raise FileNotValidError, "Could not access specified file file: #{file}."
|
153
|
+
end
|
154
|
+
metadata = get_metadata(File.absolute_path(file))
|
155
|
+
new_string = ''
|
156
|
+
scheme.each do |j|
|
157
|
+
if j.is_a?(String) then new_string += j
|
158
|
+
elsif j.is_a?(Symbol)
|
159
|
+
begin
|
160
|
+
raise EmptyMetadataError if metadata[j].nil?
|
161
|
+
new_string += metadata[j].to_s
|
162
|
+
rescue => e
|
163
|
+
puts "Could not get string for metadata tag provided in scheme: #{j} for file #{file}."
|
164
|
+
puts "Ignoring file #{file}"
|
165
|
+
return nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
return sub_hazardous_chars(new_string + File.extname(file))
|
170
|
+
|
171
|
+
rescue FileNotValidError => e
|
172
|
+
puts "Ignoring file #{file}"
|
173
|
+
puts e
|
174
|
+
return nil
|
175
|
+
rescue => e
|
176
|
+
puts e.message
|
177
|
+
puts e.backtrace.inspect
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_scheme(input_arr = [])
|
182
|
+
clean_scheme = []
|
183
|
+
input_arr.each do |i|
|
184
|
+
clean_scheme << i if i.is_a?(String) || i.is_a?(Symbol)
|
185
|
+
end
|
186
|
+
clean_scheme
|
187
|
+
end
|
188
|
+
|
189
|
+
def sub_hazardous_chars(str = '')
|
190
|
+
str.gsub(DISALLOWED_CHARACTERS, @subchar)
|
191
|
+
end
|
192
|
+
end
|
209
193
|
end
|
data/lib/scrapers/image.rb
CHANGED
@@ -1,46 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# image.rb: defines the class Image, which validates and loads
|
3
|
+
# image files, providing image metadata in a hash format.
|
4
|
+
#
|
5
|
+
|
1
6
|
require 'exifr'
|
2
7
|
|
3
8
|
module MediaOrganizer
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
puts e.message
|
41
|
-
puts e.backtrace.inspect
|
42
|
-
return false
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
9
|
+
module Image
|
10
|
+
SUPPORTED_FILETYPES = %w(.jpg .tif).freeze
|
11
|
+
|
12
|
+
def self.get_jpeg_data(file)
|
13
|
+
meta = EXIFR::JPEG.new(file)
|
14
|
+
meta.to_hash
|
15
|
+
# !!! Rescue from common file-related and exifr-related errors here
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.get_tiff_data(file)
|
19
|
+
meta = EXIFR::TIFF.new(file)
|
20
|
+
meta.to_hash
|
21
|
+
# !!! Rescue from common file-related and exifr-related errors here
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.supported_filetypes
|
25
|
+
SUPPORTED_FILETYPES
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.image?(uri)
|
29
|
+
unless !uri.nil? && uri.is_a?(String) && File.exist?(uri)
|
30
|
+
raise StandardError, "Directory given (#{uri}) could not be accessed."
|
31
|
+
end
|
32
|
+
|
33
|
+
if SUPPORTED_FILETYPES.include?(File.extname(uri).downcase)
|
34
|
+
return true
|
35
|
+
else
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
rescue FileNotFoundError => e
|
40
|
+
puts e.message
|
41
|
+
puts e.backtrace.inspect
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
end
|
46
45
|
end
|
data/lib/scrapers/music.rb
CHANGED
@@ -1,115 +1,112 @@
|
|
1
|
+
#
|
2
|
+
# music.rb: defines the scraper module Music, which validates and loads
|
3
|
+
# music files, providing image metadata in a hash format.
|
4
|
+
#
|
1
5
|
require 'taglib'
|
2
6
|
|
3
7
|
module MediaOrganizer
|
8
|
+
module Music
|
9
|
+
SUPPORTED_FILETYPES = %w(.mp3 .m4a .mp4 .flac .m4a .ogg .aiff .asf .wav).freeze
|
4
10
|
|
5
|
-
|
6
|
-
|
11
|
+
def self.get_music_data(file)
|
12
|
+
attributes = {}
|
13
|
+
TagLib::FileRef.open(file) do |fileref|
|
14
|
+
unless fileref.null?
|
15
|
+
# sign tags to local variables
|
16
|
+
tag = fileref.tag
|
17
|
+
properties = fileref.audio_properties
|
7
18
|
|
8
|
-
|
19
|
+
# load tags into attributes attribute
|
20
|
+
attributes[:title] = tag.title
|
21
|
+
attributes[:track] = tag.track
|
22
|
+
attributes[:genre] = tag.genre
|
23
|
+
attributes[:year] = tag.year
|
24
|
+
attributes[:album] = tag.album
|
25
|
+
attributes[:artist] = tag.artist
|
26
|
+
attributes[:comment] = tag.comment
|
9
27
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
#sign tags to local variables
|
15
|
-
tag = fileref.tag
|
16
|
-
properties = fileref.audio_properties
|
17
|
-
|
18
|
-
#load tags into attributes attribute
|
19
|
-
attributes[:title] = tag.title
|
20
|
-
attributes[:track] = tag.track
|
21
|
-
attributes[:genre] = tag.genre
|
22
|
-
attributes[:year] = tag.year
|
23
|
-
attributes[:album] = tag.album
|
24
|
-
attributes[:artist] = tag.artist
|
25
|
-
attributes[:comment] = tag.comment
|
26
|
-
|
27
|
-
attributes[:length] = properties.length
|
28
|
-
attributes[:bitrate] = properties.bitrate
|
29
|
-
attributes[:channels] = properties.channels
|
30
|
-
attributes[:sample_rate] = properties.sample_rate
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
34
|
-
return attributes
|
35
|
-
end
|
28
|
+
attributes[:length] = properties.length
|
29
|
+
attributes[:bitrate] = properties.bitrate
|
30
|
+
attributes[:channels] = properties.channels
|
31
|
+
attributes[:sample_rate] = properties.sample_rate
|
36
32
|
|
37
|
-
|
38
|
-
|
39
|
-
|
33
|
+
end
|
34
|
+
end
|
35
|
+
attributes
|
36
|
+
end
|
40
37
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
38
|
+
def self.supported_filetypes
|
39
|
+
reutrn SUPPORTED_FILETYPES
|
40
|
+
end
|
45
41
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
42
|
+
def self.music?(uri)
|
43
|
+
unless !uri.nil? && uri.is_a?(String) && File.exist?(uri)
|
44
|
+
raise StandardError, "Directory given (#{uri}) could not be accessed."
|
45
|
+
end
|
51
46
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
47
|
+
if SUPPORTED_FILETYPES.include?(File.extname(uri).downcase)
|
48
|
+
return true
|
49
|
+
else
|
50
|
+
return false
|
51
|
+
end
|
57
52
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
# *(2) args: optional arguments passed as hash. Set ":include_null" to false to only return populated metadata fields.
|
64
|
-
#
|
65
|
-
def Music.availableMetadata(filepath = "", args = {})
|
66
|
-
attrs = getMusicData(filepath)
|
53
|
+
rescue FileNotFoundError => e
|
54
|
+
puts e.message
|
55
|
+
puts e.backtrace.inspect
|
56
|
+
return false
|
57
|
+
end
|
67
58
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
59
|
+
#
|
60
|
+
# availableMetadata(file, args): returns list of fields available as metadata for the given file.
|
61
|
+
#
|
62
|
+
#===Inputs
|
63
|
+
# *(1) filepath: full/absolute URI of the file to be analyzed. Required input.
|
64
|
+
# *(2) args: optional arguments passed as hash. Set ":include_null" to false to only return populated metadata fields.
|
65
|
+
#
|
66
|
+
def self.availableMetadata(filepath = '', args = {})
|
67
|
+
attrs = get_music_data(filepath)
|
77
68
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
#===Outputs
|
86
|
-
#Returns true if the file was successfully saved. Note: true status does not necessarily indicate each field was successfully written.
|
87
|
-
#
|
88
|
-
#===Examples
|
89
|
-
#Music.writeMetadata("/absolute/path/to/file.mp3", {:artist => "NewArtistName", :year => "2019"})
|
90
|
-
#
|
91
|
-
def Music.writeMetadata(filepath, meta = {})
|
92
|
-
attributes = {}
|
93
|
-
successflag = false
|
94
|
-
TagLib::FileRef.open(filepath) do |fileref|
|
95
|
-
unless fileref.null?
|
96
|
-
#sign tags to local variables
|
97
|
-
tag = fileref.tag
|
98
|
-
properties = fileref.audio_properties
|
69
|
+
unless args[:include_null] == false
|
70
|
+
attrs.each do |field, value|
|
71
|
+
attrs.delete(field) if value.nil? || value == ''
|
72
|
+
end
|
73
|
+
end
|
74
|
+
attrs
|
75
|
+
end
|
99
76
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
77
|
+
#
|
78
|
+
# writeMetadata(file = "", meta = {}): returns list of fields available as metadata for the given file.
|
79
|
+
#
|
80
|
+
#===Inputs
|
81
|
+
# *(1) filepath: full/absolute URI of the file to be analyzed. Required input.
|
82
|
+
# *(2) meta: metadata to be written, passed as a hash in the format :metadata_field => metadata_value
|
83
|
+
#
|
84
|
+
#===Outputs
|
85
|
+
# Returns true if the file was successfully saved. Note: true status does not necessarily indicate each field was successfully written.
|
86
|
+
#
|
87
|
+
#===Examples
|
88
|
+
# Music.writeMetadata("/absolute/path/to/file.mp3", {:artist => "NewArtistName", :year => "2019"})
|
89
|
+
#
|
90
|
+
def self.writeMetadata(filepath, meta = {})
|
91
|
+
attributes = {}
|
92
|
+
successflag = false
|
93
|
+
TagLib::FileRef.open(filepath) do |fileref|
|
94
|
+
unless fileref.null?
|
95
|
+
# sign tags to local variables
|
96
|
+
tag = fileref.tag
|
97
|
+
properties = fileref.audio_properties
|
111
98
|
|
112
|
-
|
113
|
-
|
114
|
-
|
99
|
+
meta.each do |field, value|
|
100
|
+
if tag.respond_to?(field)
|
101
|
+
tag.send("#{field}=", value)
|
102
|
+
elsif properties.respond_to?(field)
|
103
|
+
properties.send("#{field}=", value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
successflag = fileref.save
|
107
|
+
end
|
108
|
+
end
|
109
|
+
successflag
|
110
|
+
end
|
111
|
+
end
|
115
112
|
end
|
metadata
CHANGED
@@ -1,55 +1,52 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: media-organizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
5
|
-
prerelease:
|
4
|
+
version: 0.1.6
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Stephen Johnson
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2016-03-28 00:00:00.000000000 Z
|
13
12
|
dependencies: []
|
14
|
-
description:
|
15
|
-
|
16
|
-
a customizable
|
17
|
-
|
18
|
-
Currently supports only JPEG and TIFF files. Future releases will include support
|
19
|
-
for music and additional image files.'
|
13
|
+
description: 'Dynamically renaming files using their metadata, according to a customizable
|
14
|
+
pattern. For example, use media-organizer to set filenames for a library of photos
|
15
|
+
to a customizable standard such as: "<date-taken> - Ski Vacation.jpg". Currently
|
16
|
+
supports only JPEG, TIFF, MP3, M4A, WAV, FLAC, and OGG files.'
|
20
17
|
email: djeserkare@gmail.com
|
21
18
|
executables: []
|
22
19
|
extensions: []
|
23
20
|
extra_rdoc_files: []
|
24
21
|
files:
|
25
|
-
- lib/renamer.rb
|
26
|
-
- lib/media-organizer.rb
|
27
22
|
- lib/filescanner.rb
|
28
|
-
- lib/
|
23
|
+
- lib/media-organizer.rb
|
24
|
+
- lib/metadata.rb
|
25
|
+
- lib/renamer.rb
|
29
26
|
- lib/scrapers/image.rb
|
27
|
+
- lib/scrapers/music.rb
|
30
28
|
homepage: http://rubygems.org/gems/media-organizer
|
31
29
|
licenses:
|
32
30
|
- MIT
|
31
|
+
metadata: {}
|
33
32
|
post_install_message:
|
34
33
|
rdoc_options: []
|
35
34
|
require_paths:
|
36
35
|
- lib
|
37
36
|
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
-
none: false
|
39
37
|
requirements:
|
40
|
-
- -
|
38
|
+
- - ">="
|
41
39
|
- !ruby/object:Gem::Version
|
42
40
|
version: '0'
|
43
41
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
-
none: false
|
45
42
|
requirements:
|
46
|
-
- -
|
43
|
+
- - ">="
|
47
44
|
- !ruby/object:Gem::Version
|
48
45
|
version: '0'
|
49
46
|
requirements: []
|
50
47
|
rubyforge_project:
|
51
|
-
rubygems_version:
|
48
|
+
rubygems_version: 2.4.5.1
|
52
49
|
signing_key:
|
53
|
-
specification_version:
|
54
|
-
summary:
|
50
|
+
specification_version: 4
|
51
|
+
summary: Automatically rename music and image files in bulk based on file metadata.
|
55
52
|
test_files: []
|