/home/arranoyd/telegastro/wp-content/plugins/fv-wordpress-flowplayer/models/db.php
<?php
/*  FV Wordpress Flowplayer - HTML5 video player with Flash fallback    
    Copyright (C) 2013  Foliovision

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

// class handling database shortcode generation and saving
class FV_Player_Db {

  private
    $edit_lock_timeout_seconds = 120,
    $videos_cache = array(),
    $video_atts_cache = array(),
    $video_meta_cache = array(),
    $players_cache = array(),
    //$player_atts_cache = array(),
    $player_meta_cache = array();

  public function __construct() {
    add_filter('fv_flowplayer_args_pre', array($this, 'getPlayerAttsFromDb'), 5, 1);
    add_filter('fv_player_item_pre', array($this, 'setCurrentVideoAndPlayer' ), 1, 3 );
    add_action('wp_head', array($this, 'cache_players_and_videos' ));
    
    add_action('save_post', array($this, 'store_post_ids' ));

    add_action( 'wp_ajax_fv_player_db_load', array($this, 'open_player_for_editing') );
    add_action( 'wp_ajax_fv_player_db_export', array($this, 'export_player_data') );
    add_action( 'wp_ajax_fv_player_db_import', array($this, 'import_player_data') );
    add_action( 'wp_ajax_fv_player_db_clone', array($this, 'clone_player') );
    add_action( 'wp_ajax_fv_player_db_remove', array($this, 'remove_player') );
    add_action( 'wp_ajax_fv_wp_flowplayer_retrieve_video_data', array($this, 'retrieve_video_data') ); // todo: nonce, move into controller/editor.php
    add_action( 'wp_ajax_fv_player_db_retrieve_all_players_for_dropdown', array($this, 'retrieve_all_players_for_dropdown') ); // todo: nonce
    add_action( 'wp_ajax_fv_player_db_save', array($this, 'db_store_player_data') ); // todo: error message on failure
  }

  public function getVideosCache() {
    return $this->videos_cache;
  }

  public function setVideosCache($cache) {
    return $this->videos_cache = $cache;
  }

  public function isVideoCached($id) {
    return isset($this->videos_cache[$id]);
  }

  public function getVideoMetaCache() {
    return $this->video_meta_cache;
  }

  public function setVideoMetaCache($cache) {
    return $this->video_meta_cache = $cache;
  }

  public function isVideoMetaCached($id_video, $id_meta = null) {
    return ($id_meta !== null ? isset($this->video_meta_cache[$id_video][$id_meta]) : isset($this->video_meta_cache[$id_video]));
  }

  public function getPlayersCache() {
    return $this->players_cache;
  }

  public function setPlayersCache($cache) {
    return $this->players_cache = $cache;
  }

  public function isPlayerCached($id) {
    return isset($this->players_cache[$id]);
  }

  public function getPlayerMetaCache() {
    return $this->player_meta_cache;
  }

  public function setPlayerMetaCache($cache) {
    return $this->player_meta_cache = $cache;
  }

  public function isPlayerMetaCached($id_player, $id_meta = null) {
    return ($id_meta !== null ? isset($this->player_meta_cache[$id_player][$id_meta]) : isset($this->player_meta_cache[$id_player]));
  }

  public function setCurrentVideoAndPlayer($aItem, $index, $aPlayer) {
    global $fv_fp;

    if (!empty($aPlayer['video_objects'][$index])) {
      $vid_obj = $aPlayer['video_objects'][$index];
      $fv_fp->currentVideoObject = $vid_obj;
      
      if( !empty($aItem['sources'][0]['src']) && ( is_numeric($aItem['sources'][0]['src']) ) || stripos($aItem['sources'][0]['src'],'preview-') === 0 ) {

        $new = array( 'sources' => array() );
        if( $src = $vid_obj->getSrc() ) {
          $new['sources'][] = array( 'src' => apply_filters('fv_flowplayer_video_src',$src,array()), 'type' => $fv_fp->get_mime_type($src) );
        }
        if( $src1 = $vid_obj->getSrc1() ) {
          $new['sources'][] = array( 'src' => apply_filters('fv_flowplayer_video_src',$src1,array()), 'type' => $fv_fp->get_mime_type($src1) );
        }
        if( $src2 = $vid_obj->getSrc2() ) {
          $new['sources'][] = array( 'src' => apply_filters('fv_flowplayer_video_src',$src2,array()), 'type' => $fv_fp->get_mime_type($src2));
        }
        if( $rtmp = $vid_obj->getRtmp() ) {
          $new['rtmp'] = $rtmp;
        }
        if( $rtmp_path = $vid_obj->getRtmpPath() ) {
          $ext = $fv_fp->get_mime_type($rtmp_path,false,true) ? $fv_fp->get_mime_type($rtmp_path,false,true).':' : false;
          $new['sources'][] = array( 'src' => $ext.$rtmp_path, 'type' => 'video/flash' );
        }
        
        if( count($new['sources']) ) {
          $aItem = $new;
        }
      }
            
      if ( count($vid_obj->getMetaData())) {
        foreach ($vid_obj->getMetaData() as $meta) {
          if ($meta->getMetaKey() == 'live' && $meta->getMetaValue() == 'true') {
            $aItem['live'] = 'true';
          }
          if ($meta->getMetaKey() == 'dvr' && $meta->getMetaValue() == 'true') {
            $aItem['dvr'] = 'true';
          }
          if ($meta->getMetaKey() == 'audio' && $meta->getMetaValue() == 'true') {
            $aItem['is_audio_stream'] = 'true';
          }
        }
      }
      
      if( $id = $vid_obj->getId() ) {
        $aItem['id'] = $id;
      }
      
      if( $start = $vid_obj->getStart() ) {
        $aItem['fv_start'] = $start;
      }
      if( $end = $vid_obj->getEnd() ) {
        $aItem['fv_end'] = $end;
      }      
      
    } else {
      $fv_fp->currentVideoObject = null;
      $fv_fp->currentPlayerObject = null;
    }

    return $aItem;
  }

  public function cache_players_and_videos() {
    global $posts;
    if( !empty($posts) && is_array($posts) ) {
      $player_ids = array();
      foreach( $posts AS $post ) {
        if (isset($post->post_content)) {
          preg_match_all( '/\[fvplayer id="(\d+)"[^\]]*\]/m', $post->post_content, $matches, PREG_SET_ORDER, 0 );
          if ( $matches && count( $matches ) ) {
            foreach ( $matches as $match ) {
              $player_ids[] = $match[1];
            }
          }
        }
      }

      if (count($player_ids)) {
        $this->cache_players_and_videos_do( $player_ids );
      }
    }
  }
  
  public function cache_players_and_videos_do( $player_ids ) {
    // load all players at once
    new FV_Player_Db_Player( $player_ids, array(), $this );

    // load all player meta
    new FV_Player_Db_Player_Meta( null, array( 'id_player' => $player_ids ), $this );

    // pre load all videos and their meta for these players
    $video_ids = array();
    foreach( $this->players_cache as $player ) {
      $video_ids = array_merge( $video_ids, explode( ',', $player->getVideoIds() ) );
    }

    if( count( $video_ids ) ) {
      new FV_Player_Db_Video( $video_ids, array(), $this );
      new FV_Player_Db_Video_Meta( null, array( 'id_video' => $video_ids ), $this );
    }
  }

  /**
   * Retrieves data for all players table shown in admin.
   *
   * @param $order_by  If set, data will be ordered by this column.
   * @param $order     If set, data will be ordered in this order.
   * @param $offset    If set, data will returned will be limited, starting at this offset.
   * @param $per_page  If set, data will returned will be limited, ending at this offset.
   * @param $single_id If set, data will be restricted to a single player ID.
   * @param $search    If set, results will be searched for using the GET search parameter.
   *
   * @return array     Returns an array of all list page results to be displayed.
   * @throws Exception When the underlying FV_Player_Db_Video class generates an error.
   */
  public static function getListPageData($order_by, $order, $offset, $per_page, $single_id = null, $search = null) {
    global $player_ids_when_searching, $FV_Player_Db; // this is an instance of this same class, but since we're in static context, we need to access this globally like that... sorry :P

    // sanitize variables
    $order = (in_array($order, array('asc', 'desc')) ? $order : 'asc');
    $order_by = (in_array($order_by, array('id', 'player_name', 'date_created', 'author', 'subtitles_count', 'chapters_count', 'transcript_count')) ? $order_by : 'id');
    $author_id = get_current_user_id();
    $cannot_edit_other_posts = !current_user_can('edit_others_posts');

    // load single player, as requested by the user
    if ($single_id) {
      new FV_Player_Db_Player( $single_id, array(), $FV_Player_Db );
    } else if ($search) {
      // search for videos that are consistent with the search text
      // and load their players only
      $vids = FV_Player_Db_Video::search(array('src', 'src1', 'src2', 'caption', 'splash', 'splash_text'), $search, true, 'OR', 'id');

      // if we have any data, assemble video IDs and load their players
      if ($vids !== false) {
        $player_video_ids = array();

        foreach ($vids as $db_record) {
          $player_video_ids[] = $db_record->id;
        }

        // cache this, so we can use this in the FV_Player_Db_Player::getTotalPlayersCount() method
        $player_ids_when_searching = $player_video_ids;

        $db_options = array(
          'select_fields'       => 'player_name, date_created, videos, author, status',
          'order_by'            => $order_by,
          'order'               => $order,
          'offset'              => $offset,
          'per_page'            => $per_page,
          'search_by_video_ids' => $player_video_ids
        );

        if( $cannot_edit_other_posts ) {
          $db_options['author_id'] = $author_id;
        }

        new FV_Player_Db_Player( null, array(
          'db_options' => $db_options
        ), $FV_Player_Db );
      }
    } else {
      // load all players, which will put them into the cache automatically

      $db_options = array(
        'select_fields' => 'player_name, date_created, videos, author, status',
        'order_by'      => $order_by,
        'order'         => $order,
        'offset'        => $offset,
        'per_page'      => $per_page,
      );

      if( $cannot_edit_other_posts ) {
        $db_options['author_id'] = $author_id;
      }

      new FV_Player_Db_Player( null, array(
        'db_options' => $db_options
      ), $FV_Player_Db );
    }
    
    global $fv_fp;
    $stats_enabled = $fv_fp->_get_option('video_stats_enable');

    $players = $FV_Player_Db->getPlayersCache();

    // get all video IDs used in all players
    if ($players && count($players)) {
      $videos = array();
      $result = array();

      foreach ($players as $player) {
        /* @var FV_Player_Db_Player $player */
        $videos = array_merge($videos, explode(',', $player->getVideoIds()));
      }

      // load all videos data at once
      if (count($videos)) {
        $vids_data = new FV_Player_Db_Video( $videos, array(
          'db_options' => array(
            'select_fields' => 'caption, src, splash'
          )
        ), $FV_Player_Db );

        // reset $videos variable and index all of our video data,
        // so they are easily accessible when building the resulting
        // display data
        if ($vids_data) {
          /* @var FV_Player_Db_Video[] $videos */
          $videos = array();
          if (count($FV_Player_Db->getVideosCache())) {
            foreach ( $FV_Player_Db->getVideosCache() as $video_object ) {
              $videos[ $video_object->getId() ] = $video_object;
            }
          }
        }

        // build the result
        foreach ($players as $player) {
          // player data first
          $result_row = new stdClass();
          $result_row->id = $player->getId();
          $result_row->player_name = $player->getPlayerName();
          $result_row->date_created = $player->getDateCreated();
          $result_row->thumbs = array();
          $result_row->author = $player->getAuthor();
          $result_row->subtitles_count = $player->getCount('subtitles');
          $result_row->chapters_count = $player->getCount('chapters');
          $result_row->transcript_count = $player->getCount('transcript');
          $result_row->status = __($player->getStatus(), 'fv-wordpress-flowplayer');
          $result_row->video_objects = $videos;

          // no player name, we'll assemble it from video captions and/or sources
          if (!$result_row->player_name) {
            $result_row->player_name = array();
          }

          foreach (explode(',', $player->getVideoIds()) as $video_id) {
            if( empty($videos[ $video_id ]) ) { // the videos field might point to a missing video
              continue;
            }
            
            $video = $videos[ $video_id ];
            
            $caption = $video->getCaption();
            if( !$caption ) {
              $caption = $video->getCaptionFromSrc();
            }            
            
            // assemble video name, if there's no player name
            if (is_array($result_row->player_name)) {
              $result_row->player_name[] = $caption;
            }

            // assemble video splash
            if (isset($videos[ $video_id ]) && $videos[ $video_id ]->getSplash()) {
              // use splash with caption / filename in a span
              if ( isset($videos[ $video_id ]) && $caption ) {
                $txt = $caption;
              } else {
                $txt = esc_attr($caption_src);
              }
              
              $splash = apply_filters( 'fv_flowplayer_playlist_splash', $videos[ $video_id ]->getSplash() );

              $result_row->thumbs[] = '<div class="fv_player_splash_list_preview"><img src="'.esc_attr($splash).'" width="100" alt="'.esc_attr($txt).'" title="'.esc_attr($txt).'" loading="lazy" /><span>' . $txt . '</span></div>';
            } else if ( isset($videos[ $video_id ]) && $caption ) {
              // use caption
              $result_row->thumbs[] = '<div class="fv_player_splash_list_preview fv_player_list_preview_no_splash" title="' . esc_attr($caption) . '"><span>' . $caption . '</span></div>';
            }
            
            if( $stats_enabled ) {
              if( !isset($result_row->stats_play) ) $result_row->stats_play = 0;
              $result_row->stats_play += intval($video->getMetaValue('stats_play',true)); // todo: lower SQL count
            }
          }

          // join name items, if present
          if (is_array($result_row->player_name)) {
            $result_row->player_name = join(', ', $result_row->player_name);
          }

          // add "Draft" at the end of player, if in draft status
          if ( $player->getStatus() == 'draft' ) {
            $result_row->player_name .= ' (' . __('Draft', 'fv-wordpress-flowplayer') . ')';
          }

          // join thumbnails
          $result_row->thumbs = join(' ', $result_row->thumbs);

          $result[] = $result_row;
        }

        return $result;
      }
    }

    return array();
  }
  


  /**
   * Generates a full code for a playlist from one that uses video IDs
   * stored in the database to one that uses the first video src attribute
   * Playlist items stay as IDs and are filled in flowplayer::build_playlist_html()
   *
   * @param array $atts Player attributes to build the player shortcode from.
   * @param array $preview_data Alternative data to use instead of the $atts array
   *                            when we want to show previews etc.
   *
   * @return array Returns augmented array of attributes that get picked up
   *               on the front-end side.
   * @throws Exception When any of the underlying classes throw an exception.
   */
  private function generateFullPlaylistCode($atts, $preview_data = null) {
    global $fv_fp;

    // check if we should change anything in the playlist code
    if ($preview_data || (isset($atts['playlist']) && preg_match('/^[\d,]+$/m', $atts['playlist']))) {
      $new_playlist_tag = array();      
      $first_video_data_cached = false;

      // serve what we can from the cache
      if (!$preview_data) {
        $ids    = explode( ',', $atts['playlist'] );
        $newids = array();

        // check the first video, which is the main one for the playlist
        if ( isset( $this->video_atts_cache[ $ids[0] ] ) ) {
          $first_video_data_cached = true;
          $atts                    = array_merge( $atts, $this->video_atts_cache[ $ids[0] ] );
        }

        // prepare cached data and IDs that still need loading from DB
        foreach ( $ids as $id ) {
          if ( isset( $this->video_atts_cache[ $id ] ) ) {
            $new_playlist_tag[] = $id;            
          } else {
            $newids[] = (int) $id;
          }
        }
      }

      if ($preview_data || count($newids)) {
        if ($preview_data) {
          $videos = $preview_data['videos'];
        } else {
          $videos = $fv_fp->current_player()->getVideos();
        }

        // cache first vid
        if (!$first_video_data_cached && $videos) {
          $vid = $videos[0]->getAllDataValues();

          // we need to keep the player id!
          $first_video = $vid;
          unset($first_video['id']);
          $atts = array_merge($atts, $first_video);
          $atts['video_objects'] = array($videos[0]);

          // don't cache if we're previewing
          if (!$preview_data) {
            $this->video_atts_cache[ $vid['id'] ] = $vid;
          }

          // remove the first video and keep adding the rest of the videos to the playlist tag
          array_shift( $videos );
        }

        // add rest of the videos into the playlist tag
        if ($videos && count($videos)) {
          foreach ( $videos as $k => $vid_object ) {
            $vid                               = $vid_object->getAllDataValues();
            $vid_id                            = isset($vid['id']) ? $vid['id'] : 'preview-'.($k+1);
            $atts['video_objects'][]           = $vid_object;
            $this->video_atts_cache[ $vid_id ] = $vid;
            $new_playlist_tag[]                = $vid_id;
          }

          $atts['playlist'] = implode(';', $new_playlist_tag);

        } else if (isset($videos) && is_array($videos)) {
          // only one video found, therefore this is not a playlist
          unset($atts['playlist']);
        }
      } else {
        // remove the first video from playlist, since that is
        // the video in src and would duplicate that video in player
        // as a result
        array_shift($new_playlist_tag);

        $atts['playlist'] = implode(';', $new_playlist_tag);
      }
    }
    
    return $atts;
  }


  /**
   * Maps attributes from database into their respective shortcode names.
   *
   * @param $att_name Attribute name from the database to map into shortcode format.
   *
   * @return mixed Returns the correct attribute name for shortcode use.
   */
  private function mapDbAttributes2Shortcode($att_name) {
    $atts_map = array(
      'playlist'       => 'liststyle',
      'video_ads'      => 'preroll',
      'video_ads_post' => 'postroll'
    );

    return (isset($atts_map[$att_name]) ? $atts_map[$att_name] : $att_name);
  }


  /**
   * Maps attributes values from database into their respective shortcode values.
   *
   * @param $att_name  Attribute name from the database.
   * @param $att_value Attribute value from the database.
   *
   * @return mixed Returns the correct attribute value for shortcode use.
   */
  private function mapDbAttributeValue2Shortcode($att_name, $att_value, $data) {
    switch ($att_name) {
      case 'playlist_advance':
        if($att_value == 'on' ) return 'true';
        if($att_value == 'off' ) return 'false';
      case 'share':
        if( $att_value == 'custom' && !empty($data['share_title']) && !empty($data['share_url']) ) {
          return $data['share_title'].';'.$data['share_url'];
        }
      case 'liststyle':
        // there was a bug which caused the Prev/Next Playlist style to save as prev/next rather than prevnext, so this code fixes the display without need to fix the database data
        if($att_value == 'prev/next' ) return 'prevnext';
    }

    return $att_value;
  }


  /**
   * Retrieves player attributes from the database
   * as opposed to getting them from the old full-text
   * shortcode format.
   *
   * @return array|mixed Returns an array with all player attributes in it.
   *                     If the player ID is not found, an empty array is returned.
   * @throws Exception When the underlying video object throws.
   */
  public function getPlayerAttsFromDb( $atts ) {
    // if we have a programatically-crafted shortcode that loads a player
    // to show a custom user playlist on the front-end, process it here
    if (isset( $atts['src'] ) && is_numeric( $atts['src'] ) && intval( $atts['src'] ) > 0 ) {
      return $this->setPlayerAttsFromNumericSrc( $atts );
    }

    global $fv_fp, $FV_Player_Db;

    $is_multi_playlist = false;

    if (isset($atts['id'])) {

      // video attributes which can still be set in shortcode
      // this makes the preview work with YouTube playlists obtained via API
      // this lets you set the splash screen for Vimeo channel
      $preserve = array();
      foreach( array('autoplay','splash','src') AS $attr2preserve ) {
        if( !empty($atts[$attr2preserve]) ) {
          $preserve[$attr2preserve] = $atts[$attr2preserve];
        }
      }

      // numeric ID means we're coming from a shortcode somewhere in a post
      if (preg_match('/[\d,]+/', $atts['id']) === 1) {
        $is_multi_playlist = (strpos($atts['id'], ',') !== false);
        $real_id = ($is_multi_playlist ? substr($atts['id'], 0, strpos($atts['id'], ',')) : $atts['id']);

        //if ( isset( $this->player_atts_cache[ $real_id ]) && empty($atts['sort']) ) {
          //return $this->player_atts_cache[ $real_id ];
        //}

        if ($this->isPlayerCached($real_id)) {
          $player = $this->getPlayersCache();
          $player = $player[$real_id];
        } else {
          $player = new FV_Player_Db_Player( $real_id, array(), $FV_Player_Db );
        }

        // even if we have multi-playlist tag, if we cannot find the first player
        // we don't continue here, since we get all attributes from the first player
        if (!$player || !$player->getIsValid()) {
          return false;
        }

        $fv_fp->currentPlayerObject = $player;

        $data = $player->getAllDataValues();

        // did we find the player?
        if ( $data ) {
          foreach ( $data AS $k => $v ) {
            $k = $this->mapDbAttributes2Shortcode( $k );
            $v = $this->mapDbAttributeValue2Shortcode( $k, $v, $data );
            if ( $v ) {
              // we omit empty values and they will get set to defaults if necessary
              $atts[ $k ] = $v;
            }
          }

          // if we have multiple players, load them here
          // and merge their videos with first player's videos
          if ($is_multi_playlist) {
            $ids = explode(',', $atts['id']);
            array_shift($ids);

            foreach ($ids as $id_player) {
              if ($this->isPlayerCached($id_player)) {
                $additional_player = $this->getPlayersCache();
                $additional_player = $additional_player[$id_player];
              } else {
                $additional_player = new FV_Player_Db_Player( $id_player, array(), $FV_Player_Db );
              }

              $additional_player->getVideos();
              $data['videos'] .= ',' . $additional_player->getVideoIds();
            }

            $player->setVideos($data['videos']);
          }
          
          // check if we should change order of videos
          $ordered_videos = explode(',', $data['videos']);
          if (!empty($atts['sort']) && in_array($atts['sort'], array('oldest', 'newest', 'reverse', 'title'))) {

            switch ($atts['sort']) {
              case 'oldest':
                $ordered_videos_tmp = array();
                sort($ordered_videos);
                foreach (  $ordered_videos as $video_index ) {
                  $ordered_videos_tmp['v'.$video_index] = $video_index;
                }

                ksort($ordered_videos_tmp);
                $ordered_videos = array_values($ordered_videos_tmp);
                break;

              case 'newest':
                $ordered_videos_tmp = array();
                sort($ordered_videos);
                $index = count($ordered_videos);
                while($index) {
                  $ordered_videos_tmp['v'.$ordered_videos[--$index]] = $ordered_videos[$index];
                }

                $ordered_videos = array_values($ordered_videos_tmp);
                break;

              case 'reverse':
                $ordered_videos = array_reverse($ordered_videos);
                break;

              case 'title':
                $ordered_videos_tmp = array();
                foreach (  $FV_Player_Db->getVideosCache() as $video ) {
                  // if this is not one of our videos, bail out
                  if (!in_array($video->getId(), $ordered_videos)) {
                    continue;
                  }

                  $title = $video->getCaption();

                  if (!$title) {
                    $title = $video->getSplashText();
                  }

                  if (!$title) {
                    $title = $video->getSrc();
                  }

                  $ordered_videos_tmp[$title] = $video->getId();
                }

                ksort($ordered_videos_tmp);
                $ordered_videos = array_values($ordered_videos_tmp);
                break;
            }
            
            $data['videos'] = implode(',', $ordered_videos);
            $player->setVideos($data['videos']);
            
            if( !empty($atts['video_objects']) ) {
              $new_objects = array();
              foreach( $ordered_videos AS $v ) {
                foreach( $atts['video_objects'] AS $i ) {
                  if( $i->getId() == $v ) {
                    $new_objects[] = $i;
                  }
                }                
              }
              $atts['video_objects'] = $new_objects;                
            }
            
          }          

          // preload all videos
          $player->getVideos();

          // add playlist / single video data
          $atts = array_merge( $atts, $this->generateFullPlaylistCode(
          // we need to prepare the same attributes array here
          // as is ingested by generateFullPlaylistCode()
          // when parsing the new playlist code on the front-end
            array(
              'playlist' => $data['videos']
            )
          ) );

        }
        
        //$this->player_atts_cache[ $real_id ] = $atts;
        
      } else {
        // when ID is not numeric, it's most probably a preview that we need to build
        $preview = $this->db_store_player_data( json_decode( stripslashes($_POST['fv_player_preview_json']), true ) );
        $atts = array_merge( $atts, $FV_Player_Db->generateFullPlaylistCode( array(),$preview ));
        $fv_fp->currentPlayerObject = $preview['player'];
      }

      if( count($preserve) > 0 ) {
        $atts = array_merge( $atts, $preserve );
      }
            
    } else {
      $fv_fp->currentPlayerObject = null;
    }

    // clear player cache with our player IDs
    // if we're coming from multi-ID shortcode,
    // otherwise we'd store player with manually updated
    // and therefore invalid video IDs
    if ($is_multi_playlist) {
      $cache = $FV_Player_Db->getPlayersCache();
      unset($cache[$player->getId()]);
      $FV_Player_Db->setPlayersCache($cache);
    }

    return $atts;
  }

  /**
   * Creates an empty default player from a shortcode like [fvplayer src="1" playlist="1;2;3"]
   * and fills its videos data from the database. Used for custom user front-end playlists.
   *
   * The SRC attribute of the above shortcode must be a numeric ID of the first video in the playlist,
   * then the playlist must follow with all of the videos to be shown (including the first one from the SRC attribute).
   *
   * @param $atts Original player attributes coming from the execution point of this method's filter.
   *
   * @return array|mixed Returns an array with all player attributes in it.
   *                     If the player ID is not found, an empty array is returned.
   * @throws Exception When the underlying video object throws.
   */
  public function setPlayerAttsFromNumericSrc( $atts ) {

    global $fv_fp, $FV_Player_Db;

    if (isset( $atts['src'] ) && is_numeric( $atts['src'] ) && intval( $atts['src'] ) > 0 ) {
      $player = new FV_Player_Db_Player( false, array(
        'playlist' => ( !empty($atts['playlist']) ? $atts['playlist'] : $atts['src'] ),
      ), $FV_Player_Db );

      // fill-in videos data from the "playlist" shortcode parameter
      $player->setVideos( str_replace( ';', ',', $atts['playlist'] ) );
      $fv_fp->currentPlayerObject = $player;
      $data = $player->getAllDataValues();

      // preload all videos
      $player->getVideos();

      // add playlist / single video data
      $atts = array_merge( $atts, $this->generateFullPlaylistCode(
      // we need to prepare the same attributes array here
      // as is ingested by generateFullPlaylistCode()
      // when parsing the new playlist code on the front-end
        array(
          'playlist' => $data['videos']
        )
      ) );
    }

    return $atts;
  }


  /**
   * Stored player data in a database from the POST data sent via AJAX
   * from the shortcode editor.
   *
   * @param array $data Alternative data to work with rather than getting these from $_POST.
   *                    Used when previews are being made.
   *
   * @return void|array Returns nothing when we're saving a new player into the DB,
   *                    otherwise returns a new unsaved player and video instances to be used as needed.
   * @throws Exception When any of the underlying objects throw.
   */
  public function db_store_player_data($data = null) {
    global $FV_Player_Db, $fv_fp;

    $player_options        = array();
    $video_ids             = array();
    
    $post_data = null;
    if( is_array($data) ) {
      $post_data = $data;
    } else if( !empty($_POST['data']) && wp_verify_nonce( $_POST['nonce'],"fv-player-preview-".get_current_user_id() ) ) {
      if( json_decode( stripslashes($_POST['data']) ) ) {
        $post_data = json_decode( stripslashes($_POST['data']), true );
      }
    }
    
    $ignored_player_fields = array(
      'fv_wp_flowplayer_field_subtitles_lang', // subtitles languages is a per-video value with global field name,
                                               // so the player should ignore it, as it will be added via video meta
      'fv_wp_flowplayer_field_popup', // never used, never shown in the UI, possibly a remnant of old code,
      'fv_wp_flowplayer_field_transcript', // transcript is a meta value, so it should not be stored globally per-player anymore
      'fv_wp_flowplayer_field_chapters', // chapters is a meta value, so it should not be stored globally per-player anymore
    );

    if ($post_data) {
      // parse and resolve deleted videos
      if (!$data && !empty($post_data['deleted_videos'])) { // todo: ajax!
        $deleted_videos = explode(',', $post_data['deleted_videos']);
        foreach ($deleted_videos as $d_id) {
          // we don't need to load this video data, just link it to a database
          // and then delete it
          // ... although we'll need at least 1 item in the data array to consider this
          //     video data valid for object creation
          $d_vid = new FV_Player_Db_Video(null, array('caption' => '1'), $this);
          $d_vid->link2db($d_id);
          $d_vid->delete();
        }
      }

      // parse and resolve deleted meta data
      if (!$data && !empty($post_data['deleted_video_meta'])) { // todo: probably not needed with Ajax saving
        $deleted_meta = explode(',', $post_data['deleted_video_meta']);
        foreach ($deleted_meta as $d_id) {
          // we don't need to load this meta data, just link it to a database
          // and then delete it
          // ... although we'll need at least 1 item in the data array to consider this
          //     meta data valid for object creation
          $d_meta = new FV_Player_Db_Video_Meta(null, array('meta_key' => '1'), $this);
          $d_meta->link2db($d_id);
          $d_meta->delete();
        }
      }

      // parse and resolve deleted meta data
      if (!$data && !empty($post_data['deleted_player_meta'])) { // todo: probably not needed with Ajax saving
        $deleted_meta = explode(',', $post_data['deleted_player_meta']);
        foreach ($deleted_meta as $d_id) {
          // we don't need to load this meta data, just link it to a database
          // and then delete it
          // ... although we'll need at least 1 item in the data array to consider this
          //     meta data valid for object creation
          $d_meta = new FV_Player_Db_Player_Meta(null, array('meta_key' => '1'), $this);
          $d_meta->link2db($d_id);
          $d_meta->delete();
        }
      }

      foreach ($post_data as $field_name => $field_value) {
        // global player or local video setting field
        if (strpos($field_name, 'fv_wp_flowplayer_field_') !== false) {
          if (!in_array($field_name, $ignored_player_fields)) {
            $option_name = str_replace( 'fv_wp_flowplayer_field_', '', $field_name );
            // global player option
            $player_options[ $option_name ] = $field_value;
          }
        } else if ($field_name == 'videos' && is_array($field_value)) {
          // iterate over all videos for the player
          foreach ($field_value as $video_index => $video_data) {
            // width and height are global options but are sent out for shortcode compatibility
            unset($video_data['fv_wp_flowplayer_field_width'], $video_data['fv_wp_flowplayer_field_height']);

            // remove global player HLS key option, as it's handled as meta data item
            // TODO: create proper API!
            unset($video_data['fv_wp_flowplayer_hlskey'], $video_data['fv_wp_flowplayer_hlskey_cryptic'], $video_data['fv_wp_flowplayer_field_encoding_job_id']);

            // strip video data of the prefix
            $new_video_data = array();
            foreach ($video_data as $key => $value) {
              if ($key === 'id') {
                $id = $value;
              } else {
                $new_video_data[ str_replace( 'fv_wp_flowplayer_field_', '', $key ) ] = $value;
              }
            }
            $video_data = $new_video_data;
            unset($new_video_data);

            // add any video meta data that we can gather
            $video_meta = array();

            if (!empty($post_data['video_meta']['video'][$video_index])) {
              foreach ($post_data['video_meta']['video'][$video_index] as $video_meta_section => $video_meta_array) {
                $meta_data_to_add = array(
                  'meta_key' => $video_meta_section,
                  'meta_value' => $video_meta_array['value']
                );

                if (isset($video_meta_array['id'])) {
                  $meta_data_to_add['id'] = (int) $video_meta_array['id'];
                }

                $video_meta[] = $meta_data_to_add;
              }
            }

            // add chapters
            if (!empty($post_data['video_meta']['chapters'][$video_index]['file']['value'])) {
              $chapters = array(
                'meta_key' =>'chapters',
                'meta_value' => $post_data['video_meta']['chapters'][$video_index]['file']['value']
              );

              if (!empty($post_data['video_meta']['chapters'][$video_index]['file']['id'])) {
                $chapters['id'] = $post_data['video_meta']['chapters'][$video_index]['file']['id'];
              }

              $video_meta[] = $chapters;
            }

            // add transcript
            if (!empty($post_data['video_meta']['transcript'][$video_index]['file']['value'])) {
              $transcript = array(
                'meta_key' =>'transcript',
                'meta_value' => $post_data['video_meta']['transcript'][$video_index]['file']['value']
              );

              if (!empty($post_data['video_meta']['transcript'][$video_index]['file']['id'])) {
                $transcript['id'] = $post_data['video_meta']['transcript'][$video_index]['file']['id'];
              }

              $video_meta[] = $transcript;
            }

            // call a filter which is server by plugins to augment
            // the $video_meta data with all the plugin data for this
            // particular video
            if (!empty($post_data['video_meta'])) {
              $video_meta = apply_filters( 'fv_player_db_video_meta_save', $video_meta, $post_data['video_meta'], $video_index);
            }

            // save the video
            $video = new FV_Player_Db_Video(null, $video_data, $this);

            // if we have video ID, link this video to DB
            if (isset($id)) {
              $video->link2db($id);
              unset($id);
            }

            // save only if we're not requesting new instances for preview purposes
            if (!$data) {
              $id_video = $video->save( $video_meta );
            } else {
              $video->link2meta( $video_meta );
            }

            // return videos as well as the full player
            if (!$data) {
              $video_ids[] = $id_video;
            } else {
              $video_ids[] = $video;
            }
          }
        }
      }

      // add all videos into this player
      if (!$data) {
        $player_options['videos'] = implode( ',', $video_ids );
      }

      // add any player meta data that we can gather
      $player_meta = array();

      if (!empty($post_data['player_meta']['player'])) {
        foreach ($post_data['player_meta']['player'] as $player_meta_section => $player_meta_array) {
          $meta_data_to_add = array(
            'meta_key' => $player_meta_section,
            'meta_value' => $player_meta_array['value']
          );

          if (isset($player_meta_array['id'])) {
            $meta_data_to_add['id'] = (int) $player_meta_array['id'];
          }

          $player_meta[] = $meta_data_to_add;
        }
      }

      // call a filter which is served by plugins to augment
      // the $player_meta data with all the plugin data for this
      // particular player
      if (!empty($post_data['player_meta'])) {
        $player_meta = apply_filters( 'fv_player_db_player_meta_save', $player_meta, $post_data['player_meta']);
      }

      // create and save the player
      $player = new FV_Player_Db_Player(null, $player_options, $FV_Player_Db);

      // if this player should have a "published" status, add it here
      if ( !empty( $post_data['status'] ) && $post_data['status'] == 'published' ) {
        $player->setStatus('published');
      }

      // save only if we're not requesting new instances for preview purposes
      if (!$data) {
        // link to DB, if we're doing an update
        if (!empty($post_data['update'])) {
          $player->link2db($post_data['update']);
        }

        $id = $player->save($player_meta);

        if ($id) {
          $output = array( 'id' => $id );
          $videos = array();
          foreach( $player->getVideos() AS $video ) {
            $videos[] = $video->getId();
          }

          do_action('fv_player_db_save', $id);

          $output = array( 'id' => $id, 'videos' => $videos );
          
          $preview_data = $fv_fp->build_min_player( false, array( 'id' => $id ) );
          $output['html'] = $preview_data['html'];
          
          echo wp_json_encode( $output );
        } else {
          echo -1;
        }
      } else {
        $player->link2meta( $player_meta );
        return array(
          'player' => $player,
          'videos' => $video_ids
        );
      }
    }

    if (!$data) {
      die();
    }
  }



  /**
   * AJAX method to return database data for the player ID given
   */
  public function open_player_for_editing() {
    global $fv_fp;

    if (isset($_POST['playerID']) && is_numeric($_POST['playerID']) && intval($_POST['playerID']) == $_POST['playerID']) {
      $out = array();
      
      if( defined('DOING_AJAX') && DOING_AJAX &&
        ( empty($_POST['nonce']) || !wp_verify_nonce( $_POST['nonce'],"fv-player-db-load-".get_current_user_id() ) )
      ) {
        die('Security check failed'); // todo: this doesn't show up for the user
      }

      // load player and its videos from DB
      if (!$this->getPlayerAttsFromDb(array( 'id' => $_POST['playerID'] ))) {
        header("HTTP/1.0 404 Not Found");
        die();
      }

      // check player's meta data for an edit lock
      $userID = get_current_user_id();
      if ($fv_fp->current_player() && count($fv_fp->current_player()->getMetaData())) {
        $edit_lock_found = false;
        foreach ($fv_fp->current_player()->getMetaData() as $meta_object) {
          $key = $meta_object->getMetaKey();
          $user_locked = str_replace('edit_lock_', '', $key);
          if ( strstr($key, 'edit_lock_') !== false ) {
            $edit_lock_found = true;

            if ( $user_locked != $userID) {
              // someone else is editing this video, first check the timestamp
              $last_tick = $meta_object->getMetaValue();
              if (time() - $last_tick > $this->edit_lock_timeout_seconds) {
                // timeout, remove lock, add lock for this user
                $meta_object->delete();

                $meta = new FV_Player_Db_Player_Meta(null, array(
                  'id_player' => $fv_fp->current_player()->getId(),
                  'meta_key' => 'edit_lock_'.$userID,
                  'meta_value' => time()
                ), $this);

                $meta->save();
              } else {
                $user = get_userdata($user_locked);
                $name = 'Somebody else';
                if( $user ) {
                  if( !empty($user->display_name) ) $name = $user->display_name;
                  if( !empty($user->user_nicename) ) $name = $user->user_nicename;
                }
                echo $name." is editing this player at the moment. Please try again later.";
                die();
              }
            } else {
              // same user, extend the lock
              $meta_object->setMetaValue(time());
              $meta_object->save();
            }
          }
        }

        // no edit lock meta record - create new one
        if (!$edit_lock_found) {
          $meta = new FV_Player_Db_Player_Meta( null, array(
            'id_player'  => $fv_fp->current_player()->getId(),
            'meta_key'   => 'edit_lock_' . $userID,
            'meta_value' => time()
          ), $this );

          $meta->save();
        }
      } else {
        // add player edit lock if none was found
        if ($fv_fp->current_player()) {
          $meta = new FV_Player_Db_Player_Meta( null, array(
            'id_player'  => $fv_fp->current_player()->getId(),
            'meta_key'   => 'edit_lock_' . $userID,
            'meta_value' => time()
          ), $this );

          $meta->save();
        }
      }

      // fill the $out variable with player data
      $out = array_merge($out, $fv_fp->current_player()->getAllDataValues());

      // load player meta data
      $meta = $fv_fp->current_player()->getMetaData();
      foreach ($meta as $meta_object) {
        if (!isset($out['meta'])) {
          $out['meta'] = array();
        }

        $out['meta'][] = $meta_object->getAllDataValues();
      }

      unset($out['video_objects'], $out['videos']);

      // fill the $out variable with video data
      $out['videos'] = array();
      foreach ($fv_fp->current_player()->getVideos() as $video) {
        // load video values
        $vid = $video->getAllDataValues();
        $vid['meta'] = array();

        // load all meta data
        $meta = $video->getMetaData();

        foreach ($meta as $meta_object) {
          $vid['meta'][] = $meta_object->getAllDataValues();
        }

        $out['videos'][] = $vid;
      }

      // load posts where this player is embedded
      $embeds_html = '';
      if( $posts = $fv_fp->current_player()->getMetaValue('post_id') ) {
        foreach( $posts AS $post_id ) {
          $embeds_html .= '<li><a href="'.get_permalink($post_id).'" target="_blank">'.get_the_title($post_id).'</a></li>';
        }
      }
      if( $embeds_html ) {
        $out['embeds'] = '<ol>'.$embeds_html.'</ol>';
      }

      $preview_data = $fv_fp->build_min_player( false, array( 'id' => $fv_fp->current_player()->getId() ) );
      $out['html'] = $preview_data['html'];

      header('Content-Type: application/json');      
      if (version_compare(phpversion(), '5.3', '<')) {
        echo json_encode($out);
      } else {        
        echo json_encode($out, true);
      }
    }

    wp_die();
  }

  /**
   * Receive Heartbeat data and checks for DB edit lock.
   * In case the lock is found and valid, it will be extended.
   *
   * @param array $response Heartbeat response data to pass back to front end.
   * @param array $data Data received from the front end (unslashed).
   *
   * @return array Returns the same response as received, as we don't need to update it or read it anywhere in JS.
   * @throws Exception When the underlying meta object throws an exception.
   */
  function check_db_edit_lock( $response, $data ) {
    global $FV_Player_Db;

    $userID = get_current_user_id();

    // extend an existing lock
    if ( !empty( $data['fv_flowplayer_edit_lock_id'] ) ) {
      $player_id = $data['fv_flowplayer_edit_lock_id'];
      
      if ($FV_Player_Db && $FV_Player_Db->isPlayerCached($player_id)) {
        $player = $FV_Player_Db->getPlayersCache();
        $player = $player[$player_id];
      } else {
        $player = new FV_Player_Db_Player($player_id, array(), $FV_Player_Db);
      }

      if ($player->getIsValid()) {
        $found = false;
        if (count($player->getMetaData())) {
          foreach ($player->getMetaData() as $meta_object) {
            if ( strstr($meta_object->getMetaKey(), 'edit_lock_') !== false ) {
              if (str_replace('edit_lock_', '', $meta_object->getMetaKey()) == $userID) {
                $found = true;
                
                // same user, extend the lock
                $meta_object->setMetaValue(time());
                $meta_object->save();
              }
            }
          }
        }
        
        if( !$found ) {
          $meta_object = new FV_Player_Db_Player_Meta(null, array(
            'id_player' => $player_id,
            'meta_key' => 'edit_lock_'.$userID,
            'meta_value' => time()
          ), $FV_Player_Db);
          $meta_object->save();
        }
      }
    }

    // remove locks that are no longer being edited
    if ( !empty( $data['fv_flowplayer_edit_lock_removal'] ) && count($data['fv_flowplayer_edit_lock_removal']) ) {
      // load meta for all players to remove locks for (and to auto-cache them as well)
      new FV_Player_Db_Player_Meta(null, array('id_player' => array_keys($data['fv_flowplayer_edit_lock_removal'])), $this);
      $meta = $this->getPlayerMetaCache();
      $locks_removed = array();

      if (count($meta)) {
        foreach ( $meta as $player ) {
          foreach ($player as $meta_object) {
            if ( strstr( $meta_object->getMetaKey(), 'edit_lock_' ) !== false ) {
              if ( str_replace( 'edit_lock_', '', $meta_object->getMetaKey() ) == $userID ) {
                // correct user, delete the lock
                $meta_object->delete();
              }

              $locks_removed[$meta_object->getIdPlayer()] = 1;
            }
          }
        }

        $response['fv_flowplayer_edit_locks_removed'] = $locks_removed;
      }
    }

    return $response;
  }

  /**
   * AJAX function to return JSON-formatted export data
   * for a specific player ID.
   *
   * Works for single player only right now!
   *
   * @param null $unused        Populated by WordPress, not used in this method.
   * @param bool $output_result If true, the export data will be returned instead of outputted.
   *                            Used when cloning a player.
   *
   * @return array Returns the actual export data in an associative array, if $output_result is false.
   * @throws Exception Thrown if one of the underlying DB classes throws an exception.
   */
  public function export_player_data($unused = null, $output_result = true, $id = false ) {
    
    if( !$id && isset($_POST['playerID']) && is_numeric($_POST['playerID']) && intval($_POST['playerID']) == $_POST['playerID'] ) {
      $id  = $_POST['playerID'];
    }
    
    if( defined('DOING_AJAX') && DOING_AJAX && 
      ( empty($_POST['nonce']) || !wp_verify_nonce( $_POST['nonce'],"fv-player-db-export-".$id ) )
    ) {
      die('Security check failed');
    }
    
    if ( $id ) {
      // first, load the player
      $player = new FV_Player_Db_Player($id, array(), $this);
      if ($player && $player->getIsValid()) {
        $export_data = $player->export();

        // load player meta data
        $meta = $player->getMetaData();
        if ($meta && count($meta)) {
          $export_data['meta'] = array();

          foreach ($meta as $meta_data) {
            // don't include edit locks
            if ( strstr($meta_data->getMetaKey(), 'edit_lock_') === false ) {
              $export_data['meta'][] = $meta_data->export();
            }
          }
        }

        // load videos and meta for this player
        $videos = $player->getVideos();

        // this line will load and cache meta for all videos at once
        new FV_Player_Db_Video_Meta(null, array('id_video' => explode(',', $player->getVideoIds())), $this);

        if ($videos && count($videos)) {
          $export_data['videos'] = array();

          foreach ($videos as $video) {
            $video_export_data = $video->export();

            // load all meta data for this video
            if ($this->isVideoMetaCached($video->getId())) {
              $video_export_data['meta'] = array();

              foreach ($this->video_meta_cache[$video->getId()] as $meta) {
                $video_export_data['meta'][] = $meta->export();
              }
            }

            $export_data['videos'][] = $video_export_data;
          }
        }
      } else {
        if ($output_result) {
          die( 'invalid player ID, export unsuccessful - please use the close button and try again' );
        } else {
          return false;
        }
      }

      if ($output_result) {
        if (version_compare(phpversion(), '5.3', '<')) {
          echo json_encode($export_data);
        } else {        
          echo json_encode($export_data, true);
        }
        exit;
      } else {
        return $export_data;
      }
    } else {
      if ($output_result) {
        die( 'invalid player ID, export unsuccessful - please use the close button and try again' );
      } else {
        return false;
      }
    }
  }

  /**
   * AJAX function to import JSON-formatted export data.
   *
   * Works for single player only right now!
   *
   * @param null $unused        Populated by WordPress, not used in this method.
   * @param bool $output_result If true, the import result will be returned instead of outputted.
   *                            Used when cloning a player.
   * @param array|null $alternative_data If set, this is an alternative source of data to import.
   *                                     Used when cloning a player. This also skips the nonce check!
   *
   * @return string Returns the actual player ID, if $output_result is false.
   *
   * @throws Exception Thrown if one of the underlying DB classes throws an exception.
   */
  public function import_player_data($unused = null, $output_result = true, $alternative_data = null) {
    global $FV_Player_Db;

    if (($alternative_data !== null && $data = $alternative_data) || (isset($_POST['data']) && $data = json_decode(stripslashes($_POST['data']), true))) {
      if( defined('DOING_AJAX') && DOING_AJAX && !$alternative_data &&
        ( empty($_POST['nonce']) || !wp_verify_nonce( $_POST['nonce'],"fv-player-db-import-".get_current_user_id() ) )
      ) {
        die('Security check failed');
      }      
      
      try {
        // first, create the player
        $player_keys = $data;
        unset($player_keys['meta'], $player_keys['videos']);
        
        foreach( $player_keys AS $k => $v ) {
          if( stripos($k,'fv_wp_flowplayer_field_') === 0 ) {
            $new = str_replace( 'fv_wp_flowplayer_field_', '', $k );
            $player_keys[$new] = $v;
            unset($player_keys[$k]);
          }
        }

        $player = new FV_Player_Db_Player(null, $player_keys, $FV_Player_Db);
        $player_video_ids = array();

        // create player videos, along with meta data
        // ... don't save the player yet, as we need all video IDs to be known
        //     before doing so
        if (isset($data['videos'])) {
          foreach ($data['videos'] as $video_data) {
            
            foreach( $video_data AS $k => $v ) {
              if( stripos($k,'fv_wp_flowplayer_field_') === 0 ) {
                $new = str_replace( 'fv_wp_flowplayer_field_', '', $k );
                $video_data[$new] = $v;
                unset($video_data[$k]);
              }
            }
            
            $video_object = new FV_Player_Db_Video(null, $video_data, $FV_Player_Db);
            $id_video = $video_object->save();

            // add all meta data for this video
            if (isset($video_data['meta'])) {
              foreach ($video_data['meta'] as $video_meta_data) {
                $video_meta_object = new FV_Player_Db_Video_Meta(null, $video_meta_data, $FV_Player_Db);
                $video_meta_object->link2db($id_video, true);
                $video_meta_object->save();
              }
            }

            $player_video_ids[] = $id_video;
          }
        }

        // set video IDs for the player
        $player->setVideos(implode(',', $player_video_ids));

        // save player
        $id_player = $player->save();

        // create player meta, if any
        if (isset($data['meta'])) {
          foreach ($data['meta'] as $meta_data) {
            $meta_object = new FV_Player_Db_Player_Meta(null, $meta_data, $FV_Player_Db);
            $meta_object->link2db($id_player, true);
            $meta_object->save();
          }
        }

      } catch (Exception $e) {
        if (WP_DEBUG) {
          var_dump($e);

          if (!$output_result) {
            exit;
          }
        }

        die( '0' );
      }

      if ($output_result) {
        die( (string) $id_player );
      } else {
        return (string) $id_player;
      }
    } else {
      if ($output_result) {
        die('No valid import data found, import unsuccessful');
      } else {
        return 'No valid import data found, import unsuccessful';
      }
    }
  }

  /**
   * AJAX function to remove a player from database.
   *
   * @throws Exception Thrown if one of the underlying DB classes throws an exception.
   */
  public function remove_player() {
    if (isset($_POST['playerID']) && is_numeric($_POST['playerID']) && intval($_POST['playerID']) == $_POST['playerID']) {
      if( defined('DOING_AJAX') && DOING_AJAX && 
        ( empty($_POST['nonce']) || !wp_verify_nonce( $_POST['nonce'],"fv-player-db-remove-".$_POST['playerID'] ) )
      ) {
        die('Security check failed');
      }
      
      // first, load the player
      $player = new FV_Player_Db_Player($_POST['playerID'], array(), $this);
      if ($player && $player->getIsValid()) {
        // remove the player
        if ($player->delete()) {
          echo 1;
          exit;
        } else {
          die( 'Could not remove player' );
        }
      } else {
        die( 'Invalid player ID' );
      }
    } else {
      die( 'Invalid player ID' );
    }
  }

  /**
   * AJAX function to clone a player in the database.
   *
   * Works for single player only right now!
   *
   * @throws Exception Thrown if one of the underlying DB classes throws an exception.
   */
  public function clone_player() {
    if (isset($_POST['playerID']) && is_numeric($_POST['playerID'])) {
      $export_data = $this->export_player_data(null, false);

      // do not clone information about where the player is embeded
      if (isset($export_data['meta'])) {
        foreach($export_data['meta'] as $h => $v){
          if($v['meta_key'] == 'post_id'){
            unset($export_data['meta'][$h]);
          }
        }
      }

      echo $this->import_player_data(null, false, $export_data);
      exit;
    } else {
      die('no valid player ID found, cloning unsuccessful');
    }
  }

  /**
   * AJAX method to retrieve video caption, splash screen and duration.
   * Also returns current timestamp, so we can store the last check date in DB.
   */
  public function retrieve_video_data() {
    if (!isset($_POST['video_url'])) {
      exit;
    }
    
    $url = $_POST['video_url'];

    $json_data = apply_filters('fv_player_meta_data', $url, false);
    if ($json_data !== false && is_array($json_data) ) {
      header('Content-Type: application/json');
      $json_data['ts'] = time();
      die(json_encode($json_data));
    }

    // add last update timestamp & duration
    $json_data = array(
      'ts' => time()
    );

    // was only the file path provided?
    $parsed = parse_url($url);
    if( count($parsed) == 1 && !empty($parsed['path']) ) {
      // then user the WordPress home URL
      $url = home_url($url);
      // but remove the "path" if WordPress runs in a folder
      $wordpress_home = parse_url(home_url());
      if( !empty($wordpress_home['path']) ) {
        $url = str_replace( $wordpress_home['path'], '', $url );
      }
    }

    // only run the actual check for real URLs
    if( filter_var($url, FILTER_VALIDATE_URL) ) {
      // add duration
      global $FV_Player_Checker, $fv_fp;
      if( $secured_url = $fv_fp->get_video_src( $url, array( 'dynamic' => true ) ) ) {
        $url = $secured_url;
      }
      
      $json_data['duration'] = $FV_Player_Checker->check_mimetype(array($url), false, true);
      $json_data['duration'] = $json_data['duration']['duration'];
    }

    header('Content-Type: application/json');
    die(json_encode($json_data));
  }

  /**
   * AJAX method to retrieve IDs and names of all players to be populated
   * into a dropdown in the front-end.
   */
  public function retrieve_all_players_for_dropdown() {
    $players = $this->getListPageData('date_created', 'desc', null, null);
    $json_data = array();

    foreach ($players as $player) {
      $json_data[] = array(
        'id' => $player->id,
        'name' => '#' . $player->id . ' ' . $player->player_name
      );
    }

    header('Content-Type: application/json');
    die(json_encode($json_data));
  }

  /**
   * Runs on save_post hook and it stored the post ID in player meta. It also checks any player meta which is pointing to this post and if it's no longer found in it the meta is removed.
   *
   * @param int $post_id        Populated by WordPress, the post ID
   */  
  public function store_post_ids( $post_id ) {
    if ( wp_is_post_revision( $post_id ) ) return;
    
    $post = get_post($post_id);
    
    $matches = array();
    if( preg_match_all('~\[fvplayer.*?id=[\'"]([0-9,]+)[\'"].*?\]~', $post->post_content, $matches1 ) ) {
      $matches = array_merge( $matches, $matches1[1] );
    }

    // The [fvplayer] shortcode might be stored in plain form, or with the quotes escaped like fvplayer id=\"56\"]
    if( preg_match_all('~\[fvplayer.*?id=\\\?[\'"]([0-9,]+)~', implode( array_map( 'implode', get_post_custom($post_id) ) ), $matches2 ) ) {
      $matches = array_merge( $matches, $matches2[1] );
    }
    
    if( $matches ) {
      
      $ids = array();
      foreach( $matches AS $match ) {
        foreach( explode(',',$match) AS $match_match ) {
          $ids[] = $match_match;
        }
      }
      
      $ids = array_unique($ids);
      foreach( $ids AS $player_id ) {
        
        $player = new FV_Player_Db_Player($player_id);
        if( $player->getIsValid() ) {
          
          $add = true;
          // TODO: This seems to not work when saving with Elementor, it seems store_post_ids() runs 3 times
          // but it's never aware of the player meta added using FV_Player_Db_Player_Meta in the previous run
          $metas = $player->getMetaData();
          if( count($metas) ) {
            foreach( $metas as $meta_object ) {
              if( $meta_object->getMetaKey() == 'post_id' ) {
                if( $meta_object->getMetaValue() == $post_id ) {
                  $add = false;
                }
              }
            }
          }

          // TODO: So here's the temporary work-around which should be removed once FV_Player_Db_Player_Meta()
          // does properly register the player meta with getMetaData()
          global $wpdb;
          if( $wpdb->get_var( $wpdb->prepare("SELECT meta_value FROM {$wpdb->prefix}fv_player_playermeta WHERE id_player = %d AND meta_key = %s AND meta_value = %d", $player_id, 'post_id', $post_id ) ) ) {
            $add = false;
          }
          
          if( $add ) {
            $meta = new FV_Player_Db_Player_Meta(null, array(
              'id_player' => $player_id,
              'meta_key' => 'post_id',
              'meta_value' => $post_id
            ) );

            $meta->save();
          }
        }

      }
      
      global $wpdb;
      $remove = $wpdb->get_results( "SELECT * FROM ".FV_Player_Db_Player_Meta::init_db_name()." WHERE meta_key = 'post_id' AND meta_value = '{$post_id}' ");
      if( $remove ) {
        foreach( $remove AS $removal ) {
          if( !in_array($removal->id_player,$ids) ) {
            $d_meta = new FV_Player_Db_Player_Meta($removal->id);
            $d_meta->link2db( $removal->id );
            $d_meta->delete();
          }
        }
      }
      
    }
  }

  /**
   * Retrieves a video instance where the SRC field is set
   * to the $src variable given.
   *
   * @param $src string The video SRC to search for in the database.
   */
  public function get_video_by_src( $src ) {
    global $wpdb;

    $row = $wpdb->get_row( '
          SELECT
            id
          FROM
            ' . FV_Player_Db_Video::get_db_table_name() . '
          WHERE
            src = "' . esc_sql( $src ) . '"'
    );

    if ( $row ) {
      return new FV_Player_Db_Video( $row->id );
    } else {
      return null;
    }
  }

  public static function get_player_duration( $id ) {
    global $wpdb;
    return $wpdb->get_var( "SELECT sum(vm.meta_value) FROM {$wpdb->prefix}fv_player_videometa AS vm JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) WHERE p.id = ".intval($id)." AND vm.meta_key = 'duration'" );
    
  }

}