musicfix 0.1.0

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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README +91 -0
  4. data/bin/musicfix +277 -0
  5. metadata +89 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f17741eecd6fd928c9cc4dd4a23f413d4df4f37
4
+ data.tar.gz: 9e8d84a8aad5c677fa2a1de96b935575d391ca39
5
+ SHA512:
6
+ metadata.gz: 2289ce8b5a494a1a9bdf817af6d7506fc6721523aa0c83c2a0c70d2801b8d280cb9a7fd7946c19e40bf3b0d1a418d1f2615a4d53cf94e93941e8c19234fbf3ef
7
+ data.tar.gz: ff5ce233fe598ac27e0d23119b289dd4eea27a5640bf47c70ed07a28f1f9eb1e7bbb13bbbdc57a9dada2ee7c220e7f4aa2c5a44dc045bb3854b9c2cc01bda9a3
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2012--2013 Lazaros Koromilas <lostd@2f30.org>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions
6
+ are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
15
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
16
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
17
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
18
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
19
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README ADDED
@@ -0,0 +1,91 @@
1
+ # $Id: README,v 1.11 2013/02/02 09:44:52 lostd Exp $
2
+
3
+ # Description
4
+
5
+ Musicfix is a music file renamer and tagger with consistency concerns.
6
+ It has a release-centered logic and gets data from the Discogs.com site.
7
+ First configure the target music directory (`mdir` config attribute).
8
+ Next browse the site and locate your album. Change to the album's
9
+ directory. Ensure the files are ordered and provide the release id
10
+ to the program. Enjoy!
11
+
12
+
13
+ # Dependencies
14
+
15
+ * System packages: taglib
16
+ * Ruby gems: discogs-wrapper stringex taglib-ruby
17
+
18
+
19
+ # Usage
20
+
21
+ $ musicfix relid [tracks]
22
+ $ musicfix dump relid [relfile]
23
+ $ musicfix load [relfile]
24
+
25
+ More sophisticated track selection can be performed by providing
26
+ a second command line argument of the form:
27
+
28
+ "1--3,5,8"
29
+ "A2--A4,B3"
30
+ "1-8--1-9,1-10--1-11"
31
+
32
+ Alternatively, YAML metadata files can be used (`release.yaml` by default)
33
+ in order to dump information from Discogs.com and/or load the information
34
+ from file. This way you can generate a base release file, edit to
35
+ your needs, and then use it for tagging and renaming.
36
+
37
+
38
+ # Configuration
39
+
40
+ The simplest `~/.musicfixrc` configuration file only contains:
41
+
42
+ mdir: ~/music
43
+
44
+ Furthermore, templates are used for the naming of music files
45
+ and the cover artwork image. The defaults are:
46
+
47
+ track: '"#{mdir}/#{fba}-#{my}-#{fb}-#{fv}/#{d}#{fn}-#{fa}-#{ft}.#{x}"'
48
+ image: '"#{mdir}/#{fba}-#{my}-#{fb}-#{fv}/#{zz}-#{fba}-#{fb}_cover.jpg"'
49
+
50
+ To run a shell command upon completion use the `after` attribute.
51
+ For example, to update the MPD database:
52
+
53
+ after: '"mpc update #{fba}-#{my}-#{fb}-#{fv}"'
54
+
55
+ The variables used in the templates are explained below.
56
+ The 'f' prefix means "normalized for filename".
57
+
58
+ Use everywhere:
59
+
60
+ [f]b: album name
61
+ [f]ba: album artist
62
+ y: release year
63
+ my: master release year
64
+ g: genre style
65
+ [f]v: release format
66
+
67
+ Only for track naming:
68
+
69
+ [f]a: track artist
70
+ [f]t: track title
71
+ [f]n: track number, may have letters (vinyls)
72
+ d: disc number
73
+ tn: track number counter
74
+ x: file extension in lowercase
75
+
76
+ Only for image naming:
77
+
78
+ zz: zeros that match d + n width
79
+
80
+
81
+ # Some test releases
82
+
83
+ * Sound, The (2) – From The Lions Mouth: 377432
84
+ * Various – Return Of The Banshee: 127565
85
+ * Harold Budd & Brian Eno – The Pearl: 699395
86
+ * Dead Souls Rising – Clepsydre 1993-1999: 1212954
87
+ * Metro Decay – Υπέρβαση: 1766242
88
+ * Dispossessed, The (2) – Sister Mary: 1965566
89
+ * Einstürzende Neubauten – Strategies Against Architecture IV: 2506503
90
+ * David Bowie – The Man Who Sold The World: 484622
91
+ * Various – Some Bizzare Album: 1381202
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ # $Id: musicfix,v 1.30 2013/06/13 23:09:01 lostd Exp $
5
+
6
+ require 'rubygems'
7
+ require 'fileutils'
8
+ require 'open-uri'
9
+ require 'discogs'
10
+ require 'stringex'
11
+ require 'taglib'
12
+ require 'yaml'
13
+
14
+ # Concatenate artist list using '&'
15
+ # Convert "Sound, The (2)" to "The Sound"
16
+ def mkartist al
17
+ nl = al.collect {|a| a.name}
18
+ nl.each {|n| n.gsub! /\s\(\d+\)$/, ''}
19
+ nl.each {|n| n.gsub! /(.*), The$/, 'The \1'}
20
+ nl.join ' & '
21
+ end
22
+
23
+ # Convert "3" to (nil, "03")
24
+ # Convert "A" to (nil, "A0")
25
+ # Convert "A3" to (nil, "A3")
26
+ # Convert "2.3" to ("2", "03")
27
+ # Convert "2.03" to ("2", "03")
28
+ # Convert "CD2-3" to ("2", "03")
29
+ def mkdiscnum pos
30
+ np = pos.gsub /[-.]/, '#'
31
+ d, n = np.split '#'
32
+ if not n
33
+ n = d
34
+ d = nil
35
+ else
36
+ d = d.gsub /\D/, ''
37
+ end
38
+ if n.match /[A-Z]/
39
+ n = n.ljust(2, '0')
40
+ else
41
+ n = n.rjust(2, '0')
42
+ end
43
+ return d, n
44
+ end
45
+
46
+ # Add wordings for symbols
47
+ Stringex.localize_from :en => {"≠" => "not equals"}
48
+
49
+ # Convert to lowercase ASCII without punctuation
50
+ def mkname n
51
+ n = n.gsub '12"', '12 inch'
52
+ n = n.gsub "12''", "12 inch"
53
+ n = n.gsub "12'", "12 inch"
54
+ n = n.gsub '7"', '7 inch'
55
+ n = n.gsub "7''", "7 inch"
56
+ n = n.gsub "7'", "7 inch"
57
+ n = n.gsub " & ", " and "
58
+ n.to_url.gsub '-', '_'
59
+ end
60
+
61
+ # Get cover artwork
62
+ def getimgurl rel
63
+ return nil unless rel.images
64
+ img = rel.images.select {|i| i.type == 'primary'}.first ||
65
+ rel.images.first
66
+ img.uri.gsub 'api.discogs.com', 's.pixogs.com'
67
+ end
68
+
69
+ # Construct position list from tracks filter
70
+ # The string "1--3,5,8" becomes ["1", "2", "3", "5", "8"]
71
+ # The string "A2--A4,B3" becomes ["A2", "A3", "A4", "B3"]
72
+ # The string "1-8--1-9,1-10--1-11" becomes ["1-8", "1-9", "1-10", "1-11"]
73
+ # Returns nil on error
74
+ def mkposlist tracks
75
+ pl = tracks.split(",").map do |r|
76
+ r = r.split("--")
77
+ return nil if r.include? ""
78
+ r = case r.length
79
+ when 1 then r
80
+ when 2 then (r[0] .. r[1]).map {|i| i}
81
+ else return nil
82
+ end
83
+ return nil if r.empty?
84
+ r
85
+ end
86
+ pl.flatten
87
+ end
88
+
89
+ # Parse command line
90
+ usage = ''
91
+ usage << "Usage: musicfix relid [tracks]\n"
92
+ usage << " musicfix dump relid [relfile]\n"
93
+ usage << " musicfix load [relfile]\n"
94
+ cmd = ARGV[0] || (puts usage; exit)
95
+ case cmd
96
+ when 'load' then
97
+ relfile = ARGV[1] || 'release.yaml'
98
+ when 'dump' then
99
+ relid = ARGV[1] || (puts usage; exit)
100
+ relfile = ARGV[2] || 'release.yaml'
101
+ else
102
+ relid = ARGV[0]
103
+ tracks = ARGV[1] || nil
104
+ end
105
+
106
+ # Default configuration
107
+ cfg = {}
108
+ cfg['mdir'] = '/var/music'
109
+ cfg['track'] = '"#{mdir}/#{fba}-#{my}-#{fb}-#{fv}/#{d}#{fn}-#{fa}-#{ft}.#{x}"'
110
+ cfg['image'] = '"#{mdir}/#{fba}-#{my}-#{fb}-#{fv}/#{zz}-#{fba}-#{fb}_cover.jpg"'
111
+ #cfg['after'] = '"mpc update #{fba}-#{my}-#{fb}-#{fv}"'
112
+
113
+ # User configuration overrides
114
+ cfgpath = File.expand_path('~/.musicfixrc')
115
+ if File.exists? cfgpath
116
+ new = YAML.load File.open(cfgpath, 'r')
117
+ cfg.merge! new
118
+ end
119
+
120
+ # Expand music directory
121
+ cfg['mdir'] = File.expand_path cfg['mdir']
122
+
123
+ # Print configuration
124
+ puts 'Configuration'
125
+ puts cfg.to_yaml
126
+
127
+ unless cmd == 'dump' then
128
+ # Supported formats
129
+ fmtre = /mp3|ogg|m4a|mpc|flac|wv/i
130
+ # Construct file list
131
+ fl = Dir['*'].select {|f| File.extname(f).match fmtre}.sort
132
+ if fl.empty? then
133
+ puts 'No music files found!'
134
+ exit
135
+ end
136
+ # Output file list
137
+ puts 'Files to process'
138
+ puts fl.to_yaml
139
+ end
140
+
141
+ # Initialize release info
142
+ if cmd == 'load' then
143
+ # Load release data from file
144
+ if File.exists? relfile then
145
+ puts "Loading release data..."
146
+ rel = YAML.load File.open(relfile, 'r')
147
+ else
148
+ puts "Release file #{relfile} not found."
149
+ exit
150
+ end
151
+ else
152
+ # Get release data from Discogs
153
+ puts "Geting release data..."
154
+ discogs = Discogs::Wrapper.new("musicfix")
155
+ r = discogs.get_release(relid)
156
+ mr = if r.master_id then discogs.get_master_release(r.master_id) end
157
+ # Tracklist can contain dummy header tracks, strip them
158
+ tl = r.tracklist.select {|t| t.position}
159
+ # Apply tracks filter if given
160
+ if tracks then
161
+ pl = mkposlist tracks
162
+ puts "Invalid tracks filter: #{tracks}" unless pl
163
+ tl = tl.select {|t| pl.include? t.position}
164
+ end
165
+ # Gather release-wide data
166
+ rel = {}
167
+ rel['artist'] = mkartist r.artists
168
+ rel['album'] = r.title
169
+ rel['year'] = r.released
170
+ rel['masteryear'] = if mr then mr.year end || r.released
171
+ # Year can be full-date so keep only the year part
172
+ rel['year'] = rel['year'].slice(0..3)
173
+ rel['masteryear'] = rel['masteryear'].slice(0..3)
174
+ rel['genre'] = if r.styles then r.styles.first end ||
175
+ if r.genres then r.genres.first end
176
+ rel['format'] = r.formats.first.name
177
+ rel['comment'] = "Discogs: #{r.id}"
178
+ rel['imgurl'] = getimgurl r
179
+ rel['tracklist'] = []
180
+ # Populate tracklist
181
+ tl.each do |s|
182
+ trk = {}
183
+ trk['pos'] = s.position
184
+ trk['artist'] = mkartist s.artists if s.artists
185
+ trk['title'] = s.title
186
+ rel['tracklist'] << trk
187
+ end
188
+ end
189
+
190
+ # Output release info
191
+ puts rel.to_yaml
192
+ if cmd == 'dump' then
193
+ File.open(relfile, 'w') do |f|
194
+ f.puts rel.to_yaml
195
+ end
196
+ exit
197
+ end
198
+
199
+ # Variables for use in templates
200
+ mdir = cfg['mdir']
201
+ ba = rel['artist']
202
+ b = rel['album']
203
+ y = rel['year']
204
+ my = rel['masteryear']
205
+ g = rel['genre']
206
+ v = rel['format']
207
+ c = rel['comment']
208
+ fba = mkname ba
209
+ fb = mkname b
210
+ fv = mkname v
211
+ # Internal use only
212
+ tl = rel['tracklist']
213
+
214
+ # Sanity checks
215
+ if tl.length != fl.length then
216
+ puts "Found #{tl.length} tracks for #{fl.length} music files."
217
+ if fl.length < tl.length then
218
+ # Limit entries to number of files
219
+ tl = tl.first fl.length
220
+ print "Use only the first #{fl.length} entries? [y/N] "
221
+ else
222
+ # Limit files to available tracks
223
+ fl = fl.first tl.length
224
+ print "Use only the first #{fl.length} files? [y/N] "
225
+ end
226
+ res = STDIN.readline.strip
227
+ exit unless res == 'y'
228
+ end
229
+
230
+ # Loop over the music files and
231
+ # 1. Copy them over with proper names
232
+ # 2. Fix the tags on the new files
233
+ tn = 0
234
+ fl.each do |ofname|
235
+ tn = tn.next
236
+ trk = tl[tn - 1]
237
+ # Use track artist for compilations, fallback to release
238
+ a = trk['artist'] || rel['artist']
239
+ d, n = mkdiscnum trk['pos'].to_s
240
+ t = trk['title']
241
+ fa = mkname a
242
+ ft = mkname t
243
+ fn = mkname n
244
+ x = File.extname(ofname).delete('.').downcase
245
+ nfname = eval cfg['track']
246
+ puts "Copy track to #{nfname}"
247
+ FileUtils.makedirs(File.dirname nfname)
248
+ FileUtils.copy(ofname, nfname)
249
+ TagLib::FileRef.open(nfname) do |f|
250
+ f.tag.artist = a
251
+ f.tag.album = b
252
+ f.tag.title = t
253
+ f.tag.track = tn
254
+ f.tag.year = y.to_i
255
+ f.tag.genre = g
256
+ f.tag.comment = c
257
+ f.save
258
+ end
259
+ end
260
+
261
+ # Also save the first image of the artwork
262
+ zz = '0' * (mkdiscnum tl.last['pos'].to_s).join.length
263
+ imgname = eval cfg['image']
264
+ if rel['imgurl'] then
265
+ puts "Save cover to #{imgname}"
266
+ img = open(rel['imgurl'], {"User-Agent" => "Mozilla"}).read
267
+ File.open(imgname, 'wb').write img
268
+ end
269
+
270
+ # Execute command if provided
271
+ if cfg['after'] then
272
+ puts "Executing finishing command"
273
+ run = eval cfg['after']
274
+ `#{run}`
275
+ end
276
+
277
+ # vim:set ts=4 sw=4 et:
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: musicfix
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lazaros Koromilas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: discogs-wrapper
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: stringex
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: taglib-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: 'Musicfix is a music file renamer and tagger with consistency concerns. '
56
+ email: lostd@2f30.org
57
+ executables:
58
+ - musicfix
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - bin/musicfix
63
+ - README
64
+ - LICENSE
65
+ homepage: http://www.2f30.org/~lostd/musicfix
66
+ licenses:
67
+ - BSD
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.0.0
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Music file renamer and tagger using Discogs.com
89
+ test_files: []