Package Torello.REST

Class YouTube


  • public class YouTube
    extends java.lang.Object
    YouTube - Documentation.

    This class helps use the YouTube REST API. This class requires the standard javax JAR File for JSON Parsing - or else this class will not work. Copies of this JAR are available just about anywhere on the internet. Here is a link to the downloadable version of the JAR that I use:

    http://developer.torello.directory/ralphsReference/jars/javax.json-1.1.jar

    Static (Functional) API: The methods in this class are all (100%) defined with the Java Key-Word / Key-Concept 'static'. Furthermore, there is no way to obtain an instance of this class, because there are no public (nor private) constructors. Java's Spring-Boot, MVC feature is *not* utilized because it flies directly in the face of the light-weight data-classes philosophy. This has many advantages over the rather ornate Component Annotations (@Component, @Service, @AutoWired, etc... 'Java Beans') syntax:

    • The methods here use the key-word 'static' which means (by implication) that there is no internal-state. Without any 'internal state' there is no need for constructors in the first place! (This is often the complaint by MVC Programmers).
    • A 'Static' (Functional-Programming) API expects to use fewer data-classes, and light-weight data-classes, making it easier to understand and to program.
    • The Vectorized HTML data-model allows more user-control over HTML parse, search, update & scrape. Also, memory management, memory leakage, and the Java Garbage Collector ought to be intelligible through the 'reuse' of the standard JDK class Vector for storing HTML Web-Page data.

    The power that object-oriented programming extends to a user is (mostly) limited to data-representation. Thinking of "Services" as "Objects" (Spring-MVC, 'Java Beans') is somewhat 'over-applying' the Object Oriented Programming Model. Like most classes in the Java-HTML JAR Library, this class backtracks to a more C-Styled Functional Programming Model (no Objects) - by re-using (quite profusely) the key-word static with all of its methods, and by sticking to Java's well-understood class Vector

    Static Fields: The methods in this class do not create any internal state that is maintained - however there are a few private & static fields defined. These fields are instantiated only once during the Class Loader phase (and only if this class shall be used), and serve as data 'lookup' fields (static constants). View this class' source-code in the link provided below to see internally used data.

    The static, final fields defined in this class are instances of java.text.SimpleDateFormat, used to parse and format dates returned by YouTube.



    • Field Summary

      Fields 
      Modifier and Type Field
      protected static java.text.SimpleDateFormat sdfIn
      protected static java.text.SimpleDateFormat sdfOut
    • Method Summary

      All Methods Static Methods Concrete Methods 
      Modifier and Type Method
      static Ret2<YouTube.VideoSummary[],
           ​String[]>
      listVideos​(String channelID, String API_KEY, Appendable a)
      protected static Object parseJSONVideoResult​(javax.json.JsonObject result, int i, Appendable a)
      • Methods inherited from class java.lang.Object

        clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    • Field Detail

      • sdfIn

        protected static final java.text.SimpleDateFormat sdfIn
        Used internally to parse YouTube JSON returned dates.
        Code:
        Exact Field Declaration Expression:
        1
        protected static final SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        
      • sdfOut

        protected static final java.text.SimpleDateFormat sdfOut
        Used internally to format dates for output printing.
        Code:
        Exact Field Declaration Expression:
        1
        protected static final SimpleDateFormat sdfOut = new SimpleDateFormat("EEE, MMMMM d, yyyy");
        
    • Method Detail

      • listVideos

        public static Ret2<YouTube.VideoSummary[],​java.lang.String[]> listVideos​
                    (java.lang.String channelID,
                     java.lang.String API_KEY,
                     java.lang.Appendable a)
                throws java.io.IOException
        
        Below is a sample search-result JSON Object returned from the YouTube REST API. This example is for a video from that silly "card game" called Magic the Gathering. There is an individual named "Christopher" who lives in Japan and makes videos about his adventures playing the Video Game version of that ridiculous card game, which I have never played, so how could I know about it?

        Java Script Object Notation (JSON):
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
         {
              "kind": "youtube#searchResult",
              "etag": "fnontSLtWwQqXTiHDlEmuJmrAHg",
              "id": {
                  "kind": "youtube#video",
                  "videoId": "a8GcrDfBOx4"
              },
              "snippet": {
                  "publishedAt": "2021-04-26T14:00:00Z",
                  "channelId": "UCi3VPBRXVBcvI0OW1WDHlzw",
                  "title": "TERROR OF MOUNT VELUS, THERE IS NO OTHER WAY! Carlos Ordene&#39;s Membership Deck Standard MTG Arena",
                  "description": "Original: Deck 15 Dragon's Approach (STX) 97 8 Plains (STX) 367 2 Terror of Mount Velus (THB) 295 4 Terror of the Peaks (M21) 164 14 Mountain (STX) 373 4 ...",
                  "thumbnails": {
                      "default": {
                          "url": "https://i.ytimg.com/vi/a8GcrDfBOx4/default.jpg",
                          "width": 120,
                          "height": 90
                          },
                          "medium": {
                          "url": "https://i.ytimg.com/vi/a8GcrDfBOx4/mqdefault.jpg",
                          "width": 320,
                          "height": 180
                      },
                      "high": {
                          "url": "https://i.ytimg.com/vi/a8GcrDfBOx4/hqdefault.jpg",
                          "width": 480,
                          "height": 360
                      }
                  },
                  "channelTitle": "MONO BLACK MAGIC",
                  "liveBroadcastContent": "none",
                  "publishTime": "2021-04-26T14:00:00Z"
              }
         }
         
        
        Parameters:
        channelID - The YouTube Channel whose video-summaries that are being searched.
        a - This may be used to receive text-output message as this function proceeds. This parameter may be null, and if it is it shall be ignored. This parameter expects an implementation of Java's interface java.lang.Appendable which allows for a wide range of options when logging intermediate messages.
        Class or Interface InstanceUse & Purpose
        'System.out'Sends text to the standard-out terminal
        Torello.Java.StorageWriterSends text to System.out, and saves it, internally.
        FileWriter, PrintWriter, StringWriterGeneral purpose java text-output classes
        FileOutputStream, PrintStreamMore general-purpose java text-output classes

        IMPORTANT: The interface Appendable requires that the check exception IOException must be caught when using its append(CharSequence) methods.
        Returns:
        This method will return parsed class VideoSummary objects for each search result that is retrieved from the YouTube REST API. If any search results were not successfully parsed, their raw JSON objects will be converted to String's and returned as a String[] array.

        1. Ret2.a (VideoSummary[])

          The first array shall contain all YouTube Search Results that were succesfully parsed by this method, without any problems.

        2. Ret2.b (String[])

          For any search results that were not successfully parsed, the JSON Object as a java.lang.String will be returned as elements of a String[] array.

        Throws:
        java.io.IOException - This exception may be thrown if there are any errors while communicating with the Google YouTube API, or while writing to the user-provided appendable interface.
        Code:
        Exact Method Body:
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
         if (a != null) a.append("List Videos For Channel-ID: " + channelID + '\n');
        
         // The URL of the JSON REST API for YouTube
         final String API_URL = "https://www.googleapis.com/youtube/v3/search" +
             "?channelId=" + channelID + 
             "&key=" + API_KEY +
             "&part=snippet,id" +
             "&order=date" +
             "&maxResults=50";
                
         // Save the successfully parsed VideoSummary JSON response objects in a
         // Stream<VideoSummary>
         final Stream.Builder<VideoSummary> bSummary = Stream.builder();
        
         // Save the JSON response-objects that could not be parsed as the raw JSON text
         // in a Java Stream<String>
         final Stream.Builder<String> bRawJSON = Stream.builder();
        
         // Results are returned, iteratively, in batches of 50.  This string is provided as
         // a result in the JSON Object, and is passed back to the YouTube API requesting the
         // next 50 results.  The first batch of 50 does not need a token
         String nextPageToken = "";
        
         // *** DEBUG / VIEW-JSON
         // int j=0;
        
         // This out loop iterates throug batches of 50 Video Results.
         while (nextPageToken != null)
         {
             URL url = new URL
                 (API_URL + ((nextPageToken.length() > 0) ? ("&pageToken=" + nextPageToken) : ""));
        
             if (a != null) a.append("Visiting REST-API URL: " + url + '\n');
        
             String      json        = Scrape.scrapePage(url);
             JsonObject  tempObj     = Json.createReader(new StringReader(json)).readObject();
             JsonArray   arr         = tempObj.getJsonArray("items");
        
             // *** DEBUG / VIEW-JSON
             // Torello.Java.FileRW.writeFile(json, "TEMP/youtube" + (j++) + ".json");
        
             // Update the page token.  The next loop iteration will use this string.
             // When all results have been returned, the nextPageToken will be null.
             nextPageToken = tempObj.getString("nextPageToken", null);
        
             if (nextPageToken == null) if (a != null)
                 a.append("JSON String 'nextPageToken' was null, this is the last page.\n");
        
             // Temporary Loop Iteration Number, Used for message-text output
             int i = 1;
        
             for (JsonObject result : arr.getValuesAs(JsonObject.class))
             {
                 Object ret = parseJSONVideoResult(result, i++, a);
        
                 if (ret instanceof VideoSummary)
                     bSummary.accept((VideoSummary) ret);
        
                 else if (ret instanceof String)
                     bRawJSON.accept((String) ret);
             }
         }
        
         // The JavaHTML Library's "Ret2<A, B>" syntax.  Here, two arrays are returned.
         // The VideoSummary[] array contains all successfully-parsed JSON VideoSummary objects
         // The String[] rray contains any/all of the JSON response-objects that couldn't be parsed
        
         return new Ret2<VideoSummary[], String[]>(
        
             // This is just Java's Stream's syntax for converting a Stream<VideoSummary>
             // to an array of VideoSummary[]
             bSummary.build().toArray(VideoSummary[]::new),
        
             // Stream's syntax for converting a Stream<String> to a String[] array
             bRawJSON.build().toArray(String[]::new)
        
         );
        
      • parseJSONVideoResult

        protected static java.lang.Object parseJSONVideoResult​
                    (javax.json.JsonObject result,
                     int i,
                     java.lang.Appendable a)
                throws java.io.IOException
        
        This method is used internally to parse a JSON Video-Search Result
        Parameters:
        result - A JSON Object that needs to contain one Video Search Result Object.
        i - This is the current loop-ieration number.
        a - This is the appendable that was passed to the top-level search method. This parameter may be null, and if it is then informational-text will not be printed by this method. This parameter expects an implementation of Java's interface java.lang.Appendable which allows for a wide range of options when logging intermediate messages.
        Class or Interface InstanceUse & Purpose
        'System.out'Sends text to the standard-out terminal
        Torello.Java.StorageWriterSends text to System.out, and saves it, internally.
        FileWriter, PrintWriter, StringWriterGeneral purpose java text-output classes
        FileOutputStream, PrintStreamMore general-purpose java text-output classes

        IMPORTANT: The interface Appendable requires that the check exception IOException must be caught when using its append(CharSequence) methods.
        Returns:
        This method may return two possible result instance-types:

        1. VideoResult - This shall be returned if the JsonObject result was successfully parsed, without error.

        2. String - If the 'result' did not parse properly, then this JsonObject will be converted into an instance of java.lang.String and returned.

        Throws:
        java.io.IOException
        Code:
        Exact Method Body:
          1
          2
          3
          4
          5
          6
          7
          8
          9
         10
         11
         12
         13
         14
         15
         16
         17
         18
         19
         20
         21
         22
         23
         24
         25
         26
         27
         28
         29
         30
         31
         32
         33
         34
         35
         36
         37
         38
         39
         40
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        122
        123
        124
        125
        126
        127
        128
        129
        130
        131
        132
        133
        134
        135
        136
        137
        138
        139
        140
        141
        142
        143
        144
        145
        146
        147
        148
        149
        150
        151
        152
        153
        154
        155
        156
        157
        158
        159
        160
        161
        162
        163
        164
        165
        166
        167
        168
        169
        170
        171
        172
        173
        174
        175
        176
        177
        178
        179
        180
        181
        182
        183
        184
        185
        186
        187
        188
        189
        190
        191
        192
        193
        194
        195
        196
        197
        198
        199
        200
        201
        202
        203
        204
        205
        206
        207
        208
        209
        210
        211
        212
        213
        214
        215
        216
        217
        218
        219
        220
        221
        222
        223
        224
        225
        226
        227
        228
        229
        230
        231
        232
        233
        234
        235
        236
        237
         final String                ELEM    = "JSON Array Element ";
         final String                SKIP    = "  Skipping this Search Result.\n";
         final String                IGNORE  = "  Ignoring\n";
        
         String                      videoID=null, title=null, eTag=null, description=null;
         Date                        publishedDate=null;
         TreeMap<String, Thumbnail>  thumbnails = new TreeMap<>();
         JsonString                  str;
         JsonObject                  obj;
        
         // ****************************************************************************************
         // "kind": "youtube#searchResult",
         // ****************************************************************************************
        
         // ***************************************************************
         // Check for existence of "kind" string-property
         // ***************************************************************
         str = result.getJsonString("kind");
         if (str == null)
         {
             if (a != null) a.append
                 (ELEM + i + " did not contain a 'kind' Property." + SKIP);
             return result.toString();
         }
        
         // ***************************************************************
         // Check for correct "kind" property-value
         // ***************************************************************
         if (! str.getString().equals("youtube#searchResult"))
         {
             if (a != null) a.append
                 (ELEM + i + " has a 'kind' Property whose value is not 'youtube#searchResult'." + SKIP);
             return result.toString();
         }
        
         // ****************************************************************************************
         // "etag": "fnontSLtWwQqXTiHDlEmuJmrAHg",
         // Check for "etag" property.
         // ****************************************************************************************
        
         str = result.getJsonString("etag");
         if (str == null)
         {
             if (a != null) a.append(ELEM + i + " was missing an 'etag' property." + IGNORE);
         }
         else
             eTag = str.getString().trim();
        
         // ****************************************************************************************
         // "id": {
         //     "kind": "youtube#video",
         //     "videoId": "a8GcrDfBOx4"
         // },
         // ****************************************************************************************
        
         // ***************************************************************
         // Check for for existence of "id" object-property
         // ***************************************************************
         obj = result.getJsonObject("id");
         if (obj == null)
         {
             if (a != null) a.append
                 (ELEM + i + " does not have any 'id' Property." + SKIP);
             return result.toString();
         }
        
         // ***************************************************************
         // "kind": "youtube#video",
         // Check "id" object-property for existence of "kind" string-property
         // ***************************************************************
         // Check "id" object-property for "kind" string-property
         str = obj.getJsonString("kind");
         if (str == null)
         {
             if (a != null) a.append
                 (ELEM + i + " has an 'id' property-object without a 'kind' Property." + SKIP);
             return result.toString();
         }
        
         // ***************************************************************
         // "kind": "youtube#video",
         // Check for correct "kind" string-property value
         // ***************************************************************
         if (! str.getString().equals("youtube#video"))
         {
             if (a != null) a.append(
                 ELEM + i + " has an 'id' property-object that contains a 'kind' Property " +
                 "whose value is not 'youtube#video'." + SKIP
             );
             return result.toString();
         }
        
         // ***************************************************************
         // "videoId": "a8GcrDfBOx4"
         // Get Video-ID
         // ***************************************************************
         str = obj.getJsonString("videoId");
         if (str == null)
         {
             if (a != null) a.append(
                 ELEM + i + " has an 'id' property-object which does not have any 'videoId' " +
                 "Property." + SKIP
             );
             return result.toString();
         }
         else
             videoID = str.getString();
        
         // ****************************************************************************************
         //  "snippet": {
         //      "publishedAt": "2021-04-26T14:00:00Z",
         //      "channelId": "UCi3VPBRXVBcvI0OW1WDHlzw",
         //      "title": "TERROR OF MOUNT VELUS, THERE IS NO OTHER WAY! Carlos Ordene&#39;s Membership Deck Standard MTG Arena",
         //      "description": "Original: Deck 15 Dragon's Approach (STX) 97 8 Plains (STX) 367 2 Terror of Mount Velus (THB) 295 4 Terror of the Peaks (M21) 164 14 Mountain (STX) 373 4 ...",
         //      "thumbnails" : { ... }
         //      "channelTitle": "MONO BLACK MAGIC",
         //      ...
         //  }
         // ****************************************************************************************
        
         // ***************************************************************
         // Check for existence of 'snippet' object-property
         // ***************************************************************
         obj = result.getJsonObject("snippet");
         if (obj == null)
         {
             if (a != null)
                 a.append(ELEM + i + " does not have a 'snippet' property-object." + IGNORE);
         }
         else
         {
             // ***********************************************************
             // "publishedAt": "2021-04-26T14:00:00Z",
             // Publish Date/Time
             // ***********************************************************
             str = obj.getJsonString("publishedAt");
             if (str != null)
                 try 
                     { publishedDate = sdfIn.parse(str.getString()); }
                 catch (ParseException e)
                 {
                     if (a != null) a.append
                         (ELEM + i + " has a snippet with 'publishedAt' that cannot be parsed." + IGNORE);
                 }
                        
             // ***********************************************************
             // "title": "TERROR OF MOUNT VELUS, ...""
             // Video Post Title
             // ***********************************************************
             str = obj.getJsonString("title");
             if (str == null)
             {
                 if (a != null) a.append(
                     ELEM + i + " contains a snippet JSON Object, but that snippet has no title." +
                     IGNORE
                 );
             }
             else
                 title = str.getString();
        
             // ***********************************************************
             // "description": "Original: Deck 15 Dragon's Approach (STX) 97 8 Plains (STX) ...""
             // Video Description
             // ***********************************************************
             str = obj.getJsonString("description");
             if (str == null)
             {
                 if (a != null) a.append(
                     ELEM + i + " contains a snippet JSON Object, but that snippet has no " +
                     "description string." + IGNORE
                 );
             }
             else
                 description = Escape.replace(str.getString());
        
             // ************************************************************************************
             //  "snippet": {
             //      ...
             //      "thumbnails": {
             //          "default": {
             //              "url": "https://i.ytimg.com/vi/a8GcrDfBOx4/default.jpg",
             //              "width": 120,
             //              "height": 90
             //          },
             //          "medium": { ... }
             //          "high": { ... }
             //      }
             //  }
             // ************************************************************************************
                    
             obj = obj.getJsonObject("thumbnails");
             if (obj == null)
             {
                 if (a != null) a.append
                     (ELEM + i + " has a snippet without any thumbnail images." + IGNORE);
             }
             else
             {
                 for (String thumbnailName : obj.keySet())
                 {
                     JsonObject thumb = obj.getJsonObject(thumbnailName);
        
                     // Get the Thumbnail URL
                     str = thumb.getJsonString("url");
                     if (str == null)
                     {
                         if (a != null) a.append
                             (ELEM + i + " contains a thumbnail with no URL.  Skipping this thumbnail.\n");
                     }
                     String thumbURL = str.getString();
        
                     // Get the Thumbnail width
                     JsonNumber num = thumb.getJsonNumber("width");
                     short width = -1;
                     if (num != null) width = (short) num.intValue();
        
                     // Get the Thumbnail height
                     num = thumb.getJsonNumber("height");
                     short height = -1;
                     if (num != null) height = (short) num.intValue();
        
                     thumbnails.put(
                         thumbnailName,
                         new Thumbnail(thumbnailName, thumbURL, width, height)
                     );
                 }
             }
         }
            
         /*
         VideoSummary(
                 String id, String title, String descSnippet, Date datePublished,
                 String eTag, TreeMap<Thumbnail> thumbnails
             )
         */
         return new VideoSummary
             (videoID, title, description, publishedDate, eTag, thumbnails);