DefaultAudioRendererPresentationControl.java

/*
 * *********************************************************************************************************************
 *
 * blueMarine II: Semantic Media Centre
 * http://tidalwave.it/projects/bluemarine2
 *
 * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
 *
 * *********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * *********************************************************************************************************************
 *
 * git clone https://bitbucket.org/tidalwave/bluemarine2-src
 * git clone https://github.com/tidalwave-it/bluemarine2-src
 *
 * *********************************************************************************************************************
 */
package it.tidalwave.bluemarine2.ui.audio.renderer.impl;

import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.time.Duration;
import java.util.stream.Collectors;
import it.tidalwave.util.annotation.VisibleForTesting;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.application.Platform;
import it.tidalwave.role.ui.UserAction;
import it.tidalwave.messagebus.annotation.ListensTo;
import it.tidalwave.messagebus.annotation.SimpleMessageSubscriber;
import it.tidalwave.bluemarine2.model.audio.AudioFile;
import it.tidalwave.bluemarine2.model.MediaItem.Metadata;
import it.tidalwave.bluemarine2.model.PlayList;
import it.tidalwave.bluemarine2.ui.commons.RenderAudioFileRequest;
import it.tidalwave.bluemarine2.ui.commons.OnDeactivate;
import it.tidalwave.bluemarine2.ui.audio.renderer.MediaPlayer;
import it.tidalwave.bluemarine2.ui.audio.renderer.AudioRendererPresentation;
import it.tidalwave.bluemarine2.ui.audio.renderer.MediaPlayer.Status;
import lombok.extern.slf4j.Slf4j;
import static it.tidalwave.role.ui.Displayable._Displayable_;
import static it.tidalwave.bluemarine2.util.Formatters.format;
import static it.tidalwave.bluemarine2.ui.audio.renderer.MediaPlayer.Status.*;
import static it.tidalwave.bluemarine2.model.MediaItem.Metadata.*;
import static it.tidalwave.util.PropertyWrapper.wrap;

