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.
@@ -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
+ }}