require "thor/rake_compat" require "rspec/core/rake_task" require "sinatra/activerecord/rake" require "pry" require "fileutils" module DubbletrackRemote class CLI < Thor include Thor::Actions include Thor::RakeCompat class_option :debug, type: :boolean, default: false class_option :dir, type: :string, default: "/opt/dubbletrack-remote/" desc "watch", "watch playlists folder and post on changes" def watch ensure_setup Reader.watch(watch_path, pattern: watch_pattern) do |file| puts "detected change in #{file}" Reader.new(file, {cuts_path: local_cuts_file}).ingest end Thread.new do loop do client.send(Item.next(25)) update # this feels redundant with the watch above status rescue => e puts "errored, but will try to move on" puts e ensure sleep 10 end end backfill sleep end default_task :watch desc "status", "get status" def status ensure_setup puts "total remaining: #{Item.remaining.count}" puts "total transmitted: #{Item.successful.count}" puts "recent items needing transmission: #{Item.recent_remaining.count}" end desc "config", "open config file" def config system("open #{settings_path}") end desc "setup", "setup dubbletrack remote for the first time" def setup create_settings_file ensure_setup end desc "update", "post latest tracks from current playlist" def update ensure_setup if todays_playlist_path [todays_playlist_path].flatten.each { |f| Reader.new(f, {cuts_path: local_cuts_file}).ingest } else puts "no file found matching pattern #{playlist_pattern}" end end desc "post FILE_PATH or DATES", "post latest track from playlist" def post(*args) ensure_setup paths = args_to_paths(args) if paths.any? items = paths.collect do |path| puts "reading #{path}" items = Reader.new(path, {cuts_path: local_cuts_file}).items end.flatten.sort_by(&:played_at) remaining = Item.where(id: items.pluck(:id)).remaining if remaining.count > 0 puts "sending #{remaining.size} from playlist #{file_path}" Item.where(id: remaining.pluck(:id)).remaining.find_in_batches(batch_size: 25) do |items| client.send(items) end end else puts "could not find any files with args: #{args}" end end # desc "next SIZE", "check the next up to be sent" # def next(size = 20) # ensure_setup # Item.next(size).each do |item| # puts item.pretty_print # end # end desc "read FILE_PATH or DATES", "read tracks from playlist" option :raw, type: :boolean, description: "Show raw fields?", default: false def read(*args) ensure_setup paths = args_to_paths(args) if paths.any? items = paths.collect do |path| puts "reading #{path}" items = Reader.new(path, {cuts_path: local_cuts_file}).items end.flatten.sort_by(&:played_at) items.each do |e| puts e.pretty_print puts e.raw if options[:raw] end else puts "could not find any files with args: #{args}" end end desc "import FILE_PATH or DATES", "import tracks from playlist" def import(*args) ensure_setup paths = args_to_paths(args) if paths.any? paths.each do |path| puts "importing #{path}" items = Reader.new(path, {cuts_path: local_cuts_file}).ingest items.each { |e| puts e.pretty_print } end else puts "could not find any files with args:" end end desc "backfill", "read previous playlists and post changes" def backfill(from_date = nil) ensure_setup from_date = from_date ? Date.parse(from_date) : Date.today - 7.days paths = [] while from_date < Date.today paths << playlist_path_from_date(from_date) from_date += 1.day end paths.flatten! if paths.any? paths.each do |path| puts "importing #{path}" items = Reader.new(path, {cuts_path: local_cuts_file}).ingest items.each { |e| puts e.pretty_print } end else puts "could not find any files with args:" end end # desc "list", "list all available files based on settings" # def list # ensure_setup # playlist_pattern, _ = settings[:automation][:playlist_path].split("%") # playlist_pattern = "#{playlist_pattern}*" # files = Dir.glob(playlist_pattern) # files.map { |f| File.expand_path(f) } # end desc "console", "" def console ensure_setup # turns on logging of SQL queries while in the task ActiveRecord::Base.logger = Logger.new(STDOUT) # starts a Ruby REPL session Pry.start end desc "version", "prints current dubbletrack_remote version" def version ensure_setup puts "version: #{DubbletrackRemote::VERSION}" end # no_commands do # def options # super # # original_options = super # # defaults = if original_options[:dir] # the settings path was passed in # # config = File.expand_path(File.join(original_options[:dir], settings_file_name)) # # if File.exist?(config) # # ::YAML.load_file(config) || {} # # else # # {} # # end # # end # # return Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options)) # end # end protected def update_current_cuts_file? return true if find_local_cuts_file.blank? latest_time = Time.at(File.basename(find_local_cuts_file, File.extname(find_local_cuts_file)).split("_").last.to_i) latest_time < 6.hours.ago end def delete_old_cuts deletable = Dir.glob(File.join(options[:dir], "CUTS_*.DBF")).sort.reverse.slice(1...-1) || [] deletable.each { |to_delete| FileUtils.rm(to_delete) } end def find_local_cuts_file cuts = Dir.glob(File.join(options[:dir], "CUTS_*.DBF")).sort.reverse cuts.last end def local_cuts_file if remote_cuts_path = Settings.automation.cuts_path if update_current_cuts_file? current_cut_path = File.join(options[:dir], "CUTS_#{Time.now.to_i}.DBF") delete_old_cuts FileUtils.cp(remote_cuts_path, current_cut_path) return current_cut_path end find_local_cuts_file end end def args_to_paths(args) paths = [] args.each do |arg| if File.exist?(arg) paths << arg elsif arg.scan(/\d\d\d\d-\d\d-\d\d/) paths << playlist_path_from_date(Time.zone.parse(arg)) end end paths = paths.flatten.compact end def client @client ||= Client.new( url: settings[:api_url], key: settings[:api_key], secret: settings[:api_secret] ) end def watch_path path = File.expand_path(settings.dig(:automation, :playlist_path)) File.dirname(path) end def watch_pattern path = File.expand_path(settings.dig(:automation, :playlist_path)) Regexp.new(File.basename(path).gsub(/%\w/, ".+"), "i") end def watch_extension path = File.expand_path(settings.dig(:automation, :playlist_path)) Regexp.new("#{File.extname(path).downcase}$", "i") end def todays_playlist_path playlist_path_from_date(Time.now) end def playlist_path_from_date(date = Time.now) playlist_pattern = date.strftime(settings[:automation][:playlist_path]) files = Dir.glob(playlist_pattern) files.map { |f| File.expand_path(f) } end def settings Settings end def settings_path File.expand_path(File.join(options[:dir], settings_file_name)) end def settings_file_name ".dubbletrack-remote-settings" end def ensure_setup connect_db if !File.exist?(settings_path) if yes? "dubbletrack_remote is not setup at #{options[:dir]}. Setup? (y/n)" create_settings_file end end Config.load_and_set_settings(settings_path) Time.zone = Settings.automation.time_zone begin Rake::Task["db:migrate"].invoke rescue end end def create_settings_file content = "" create_file(settings_path) do station_id = ask "What's your dubbletrack station ID? (e.g. koop, kvrx)" api_key = ask "What's your dubbletrack API Key?" api_secret = ask "What's your dubbletrack API Secret?", echo: false automation_type = ask "What kind of automation system is this?", default: "enco" dump_delay = ask "How much of a delay (in seconds) is there before broadcast? (i.e. do you have a dump button)", default: 0 time_zone = ask "What timezone are you in?", default: "America/Chicago" if automation_type == "enco" format = "dbf" cuts_path = ask "Where is the cuts database? (CUTS.DBF)", path: true playlists_path = ask "Where should I look for playlists? (This can include regex and strftime components, i.e. /Enco/Dad/STUDIO[1-2]%m%d%y.DBF)", path: true end <<~YAMS.strip api_url: https://api.dubbletrack.com/api/v2/automation api_key: #{api_key} api_secret: #{api_secret} automation: time_zone: #{time_zone} type: #{automation_type} playlist_path: #{playlists_path} cuts_path: #{cuts_path} delay_seconds: #{dump_delay} YAMS end end def connect_db unless ENV["DUBBLETRACK_REMOTE_ENV"] == "test" Sinatra::Application.set :database, { adapter: "sqlite3", database: File.expand_path(File.join(options[:dir], "dubbletrack-remote.sqlite3")) } end end end end