Skip to content

Instantly share code, notes, and snippets.

@bradennapier
Created May 24, 2018 02:48
Show Gist options
  • Save bradennapier/b87dc8089908bef8f260abe12f0a5d20 to your computer and use it in GitHub Desktop.
Save bradennapier/b87dc8089908bef8f260abe12f0a5d20 to your computer and use it in GitHub Desktop.
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