GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( aee94c...f9909c )
by Bob Olde
07:49
created

YouTubeService::uploadVideo()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 36
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 7
Bugs 0 Features 1
Metric Value
c 7
b 0
f 1
dl 0
loc 36
ccs 11
cts 11
cp 1
rs 8.8571
cc 2
eloc 12
nc 2
nop 2
crap 2
1
<?php
2
3
namespace Craft;
4
5
/**
6
 * YouTube Upload Service.
7
 *
8
 * Upload video assets to YouTube.
9
 *
10
 * @author    Bob Olde Hampsink <[email protected]>
11
 * @copyright Copyright (c) 2015, Itmundi
12
 * @license   MIT
13
 *
14
 * @link      http://github.com/boboldehampsink
15
 */
16
class YouTubeService extends BaseApplicationComponent
17
{
18
    /**
19
     * Holds the OAuth client.
20
     *
21
     * @var \Google_Client|null
22
     */
23
    protected $client;
24
25
    /**
26
     * Holds the YouTube API.
27
     *
28
     * @var \Google_Service_YouTube|null
29
     */
30
    protected $youtube;
31
32
    /**
33
     * Holds asset existence checks.
34
     *
35
     * @var array
36
     */
37
    protected $exists = array();
38
39
    /**
40
     * Holds cached asset locations.
41
     *
42
     * @var array
43
     */
44
    protected $assets = array();
45
46
    /**
47
     * Holds cached file hashes.
48
     *
49
     * @var array
50
     */
51
    protected $hashes = array();
52
53
    /**
54
     * Upload and process the result.
55
     *
56
     * @param BaseElementModel $element
57
     * @param AssetFileModel   $asset
58
     * @param string           $handle
59
     *
60
     * @return bool
61
     */
62 6
    public function process(BaseElementModel $element, AssetFileModel $asset, $handle)
63
    {
64
        // Check if we have this asset already or upload to YouTube
65 6
        if (!($youTubeId = $this->exists($asset))) {
66
            try {
67 6
                $youTubeId = $this->assemble($asset);
68 5
            } catch (Exception $e) {
69 5
                return $e->getMessage();
70
            }
71
        }
72
73
        // Get current video's
74 1
        $content = $element->getContent()->getAttribute($handle);
75
76
        // Make sure content's an array
77 1
        $content = is_array($content) ? $content : array();
78
79
        // Remove this asset's id from the content
80 1
        unset($content[array_search($asset->id, $content)]);
81
82
        // Add video to (existing) content
83 1
        $element->getContent()->$handle = array_merge($content, array($youTubeId));
84
85
        // Save the content without validation
86 1
        craft()->content->saveContent($element, false);
87
88
        // All went well
89 1
        return true;
90
    }
91
92
    /**
93
     * Check if this asset file already exists.
94
     *
95
     * @param AssetFileModel $asset
96
     *
97
     * @return string|bool
98
     */
99
    protected function exists(AssetFileModel $asset)
100
    {
101
        // Check if we have this exist cached already
102
        if (!isset($this->exists[$asset->id])) {
103
            $hash = $this->getAssetFileHash($asset);
104
105
            // Look up in db
106
            $record = YouTube_HashesRecord::model()->findByAttributes(array(
107
                'hash' => $hash,
108
            ));
109
110
            // Get YouTube ID
111
            $this->exists[$asset->id] = $record ? $record->youtubeId : false;
112
        }
113
114
        return $this->exists[$asset->id];
115
    }
116
117
    /**
118
     * Send video's to YouTube.
119
     *
120
     * @param AssetFileModel $asset
121
     *
122
     * @return string|bool
123
     *
124
     * @throws Exception
125
     */
126 6
    protected function assemble(AssetFileModel $asset)
127
    {
128
        // Autenticate first
129 6
        $this->authenticate();
130
131
        try {
132
            // Create YouTube Video snippet
133 6
            $snippet = $this->createVideoSnippet($asset);
134
135
            // Set the YouTube Video's status
136 6
            $status = $this->setVideoStatus();
137
138
            // Create a new video resource
139 6
            $video = $this->createVideoResource($snippet, $status);
140
141
            // Now upload the resource and get the status
142 6
            $status = $this->uploadVideo($asset, $video);
143
144
        // Catch exceptions if we fail and rethrow
145 4
        } catch (\Google_Service_Exception $e) {
146 1
            throw new Exception(Craft::t('A service error occurred: {error}', array('error' => $e->getMessage())));
147 3
        } catch (\Google_Exception $e) {
148 1
            throw new Exception(Craft::t('A client error occurred: {error}', array('error' => $e->getMessage())));
149 2
        } catch (\Exception $e) {
150 2
            throw new Exception(Craft::t('An unknown error occurred: {error}', array('error' => $e->getMessage())));
151
        }
152
153
        // Validate status
154 2
        if ($status instanceof \Google_Service_YouTube_Video) {
155
            // Save hash
156 1
            $this->saveHash($asset, $status->id);
157
158
            // Return YouTube ID
159 1
            return $status->id;
160
        }
161
162
        // Or die
163 1
        throw new Exception(Craft::t('Unable to communicate with the YouTube API client.'));
164
    }
165
166
    /**
167
     * Authenticate with YouTube.
168
     */
169 6
    protected function authenticate()
170
    {
171
        // Get token
172 6
        $token = craft()->youTube_oauth->getToken();
173
174
        // Make token compatible with Google API
175 6
        $json = JsonHelper::encode(array(
176 6
            'access_token'  => $token->accessToken,
177 6
            'refresh_token' => $token->refreshToken,
178 6
            'expires_in'    => $token->endOfLife,
179 6
            'created'       => time(),
180
        ));
181
182
        // Set up a Google Client
183 6
        $this->client = new \Google_Client();
184 6
        $this->client->setAccessToken($json);
185
186
        // Define an object that will be used to make all API requests.
187 6
        $this->youtube = new \Google_Service_YouTube($this->client);
188 6
    }
189
190
    /**
191
     * Create a snippet with title, description, tags and category ID
192
     * Create an asset resource and set its snippet metadata and type.
193
     *
194
     * @param AssetFileModel $asset
195
     *
196
     * @return \Google_Service_YouTube_VideoSnippet
197
     */
198 6
    protected function createVideoSnippet(AssetFileModel $asset)
199
    {
200 6
        $snippet = new \Google_Service_YouTube_VideoSnippet();
201 6
        $snippet->setTitle((string) $asset);
202
203 6
        return $snippet;
204
    }
205
206
    /**
207
     * Set the video's status to "public". Valid statuses are "public",
208
     * "private" and "unlisted".
209
     *
210
     * @return \Google_Service_YouTube_VideoStatus
211
     */
212 6
    protected function setVideoStatus()
213
    {
214 6
        $status = new \Google_Service_YouTube_VideoStatus();
215 6
        $status->privacyStatus = 'unlisted';
216
217 6
        return $status;
218
    }
219
220
    /**
221
     * Associate the snippet and status objects with a new video resource.
222
     *
223
     * @param \Google_Service_YouTube_VideoSnippet $snippet
224
     * @param \Google_Service_YouTube_VideoStatus  $status
225
     *
226
     * @return \Google_Service_YouTube_Video
227
     */
228 6
    protected function createVideoResource(\Google_Service_YouTube_VideoSnippet $snippet, \Google_Service_YouTube_VideoStatus $status)
229
    {
230 6
        $video = new \Google_Service_YouTube_Video();
231 6
        $video->setSnippet($snippet);
232 6
        $video->setStatus($status);
233
234 6
        return $video;
235
    }
236
237
    /**
238
     * Create a resumable video upload to YouTube.
239
     *
240
     * @param AssetFileModel                $asset
241
     * @param \Google_Service_YouTube_Video $video
242
     *
243
     * @throws Exception
244
     *
245
     * @return bool|string
246
     */
247 6
    protected function uploadVideo(AssetFileModel $asset, \Google_Service_YouTube_Video $video)
248
    {
249
        // Get file by asset
250 6
        $file = $this->getAssetFile($asset);
251
252
        // Specify the size of each chunk of data, in bytes. Set a higher value for
253
        // reliable connection as fewer chunks lead to faster uploads. Set a lower
254
        // value for better recovery on less reliable connections.
255 6
        $chunkSizeBytes = 1 * 1024 * 1024;
256
257
        // Verify the client
258 6
        if ($this->client instanceof \Google_Client) {
259
            // Setting the defer flag to true tells the client to return a request which can be called
260
            // with ->execute(); instead of making the API call immediately.
261 6
            $this->client->setDefer(true);
262
263
            // Create a request for the API's videos.insert method to create and upload the video.
264 6
            $insertRequest = $this->youtube->videos->insert('status,snippet', $video);
265
266
            // Create a MediaFileUpload object for resumable uploads.
267 6
            $media = new \Google_Http_MediaFileUpload($this->client, $insertRequest, 'video/*', null, true, $chunkSizeBytes);
268 6
            $media->setFileSize(IOHelper::getFileSize($file));
269
270
            // Read the media file and upload it chunk by chunk.
271 6
            $status = $this->uploadChunks($file, $media, $chunkSizeBytes);
272
273
            // If you want to make other calls after the file upload, set setDefer back to false
274 2
            $this->client->setDefer(false);
275
276
            // Return the status
277 2
            return $status;
278
        }
279
280
        // Or die
281
        throw new Exception(Craft::t('Unable to authenticate the YouTube API client'));
282
    }
283
284
    /**
285
     * Upload file in chunks.
286
     *
287
     * @param string                       $file
288
     * @param \Google_Http_MediaFileUpload $media
289
     * @param int                          $chunkSizeBytes
290
     *
291
     * @return bool|string
292
     */
293
    protected function uploadChunks($file, \Google_Http_MediaFileUpload $media, $chunkSizeBytes)
294
    {
295
        // Upload the various chunks. $status will be false until the process is complete.
296
        $status = false;
297
        $handle = fopen($file, 'rb');
298
        while (!$status && !feof($handle)) {
299
            $chunk = $this->readVideoChunk($handle, $chunkSizeBytes);
300
            $status = $media->nextChunk($chunk);
301
        }
302
303
        // The final value of $status will be the data from the API for the object
304
        // that has been uploaded.
305
        $result = false;
306
        if ($status != false) {
307
            $result = $status;
308
        }
309
310
        fclose($handle);
311
312
        // Remove the local asset file
313
        IOHelper::deleteFile($file);
314
315
        // Return YouTube ID or false
316
        return $result;
317
    }
318
319
    /**
320
     * fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file.
321
     *
322
     * @param resource $handle
323
     * @param int      $chunkSize
324
     *
325
     * @return string
326
     */
327
    protected function readVideoChunk($handle, $chunkSize)
328
    {
329
        $byteCount = 0;
330
        $giantChunk = '';
331
        while (!feof($handle)) {
332
            $chunk = fread($handle, 8192);
333
            $byteCount += strlen($chunk);
334
            $giantChunk .= $chunk;
335
            if ($byteCount >= $chunkSize) {
336
                return $giantChunk;
337
            }
338
        }
339
340
        return $giantChunk;
341
    }
342
343
    /**
344
     * Save asset hash.
345
     *
346
     * @param AssetFileModel $asset
347
     * @param string         $youtubeId
348
     */
349
    protected function saveHash(AssetFileModel $asset, $youtubeId)
350
    {
351
        // Check if its new
352
        if (!$this->exists($asset)) {
353
            // Get asset file hash
354
            $hash = $this->getAssetFileHash($asset);
355
356
            // Save to db
357
            $record = new YouTube_HashesRecord();
358
            $record->youtubeId = $youtubeId;
359
            $record->hash = $hash;
360
            $record->save();
361
        }
362
    }
363
364
    /**
365
     * Gets a file by its asset.
366
     *
367
     * @param AssetFileModel $asset
368
     *
369
     * @return string
370
     */
371 6
    protected function getAssetFile(AssetFileModel $asset)
372
    {
373
        // Check if we have this filenname cached already
374 6
        if (!isset($this->assets[$asset->id])) {
375
            // Get asset source
376 6
            $source = $asset->getSource();
377
378
            // Get asset source type
379 6
            $sourceType = $source->getSourceType();
380
381
            // Get asset file
382 6
            $this->assets[$asset->id] = $sourceType->getLocalCopy($asset);
383
        }
384
385 6
        return $this->assets[$asset->id];
386
    }
387
388
    /**
389
     * Get file hash.
390
     *
391
     * @param AssetFileModel $asset
392
     *
393
     * @return string
394
     */
395
    protected function getAssetFileHash(AssetFileModel $asset)
396
    {
397
        // Check if we have this hash cached already
398
        if (!isset($this->hashes[$asset->id])) {
399
            // Get asset file
400
            $file = $this->getAssetFile($asset);
401
402
            // Calculate md5 hash of file
403
            $this->hashes[$asset->id] = md5_file($file);
404
        }
405
406
        return $this->hashes[$asset->id];
407
    }
408
}
409