popcap 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +13 -0
  4. data/Gemfile.lock +52 -0
  5. data/LICENSE +9 -0
  6. data/README.md +137 -0
  7. data/lib/pop_cap/audio_file.rb +100 -0
  8. data/lib/pop_cap/commander.rb +64 -0
  9. data/lib/pop_cap/converter.rb +40 -0
  10. data/lib/pop_cap/ffmpeg.rb +130 -0
  11. data/lib/pop_cap/fileable.rb +134 -0
  12. data/lib/pop_cap/formatters/bit_rate.rb +29 -0
  13. data/lib/pop_cap/formatters/date.rb +42 -0
  14. data/lib/pop_cap/formatters/duration.rb +44 -0
  15. data/lib/pop_cap/formatters/filesize.rb +69 -0
  16. data/lib/pop_cap/formatters.rb +33 -0
  17. data/lib/pop_cap/helper.rb +53 -0
  18. data/lib/pop_cap/tag_key.rb +32 -0
  19. data/lib/pop_cap/tag_line.rb +36 -0
  20. data/lib/pop_cap/tag_struct.rb +45 -0
  21. data/lib/pop_cap/taggable.rb +100 -0
  22. data/lib/pop_cap/version.rb +3 -0
  23. data/lib/popcap.rb +5 -0
  24. data/popcap.gemspec +27 -0
  25. data/spec/integration/convert_audio_file_spec.rb +15 -0
  26. data/spec/integration/read_metatags_spec.rb +12 -0
  27. data/spec/integration/update_metatags_spec.rb +20 -0
  28. data/spec/lib/pop_cap/audio_file_spec.rb +72 -0
  29. data/spec/lib/pop_cap/commander_spec.rb +64 -0
  30. data/spec/lib/pop_cap/converter_spec.rb +67 -0
  31. data/spec/lib/pop_cap/ffmpeg_spec.rb +96 -0
  32. data/spec/lib/pop_cap/fileable_spec.rb +118 -0
  33. data/spec/lib/pop_cap/formatters/bit_rate_spec.rb +53 -0
  34. data/spec/lib/pop_cap/formatters/date_spec.rb +74 -0
  35. data/spec/lib/pop_cap/formatters/duration_spec.rb +64 -0
  36. data/spec/lib/pop_cap/formatters/filesize_spec.rb +89 -0
  37. data/spec/lib/pop_cap/formatters_spec.rb +36 -0
  38. data/spec/lib/pop_cap/helper_spec.rb +42 -0
  39. data/spec/lib/pop_cap/tag_key_spec.rb +48 -0
  40. data/spec/lib/pop_cap/tag_line_spec.rb +26 -0
  41. data/spec/lib/pop_cap/tag_struct_spec.rb +50 -0
  42. data/spec/lib/pop_cap/taggable_spec.rb +62 -0
  43. data/spec/spec_helper.rb +11 -0
  44. data/spec/support/popcap_spec_helper.rb +86 -0
  45. data/spec/support/reek_spec.rb +8 -0
  46. data/spec/support/sample.flac +0 -0
  47. metadata +163 -0
