Last active
March 18, 2018 16:02
-
-
Save TheDIM47/dde67c23cfd3b5933b2ba604f29c16ed to your computer and use it in GitHub Desktop.
Simple scala-xml parser for XmlTv format
This file contains 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
import scala.xml._ | |
import java.util.Date | |
import java.lang.ThreadLocal | |
import java.text.SimpleDateFormat | |
import scala.collection.immutable._ | |
/** | |
* Simple XmlTv format parser | |
* http://wiki.xmltv.org/index.php/XMLTVFormat | |
*/ | |
sealed case class Channel( | |
id: String, | |
displayName: Seq[String] = Seq.empty, | |
icon: Option[String] = None | |
) | |
sealed case class Programm( | |
start: Date, | |
stop: Date, | |
channel: Channel, | |
title: Map[String, String] = Map.empty, | |
description: Map[String, String] = Map.empty, | |
credits: Map[String, List[String]] = Map.empty, | |
category: Map[String, List[String]] = Map.empty, | |
episode: Map[String, List[String]] = Map.empty, | |
rating: Map[String, List[String]] = Map.empty, | |
stars: String, // star-rating | |
audio: Map[String, String] = Map.empty, | |
video: Map[String, String] = Map.empty, | |
prevShown: Option[Date] = None, | |
date: Option[Date] = None, | |
subtitlesType: String, | |
country: String | |
) | |
sealed case class Tv( | |
sourceInfoUrl: String, | |
sourceInfoName: String, | |
generatorInfoUrl: String, | |
generatorInfoName: String, | |
channels: Map[String, Channel], | |
programs: Map[Channel, Seq[Programm]] | |
) | |
object XmlTvParser { | |
val defaultChannel = new Channel("xmltv-default") | |
val DateFormatters = ListMap[Int, ThreadLocal[SimpleDateFormat]]() ++ List( | |
"yyyyMMddHHmmss Z", "yyyyMMddHHmmss", "yyyyMMddHHmm", "yyyyMMdd" | |
).map(s => s.length() -> new ThreadLocal[SimpleDateFormat]() { | |
override def initialValue(): SimpleDateFormat = new SimpleDateFormat(s) | |
}).toMap | |
implicit final class MyStringOps(val s: String) extends AnyVal { | |
def toOption: Option[String] = for (v <- Option(s) if s.trim.nonEmpty) yield v | |
} | |
implicit class MySeqOps(seq: Seq[(String, String)]) { | |
// def toFlattenMap: Map[String, List[String]] = seq.groupBy(_._1).map { case (k, v) => (k, v.map(_._2).toList) } | |
def toFlattenMap: Map[String, List[String]] = seq.groupBy(_._1).mapValues(_.map(_._2).toList) | |
} | |
def parseToDate(s: String): Date = { | |
val len = s.length | |
val r = XmlTvParser.DateFormatters.find{case (k,v) => k <= len } | |
r.map(_._2.get().parse(s)).getOrElse(throw new IllegalArgumentException(s)) | |
} | |
def parseChannel(ch: Node): Channel = new Channel( | |
id = (ch \ "@id").text, | |
displayName = (ch \ "display-name").map(_.text), | |
icon = (ch \ "icon" \ "@src").text.toOption | |
) | |
def parseProgramm(p: Node, channels: Map[String, Channel]): Programm = | |
new Programm( | |
start = parseToDate((p \ "@start").text), // "20080715003000 -0600" | |
stop = parseToDate((p \ "@stop").text), // "20080715010000 -0600" | |
channel = channels.getOrElse((p \ "@channel").text, defaultChannel), // "I10436.labs.zap2it.com" | |
title = (p \ "title").map(v => (v \ "@lang").text -> v.text).toMap, | |
description = (p \ "desc").map(v => (v \ "@lang").text -> v.text).toMap, | |
credits = (p \ "credits").map(v => v.label -> v.text).toFlattenMap, | |
category = (p \ "category").map(v => (v \ "@lang").text -> v.text).toFlattenMap, | |
episode = (p \ "episode-num").map(v => (v \ "@system").text -> v.text).toFlattenMap, | |
rating = (p \ "rating").map(v => (v \ "@system").text -> (v \ "value").text).toFlattenMap, | |
stars = (p \ "star-rating" \ "value").text, | |
audio = (p \ "audio").map(v => v.label -> v.text).toMap, | |
video = (p \ "video").map(v => v.label -> v.text).toMap, | |
prevShown = (p \ "previously-shown" \ "@start").text.toOption.map(parseToDate), | |
date = (p \ "date").text.toOption.map(parseToDate), | |
subtitlesType = (p \ "subtitles" \ "@type").text, | |
country = (p \ "country").text | |
) | |
def parseTv(p: Node): Tv = { | |
val channelMap = (p \ "channel").map(parseChannel).map(v => v.id -> v).toMap | |
val programMap = (p \ "programme").map(v => parseProgramm(v, channelMap)).groupBy(_.channel) | |
Tv( | |
sourceInfoUrl = (p \ "@source-info-url").text, | |
sourceInfoName = (p \ "@source-info-name").text, | |
generatorInfoUrl = (p \ "@generator-info-url").text, | |
generatorInfoName = (p \ "@generator-info-name").text, | |
channelMap, | |
programMap | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment