2014-12-06 08:45:42 +00:00
|
|
|
#!/usr/local/bin/hy
|
|
|
|
|
|
|
|
(def *version* "0.0.1")
|
|
|
|
|
|
|
|
(require hy.contrib.anaphoric)
|
|
|
|
(import eyeD3 os re sys getopt string
|
|
|
|
[collections [defaultdict]]
|
|
|
|
[django.utils.encoding [smart_str]])
|
|
|
|
|
|
|
|
(def optlist [["g" "genre" true "Set the genre"]
|
|
|
|
["a" "album" true "Set the album"]
|
|
|
|
["r" "artist" true "Set the artist"]
|
|
|
|
["n" "usedir" false "Use the directory as the album name, even if it's set in ID3"]
|
|
|
|
["t" "usefilename" false "Use the filename as the title, even if it's set in ID3"]
|
|
|
|
["h" "help" false "This help message"]
|
|
|
|
["v" "version" false "Version information"]])
|
|
|
|
|
|
|
|
(defn print-version []
|
|
|
|
(print (.format "mp_suggest (hy version {})" *version*))
|
2014-12-07 00:05:37 +00:00
|
|
|
(print "Copyright (c) 2008, 2014 Elf M. Sternberg <elf.sternberg@gmail.com>")
|
|
|
|
(sys.exit))
|
2014-12-06 08:45:42 +00:00
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
(defn print-help []
|
|
|
|
(print "Usage:")
|
|
|
|
(ap-each optlist (print (.format " -{} --{} {}" (get it 0) (get it 1) (get it 3))))
|
2014-12-07 00:05:37 +00:00
|
|
|
(sys.exit))
|
2014-12-06 18:17:16 +00:00
|
|
|
|
|
|
|
; Given a set of command-line arguments, compare that to a mapped
|
|
|
|
; version of the optlist and return a canonicalized dictionary of all
|
|
|
|
; the arguments that have been set.
|
|
|
|
|
2014-12-07 00:05:37 +00:00
|
|
|
(defn make-opt-assoc [prefix pos]
|
|
|
|
(fn [acc it] (assoc acc (+ prefix (get it pos)) (get it 1)) acc))
|
|
|
|
|
|
|
|
(defn make-options-rationalizer [optlist]
|
|
|
|
(let [
|
|
|
|
[short-opt-assoc (make-opt-assoc "-" 0)]
|
|
|
|
[long-opt-assoc (make-opt-assoc "--" 1)]
|
|
|
|
[fullset
|
|
|
|
(ap-reduce (-> (short-opt-assoc acc it)
|
|
|
|
(long-opt-assoc it)) optlist {})]]
|
|
|
|
(fn [acc it] (do (assoc acc (get fullset (get it 0)) (get it 1)) acc))))
|
2014-12-06 08:45:42 +00:00
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
; Assuming the directory name looked like "Artist - Album", return the
|
|
|
|
; two names separately. If only one name is here, assume a compilation
|
|
|
|
; or mixtape album and default to "VA" (Various Artists).
|
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
(defn artist-album []
|
2014-12-07 00:05:37 +00:00
|
|
|
(let [[aa (-> (.getcwd os) (.split "/") (get -1) (.split " - "))]]
|
2014-12-06 08:45:42 +00:00
|
|
|
(if (= (len aa) 1)
|
|
|
|
(, "VA" (get aa 0))
|
2014-12-07 00:05:37 +00:00
|
|
|
(, (.strip (get aa 0)) (.strip (get aa 1))))))
|
2014-12-06 08:45:42 +00:00
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
; A long list of substitutions intended to turn a filename into a
|
|
|
|
; human-readable strategy. This operation is the result of weeks
|
|
|
|
; of experimentation. Doubt it at your peril! :-)
|
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
(defn title-strategy [orig]
|
|
|
|
(->> (.strip orig)
|
|
|
|
(.sub re "\.[Mm][Pp]3$" "")
|
|
|
|
(.sub re "_" " ")
|
|
|
|
(.strip)
|
|
|
|
(.sub re "^.* - " "")
|
|
|
|
(.sub re "^[\d\. ]+" "")
|
|
|
|
(.sub re ".* \d{2} " "")))
|
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
; Given a list of mp3s, derive the list of ID3 tags. Obviously,
|
|
|
|
; filesystem access is a point of failure, but this is mostly
|
|
|
|
; reliable.
|
2014-12-06 08:45:42 +00:00
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
(defn tag-deriver [usefilenames]
|
|
|
|
(fn [mp3s]
|
|
|
|
(defn derive-tag [pos mp3]
|
|
|
|
(try
|
|
|
|
(let [[tag (.Tag eyeD3)]]
|
|
|
|
(tag.link mp3)
|
|
|
|
(, mp3 (str (.getArtist tag)) (str (.getAlbum tag))
|
|
|
|
(str (.getGenre tag)) (str (.getTitle tag)) pos))
|
|
|
|
(catch [err]
|
|
|
|
(, mp3 "" "" "" ""))))
|
|
|
|
(ap-map (apply derive-tag it) mp3s)))
|
|
|
|
|
|
|
|
; For removing subgenre parentheses. This is why there's the -g option.
|
2014-12-06 08:45:42 +00:00
|
|
|
|
|
|
|
(defn clean-paren [s]
|
|
|
|
(if (not (= (.find s "(") -1))
|
|
|
|
(.sub re "\(.*?\)" "" s)
|
|
|
|
s))
|
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
; My FAT-32 based file store via Samba isn't happy with unicode, so
|
|
|
|
; this is here...
|
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
(defn is-ascii [s]
|
|
|
|
(= (.decode (.encode s "ascii" "ignore") "ascii") s))
|
|
|
|
|
|
|
|
(defn ascii-or-nothing [s]
|
|
|
|
(if (is-ascii s) s ""))
|
2014-12-06 18:17:16 +00:00
|
|
|
|
|
|
|
; For all the songs, analyze a consist entry (usually genre and album
|
|
|
|
; names), and return the one with the most votes.
|
2014-12-06 08:45:42 +00:00
|
|
|
|
|
|
|
(defn find-likely [l]
|
|
|
|
(let [[cts
|
|
|
|
(->>
|
|
|
|
(map (fn [i] (, (get i 1) (get i 0)))
|
|
|
|
(.items
|
|
|
|
(ap-reduce
|
|
|
|
(do (assoc acc it (+ 1 (get acc it))) acc) l (defaultdict int))))
|
|
|
|
(sorted)
|
|
|
|
(reversed)
|
|
|
|
(list))]]
|
|
|
|
(if (= (len cts) 0)
|
|
|
|
""
|
|
|
|
(get (get cts 0) 1))))
|
|
|
|
|
2014-12-06 18:17:16 +00:00
|
|
|
; Auto-capitalize "found" entries like album name and title. Will not
|
|
|
|
; affect manually set entries.
|
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
(defn sfix [s]
|
|
|
|
(let [[seq (.split (.strip s))]]
|
|
|
|
(smart_str (string.join (ap-map (.capitalize it) seq) " "))))
|
|
|
|
|
|
|
|
(defn suggest [opts]
|
|
|
|
(let [[mp3s
|
|
|
|
(->> (os.listdir ".")
|
|
|
|
(ap-filter (and (> (len it) 4) (= (slice (.lower it) -4) ".mp3")))
|
|
|
|
(sorted)
|
|
|
|
(enumerate)
|
2014-12-06 18:17:16 +00:00
|
|
|
((tag-deriver true))
|
2014-12-06 08:45:42 +00:00
|
|
|
(list))]
|
|
|
|
[(, loc_artist loc_album) (artist-album)]
|
|
|
|
|
|
|
|
[genre
|
|
|
|
(if (.has_key opts "genre")
|
|
|
|
(get opts "genre")
|
|
|
|
(clean-paren (find-likely (map (fn [m] (get m 3)) mp3s))))]
|
|
|
|
|
|
|
|
[pos_album
|
|
|
|
(ascii-or-nothing
|
|
|
|
(find-likely
|
|
|
|
(map (fn [m] (get m 2)) mp3s)))]
|
|
|
|
|
|
|
|
[album
|
|
|
|
(cond
|
|
|
|
[(.has_key opts "album") (get opts "album")]
|
|
|
|
[(not (= "" pos_album)) pos_album]
|
|
|
|
[true (sfix loc_album)])]
|
|
|
|
|
|
|
|
[artist
|
|
|
|
(if (.has_key opts "artist")
|
|
|
|
(get opts "artist")
|
2014-12-07 00:05:37 +00:00
|
|
|
(sfix loc_artist))]
|
|
|
|
|
|
|
|
[format-string
|
|
|
|
(string.join ["id3v2 -T \"{}\""
|
|
|
|
"--album \"{}\""
|
|
|
|
"--artist \"{}\""
|
|
|
|
"--genre \"{}\""
|
|
|
|
"--song \"{}\""
|
|
|
|
"\"{}\""] " ")]]
|
2014-12-06 08:45:42 +00:00
|
|
|
|
2014-12-07 00:05:37 +00:00
|
|
|
(ap-each mp3s
|
|
|
|
(print (.format format-string (get it 5) album artist genre (get it 4) (get it 0))))))
|
2014-12-06 08:45:42 +00:00
|
|
|
|
|
|
|
(defmain [&rest args]
|
|
|
|
(try
|
|
|
|
(let [[optstringsshort
|
2014-12-07 00:05:37 +00:00
|
|
|
(string.join (ap-map (+ (. it [0]) (cond [(. it [2]) ":"] [true ""])) optlist) "")]
|
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
[optstringslong
|
|
|
|
(list (ap-map (+ (. it [1]) (cond [(. it [2]) "="] [true ""])) optlist))]
|
2014-12-07 00:05:37 +00:00
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
[(, opt arg)
|
|
|
|
(getopt.getopt (slice args 1) optstringsshort optstringslong)]
|
2014-12-07 00:05:37 +00:00
|
|
|
|
|
|
|
[rationalize-options
|
|
|
|
(make-options-rationalizer optlist)]
|
|
|
|
|
2014-12-06 08:45:42 +00:00
|
|
|
[options
|
2014-12-07 00:05:37 +00:00
|
|
|
(ap-reduce (rationalize-options acc it) opt {})]]
|
|
|
|
|
|
|
|
(cond [(.has_key options "help") (print-help)]
|
|
|
|
[(.has_key options "version") (print-version)]
|
|
|
|
[true (suggest options)]))))
|
2014-12-06 08:45:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|