Skip to content

Instantly share code, notes, and snippets.

@arberg
Created December 7, 2018 12:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arberg/dd1aecbf8daccdb99c2a5a64f17d376d to your computer and use it in GitHub Desktop.
Save arberg/dd1aecbf8daccdb99c2a5a64f17d376d to your computer and use it in GitHub Desktop.
Download from youtubeDl with my custom video quality preference and downnload subtitles and convert vtt to srt with ffmpeg.
import ammonite.ops._
import ujson.Js.Value
import upickle.default.{macroRW, ReadWriter => RW}
//import scala.math.Ordering.Implicits._
//https://transform.now.sh/json-to-scala-case-class
case class Da(ext: String, url: String)
object Da { // uPickle 0.7.1 needs these to parse the objects. Really ugly, maybe gson is better?
implicit val rw: RW[Da] = macroRW
}
case class Formats(
format: String,
format_id: String,
height: Int,
width: Int,
preference: String,
manifest_url: String,
url: String,
protocol: String,
// http_headers: HttpHeaders,
ext: String,
vcodec: String,
tbr: Int, // total bitrate
// acodec: String,
// fps: String
) extends Ordered[Formats]
{
def compareValueFormatId = {
val pattern = """.*-(\d+)""".r
format_id match {
case pattern(number) => -number.toInt
case _ => 0
}
}
def compare(that: Formats): Int = {
val aTuple: (Int, Int, Int, Int) = (this.height, this.tbr, formatPreference(this.ext), this.compareValueFormatId)
val bTuple: (Int, Int, Int, Int) = (that.height, that.tbr, formatPreference(that.ext), that.compareValueFormatId)
implicitly[Ordering[Tuple4[Int, Int, Int, Int]]].compare(aTuple, bTuple)
}
}
object Formats {
implicit val rw: RW[Formats] = macroRW
}
case class Subtitles(da: Seq[Da])
object Subtitles {
implicit val rw: RW[Subtitles] = macroRW
}
case class Thumbnails(id: String, url: String)
object Thumbnails {
implicit val rw: RW[Thumbnails] = macroRW
}
//case class HttpHeaders(
//"Accept-Encoding": String,
//"User-Agent": String,
//"Accept-Charset": String,
//"Accept-Language": String,
//Accept: String,
//Cookie: String
//)
case class YoutubeDl
(
height: Int,
// playlist: String,
title: String,
formats: Seq[Formats],
// description: String,
// url: String,
// thumbnails: Seq[Thumbnails],
// http_headers: HttpHeaders,
fps: String,
display_id: String,
format_id: String,
// extractor: String,
// duration: Double,
// preference: String,
// manifest_url: String,
// acodec: String,
// protocol: String,
// id: String,
// vcodec: String,
// requested_subtitles: String,
// ext: String,
// subtitles: Subtitles,
// webpage_url_basename: String,
// playlist_index: String,
// upload_date: String,
// format: String,
// webpage_url: String,
// width: Int,
// timestamp: Int,
// thumbnail: String,
// extractor_key: String,
tbr: Int
)
object YoutubeDl{
implicit val rw: RW[YoutubeDl] = macroRW
}
// Also in AlexFunctions, todo import it instead
def basename(d: Path) = {
// OR d.name.split("\\.(?=[^\\.]+$)")(0)
val n = d.name
val extSize = d.ext.size
val extSizeWithPeriod = if (extSize > 0) extSize + 1 else 0
d.name.substring(0, d.name.size - extSizeWithPeriod)
}
def convertSubtitlesAllVttToSrt(path: Path): Unit = {
ls(path) |? {
_.ext == "vtt"
} | { f =>
val srtName = basename(f) + ".srt"
if (!exists(path / srtName)) {
%("ffmpeg.exe", "-i", f.name, basename(f) + ".srt")(path)
}
rm(f)
}
}
// Unused
def getBestFormatIdWithuJsonExample(jsonString: String): String = {
val tbr="tbr"
val ext="ext"
val json: Value = ujson.read(jsonString)
val formats: ujson.Js.Arr = json("formats").arr
var best : Option[ujson.Js] = None
for (current <- formats.value) {
if (best.isEmpty) {
best = Some(current)
} else {
val theBest=best.get
if (current(tbr).num > theBest(tbr).num ||
(current(tbr).num == theBest(tbr).num &&
current(ext).str == "mp4" && theBest(ext).str != "mp4") ) {
best = Some(current)
}
}
}
best.get("format_id").str
}
def formatPreference(ext: String): Int = {
// 3gp, aac, flv, m4a, mp3, mp4, ogg, wav, webm
ext match {
// video containers
case "mkv" => 10 // not possible in youtube-dl
case "mp4" => 9
case "webm" => 8
case "flv" => 7
case "3gp" => 6
// audio only
case "m4a" => 5
case "aac" => 4
case "ogg" => 3
case "mp3" => 2
case "wav" => 1
case _ => 0
}
}
def selectBestFormat(a: Formats, b: Formats) = {
if (a.compare(b) >= 0) a else b
}
def getBestFormatId(path: Path, videoUrl: String): String = {
val jsonString = %%("youtube-dl", "-J", videoUrl)(path).out.lines(0)
val json = upickle.default.read[YoutubeDl](jsonString) // upickle variant
val formats: Seq[Formats] = json.formats
if (formats.isEmpty) ""
else formats.foldLeft(formats.head)(selectBestFormat).format_id
}
def downloadVideo(path: Path, videoUrl: String): Unit = {
// --download-archive : I think it zips and keeps all intermediaries. Might be needed if ffmpeg corrupts stream
val formatId = getBestFormatId(path, videoUrl)
val p = %%("youtube-dl", "--all-subs", "--sub-format", "srt", "--fixup", "warn", "-f", formatId, videoUrl)(path)
// rm(path/"*temp.mp4") // HowTo, maybe: ls(path) |? {_.name.endsWith("temp.mp4"} | rm
println(p.out.lines.mkString("\n"))
if (p.exitCode == 0) {
println("Success")
convertSubtitlesAllVttToSrt(path)
} else {
println("Failed: " + p.err.lines.mkString("\n"))
}
}
val filmPath = Path("""m:\Film""")
val serierJul = filmPath / "Serier Jul"
downloadVideo(serierJul / "MoviePath", "https://www.youtube.com/watch?v=_JUwcv7dUQI")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment