# Reads in a DBF file and creates items out of them, annotating with information from CUTS module DubbletrackRemote module Reader class DBF < Base attr_reader :use_database def initialize(file_path, cuts_path: nil, use_database: true) raise "cuts not found at #{cuts_path}" unless File.exist?(cuts_path) raise "db not found at #{file_path}" unless File.exist?(file_path) @file_path = file_path @automation_system = "enco" @db = ::DBF::Table.new(File.open(file_path)) @cuts = ::DBF::Table.new(File.open(cuts_path)) @use_database = use_database end protected def timestamp(date, time) Time.zone = "America/Chicago" month, day, year = date.split("/") Time.zone.parse("20#{year}-#{month}-#{day} #{time}").in_time_zone(Time.zone) rescue => e puts e.inspect puts "date = #{date}, time = #{time}" raise e end def rows @rows ||= @db.entries end def entry_to_hash(entry) # {"CUT"=>"00294", "STATUS"=>"1", "TITLE"=>"PSA Austin Music Foundation 2020", "USER"=>"DEF32", "PLAYER"=>"4", "DATE"=>"10/31/20", "SCHSTART"=>"09:01:48", "SCHDUR"=>"00:00:37", "ACTSTART"=>"08:35:39", "ACTDUR"=>"00:00:37", "ACTSTOP"=>"08:36:16", "TYPE"=>"N", "PLAYLIST"=>"1031AUTO", "COMMENT"=>"", "LINEID"=>"", "ALTCUT"=>"", "BOARDID"=>"1", "DEVICEID"=>"0", "GROUP"=>"PSA", "WASPLAYED"=>"T", "LENGTH"=>"37.49", "OUTCUE"=>"", "AGENCY"=>"", "RECORDDATE"=>"09/05/20", "KILLDATE"=>"", "STARTTIME"=>"0.00", "ENDTIME"=>"37.49", "SECTIME"=>"0.00", "TERTIME"=>"37.49", "FORMAT"=>"PCM16", "RATE"=>"48000", "TOT_LENGTH"=>"37.49", "FADEIN"=>"0.00", "FADEOUT"=>"37.49", "MODE"=>"1", "LOCATION"=>"K:", "USERDEF"=>"", "STARTTALK"=>"0.00", "ENDTALK"=>"37.49", "SWITCHER"=>"", "SEGUELEN"=>"37.49", "SEGUESTART"=>"0.00", "SCRIPT"=>"", "FILLCUT"=>"", "STARTDATE"=>"", "NOPLAYS"=>"0161", "LASTPLDATE"=>"11/11/20", "LASTPLTIME"=>"18:31:01", "ACTIVETIME"=>"", "KILLTIME"=>"", "LEVEL"=>"", "PLAYTIME"=>"", "PITCH"=>"", "HOOKSTART"=>"0.00", "HOOKEND"=>"37.49", "EXT"=>"WAV", "GUID"=>"c0e71ba3-dcf0-4a09-8e9e-46976942fb12", "BILLBOARD"=>"", "ARTIST"=>"", "GENRE"=>"", "ALBUM"=>"", "PRODUCER"=>"", "PRODDATE"=>"", "STARTDCL"=>"", "STOPDCL"=>"", "LOOP"=>"", "DEFTRANS"=>"", "DOW"=>"TTTTTTT", "URL"=>"", "FILECHECK"=>"0", "COMPOSER"=>"", "FILMTITLE"=>"", "SUBGROUP"=>"", "RECORDTIME"=>"17:38:35", "ACTOR"=>"", "ACTRESS"=>"", "DIRECTOR"=>"", "LYRICIST"=>"", "ALBUMID"=>"", "SONGID"=>"", "TEMPO"=>"", "GENDER"=>"", "HOD"=>"", "SCHEDAUX"=>"", "REGION"=>"", "PICTURE"=>""} attrs = entry.attributes.dup (cut_index[entry.cut] || {}).keys.each do |key| attrs[key] = attrs[key] || cut_index[entry.cut][key] end attrs.keys.each do |key| attrs[key] = attrs[key].mb_chars.tidy_bytes.to_s end attrs end def cut_index @cut_index ||= {}.tap do |hash| ids = @db.entries.pluck("CUT") @cuts.each do |entry| if entry && ids.include?(entry.cut) && entry.guid.present? hash[entry.cut] = entry.attributes end end end end def data @data ||= (rows || []).collect { |entry| entry_to_hash(entry) } end def existing_items @existing_items ||= Item.where(played_at: data.collect { |hash| timestamp(hash["DATE"], hash["ACTSTART"]) }) end def read_items items = [] return items unless data data.each do |hash| next if hash.all? { |k, v| k.strip == v.strip } title = (hash["TITLE"] || "").strip artist = (hash["ARTIST"] || "").strip album = (hash["ALBUM"] || "").strip label = (hash["PRODUCER"] || "").strip date = (hash["DATE"] || "").strip time = (hash["ACTSTART"] || "").strip duration = (hash["LENGTH"] || "0.0").to_f automation_id = (hash["CUT"] || "").strip automation_group = (hash["GROUP"] || "").strip genre = (hash["GENRE"] || "").strip guid = (hash["GUID"] || "").strip played_at = timestamp(date, time) begin intended_played_at_time = (hash["SCHSTART"] || "").strip intended_played_at = timestamp(date, intended_played_at_time) if intended_played_at_time rescue => e puts e.inspect end unless played_at.respond_to?(:dst?) puts "played_at is not a time: #{played_at.inspect} for #{hash.inspect}" next end item = if @use_database # Use intended played at for keying over multiple ENCO installs. # It isn't that reliable and if two encos are active in the future some other method should be found next if items.find { |i| i.played_at == played_at && i.automation_id == automation_id } found = existing_items.find { |i| i.played_at == played_at && i.automation_id == automation_id } end item ||= Item.new(played_at: played_at) if item.new_record? || item.artist_name.blank? item.title = title item.label_name = label item.artist_name = artist item.release_name = album item.duration = duration item.automation_system = @automation_system item.automation_id = automation_id item.automation_group = automation_group item.genre = genre item.source = File.basename(@file_path) item.guid = guid if played_at.is_a?(Time) && intended_played_at.is_a?(Time) && played_at.dst? != intended_played_at.dst? && intended_played_at.to_i > played_at.to_i # This compensates and sends correct time during the hour where DST changes and the hour is repeated difference = intended_played_at.hour - played_at.hour played_at = played_at + difference.hours end item.played_at = played_at item.intended_played_at = intended_played_at # puts "#{automation_id}: #{played_at.utc} (#{played_at}) #{item.title}" end item.raw = hash item.valid? # run validations, set item_type items << item end items.sort_by(&:played_at) end end end end