/**
   * This populates the file-specific transcode folder with all combinations of players, audio
   * tracks and subtitles.
   */
  @Override
  protected void resolveOnce() {
    if (getChildren().size() == 1) { // OK
      DLNAResource child = getChildren().get(0);
      child.syncResolve();

      RendererConfiguration renderer = null;
      if (this.getParent() != null) {
        renderer = this.getParent().getDefaultRenderer();
      }

      // create copies of the audio/subtitle track lists as we're making (local)
      // modifications to them
      List<DLNAMediaAudio> audioTracks = new ArrayList<>(child.getMedia().getAudioTracksList());
      List<DLNAMediaSubtitle> subtitleTracks =
          new ArrayList<>(child.getMedia().getSubtitleTracksList());

      // assemble copies for each combination of audio, subtitle and player
      ArrayList<DLNAResource> entries = new ArrayList<>();

      // First, add the option to simply stream the resource.
      if (renderer != null) {
        LOGGER.trace(
            "Duplicating {} for direct streaming to renderer: {}",
            child.getName(),
            renderer.getRendererName());
      }

      DLNAResource noTranscode = createResourceWithAudioSubtitlePlayer(child, null, null, null);
      addChildInternal(noTranscode);
      addChapterFolder(noTranscode);

      // add options for renderer capable to handle streamed subtitles
      if (!configuration.isDisableSubtitles()
          && renderer != null
          && renderer.isSubtitlesStreamingSupported()) {
        for (DLNAMediaSubtitle subtitle : subtitleTracks) {
          // only add the option if the renderer supports the given format
          if (subtitle.isExternal()) { // do not check for embedded subs
            if (renderer.isExternalSubtitlesFormatSupported(subtitle, child.getMedia())) {
              DLNAResource copy =
                  createResourceWithAudioSubtitlePlayer(child, null, subtitle, null);
              copy.getMediaSubtitle().setSubsStreamable(true);
              entries.add(copy);
              LOGGER.trace(
                  "Duplicating {} for direct streaming subtitles {}",
                  child.getName(),
                  subtitle.toString());
            }
          }
        }
      }

      /*
      we add (or may add) a null entry to the audio list and/or subtitle list
      to ensure the inner loop is always entered:

      for audio in audioTracks:
      for subtitle in subtitleTracks:
      for player in players:
      newResource(audio, subtitle, player)

      there are 4 different scenarios:

      1) a file with audio tracks and no subtitles (subtitle == null): in that case we want
      to assign a player for each audio track

      2) a file with subtitles and no audio tracks (audio == null): in that case we want
      to assign a player for each subtitle track

      3) a file with no audio tracks (audio == null) and no subtitles (subtitle == null)
      e.g. an audio file, a video with no sound and no subtitles or a web audio/video file:
      in that case we still want to provide a selection of players e.g. FFmpeg Web Video
      and VLC Web Video for a web video or FFmpeg Audio and MPlayer Audio for an audio file

      4) one or more audio tracks AND one or more subtitle tracks: this is the case this code
      used to handle when it solely dealt with (local) video files: assign a player
      for each combination of audio track and subtitle track

      If a null audio or subtitle track is passed to createResourceWithAudioSubtitlePlayer,
      it sets the copy's corresponding mediaAudio (AKA params.aid) or mediaSubtitle
      (AKA params.sid) value to null.

      Note: this is the only place in the codebase where mediaAudio and mediaSubtitle
      are assigned (ignoring the trivial clone operation in ChapterFileTranscodeVirtualFolder),
      so setting one or both of them to null is a no-op as they're already null.
      */

      if (audioTracks.isEmpty()) {
        audioTracks.add(null);
      }

      if (subtitleTracks.isEmpty()) {
        subtitleTracks.add(null);
      } else {
        // if there are subtitles, make sure a no-subtitle option is added
        // for each player
        DLNAMediaSubtitle noSubtitle = new DLNAMediaSubtitle();
        noSubtitle.setId(-1);
        subtitleTracks.add(noSubtitle);
      }

      for (DLNAMediaAudio audio : audioTracks) {
        // Create combinations of all audio tracks, subtitles and players.
        for (DLNAMediaSubtitle subtitle : subtitleTracks) {
          // Create a temporary copy of the child with the audio and
          // subtitle modified in order to be able to match players to it.
          DLNAResource temp = createResourceWithAudioSubtitlePlayer(child, audio, subtitle, null);

          // Determine which players match this audio track and subtitle
          ArrayList<Player> players = PlayerFactory.getPlayers(temp);

          // create a copy for each compatible player
          for (Player player : players) {
            DLNAResource copy =
                createResourceWithAudioSubtitlePlayer(child, audio, subtitle, player);
            entries.add(copy);
          }
        }
      }

      // Sort the list of combinations
      Collections.sort(entries, new ResourceSort(PlayerFactory.getPlayers()));

      // Now add the sorted list of combinations to the folder
      for (DLNAResource dlna : entries) {
        LOGGER.trace(
            "Adding {}: audio: {}, subtitle: {}, player: {}",
            new Object[] {
              dlna.getName(),
              dlna.getMediaAudio(),
              dlna.getMediaSubtitle(),
              (dlna.getPlayer() != null ? dlna.getPlayer().name() : null),
            });

        addChildInternal(dlna);
        addChapterFolder(dlna);
      }
    }
  }
  @Override
  public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    RequestV2 request = null;
    RendererConfiguration renderer = null;
    String userAgentString = null;
    StringBuilder unknownHeaders = new StringBuilder();
    String separator = "";
    boolean isWindowsMediaPlayer = false;

    HttpRequest nettyRequest = this.nettyRequest = (HttpRequest) e.getMessage();

    InetSocketAddress remoteAddress = (InetSocketAddress) e.getChannel().getRemoteAddress();
    InetAddress ia = remoteAddress.getAddress();

    // Apply the IP filter
    if (filterIp(ia)) {
      e.getChannel().close();
      LOGGER.trace("Access denied for address " + ia + " based on IP filter");
      return;
    }

    LOGGER.trace("Opened request handler on socket " + remoteAddress);
    PMS.get().getRegistry().disableGoToSleep();

    if (HttpMethod.GET.equals(nettyRequest.getMethod())) {
      request = new RequestV2("GET", nettyRequest.getUri().substring(1));
    } else if (HttpMethod.POST.equals(nettyRequest.getMethod())) {
      request = new RequestV2("POST", nettyRequest.getUri().substring(1));
    } else if (HttpMethod.HEAD.equals(nettyRequest.getMethod())) {
      request = new RequestV2("HEAD", nettyRequest.getUri().substring(1));
    } else {
      request =
          new RequestV2(nettyRequest.getMethod().getName(), nettyRequest.getUri().substring(1));
    }

    LOGGER.trace(
        "Request: "
            + nettyRequest.getProtocolVersion().getText()
            + " : "
            + request.getMethod()
            + " : "
            + request.getArgument());

    if (nettyRequest.getProtocolVersion().getMinorVersion() == 0) {
      request.setHttp10(true);
    }

    // The handler makes a couple of attempts to recognize a renderer from its requests.
    // IP address matches from previous requests are preferred, when that fails request
    // header matches are attempted and if those fail as well we're stuck with the
    // default renderer.

    // Attempt 1: try to recognize the renderer by its socket address from previous requests
    renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(ia);

    if (renderer != null) {
      if (!"WMP".equals(renderer.getRendererName())) {
        PMS.get().setRendererfound(renderer);
        request.setMediaRenderer(renderer);
        LOGGER.trace(
            "Matched media renderer \"" + renderer.getRendererName() + "\" based on address " + ia);
      } else {
        LOGGER.trace("Detected and blocked Windows Media Player");
        isWindowsMediaPlayer = true;
      }
    }

    for (String name : nettyRequest.getHeaderNames()) {
      String headerLine = name + ": " + nettyRequest.getHeader(name);
      LOGGER.trace("Received on socket: " + headerLine);

      if (renderer == null
          && headerLine != null
          && headerLine.toUpperCase().startsWith("USER-AGENT")) {
        userAgentString = headerLine.substring(headerLine.indexOf(":") + 1).trim();

        // Attempt 2: try to recognize the renderer by matching the "User-Agent" header
        renderer = RendererConfiguration.getRendererConfigurationByUA(userAgentString);

        if (renderer != null) {
          if (!"WMP".equals(renderer.getRendererName())) {
            request.setMediaRenderer(renderer);
            renderer.associateIP(ia); // Associate IP address for later requests
            PMS.get().setRendererfound(renderer);
            LOGGER.trace(
                "Matched media renderer \""
                    + renderer.getRendererName()
                    + "\" based on header \""
                    + headerLine
                    + "\"");
          } else if (!isWindowsMediaPlayer) {
            LOGGER.trace("Detected and blocked Windows Media Player");
            isWindowsMediaPlayer = true;
          }
        }
      }

      if (renderer == null && headerLine != null) {
        // Attempt 3: try to recognize the renderer by matching an additional header
        renderer = RendererConfiguration.getRendererConfigurationByUAAHH(headerLine);

        if (renderer != null) {
          request.setMediaRenderer(renderer);
          renderer.associateIP(ia); // Associate IP address for later requests
          PMS.get().setRendererfound(renderer);
          LOGGER.trace(
              "Matched media renderer \""
                  + renderer.getRendererName()
                  + "\" based on header \""
                  + headerLine
                  + "\"");
        }
      }

      try {
        StringTokenizer s = new StringTokenizer(headerLine);
        String temp = s.nextToken();
        if (temp.toUpperCase().equals("SOAPACTION:")) {
          request.setSoapaction(s.nextToken());
        } else if (temp.toUpperCase().equals("CALLBACK:")) {
          request.setSoapaction(s.nextToken());
        } else if (headerLine.toUpperCase().indexOf("RANGE: BYTES=") > -1) {
          String nums =
              headerLine.substring(headerLine.toUpperCase().indexOf("RANGE: BYTES=") + 13).trim();
          StringTokenizer st = new StringTokenizer(nums, "-");
          if (!nums.startsWith("-")) {
            request.setLowRange(Long.parseLong(st.nextToken()));
          }
          if (!nums.startsWith("-") && !nums.endsWith("-")) {
            request.setHighRange(Long.parseLong(st.nextToken()));
          } else {
            request.setHighRange(-1);
          }
        } else if (headerLine.toLowerCase().indexOf("transfermode.dlna.org:") > -1) {
          request.setTransferMode(
              headerLine
                  .substring(headerLine.toLowerCase().indexOf("transfermode.dlna.org:") + 22)
                  .trim());
        } else if (headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") > -1) {
          request.setContentFeatures(
              headerLine
                  .substring(headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") + 28)
                  .trim());
        } else {
          Matcher matcher = TIMERANGE_PATTERN.matcher(headerLine);
          if (matcher.find()) {
            String first = matcher.group(1);
            if (first != null) {
              request.setTimeRangeStartString(first);
            }
            String end = matcher.group(2);
            if (end != null) {
              request.setTimeRangeEndString(end);
            }
          } else {
            // If we made it to here, none of the previous header checks matched.
            // Unknown headers make interesting logging info when we cannot recognize
            // the media renderer, so keep track of the truly unknown ones.
            boolean isKnown = false;

            // Try to match possible known headers.
            for (String knownHeaderString : KNOWN_HEADERS) {
              if (headerLine.toLowerCase().startsWith(knownHeaderString.toLowerCase())) {
                isKnown = true;
                break;
              }
            }

            if (!isKnown) {
              // Truly unknown header, therefore interesting. Save for later use.
              unknownHeaders.append(separator).append(headerLine);
              separator = ", ";
            }
          }
        }
      } catch (Exception ee) {
        LOGGER.error("Error parsing HTTP headers", ee);
      }
    }

    if (!isWindowsMediaPlayer) {
      if (request != null) {
        // Still no media renderer recognized?
        if (request.getMediaRenderer() == null) {

          // Attempt 4: Not really an attempt; all other attempts to recognize
          // the renderer have failed. The only option left is to assume the
          // default renderer.
          request.setMediaRenderer(RendererConfiguration.getDefaultConf());
          LOGGER.trace(
              "Using default media renderer: " + request.getMediaRenderer().getRendererName());

          if (userAgentString != null && !userAgentString.equals("FDSSDP")) {
            // We have found an unknown renderer
            LOGGER.info(
                "Media renderer was not recognized. Possible identifying HTTP headers: User-Agent: "
                    + userAgentString
                    + ("".equals(unknownHeaders.toString())
                        ? ""
                        : ", " + unknownHeaders.toString()));
            PMS.get().setRendererfound(request.getMediaRenderer());
          }
        } else {
          if (userAgentString != null) {
            LOGGER.debug("HTTP User-Agent: " + userAgentString);
          }

          LOGGER.trace(
              "Recognized media renderer: " + request.getMediaRenderer().getRendererName());
        }
      }

      if (HttpHeaders.getContentLength(nettyRequest) > 0) {
        byte data[] = new byte[(int) HttpHeaders.getContentLength(nettyRequest)];
        ChannelBuffer content = nettyRequest.getContent();
        content.readBytes(data);
        request.setTextContent(new String(data, "UTF-8"));
      }

      if (request != null) {
        LOGGER.trace(
            "HTTP: "
                + request.getArgument()
                + " / "
                + request.getLowRange()
                + "-"
                + request.getHighRange());
      }

      writeResponse(e, request, ia);
    }
  }