dubbletrack_remote 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,143 @@
1
+ # Reads in a DBF file and creates items out of them, annotating with information from CUTS
2
+ module DubbletrackRemote
3
+ module Reader
4
+ class DBF < Base
5
+ attr_reader :use_database
6
+
7
+ def initialize(file_path, cuts_path: nil, use_database: true)
8
+ raise "cuts not found at #{cuts_path}" unless File.exist?(cuts_path)
9
+ raise "db not found at #{file_path}" unless File.exist?(file_path)
10
+
11
+ @file_path = file_path
12
+ @automation_system = "enco"
13
+ @db = ::DBF::Table.new(File.open(file_path))
14
+ @cuts = ::DBF::Table.new(File.open(cuts_path))
15
+ @use_database = use_database
16
+ end
17
+
18
+ protected
19
+
20
+ def timestamp(date, time)
21
+ Time.zone = "America/Chicago"
22
+ month, day, year = date.split("/")
23
+ Time.zone.parse("20#{year}-#{month}-#{day} #{time}").in_time_zone(Time.zone)
24
+ rescue => e
25
+ puts e.inspect
26
+ puts "date = #{date}, time = #{time}"
27
+ date
28
+ end
29
+
30
+ def rows
31
+ @rows ||= @db.entries
32
+ end
33
+
34
+ def entry_to_hash(entry)
35
+ # {"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"=>""}
36
+ attrs = entry.attributes.dup
37
+
38
+ (cut_index[entry.cut] || {}).keys.each do |key|
39
+ attrs[key] = attrs[key] || cut_index[entry.cut][key]
40
+ end
41
+
42
+ attrs.keys.each do |key|
43
+ attrs[key] = attrs[key].mb_chars.tidy_bytes.to_s
44
+ end
45
+
46
+ attrs
47
+ end
48
+
49
+ def cut_index
50
+ @cut_index ||= {}.tap do |hash|
51
+ ids = @db.entries.pluck("CUT")
52
+
53
+ @cuts.each do |entry|
54
+ if entry && ids.include?(entry.cut)
55
+ hash[entry.cut] = entry.attributes
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def data
62
+ @data ||= (rows || []).collect { |entry|
63
+ entry_to_hash(entry)
64
+ }
65
+ end
66
+
67
+ def existing_items
68
+ @existing_items ||= Item.where(played_at: data.collect { |hash|
69
+ timestamp(hash["DATE"], hash["ACTSTART"])
70
+ })
71
+ end
72
+
73
+ def read_items
74
+ items = []
75
+ return items unless data
76
+
77
+ data.each do |hash|
78
+ next if hash.all? { |k, v| k.strip == v.strip }
79
+
80
+ title = (hash["TITLE"] || "").strip
81
+ artist = (hash["ARTIST"] || "").strip
82
+ album = (hash["ALBUM"] || "").strip
83
+ label = (hash["PRODUCER"] || "").strip
84
+ date = (hash["DATE"] || "").strip
85
+ time = (hash["ACTSTART"] || "").strip
86
+ duration = hash["LENGTH"] || 0
87
+ automation_id = (hash["CUT"] || "").strip
88
+ automation_group = (hash["GROUP"] || "").strip
89
+ genre = (hash["GENRE"] || "").strip
90
+ guid = (hash["GUID"] || "").strip
91
+ played_at = timestamp(date, time)
92
+ intended_played_at_time = (hash["SCHSTART"] || "").strip
93
+ intended_played_at = timestamp(date, intended_played_at_time) if intended_played_at_time
94
+
95
+ item = if @use_database
96
+ # Use intended played at for keying over multiple ENCO installs.
97
+ # It isn't that reliable and if two encos are active in the future some other method should be found
98
+
99
+ next if items.find { |i| i.played_at == played_at && i.automation_id == automation_id }
100
+
101
+ found = existing_items.find { |i| i.played_at == played_at && i.automation_id == automation_id }
102
+ end
103
+
104
+ item ||= Item.new(played_at: played_at)
105
+
106
+ if item.new_record? || item.artist_name.blank?
107
+ item.title = title
108
+ item.label_name = label
109
+ item.artist_name = artist
110
+ item.release_name = album
111
+ item.duration = duration
112
+ item.automation_system = @automation_system
113
+ item.automation_id = automation_id
114
+ item.automation_group = automation_group
115
+ item.genre = genre
116
+ item.source = File.basename(@file_path)
117
+ item.guid = guid
118
+
119
+ if played_at.dst? != intended_played_at.dst? && intended_played_at.to_i > played_at.to_i
120
+ # This compensates and sends correct time during the hour where DST changes and the hour is repeated
121
+ difference = intended_played_at.hour - played_at.hour
122
+ played_at = played_at + difference.hours
123
+ end
124
+
125
+ item.played_at = played_at
126
+ item.intended_played_at = intended_played_at
127
+
128
+ # puts "#{automation_id}: #{played_at.utc} (#{played_at}) #{item.title}"
129
+
130
+ end
131
+
132
+ item.raw = hash
133
+ item.valid? # run validations, set item_type
134
+
135
+
136
+ items << item
137
+ end
138
+
139
+ items.sort_by(&:played_at)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,112 @@
1
+ # Reads in a TSV file and creates items out of them
2
+ module DubbletrackRemote
3
+ module Reader
4
+ class TSV < Base
5
+ attr_reader :use_database
6
+
7
+ def initialize(path, use_database: true)
8
+ @tsv_path = path # ENV['ENCO_LOG_PATH'] || 'RA071819.CSV' ||"K:\\DAD\\ASPLAY\\"
9
+
10
+ @use_database = use_database
11
+ end
12
+
13
+ protected
14
+
15
+ def header
16
+ @header ||= entries.slice(0, 1).flatten
17
+ end
18
+
19
+ def header_indexes
20
+ @header_indexes ||= {}.tap do |hi|
21
+ header.flatten.each_with_index do |value, index|
22
+ hi[value] = index
23
+ end
24
+ end
25
+ end
26
+
27
+ def timestamp(date, time)
28
+ month, day, year = date.split("/")
29
+ Time.zone.parse("20#{year}-#{month}-#{day} #{time}")
30
+ end
31
+
32
+ def rows
33
+ @rows ||= entries.slice(1..-1)
34
+ end
35
+
36
+ def parse_line(line)
37
+ parsed = CSV.parse(line.to_s, liberal_parsing: true, col_sep: "\t", headers: false).try(:first)
38
+ if parsed.flatten.size > 0 && parsed.any? { |p| p.present? }
39
+ parsed.flatten.collect { |v| v.strip }
40
+ end
41
+ end
42
+
43
+ def entries
44
+ @entries ||= file_lines.collect do |line|
45
+ parse_line(line) unless line.to_s.strip.blank?
46
+ end.compact
47
+ end
48
+
49
+ def file_lines
50
+ @file_lines ||= begin
51
+ file = File.read(@tsv_path)
52
+ cd = CharDet.detect(file)
53
+ file = File.read(@tsv_path, encoding: cd["encoding"])
54
+
55
+ lines = file.lines.collect do |line|
56
+ line.mb_chars.tidy_bytes.to_s
57
+ end.select { |l|
58
+ l.to_s.strip.present?
59
+ }
60
+ rescue
61
+ lines = [] unless lines.present?
62
+ end
63
+ end
64
+
65
+ def entry_to_hash(entry)
66
+ label_to_index = header_indexes
67
+ index_to_label = header_indexes.invert
68
+
69
+ entry_to_hash = {}.tap do |hash|
70
+ entry.each_with_index do |value, index|
71
+ hash[index_to_label[index]] = value
72
+ end
73
+ end
74
+ end
75
+
76
+ def read_items
77
+ items = []
78
+ return items unless rows
79
+
80
+ rows.each do |entry|
81
+ hash = entry_to_hash(entry)
82
+
83
+ next if hash.all? { |h, v| h.strip == v.strip }
84
+
85
+ title = (hash["TITLE"] || "").strip
86
+ artist = (hash["ARTIST"] || "").strip
87
+ album = (hash["ALBUM"] || "").strip
88
+ label = (hash["LABEL"] || "").strip
89
+ date = (hash["DATE"] || "").strip
90
+ time = (hash["TIME"] || "").strip
91
+
92
+ item = if use_database
93
+ Item.find_or_initialize_by(played_at: timestamp(date, time))
94
+ else
95
+ Item.new(played_at: timestamp(date, time))
96
+ end
97
+ if item.new_record?
98
+ item.title = title
99
+ item.label_name = label
100
+ item.artist_name = artist
101
+ item.release_name = album
102
+ end
103
+ item.valid? # run validations, set item_type
104
+
105
+ items << item
106
+ end
107
+
108
+ items
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,3 @@
1
+ module DubbletrackRemote
2
+ VERSION = "0.7.1"
3
+ end
@@ -0,0 +1,29 @@
1
+ require "bundler/setup"
2
+ require "sinatra/activerecord"
3
+ require "thor"
4
+ require "byebug"
5
+ require "rchardet"
6
+ require "csv"
7
+ require "dbf"
8
+ require "logger"
9
+ require "listen"
10
+ require "faraday"
11
+ require "config"
12
+ require "active_support/all"
13
+
14
+ require "dubbletrack_remote/client"
15
+ require "dubbletrack_remote/cli"
16
+ require "dubbletrack_remote/errors"
17
+ require "dubbletrack_remote/reader/base"
18
+ require "dubbletrack_remote/reader/tsv"
19
+ require "dubbletrack_remote/reader/dbf"
20
+ require "dubbletrack_remote/item"
21
+ require "dubbletrack_remote/version"
22
+
23
+ if ENV["DUBBLETRACK_REMOTE_ENV"] == "test"
24
+ Config.load_and_set_settings(File.expand_path("spec/.dubbletrack-remote-settings"))
25
+ Sinatra::Application.set :database, {
26
+ adapter: "sqlite3",
27
+ database: "db/dubbletrack-remote-test.sqlite3"
28
+ }
29
+ end
data/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@dubbletrack/remote",
3
+ "version": "0.0.0-semantic-release",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/dubbletrack/remote.git"
7
+ },
8
+ "author": "jkeen",
9
+ "license": "MIT",
10
+ "bugs": {
11
+ "url": "https://github.com/dubbletrack/remote/issues"
12
+ },
13
+ "homepage": "https://github.com/dubbletrack/remote#readme",
14
+ "scripts": {
15
+ "semantic-release": "semantic-release"
16
+ },
17
+ "devDependencies": {
18
+ "semantic-release-rubygem": "^1.2.0",
19
+ "semantic-release": "^19.0.3",
20
+ "@semantic-release/changelog": "^6.0.1",
21
+ "@semantic-release/git": "^10.0.1"
22
+ },
23
+ "release": {
24
+ "branches": ["main"],
25
+ "plugins": [
26
+ [
27
+ "@semantic-release/commit-analyzer",
28
+ {
29
+ "releaseRules": [
30
+ {
31
+ "type": "*!",
32
+ "release": "major"
33
+ },
34
+ {
35
+ "type": "feat",
36
+ "release": "minor"
37
+ },
38
+ {
39
+ "type": "build",
40
+ "release": "patch"
41
+ },
42
+ {
43
+ "type": "ci",
44
+ "release": "patch"
45
+ },
46
+ {
47
+ "type": "chore",
48
+ "release": "patch"
49
+ },
50
+ {
51
+ "type": "docs",
52
+ "release": "patch"
53
+ },
54
+ {
55
+ "type": "refactor",
56
+ "release": "patch"
57
+ },
58
+ {
59
+ "type": "style",
60
+ "release": "patch"
61
+ },
62
+ {
63
+ "type": "test",
64
+ "release": "patch"
65
+ }
66
+ ],
67
+ "parserOpts": {
68
+ "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
69
+ }
70
+ }
71
+ ],
72
+ "@semantic-release/release-notes-generator",
73
+ [
74
+ "@semantic-release/changelog",
75
+ {
76
+ "changelogTitle": "dubbletrack remote changelog",
77
+ "changelogFile": "CHANGELOG.md"
78
+ }
79
+ ],
80
+ "semantic-release-rubygem",
81
+ "@semantic-release/github",
82
+ [
83
+ "@semantic-release/git",
84
+ {
85
+ "assets": ["CHANGELOG.md"],
86
+ "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
87
+ }
88
+ ]
89
+ ],
90
+ "debug": false,
91
+ "dryRun": false
92
+ }}