speedrun 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0622185252d91812dedda26e692722bca73497fb9ef979cab8d4af602391867
4
- data.tar.gz: 8c8d1ffb726b10f5aa0bf6c948713af211f710617fe348a9174f0cf57bd2a689
3
+ metadata.gz: d3fd70626ab0029a0a22690b09bb98c59860e679b807b201fbc40ff34aaebbde
4
+ data.tar.gz: bb29f4d74a711e2a32d928daf2f50bd3040e4949f045c3889054e7cf511ad0f2
5
5
  SHA512:
6
- metadata.gz: b46d265c87d2679cef58df69c092bf72b297b27ca1663dfb5b72dd8f41a4208d9ab982cb0e26d8bab53aded0f6cdaf6a84b4fdb6753bef419faf1cc7ceb0a35c
7
- data.tar.gz: cd11c55079861ada32b8eca148fe3c022b628b6d36b682d6a38126330f10c687bde7fdd66f787c82ddf8592118d47f429e30681837113cf46f90e1d368330796
6
+ metadata.gz: b39704ccbd0c2a8cecf836d923bf52dd251568eb2ca7cc5801d77e9358290e96de6799d37c33040b74b85a715cab042ac853770060b82ac6095850c16997146c
7
+ data.tar.gz: 6fee192f0e6d3fb312f8f0e054e4ce2602a2e141f22e025ea27d4a9b5bc121e465abbc0b67bd21761c38ae3a8fb0f2aa5b9dd30a52bc065b51c426857bb16eb4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2025-11-03
9
+
10
+ ### Added
11
+ - Streaming progress tracking for FFmpeg operations
12
+ - Progress bar support during video analysis and processing
13
+ - Quiet mode for less verbose output
14
+ - Enhanced time parsing and formatting utilities
15
+ - Comprehensive test coverage for progress tracking functionality
16
+
17
+ ### Improved
18
+ - Real-time progress updates during video processing
19
+ - More robust time parsing and conversion
20
+ - Ability to suppress output with quiet flag
21
+
22
+ ### Changed
23
+ - Added .claude directory to .gitignore for user-specific configuration protection
24
+
8
25
  ## [0.1.0] - 2025-11-03
9
26
 
10
27
  ### Added
data/lib/speedrun/cli.rb CHANGED
@@ -24,7 +24,8 @@ module Speedrun
24
24
  input_file,
25
25
  output_file,
26
26
  noise_threshold: options[:noise],
27
- min_duration: options[:duration]
27
+ min_duration: options[:duration],
28
+ quiet: options[:quiet]
28
29
  )
29
30
 
30
31
  trimmer.dry_run = options[:'dry-run']
@@ -2,11 +2,14 @@
2
2
 
3
3
  require 'shellwords'
4
4
  require 'tempfile'
5
+ require 'open3'
5
6
 
6
7
  module Speedrun
7
8
  module FFmpeg
8
9
  FREEZE_START_PATTERN = /freeze_start:\s*([\d.]+)/
9
10
  FREEZE_END_PATTERN = /freeze_end:\s*([\d.]+)/
11
+ DURATION_PATTERN = /Duration:\s*(\d{2}):(\d{2}):(\d{2}\.\d+)/
12
+ TIME_PATTERN = /time=(\d{2}):(\d{2}):(\d{2}\.\d+)/
10
13
 
11
14
  def self.parse_duration(output)
12
15
  output.strip.to_f
@@ -44,7 +47,7 @@ module Speedrun
44
47
  parse_duration(output)
45
48
  end
46
49
 
47
- def self.detect_freezes(file_path, noise_threshold: -70, min_duration: 1.0)
50
+ def self.detect_freezes(file_path, noise_threshold: -70, min_duration: 1.0, quiet: false, &block)
48
51
  raise ArgumentError, "File not found: #{file_path}" unless File.exist?(file_path)
49
52
 
50
53
  cmd = [
@@ -53,13 +56,37 @@ module Speedrun
53
56
  '-vf', "freezedetect=n=#{noise_threshold}dB:d=#{min_duration}",
54
57
  '-f', 'null',
55
58
  '-'
56
- ].shelljoin
59
+ ]
60
+
61
+ output = String.new
62
+ duration = nil
63
+
64
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
65
+ stdin.close
66
+
67
+ stderr.each_line do |line|
68
+ output << line
69
+
70
+ # Extract duration from first line that contains it
71
+ if duration.nil? && line =~ DURATION_PATTERN
72
+ duration = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
73
+ end
74
+
75
+ # Extract current time and calculate progress
76
+ if duration && block_given? && line =~ TIME_PATTERN
77
+ current_time = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
78
+ progress = [(current_time / duration * 100).round, 100].min
79
+ block.call(progress)
80
+ end
81
+ end
82
+
83
+ wait_thr.value # Wait for process to complete
84
+ end
57
85
 
58
- output = `#{cmd} 2>&1`
59
86
  parse_freezes(output)
60
87
  end
61
88
 
62
- def self.extract_and_concat(input_file, output_file, keep_regions)
89
+ def self.extract_and_concat(input_file, output_file, keep_regions, quiet: false, &block)
63
90
  raise ArgumentError, "File not found: #{input_file}" unless File.exist?(input_file)
64
91
  raise ArgumentError, "No regions to keep" if keep_regions.empty?
65
92
 
@@ -81,10 +108,33 @@ module Speedrun
81
108
  '-c', 'copy',
82
109
  '-y',
83
110
  output_file
84
- ].shelljoin
111
+ ]
112
+
113
+ output = String.new
114
+ duration = nil
115
+ success = false
116
+
117
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
118
+ stdin.close
119
+
120
+ stderr.each_line do |line|
121
+ output << line
85
122
 
86
- output = `#{cmd} 2>&1`
87
- success = $?.success?
123
+ # Extract duration from first line that contains it
124
+ if duration.nil? && line =~ DURATION_PATTERN
125
+ duration = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
126
+ end
127
+
128
+ # Extract current time and calculate progress
129
+ if duration && block_given? && line =~ TIME_PATTERN
130
+ current_time = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
131
+ progress = [(current_time / duration * 100).round, 100].min
132
+ block.call(progress)
133
+ end
134
+ end
135
+
136
+ success = wait_thr.value.success?
137
+ end
88
138
 
89
139
  raise "FFmpeg failed: #{output}" unless success
90
140
 
@@ -16,6 +16,11 @@ module Speedrun
16
16
  format('%02d:%02d:%06.3f', hours, minutes, secs)
17
17
  end
18
18
 
19
+ def self.parse_time(time_string)
20
+ hours, minutes, seconds = time_string.split(':').map(&:to_f)
21
+ (hours * SECONDS_PER_HOUR) + (minutes * SECONDS_PER_MINUTE) + seconds
22
+ end
23
+
19
24
  def self.format_duration(seconds)
20
25
  if seconds < SECONDS_PER_MINUTE
21
26
  format('%.2fs', seconds)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-progressbar"
4
+
5
+ module Speedrun
6
+ module Progress
7
+ def self.create(title:, total:, quiet: false)
8
+ return nil if quiet
9
+
10
+ ProgressBar.create(
11
+ title: title,
12
+ total: total,
13
+ format: "%t: |%B| %p%%"
14
+ )
15
+ end
16
+ end
17
+ end
@@ -7,7 +7,7 @@ module Speedrun
7
7
  attr_reader :input_file, :output_file
8
8
  attr_accessor :dry_run
9
9
 
10
- def initialize(input_file, output_file = nil, noise_threshold: -70, min_duration: 1.0)
10
+ def initialize(input_file, output_file = nil, noise_threshold: -70, min_duration: 1.0, quiet: false)
11
11
  raise ArgumentError, "File not found: #{input_file}" unless File.exist?(input_file)
12
12
 
13
13
  @input_file = input_file
@@ -15,26 +15,36 @@ module Speedrun
15
15
  @noise_threshold = noise_threshold
16
16
  @min_duration = min_duration
17
17
  @dry_run = false
18
+ @quiet = quiet
18
19
  end
19
20
 
20
21
  def dry_run?
21
22
  @dry_run
22
23
  end
23
24
 
25
+ def quiet?
26
+ @quiet
27
+ end
28
+
24
29
  def run
25
- puts "Input: #{@input_file}"
26
- puts "Output: #{@output_file}"
27
- puts
30
+ output "Input: #{@input_file}"
31
+ output "Output: #{@output_file}"
32
+ output ""
28
33
 
29
34
  # Step 1: Detect freeze regions
