media-organizer 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/lib/filescanner.rb +112 -0
- data/lib/media-organizer.rb +9 -0
- data/lib/renamer.rb +158 -0
- data/lib/scrapers/image.rb +41 -0
- data/lib/scrapers/music.rb +59 -0
- metadata +86 -0
data/lib/filescanner.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
#
|
2
|
+
#filescanner.rb: defines the class Filescanner, which scans directory trees for media files
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'scrapers/image.rb'
|
6
|
+
require 'scrapers/music.rb'
|
7
|
+
|
8
|
+
class FileNotValidError < StandardError ; end
|
9
|
+
|
10
|
+
class Filescanner
|
11
|
+
include Image
|
12
|
+
include Music
|
13
|
+
|
14
|
+
attr_reader :root_nodes
|
15
|
+
attr_accessor :source_list
|
16
|
+
|
17
|
+
|
18
|
+
def initialize()
|
19
|
+
@root_nodes = []
|
20
|
+
@source_list = []
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
#Filescanner.open(String, {}): scans directory tree for media files, starting at String specified in first argument.
|
25
|
+
#
|
26
|
+
#==Inputs
|
27
|
+
#===Required
|
28
|
+
#(1) String: String containing the URI of the top of the directory tree to scan
|
29
|
+
#
|
30
|
+
#===Optional
|
31
|
+
#(2) Arguments Hash:
|
32
|
+
#*:mode => (:single) -- if set to :single, only the given URI will be scanned. Subdirectories will be ignored.
|
33
|
+
#*:music => (true, false) -- if true, music files will be included in the scan. Set to false to exclude music files. Defaults to true
|
34
|
+
#*:image => (true, false) -- if true, image files will be included in the scan. Set to false to exclude image files. Defaults to true
|
35
|
+
#
|
36
|
+
#==Outputs
|
37
|
+
#Returns array of strings, where each string is a file URI for a music or image file.
|
38
|
+
#
|
39
|
+
#
|
40
|
+
#==Usage Example
|
41
|
+
#Filescanner.open("/absolute/path/for/top/of/directory/tree")
|
42
|
+
#
|
43
|
+
def open(uri = "", args = {})
|
44
|
+
unless !uri.nil? && uri.is_a?(String) && (File.directory?(uri) || File.exists?(uri))
|
45
|
+
raise FileNotFoundError, "Directory given (#{uri}) could not be accessed."
|
46
|
+
end
|
47
|
+
|
48
|
+
include_images = true unless args[:image] == false
|
49
|
+
include_music = true unless args[:music] == false
|
50
|
+
files = []
|
51
|
+
if args[:mode] == :single
|
52
|
+
files = Dir.glob("#{uri}/*")
|
53
|
+
else
|
54
|
+
files = Dir.glob("#{uri}/**/*")
|
55
|
+
end
|
56
|
+
|
57
|
+
#add all files found to @source_list, if they are music files
|
58
|
+
files.each do |f|
|
59
|
+
if (Music.is_music?(f) && include_music) || (Image.is_image?(f) && include_images)
|
60
|
+
@source_list << f
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
return @source_list
|
65
|
+
|
66
|
+
rescue FileNotFoundError => e
|
67
|
+
puts e.message
|
68
|
+
puts e.backtrace.inspect
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
#alternative run mode. Add multiple "root" directories to scan at once
|
73
|
+
def addRoot(dir_uri)
|
74
|
+
unless !dir_uri.nil? && dir_uri.is_a?(String) && File.directory?(dir_uri)
|
75
|
+
raise FileNotFoundError, "Directory given (#{dir_uri}) could not be accessed."
|
76
|
+
end
|
77
|
+
@root_nodes << dir_uri
|
78
|
+
rescue FileNotFoundError => e
|
79
|
+
puts e.message
|
80
|
+
puts e.backtrace.inspect
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
|
84
|
+
#<<(): synonym for addRoot(). Also a deformed emoticon.
|
85
|
+
# def << (dir_uri) addRoot(dir_uri) end
|
86
|
+
|
87
|
+
#
|
88
|
+
#multiscan(): scans multiple directories added to @root_nodes using the addRoot() method.
|
89
|
+
#
|
90
|
+
#==Inputs
|
91
|
+
#===Required
|
92
|
+
#none
|
93
|
+
#
|
94
|
+
#===Optional
|
95
|
+
#(1) Arguments Hash:
|
96
|
+
#*:mode => (:single, :multiple)
|
97
|
+
#*:music => (true, false) -- if true, music files will be included in the scan. Set to false to exclude music files. Defaults to true
|
98
|
+
#*:image => (true, false) -- if true, image files will be included in the scan. Set to false to exclude image files. Defaults to true
|
99
|
+
#
|
100
|
+
#==Outputs
|
101
|
+
#Array of strings, where each string is a file URI for a music or image file.
|
102
|
+
#
|
103
|
+
def multiscan(args = {})
|
104
|
+
@root_nodes.each do |uri|
|
105
|
+
open(uri, args)
|
106
|
+
end
|
107
|
+
return @source_list
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
data/lib/renamer.rb
ADDED
@@ -0,0 +1,158 @@
|
|
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
|
+
|
5
|
+
|
6
|
+
require 'scrapers/image.rb'
|
7
|
+
require 'scrapers/music.rb'
|
8
|
+
|
9
|
+
class FileNotValidError < StandardError ; end
|
10
|
+
class InvalidArgumentError < StandardError ; end
|
11
|
+
class UnsupportedFileTypeError < StandardError ; end
|
12
|
+
class RenameFailedError < StandardError ; end
|
13
|
+
|
14
|
+
|
15
|
+
class Renamer
|
16
|
+
include Image
|
17
|
+
include Music
|
18
|
+
attr_accessor :naming_scheme # => array of strings and literals used to construct filenames
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@naming_scheme = ["Renamed-default-"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def setNamingScheme(arr = [])
|
25
|
+
@naming_scheme = setScheme(arr)
|
26
|
+
end
|
27
|
+
#Input: list of URIs in the form of an array
|
28
|
+
#Output: hash of "file name pairs." old_file => new_file
|
29
|
+
#Accepts optional arguments: :scheme (array of strings and symbols specifying file naming convention)
|
30
|
+
def generateRenameList(uri_list = [], args = {})
|
31
|
+
if args[:scheme] != nil && args[:scheme].is_a?(Array) && !args[:scheme].empty?
|
32
|
+
scheme = setScheme(args[:scheme])
|
33
|
+
else
|
34
|
+
scheme = @naming_scheme
|
35
|
+
end
|
36
|
+
unless !uri_list.nil? && uri_list.is_a?(Array)
|
37
|
+
raise InvalidArgumentError
|
38
|
+
end
|
39
|
+
|
40
|
+
filename_pairs = {}
|
41
|
+
uri_list.each do |i|
|
42
|
+
new_string = handleFile(i, scheme)
|
43
|
+
#If this is a valid file path, add it to the filename_pairs
|
44
|
+
#puts "New file rename added: #{new_string}"
|
45
|
+
if new_string != nil && new_string != ""
|
46
|
+
filename_pairs[i] = new_string
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
return filename_pairs
|
51
|
+
|
52
|
+
rescue InvalidArgumentError => arg_e
|
53
|
+
puts arg_e
|
54
|
+
puts "Invalid arguments provided. Expected: uri_list = [], args = {}"
|
55
|
+
puts arg_e.backtrace.inspect
|
56
|
+
rescue => e
|
57
|
+
puts e
|
58
|
+
puts e.message
|
59
|
+
puts e.backtrace.inspect
|
60
|
+
end
|
61
|
+
|
62
|
+
def overwrite(renames_hash = {})
|
63
|
+
renames_hash.each do |old_name, new_name|
|
64
|
+
begin
|
65
|
+
#error/integrity checking on old_name and new_name
|
66
|
+
raise FileNotValidError, "Could not access specified source file: #{i}." unless old_name.is_a?(String) && File.exists?(old_name)
|
67
|
+
raise FileNotValidError, "New file name provided is not a string" unless new_name.is_a?(String)
|
68
|
+
|
69
|
+
#puts (File.dirname(File.absolute_path(old_name)) + "/" + new_name) #Comment this line out unless testing
|
70
|
+
File.rename(File.absolute_path(old_name),File.dirname(File.absolute_path(old_name)) + "/" + new_name)
|
71
|
+
|
72
|
+
#check that renamed file exists - Commented out because this currently does not work.
|
73
|
+
#unless new_name.is_a?(String) && File.exists?(new_name)
|
74
|
+
# raise RenameFailedError, "Could not successfuly rename file: #{old_name} => #{new_name}. Invalid URI or file does not exist."
|
75
|
+
#end
|
76
|
+
rescue => e
|
77
|
+
puts "Ignoring rename for #{old_name} => #{new_name}"
|
78
|
+
puts e
|
79
|
+
puts e.backtrace.inspect
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#Routes metadata scrape based on file type (currently relies on extension - future version should use MIME)
|
85
|
+
#currently assumes file was checked for validity in calling code
|
86
|
+
def getFileMetadata(file)
|
87
|
+
|
88
|
+
#LOAD EXIF DATA
|
89
|
+
case File.extname(file).downcase
|
90
|
+
when '.jpg'
|
91
|
+
Image::getJpegData(file)
|
92
|
+
when '.tif'
|
93
|
+
Image::getTiffData(file)
|
94
|
+
when '.mp3' , '.wav' , '.flac' , '.aiff', '.ogg', '.m4a', '.asf'
|
95
|
+
Music::getMusicData(file)
|
96
|
+
else
|
97
|
+
raise UnsupportedFileTypeError, "Error processing #{file}"
|
98
|
+
end
|
99
|
+
#otherwise, outsource
|
100
|
+
rescue UnsupportedFileTypeError => e
|
101
|
+
puts "Could not process file: Extension #{File.extname(file)} is not supported."
|
102
|
+
puts e.backtrace.inspect
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
private
|
107
|
+
def handleFile(file, scheme)
|
108
|
+
#Check file is real
|
109
|
+
unless file.is_a?(String) && File.exists?(file)
|
110
|
+
raise FileNotValidError, "Could not access specified file file: #{file}."
|
111
|
+
end
|
112
|
+
#convert URI (i) to absolute path
|
113
|
+
|
114
|
+
#get metadata hash for this file (i)
|
115
|
+
metadata = getFileMetadata(File.absolute_path(file))
|
116
|
+
#build URI string
|
117
|
+
new_string = ""
|
118
|
+
scheme.each do |j|
|
119
|
+
if j.is_a?(String) then new_string += j
|
120
|
+
elsif j.is_a?(Symbol)
|
121
|
+
begin
|
122
|
+
raise EmptyMetadataError unless metadata[j] != nil
|
123
|
+
new_string += metadata[j].to_s
|
124
|
+
rescue => e
|
125
|
+
puts "Could not get string for metadata tag provided in scheme: #{j} for file #{file}."
|
126
|
+
puts "Ignoring file #{file}"
|
127
|
+
puts e.backtrace.inspect
|
128
|
+
return nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
#puts "Found file metadata: #{metadata[:date_time]}"
|
133
|
+
return new_string + File.extname(file)
|
134
|
+
rescue FileNotValidError => e
|
135
|
+
puts ("Ignoring file #{file}")
|
136
|
+
puts e
|
137
|
+
puts e.backtrace
|
138
|
+
return nil
|
139
|
+
rescue => e
|
140
|
+
puts e.message
|
141
|
+
puts e.backtrace.inspect
|
142
|
+
return nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def setScheme(input_arr = [])
|
146
|
+
clean_scheme = []
|
147
|
+
input_arr.each do |i|
|
148
|
+
if i.is_a?(String) || i.is_a?(Symbol)
|
149
|
+
clean_scheme << i
|
150
|
+
end
|
151
|
+
end
|
152
|
+
return clean_scheme
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'exifr'
|
2
|
+
|
3
|
+
class FileNotFoundError < StandardError ; end
|
4
|
+
|
5
|
+
module Image
|
6
|
+
SUPPORTED_FILETYPES = %w{.jpg .tif}
|
7
|
+
|
8
|
+
def Image.getJpegData(file)
|
9
|
+
meta = EXIFR::JPEG.new(file)
|
10
|
+
return meta.to_hash
|
11
|
+
#!!! Rescue from common file-related and exifr-related errors here
|
12
|
+
end
|
13
|
+
|
14
|
+
def Image.getTiffData(file)
|
15
|
+
meta = EXIFR::TIFF.new(file)
|
16
|
+
return meta.to_hash
|
17
|
+
#!!! Rescue from common file-related and exifr-related errors here
|
18
|
+
end
|
19
|
+
|
20
|
+
def Image.supported_filetypes
|
21
|
+
return SUPPORTED_FILETYPES
|
22
|
+
end
|
23
|
+
|
24
|
+
def Image.is_image?(uri)
|
25
|
+
unless !uri.nil? && uri.is_a?(String) && File.exists?(uri)
|
26
|
+
raise FileNotFoundError, "Directory given (#{uri}) could not be accessed."
|
27
|
+
end
|
28
|
+
|
29
|
+
if SUPPORTED_FILETYPES.include?(File.extname(uri).downcase)
|
30
|
+
return true
|
31
|
+
else
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
rescue FileNotFoundError => e
|
36
|
+
puts e.message
|
37
|
+
puts e.backtrace.inspect
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'taglib'
|
2
|
+
|
3
|
+
class FileNotFoundError < StandardError ; end
|
4
|
+
|
5
|
+
module Music
|
6
|
+
|
7
|
+
SUPPORTED_FILETYPES = %w{.mp3 .m4a .mp4 .flac .m4a .ogg .aiff .asf .wav}
|
8
|
+
|
9
|
+
def Music.getMusicData(file)
|
10
|
+
attributes = {}
|
11
|
+
TagLib::FileRef.open(file) do |fileref|
|
12
|
+
unless fileref.null?
|
13
|
+
#sign tags to local variables
|
14
|
+
tag = fileref.tag
|
15
|
+
properties = fileref.audio_properties
|
16
|
+
|
17
|
+
#load tags into attributes attribute
|
18
|
+
attributes[:track_name] = tag.title
|
19
|
+
attributes[:track_number] = tag.track
|
20
|
+
attributes[:track_genre] = tag.genre
|
21
|
+
attributes[:track_release_date] = tag.year
|
22
|
+
attributes[:album_name] = tag.album
|
23
|
+
attributes[:artist_name] = tag.artist
|
24
|
+
attributes[:comment] = tag.comment
|
25
|
+
|
26
|
+
attributes[:track_length] = properties.length
|
27
|
+
attributes[:track_bitrate] = properties.bitrate
|
28
|
+
attributes[:track_channels] = properties.channels
|
29
|
+
attributes[:track_sample_rate] = properties.sample_rate
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return attributes
|
34
|
+
end
|
35
|
+
|
36
|
+
def Music.supported_filetypes
|
37
|
+
reutrn SUPPORTED_FILETYPES
|
38
|
+
end
|
39
|
+
|
40
|
+
def Music.is_music?(uri)
|
41
|
+
unless !uri.nil? && uri.is_a?(String) && File.exists?(uri)
|
42
|
+
raise FileNotFoundError, "Directory given (#{uri}) could not be accessed."
|
43
|
+
end
|
44
|
+
|
45
|
+
if SUPPORTED_FILETYPES.include?(File.extname(uri).downcase)
|
46
|
+
return true
|
47
|
+
else
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
|
51
|
+
rescue FileNotFoundError => e
|
52
|
+
puts e.message
|
53
|
+
puts e.backtrace.inspect
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
end
|
59
|
+
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: media-organizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Stephen Johnson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: taglib-ruby
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: exifr
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: ! 'Provides a set of functions for scanning directory trees and dynamically
|
47
|
+
renaming files using their metadata, according to a customizable taxonomy. For example,
|
48
|
+
use media-organizer to set filenames for a directory of photos to a standard such
|
49
|
+
as: "<date-taken> - Ski Vacation.jpg". Currently supports only JPEG and TIFF files,
|
50
|
+
and various music formats.'
|
51
|
+
email: djeserkare@gmail.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- lib/media-organizer.rb
|
57
|
+
- lib/renamer.rb
|
58
|
+
- lib/filescanner.rb
|
59
|
+
- lib/scrapers/image.rb
|
60
|
+
- lib/scrapers/music.rb
|
61
|
+
homepage: http://rubygems.org/gems/media-organizer
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.8.23
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: Organize & rename files in bulk based on file metadata.
|
86
|
+
test_files: []
|