dubbletrack_remote 0.7.1
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 +7 -0
- data/.github/workflows/release.yml +27 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitignore +48 -0
- data/.rspec +1 -0
- data/.rubocop.yml +54 -0
- data/.tool-versions +1 -0
- data/Gemfile +2 -0
- data/README.md +14 -0
- data/Rakefile +29 -0
- data/bin/console +14 -0
- data/bin/dubbletrack_remote +6 -0
- data/config/database.yml +15 -0
- data/config/settings-example.yml +8 -0
- data/db/migrate/001_schema.rb +19 -0
- data/db/migrate/002_add_item_columns.rb +8 -0
- data/db/migrate/003_add_debug_item_columns.rb +7 -0
- data/db/migrate/004_add_automation_system_column.rb +5 -0
- data/db/schema.rb +40 -0
- data/db/seed.rb +0 -0
- data/dubbletrack-remote +0 -0
- data/dubbletrack_remote.gemspec +64 -0
- data/enco-support/Asplay.rpg +34 -0
- data/enco-support/readme.txt +15 -0
- data/lib/dubbletrack_remote/cli.rb +353 -0
- data/lib/dubbletrack_remote/client.rb +136 -0
- data/lib/dubbletrack_remote/errors.rb +36 -0
- data/lib/dubbletrack_remote/item.rb +79 -0
- data/lib/dubbletrack_remote/reader/base.rb +57 -0
- data/lib/dubbletrack_remote/reader/dbf.rb +143 -0
- data/lib/dubbletrack_remote/reader/tsv.rb +112 -0
- data/lib/dubbletrack_remote/version.rb +3 -0
- data/lib/dubbletrack_remote.rb +29 -0
- data/package.json +92 -0
- metadata +428 -0
@@ -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,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
|
+
}}
|