17

The aim is to create a Watch Later button using the YouTube API. When a user clicks the button, the video is saved into the user's Watch Later playlist. Similar to how it works when you implement a Facebook like button on your own site.

So far, we have two official entries in the API documentations:

And we have a number of PHP code samples on the API docs.

How can this be achieved using either PHP or/and javascript?

Henrik Petterson
  • 6,862
  • 20
  • 71
  • 155

3 Answers3

20

The Watch Later playlist is the playlist with id WL. You can add a video to this playlist the same way as the other Youtube playlists.

You will first need to go to your Google developer console :

  • enable Youtube Data API for your project
  • generate an Oauth Client ID

Then you can use the code below which will authenticate, retrieve an access token with https://www.googleapis.com/auth/youtube scope and then add a video to your watch later playlist.

For the following Javascript & PHP samples, when a button is pressed, it logs-in the user if not already authenticated and add the video to the watch later playlist of the authenticated user.


Javascript

This is based on api-samples provided by Google here.

Here is a live demo with the source code (as below)

Here is a fiddle. Replace your client id and add as Authorized JavaScript origins in developer console : https://fiddle.jshell.net

index.html :

<!doctype html>
<html>

<head>
    <title>Add to Watch Later playlist</title>
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
    <style>
    .btn-tech {
        color: #2c3e50;
        border: solid 2px #2c3e50;
        background: transparent;
        transition: all 0.3s ease-in-out;
        margin: 20px;
        border-radius: 20% 20% 20% 20%;
    }

    .btn-tech:hover,
    .btn-tech:active,
    .btn-tech.active {
        color: #FFFFFF;
        background: #2c3e50;
        cursor: pointer;
    }
    </style>
</head>

<body>
    <div id="watch_later">
        <div id="buttons">
            <label>Enter Video ID you want to add to Watch Later playlist :
                <input id="video-id" value='T4ZE2KtoFzs' type="text" />
            </label>
        </div>
        <div class="like">
            <a id="fb-link">
                <span class="btn-tech fa-stack fa-3x">
                  <i class="fa fa-thumbs-up fa-stack-1x"></i>
                </span>
            </a>
        </div>
        <div id="playlist-container">
            <span id="status"></span>
        </div>
        <p>
            <a href="https://www.youtube.com/playlist?list=WL">check your watch later playlist</a>
        </p>
    </div>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
    <script>
    var OAUTH2_CLIENT_ID = '28993181493-c9o6hdll3di0ssvebfd4atf13edqfu9g.apps.googleusercontent.com';
    var OAUTH2_SCOPES = [
        'https://www.googleapis.com/auth/youtube'
    ];
    var init = false;
    googleApiClientReady = function() {
        gapi.auth.init(function() {
            window.setTimeout(checkAuth, 1);
        });
    }

    function checkAuth() {
        gapi.auth.authorize({
            client_id: OAUTH2_CLIENT_ID,
            scope: OAUTH2_SCOPES,
            immediate: true
        }, handleAuthResult);
    }
    // Handle the result of a gapi.auth.authorize() call.
    function handleAuthResult(authResult) {

        $('.like').off('click');
        $('.like').click(function(e) {
            if (authResult && !authResult.error) {
                addVideoToPlaylist();
            } else {
                init = true;
                gapi.auth.authorize({
                    client_id: OAUTH2_CLIENT_ID,
                    scope: OAUTH2_SCOPES,
                    immediate: false
                }, handleAuthResult);
            }
            return false;
        });

        if (authResult && !authResult.error) {
            // Authorization was successful. Hide authorization prompts and show
            // content that should be visible after authorization succeeds.
            $('.pre-auth').hide();
            $('.post-auth').show();
            loadAPIClientInterfaces();

            $('#add_to_wl').click(function(e) {
                addVideoToPlaylist();
            });
        }
    }

    function loadAPIClientInterfaces() {
        gapi.client.load('youtube', 'v3', function() {
            if (init) {
                init = false;
                addVideoToPlaylist();
            }
        });
    }
    // Add a video ID specified in the form to the playlist.
    function addVideoToPlaylist() {
        addToPlaylist($('#video-id').val());
    }
    // Add a video to a playlist. The "startPos" and "endPos" values let you
    // start and stop the video at specific times when the video is played as
    // part of the playlist. However, these values are not set in this example.
    function addToPlaylist(id, startPos, endPos) {
        var details = {
            videoId: id,
            kind: 'youtube#video'
        }
        if (startPos != undefined) {
            details['startAt'] = startPos;
        }
        if (endPos != undefined) {
            details['endAt'] = endPos;
        }
        var request = gapi.client.youtube.playlistItems.insert({
            part: 'snippet',
            resource: {
                snippet: {
                    playlistId: "WL",
                    resourceId: details
                }
            }
        });
        request.execute(function(response) {
            console.log(response);
            if (!response.code) {
                $('#status').html('<pre>Succesfully added the video : ' + JSON.stringify(response.result) + '</pre>');
            } else if (response.code == 409) {
                $('#status').html('<p>Conflict : this video is already on your Watch Later playlist</p>');
            } else if (response.code == 404) {
                $('#status').html('<p>Not Found : this video hasnt been found</p>');
            } else {
                $('#status').html('<p>Error : code ' + response.code + '</p>');
            }
        });
    }
    </script>
    <script src="https://apis.google.com/js/client.js?onload=googleApiClientReady"></script>
