mirror of https://github.com/sualko/cloud_bbb
feat: manage recordings
- list recordings - delete recording - store url to player as url file fix #19pull/36/head
parent
6005928e78
commit
b437d7c33d
|
@ -2,9 +2,11 @@
|
||||||
return [
|
return [
|
||||||
'resources' => [
|
'resources' => [
|
||||||
'room' => ['url' => '/rooms'],
|
'room' => ['url' => '/rooms'],
|
||||||
'room_api' => ['url' => '/api/0.1/rooms']
|
'room_api' => ['url' => '/api/0.1/rooms'],
|
||||||
],
|
],
|
||||||
'routes' => [
|
'routes' => [
|
||||||
|
['name' => 'server#records', 'url' => '/server/{roomUid}/records', 'verb' => 'GET'],
|
||||||
|
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
||||||
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
|
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
|
||||||
['name' => 'room_api#preflighted_cors', 'url' => '/api/0.1/{path}',
|
['name' => 'room_api#preflighted_cors', 'url' => '/api/0.1/{path}',
|
||||||
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]
|
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]
|
||||||
|
|
|
@ -12,12 +12,12 @@
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sualko/bigbluebutton-api-php.git",
|
"url": "https://github.com/sualko/bigbluebutton-api-php.git",
|
||||||
"reference": "75230993ab7714ef27b7177486dd4c99146b3bf7"
|
"reference": "3b8da2e1b9469deebe2713bb679e9c88e258ec9b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sualko/bigbluebutton-api-php/zipball/75230993ab7714ef27b7177486dd4c99146b3bf7",
|
"url": "https://api.github.com/repos/sualko/bigbluebutton-api-php/zipball/3b8da2e1b9469deebe2713bb679e9c88e258ec9b",
|
||||||
"reference": "75230993ab7714ef27b7177486dd4c99146b3bf7",
|
"reference": "3b8da2e1b9469deebe2713bb679e9c88e258ec9b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/sualko/bigbluebutton-api-php/tree/fork"
|
"source": "https://github.com/sualko/bigbluebutton-api-php/tree/fork"
|
||||||
},
|
},
|
||||||
"time": "2020-04-27T14:32:50+00:00"
|
"time": "2020-05-17T08:37:02+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
@ -1964,7 +1964,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/console.git",
|
"url": "https://github.com/symfony/console.git",
|
||||||
|
@ -2054,7 +2054,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher",
|
"name": "symfony/event-dispatcher",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||||
|
@ -2196,16 +2196,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/filesystem",
|
"name": "symfony/filesystem",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/filesystem.git",
|
"url": "https://github.com/symfony/filesystem.git",
|
||||||
"reference": "ca3b87dd09fff9b771731637f5379965fbfab420"
|
"reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ca3b87dd09fff9b771731637f5379965fbfab420",
|
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7cd0dafc4353a0f62e307df90b48466379c8cc91",
|
||||||
"reference": "ca3b87dd09fff9b771731637f5379965fbfab420",
|
"reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2256,11 +2256,11 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-03-27T16:56:45+00:00"
|
"time": "2020-04-12T14:40:17+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/finder.git",
|
"url": "https://github.com/symfony/finder.git",
|
||||||
|
@ -2323,16 +2323,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/options-resolver",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/options-resolver.git",
|
"url": "https://github.com/symfony/options-resolver.git",
|
||||||
"reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d"
|
"reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/09dccfffd24b311df7f184aa80ee7b61ad61ed8d",
|
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/3707e3caeff2b797c0bfaadd5eba723dd44e6bf1",
|
||||||
"reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d",
|
"reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2387,20 +2387,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-03-27T16:56:45+00:00"
|
"time": "2020-04-06T10:40:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.15.0",
|
"version": "v1.17.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||||
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14"
|
"reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
|
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
|
||||||
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
|
"reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2412,7 +2412,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.15-dev"
|
"dev-master": "1.17-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -2459,20 +2459,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-02-27T09:26:54+00:00"
|
"time": "2020-05-12T16:14:59+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-mbstring",
|
"name": "symfony/polyfill-mbstring",
|
||||||
"version": "v1.15.0",
|
"version": "v1.17.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
|
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||||
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
|
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2484,7 +2484,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.15-dev"
|
"dev-master": "1.17-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -2532,20 +2532,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-03-09T19:04:49+00:00"
|
"time": "2020-05-12T16:47:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php70",
|
"name": "symfony/polyfill-php70",
|
||||||
"version": "v1.15.0",
|
"version": "v1.17.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php70.git",
|
"url": "https://github.com/symfony/polyfill-php70.git",
|
||||||
"reference": "2a18e37a489803559284416df58c71ccebe50bf0"
|
"reference": "82225c2d7d23d7e70515496d249c0152679b468e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0",
|
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/82225c2d7d23d7e70515496d249c0152679b468e",
|
||||||
"reference": "2a18e37a489803559284416df58c71ccebe50bf0",
|
"reference": "82225c2d7d23d7e70515496d249c0152679b468e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2555,7 +2555,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.15-dev"
|
"dev-master": "1.17-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -2591,20 +2591,34 @@
|
||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"time": "2020-02-27T09:26:54+00:00"
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2020-05-12T16:47:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php72",
|
"name": "symfony/polyfill-php72",
|
||||||
"version": "v1.15.0",
|
"version": "v1.17.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php72.git",
|
"url": "https://github.com/symfony/polyfill-php72.git",
|
||||||
"reference": "37b0976c78b94856543260ce09b460a7bc852747"
|
"reference": "f048e612a3905f34931127360bdd2def19a5e582"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
|
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582",
|
||||||
"reference": "37b0976c78b94856543260ce09b460a7bc852747",
|
"reference": "f048e612a3905f34931127360bdd2def19a5e582",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2613,7 +2627,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.15-dev"
|
"dev-master": "1.17-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -2660,20 +2674,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-02-27T09:26:54+00:00"
|
"time": "2020-05-12T16:47:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php73",
|
"name": "symfony/polyfill-php73",
|
||||||
"version": "v1.15.0",
|
"version": "v1.17.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php73.git",
|
"url": "https://github.com/symfony/polyfill-php73.git",
|
||||||
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7"
|
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
|
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||||
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
|
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2682,7 +2696,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.15-dev"
|
"dev-master": "1.17-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -2732,20 +2746,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-02-27T09:26:54+00:00"
|
"time": "2020-05-12T16:47:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/process",
|
"name": "symfony/process",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/process.git",
|
"url": "https://github.com/symfony/process.git",
|
||||||
"reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e"
|
"reference": "3179f68dff5bad14d38c4114a1dab98030801fd7"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
|
"url": "https://api.github.com/repos/symfony/process/zipball/3179f68dff5bad14d38c4114a1dab98030801fd7",
|
||||||
"reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
|
"reference": "3179f68dff5bad14d38c4114a1dab98030801fd7",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -2795,7 +2809,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-03-27T16:56:45+00:00"
|
"time": "2020-04-15T15:59:10+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/service-contracts",
|
"name": "symfony/service-contracts",
|
||||||
|
@ -2857,7 +2871,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/stopwatch",
|
"name": "symfony/stopwatch",
|
||||||
"version": "v5.0.7",
|
"version": "v5.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/stopwatch.git",
|
"url": "https://github.com/symfony/stopwatch.git",
|
||||||
|
|
|
@ -5,6 +5,9 @@ namespace OCA\BigBlueButton\BigBlueButton;
|
||||||
use BigBlueButton\BigBlueButton;
|
use BigBlueButton\BigBlueButton;
|
||||||
use BigBlueButton\Parameters\CreateMeetingParameters;
|
use BigBlueButton\Parameters\CreateMeetingParameters;
|
||||||
use BigBlueButton\Parameters\JoinMeetingParameters;
|
use BigBlueButton\Parameters\JoinMeetingParameters;
|
||||||
|
use BigBlueButton\Parameters\GetRecordingsParameters;
|
||||||
|
use BigBlueButton\Core\Record;
|
||||||
|
use BigBlueButton\Parameters\DeleteRecordingsParameters;
|
||||||
use OCA\BigBlueButton\Db\Room;
|
use OCA\BigBlueButton\Db\Room;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
|
@ -112,4 +115,69 @@ class API
|
||||||
|
|
||||||
return $createMeetingParams;
|
return $createMeetingParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRecording(string $recordId)
|
||||||
|
{
|
||||||
|
$recordingParams = new GetRecordingsParameters();
|
||||||
|
$recordingParams->setRecordId($recordId);
|
||||||
|
$recordingParams->setState('any');
|
||||||
|
|
||||||
|
$response = $this->getServer()->getRecordings($recordingParams);
|
||||||
|
|
||||||
|
if (!$response->success()) {
|
||||||
|
throw new \Exception('Could not process get recording request');
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = $response->getRecords();
|
||||||
|
|
||||||
|
if (count($records) === 0) {
|
||||||
|
throw new \Exception('Found no record with given id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->recordToArray($records[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecordings(Room $room)
|
||||||
|
{
|
||||||
|
$recordingParams = new GetRecordingsParameters();
|
||||||
|
$recordingParams->setMeetingId($room->uid);
|
||||||
|
$recordingParams->setState('processing,processed,published,unpublished');
|
||||||
|
|
||||||
|
$response = $this->getServer()->getRecordings($recordingParams);
|
||||||
|
|
||||||
|
if (!$response->success()) {
|
||||||
|
throw new \Exception('Could not process get recordings request');
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = $response->getRecords();
|
||||||
|
|
||||||
|
return array_map(function ($record) {
|
||||||
|
return $this->recordToArray($record);
|
||||||
|
}, $records);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteRecording(string $recordingId): bool
|
||||||
|
{
|
||||||
|
$deleteParams = new DeleteRecordingsParameters($recordingId);
|
||||||
|
|
||||||
|
$response = $this->getServer()->deleteRecordings($deleteParams);
|
||||||
|
|
||||||
|
return $response->isDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function recordToArray(Record $record)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $record->getRecordId(),
|
||||||
|
'name' => $record->getName(),
|
||||||
|
'published' => $record->isPublished(),
|
||||||
|
'state' => $record->getState(),
|
||||||
|
'startTime' => $record->getStartTime(),
|
||||||
|
'participants' => $record->getParticipantCount(),
|
||||||
|
'type' => $record->getPlaybackType(),
|
||||||
|
'length' => $record->getPlaybackLength(),
|
||||||
|
'url' => $record->getPlaybackUrl(),
|
||||||
|
'metas' => $record->getMetas(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Controller;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\BigBlueButton\API;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
use OCP\AppFramework\Controller;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\Service\RoomService;
|
||||||
|
|
||||||
|
class ServerController extends Controller
|
||||||
|
{
|
||||||
|
/** @var RoomService */
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
/** @var API */
|
||||||
|
private $server;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private $userId;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
$appName,
|
||||||
|
IRequest $request,
|
||||||
|
RoomService $service,
|
||||||
|
API $server,
|
||||||
|
$UserId
|
||||||
|
) {
|
||||||
|
parent::__construct($appName, $request);
|
||||||
|
|
||||||
|
$this->service = $service;
|
||||||
|
$this->server = $server;
|
||||||
|
$this->userId = $UserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function records(string $roomUid): DataResponse
|
||||||
|
{
|
||||||
|
$room = $this->service->findByUid($roomUid);
|
||||||
|
|
||||||
|
if ($room === null) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($room->userId !== $this->userId) {
|
||||||
|
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$recordings = $this->server->getRecordings($room);
|
||||||
|
|
||||||
|
return new DataResponse($recordings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function deleteRecord(string $recordId): DataResponse
|
||||||
|
{
|
||||||
|
$record = $this->server->getRecording($recordId);
|
||||||
|
|
||||||
|
$room = $this->service->findByUid($record['metas']['meetingId']);
|
||||||
|
|
||||||
|
if ($room === null) {
|
||||||
|
return new DataResponse(false, Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($room->userId !== $this->userId) {
|
||||||
|
return new DataResponse(false, Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = $this->server->deleteRecording($recordId);
|
||||||
|
|
||||||
|
return new DataResponse($success);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,19 @@ export interface Room {
|
||||||
record: boolean;
|
record: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Recording = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
published: boolean;
|
||||||
|
state: 'processing' | 'processed' | 'published' | 'unpublished' | 'deleted';
|
||||||
|
startTime: number;
|
||||||
|
participants: number;
|
||||||
|
type: string;
|
||||||
|
length: number;
|
||||||
|
url: string;
|
||||||
|
meta: any;
|
||||||
|
}
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
public getUrl(endpoint: string): string {
|
public getUrl(endpoint: string): string {
|
||||||
return OC.generateUrl(`apps/bbb/${endpoint}`);
|
return OC.generateUrl(`apps/bbb/${endpoint}`);
|
||||||
|
@ -42,6 +55,26 @@ class Api {
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getRecordings(uid: string) {
|
||||||
|
const response = await axios.get(this.getUrl(`server/${uid}/records`));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteRecording(id: string) {
|
||||||
|
const response = await axios.delete(this.getUrl(`server/record/${id}`));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeRecording(recording: Recording, path: string) {
|
||||||
|
const startDate = new Date(recording.startTime);
|
||||||
|
const url = `/remote.php/dav/files/${OC.currentUser}${path}/${encodeURIComponent(recording.name + ' ' + startDate.toISOString())}.url`;
|
||||||
|
const response = await axios.put(url, `[InternetShortcut]\nURL=${recording.url}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new Api();
|
export const api = new Api();
|
||||||
|
|
|
@ -102,4 +102,25 @@
|
||||||
background-color: rgba(0, 128, 0, 0.445);
|
background-color: rgba(0, 128, 0, 0.445);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-row,
|
||||||
|
.recordings-row {
|
||||||
|
border-left: 3px solid #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-row {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recordings-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
|
||||||
|
&> td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,9 @@ const App: React.FC<Props> = () => {
|
||||||
<th onClick={() => onOrderBy('record')}>
|
<th onClick={() => onOrderBy('record')}>
|
||||||
{t('bbb', 'Record')} <SortArrow name='record' value={orderBy} direction={sortOrder} />
|
{t('bbb', 'Record')} <SortArrow name='record' value={orderBy} direction={sortOrder} />
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
{t('bbb', 'Recordings')}
|
||||||
|
</th>
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
@ -10,6 +10,8 @@ declare namespace OC {
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace dialogs {
|
namespace dialogs {
|
||||||
|
function alert(text: string, title: string, callback: () => void, modal?: boolean): void;
|
||||||
|
|
||||||
function info(text: string, title: string, callback: () => void, modal?: boolean): void;
|
function info(text: string, title: string, callback: () => void, modal?: boolean): void;
|
||||||
|
|
||||||
function confirm(text: string, title: string, callback: (result: boolean) => void, modal?: boolean): void;
|
function confirm(text: string, title: string, callback: (result: boolean) => void, modal?: boolean): void;
|
||||||
|
@ -61,6 +63,8 @@ declare namespace OC {
|
||||||
const PERMISSION_SHARE = 16;
|
const PERMISSION_SHARE = 16;
|
||||||
const PERMISSION_ALL = 31;
|
const PERMISSION_ALL = 31;
|
||||||
|
|
||||||
|
const currentUser: string;
|
||||||
|
|
||||||
const config: {
|
const config: {
|
||||||
blacklist_files_regex: string;
|
blacklist_files_regex: string;
|
||||||
enable_avatars: boolean;
|
enable_avatars: boolean;
|
||||||
|
@ -77,6 +81,8 @@ declare namespace OC {
|
||||||
|
|
||||||
declare function t(app: string, string: string, vars?: { [key: string]: string }, count?: number, options?: EscapeOptions): string;
|
declare function t(app: string, string: string, vars?: { [key: string]: string }, count?: number, options?: EscapeOptions): string;
|
||||||
|
|
||||||
|
declare function n(app: string, singular: string, plural: string, number: number, vars?: { [key: string]: string }): string;
|
||||||
|
|
||||||
declare module 'NC' {
|
declare module 'NC' {
|
||||||
export interface OCSResult<T> {
|
export interface OCSResult<T> {
|
||||||
ocs: {
|
ocs: {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
|
import { Recording } from './Api';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
recording: Recording;
|
||||||
|
deleteRecording: (recording: Recording) => void;
|
||||||
|
storeRecording: (recording: Recording) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecordingRow: React.FC<Props> = ({recording, deleteRecording, storeRecording}) => {
|
||||||
|
return (
|
||||||
|
<tr key={recording.id}>
|
||||||
|
<td className="share icon-col">
|
||||||
|
<CopyToClipboard text={recording.url}>
|
||||||
|
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</td>
|
||||||
|
<td className="start icon-col">
|
||||||
|
<a href={recording.url} className="icon icon-external icon-visible" target="_blank" rel="noopener noreferrer"></a>
|
||||||
|
</td>
|
||||||
|
<td className="icon-col">
|
||||||
|
<a onClick={() => storeRecording(recording)} className="icon icon-download icon-visible"></a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{(new Date(recording.startTime)).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{recording.length === 0 ? '< 1 min' : (recording.length + ' min')}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{n('bbb', '%n participant', '%n participants', recording.participants)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{recording.type}
|
||||||
|
</td>
|
||||||
|
<td className="remove icon-col">
|
||||||
|
<a className="icon icon-delete icon-visible"
|
||||||
|
onClick={() => deleteRecording(recording)}
|
||||||
|
title={t('bbb', 'Delete')} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingRow;
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { SubmitInput } from './SubmitInput';
|
import { SubmitInput } from './SubmitInput';
|
||||||
import { Room, api } from './Api';
|
import { Room, Recording, api } from './Api';
|
||||||
|
import RecordingRow from './RecordingRow';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -10,7 +11,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditableValueProps = {
|
type EditableValueProps = {
|
||||||
setValue: (key: string, value: string|number) => void;
|
setValue: (key: string, value: string | number) => void;
|
||||||
setActive: (key: string) => void;
|
setActive: (key: string) => void;
|
||||||
active: string;
|
active: string;
|
||||||
field: string;
|
field: string;
|
||||||
|
@ -18,11 +19,33 @@ type EditableValueProps = {
|
||||||
type: 'text' | 'number';
|
type: 'text' | 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecordingsNumberProps = {
|
||||||
|
recordings: null | Recording[];
|
||||||
|
showRecordings: boolean;
|
||||||
|
setShowRecordings: (showRecordings: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecordingsNumber: React.FC<RecordingsNumberProps> = ({ recordings, showRecordings, setShowRecordings }) => {
|
||||||
|
if (recordings === null) {
|
||||||
|
return <span className="icon icon-loading-small icon-visible"></span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordings.length > 0) {
|
||||||
|
return (
|
||||||
|
<a onClick={() => setShowRecordings(!showRecordings)}>
|
||||||
|
{recordings.length} <span className='sort_arrow'>{showRecordings ? '▼' : '▲'}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>0</span>;
|
||||||
|
};
|
||||||
|
|
||||||
const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, active, field, value, type }) => {
|
const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, active, field, value, type }) => {
|
||||||
if (active === field) {
|
if (active === field) {
|
||||||
return <SubmitInput
|
return <SubmitInput
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value):value)}
|
onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value) : value)}
|
||||||
onClick={event => event.stopPropagation()}
|
onClick={event => event.stopPropagation()}
|
||||||
initialValue={value}
|
initialValue={value}
|
||||||
type={type}
|
type={type}
|
||||||
|
@ -40,9 +63,26 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, acti
|
||||||
|
|
||||||
const RoomRow: React.FC<Props> = (props) => {
|
const RoomRow: React.FC<Props> = (props) => {
|
||||||
const [activeEdit, setActiveEdit] = useState('');
|
const [activeEdit, setActiveEdit] = useState('');
|
||||||
|
const [recordings, setRecordings] = useState<Recording[] | null>(null);
|
||||||
|
const [showRecordings, setShowRecordings] = useState<boolean>(false);
|
||||||
const room = props.room;
|
const room = props.room;
|
||||||
|
const areRecordingsLoaded = recordings !== null;
|
||||||
|
|
||||||
function updateRoom(key: string, value: string|boolean|number) {
|
useEffect(() => {
|
||||||
|
if (areRecordingsLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.getRecordings(room.uid).then(recordings => {
|
||||||
|
setRecordings(recordings);
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn('Could not request recordings: ' + room.uid, err);
|
||||||
|
|
||||||
|
setRecordings([]);
|
||||||
|
});
|
||||||
|
}, [areRecordingsLoaded]);
|
||||||
|
|
||||||
|
function updateRoom(key: string, value: string | boolean | number) {
|
||||||
props.updateRoom({
|
props.updateRoom({
|
||||||
...props.room,
|
...props.room,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
|
@ -66,12 +106,70 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(field: string, type: 'text' | 'number' = 'text'){
|
function storeRecording(recording: Recording) {
|
||||||
|
OC.dialogs.filepicker(t('bbb', 'Select target folder'), (path: string) => {
|
||||||
|
api.storeRecording(recording, path).then(() => {
|
||||||
|
OC.dialogs.info(
|
||||||
|
t('bbb', 'URL to presentation was stored in "{path}"', { path: path + '/' }),
|
||||||
|
t('bbb', 'File stored'),
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn('Could not store recording', err);
|
||||||
|
|
||||||
|
OC.dialogs.alert(
|
||||||
|
t('bbb', 'URL to presentation could not be stored.'),
|
||||||
|
t('bbb', 'Error'),
|
||||||
|
() => undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, undefined, 'httpd/unix-directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRecording(recording: Recording) {
|
||||||
|
OC.dialogs.confirm(
|
||||||
|
t('bbb', 'Are you sure you want to delete the recording from "{startDate}"? This operation can not be undone', { startDate: (new Date(recording.startTime)).toLocaleString() }),
|
||||||
|
t('bbb', 'Delete?'),
|
||||||
|
confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
api.deleteRecording(recording.id).then(success => {
|
||||||
|
if (!success) {
|
||||||
|
OC.dialogs.info(
|
||||||
|
t('bbb', 'Could not delete record'),
|
||||||
|
t('bbb', 'Error'),
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordings === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRecordings(recordings.filter(r => r.id !== recording.id));
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn('Could not delete recording', err);
|
||||||
|
|
||||||
|
OC.dialogs.info(
|
||||||
|
t('bbb', 'Could not delete record'),
|
||||||
|
t('bbb', 'Server error'),
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(field: string, type: 'text' | 'number' = 'text') {
|
||||||
return <EditableValue field={field} value={room[field]} active={activeEdit} setActive={setActiveEdit} setValue={updateRoom} type={type} />;
|
return <EditableValue field={field} value={room[field]} active={activeEdit} setActive={setActiveEdit} setValue={updateRoom} type={type} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={room.id}>
|
<>
|
||||||
|
<tr className={showRecordings ? 'selected-row' : ''}>
|
||||||
<td className="share icon-col">
|
<td className="share icon-col">
|
||||||
<CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}>
|
<CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}>
|
||||||
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
|
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
|
||||||
|
@ -93,12 +191,23 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||||
<label htmlFor={`bbb-record-${room.id}`}></label>
|
<label htmlFor={`bbb-record-${room.id}`}></label>
|
||||||
</td>
|
</td>
|
||||||
|
<td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
||||||
<td className="remove icon-col">
|
<td className="remove icon-col">
|
||||||
<a className="icon icon-delete icon-visible"
|
<a className="icon icon-delete icon-visible"
|
||||||
onClick={deleteRow as any}
|
onClick={deleteRow as any}
|
||||||
title={t('bbb', 'Delete')} />
|
title={t('bbb', 'Delete')} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{showRecordings && <tr className="recordings-row">
|
||||||
|
<td colSpan={8}>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{recordings?.map(recording => <RecordingRow key={recording.id} recording={recording} deleteRecording={deleteRecording} storeRecording={storeRecording} />)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue