Skip to content

Instantly share code, notes, and snippets.

@arturaz
Created July 7, 2015 07:50
Show Gist options
  • Save arturaz/1847462e8fac5d3fb736 to your computer and use it in GitHub Desktop.
Save arturaz/1847462e8fac5d3fb736 to your computer and use it in GitHub Desktop.
package app
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import scala.collection.JavaConverters._
import scala.io.Source
import scalaz._, Scalaz._, scalaz.effect._
object Main extends SafeApp {
import Extractor._
val Extractors = Vector(
new 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]
val Re = """^\s*(\d+)\.\)?\s*\[((\d+):)?(\d+):(\d+)\] (.+?)$""".r
def extract(tracksRead: Int, line: String) = line match {
case Re(IntE(no), _, OptE.int(hh), IntE(mm), IntE(ss), title) =>
Some(Track(no, Time(hh getOrElse 0, mm, ss), title))
case _ => None
}
},
new Extractor {
// 0:00 Module Module - An Interlude
// 00:00 - 04:10 - Feint Promises
val Re = """^\s*((\d+):)?(\d+):(\d+)( - \d*:?\d+:\d+ -)?\s+(.+?)$""".r
def extract(tracksRead: Int, line: String) = line match {
case Re(_, OptE.int(hh), IntE(mm), IntE(ss), _, title) =>
Some(Track(tracksRead + 1, Time(hh getOrElse 0, mm, ss), title))
case _ => None
}
}
)
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(tracksRead: Int, line: String) =
Extractors.findMap(_.extract(tracksRead, 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(read.size, line))
}
rec(Vector.empty)
}
def readFile(file: File): IO[Vector[Track]] = IO {
Source.fromFile(file).getLines().foldLeft(Vector.empty[Track]) {
case (tracks, line) => tracks ++ process(tracks.size, line)
}
}
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)
for {
tracks <- inputFileS.map(new File(_)).fold(readStdin)(readFile)
cueStrs = cueLines(fullFile, tracks)
_ <- writeFile(cue, cueStrs)
_ <- info(cue, fullFile).map(IO.putStrLn).sequence_
// Debug tracklist
//_ <- tracks.map(_.toString).map(IO.putStrLn).sequence_
} yield ()
}
}
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 (performer, songName) = title.split("-", 2) match {
case Array(performer, songName) =>
(performer, songName)
case Array(songName) =>
("Unknown", songName)
}
Track(no, time, performer.trim, songName.trim)
}
}
case class Track(
no: Int, time: Time, performer: String, songName: String
) {
def title = s"$performer - $songName"
}
trait Extractor {
def extract(tracksRead: Int, line: String): Option[Track]
}
object Extractor {
trait E[A] { self =>
def unapply(str: String): Option[A]
def map[B](f: A => B): E[B] = new E[B] {
def unapply(str: String) = self.unapply(str).map(f)
}
def mapO[OA, B](f: OA => Option[B])(implicit ev: A <:< Option[OA])
: E[Option[B]] = map(_ flatMap f)
}
object IntE extends E[Int] {
def unapply(str: String) = str.parseInt.toOption
}
object OptE extends E[Option[String]] {
def unapply(str: String) = Some(Option(str))
val int = this mapO IntE.unapply
}
}
import java.io.File
import scala.util.matching.Regex.Match
/**
* Created by arturas on 15.6.14.
*/
package object app {
implicit class MatchExts(val m: Match) extends AnyVal {
def oGroup(idx: Int) = Option(m.group(idx))
def mapOGroup[A](idx: Int, f: String => A, orElse: => A) =
oGroup(idx).map(f).getOrElse(orElse)
}
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