diff --git a/lib/Caching/VideosCaching.php b/lib/Caching/VideosCaching.php new file mode 100644 index 00000000..266fbac6 --- /dev/null +++ b/lib/Caching/VideosCaching.php @@ -0,0 +1,137 @@ +cache_factory = \StudipCacheFactory::getCache(); + } + + public function userVideos(string $user_id) + { + $this->cache_name = self::OC_CACHE_KEY_DOMAIN_USERS . $user_id; + return $this; + } + + public function courseVideos(string $course_id) + { + $this->cache_name = self::OC_CACHE_KEY_DOMAIN_COURSES . $course_id; + return $this; + } + + public function playlistVideos(int $playlist_id) + { + $this->cache_name = self::OC_CACHE_KEY_DOMAIN_PLAYLIST . $playlist_id; + return $this; + } + + public function readAll() + { + if (empty($this->cache_name)) { + throw new \Error('Unable to read the cache due to missing cache name!'); + } + + $content = $this->cache_factory->read($this->cache_name); + return $content ? unserialize($content) : []; + } + + public function read(string $unique_query_id) + { + if (empty($unique_query_id)) { + throw new \Error('Unable to read the cache due to missing cache name!'); + } + + $all = $this->readAll(); + + if (!isset($all[$unique_query_id])) { + return false; + } + + return $all[$unique_query_id]; + } + + public function write($unique_query_id, $content) + { + if (empty($unique_query_id)) { + throw new \Error('Unable to write the cache data due to missing cache name!'); + } + + $all = $this->readAll(); + + $all[$unique_query_id] = $content; + + $serialized_records = serialize($all); + + return $this->cache_factory->write($this->cache_name, $serialized_records); + } + + public function delete($unique_query_id) + { + if (empty($unique_query_id)) { + throw new \Error('Unable to expire the cache data due to missing cache name!'); + } + + $all = $this->readAll(); + + if (empty($all)) { + return true; + } + + if (isset($all[$unique_query_id])) { + unset($all[$unique_query_id]); + } + + if (empty($all)) { + $this->expire(); + return true; + } + + return $this->cache_factory->write($this->cache_name, serialize($all)); + } + + public function expire() + { + $this->cache_factory->expire($this->cache_name); + } + + public static function expireAllVideoCaches(Videos $video) + { + $cache = \StudipCacheFactory::getCache(); + if (!empty($video->perms)) { + foreach ($video->perms->pluck('user_id') as $user_id) { + $cache->expire(self::OC_CACHE_KEY_DOMAIN_USERS . $user_id); + } + } + + if (!empty($video->playlists)) { + foreach ($video->playlists as $playlist) { + $cache->expire(self::OC_CACHE_KEY_DOMAIN_PLAYLIST . $playlist->id); + + if (!empty($playlist->courses)) { + foreach ($playlist->courses as $course) { + $cache->expire(self::OC_CACHE_KEY_DOMAIN_COURSES . $course->id); + } + } + } + } + + // We need to also look for root accounts! + $stmt = \DBManager::get()->prepare($q = "SELECT `user_id` FROM `auth_user_md5` WHERE `perms` = :perm"); + $stmt->execute([ + ':perm' => 'root', + ]); + $root_user_ids = $stmt->fetchAll(\PDO::FETCH_COLUMN); + foreach ($root_user_ids as $root_user_id) { + $cache->expire(self::OC_CACHE_KEY_DOMAIN_USERS . $root_user_id); + } + } +} diff --git a/lib/Models/Filter.php b/lib/Models/Filter.php index 9563ce70..ad063553 100644 --- a/lib/Models/Filter.php +++ b/lib/Models/Filter.php @@ -106,4 +106,9 @@ public function getTrashed() { return $this->trashed; } + + public function decodeVars() + { + return base64_encode(json_encode(get_object_vars($this))); + } } diff --git a/lib/Models/Playlists.php b/lib/Models/Playlists.php index e42ba791..98b11716 100644 --- a/lib/Models/Playlists.php +++ b/lib/Models/Playlists.php @@ -5,6 +5,7 @@ use Opencast\Models\REST\ApiPlaylistsClient; use Opencast\Helpers\PlaylistMigration; use Opencast\Errors\Error; +use Opencast\Caching\VideosCaching; class Playlists extends UPMap { @@ -575,6 +576,8 @@ public function delete() $playlist_client->deletePlaylist($this->service_playlist_id); } + (new VideosCaching())->playlistVideos($this->id)->expire(); + return parent::delete(); } diff --git a/lib/Models/ScheduledRecordings.php b/lib/Models/ScheduledRecordings.php index 2d047377..05a89be3 100644 --- a/lib/Models/ScheduledRecordings.php +++ b/lib/Models/ScheduledRecordings.php @@ -3,7 +3,7 @@ namespace Opencast\Models; use Opencast\Models\PlaylistSeminars; -use Opencast\Models\Video; +use Opencast\Models\Videos; class ScheduledRecordings extends \SimpleORMap { diff --git a/lib/Models/Videos.php b/lib/Models/Videos.php index ec553bcf..2723bf65 100644 --- a/lib/Models/Videos.php +++ b/lib/Models/Videos.php @@ -10,6 +10,7 @@ use Opencast\Models\REST\ApiWorkflowsClient; use Opencast\Models\Helpers; use Opencast\Models\ScheduledRecordings; +use Opencast\Caching\VideosCaching; class Videos extends UPMap { @@ -297,9 +298,14 @@ protected static function getFilteredVideos($query, $filters) $course_ids = []; $lecturer_ids = []; + $count_selected_columns = [ + 'id', 'token', 'config_id', 'created' + ]; + foreach ($filters->getFilters() as $filter) { switch ($filter['type']) { case 'text': + $count_selected_columns[] = 'title'; $pname = ':text' . sizeof($params); $where .= " AND (title LIKE $pname OR description LIKE $pname)"; $params[$pname] = '%' . $filter['value'] .'%'; @@ -450,7 +456,11 @@ protected static function getFilteredVideos($query, $filters) $sql .= ' GROUP BY oc_video.id'; - $stmt = \DBManager::get()->prepare($s = "SELECT COUNT(*) FROM (SELECT oc_video.* FROM oc_video $sql) t"); + // Preparing count sql, specifically define columns to select in order to increase performance. + $count_selected_columns = array_map(function($clmn) { return 'oc_video.' . $clmn; }, array_unique($count_selected_columns)); + $count_select_columns_sql = implode(', ', $count_selected_columns); + $s = "SELECT COUNT(*) FROM (SELECT $count_select_columns_sql FROM oc_video $sql) t"; + $stmt = \DBManager::get()->prepare($s); $stmt->execute($params); $count = $stmt->fetchColumn(); @@ -1273,4 +1283,26 @@ public static function addToCoursePlaylist($episode, $video) } } } + + /** + * @inheritDoc + * + * Overriding store method, in order to expire caches. + */ + public function store() + { + VideosCaching::expireAllVideoCaches($this); + return parent::store(); + } + + /** + * @inheritDoc + * + * Overriding delete method, in order to expire caches. + */ + public function delete() + { + VideosCaching::expireAllVideoCaches($this); + return parent::delete(); + } } diff --git a/lib/Routes/Course/CourseVideoList.php b/lib/Routes/Course/CourseVideoList.php index 17a5794f..86d722fa 100644 --- a/lib/Routes/Course/CourseVideoList.php +++ b/lib/Routes/Course/CourseVideoList.php @@ -8,6 +8,7 @@ use Opencast\OpencastController; use Opencast\Models\Filter; use Opencast\Models\Videos; +use Opencast\Caching\VideosCaching; class CourseVideoList extends OpencastController { @@ -29,22 +30,38 @@ public function __invoke(Request $request, Response $response, $args) throw new \AccessDeniedException(); } - // show videos for this course and filter them with optional additional filters - $videos = Videos::getCourseVideos($course_id, new Filter($params)); + $response_result = [ + 'videos' => [], + 'count' => 0, + ]; - $ret = []; - foreach ($videos['videos'] as $video) { - $video_array = $video->toSanitizedArray($params['cid']); - if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write')) - { - $video_array['perms'] = $video->perms->toSanitizedArray(); + $filter = new Filter($params); + $video_caching = new VideosCaching(); + $course_videos_cache = $video_caching->courseVideos($course_id); + $unique_query_id = $filter->decodeVars(); + $response_result = $course_videos_cache->read($unique_query_id); + if (empty($response_result)) { + // show videos for this course and filter them with optional additional filters + $videos = Videos::getCourseVideos($course_id, $filter); + + $ret = []; + foreach ($videos['videos'] as $video) { + $video_array = $video->toSanitizedArray($params['cid']); + if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write')) + { + $video_array['perms'] = $video->perms->toSanitizedArray(); + } + $ret[] = $video_array; } - $ret[] = $video_array; + + $response_result = [ + 'videos' => $ret, + 'count' => $videos['count'], + ]; + + $course_videos_cache->write($unique_query_id, $response_result); } - return $this->createResponse([ - 'videos' => $ret, - 'count' => $videos['count'], - ], $response); + return $this->createResponse($response_result, $response); } } diff --git a/lib/Routes/Playlist/PlaylistVideoList.php b/lib/Routes/Playlist/PlaylistVideoList.php index 112b33db..9a1d2a1c 100644 --- a/lib/Routes/Playlist/PlaylistVideoList.php +++ b/lib/Routes/Playlist/PlaylistVideoList.php @@ -12,6 +12,7 @@ use Opencast\Models\Playlists; use Opencast\Models\Videos; use Opencast\Models\PlaylistSeminarVideos; +use Opencast\Caching\VideosCaching; class PlaylistVideoList extends OpencastController { @@ -49,22 +50,38 @@ public function __invoke(Request $request, Response $response, $args) } } - // show videos for this playlist and filter them with optional additional filters - $videos = Videos::getPlaylistVideos($playlist->id, new Filter($params)); + $response_result = [ + 'videos' => [], + 'count' => 0, + ]; - $ret = []; - foreach ($videos['videos'] as $video) { - $video_array = $video->toSanitizedArray($course_id, $playlist->id); - if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write')) - { - $video_array['perms'] = $video->perms->toSanitizedArray(); + $filter = new Filter($params); + $video_caching = new VideosCaching(); + $playlist_videos_cache = $video_caching->playlistVideos($playlist->id); + $unique_query_id = $filter->decodeVars(); + $response_result = $playlist_videos_cache->read($unique_query_id); + if (empty($response_result)) { + // show videos for this playlist and filter them with optional additional filters + $videos = Videos::getPlaylistVideos($playlist->id, $filter); + + $ret = []; + foreach ($videos['videos'] as $video) { + $video_array = $video->toSanitizedArray($course_id, $playlist->id); + if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write')) + { + $video_array['perms'] = $video->perms->toSanitizedArray(); + } + $ret[] = $video_array; } - $ret[] = $video_array; + + $response_result = [ + 'videos' => $ret, + 'count' => $videos['count'], + ]; + + $playlist_videos_cache->write($unique_query_id, $response_result); } - return $this->createResponse([ - 'videos' => $ret, - 'count' => $videos['count'] - ], $response); + return $this->createResponse($response_result, $response); } } diff --git a/lib/Routes/Video/VideoList.php b/lib/Routes/Video/VideoList.php index 3f74438e..6cb6d42a 100644 --- a/lib/Routes/Video/VideoList.php +++ b/lib/Routes/Video/VideoList.php @@ -8,6 +8,7 @@ use Opencast\OpencastController; use Opencast\Models\Filter; use Opencast\Models\Videos; +use Opencast\Caching\VideosCaching; class VideoList extends OpencastController { @@ -15,24 +16,41 @@ class VideoList extends OpencastController public function __invoke(Request $request, Response $response, $args) { + global $user; $params = $request->getQueryParams(); - // select all videos the current user has perms on - $videos = Videos::getUserVideos(new Filter($params)); + $response_result = [ + 'videos' => [], + 'count' => 0, + ]; - $ret = []; - foreach ($videos['videos'] as $video) { - $video_array = $video->toSanitizedArray(); - if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write')) - { - $video_array['perms'] = $video->perms->toSanitizedArray(); + $filter = new Filter($params); + $video_caching = new VideosCaching(); + $user_videos_cache = $video_caching->userVideos($user->id); + $unique_query_id = $filter->decodeVars(); + $response_result = $user_videos_cache->read($unique_query_id); + if (empty($response_result)) { + // select all videos the current user has perms on + $videos = Videos::getUserVideos($filter); + + $ret = []; + foreach ($videos['videos'] as $video) { + $video_array = $video->toSanitizedArray(); + if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write')) + { + $video_array['perms'] = $video->perms->toSanitizedArray(); + } + $ret[] = $video_array; } - $ret[] = $video_array; + + $response_result = [ + 'videos' => $ret, + 'count' => $videos['count'], + ]; + + $user_videos_cache->write($unique_query_id, $response_result); } - return $this->createResponse([ - 'videos' => $ret, - 'count' => $videos['count'], - ], $response); + return $this->createResponse($response_result, $response); } } diff --git a/migrations/113_tables_optimizations.php b/migrations/113_tables_optimizations.php new file mode 100644 index 00000000..6a615146 --- /dev/null +++ b/migrations/113_tables_optimizations.php @@ -0,0 +1,74 @@ + [ + 'name' => 'video_filtering', + 'cols' => ['trashed', 'token', 'config_id', 'created'], + ], + 'oc_video_user_perms' => [ + 'name' => 'video_user_perms_video_id', + 'cols' => ['video_id'], + ], + 'oc_config' => [ + 'name' => 'config_id_active', + 'cols' => ['id', 'active'], + ], + 'oc_video_tags' => [ + 'name' => 'tag_video', + 'cols' => ['video_id', 'tag_id'], + ], + // Since this table has foreign key related to the index, dropping the index in down() would throw error! + 'oc_playlist_video' => [ + 'name' => 'video_playlist', + 'cols' => ['video_id', 'playlist_id'], + ], + // Since this table has foreign key related to the index, dropping the index in down() would throw error! + 'oc_playlist_seminar' => [ + 'name' => 'seminar_playlist', + 'cols' => ['seminar_id', 'playlist_id'], + ], + ]; + + public function description() + { + return 'This migration contains various optimizations and indexes related to fetching videos from database'; + } + + public function up() + { + $db = DBManager::get(); + foreach ($this->table_indexes_mapping as $table_name => $index_data) { + $index_name = self::INDEX_PREFIX . $index_data['name']; + if ($this->keyExists($table_name, $index_name)) { + continue; + } + + $cols = array_map(function($col) { return "`{$col}`"; }, array_unique($index_data['cols'])); + $cols_str = implode(', ', $cols); + + $query = "CREATE INDEX `$index_name` ON `$table_name` ($cols_str)"; + $db->exec($query); + } + } + + public function down() + { + $db = DBManager::get(); + $db->exec("SET foreign_key_checks = 0"); + foreach ($this->table_indexes_mapping as $table_name => $index_data) { + $index_name = self::INDEX_PREFIX . $index_data['name']; + if (!$this->keyExists($table_name, $index_name)) { + continue; + } + $query = "ALTER TABLE `$table_name` DROP INDEX `$index_name`"; + $db->exec($query); + } + $db->exec("SET foreign_key_checks = 1"); + } +}