components_config_ServerDiscoveryTask.bs

import "pkg:/source/roku_modules/log/LogMixin.brs"

'
' Task used to discover jellyfin servers on the local network
'
sub init()
  m.log = log.Logger("ServerDiscoveryTask")
  m.top.functionName = "execute"
end sub

sub execute()
  m.servers = []
  m.serverUrlMap = {}
  m.locationUrlMap = {}
  'send both requests at the same time
  SendSSDPBroadcast()
  SendClientDiscoveryBroadcast()

  ts = CreateObject("roTimespan")
  maxTimeMs = 1500

  'monitor each port and collect messages
  while True
    elapsed = ts.TotalMilliseconds()
    if elapsed >= maxTimeMs
      exit while
    end if

    msg = Wait(100, m.ssdp.port)
    if msg <> invalid
      ProcessSSDPResponse(msg)
    end if

    msg = Wait(100, m.clientDiscovery.port)
    if msg <> invalid
      ProcessClientDiscoveryResponse(msg)
    end if

  end while

  m.top.content = m.servers
  m.log.debug("Jellyfin servers found", m.servers[0], m.servers[1], m.servers[2])
end sub

sub AddServer(server)
  if m.serverUrlMap[server.baseUrl] = invalid
    m.serverUrlMap[server.baseUrl] = true
    m.servers.push(server)
  end if
end sub

sub SendClientDiscoveryBroadcast()
  m.clientDiscovery = {
    port: CreateObject("roMessagePort"),
    address: CreateObject("roSocketAddress"),
    socket: CreateObject("roDatagramSocket"),
    urlTransfer: CreateObject("roUrlTransfer")
  }
  m.clientDiscovery.address.SetAddress("255.255.255.255:7359")
  m.clientDiscovery.urlTransfer.SetPort(m.clientDiscoveryPort)
  m.clientDiscovery.socket.SetMessagePort(m.clientDiscovery.port)
  m.clientDiscovery.socket.SetSendToAddress(m.clientDiscovery.address)
  m.clientDiscovery.socket.NotifyReadable(true)
  m.clientDiscovery.socket.SetBroadcast(true)
  m.clientDiscovery.socket.SendStr("Who is JellyfinServer?")
end sub

sub ProcessClientDiscoveryResponse(message)
  if Type(message) = "roSocketEvent" and message.GetSocketId() = m.clientDiscovery.socket.GetId() and m.clientDiscovery.socket.IsReadable()
    try
      responseJson = m.clientDiscovery.socket.ReceiveStr(4096)
      server = ParseJson(responseJson)
      AddServer({
        name: server.Name,
        baseUrl: server.Address,
        'hardcoded icon since this service doesn't include them
        iconUrl: "pkg:/images/branding/logo-icon120.jpg",
        iconWidth: 120,
        iconHeight: 120
      })
      m.log.info("Found Jellyfin server using client discovery", server.Address)
    catch e
      m.log.error("Error scanning for jellyfin server", message)
    end try
  end if
end sub

sub SendSSDPBroadcast()
  m.ssdp = {
    port: CreateObject("roMessagePort"),
    address: CreateObject("roSocketAddress"),
    socket: CreateObject("roDatagramSocket"),
    urlTransfer: CreateObject("roUrlTransfer")
  }
  m.ssdp.address.SetAddress("239.255.255.250:1900")
  m.ssdp.socket.SetMessagePort(m.ssdp.port)
  m.ssdp.socket.SetSendToAddress(m.ssdp.address)
  m.ssdp.socket.NotifyReadable(true)
  m.ssdp.urlTransfer.SetPort(m.ssdp.port)

  'brightscript can't escape characters in strings, so create a few vars here so we can use them in the strings below
  Q = Chr(34)
  CRLF = Chr(13) + Chr(10)

  ssdpStr = "M-SEARCH * HTTP/1.1" + CRLF
  ssdpStr += "HOST: 239.255.255.250:1900" + CRLF
  ssdpStr += "MAN: " + Q + "ssdp:discover" + Q + CRLF
  ssdpStr += "ST:urn:schemas-upnp-org:device:MediaServer:1" + CRLF
  ssdpStr += "MX: 2" + CRLF
  ssdpStr += CRLF

  m.ssdp.socket.SendStr(ssdpStr)
end sub

sub ProcessSSDPResponse(message)
  locationUrl = invalid
  if Type (message) = "roSocketEvent" and message.GetSocketId() = m.ssdp.socket.GetId() and m.ssdp.socket.IsReadable()
    recvStr = m.ssdp.socket.ReceiveStr(4096)
    match = CreateObject("roRegex", "\r\nLocation:\s*(.*?)\s*\r\n", "i").Match(recvStr)
    if match.Count() = 2
      locationUrl = match[1]
    end if
  end if

  if locationUrl = invalid
    return
  else if m.locationUrlMap[locationUrl] <> invalid
    m.log.warn("Already discovered this location", locationUrl)
    return
  end if

  m.locationUrlMap[locationUrl] = true

  http = CreateObject("roUrlTransfer")
  http.SetUrl(locationUrl)
  responseText = http.GetToString()
  xml = CreateObject("roXMLElement")
  'if we successfully parsed the response, process it
  if xml.Parse(responseText)
    deviceNode = xml.GetNamedElementsCi("device")[0]
    manufacturer = deviceNode.GetNamedElementsCi("manufacturer").GetText()
    'only process jellyfin servers
    if lcase(manufacturer) = "jellyfin"
      'find the largest icon
      width = 0
      server = invalid
      icons = deviceNode.GetNamedElementsCi("iconList")[0].GetNamedElementsCi("icon")
      dlnaRegex = CreateObject("roRegex", "(.*?)\/dlna\/", "i")
      for each iconNode in icons
        iconUrl = iconNode.GetNamedElementsCi("url").GetText()
        baseUrl = invalid
        match = dlnaRegex.Match(iconUrl)
        if match.Count() = 2
          baseUrl = match[1]
        end if
        loopResult = {
          name: deviceNode.GetNamedElementsCi("friendlyName").GetText(),
          baseUrl: baseUrl,
          iconUrl: iconUrl,
          iconWidth: iconNode.GetNamedElementsCi("width")[0].GetText().ToInt(),
          iconHeight: iconNode.GetNamedElementsCi("height")[0].GetText().ToInt()
        }
        if baseUrl <> invalid and loopResult.iconWidth > width
          width = loopResult.iconWidth
          server = loopResult
        end if
      end for
      AddServer(server)
      m.log.info("Found jellyfin server using SSDP and DLNA", server.baseUrl)
    end if
  end if
end sub