musicfix 0.1.0

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