</body>

</html>

Replace OAUTH2_CLIENT_ID with your own client ID

In the API response, I check the following status code :

  • 409 : the video already on playlist
  • 404 : the video isn't found

PHP

Based on google-api php sample :

  • install composer : see instructions
  • install google-api client :

    composer require google/apiclient:~2.0
    

The php script watchlater.php :

<?php
/**
 * Library Requirements
 *
 * 1. Install composer (https://getcomposer.org)
 * 2. On the command line, change to this directory (api-samples/php)
 * 3. Require the google/apiclient library
 *    $ composer require google/apiclient:~2.0
 */
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
  throw new \Exception('please run "composer require google/apiclient:~2.0" in "' . __DIR__ .'"');
}
require_once __DIR__ . '/vendor/autoload.php';
session_start();

$response = "";

/*
 * You can acquire an OAuth 2.0 client ID and client secret from the
 * {{ Google Cloud Console }} <{{ https://cloud.google.com/console }}>
 * For more information about using OAuth 2.0 to access Google APIs, please see:
 * <https://developers.google.com/youtube/v3/guides/authentication>
 * Please ensure that you have enabled the YouTube Data API for your project.
 */
$OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID';
$OAUTH2_CLIENT_SECRET = 'YOUR_CLIENT_SECRET';

$client = new Google_Client();
$client->setClientId($OAUTH2_CLIENT_ID);
$client->setClientSecret($OAUTH2_CLIENT_SECRET);
$client->setScopes('https://www.googleapis.com/auth/youtube');

$redirect = filter_var('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'],
    FILTER_SANITIZE_URL);

$client->setRedirectUri($redirect);
// Define an object that will be used to make all API requests.
$youtube = new Google_Service_YouTube($client);
// Check if an auth token exists for the required scopes
$tokenSessionKey = 'token-' . $client->prepareScopes();
if (isset($_GET['code'])) {
  if (strval($_SESSION['state']) !== strval($_GET['state'])) {
    die('The session state did not match.');
  }
  $client->authenticate($_GET['code']);
  $_SESSION[$tokenSessionKey] = $client->getAccessToken();
  header('Location: ' . $redirect);
}
if (isset($_SESSION[$tokenSessionKey])) {
  $client->setAccessToken($_SESSION[$tokenSessionKey]);
}
// Check to ensure that the access token was successfully acquired.

if ($client->getAccessToken()) {
  try {

    $videoId = "";

    if (isset($_GET['video'])){
    $videoId = $_GET['video'];
    }
    else if(isset($_SESSION['video'])){
      $videoId = $_SESSION['video'];
    }

    if(isset($videoId) && !isset($_GET['state'])) {

      file_put_contents('php://stderr', print_r("adding video to watch later playlist " . $videoId . "\n", TRUE));

      $playlistId = "WL";
      // 5. Add a video to the playlist. First, define the resource being added
      // to the playlist by setting its video ID and kind.
      $resourceId = new Google_Service_YouTube_ResourceId();
      $resourceId->setVideoId($videoId);
      $resourceId->setKind('youtube#video');

      // Then define a snippet for the playlist item. Set the playlist item's
      // title if you want to display a different value than the title of the
      // video being added. Add the resource ID and the playlist ID retrieved
      // in step 4 to the snippet as well.
      $playlistItemSnippet = new Google_Service_YouTube_PlaylistItemSnippet();
      $playlistItemSnippet->setTitle('First video in the test playlist');
      $playlistItemSnippet->setPlaylistId($playlistId);
      $playlistItemSnippet->setResourceId($resourceId);
      // Finally, create a playlistItem resource and add the snippet to the
      // resource, then call the playlistItems.insert method to add the playlist
      // item.
      $playlistItem = new Google_Service_YouTube_PlaylistItem();
      $playlistItem->setSnippet($playlistItemSnippet);

      $playlistItemResponse = $youtube->playlistItems->insert(
          'snippet,contentDetails', $playlistItem, array());

      $response = json_encode($playlistItem);

      $_SESSION['video'] = "";
  }
  else{
    file_put_contents('php://stderr', print_r("no video was specified", TRUE));
  }

  } catch (Google_Service_Exception $e) {
    $response = htmlspecialchars($e->getMessage());
  } catch (Google_Exception $e) {
    $response = htmlspecialchars($e->getMessage());
  }
  $_SESSION[$tokenSessionKey] = $client->getAccessToken();
} else {

  if(isset($_GET['video'])){

    $_SESSION["video"] = $_GET['video'];

    // If the user hasn't authorized the app, initiate the OAuth flow
    $state = mt_rand();
    $client->setState($state);
    $_SESSION['state'] = $state;
    $authUrl = $client->createAuthUrl();
    header('Location: ' . $authUrl);
  }
}
?>

<!doctype html>
<html>
<head>
 <title>Add to Watch Later playlist</title>
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
    <style>
    .btn-tech {
        color: #2c3e50;
        border: solid 2px #2c3e50;
        background: transparent;
        transition: all 0.3s ease-in-out;
        margin: 20px;
        border-radius: 20% 20% 20% 20%;
    }

    .btn-tech:hover,
    .btn-tech:active,
    .btn-tech.active {
        color: #FFFFFF;
        background: #2c3e50;
        cursor: pointer;
    }
    </style>
</head>
<body>
   <div id="watch_later">
        <form id="form" action="watchlater.php"">

          <label>Enter Video ID you want to add to Watch Later playlist :
                <input id="video-id" name="video" value='T4ZE2KtoFzs' type="text" />
          </label>

          <div>
              <span class="btn-tech fa-stack fa-3x" onclick="javascript:document.getElementById('form').submit();">
                <i class="fa fa-thumbs-up fa-stack-1x"></i>
              </span>
          </div>
        </form>
        <div id="playlist-container">
          <?php echo $response ?>
        </div>
        <p>
            <a href="https://www.youtube.com/playlist?list=WL">check your watch later playlist</a>
        </p>
    </div>
</body>
</html>

Replace $OAUTH2_CLIENT_ID and $OAUTH2_CLIENT_SECRET with their respective value. Also, you have to set a redirect_uri in google console, here it will be http://localhost/watchlater.php

In the PHP version, you can see that I store video id in $_SESSION["video"] to be able to add it immediately when google authentication redirect to watchlater.php


Here is a screen of google console Oauth Client ID for this project (valid for the Javascript & PHP version above) :

enter image description here

Note that :

  • for the Javascript version : you need CLIENT_ID and set Javascript Origin
  • for the PHP version : you need CLIENT_ID, CLIENT_SECRET, set Javascript Origin and set Redirect URI

Note for testing, I noticed that it can take some time to delete the video when doing it manually if you want to re-add it again

Bertrand Martel
  • 42,756
  • 16
  • 135
  • 159
  • 1
    Thank you very much for the response. I'm testing this on jsbin as we speak. Is there any way you could update this answer with an example of a single button (similar to a Facebook Like button) where when you click it, it adds a specific YouTube video to the Watch Later playlist. And if the user is logged out, then it loads up a popup to allow the user to login; and then adds it to their playlist. My problem so far is to do the login process fluently. – Henrik Petterson Mar 02 '17 at 18:35
  • Quality answer! I would like to see this as well; a FB like style feature would be outstanding. – Gary Woods Mar 02 '17 at 18:54
  • 1
    I've updated my post with your requirements, see a live demo [here](https://bertrandmartel.github.io/youtube-watch-later/). Note that when the user login the first time, the `videoId` in the input text is added to the playlist just after that, and then can add more video without the login popup – Bertrand Martel Mar 02 '17 at 19:07
  • Please see the [updated version](https://bertrandmartel.github.io/youtube-watch-later/) which fixes re-login issue – Bertrand Martel Mar 02 '17 at 19:35
  • Eternal thanks for this. When you obtained OAuth credentials, what did you select for the "Authorized JavaScript origins" URL? Are you required to add the **full** URL of where the script is being executed or can you simply add the base domain, like `http://example.com`? And what did you set for `Authorized redirect URIs`? Are you basically required to fill in these two fields to make this work? – Henrik Petterson Mar 03 '17 at 11:51
  • 2
    `Authorized redirect URIs` is not needed as you don't want to redirect the user to another page when the authentication is succesfull. You have to specify one `Authorized JavaScript origins` with the base URI as in my case `https://bertrandmartel.github.io` for [this demo](https://bertrandmartel.github.io/youtube-watch-later/). For the fiddle link above you can use `https://fiddle.jshell.net` – Bertrand Martel Mar 03 '17 at 12:28
  • I've added a PHP version and added some clarifications and an example of `Oauth Client ID` config in Google Console – Bertrand Martel Mar 04 '17 at 16:42
  • To say that you have taken the extra step to provide an outstanding answer would certainly be an understatement. I am not performing local tests with this and so far, I've only tested the JS solution and it is working perfectly. I have a question. I have added the JS part of the code into .js file. I may want to add the "watch later" button dynamically after pageload. I tried to wrap the code in `jQuery( document ).ready(function() {...` but it didn't seem to work. Do you know why? – Henrik Petterson Mar 07 '17 at 12:37
  • If you include your file.js just before `

    ` as in the linked fiddle you shouldn't have load problem. When page is loaded (`onload`) it calls `googleApiClientReady` and all the JS code. So you don't need to wait for document ready, it will already be ready when it's loaded. You can load the button in the `handleAuthResult` a the level of `$('.like').click` for instance

    – Bertrand Martel Mar 07 '17 at 13:39
  • I will accept this answer in a bit. Just leaving it as it is for more people to discover this excellent answer. I have a concept question. Is it possible to create a "Watch List" button that could be added to any domain? To elaborate, currently, the code is restricted to only one domain (what you add in `Authorized JavaScript origins`), is it possible to make it work with any domain *without* customizing the code for each site? – Henrik Petterson Mar 09 '17 at 11:22
  • You don't need to customize the code, just add Authorized Javascript origin matching your domain to your Oauth token in developer console. There are limitations though cause you can't use wildcards for subdomain for instance, there are some [workaround](http://stackoverflow.com/questions/24166191/google-api-authorized-javascript-origins) for this, not sure if that's your issue. – Bertrand Martel Mar 09 '17 at 12:15
  • Sorry if I didn't explain this properly. To elaborate, is it possible to lift the restriction of the set domains in `Authorized Javascript origin` so that I can create a plugin that other people can download and use on their own site (domain) *without* needing to create their own OAuth, Data API key, and so on. – Henrik Petterson Mar 09 '17 at 12:58
  • You can still embed your code in an iframe as proposed [here](http://stackoverflow.com/questions/24166191/google-api-authorized-javascript-origins) and let people embed your iframe so it always load on your domain – Bertrand Martel Mar 09 '17 at 15:51
  • 1
    Ah, you got it my friend. Thank you again for taking your time to post this very complete answer. – Henrik Petterson Mar 10 '17 at 08:28
  • *Great* answer! I've put it to practice and have one issue. Problem is that I have the button displayed on multiple areas on the same page. So need the action to be confined to the very button the user clicks on (normally by using `this` variable). I add the video ID as a data attribute to the button div. I moved your `addVideoToPlaylist()` function and changed it to it uses this data attribute. Problem is `request.execute(function(response)` part where I want to show results within the clicked *button*, like `$(this).text('Video added');` - how can I do that? See: http://pastebin.com/fq2TXfvv – Gary Woods Mar 10 '17 at 12:53
  • The code is quite self-exclamatory. My aim is to use `$(this).text('Video added');` instead of `jQuery('.ma_youtube_watch_later_text').text('Video added');` -- any way I could use your wisdom on how to achieve this? Currently, when a user clicks on button, all other buttons on the page shows the `.text('Video added')` response because it's targeting a class and not `this`. – Gary Woods Mar 10 '17 at 12:56
  • Why not using something like `$('#myButton').text('Video added')`. I haven't tested it but maybe you can use `var self = this;` in `addVideoToPlaylist()` and in you request callback use `$(self).text('some text')`. If you could make a jsfiddle so we can have the same template – Bertrand Martel Mar 10 '17 at 14:22
  • 1
    Using `$('#myButton').text('Video added')` will only affect *one* button on the page and won't work if you have several. I have created this jsFiddle: https://jsfiddle.net/q3L8n347/ - the aim is to change the response part so it only triggers the clicked button, and not **all** buttons on the page (by targeting just a class). So instead of `$('.ma_youtube_watch_later_text').text('Video added');` we need something like `$(this).find('ma_youtube_watch_later_text').text('Video added');`... Did I explain this good? – Gary Woods Mar 11 '17 at 08:43
  • 2
    You can store the reference to the clicked button as a global variable or pass it over your callback function. [Here](https://jsfiddle.net/bmartel/czw2kj15/) is fiddle for a solution with global variable – Bertrand Martel Mar 11 '17 at 14:33
  • Exactly what I needed. This site needs a PayPal done button to its contributors. – Gary Woods Mar 13 '17 at 15:49
  • @BertrandMartel Follow up question that may be of interest to others as well. Is it easy to adjust the code so it targets the user's *Favorite* playlist instead of the *Watch Later* playlist. I would imagine that we simply need to adjust the `playlistId: "WL"`, correct? – Henrik Petterson Mar 29 '17 at 11:50
  • @BertrandMartel If I want to target the *Favorites* playlist, do you know if going with ID `FL` is enough? Like this: `playlistId: "FL"`? If it is more complex than this, I wouldn't mind to open a new question but thought I would run it by you (the author of this code) first. – Henrik Petterson Apr 03 '17 at 12:04
  • 1
    You can replace the "WL" with the playlist ID you want. It must be a valid playlist ID. You can find them with [channel list endpoint](https://developers.google.com/youtube/v3/docs/channels/list) with `mine: true` and `part: contentDetails`. You will get the list of playlists in `relatedPlaylists` including `likes`, `favorites`, `uploads`, `watch later` and `watch history`. Check [this sample request](https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.channels.list?part=contentDetails&mine=true) – Bertrand Martel Apr 03 '17 at 12:31
  • @BertrandMartel Thank you for the outline, although i have decided to open [a new question](http://stackoverflow.com/q/43229335/1185126) regarding this as I believe more users like myself will find this interesting. Essentially, the question is directed at you. I will also open a bounty (in 2 days when it's eligible). I decided to add the latest code you added in the comments because it works with multiple buttons on the same page by passing the `this` variable. Would be great to have you expertise on that question thread if you have the time and interest. – Henrik Petterson Apr 05 '17 at 11:07
  • @BertrandMartel I've set a bounty on the *favorite* one in case you are interested: http://stackoverflow.com/q/43229335/1185126 – Henrik Petterson Apr 13 '17 at 11:33
  • I was just redacting an answer for this, will answer in a few minutes – Bertrand Martel Apr 13 '17 at 11:36
1

Retrieving "Watch Later" playlist:

As pointed out by others, YouTube no longer allow this

Adding a video to that same list is rather straight forward though:

curl --request POST \
  'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&key=[YOUR_API_KEY]' \
  --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data '{"snippet":{"playlistId":"WL","position":0,"resourceId":{"kind":"youtube#video","videoId":"M7FIvfx5J10"}}}' \
  --compressed

You can find Javascript code snippet and more here: https://developers.google.com/youtube/v3/docs/playlistItems/insert

enter image description here

ericn
  • 12,476
  • 16
  • 84
  • 127
0

I don't know if this will help you much since I never worked with Youtube's API. But have a look at this answer on how to get the Watch Later playlist using Youtube's API v3.

Then you can take a look this, from Youtube docs, for inserting an element into a given playlist.

ANYWAY I'm not sure this is possible anymore (you can give it a try) since September 15, 2016 revision:

Referring to point 2.2 in the list:

The channel resource's contentDetails.relatedPlaylists.watchHistory and contentDetails.relatedPlaylists.watchLater properties now contain values of HL and WL, respectively, for all channels.

To be clear, these properties are only visible to an authorized user retrieving data about the user's own channel. The properties always contain the values HL and WL, even for an authorized user retrieving data about the user's own channel. Thus, the watch history and watch later playlist IDs cannot be retrieved via the API.

In addition, requests to retrieve playlist details (playlists.list) or playlist items (playlistItems.list) for a channel's watch history or watch later playlist now return empty lists. This behavior is true for the new values, HL and WL, as well as for any watch history or watch later playlist IDs that your API Client may have already stored.

Community
  • 1
  • 1
Condorcho
  • 503
  • 4
  • 12