Updated 2012-09-18 11:41:15 by RLE

FPX: Watching videos on YouTube [1] or Google Video [2] is fun. Sometimes, I want to keep them. You never know when they will be (re-)moved for whatever reason, and it's also nice to have the file available for offline viewing.

For YouTube, there is youtube-dl [3]. I could not find an equivalent utility for Google Video, so here's one. Please upgrade as necessary.

This code is based on the hints at [4], and some trial-and-error.

Compared with Google's own "download" feature, this one does not require a Google-specific player. The files that you receive are in Flash Video format and can be played using, e.g., VideoLAN [5]. More information about the Flash Video format is at http://en.wikipedia.org/wiki/FLV.

Update 2006-11-15: Sometimes, the initial web page directly links directly to the video data, sometimes the page resolves to a redirection. Account for both cases.
 #! /bin/sh
 # the next line restarts using tclsh \
 exec tclsh8.4 $0 "$@"

 package require http

 if {[llength $argv] != 1} {
    puts "usage: $argv0 <google video URL>"
    exit 0
 }

 set url [lindex $argv 0]

 puts -nonewline "Downloading $url ..."
 flush stdout

 if {[catch {set urlToken [http::geturl $url]} oops]} {
    puts "$oops"
    exit 1
 }

 if {[http::status $urlToken] ne "ok"} {
    puts [http::error $urlToken]
    http::cleanup $urlToken
    exit 1
 }

 set urlData [http::data $urlToken]
 http::cleanup $urlToken

 puts "done."

 #
 # Looking for ...googleplayer.swf?&videoUrl\u003d[<video URL>]&
 #

 puts -nonewline "Extracting encoded video URL ... "
 flush stdout

 if {[set googleplayerIndex [string first "googleplayer.swf" $urlData]] == -1} {
    puts "failed."
    puts "Error: magic string \"googleplayer.swf\" not found."
    exit 1
 }

 if {[string range $urlData \
         [expr {$googleplayerIndex + 18}] \
         [expr {$googleplayerIndex + 25}]] ne "videoUrl"} {
    puts "failed."
    puts "Error: magic string \"videoUrl\" not found."
    exit 1
 }

 if {[string range $urlData \
         [expr {$googleplayerIndex + 26}] \
         [expr {$googleplayerIndex + 31}]] ne "\\u003d"} {
    puts "failed."
    puts "Error: magic URL marker not found."
    exit 1
 }

 set urlBeginIndex [expr {$googleplayerIndex + 32}]

 if {[set urlEndIndex [string first "&" $urlData $urlBeginIndex]] == -1} {
    puts "failed."
    puts "Error: magic end of URL marker not found."
    exit 1
 }

 incr urlEndIndex -1

 set encodedVideoUrl [string range $urlData $urlBeginIndex $urlEndIndex]

 if {[string first "\n" $encodedVideoUrl] != -1 || \
        [string first "\"" $encodedVideoUrl] != -1} {
    puts "failed."
    puts "Error: video URL looks fishy."
    exit 1
 }

 puts "done."

 #
 # Replace all URL-encoded characters, e.g., replace "%3D" with "="
 #

 puts -nonewline "Unencoding video URL ... "
 flush stdout

 lappend ud_map + { }

 for {set i 0} {$i < 256} {incr i} {
    set c [format %c $i]
    set x %[format %02x $i]
    if {![string match {[a-zA-Z0-9]} $c]} {
        lappend ud_map $x $c
    }
 }

 set videoUrl [string map -nocase $ud_map $encodedVideoUrl]
 puts "done."

 #
 # At this URL, we may get the video data. Or, we might be redirected.
 #

 puts -nonewline "Determining file name ... "
 flush stdout

 if {[catch {set videoToken [http::geturl $videoUrl -validate 1]} oops]} {
    puts "$oops"
    exit 1
 }

 upvar \#0 $videoToken videoState
 array set videoMeta $videoState(meta)
 http::cleanup $videoToken

 if {[info exists videoMeta(Location)]} {
    puts -nonewline "Following redirection ... "
    flush stdout
    set videoUrl $videoMeta(Location)

    if {[catch {set videoToken [http::geturl $videoUrl -validate 1]} oops]} {
        puts "$oops"
        exit 1
    }

    upvar \#0 $videoToken videoState
    array set videoMeta $videoState(meta)
    http::cleanup $videoToken
 }

 #
 # There should be a Content-Disposition: attachment; filename=<file name> header.
 #

 if {![info exists videoMeta(Content-Disposition)]} {
    puts "failed."
    puts "Error: no Content-Disposition header found."
    exit 1
 }

 set disposition $videoMeta(Content-Disposition)

 if {[set filenameIndex [string first "filename=" $disposition]] == -1} {
    puts "failed."
    puts "Error: filename not found in \"$disposition\""
    exit 1
 }

 set filenameFirst [expr {$filenameIndex + 9}]
 set filename [string range $disposition $filenameFirst end]

 puts "done."

 #
 # Download the video for real.
 #

 proc progressIndicator {token total current} {
    set currentKb [expr {$current / 1024}]
    set totalKb [expr {$total / 1024}]

    puts -nonewline "\rDownloading $::filename ... $currentKb "

    if {$total != 0} {
        puts -nonewline "/ $totalKb "
    }
 }

 puts -nonewline "Downloading $filename ... "

 if {[catch {set output [open $filename "w"]} oops]} {
    puts "failed."
    puts "Error: can not open \"$filename\" for writing: $oops"
    exit 1
 }

 if {[catch {set videoToken [http::geturl $videoUrl -channel $output -progress progressIndicator]} oops]} {
    puts "$oops"
    catch {close $output}
    exit 1
 }

 if {[http::status $videoToken] ne "ok"} {
    puts [http::error $videoToken]
    http::cleanup $videoToken
    exit 1
 }

 http::cleanup $videoToken
 puts "done."