35
+ progress_bar = Progress.create(title: "Analyzing video", total: 100, quiet: @quiet)
30
36
  freeze_regions = FFmpeg.detect_freezes(
31
37
  @input_file,
32
38
  noise_threshold: @noise_threshold,
33
- min_duration: @min_duration
34
- )
39
+ min_duration: @min_duration,
40
+ quiet: @quiet
41
+ ) do |progress|
42
+ progress_bar&.progress = progress if progress
43
+ end
44
+ progress_bar&.finish
35
45
 
36
46
  if freeze_regions.empty?
37
- puts "No freeze regions detected. Copying original video..."
47
+ output "No freeze regions detected. Copying original video..."
38
48
  FileUtils.cp(@input_file, @output_file) unless dry_run?
39
49
  return
40
50
  end
@@ -52,21 +62,28 @@ module Speedrun
52
62
 
53
63
  # Step 4: Process or dry-run
54
64
  if dry_run?
55
- puts
56
- puts "DRY RUN - No files were modified"
65
+ output ""
66
+ output "DRY RUN - No files were modified"
57
67
  else
58
- puts
59
- puts "Processing video..."
60
- FFmpeg.extract_and_concat(@input_file, @output_file, keep_regions)
68
+ output ""
69
+ progress_bar = Progress.create(title: "Processing video", total: 100, quiet: @quiet)
70
+ FFmpeg.extract_and_concat(@input_file, @output_file, keep_regions, quiet: @quiet) do |progress|
71
+ progress_bar&.progress = progress if progress
72
+ end
73
+ progress_bar&.finish
61
74
 
62
75
  output_size = File.size(@output_file)
63
- puts "Complete! Output saved to #{@output_file}"
64
- puts " File size: #{Formatter.format_filesize(output_size)}"
76
+ output "Complete! Output saved to #{@output_file}"
77
+ output " File size: #{Formatter.format_filesize(output_size)}"
65
78
  end
66
79
  end
67
80
 
68
81
  private
69
82
 
83
+ def output(message)
84
+ puts message unless @quiet
85
+ end
86
+
70
87
  def generate_output_filename(input_file)
71
88
  ext = File.extname(input_file)
72
89
  base = File.basename(input_file, ext)
@@ -98,14 +115,14 @@ module Speedrun
98
115
  total_removed = freeze_regions.sum { |s, e| e - s }
99
116
  total_kept = keep_regions.sum { |s, e| e - s }
100
117
 
101
- puts "Analysis complete:"
102
- puts " Found #{freeze_regions.length} freeze region#{freeze_regions.length == 1 ? '' : 's'}"
103
- puts " Keeping #{keep_regions.length} segment#{keep_regions.length == 1 ? '' : 's'} with motion"
104
- puts
105
- puts "Summary:"
106
- puts " Original duration: #{Formatter.format_duration(video_duration)}"
107
- puts " Keeping: #{Formatter.format_duration(total_kept)} (#{'%.1f' % (total_kept / video_duration * 100)}%)"
108
- puts " Removing: #{Formatter.format_duration(total_removed)} (#{'%.1f' % (total_removed / video_duration * 100)}%)"
118
+ output "Analysis complete:"
119
+ output " Found #{freeze_regions.length} freeze region#{freeze_regions.length == 1 ? '' : 's'}"
120
+ output " Keeping #{keep_regions.length} segment#{keep_regions.length == 1 ? '' : 's'} with motion"
121
+ output ""
122
+ output "Summary:"
123
+ output " Original duration: #{Formatter.format_duration(video_duration)}"
124
+ output " Keeping: #{Formatter.format_duration(total_kept)} (#{'%.1f' % (total_kept / video_duration * 100)}%)"
125
+ output " Removing: #{Formatter.format_duration(total_removed)} (#{'%.1f' % (total_removed / video_duration * 100)}%)"
109
126
  end
110
127
  end
111
128
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Speedrun
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/speedrun.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "speedrun/version"
4
4
  require_relative "speedrun/formatter"
5
+ require_relative "speedrun/progress"
5
6
  require_relative "speedrun/ffmpeg"
6
7
  require_relative "speedrun/trimmer"
7
8
  require_relative "speedrun/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: speedrun
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Jackson
@@ -112,6 +112,7 @@ files:
112
112
  - lib/speedrun/cli.rb
113
113
  - lib/speedrun/ffmpeg.rb
114
114
  - lib/speedrun/formatter.rb
115
+ - lib/speedrun/progress.rb
115
116
  - lib/speedrun/trimmer.rb
116
117
  - lib/speedrun/version.rb
117
118
  - sig/speedrun.rbs