Last active
August 29, 2015 14:23
-
-
Save arturaz/0217b2746ecd823aed44 to your computer and use it in GitHub Desktop.
Same software (tracklist -> cue translator) in both Ruby and functional Scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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}"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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