Created
May 24, 2018 02:48
-
-
Save bradennapier/b87dc8089908bef8f260abe12f0a5d20 to your computer and use it in GitHub Desktop.
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
namespace eval DAAP { | |
variable groups "cmst mlog agal mlcl mshl mlit abro abar \ | |
apso caci avdb cmcp cmgt aply adbs cmpa \ | |
mdcl msrv merr mccr mupd agar aply msml \ | |
casp agal" | |
variable containers "cmcp cmst msrv mlog avdb aply msml casp | |
mccr agar agal" | |
variable asciiCodes "cann cang minm cana canl mcmn" | |
variable deccodes "mdbk" | |
variable hexCodes "canp capl ated asgr mcty" | |
variable keyCodes "miid mlid mdcl minm cann cang cast cant \ | |
cana canl caps cmmk carps cash ceNR | |
ceQA casc caks" | |
variable binaryCodes "ceSD" | |
variable databaseContainers "avdb adbs aply agar agal" | |
# ::DAAP::Store is used to store information about a specific container | |
# it should be set back to empty upon finishing a parse. | |
variable StoreContainer "" | |
variable Store [dict create] | |
proc Complete {} { | |
# Called when the binary2dict command is completed as a means to know | |
# when to capture parsed data and send to the proper procedures | |
# for example, the mdcl dictionaries that are built up over the course | |
# of a parse. | |
set container $::DAAP::StoreContainer | |
if {$container == ""} {~! "Warning" "Parsed An Unknown Container Type"; return} | |
~ "Finished Parsing: $container" | |
switch -- $container { | |
cmcp { | |
## Control Prompt Container | |
set ::ATV::CurrentKeyboard $::DAAP::Store | |
~ "Current Keyboard:" | |
~ $::ATV::CurrentKeyboard | |
dict with ::ATV::CurrentKeyboard {} | |
if {$event == "5"} { | |
~! "User Session" "A New User Session has been Detected: $description | $string" | |
return | |
} | |
if {$secure == 1} { | |
~! "Keyboard" "Secure Keyboard (${session} / ${event}): $title | $description (SECURE TEXT)" | |
} else { | |
~! "Keyboard" "Keyboard (${session} / ${event}): $title | $description | $string" | |
} | |
::SendToInterfaces ::Event::Keyboard "$::ATV::CurrentKeyboard" | |
} | |
cmst { | |
## Play Status Container | |
set state "" | |
set ::ATV::PrevNowPlaying $::ATV::NowPlaying | |
set ::ATV::NowPlaying $::DAAP::Store | |
dict with ::ATV::NowPlaying {} | |
~ "Pre-Formatted Now Playing:\n\n$::ATV::NowPlaying" | |
set airplay 0; set newsession 0; set id 0 | |
if {[dict exists $status official]} {set official [dict get $status official]} | |
if {[info exists plist]} { | |
if {[dict exists $plist d1 id]} { | |
set id [dict get $plist d1 id] | |
dict set ::ATV::NowPlaying status id $id | |
dict unset ::ATV::NowPlaying plist | |
} | |
} | |
if {[dict exists $status serviceKind]} { | |
switch -- [dict get $status serviceKind] { | |
2 { | |
set ::ATV::Session iradio | |
} | |
default { | |
if {$::ATV::Session == "iradio"} { | |
set ::ATV::Session 0 | |
} | |
} | |
} | |
} | |
if {$::ATV::Session == 0} { | |
if {[dict exists $meta state] && [dict get $meta state] == "new"} { | |
set newsession 1 | |
if {[info exists id]} { | |
if {$id == 1 && $official == "true"} { | |
dict set ::ATV::NowPlaying status airplay true | |
if {[dict exists $meta album]} { | |
set ::ATV::Session airplay | |
} | |
} | |
} | |
if {[dict exists $status serviceKind]} { | |
switch -- [dict get $status serviceKind] { | |
2 { | |
set ::ATV::Session iradio | |
} | |
} | |
} | |
if {[dict exists $status kind]} { | |
switch -- [dict get $status kind] { | |
64 { | |
set ::ATV::Session tv | |
} | |
2 { | |
set ::ATV::Session video | |
} | |
1 { | |
if {$::ATV::Session != "iradio"} { | |
set ::ATV::Session music | |
} | |
} | |
} | |
} | |
if {$::ATV::Session == 0} { | |
set ::ATV::Session unknown | |
} | |
} | |
} elseif {[info exists meta] && $meta == ""} { | |
set ::ATV::Session 0 | |
set ::ATV::iTunesMeta "" | |
} | |
if {[info exists status]} { | |
dict with status {} | |
if {[info exists official] && $official == "true"} { | |
if {[info exists kind] && $kind == 1} { | |
# Apple Audio Service (Airplay, iTunes Radio) | |
if {[info exists $serviceKind] && $serviceKind == 2} { | |
# iTunes Radio? | |
} | |
} else { | |
# Apple Video Service? | |
dict set ::ATV::NowPlaying status kind 0 | |
} | |
} else { | |
set official "false" | |
} | |
} | |
if {$::ATV::Session != 0} { | |
dict set ::ATV::NowPlaying session $::ATV::Session | |
switch -- $::ATV::Session { | |
airplay { | |
# Currently In An Airplay Session | |
set airplay 1 | |
} | |
iradio { | |
# Currently In An iTunes Radio Session | |
} | |
} | |
} | |
if {$airplay == 1} { | |
if {$::ATV::Airplay != 1} { | |
~! "Airplay Event" "Evaluating for Airplay Starts Events" | |
::DeviceEvents::Evaluate 2 -event Starts | |
} | |
set ::ATV::Airplay 1 | |
dict set ::ATV::NowPlaying status airplay true | |
} else { | |
if {$::ATV::Airplay == 1} { | |
~! "Airplay Event" "Evaluating for Airplay Stops Events" | |
::DeviceEvents::Evaluate 2 -event Stops | |
} | |
set ::ATV::Airplay 0 | |
dict set ::ATV::NowPlaying status airplay false | |
} | |
if {[dict exists $::ATV::NowPlaying iTunes]} {dict unset ::ATV::NowPlaying iTunes} | |
if {[dict exists $::ATV::iTunesMeta queryid] && [dict get $::ATV::iTunesMeta queryid] == $id} { | |
dict set ::ATV::NowPlaying iTunes $::ATV::iTunesMeta | |
} | |
::SendToInterfaces ::Event::NowPlaying "$::ATV::NowPlaying" | |
if {[info exists meta]} { | |
dict with meta {} | |
if {[info exists remaining]} { | |
set ::ATV::UI::PlayingRemaining [expr {$remaining - 1350}] | |
} else { | |
set ::ATV::UI::PlayingRemaining 0 | |
} | |
if {[info exists runtime]} { | |
set ::ATV::UI::PlayingRuntime $runtime | |
} else { | |
set ::ATV::UI::PlayingRuntime 0 | |
} | |
if {[info exists state]} { | |
::ATV::UI::Status $state | |
} | |
} | |
if {[dict exists $meta state] && [dict get $meta state] == "playing"} { | |
# Session Wrapup Operations - Generally to Get Extra Meta | |
switch -- $::ATV::Session { | |
iradio { | |
if {$id != $::iTunes::LastQuery} {::iTunes::Query id $id} | |
} | |
tv { | |
if {$id != $::iTunes::LastQuery} {::iTunes::Query id $id} | |
} | |
video - | |
unknown { | |
set idlength [string length $id] | |
if {$idlength >= 5 || $idlength <= 10} { | |
~! "Query Attempt" "We are unsure what type of media is playing, we will attempt to query iTunes" | |
if {$id != $::iTunes::LastQuery} {::iTunes::Query id $id} | |
} | |
} | |
} | |
} | |
~! "Media Event" "$::ATV::NowPlaying" | |
::ATV::Control::GetNowPlayingArtwork | |
} | |
avdb {} | |
default {~ "Ignore Container"} | |
} | |
::DAAP::Reset | |
} | |
proc Initialize {container} { | |
set ::DAAP::StoreContainer $container | |
# Initialize the Container Objects | |
switch -- $container { | |
cmcp {set ::DAAP::Store [dict create \ | |
string "" description "" session "" \ | |
event "" title "" secure "" type "" | |
]} | |
cmst {set ::DAAP::Store [dict create meta {} status {}]} | |
} | |
} | |
proc Reset {} { | |
# Reset for the next DAAP Parse | |
set ::DAAP::Store [dict create] | |
set ::DAAP::StoreContainer "" | |
} | |
namespace eval Codes { | |
proc miid {value ascii} { | |
# miid = Prompt ID | |
~ "MIID Parse For Container: $::DAAP::StoreContainer" | |
if {$::DAAP::StoreContainer in $::DAAP::databaseContainers} { | |
} else { | |
~! "Control Prompt" "New Control Prompt ID: $value" | |
set cpromptID [dict get $value dec] | |
STATE set Settings [dict push -> cpromptID] | |
} | |
} | |
proc ceQA {value ascii} { | |
# ceQA = Partner Service? | |
set value [dict get $value dec] | |
if {$value == 0} {set value true} else {set value false} | |
dict set ::DAAP::Store status official $value | |
} | |
proc minm {value ascii} { | |
# minm = Apple TV Name | |
if {$::DAAP::StoreContainer == "msrv"} { | |
~ "Apple TV Name: $value" | |
STATE set Settings [dict create atvName $value] | |
} | |
} | |
proc mlid {value ascii} { | |
# mlid = session-id | |
# mlid Received in DAAP Response | |
~ "mlid value: $value" | |
set mliddec [dict get? $value dec] | |
STATE set Settings [dict push -> mliddec] | |
} | |
proc casc {value ascii} { | |
# casc = Speaker Container? | |
# Only seems to identify Airplay during Apple Streaming Audio Service | |
dict set ::DAAP::Store status speakerType [dict get $value dec] | |
} | |
proc caks {value ascii} { | |
# caks = Kind of Service? | |
# 2 - iTunes Radio | |
# 6 - Anything Else? | |
dict set ::DAAP::Store status serviceKind [dict get $value dec] | |
} | |
proc cann {value ascii} { | |
# cann = Now Playing Title | |
dict set ::DAAP::Store meta title $value | |
} | |
proc ceNR {value ascii} { | |
# ceNR = Radio Station Name | |
dict set ::DAAP::Store meta station $value | |
} | |
proc cash {value ascii} { | |
# cash = Shuffle Status | |
dict set ::DAAP::Store status shuffle [dict get $value dec] | |
} | |
proc ceSD {value ascii} { | |
# ceSD = bplist | |
set value [::DAAP::plist2dict $value] | |
dict set ::DAAP::Store plist $value | |
return $value | |
} | |
proc carps {value ascii} { | |
# carps = Repeat Status | |
dict set ::DAAP::Store status repeat [dict get $value dec] | |
} | |
proc cana {value ascii} { | |
# cana = Now Playing Artist | |
dict set ::DAAP::Store meta artist $value | |
} | |
proc canl {value ascii} { | |
# canl = Now Playing Album | |
dict set ::DAAP::Store meta album $value | |
} | |
proc cang {value ascii} { | |
# cang = Now Playing Genre | |
dict set ::DAAP::Store meta genre $value | |
} | |
proc cast {value ascii} { | |
# cast = Runtime | |
dict set ::DAAP::Store meta runtime [dict get $value dec] | |
} | |
proc cant {value ascii} { | |
# cant = Time Left | |
dict set ::DAAP::Store meta remaining [dict get $value dec] | |
} | |
proc caps {value ascii} { | |
# caps = Play Status | |
set status [dict get $value dec] | |
switch -- $status { | |
1 {set state new} | |
2 {set state stopped} | |
3 {set state paused} | |
4 {set state playing} | |
5 {set state fastforward} | |
6 {set state rewind} | |
default {set state "unknown$status"} | |
} | |
dict set ::DAAP::Store meta state $state | |
} | |
proc cmmk {value ascii} { | |
# cmmk = Media Kind | |
dict set ::DAAP::Store status kind [dict get $value dec] | |
} | |
proc mdcl {tempDict ascii} { | |
# mdcl = Dictionary Contents | |
if {![dict exists $tempDict cmce]} {return} else {set key [dict get $tempDict cmce]} | |
if {![dict exists $tempDict cmcv]} {set value -1} else {set value [dict get $tempDict cmcv]} | |
if {[dict exists $tempDict cmcv hex]} { | |
set ascii 0; set dec [dict get $value dec]; set hex [dict get $value hex] | |
~ "Decimal: $dec" | |
~ "HEX: $hex" | |
} else { | |
set ascii 1; set dec ""; set hex "" | |
} | |
~ "MDCL KEY: $key" | |
switch -nocase -glob -- $key { | |
"certificate" {return} | |
"*MsgKey_String*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
dict set ::DAAP::Store string $value | |
} else {~ "No Buffer String Found"} | |
} | |
"*MsgKey_SubText*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
dict set ::DAAP::Store description $value | |
} else {~ "Failed to Get Subtext"} | |
} | |
"*SessionID*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
STATE set Settings [dict create kbSessionID $value] | |
dict set ::DAAP::Store session $value | |
} else {~! "Warning" "Failed to Get Keyboard Session ID"} | |
} | |
"*TextInputType*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
dict set ::DAAP::Store type $value | |
} else {~ "Failed to Get Input Type"} | |
} | |
"*MsgKey_MessageType*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
switch -- $value { | |
0 {set value open} | |
1 {~ "Unknown Message Type!"; set value unknown} | |
2 {set value close} | |
3 { | |
set value Certificate | |
~! "Certificate" "Received Apple Fairplay Certificate" | |
set ::DAAP::StoreContainer 0 | |
} | |
4 { | |
set value "4" | |
} | |
5 { | |
# Remote App Opens? | |
# New Remote Authenticates? | |
#cmcp {mstt {hex 000000c8 dec 200} | |
#miid {hex 00000052 dec 82} mdcl { | |
#0 {cmce kKeybMsgKey_SubText cmcv 1681653760} | |
#1 {cmce kKeybMsgKey_Version cmcv {hex 30 dec 48}} | |
#2 {cmce kKeybMsgKey_MinCharacters cmcv {hex 30 dec 48}} | |
#3 {cmce kKeybMsgKey_MaxCharacters cmcv {hex 30 dec 48}} | |
#4 {cmce kKeybMsgKey_SecureText cmcv {hex 30 dec 48}} | |
#5 {cmce kKeybMsgKey_KeyboardType cmcv {hex 30 dec 48}} | |
#6 {cmce kKeybMsgKey_String cmcv 23382} | |
#7 {cmce kKeybMsgKey_TextInputType cmcv {hex 30 dec 48}} | |
#8 {cmce kKeybMsgKey_Title cmcv (null)} | |
#9 {cmce kKeybMsgKey_MessageType cmcv {hex 35 dec 53}} | |
#10 {cmce kKeybMsgKey_SessionID cmcv 53}}} | |
set value "5" | |
} | |
default {set value "defaulted"} | |
} | |
dict set ::DAAP::Store event $value | |
} else {~! "Warning" "Failed to Get Event"} | |
} | |
"*KeyboardType*" { | |
# Check for Different Keyboard Types? | |
} | |
"*SecureText*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
dict set ::DAAP::Store secure $value | |
} else {~ "Failed to Get Input Type"} | |
} | |
"*MsgKey_Title*" { | |
if {$ascii == 0} {set value [Convert HEX to ASCII -> {*}$hex]} | |
if {$value != -1} { | |
dict set ::DAAP::Store title $value | |
} else {~! "Warning" "Failed to Get Keyboard Title"} | |
} | |
} | |
} | |
} | |
proc binary2dict {data {i 0}} { | |
set retval [list] | |
set codes "" | |
set container "" | |
while {[string length $data] > 0} { | |
set ascii 0; set binary 0 | |
if {![binary scan $data a4I contentCode contentLength]} {return -1} | |
# 62706c6973743030d1010252696459383739323733353635080b0e0000000000000101000000000000000300000000000000000000000000000018 | |
set data [string range $data 8 end] | |
set contentValue [string range $data 0 [expr {$contentLength - 1}]] | |
set data [string range $data $contentLength end] | |
~ "$contentCode $contentLength" | |
if {$contentCode in $::DAAP::binaryCodes} {~ "Binary Code"; set contentValue [::DAAP::Codes::$contentCode $contentValue $ascii]; set binary 1} | |
if {$contentCode in $::DAAP::groups} { | |
if {$contentCode in $::DAAP::containers} {::DAAP::Initialize $contentCode; ~ "STORE CONTAINER: $contentCode"} | |
set codeCount [lsearch -all $codes $contentCode] | |
if {$codeCount != ""} { | |
set count [llength $codeCount] | |
} else {set count 0} | |
lappend codes $contentCode | |
set contentValue [binary2dict $contentValue 1] | |
set nested 1 | |
} else { | |
if {$contentCode in $::DAAP::hexCodes} { | |
set ascii 0 | |
} else { | |
if {$contentLength ni "1 4 8" || $contentCode in $::DAAP::asciiCodes} { | |
~ "Set $contentCode to ASCII" | |
set contentValue [string map {"\x00" ""} $contentValue] | |
set contentValue [encoding convertfrom utf-8 $contentValue] | |
set ascii 1 | |
} | |
} | |
if {$ascii == 0 && $binary != 1} { | |
~ "Set $contentCode to HEX" | |
binary scan $contentValue H* contentValue | |
set dec [format %i 0x$contentValue] | |
#scan 0x$contentValue %x dec | |
} | |
set nested 0 | |
} | |
if {!$nested && !$ascii && !$binary} { | |
set value [dict create hex $contentValue dec $dec] | |
lappend retval $contentCode $value | |
} elseif {!$nested && $ascii || !$nested && $binary} { | |
set value $contentValue | |
lappend retval $contentCode $value | |
} else { | |
if {$i == 0} { | |
set value $contentValue | |
dict set retval $contentCode $value | |
} else { | |
set value $contentValue | |
dict set retval $contentCode $count $value | |
incr i | |
} | |
} | |
if {$contentCode in $::DAAP::keyCodes} { | |
~ "$contentCode is a Key Code, Sending to Script" | |
if {[catch {::DAAP::Codes::$contentCode $value $ascii} response]} { | |
~! "DAAP Error" "Couldnt Parse $contentCode" | |
~! "DAAP Error" "Message: $response" | |
} | |
} | |
} | |
return $retval | |
} | |
proc Parse {data} { | |
set tempDict [::DAAP::binary2dict $data] | |
::DAAP::Complete | |
return $tempDict | |
} | |
proc plist2dict {data} { | |
~ "Scanning Apple Binary plist" | |
set tempDict [dict create] | |
set hex [Convert ASCII to HEX -> $data] | |
~ "$hex" | |
# Get the Footer Data from the Data (Last 32 Bits) | |
binary scan $data a8x*X32x6c1c1x4c4x4c4x4c4 name offset objref objCount rootObjRef offsetTableIndex | |
~ "Name: $name | Offset: $offset | Reference: $objref | Object Count: $objCount | Root Reference: $rootObjRef | Offset Table Index: $offsetTableIndex" | |
set offsetTableIndex [::tcl::mathop::+ {*}$offsetTableIndex] | |
set objCount [::tcl::mathop::+ {*}$objCount] | |
set data [string range $data 0 end-32] | |
binary scan $data x${offsetTableIndex}c* dataOffsets | |
# i = Iterations, k = Key, P = Total Pairs (For Parsing Dictionaries), buf = Data Buffer, parsing = Type Currently Being Parsed | |
set i 0; set k 0; set p 0; set buf ""; ; set parsing "" | |
foreach offset $dataOffsets { | |
binary scan $data @${offset}H1 type | |
switch -- $type { | |
d { | |
if {$parsing == ""} {incr i} | |
binary scan $data @${offset}x1c1 value | |
set parsing $type | |
set p $value | |
} | |
5 { | |
# Parsing Single Byte String | |
binary scan $data @${offset}h1a* value remaining | |
scan $value %x value | |
if {$value == 15} { | |
# Value is Larger than Single HEX Can Handle, Must Parse Value | |
binary scan $remaining H1Xh1a* rtype rnum remaining | |
set rlength [expr {int(pow(2,$rnum))}] | |
binary scan $remaining c${rlength}a* flength remaining | |
set flength [::tcl::mathop::+ {*}$flength] | |
binary scan $remaining a$flength v | |
} else { | |
binary scan $data @${offset}x1a${value} v | |
} | |
switch -- $parsing { | |
d { | |
if {$k == 0} { | |
set buf $v | |
set k 1 | |
} elseif {$buf != ""} { | |
dict set tempDict "d$i" $buf $v | |
incr p -1 | |
set k 0 | |
} | |
if {$p == 0} {set parsing ""} | |
} | |
} | |
} | |
} | |
} | |
dict set tempDict dictionaries $i | |
return $tempDict | |
} | |
proc Encode {daapBody} { | |
## ::DAAP::Encode | |
# Encode a Dictionary to DAAP | |
set daap [dict create] | |
dict set daap elementArray {} | |
set daapNames [list cmcc cmbe cmte menu] | |
foreach name $daapNames { | |
if {[dict exists $daapBody $name]} { | |
set daap [daapAddElement $daap $name [dict get $daapBody $name]] | |
} | |
} | |
return [daapPacket $daap] | |
} | |
proc daapAddElement {daap elementName elementValue} { | |
set data $elementName | |
set elementLength [string length $elementValue] | |
set length1 [expr ($elementLength >> 16) & 0xffff] | |
set length2 [expr $elementLength & 0xffff] | |
append data [binary format S1 $length1] | |
append data [binary format S1 $length2] | |
if {[string is integer -strict $elementValue]} { | |
append data $elementValue | |
#append data [binary format c $elementValue] | |
} else { | |
append data $elementValue | |
} | |
set elementArray [dict get $daap elementArray] | |
lappend elementArray $data | |
dict set daap elementArray $elementArray | |
return $daap | |
} | |
proc daapPacket {daap} { | |
set packet "" | |
foreach element [dict get $daap elementArray] {append packet $element} | |
return $packet | |
} | |
proc putsHex {pak} { | |
binary scan $pak H* bh | |
puts $bh | |
} | |
} | |
#set daapBody [dict create cmcc "<keyboardSessionID>" cmbe "<PromptDone>" cmte "This is the keyboard text"] | |
#set packet [::DAAP::Encode $daapBody] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment