popcap 0.7.2

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.
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