VideoController.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. namespace app\Controllers;
  3. use app\Exceptions\AccountNotFoundException;
  4. use app\Exceptions\CommentNotFoundException;
  5. use app\Exceptions\InvalidRequestException;
  6. use app\Exceptions\InvalidVideoException;
  7. use app\Exceptions\UnauthenticatedException;
  8. use app\Exceptions\VideoNotFoundException;
  9. use app\Hajeebtok;
  10. use app\Types\DatabaseObjects\View;
  11. use Cassandra\Exception\UnauthorizedException;
  12. use Exception;
  13. use app\Exceptions\SecurityFaultException;
  14. use Pecee\SimpleRouter\SimpleRouter;
  15. use Mimey\MimeTypes;
  16. use app\Interfaces\IRouteController;
  17. use app\Types\DatabaseObjects\Video;
  18. use app\Types\DatabaseObjects\Account;
  19. use app\Logger;
  20. use FFMpeg;
  21. class VideoController implements IRouteController
  22. {
  23. public static function getVideo(string $id): string
  24. {
  25. $signed_in = signed_in(request());
  26. $video_information = new Video($id);
  27. $video_information->Load();
  28. $mime_types = new MimeTypes();
  29. if ($signed_in) { // Signed in
  30. $video_path = APP_ROOT . "/usercontent/videos/$id/video.mp4";
  31. } else { // not signed in
  32. $rand = rand(1, 48);
  33. $video_path = APP_ROOT . "/usercontent/fake_videos/$rand/video.mp4";
  34. }
  35. CORSHelper();
  36. if (file_exists($video_path)) {
  37. $video_contents = file_get_contents($video_path);
  38. $video_size = filesize($video_path);
  39. $mime = $mime_types->getMimeType(pathinfo($video_path, PATHINFO_EXTENSION));
  40. $response = response();
  41. $response->header("Content-Type: $mime");
  42. $response->header("Content-Length: $video_size");
  43. $response->header("Cache-Control: max-age=86400, public");
  44. return $video_contents;
  45. } else {
  46. throw new VideoNotFoundException($id, 404);
  47. }
  48. }
  49. public static function getInfo(string $id): string
  50. {
  51. $video_information = new Video($id);
  52. $video_information->Load();
  53. $author_information = new Account($video_information->author_id);
  54. $author_information->Load();
  55. CORSHelper();
  56. return api_json([
  57. "id" => $video_information->id,
  58. "title" => $video_information->title,
  59. "description" => $video_information->description,
  60. "likes" => $video_information->likes,
  61. "dislikes" => $video_information->dislikes,
  62. "comments" => $video_information->comments,
  63. "shares" => $video_information->shares,
  64. "author" => [
  65. "id" => $author_information->id,
  66. "pictureHash" => $author_information->picture_hash,
  67. "username" => $author_information->username
  68. ],
  69. ]);
  70. }
  71. public static function search(): string
  72. {
  73. $query = input("query");
  74. $video = new Video(title: $query);
  75. $videos = $video->LoadMany();
  76. // idk if this is the greatest thing to do but jetbrains phpfart ai recommended it and it looks fine to me /shurg
  77. $accounts = Hajeebtok::$Database->Query("SELECT * FROM accounts WHERE id IN (SELECT author_id FROM videos WHERE title LIKE :title)", ["title" => "%$query%"]);
  78. if (empty($accounts)) throw new AccountNotFoundException(0, 404);
  79. $data = [];
  80. foreach ($videos as $vid) {
  81. $data[] = [
  82. "id" => $vid["id"],
  83. "title" => $vid["title"],
  84. "description" => $vid["description"],
  85. "likes" => $vid["likes"],
  86. "dislikes" => $vid["dislikes"],
  87. "comments" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM comments WHERE video_id = :id", ["id" => $vid["id"]]),
  88. "shares" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM messages INNER JOIN videos ON messages.video_id = videos.id WHERE videos.id = :id", ["id" => $vid["id"]]),
  89. "author" => [
  90. "id" => $vid["author_id"],
  91. "pictureHash" => $accounts[$vid["author_id"] - 1]["picture_hash"], // kinda scuffed fix
  92. "username" => $accounts[$vid["author_id"] - 1]["username"]
  93. ],
  94. ];
  95. }
  96. CORSHelper();
  97. return api_json($data);
  98. }
  99. public static function getThumbnail(string $id): string
  100. {
  101. $signed_in = signed_in(request());
  102. $video_information = new Video($id);
  103. $video_information->Load();
  104. if ($signed_in) { // Signed in
  105. $seconds = 2;
  106. $video_path = APP_ROOT . "/usercontent/videos/$id/video.mp4";
  107. $frame_path = APP_ROOT . "/usercontent/videos/$id/thumbnail.png";
  108. } else { // not signed in
  109. $rand = rand(1, 47);
  110. $seconds = 6;
  111. $video_path = APP_ROOT . "/usercontent/fake_videos/$rand/video.mp4";
  112. $frame_path = APP_ROOT . "/usercontent/fake_videos/$rand/thumbnail.png";
  113. }
  114. $mime_types = new MimeTypes();
  115. if (!file_exists($video_path)) throw new VideoNotFoundException($id, 404);
  116. if (!file_exists($frame_path)) {
  117. Logger::Debug("Generating thumbnail for video $id");
  118. $ffmpeg = FFMpeg\FFMpeg::create();
  119. $video = $ffmpeg->open($video_path);
  120. $frame = $video->frame(FFMpeg\Coordinate\TimeCode::fromSeconds($seconds));
  121. $frame->save($frame_path);
  122. }
  123. CORSHelper();
  124. $mime = $mime_types->getMimeType(pathinfo($frame_path, PATHINFO_EXTENSION));
  125. $response = response();
  126. $response->header("Content-Type: $mime");
  127. $response->header("Cache-Control: max-age=86400, public");
  128. return file_get_contents($frame_path);
  129. }
  130. public static function getFeed(): string
  131. {
  132. // todo: implement offset & limit
  133. $signed_in = signed_in(request());
  134. if (!$signed_in) throw new AccountNotFoundException(0, 404);
  135. $id = get_token_id(request());
  136. $feed = [];
  137. $videos = Hajeebtok::$Database->Query("SELECT * FROM videos WHERE id NOT IN (SELECT video_id FROM views WHERE account_id = :account_id AND video_id > :offset) ORDER BY id DESC", ["account_id" => $id, "offset" => $offset]);
  138. foreach ($videos as $video) {
  139. $account = new Account($video["author_id"]);
  140. $account->Load();
  141. $feed[] = [
  142. "id" => $video["id"],
  143. "title" => $video["title"],
  144. "description" => $video["description"],
  145. "likes" => $video["likes"],
  146. "dislikes" => $video["dislikes"],
  147. "comments" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM comments WHERE video_id = :id", ["id" => $video["id"]]),
  148. "shares" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM messages INNER JOIN videos ON messages.video_id = videos.id WHERE videos.id = :id", ["id" => $video["id"]]),
  149. "author" => [
  150. "id" => $video["author_id"],
  151. "pictureHash" => $account->picture_hash,
  152. "username" => $account->username,
  153. ],
  154. ];
  155. }
  156. CORSHelper();
  157. return api_json($feed);
  158. }
  159. public static function uploadVideo(): string
  160. {
  161. if (!signed_in(request())) throw new UnauthenticatedException(0, 401);
  162. $video_file = input()->file("video");
  163. $title = input("title");
  164. $description = input("description");
  165. $author_id = get_token_id(request());
  166. if (empty($video_file) || empty($title) || empty($author_id)) throw new InvalidRequestException(0, 400);
  167. if (!file_exists($video_file->getTmpName()))
  168. throw new InvalidRequestException(400);
  169. // save database object
  170. $video = new Video(title: $title, description: $description, author_id: $author_id);
  171. $video->Save();
  172. // Save file
  173. $video_folder = APP_ROOT . "/usercontent/videos/" . $video->id;
  174. // check video file type
  175. $mimey = new MimeTypes();
  176. $mime_type = $mimey->getMimeType($video_file->getExtension());
  177. if ($mime_type != "video/mp4" && $mime_type != "video/ogg" && $mime_type != "video/webm" && $mime_type != "video/x-matroska") {
  178. $video->Delete();
  179. throw new InvalidVideoException($video->id, 415); // media type unsupported
  180. }
  181. // move video
  182. mkdir($video_folder);
  183. Logger::Debug($video_file->getFilename() . " -> " . $video_folder . "/video.mp4");
  184. $video_file->move($video_folder . "/video.mp4");
  185. CORSHelper();
  186. return api_json([
  187. "id" => $video->id,
  188. "title" => $video->title,
  189. "description" => $video->description,
  190. "likes" => $video->likes,
  191. "dislikes" => $video->dislikes,
  192. "comments" => 0,
  193. "shares" => 0,
  194. "author" => [
  195. "id" => $author_id,
  196. ]
  197. ]);
  198. }
  199. public static function RegisterRoutes(): void
  200. {
  201. SimpleRouter::group([
  202. "prefix" => "/video",
  203. ], function () {
  204. SimpleRouter::get("/{id}/get", [VideoController::class, "getVideo"]);
  205. SimpleRouter::get("/{id}/info", [VideoController::class, "getInfo"]);
  206. SimpleRouter::get("/{id}/thumbnail", [VideoController::class, "getThumbnail"]);
  207. SimpleRouter::post("/upload", [VideoController::class, "uploadVideo"]);
  208. SimpleRouter::post("/search", [VideoController::class, "search"]);
  209. SimpleRouter::get("/feed", [VideoController::class, "getFeed"]);
  210. SimpleRouter::options("/{id}/get", "CORSHelper");
  211. SimpleRouter::options("/{id}/info", "CORSHelper");
  212. SimpleRouter::options("/{id}/thumbnail", "CORSHelper");
  213. SimpleRouter::options("/upload", "CORSHelper");
  214. SimpleRouter::options("/search", "CORSHelper");
  215. });
  216. }
  217. }