Skip to content

Instantly share code, notes, and snippets.

@arturaz
Last active August 29, 2015 14:23
Show Gist options
  • Save arturaz/0217b2746ecd823aed44 to your computer and use it in GitHub Desktop.
Save arturaz/0217b2746ecd823aed44 to your computer and use it in GitHub Desktop.
Same software (tracklist -> cue translator) in both Ruby and functional Scala
#!/usr/bin/env ruby
$tracks = []
class Track
attr_accessor :no, :hours, :minutes, :seconds, :performer, :song_name
def initialize(no, hours, minutes, seconds, title)
@no, @hours, @minutes, @seconds = no, hours, minutes, seconds
split_title = title.split("-", 2)
@performer = split_title[0].strip
@song_name = split_title[1].strip
end
def mp3splt_time
"#{@hours * 60 + @minutes}.#{@seconds}"
end
def cue_time
"%02d:%02d:00" % [@hours * 60 + @minutes, @seconds]
end
def title
"#{@performer} - #{@song_name}"
end
end
class Extractor
def initialize(re, extractor)
@re = re
@extractor = extractor
end
def match(line)
md = @re.match(line)
if md.nil?
nil
else
@extractor.call(md)
end
end
end
EXTRACTORS = [
# 1.) [1:00:00] Will Canas - Find Our Way (Vintage & Morelli Remix) [sunsetmelodies]
# 1.[0:00] Justin Oh - A Different Place (PROFF Remix)[Nueva digital]
Extractor.new(
%r{^\s*(\d+)\.\)?\s*\[((\d+):)?(\d+):(\d+)\] (.+?)$},
lambda do |match|
Track.new(
match[1].to_i, match[3].to_i, match[4].to_i, match[5].to_i, match[6]
)
end
)
]
def collect_first(enum)
collected = nil
enum.find do |item|
collected = yield item
! collected.nil?
end
collected
end
def process(line)
matched = collect_first(EXTRACTORS) { |ex| ex.match(line) }
if matched
$tracks << matched
end
end
def read_stdin
line = gets
while line != nil
line = line.chomp
if line == "!done"
break
else
process(line)
end
line = gets
end
end
def read_file(path)
File.read(path).each_line { |line| process(line) }
end
if ARGV.size < 2
puts "Usage: tracklist_parser.rb file.mp3 file.cue [tracklist_or_stdin]"
exit 1
end
full_file = ARGV[0]
file = File.basename(full_file)
cue = ARGV[1]
if ARGV[2].nil?
read_stdin
else
read_file(ARGV[2])
end
File.open(cue, "w") do |f|
# FILE "Faithless - Live in Berlin.mp3" MP3
f.puts %Q{PERFORMER "VA"}
f.puts %Q{TITLE "#{File.basename(file, ".mp3")}"}
f.puts %Q{FILE "#{file}" MP3}
$tracks.each_with_index do |track, index|
# TRACK 01 AUDIO
# TITLE "Reverence"
# PERFORMER "Faithless"
# INDEX 01 00:00:00
f.puts %Q{ TRACK %02d AUDIO} % (index + 1)
f.puts %Q{ TITLE "#{track.song_name}"}
f.puts %Q{ PERFORMER "#{track.performer}"}
f.puts %Q{ INDEX 01 #{track.cue_time}}
end
end
puts
puts ".cue file generation complete: #{cue}"
puts
puts "Split with:"
puts %Q{mp3splt -o "@n - @p - @t" -c "#{cue}" "#{full_file}"}
package app
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import app.Exts._
import scala.collection.JavaConverters._
import scala.io.Source
import scala.util.matching.Regex
import scala.util.matching.Regex.Match
import scalaz._, Scalaz._, scalaz.effect._
object Main extends SafeApp {
val Extractors = Vector(
Extractor(
// 1.) [1:00:00] Will Canas - Find Our Way (Vintage & Morelli Remix) [sunsetmelodies]
// 1.[0:00] Justin Oh - A Different Place (PROFF Remix)[Nueva digital]
"""^\s*(\d+)\.\)?\s*\[((\d+):)?(\d+):(\d+)\] (.+?)$""".r,
m => Track(
m.group(1).toInt,
Time(Option(m.group(3)).map(_.toInt).getOrElse(0), m.group(4).toInt, m.group(5).toInt),
m.group(6)
)
)
)
override def run(args: ImmutableArray[String]) = {
if (args.size < 2) for {
_ <- IO.putStrLn("Usage: tracklist_parser file.mp3 file.cue [tracklist_or_stdin]")
_ <- IO { System.exit(1) }
} yield ()
else process(args(0), args(1), args.lift(2))
}
def process(line: String) = Extractors.findMap(_.unapply(line))
def readStdin: IO[Vector[Track]] = {
def rec(read: Vector[Track]): IO[Vector[Track]] =
IO.readLn.map(_.trim).flatMap {
case "!done" => IO(read)
case line => rec(read ++ process(line))
}
rec(Vector.empty)
}
def readFile(file: File): IO[Vector[Track]] =
IO { Source.fromFile(file).getLines().flatMap(process(_)).toVector }
def writeFile(file: File, lines: Seq[String]): IO[Unit] =
IO { Files.write(file.toPath, lines.asJava, StandardCharsets.UTF_8) }
def cueLines(mp3: File, tracks: Vector[Track]): Vector[String] = Vector(
// # FILE "Faithless - Live in Berlin.mp3" MP3
"""PERFORMER "VA"""",
s"""TITLE "${mp3.basename}"""",
s"""FILE "${mp3.getName}" MP3"""
) ++ tracks.zipWithIndex.flatMap { case (track, index) => Vector(
// TRACK 01 AUDIO
// TITLE "Reverence"
// PERFORMER "Faithless"
// INDEX 01 00:00:00
f" TRACK ${index + 1}%02d AUDIO",
s""" TITLE "${track.songName}"""",
s""" PERFORMER "${track.performer}"""",
s""" INDEX 01 ${track.time.cue}"""
) }
def info(cue: File, mp3: File) = Vector(
s"",
s".cue file generation complete: $cue",
s"",
s"Split with:",
s"""mp3splt -o "@n - @p - @t" -c "$cue" "$mp3""""
)
def process(
fullFileS: String, cueS: String, inputFileS: Option[String]
): IO[Unit] = {
val fullFile = new File(fullFileS)
val cue = new File(cueS)
inputFileS
.map(new File(_)).fold(readStdin)(readFile)
.map(cueLines(fullFile, _))
.flatMap(writeFile(cue, _))
.flatMap(_ => info(cue, fullFile).map(IO.putStrLn).sequence_)
}
}
case class Time(hours: Int, minutes: Int, seconds: Int) {
def mp3splt = s"${hours * 60 + minutes}.$seconds"
def cue = f"${hours * 60 + minutes}%02d:$seconds%02d:00"
}
object Track {
def apply(no: Int, time: Time, title: String): Track = {
val Array(performer, songName) = title.split("-", 2)
Track(no, time, performer, songName)
}
}
case class Track(
no: Int, time: Time, performer: String, songName: String
) {
def title = s"$performer - $songName"
}
case class Extractor(re: Regex, extractor: Match => Track) {
def unapply(line: String) = re.findFirstMatchIn(line).map(extractor)
}
object Exts {
implicit class FileExts(val file: File) extends AnyVal {
def basename = file.getName.replaceAll("\\..*$", "")
}
implicit class IterableExts[A](val i: Iterable[A]) extends AnyVal {
def findMap[B](f: A => Option[B]): Option[B] = {
i.foreach { a =>
val opt = f(a)
if (opt.isDefined) return opt
}
None
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment