ffsplitter 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9544a8a00210da89af516422706274002610c6eb
4
+ data.tar.gz: 4d0f063c8deb2fd37ca5c697743a644f073626ea
5
+ SHA512:
6
+ metadata.gz: cbb8063f2907f1e776e7f95c09685e73f96642e216adb7a0247e964546fff7bc9e935e565d0795fc6e3c6bdfde7e10afa56db95eae197105eb947df8304d2cb1
7
+ data.tar.gz: a0192b0d962f4ae8aee208dacf7a719db6b2852d8ae870858164ea431775abad24ac2d98398493f60eeedb2d000bb3a352dd8157ab89a919f09c87820597f119
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ vendor/ruby
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Kevin Rockwood
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # FFSplitter
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ gem install ffsplitter
8
+
9
+ ## Usage
10
+
11
+ Given the video file `my-video.mp4`
12
+ And `my-video.mp4` has embedded chapter markers
13
+ When I execute `ffsplitter my-video.mp4`
14
+ Then `.mp4` files will be created for each chapter
15
+
16
+ ## Contributing
17
+
18
+ 1. Fork it
19
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
20
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
21
+ 4. Push to the branch (`git push origin my-new-feature`)
22
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/ffsplitter ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ffsplitter'
4
+ FFSplitter::CLI.run(ARGV)
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ffsplitter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ffsplitter"
8
+ spec.version = FFSplitter::VERSION
9
+ spec.authors = ["Kevin Rockwood"]
10
+ spec.email = ["kevin@rockwood.me"]
11
+ spec.description = %q{Split video files into chapters}
12
+ spec.summary = %q{ffsplitter will split a video file by its chapters}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "virtus", "~> 1.0.0.rc2"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ end
@@ -0,0 +1,53 @@
1
+ require 'virtus'
2
+
3
+ module FFSplitter
4
+ class ChapterList
5
+
6
+ include Enumerable
7
+
8
+ def initialize
9
+ @chapters = []
10
+ end
11
+
12
+ def each(&block)
13
+ @chapters.each { |c| yield c }
14
+ end
15
+
16
+ def add(chapter)
17
+ chapter.list = self
18
+ @chapters << chapter
19
+ end
20
+ end
21
+
22
+ class Chapter
23
+ include Virtus.model
24
+
25
+ attribute :start_frames, Integer, default: 0
26
+ attribute :end_frames, Integer, default: 0
27
+ attribute :timebase, Float, default: 1/30000
28
+ attribute :title, String
29
+
30
+ attr_accessor :list
31
+
32
+ def start_time
33
+ timebase * start_frames
34
+ end
35
+
36
+ def end_time
37
+ timebase * end_frames
38
+ end
39
+
40
+ def filename
41
+ "#{track} #{title.lstrip}"
42
+ end
43
+
44
+ def index
45
+ return 0 if list.nil?
46
+ list.find_index(self)
47
+ end
48
+
49
+ def track
50
+ (index + 1).to_s.rjust(2, '0')
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ module FFSplitter
2
+ class CommandRunner
3
+ def self.run(command)
4
+ `#{command}`
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module FFSplitter
2
+ class FFMpeg
3
+ attr_accessor :filename, :runner
4
+
5
+ def initialize(filename)
6
+ self.filename = filename
7
+ self.runner = CommandRunner
8
+ end
9
+
10
+ def encode(chapters)
11
+ chapters.each do |chapter|
12
+ runner.run(chapter_command(chapter))
13
+ end
14
+ end
15
+
16
+ def read_metadata
17
+ runner.run("ffmpeg -i #{filename} -v quiet -f ffmetadata -")
18
+ end
19
+
20
+ private
21
+
22
+ def chapter_command(chapter)
23
+ "ffmpeg -ss #{chapter.start_time} -i #{filename} -to #{chapter.end_time} -c copy '#{chapter.filename}.mp4'"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ module FFSplitter
2
+ class MetadataParser
3
+ def self.parse(metadata)
4
+ chapter_list = ChapterList.new
5
+ chapter = nil
6
+
7
+ metadata.split("\n").each do |line|
8
+ if line =~ /CHAPTER/
9
+ chapter = Chapter.new
10
+ end
11
+
12
+ if line =~ /TIMEBASE=(\d+)\/(\d+)/
13
+ chapter.timebase = $1.to_f / $2.to_f
14
+ end
15
+
16
+ if line =~ /START=(\d+)/
17
+ chapter.start_frames = $1
18
+ end
19
+
20
+ if line =~ /END=(\d+)/
21
+ chapter.end_frames = $1
22
+ end
23
+
24
+ if chapter && line =~ /title=(.*)/
25
+ chapter.title = $1
26
+ chapter_list.add(chapter)
27
+ end
28
+ end
29
+
30
+ chapter_list
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ module FFSplitter
2
+ class Splitter
3
+ def self.split_via_ffmpeg(filename)
4
+ split(FFMpeg.new(filename))
5
+ end
6
+
7
+ def self.split(codec)
8
+ chapter_list = MetadataParser.parse(codec.read_metadata)
9
+ codec.encode(chapter_list)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module FFSplitter
2
+ VERSION = "0.0.1"
3
+ end
data/lib/ffsplitter.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "ffsplitter/version"
2
+ require "ffsplitter/metadata_parser"
3
+ require "ffsplitter/chapter"
4
+ require "ffsplitter/command_runner"
5
+ require "ffsplitter/ffmpeg"
6
+ require "ffsplitter/splitter"
7
+
8
+ module FFSplitter
9
+ class CLI
10
+ def self.run(argv = ARGV)
11
+ new(argv).run
12
+ end
13
+
14
+ def initialize(argv)
15
+ @argv = argv
16
+ end
17
+
18
+ def run
19
+ if @argv.empty?
20
+ puts "WTF mate?"
21
+ exit(1)
22
+ else
23
+ Splitter.split_via_ffmpeg(@argv[0])
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ module FFSplitter
4
+ describe ChapterList do
5
+ describe "#add" do
6
+ let(:chapter_list) { ChapterList.new }
7
+ let!(:chapter1) { Chapter.new(title: "Chapter One") }
8
+ let!(:chapter2) { Chapter.new(title: "Chapter Two") }
9
+ before do
10
+ chapter_list.add(chapter1)
11
+ chapter_list.add(chapter2)
12
+ end
13
+ it "adds chapters to its collection" do
14
+ expect(chapter_list.count).to eq(2)
15
+ expect(chapter_list).to include(chapter1)
16
+ expect(chapter_list).to include(chapter2)
17
+ end
18
+ it "sets the chapter's list" do
19
+ expect(chapter1.list).to eq(chapter_list)
20
+ end
21
+ end
22
+ end
23
+
24
+ describe Chapter do
25
+ describe "#start_time, #end_time" do
26
+ let(:chapter){ Chapter.new(start_frames: 100, end_frames: 200, timebase: 0.5) }
27
+ it "calculates based on frames and timebase" do
28
+ expect(chapter.start_time).to eq(50)
29
+ expect(chapter.end_time).to eq(100)
30
+ end
31
+ end
32
+
33
+ describe "filename" do
34
+ let(:chapter){ Chapter.new(title: " test title") }
35
+ it "prettys up the chapter title" do
36
+ expect(chapter.filename).to eq("01 test title")
37
+ end
38
+ end
39
+
40
+ describe "#index" do
41
+ let(:chapter) { Chapter.new }
42
+ context "without a list" do
43
+ it "returns 0" do
44
+ expect(chapter.index).to eq(0)
45
+ end
46
+ end
47
+ context "with a list" do
48
+ let(:chapter_list) { ChapterList.new }
49
+ before do
50
+ chapter_list.add Chapter.new
51
+ chapter_list.add chapter
52
+ end
53
+ it "returns the index" do
54
+ expect(chapter.index).to eq(1)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ module FFSplitter
4
+ describe FFMpeg do
5
+ let(:ffmpeg){ FFMpeg.new("test.mp4") }
6
+ let(:runner) { double("runner") }
7
+ before { ffmpeg.runner = runner }
8
+
9
+ describe "#encode" do
10
+ let(:chapter_list) { ChapterList.new }
11
+ let(:chapter) { Chapter.new(start_frames: 10, end_frames: 20, timebase: 1, title: "title") }
12
+ before { chapter_list.add(chapter) }
13
+ it "encodes the chapters" do
14
+ expect(runner).to receive(:run).with "ffmpeg -ss 10.0 -i test.mp4 -to 20.0 -c copy '01 title.mp4'"
15
+ ffmpeg.encode(chapter_list)
16
+ end
17
+ end
18
+
19
+ describe "#read_metadata" do
20
+ it "reads the metadata" do
21
+ expect(runner).to receive(:run).with("ffmpeg -i test.mp4 -v quiet -f ffmetadata -")
22
+ ffmpeg.read_metadata
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ module FFSplitter
2
+ module MetadataFixtures
3
+ MULTICHAPTER = <<-EOF
4
+ ;FFMETADATA1
5
+ major_brand=M4V
6
+ minor_version=1
7
+ compatible_brands=M4V M4A mp42isom
8
+ title=2013-10-02 Sandra Poon
9
+ composer=Kevin Rockwood
10
+ [CHAPTER]
11
+ TIMEBASE=1/30000
12
+ START=0
13
+ END=2321319
14
+ title=Ludwig van Beethoven - Piano Trio in D Major, Op. 70 No. 1 ("Ghost Trio")
15
+ [CHAPTER]
16
+ TIMEBASE=1/30000
17
+ START=2321319
18
+ END=15450435
19
+ title=- I. Allegro vivace e con brio
20
+ [CHAPTER]
21
+ TIMEBASE=1/30000
22
+ START=15450435
23
+ END=15451436
24
+ title=- II. Largo assai ed espressivo
25
+ EOF
26
+
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'fixtures/metadata_fixtures'
3
+
4
+ module FFSplitter
5
+ describe MetadataParser do
6
+ describe ".parse" do
7
+ context "with a single chapter" do
8
+ let(:metadata) do
9
+ <<-EOF
10
+ [CHAPTER]
11
+ TIMEBASE=1/2
12
+ START=100
13
+ END=200
14
+ title=Ludwig van Beethoven - Piano Trio in D Major
15
+ EOF
16
+ end
17
+ let(:chapters){ MetadataParser.parse(metadata) }
18
+ it "parses the chapters as an array" do
19
+ expect(chapters.count).to eq(1)
20
+ end
21
+
22
+ it "calculates the timebase" do
23
+ expect(chapters.first[:timebase]).to eq(0.5)
24
+ end
25
+
26
+ it "sets the start_frames" do
27
+ expect(chapters.first[:start_frames]).to eq(100)
28
+ end
29
+
30
+ it "sets the end_frames" do
31
+ expect(chapters.first[:end_frames]).to eq(200)
32
+ end
33
+
34
+ it "parses the title" do
35
+ expect(chapters.first[:title]).to eq("Ludwig van Beethoven - Piano Trio in D Major")
36
+ end
37
+ end
38
+
39
+ context "with multiple chapters" do
40
+ let(:metadata) do
41
+ <<-EOF
42
+ [CHAPTER]
43
+ title=Chapter One
44
+ [CHAPTER]
45
+ title=Chapter Two
46
+ EOF
47
+ end
48
+ let(:chapters){ MetadataParser.parse(metadata) }
49
+ it "parses all the chapters" do
50
+ expect(chapters.count).to eq(2)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1 @@
1
+ require 'ffsplitter'
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ module FFSplitter
4
+ describe Splitter do
5
+ describe ".split!" do
6
+ let(:codec) { double('Codec') }
7
+ let(:metadata) do
8
+ <<-EOF
9
+ [CHAPTER]
10
+ title=Chapter One
11
+ EOF
12
+ end
13
+ it "parses and encodes" do
14
+ expect(codec).to receive(:read_metadata).and_return(metadata)
15
+ expect(codec).to receive(:encode).with(an_instance_of(ChapterList))
16
+ Splitter.split(codec)
17
+ end
18
+ end
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffsplitter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Rockwood
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0.rc2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0.rc2
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Split video files into chapters
70
+ email:
71
+ - kevin@rockwood.me
72
+ executables:
73
+ - ffsplitter
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/ffsplitter
83
+ - ffsplitter.gemspec
84
+ - lib/ffsplitter.rb
85
+ - lib/ffsplitter/chapter.rb
86
+ - lib/ffsplitter/command_runner.rb
87
+ - lib/ffsplitter/ffmpeg.rb
88
+ - lib/ffsplitter/metadata_parser.rb
89
+ - lib/ffsplitter/splitter.rb
90
+ - lib/ffsplitter/version.rb
91
+ - spec/chapter_spec.rb
92
+ - spec/ffmpeg_spec.rb
93
+ - spec/fixtures/metadata_fixtures.rb
94
+ - spec/metadata_parser_spec.rb
95
+ - spec/spec_helper.rb
96
+ - spec/splitter_spec.rb
97
+ homepage: ''
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.0.3
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: ffsplitter will split a video file by its chapters
121
+ test_files:
122
+ - spec/chapter_spec.rb
123
+ - spec/ffmpeg_spec.rb
124
+ - spec/fixtures/metadata_fixtures.rb
125
+ - spec/metadata_parser_spec.rb
126
+ - spec/spec_helper.rb
127
+ - spec/splitter_spec.rb