mpd_report_monthly (3008B)
1 #!/usr/bin/env ruby 2 # frozen_string_literal: true 3 4 require "date" 5 require "open3" 6 require "optparse" 7 require "tempfile" 8 9 REPORT_COUNT = 4 10 MANAGED_MPD_OPTIONS = %w[-i --indexed -o --output -w --window].freeze 11 12 def main(arguments) 13 options = {output_directory: "."} 14 parser = OptionParser.new do |opts| 15 opts.banner = "Usage: #{opts.program_name} [-o DIRECTORY] [MONTH [YEAR]] [-- MPD_OPTION ...]" 16 opts.on("-o", "--output-dir DIRECTORY", "write reports beneath DIRECTORY") do |directory| 17 options[:output_directory] = directory 18 end 19 opts.on("-h", "--help", "show this help") do 20 puts opts 21 return 0 22 end 23 end 24 25 separator = arguments.index("--") 26 mpd_options = separator ? arguments.slice!((separator + 1)..) : [] 27 arguments.pop if arguments.last == "--" 28 parser.parse!(arguments) 29 30 month = (v = arguments.shift) ? Integer(v, 10) : Date.today.month 31 year = (v = arguments.shift) ? Integer(v, 10) : Date.today.year 32 raise OptionParser::InvalidArgument, arguments.first unless arguments.empty? 33 34 first = Date.new(year, month, 1) 35 output_directory = options[:output_directory] 36 raise OptionParser::InvalidArgument, "not a directory: #{output_directory}" unless Dir.exist?(output_directory) 37 38 reserved = mpd_options.find do |option| 39 MANAGED_MPD_OPTIONS.include?(option) || option.start_with?("--window=") 40 end 41 raise OptionParser::InvalidArgument, "#{reserved} is managed by this helper" if reserved 42 43 command = ENV["MPD_REPORT"] || begin 44 sibling = File.join(__dir__, "mpd_report") 45 File.executable?(sibling) ? sibling : "mpd_report" 46 end 47 48 windows = REPORT_COUNT.times.flat_map do |week| 49 dates = [first + week * 7, first + (week + 1) * 7] 50 epochs = dates.map { |date| Time.local(date.year, date.month, date.day).to_i } 51 ["--window", epochs.join(",")] 52 end 53 54 stdout, stderr, status = Open3.capture3(command, *mpd_options, "--output", "-", *windows) 55 warn stderr unless stderr.empty? 56 return status.exitstatus || 1 unless status.success? 57 58 reports = Array.new(REPORT_COUNT) { [] } 59 stdout.each_line do |line| 60 index, uri = line.chomp.split("\t", 2) 61 unless index&.match?(/\A[0-3]\z/) && uri 62 warn "#{parser.program_name}: malformed indexed output: #{line.chomp}" 63 return 1 64 end 65 reports[index.to_i] << uri 66 end 67 68 reports.each_with_index do |uris, index| 69 name = format("%d_%02d_%04d.txt", index + 1, month, year) 70 target = File.join(output_directory, name) 71 Tempfile.create([".#{name}.", ".tmp"], output_directory) do |file| 72 uris.each { |uri| file.puts(uri) } 73 file.flush 74 file.fsync 75 file.close 76 File.rename(file.path, target) 77 end 78 puts target 79 end 80 0 81 rescue OptionParser::ParseError, ArgumentError => error 82 warn "#{parser.program_name}: #{error.message}" 83 warn parser 84 2 85 rescue Errno::ENOENT => error 86 warn "#{parser.program_name}: #{error.message}" 87 127 88 rescue SystemCallError => error 89 warn "#{parser.program_name}: #{error.message}" 90 1 91 end 92 93 exit main(ARGV)