/***********************************************************************************************************************
 *
 * The Control of the {@link AudioRendererPresentation}.
 *
 * @stereotype  Control
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
@SimpleMessageSubscriber @Slf4j
public class DefaultAudioRendererPresentationControl
  {
    @Inject
    private AudioRendererPresentation presentation;

    @Inject
    private MediaPlayer mediaPlayer;

    private final AudioRendererPresentation.Properties properties = new AudioRendererPresentation.Properties();

    private Duration duration = Duration.ZERO;

    private PlayList<AudioFile> playList = PlayList.empty();

    // Discriminates a forced stop from media player just terminating
    private boolean stopped;

    private final UserAction prevAction = UserAction.of(() -> changeTrack(playList.previous().get()));

    private final UserAction nextAction = UserAction.of(() -> changeTrack(playList.next().get()));

    private final UserAction rewindAction = UserAction.of(() -> mediaPlayer.rewind());

    private final UserAction fastForwardAction = UserAction.of(() -> mediaPlayer.fastForward());

    private final UserAction pauseAction = UserAction.of(() -> mediaPlayer.pause());

    private final UserAction playAction = UserAction.of(this::play);

    private final UserAction stopAction = UserAction.of(this::stop);

    // FIXME: use expression binding
    // e.g.  properties.progressProperty().bind(mediaPlayer.playTimeProperty().asDuration().dividedBy/duration));
    // FIXME: weak, remove previous listeners
    private final ChangeListener<Duration> l =
                (ObservableValue<? extends Duration> observable,
                 Duration oldValue,
                 Duration newValue) ->
        {
          // FIXME: the control shouldn't mess with JavaFX stuff
          Platform.runLater(() ->
            {
              properties.playTimeProperty().setValue(format(newValue));
              properties.progressProperty().setValue((double)newValue.toMillis() / duration.toMillis());
            });
        };

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @PostConstruct
    @VisibleForTesting void initialize()
      {
        presentation.bind(properties,
                          prevAction, rewindAction, stopAction, pauseAction, playAction, fastForwardAction, nextAction);
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @VisibleForTesting void onRenderAudioFileRequest (@ListensTo @Nonnull final RenderAudioFileRequest request)
      throws MediaPlayer.Exception
      {
        log.info("onRenderAudioFileRequest({})", request);

        playList = request.getPlayList();
        setAudioFile(playList.getCurrentItem().get());
        bindMediaPlayer();
        presentation.showUp(this);
        presentation.focusOnPlayButton();
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @OnDeactivate
    @VisibleForTesting OnDeactivate.Result onDeactivate()
      throws MediaPlayer.Exception
      {
        stop();
        unbindMediaPlayer();
        playList = PlayList.empty();
        return OnDeactivate.Result.PROCEED;
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private void setAudioFile (@Nonnull final AudioFile audioFile)
      throws MediaPlayer.Exception
      {
        log.info("setAudioFile({})", audioFile);
        final Metadata metadata = audioFile.getMetadata();
        log.info(">>>> metadata:  {}", metadata);

        // FIXME: the control shouldn't mess with JavaFX stuff
        // FIXME: this performs some (short) queries that are executed in the JavaFX thread
        Platform.runLater(() ->
          {
            properties.titleProperty().setValue(metadata.get(TITLE).orElse(""));
            properties.artistProperty().setValue(audioFile.findMakers().stream()
                    .map(maker -> maker.as(_Displayable_).getDisplayName())
                    .collect(Collectors.joining(", ")));
            properties.composerProperty().setValue(audioFile.findComposers().stream()
                    .map(composer -> composer.as(_Displayable_).getDisplayName())
                    .collect(Collectors.joining(", ")));
            duration = metadata.get(DURATION).orElse(Duration.ZERO);
            properties.durationProperty().setValue(format(duration));
            properties.folderNameProperty().setValue(
                    audioFile.getRecord().map(record -> record.as(_Displayable_).getDisplayName()).orElse(""));
            properties.nextTrackProperty().setValue(
                    ((playList.getSize() == 1) ? "" : String.format("%d / %d", playList.getIndex() + 1, playList.getSize()) +
                    playList.peekNext().map(t -> " - Next track: " + t.getMetadata().get(TITLE).orElse("")).orElse("")));
          });

        mediaPlayer.setMediaItem(audioFile);
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    private void onMediaPlayerStarted()
      {
        log.info("onMediaPlayerStarted()");
//        presentation.focusOnStopButton();
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    private void onMediaPlayerStopped()
      {
        log.info("onMediaPlayerStopped()");

        if (!stopped)
          {
            presentation.focusOnPlayButton();
          }

        if (!stopped && playList.hasNext())
          {
            // FIXME: check whether the disk is not gapless, and eventually pause
            try
              {
                setAudioFile(playList.next().get());
                play();
              }
            catch (MediaPlayer.Exception e)
              {
                log.error("", e);
              }
          }
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    private void play()
      throws MediaPlayer.Exception
      {
        stopped = false;
        mediaPlayer.play();
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    private void stop()
      throws MediaPlayer.Exception
      {
        stopped = true;
        mediaPlayer.stop();
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    private void changeTrack (@Nonnull final AudioFile audioFile)
      throws MediaPlayer.Exception
      {
        final boolean wasPlaying = mediaPlayer.statusProperty().get().equals(PLAYING);

        if (wasPlaying)
          {
            stop();
          }

        setAudioFile(audioFile);

        if (wasPlaying)
          {
            play();
          }
      }

    /*******************************************************************************************************************
     *
     * Binds to the {@link MediaPlayer}.
     *
     ******************************************************************************************************************/
    @VisibleForTesting void bindMediaPlayer()
      {
        log.debug("bindMediaPlayer()");
        final ObjectProperty<Status> status = mediaPlayer.statusProperty();
        wrap(stopAction.enabled()).bind(status.isEqualTo(PLAYING));
        wrap(pauseAction.enabled()).bind(status.isEqualTo(PLAYING));
        wrap(playAction.enabled()).bind(status.isNotEqualTo(PLAYING));
        wrap(prevAction.enabled()).bind(playList.hasPreviousProperty());
        wrap(nextAction.enabled()).bind(playList.hasNextProperty());
        mediaPlayer.playTimeProperty().addListener(l);

        status.addListener((observable, oldValue, newValue) ->
          {
            switch (newValue)
              {
                case STOPPED:
                    onMediaPlayerStopped();
                    break;

                case PLAYING:
                    onMediaPlayerStarted();
                    break;
              }
          });
      }

    /*******************************************************************************************************************
     *
     * Unbinds from the {@link MediaPlayer}.
     *
     ******************************************************************************************************************/
    @VisibleForTesting void unbindMediaPlayer()
      {
        log.debug("unbindMediaPlayer()");
        wrap(stopAction.enabled()).unbind();
        wrap(pauseAction.enabled()).unbind();
        wrap(playAction.enabled()).unbind();
        wrap(prevAction.enabled()).unbind();
        wrap(nextAction.enabled()).unbind();
        mediaPlayer.playTimeProperty().removeListener(l);
      }
  }