@@ -0,0 +1,29 @@
1
+ module PopCap
2
+ module Formatters
3
+ # Public: This is a formatter for the bit_rate tag. It is used
4
+ # to make the bitrate human readable.
5
+ #
6
+ # bitrate - The bitrate can be sent as a string or integer.
7
+ #
8
+ class BitRate
9
+ def initialize(bitrate)
10
+ @bitrate = bitrate
11
+ end
12
+
13
+ # Public: This method returns a bitrate represented in kilobytes.
14
+ #
15
+ # It returns nil for anything that is not a number greater than
16
+ # zero.
17
+ #
18
+ # Examples
19
+ # br = BitRate.new(128456)
20
+ # br.format
21
+ # # => '128 kb/s'
22
+ #
23
+ def format
24
+ return unless @bitrate.to_i > 0
25
+ @bitrate.to_s[0..-4] + ' kb/s'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ module PopCap
2
+ module Formatters
3
+ # Public: This is a formatter for the date tag. It is used
4
+ # to match and return the year.
5
+ #
6
+ # date - The date can be sent as a string or integer.
7
+ # options - An optional hash for a start & end date range.
8
+ # The start_date defaults to 1800, end_date defaults
9
+ # to 2100.
10
+ #
11
+ class Date
12
+ attr_reader :start_date, :end_date
13
+
14
+ def initialize(date, options={})
15
+ @date = date.to_s
16
+ @start_date = options[:start_date] || 1800
17
+ @end_date = options[:end_date] || 2100
18
+ end
19
+
20
+ # Public: This method returns a year if it is matched.
21
+ #
22
+ # Examples
23
+ # date = Date.new('October 5, 1975')
24
+ # date.format
25
+ # # => '1975'
26
+ #
27
+ def format
28
+ return unless ( date_match && within_date_range? )
29
+ @match[0].to_i
30
+ end
31
+
32
+ private
33
+ def date_match
34
+ @match ||= @date.match(/\b\d{4}\b/)
35
+ end
36
+
37
+ def within_date_range?
38
+ (start_date..end_date).include?(@match[0].to_i)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ module PopCap
2
+ module Formatters
3
+ # Public: This will format a duration tag as strftime.
4
+ #
5
+ # time - Provide a string, float, or integer.
6
+ class Duration
7
+ def initialize(time)
8
+ @time = time
9
+ end
10
+
11
+ # Public: This will format a duration tag as strftime.
12
+ # It will raise a warning if the time is greater than 24 hours.
13
+ # Leading zeroes & colons are removed.
14
+ #
15
+ # Examples
16
+ # dur = Duration.new(420)
17
+ # dur.format
18
+ # # => '7:00'
19
+ #
20
+ def format
21
+ return unless @time.to_i > 0
22
+ return warning_message if over_twenty_four_hours?
23
+ remove_leading_zeroes(to_strftime)
24
+ end
25
+
26
+ private
27
+ def to_strftime
28
+ @strftime = Time.at(@time.to_f).gmtime.strftime('%H:%M:%S')
29
+ end
30
+
31
+ def remove_leading_zeroes(strftime)
32
+ @strftime.sub(/^(0+|:)+/,'')
33
+ end
34
+
35
+ def over_twenty_four_hours?
36
+ @time.to_i > 86399
37
+ end
38
+
39
+ def warning_message
40
+ 'Warning: Time is greater than 24 hours.'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,69 @@
1
+ module PopCap
2
+ module Formatters
3
+ # Public: This class formats a filesize as human readable,
4
+ # following a UNIX formatting standard.
5
+ #
6
+ # filesize - Provide a filesize as string, integer, or float.
7
+ #
8
+ class Filesize
9
+ ::BASE = 1024
10
+ ::UNITS = %W{B K M G T}
11
+
12
+ def initialize(filesize)
13
+ @filesize = filesize
14
+ end
15
+
16
+ # Public: This method will format the filesize.
17
+ # It raises a warning message if size is greater than 999 terabytes.
18
+ #
19
+ # Examples
20
+ # fs = Filesize.new(12345678)
21
+ # fs.format
22
+ # # => '11.8M'
23
+ #
24
+ def format
25
+ return if @filesize.to_i == 0
26
+ return warning_message if too_large?
27
+ converted_filesize.to_s + measurement_character
28
+ end
29
+
30
+ private
31
+ def binary_filesize
32
+ float / ::BASE ** exponent
33
+ end
34
+
35
+ def converted_filesize
36
+ rounded = rounded_filesize
37
+ is_zero_decimal? ? rounded.ceil : rounded
38
+ end
39
+
40
+ def exponent
41
+ (Math.log(float)/Math.log(::BASE)).to_i
42
+ end
43
+
44
+ def float
45
+ Float(@filesize)
46
+ end
47
+
48
+ def is_zero_decimal?
49
+ rounded_filesize.denominator == 1
50
+ end
51
+
52
+ def measurement_character
53
+ ::UNITS[exponent]
54
+ end
55
+
56
+ def rounded_filesize
57
+ binary_filesize.round(1)
58
+ end
59
+
60
+ def too_large?
61
+ @filesize.to_i > 1099400000000000
62
+ end
63
+
64
+ def warning_message
65
+ 'Warning: Number is larger than 999 terabytes.'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ module PopCap
2
+ # Internal: This module requires all formatters in "formatters/."
3
+ #
4
+ module Formatters
5
+ # Internal: This constant is a hash of all files in "formatters/."
6
+ # To add new custom formatters to Taggable#tags, add the formatter
7
+ # to "formatters/."
8
+ #
9
+ # Formatters should follow this format:
10
+ #
11
+ # Examples
12
+ # attribute - :custom_formatter
13
+ # path - lib/pop_cap/formatters/custom_formatter.rb
14
+ # class - CustomFormatter
15
+ # format instance method - #format
16
+ #
17
+ # # lib/pop_cap/formatters/custom_formatter.rb
18
+ # class CustomFormatter
19
+ # def format
20
+ # # code that formats
21
+ # end
22
+ # end
23
+ #
24
+ ::INCLUDED_FORMATTERS= {}
25
+
26
+ Dir["#{File.dirname(__FILE__)}/formatters/*.rb"].each do |path|
27
+ @file_name = File.basename(path , '.rb')
28
+ @required = 'pop_cap/formatters/' + @file_name
29
+ require @required
30
+ ::INCLUDED_FORMATTERS[@file_name.to_sym] = @required
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ module PopCap
2
+ # Public: This class adds helper methods to construct a class.
3
+ #
4
+ # name - This is the name of the class.
5
+ #
6
+ # Examples
7
+ # Helper.new('array')
8
+ # Helper.new('active_support')
9
+ # Helper.new('active_record/base')
10
+ #
11
+ class Helper
12
+ def initialize(name)
13
+ @name = name.to_s
14
+ end
15
+
16
+ # Public: This method camel cases a string or symbol.
17
+ #
18
+ # Examples
19
+ # helper = Helper.new('active_support')
20
+ # helper.camelize
21
+ # # => 'ActiveSupport'
22
+ #
23
+ def camelize
24
+ @name.split('_').map { |word| word.capitalize }.join
25
+ end
26
+
27
+ # Public: This namespaces a string by converting a filepath
28
+ # to a namespaced constant.
29
+ #
30
+ # Examples
31
+ # helper = Helper.new('active_record/base')
32
+ # helper.namespace
33
+ # # => 'ActiveRecord::Base'
34
+ #
35
+ def namespace
36
+ camelize.split('/').map do |word|
37
+ _,head,tail = word.partition(%r(^[a-zA-Z]))
38
+ head.upcase + tail
39
+ end.join('::')
40
+ end
41
+
42
+ # Public: This converts a string into a constant.
43
+ #
44
+ # Examples
45
+ # helper = Helper.new('active_record/base')
46
+ # helper.constantize
47
+ # # => ActiveSupport::Base
48
+ #
49
+ def constantize
50
+ Object.module_eval(namespace)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ module PopCap
2
+ # Internal: This class sanitizes the raw output of FFmpeg to
3
+ # be used as a hash key.
4
+ #
5
+ # key - This is a single key as created by TagLine.
6
+ #
7
+ class TagKey
8
+ def initialize(key)
9
+ @key = key
10
+ end
11
+
12
+ # Internal: This method removes unwanted strings, downcases,
13
+ # & symbolizes a key. Additionally, it renames keys named
14
+ # 'size' to 'filesize' in order to avoid potential conflicts
15
+ # with Ruby's build-in method of the same name.
16
+ #
17
+ # Examples
18
+ # tk = TagKey.new('size')
19
+ # tk.format
20
+ # # => :filesize
21
+ #
22
+ def format
23
+ return '' if ( @key.nil? || @key.empty? )
24
+
25
+ @key.
26
+ sub(/^TAG:/,'').
27
+ sub(/^size\b/,'filesize').
28
+ downcase.
29
+ to_sym
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ require 'pop_cap/tag_key'
2
+ require 'pop_cap/formatters'
3
+
4
+ module PopCap
5
+ # Internal: This class sanitizes the raw output of FFmpeg to
6
+ # and builds a hash.
7
+ #
8
+ # line - This is a single line of raw output from FFmpeg.
9
+ #
10
+ class TagLine
11
+ include
12
+ def initialize(line)
13
+ @line = line
14
+ end
15
+
16
+ # Internal: This method builds a hash by splitting on the
17
+ # first equal sign in a single line of content from FFmpeg.
18
+ # It uses TagKey to create the key for the hash.
19
+ #
20
+ # Examples
21
+ # tl = TagLine.new('TAG:ARTIST=David Bowie')
22
+ # to.to_hash
23
+ # # => {artist: 'David Bowie'}
24
+ #
25
+ def to_hash
26
+ return {} unless ( @line && is_a_tag? )
27
+ key,val = @line.split('=',2)
28
+ {TagKey.new(key).format => val}
29
+ end
30
+
31
+ private
32
+ def is_a_tag?
33
+ @line.match('=')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ module PopCap
2
+ # Public: This class is a simple implementation of a Ruby struct
3
+ # like object. It has a custom #to_s method & creates getter methods.
4
+ #
5
+ # new - This method takes a hash. It raises an error if anything else
6
+ # is provided.
7
+ #
8
+ # Examples
9
+ # ts = TagStruct.new({artist: 'Artist', date: 1984})
10
+ # ts.artist => 'Artist'
11
+ # ts.date => 1984
12
+ #
13
+ class TagStruct
14
+ def initialize(hash)
15
+ raise(ArgumentError, argument_error_message) unless hash.kind_of?(Hash)
16
+ @hash = hash
17
+ define_instance_methods
18
+ end
19
+
20
+ # Public: This method shows the class name & hash values as a string.
21
+ #
22
+ # Examples
23
+ # ts = TagStruct.new({artist: 'Artist', date: 1984})
24
+ # puts ts
25
+ # #=> '#<PopCap::TagStruct artist: Artist, date: 1984>'
26
+ #
27
+ def to_s
28
+ methods = @hash.map { |key,val| %(#{key}: #{val}) }.join(', ')
29
+ "#<#{self.class.name} " + methods + '>'
30
+ end
31
+
32
+ private
33
+ def argument_error_message
34
+ 'Initialize with a hash.'
35
+ end
36
+
37
+ def define_instance_methods
38
+ @hash.each do |key, val|
39
+ unless self.class.respond_to? key
40
+ define_singleton_method(key) { val }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,100 @@
1
+ require 'pop_cap/helper'
2
+ require 'pop_cap/formatters'
3
+ require 'pop_cap/tag_line'
4
+ require 'pop_cap/tag_struct'
5
+
6
+ module PopCap
7
+ # Internal: This module is included in anything with a #raw_tags
8
+ # attribute. It is used to parse and build tags from FFmpeg raw output.
9
+ #
10
+ module Taggable
11
+ include Formatters
12
+
13
+ # Internal: This method reloads memoized tags.
14
+ def reload!
15
+ @lined, @tags, @hash = nil, nil, nil
16
+ end
17
+
18
+ # Internal: This method builds a sanitized hash from #raw_tags.
19
+ #
20
+ # Examples
21
+ # class SomeClass
22
+ # def raw_tags
23
+ # end
24
+ # end
25
+ #
26
+ # klass = SomeClass.new
27
+ # klass.to_hash
28
+ # # =>
29
+ # { filename: 'spec/support/sample.flac',
30
+ # format_name: 'flac',
31
+ # duration: '1.000000',
32
+ # filesize: '18291',
33
+ # bit_rate: '146328',
34
+ # genre: 'Sample Genre',
35
+ # track: '01',
36
+ # album: 'Sample Album',
37
+ # date: '2012',
38
+ # title: 'Sample Title',
39
+ # artist: 'Sample Artist' }
40
+ #
41
+ def to_hash
42
+ @hash ||= lined_hash.merge(formatted_hash)
43
+ end
44
+
45
+ # Public: This method builds an tag structure from #to_hash. Also,
46
+ # TagFormatters are applied to any tag with a custom formatter.
47
+ #
48
+ # Examples
49
+ # class SomeClass
50
+ # def raw_tags
51
+ # end
52
+ # end
53
+ #
54
+ # klass = SomeClass.new
55
+ # klass.tags
56
+ #
57
+ # # =>
58
+ # .album => 'Sample Album'
59
+ # .artist => 'Sample Artist'
60
+ # .bit_rate => '146 kb/s'
61
+ # .date => 2012
62
+ # .duration => '1'
63
+ # .filename => 'spec/support/sample.flac'
64
+ # .filesize => '17.9K'
65
+ # .format_long_name => 'raw FLAC'
66
+ # .format_name => 'flac'
67
+ # .genre => 'Sample Genre'
68
+ # .nb_streams => '1'
69
+ # .start_time => 'N/A'
70
+ # .title => 'Sample Title'
71
+ # .track => '01'
72
+ #
73
+ def tags
74
+ @tags ||= build_tag_struct(to_hash)
75
+ end
76
+
77
+ private
78
+ def lines
79
+ self.raw_tags.split("\n")
80
+ end
81
+
82
+ def build_tag_struct(hash)
83
+ TagStruct.new(hash)
84
+ end
85
+
86
+ def lined_hash
87
+ @lined ||=
88
+ lines.inject({}) { |hsh,line| hsh.merge(TagLine.new(line)).to_hash }
89
+ end
90
+
91
+ def formatted_hash
92
+ ::INCLUDED_FORMATTERS.inject({}) do |formatted, formatter|
93
+ key, value = formatter
94
+ helper = Helper.new(value)
95
+ klass = helper.constantize
96
+ formatted.merge({key => klass.new(lined_hash[key]).format})
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module PopCap
2
+ VERSION = '0.7.2'
3
+ end
data/lib/popcap.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'pop_cap/audio_file'
2
+ require 'pop_cap/version'
3
+
4
+ module PopCap
5
+ end
data/popcap.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'popcap'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'popcap'
9
+ s.version = PopCap::VERSION
10
+ s.authors = ["Culley Smith"]
11
+ s.email = ["culley.smith@gmail.com"]
12
+ s.homepage = %q{http://madstance.com}
13
+ s.summary = %q{A library work with audio files on the filesystem .}
14
+ s.description = %q{Read & write metadata tags, convert audio files to alternate formats, manage files on the filesystem.}
15
+
16
+ s.required_ruby_version = '>= 1.9.3'
17
+ s.required_rubygems_version = '>= 1.3.6'
18
+
19
+ s.add_development_dependency 'reek', '~> 1.2'
20
+ s.add_development_dependency 'rspec', '~> 2.11'
21
+ s.add_development_dependency 'simplecov', '~> 0.7'
22
+
23
+ git_files = `git ls-files -z`.split("\0") rescue ''
24
+ s.files = git_files
25
+ s.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\0")
26
+ s.require_paths = ['lib']
27
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'support/popcap_spec_helper'
3
+ require 'pop_cap/audio_file'
4
+
5
+ module PopCap
6
+ describe 'Convert Audio Files' do
7
+ after { PopCapSpecHelper.remove_converted }
8
+
9
+ it 'creates a new audio file with the specified format & bitrate' do
10
+ audio_file = AudioFile.new('spec/support/sample.flac')
11
+ audio_file.convert(:mp3, 128)
12
+ expect(File.exists?('spec/support/sample.mp3')).to be_true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ require 'popcap'
2
+ require 'spec_helper'
3
+ require 'support/popcap_spec_helper'
4
+
5
+ describe PopCap do
6
+ let(:audio_file) { PopCap::AudioFile.new(filepath) }
7
+ let(:filepath) { 'spec/support/sample.flac' }
8
+
9
+ it '#raw_tags' do
10
+ expect(audio_file.raw_tags).to eq PopCapSpecHelper.raw_tags
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ require 'popcap'
2
+ require 'spec_helper'
3
+ require 'support/popcap_spec_helper'
4
+
5
+ describe PopCap do
6
+ let(:audio_file) { PopCap::AudioFile.new(filepath) }
7
+ let(:filepath) { 'spec/support/sample.flac' }
8
+
9
+ context '#update_tags' do
10
+ before { PopCapSpecHelper.setup }
11
+ after { PopCapSpecHelper.teardown }
12
+
13
+ it 'updates tags for file' do
14
+ expect(audio_file.tags.artist).to eq 'Sample Artist'
15
+ updates = {artist: 'New Artist'}
16
+ audio_file.update_tags(updates)
17
+ expect(audio_file.tags.artist).to eq 'New Artist'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,72 @@
1
+ require 'pop_cap/audio_file'
2
+ require 'spec_helper'
3
+ require 'support/popcap_spec_helper'
4
+
5
+ module PopCap
6
+ describe AudioFile do
7
+ let(:audio_file) { AudioFile.new(filepath) }
8
+ let(:filepath) { 'spec/support/sample.flac' }
9
+ let(:included_modules) { AudioFile.included_modules }
10
+
11
+ before { PopCapSpecHelper.setup }
12
+ after { PopCapSpecHelper.teardown }
13
+
14
+ subject { audio_file }
15
+
16
+ context '#filepath' do
17
+ its(:filepath) { should eq File.realpath(filepath) }
18
+
19
+ it 'raises an error if file does not exist' do
20
+ expect do
21
+ AudioFile.new('not here.file')
22
+ end.to raise_error(FileNotFound, 'not here.file')
23
+ end
24
+ end
25
+
26
+ context 'ffmpeg methods' do
27
+ it { expect(audio_file).to respond_to(:convert) }
28
+ it { expect(audio_file).to respond_to(:raw_tags) }
29
+ it { expect(audio_file).to respond_to(:update_tags) }
30
+ end
31
+
32
+ context 'included modules' do
33
+ it 'includes Fileable' do
34
+ expect(included_modules).to include Fileable
35
+ end
36
+
37
+ it 'includes Taggable' do
38
+ expect(included_modules).to include Taggable
39
+ end
40
+ end
41
+
42
+ context '#raw_tags' do
43
+ it 'is memoized' do
44
+ audio_file.raw_tags
45
+ expect(audio_file.instance_variable_get('@raw')).
46
+ to eq PopCapSpecHelper.raw_tags
47
+ end
48
+ end
49
+
50
+ context '#reload!' do
51
+ it 'reloads raw tags' do
52
+ audio_file.raw_tags
53
+ audio_file.reload!
54
+ expect(audio_file.instance_variable_get('@raw')).to be_nil
55
+ end
56
+
57
+ it 'calls up to Taggable#reload!' do
58
+ audio_file.raw_tags
59
+ audio_file.reload!
60
+ expect(audio_file.instance_variable_get('@tags')).to be_nil
61
+ expect(audio_file.instance_variable_get('@to_hash')).to be_nil
62
+ end
63
+ end
64
+
65
+ context '#update_tags' do
66
+ it 'reloads after tags updated' do
67
+ audio_file.should_receive(:reload!)
68
+ expect(audio_file.update_tags({foo: 'foo'}))
69
+ end
70
+ end
71
+ end
72
+ end