From b329d75c3526da5f364cfec58dada033d26f3a89 Mon Sep 17 00:00:00 2001 From: Bahaa Alhagar Date: Tue, 31 Jul 2018 18:01:41 +0200 Subject: [PATCH] package files --- README.md | 2 +- config/youtubeUploader.php | 57 +++ ...030_create_youtube_access_tokens_table.php | 32 ++ prefill.php | 111 ----- routes/web.php | 32 ++ src/Facades/YoutubeUploader.php | 2 +- src/YoutubeUploader.php | 414 +++++++++++++++++- 7 files changed, 534 insertions(+), 116 deletions(-) create mode 100644 config/youtubeUploader.php create mode 100644 migrations/2015_05_06_194030_create_youtube_access_tokens_table.php delete mode 100644 prefill.php create mode 100644 routes/web.php diff --git a/README.md b/README.md index 6c05820..08150c3 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# :package_name +# :Youtube Uploader diff --git a/config/youtubeUploader.php b/config/youtubeUploader.php new file mode 100644 index 0000000..fdc0cd4 --- /dev/null +++ b/config/youtubeUploader.php @@ -0,0 +1,57 @@ + env('GOOGLE_CLIENT_ID', null), + + /** + * Client Secret. + */ + 'client_secret' => env('GOOGLE_CLIENT_SECRET', null), + + /** + * Scopes. + */ + 'scopes' => [ + 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/youtube.upload', + 'https://www.googleapis.com/auth/youtube.readonly' + ], + + /** + * Route URI's + */ + 'routes' => [ + + /** + * Determine if the Routes should be disabled. + * Note: We recommend this to be set to "false" immediately after authentication. + */ + 'enabled' => false, + + /** + * The prefix for the below URI's + */ + 'prefix' => 'youtube', + + /** + * Redirect URI + */ + 'redirect_uri' => 'callback', + + /** + * The autentication URI + */ + 'authentication_uri' => 'auth', + + /** + * The redirect back URI + */ + 'redirect_back_uri' => '/', + + ] + +]; diff --git a/migrations/2015_05_06_194030_create_youtube_access_tokens_table.php b/migrations/2015_05_06_194030_create_youtube_access_tokens_table.php new file mode 100644 index 0000000..c2e51d6 --- /dev/null +++ b/migrations/2015_05_06_194030_create_youtube_access_tokens_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->text('access_token'); + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('youtube_access_tokens'); + } +} \ No newline at end of file diff --git a/prefill.php b/prefill.php deleted file mode 100644 index 822f88a..0000000 --- a/prefill.php +++ /dev/null @@ -1,111 +0,0 @@ - ['Your name', '', ''], - 'author_github_username' => ['Your Github username', ' in https://github.com/username', ''], - 'author_email' => ['Your email address', '', ''], - 'author_twitter' => ['Your twitter username', '', '@{author_github_username}'], - 'author_website' => ['Your website', '', 'https://github.com/{author_github_username}'], - - 'package_vendor' => ['Package vendor', ' in https://github.com/vendor/package', '{author_github_username}'], - 'package_name' => ['Package name', ' in https://github.com/vendor/package', ''], - 'package_description' => ['Package very short description', '', ''], - - 'psr4_namespace' => ['PSR-4 namespace', 'usually, Vendor\\Package', '{package_vendor}\\{package_name}'], -]; - -$values = []; - -$replacements = [ - ':vendor\\\\:package_name\\\\' => function () use(&$values) { return str_replace('\\', '\\\\', $values['psr4_namespace']) . '\\\\'; }, - ':author_name' => function () use(&$values) { return $values['author_name']; }, - ':author_username' => function () use(&$values) { return $values['author_github_username']; }, - ':author_website' => function () use(&$values) { return $values['author_website'] ?: ('https://github.com/' . $values['author_github_username']); }, - ':author_email' => function () use(&$values) { return $values['author_email'] ?: ($values['author_github_username'] . '@example.com'); }, - ':vendor' => function () use(&$values) { return $values['package_vendor']; }, - ':package_name' => function () use(&$values) { return $values['package_name']; }, - ':package_description' => function () use(&$values) { return $values['package_description']; }, - 'League\\Skeleton' => function () use(&$values) { return $values['psr4_namespace']; }, -]; - -function read_from_console ($prompt) { - if ( function_exists('readline') ) { - $line = trim(readline($prompt)); - if (!empty($line)) { - readline_add_history($line); - } - } else { - echo $prompt; - $line = trim(fgets(STDIN)); - } - return $line; -} - -function interpolate($text, $values) -{ - if (!preg_match_all('/\{(\w+)\}/', $text, $m)) { - return $text; - } - foreach ($m[0] as $k => $str) { - $f = $m[1][$k]; - $text = str_replace($str, $values[$f], $text); - } - return $text; -} - -$modify = 'n'; -do { - if ($modify == 'q') { - exit; - } - - $values = []; - - echo "----------------------------------------------------------------------\n"; - echo "Please, provide the following information:\n"; - echo "----------------------------------------------------------------------\n"; - foreach ($fields as $f => $field) { - $default = isset($field[COL_DEFAULT]) ? interpolate($field[COL_DEFAULT], $values): ''; - $prompt = sprintf( - '%s%s%s: ', - $field[COL_DESCRIPTION], - $field[COL_HELP] ? ' (' . $field[COL_HELP] . ')': '', - $field[COL_DEFAULT] !== '' ? ' [' . $default . ']': '' - ); - $values[$f] = read_from_console($prompt); - if (empty($values[$f])) { - $values[$f] = $default; - } - } - echo "\n"; - - echo "----------------------------------------------------------------------\n"; - echo "Please, check that everything is correct:\n"; - echo "----------------------------------------------------------------------\n"; - foreach ($fields as $f => $field) { - echo $field[COL_DESCRIPTION] . ": $values[$f]\n"; - } - echo "\n"; -} while (($modify = strtolower(read_from_console('Modify files with these values? [y/N/q] '))) != 'y'); -echo "\n"; - -$files = array_merge( - glob(__DIR__ . '/*.md'), - glob(__DIR__ . '/*.xml.dist'), - glob(__DIR__ . '/composer.json'), - glob(__DIR__ . '/src/*.php'), - glob(__DIR__ . '/tests/*.php') -); -foreach ($files as $f) { - $contents = file_get_contents($f); - foreach ($replacements as $str => $func) { - $contents = str_replace($str, $func(), $contents); - } - file_put_contents($f, $contents); -} - -echo "Done.\n"; -echo "Now you should remove the file '" . basename(__FILE__) . "'.\n"; diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..b2a51c9 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,32 @@ + config('youtube.routes.prefix')], function() { + + /** + * Authentication + */ + Route::get(config('youtube.routes.authentication_uri'), function() + { + return redirect()->to(YoutubeUploader::createAuthUrl()); + }); + + /** + * Redirect + */ + Route::get(config('youtube.routes.redirect_uri'), function(Illuminate\Http\Request $request) + { + if(!$request->has('code')) { + throw new Exception('$_GET[\'code\'] is not set. Please re-authenticate.'); + } + + $token = YoutubeUploader::authenticate($request->get('code')); + + YoutubeUploader::saveAccessTokenToDB($token); + + return redirect(config('youtube.routes.redirect_back_uri', '/')); + }); + +}); diff --git a/src/Facades/YoutubeUploader.php b/src/Facades/YoutubeUploader.php index 2091475..40bf8f4 100644 --- a/src/Facades/YoutubeUploader.php +++ b/src/Facades/YoutubeUploader.php @@ -1,6 +1,6 @@ app = $app; + + $this->client = $this->setup($client); + + $this->youtube = new \Google_Service_YouTube($this->client); + + if ($accessToken = $this->getLatestAccessTokenFromDB()) { + $this->client->setAccessToken($accessToken); + } + } + + /** + * Upload the video to YouTube + * + * @param string $path + * @param array $data + * @param string $privacyStatus + * @return self + * @throws Exception + */ + public function upload($path, array $data = [], $privacyStatus = 'public') + { + if(!file_exists($path)) { + throw new Exception('Video file does not exist at path: "'. $path .'". Provide a full path to the file before attempting to upload.'); + } + + $this->handleAccessToken(); + + try { + $video = $this->getVideo($data, $privacyStatus); + + // Set the Chunk Size + $chunkSize = 1 * 1024 * 1024; + + // Set the defer to true + $this->client->setDefer(true); + + // Build the request + $insert = $this->youtube->videos->insert('status,snippet', $video); + + // Upload + $media = new \Google_Http_MediaFileUpload( + $this->client, + $insert, + 'video/*', + null, + true, + $chunkSize + ); + + // Set the Filesize + $media->setFileSize(filesize($path)); + + // Read the file and upload in chunks + $status = false; + $handle = fopen($path, "rb"); + + while (!$status && !feof($handle)) { + $chunk = fread($handle, $chunkSize); + $status = $media->nextChunk($chunk); + } + + fclose($handle); + + $this->client->setDefer(false); + + // Set ID of the Uploaded Video + $this->videoId = $status['id']; + + // Set the Snippet from Uploaded Video + $this->snippet = $status['snippet']; + + } catch (\Google_Service_Exception $e) { + throw new Exception($e->getMessage()); + } catch (\Google_Exception $e) { + throw new Exception($e->getMessage()); + } + + return $this; + } + + /** + * Update the video on YouTube + * + * @param string $id + * @param array $data + * @param string $privacyStatus + * @return self + * @throws Exception + */ + public function update($id, array $data = [], $privacyStatus = 'public') + { + $this->handleAccessToken(); + + if (!$this->exists($id)) { + throw new Exception('A video matching id "'. $id .'" could not be found.'); + } + + try { + $video = $this->getVideo($data, $privacyStatus, $id); + + $status = $this->youtube->videos->update('status,snippet', $video); + + // Set ID of the Updated Video + $this->videoId = $status['id']; + + // Set the Snippet from Updated Video + $this->snippet = $status['snippet']; + } catch (\Google_Service_Exception $e) { + throw new Exception($e->getMessage()); + } catch (\Google_Exception $e) { + throw new Exception($e->getMessage()); + } + + return $this; + } + + /** + * Set a Custom Thumbnail for the Upload + * + * @param string $imagePath + * @return self + * @throws Exception + */ + public function withThumbnail($imagePath) + { + try { + $videoId = $this->getVideoId(); + + $chunkSizeBytes = 1 * 1024 * 1024; + + $this->client->setDefer(true); + + $setRequest = $this->youtube->thumbnails->set($videoId); + + $media = new \Google_Http_MediaFileUpload( + $this->client, + $setRequest, + 'image/png', + null, + true, + $chunkSizeBytes + ); + $media->setFileSize(filesize($imagePath)); + + $status = false; + $handle = fopen($imagePath, "rb"); + + while (!$status && !feof($handle)) { + $chunk = fread($handle, $chunkSizeBytes); + $status = $media->nextChunk($chunk); + } + + fclose($handle); + + $this->client->setDefer(false); + $this->thumbnailUrl = $status['items'][0]['default']['url']; + + } catch (\Google_Service_Exception $e) { + throw new Exception($e->getMessage()); + } catch (\Google_Exception $e) { + throw new Exception($e->getMessage()); + } + + return $this; + } + + /** + * Delete a YouTube video by it's ID. + * + * @param int $id + * @return bool + * @throws Exception + */ + public function delete($id) + { + $this->handleAccessToken(); + + if (!$this->exists($id)) { + throw new Exception('A video matching id "'. $id .'" could not be found.'); + } + + return $this->youtube->videos->delete($id); + } + + /** + * @param $data + * @param $privacyStatus + * @param null $id + * @return \Google_Service_YouTube_Video + */ + private function getVideo($data, $privacyStatus, $id = null) + { + // Setup the Snippet + $snippet = new \Google_Service_YouTube_VideoSnippet(); + + if (array_key_exists('title', $data)) $snippet->setTitle($data['title']); + if (array_key_exists('description', $data)) $snippet->setDescription($data['description']); + if (array_key_exists('tags', $data)) $snippet->setTags($data['tags']); + if (array_key_exists('category_id', $data)) $snippet->setCategoryId($data['category_id']); + + // Set the Privacy Status + $status = new \Google_Service_YouTube_VideoStatus(); + $status->privacyStatus = $privacyStatus; + + // Set the Snippet & Status + $video = new \Google_Service_YouTube_Video(); + if ($id) + { + $video->setId($id); + } + + $video->setSnippet($snippet); + $video->setStatus($status); + + return $video; + } + + /** + * Check if a YouTube video exists by it's ID. + * + * @param int $id + * + * @return bool + */ + public function exists($id) + { + $this->handleAccessToken(); + + $response = $this->youtube->videos->listVideos('status', ['id' => $id]); + + if (empty($response->items)) return false; + + return true; + } + + /** + * Return the Video ID + * + * @return string + */ + public function getVideoId() + { + return $this->videoId; + } + + /** + * Return the snippet of the uploaded Video + * + * @return array + */ + public function getSnippet() + { + return $this->snippet; + } + + /** + * Return the URL for the Custom Thumbnail + * + * @return string + */ + public function getThumbnailUrl() + { + return $this->thumbnailUrl; + } + + /** + * Setup the Google Client + * + * @param Google_Client $client + * @return Google_Client $client + * @throws Exception + */ + private function setup(Google_Client $client) + { + if( + !$this->app->config->get('youtube.client_id') || + !$this->app->config->get('youtube.client_secret') + ) { + throw new Exception('A Google "client_id" and "client_secret" must be configured.'); + } + + $client->setClientId($this->app->config->get('youtube.client_id')); + $client->setClientSecret($this->app->config->get('youtube.client_secret')); + $client->setScopes($this->app->config->get('youtube.scopes')); + $client->setAccessType('offline'); + $client->setApprovalPrompt('force'); + $client->setRedirectUri(url( + $this->app->config->get('youtube.routes.prefix') + . '/' . + $this->app->config->get('youtube.routes.redirect_uri') + )); + + return $this->client = $client; + } + + /** + * Saves the access token to the database. + * + * @param string $accessToken + */ + public function saveAccessTokenToDB($accessToken) + { + return DB::table('youtube_access_tokens')->insert([ + 'access_token' => json_encode($accessToken), + 'created_at' => Carbon::createFromTimestamp($accessToken['created']) + ]); + } + + /** + * Get the latest access token from the database. + * + * @return string + */ + public function getLatestAccessTokenFromDB() + { + $latest = DB::table('youtube_access_tokens') + ->latest('created_at') + ->first(); + + return $latest ? (is_array($latest) ? $latest['access_token'] : $latest->access_token ) : null; + } + + /** + * Handle the Access Token + * + * @return void + */ + public function handleAccessToken() + { + if (is_null($accessToken = $this->client->getAccessToken())) { + throw new \Exception('An access token is required.'); + } + + if($this->client->isAccessTokenExpired()) + { + // If we have a "refresh_token" + if (array_key_exists('refresh_token', $accessToken)) + { + // Refresh the access token + $this->client->refreshToken($accessToken['refresh_token']); + + // Save the access token + $this->saveAccessTokenToDB($this->client->getAccessToken()); + } + } + } + + /** + * Pass method calls to the Google Client. + * + * @param string $method + * @param array $args + * + * @return mixed */ - public function __construct() + public function __call($method, $args) { - // constructor body + return call_user_func_array([$this->client, $method], $args); } }