musicbox 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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