musicbox 0.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,179 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Album < Group::Item
6
+
7
+ attr_accessor :title
8
+ attr_accessor :artist
9
+ attr_accessor :year
10
+ attr_accessor :discs
11
+ attr_accessor :tracks
12
+
13
+ def initialize(params={})
14
+ @tracks = []
15
+ super
16
+ end
17
+
18
+ def tracks=(tracks)
19
+ @tracks = tracks.map { |h| AlbumTrack.new(h.merge(album: self)) }
20
+ end
21
+
22
+ def date=(date)
23
+ @year = case date
24
+ when Date
25
+ date.year
26
+ when String
27
+ date.to_i
28
+ else
29
+ date
30
+ end
31
+ end
32
+
33
+ def release_id=(id)
34
+ @id = id
35
+ end
36
+
37
+ def log_files=(*); end
38
+
39
+ def artist
40
+ @artist || @tracks&.first&.artist
41
+ end
42
+
43
+ def cover_file
44
+ files = @dir.glob('cover.{jpg,png}')
45
+ raise Error, "Multiple cover files" if files.length > 1
46
+ files.first
47
+ end
48
+
49
+ def has_cover?
50
+ !cover_file.nil?
51
+ end
52
+
53
+ def show_cover(width: nil, height: nil, preserve_aspect_ratio: nil)
54
+ # see https://iterm2.com/documentation-images.html
55
+ file = cover_file
56
+ if file && file.exist?
57
+ data = Base64.strict_encode64(file.read)
58
+ args = {
59
+ name: Base64.strict_encode64(file.to_s),
60
+ size: data.length,
61
+ width: width,
62
+ height: height,
63
+ preserveAspectRatio: preserve_aspect_ratio,
64
+ inline: 1,
65
+ }.compact
66
+ puts "\033]1337;File=%s:%s\a" % [
67
+ args.map { |a| a.join('=') }.join(';'),
68
+ data,
69
+ ]
70
+ end
71
+ end
72
+
73
+ def validate_logs
74
+ log_files = @dir.glob('*.log')
75
+ raise Error, "No rip logs" if log_files.empty?
76
+ state = :initial
77
+ log_files.each do |log_file|
78
+ log_file.readlines.map(&:chomp).each do |line|
79
+ case state
80
+ when :initial
81
+ if line =~ /^AccurateRip Summary/
82
+ state = :accuraterip_summary
83
+ end
84
+ when :accuraterip_summary
85
+ if line =~ /^\s+Track \d+ : (\S+)/
86
+ raise Error, "Not accurately ripped" unless $1 == 'OK'
87
+ else
88
+ break
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def update_tags(force: false)
96
+ changes = []
97
+ @tracks.each do |track|
98
+ track.update_tags
99
+ changes << track if track.tags.changed?
100
+ end
101
+ unless changes.empty?
102
+ puts
103
+ puts "#{@title} [#{@dir}]"
104
+ changes.each do |track|
105
+ puts "\t" + track.file.to_s
106
+ track.tags.changes.each do |change|
107
+ puts "\t\t" + change.inspect
108
+ end
109
+ end
110
+ if force || TTY::Prompt.new.yes?('Update track files?')
111
+ changes.each do |track|
112
+ track.save_tags
113
+ end
114
+ end
115
+ end
116
+ if has_cover?
117
+ # --replace apparently doesn't work, so must do --remove, then --add
118
+ @tracks.each do |track|
119
+ begin
120
+ run_command('mp4art',
121
+ '--quiet',
122
+ '--remove',
123
+ track.path)
124
+ rescue RunCommandFailed => e
125
+ # ignore
126
+ end
127
+ run_command('mp4art',
128
+ '--quiet',
129
+ '--add',
130
+ cover_file,
131
+ track.path)
132
+ end
133
+ end
134
+ end
135
+
136
+ def extract_cover
137
+ if has_cover?
138
+ puts "#{@id}: already has cover"
139
+ return
140
+ end
141
+ file = @dir / @tracks.first.file
142
+ begin
143
+ run_command('mp4art',
144
+ '--extract',
145
+ '--art-index', 0,
146
+ '--overwrite',
147
+ '--quiet',
148
+ file)
149
+ rescue RunCommandFailed => e
150
+ # ignore
151
+ end
152
+ # cover is in FILE.art[0].TYPE
153
+ files = @dir.glob('*.art*.*').reject { |f| f.extname.downcase == '.gif' }
154
+ if files.length == 0
155
+ puts "#{@id}: no cover to extract"
156
+ elsif files.length > 1
157
+ raise Error, "Multiple covers found"
158
+ else
159
+ file = files.first
160
+ new_cover_file = (@dir / 'cover').add_extension(file.extname)
161
+ puts "#{@id}: extracted cover: #{new_cover_file.basename}"
162
+ file.rename(new_cover_file)
163
+ end
164
+ end
165
+
166
+ def serialize
167
+ super(
168
+ title: @title,
169
+ artist: @artist,
170
+ year: @year,
171
+ discs: @discs,
172
+ tracks: @tracks.map(&:to_h))
173
+ end
174
+
175
+ end
176
+
177
+ end
178
+
179
+ end
@@ -0,0 +1,66 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class AlbumTrack
6
+
7
+ attr_accessor :title
8
+ attr_accessor :artist
9
+ attr_accessor :track
10
+ attr_accessor :disc
11
+ attr_accessor :album
12
+ attr_accessor :file
13
+ attr_accessor :tags
14
+
15
+ def initialize(params={})
16
+ params.each { |k, v| send("#{k}=", v) }
17
+ end
18
+
19
+ def file=(file)
20
+ @file = Path.new(file)
21
+ end
22
+
23
+ def path
24
+ @album.dir / @file
25
+ end
26
+
27
+ def load_tags
28
+ @tags ||= Tags.load(path)
29
+ end
30
+
31
+ def save_tags
32
+ @tags.save(path)
33
+ end
34
+
35
+ def update_tags
36
+ load_tags
37
+ @tags.update(
38
+ {
39
+ title: @title,
40
+ album: @album.title,
41
+ track: @track,
42
+ disc: @disc,
43
+ discs: @album.discs,
44
+ artist: @artist || @album.artist,
45
+ album_artist: @album.artist,
46
+ grouping: @album.title,
47
+ year: @album.year,
48
+ }.reject { |k, v| v.to_s.empty? }
49
+ )
50
+ end
51
+
52
+ def to_h
53
+ {
54
+ title: @title,
55
+ artist: @artist,
56
+ track: @track,
57
+ disc: @disc,
58
+ file: @file.to_s,
59
+ }.compact
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,19 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Albums < Group
6
+
7
+ def self.item_class
8
+ Album
9
+ end
10
+
11
+ def new_album(id, args={})
12
+ new_item(id, args)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,52 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Artist < Group::Item
6
+
7
+ attr_accessor :aliases
8
+ attr_accessor :data_quality
9
+ attr_accessor :groups
10
+ attr_accessor :images #FIXME: make Image class?
11
+ attr_accessor :members
12
+ attr_accessor :name
13
+ attr_accessor :namevariations
14
+ attr_accessor :profile
15
+ attr_accessor :realname
16
+ attr_accessor :releases_url
17
+ attr_accessor :resource_url
18
+ attr_accessor :uri
19
+ attr_accessor :urls
20
+
21
+ def aliases=(aliases)
22
+ @aliases = aliases.map { |a| ReleaseArtist.new(a) }
23
+ end
24
+
25
+ def groups=(groups)
26
+ @groups = groups.map { |a| ReleaseArtist.new(a) }
27
+ end
28
+
29
+ def members=(members)
30
+ @members = members.map { |a| ReleaseArtist.new(a) }
31
+ end
32
+
33
+ def to_s
34
+ @name
35
+ end
36
+
37
+ def summary_to_s
38
+ '%-8s | %s' % [
39
+ @id,
40
+ @name,
41
+ ]
42
+ end
43
+
44
+ def <=>(other)
45
+ @name <=> other.name
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,15 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Artists < Group
6
+
7
+ def self.item_class
8
+ Artist
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,15 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Collection < Group
6
+
7
+ def self.item_class
8
+ CollectionItem
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,35 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class CollectionItem < Group::Item
6
+
7
+ attr_accessor :basic_information
8
+ attr_accessor :date_added
9
+ attr_accessor :folder_id
10
+ attr_accessor :instance_id
11
+ attr_accessor :notes
12
+ attr_accessor :rating
13
+ attr_accessor :resource_url
14
+ attr_accessor :release # linked on load
15
+
16
+ def date_added=(date)
17
+ @date_added = DateTime.parse(date.to_s)
18
+ end
19
+
20
+ def serialize(args={})
21
+ super(
22
+ basic_information: @basic_information,
23
+ date_added: @date_added,
24
+ folder_id: @folder_id,
25
+ instance_id: @instance_id,
26
+ notes: @notes,
27
+ rating: @rating,
28
+ resource_url: @resource_url)
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,52 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Format
6
+
7
+ attr_accessor :descriptions
8
+ attr_accessor :name
9
+ attr_accessor :qty
10
+ attr_accessor :text
11
+
12
+ def self.to_s(formats)
13
+ formats.map(&:short_to_s).join(', ')
14
+ end
15
+
16
+ def initialize(params={})
17
+ params.each { |k, v| send("#{k}=", v) }
18
+ end
19
+
20
+ def qty=(n)
21
+ @qty = n.to_i
22
+ end
23
+
24
+ def to_s
25
+ @name + descriptions_to_s + qty_to_s
26
+ end
27
+
28
+ def short_to_s
29
+ @name + qty_to_s
30
+ end
31
+
32
+ def descriptions_to_s
33
+ @descriptions ? " (#{@descriptions.join(', ')})" : ''
34
+ end
35
+
36
+ def qty_to_s
37
+ (@qty && @qty > 1) ? " [#{@qty}]" : ''
38
+ end
39
+
40
+ def cd?
41
+ @name == 'CD'
42
+ end
43
+
44
+ def vinyl?
45
+ @name == 'Vinyl'
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,243 @@
1
+ class MusicBox
2
+
3
+ class Catalog
4
+
5
+ class Release < Group::Item
6
+
7
+ attr_accessor :artists
8
+ attr_accessor :artists_sort
9
+ attr_accessor :blocked_from_sale
10
+ attr_accessor :community
11
+ attr_accessor :companies
12
+ attr_accessor :country
13
+ attr_accessor :data_quality
14
+ attr_accessor :date_added
15
+ attr_accessor :date_changed
16
+ attr_accessor :estimated_weight
17
+ attr_accessor :extraartists
18
+ attr_accessor :format_quantity
19
+ attr_accessor :formats
20
+ attr_accessor :genres
21
+ attr_accessor :identifiers
22
+ attr_accessor :images
23
+ attr_accessor :labels
24
+ attr_accessor :lowest_price
25
+ attr_accessor :main_release
26
+ attr_accessor :main_release_url
27
+ attr_accessor :master_id
28
+ attr_accessor :master_url
29
+ attr_accessor :most_recent_release
30
+ attr_accessor :most_recent_release_url
31
+ attr_accessor :notes
32
+ attr_accessor :num_for_sale
33
+ attr_accessor :released
34
+ attr_accessor :released_formatted
35
+ attr_accessor :resource_url
36
+ attr_accessor :series
37
+ attr_accessor :status
38
+ attr_accessor :styles
39
+ attr_accessor :thumb
40
+ attr_accessor :tracklist
41
+ attr_accessor :title
42
+ attr_accessor :uri
43
+ attr_accessor :versions_url
44
+ attr_accessor :videos
45
+ attr_accessor :year
46
+ attr_accessor :master # linked on load
47
+ attr_accessor :album # linked on load
48
+
49
+ def self.csv_header
50
+ %w[ID year artist title].to_csv
51
+ end
52
+
53
+ def artists=(artists)
54
+ @artists = artists.map { |a| ReleaseArtist.new(a) }
55
+ end
56
+
57
+ def extraartists=(artists)
58
+ @extraartists = artists.map { |a| ReleaseArtist.new(a) }
59
+ end
60
+
61
+ def date_added=(date)
62
+ @date_added = DateTime.parse(date.to_s)
63
+ end
64
+
65
+ def date_changed=(date)
66
+ @date_changed = DateTime.parse(date.to_s)
67
+ end
68
+
69
+ def formats=(formats)
70
+ @formats = formats.map { |f| Format.new(f) }
71
+ end
72
+
73
+ def tracklist=(tracklist)
74
+ @tracklist = tracklist.map { |t| Track.new(t) }
75
+ end
76
+
77
+ def release_year
78
+ if @year && @year != 0
79
+ @year
80
+ elsif @released
81
+ @released.to_s.split('-').first&.to_i
82
+ end
83
+ end
84
+
85
+ def original_release_year
86
+ @master&.release_year || release_year
87
+ end
88
+
89
+ def cd?
90
+ @formats.find(&:cd?) != nil
91
+ end
92
+
93
+ def artist
94
+ ReleaseArtist.artists_to_s(@artists)
95
+ end
96
+
97
+ def artist_key
98
+ @artists.first.key
99
+ end
100
+
101
+ def <=>(other)
102
+ sort_tuple <=> other.sort_tuple
103
+ end
104
+
105
+ def sort_tuple
106
+ [artist_key, original_release_year || 0, @title]
107
+ end
108
+
109
+ def to_csv
110
+ [@id, original_release_year, artist, @title].to_csv
111
+ end
112
+
113
+ def to_s
114
+ summary_to_s
115
+ end
116
+
117
+ def summary_to_s
118
+ '%-8s | %1s%1s | %-4s %4s | %-50.50s | %-60.60s | %-6s' % [
119
+ @id,
120
+ @album ? 'A' : '',
121
+ @album&.has_cover? ? 'C' : '',
122
+ artist_key,
123
+ original_release_year || '-',
124
+ artist,
125
+ @title,
126
+ Format.to_s(@formats),
127
+ ]
128
+ end
129
+
130
+ def details_to_s
131
+ info = [
132
+ ['ID', @id],
133
+ ['Master ID', @master_id || '-'],
134
+ ['Artist', ReleaseArtist.artists_to_s(@artists)],
135
+ ['Title', @title],
136
+ ['Formats', Format.to_s(@formats)],
137
+ ['Released', release_year || '-'],
138
+ ['Originally released', original_release_year || '-'],
139
+ ['Discogs URI', @uri || '-'],
140
+ ['Album dir', @album&.dir || '-'],
141
+ ['Tracks', nil, tracklist_to_info],
142
+ ]
143
+ MusicBox.info_to_s(info)
144
+ end
145
+
146
+ def tracklist_flattened(tracklist=nil)
147
+ tracklist ||= @tracklist
148
+ tracks = []
149
+ tracklist.each do |track|
150
+ tracks << track
151
+ tracks += tracklist_flattened(track.sub_tracks) if track.type == 'index'
152
+ end
153
+ tracks
154
+ end
155
+
156
+ def tracklist_to_info(tracklist=nil)
157
+ tracklist ||= @tracklist
158
+ max_position_length = tracklist.select(&:position).map { |t| t.position.to_s.length }.max
159
+ tracklist.map do |track|
160
+ [
161
+ track.type,
162
+ [
163
+ !track.position.to_s.empty? ? ('%*s:' % [max_position_length, track.position]) : nil,
164
+ track.title || '-',
165
+ track.artists ? "(#{ReleaseArtist.artists_to_s(track.artists)})" : nil,
166
+ !track.duration.to_s.empty? ? "[#{track.duration}]" : nil,
167
+ ].compact.join(' '),
168
+ track.sub_tracks ? tracklist_to_info(track.sub_tracks) : nil,
169
+ ]
170
+ end
171
+ end
172
+
173
+ def to_label
174
+ {
175
+ artist: artist,
176
+ title: title,
177
+ key: artist_key,
178
+ year: original_release_year,
179
+ format: Format.to_s(@formats),
180
+ id: id,
181
+ }
182
+ end
183
+
184
+ def link_images(images_dir)
185
+ if @images
186
+ @images.each do |image|
187
+ uri = URI.parse(image['uri'])
188
+ image['file'] = images_dir / Path.new(uri.path).basename
189
+ end
190
+ end
191
+ end
192
+
193
+ def download_images
194
+ image = @images.find { |image| image['type'] == 'primary' }
195
+ if image
196
+ download_image(uri: image['uri'], file: image['file'])
197
+ else
198
+ @images.each do |image|
199
+ download_image(uri: image['uri'], file: image['file'])
200
+ end
201
+ end
202
+ @master.download_images if @master
203
+ end
204
+
205
+ def download_image(uri:, file:)
206
+ if uri && file
207
+ unless file.exist?
208
+ puts "#{@id}: downloading #{uri}"
209
+ file.dirname.mkpath unless file.dirname.exist?
210
+ file.write(HTTP.get(uri))
211
+ sleep(1)
212
+ end
213
+ end
214
+ end
215
+
216
+ def select_cover
217
+ if @album.has_cover?
218
+ puts "#{@id}: cover already exists"
219
+ return
220
+ end
221
+ download_images
222
+ @album.extract_cover
223
+ choices = [
224
+ @master&.images&.map { |i| i['file'] },
225
+ @images&.map { |i| i['file'] },
226
+ @album.cover_file,
227
+ ].flatten.compact.uniq.select(&:exist?)
228
+ if choices.empty?
229
+ puts "#{@id}: no covers exist"
230
+ else
231
+ choices.each { |f| run_command('open', f) }
232
+ choice = TTY::Prompt.new.select('Cover?', choices)
233
+ cover_file = (album.dir / 'cover').add_extension(choice.extname)
234
+ choice.cp(cover_file)
235
+ @album.update_tags
236
+ end
237
+ end
238
+
239
+ end
240
+
241
+ end
242
+
243
+ end