YouTube auto-play
authorJoseph Nuthalapati <njoseph@riseup.net>
Sat, 26 Dec 2020 14:42:46 +0000 (20:12 +0530)
committerJoseph Nuthalapati <njoseph@riseup.net>
Sat, 26 Dec 2020 14:42:46 +0000 (20:12 +0530)
Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
README.md
src/lib.nim
src/nimcoon.nim
src/youtube.nim
tests/tests.nim

index 7de80d0e0922d54ce141d88157f8b4c34871d322..2b09f97f4d3353392f522b886303e141a4ce081f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ I made this just for myself. The development is completely based on my needs and
 - [x] Stream video from torrent file URLs
 - [x] BitTorrent is preferred for PeerTube video links
 - [ ] Search PeerTube (3.0 or later)
-- [ ] YouTube Autoplay (music only)
+- [x] YouTube Autoplay (music only)
 - [ ] Configuration options
 
 |                         | YouTube  | PeerTube (HTTP) | PeerTube (WebTorrent) |
@@ -66,6 +66,7 @@ I made this just for myself. The development is completely based on my needs and
 | Stream Video Playlist   | ✅       |                 |                       |
 | Download Music Playlist |          |                 |                       |
 | Download Video Playlist |          |                 |                       |
+| Play Recommended Music  | ✅       |                 |                       |
 
 ## Installation
 
@@ -156,6 +157,7 @@ case-by-case basis using the interactive arguments.
 | -l, --lucky                   | Try your luck with the first search result |
 | -f, --full-screen             | Play video in full screen                  |
 | -d, --download                | Download video or music                    |
+| -a, --auto-play               | Play the next search result (YouTube only) |
 
 Feel free to use these options in any combination. NimCoon will show a helpful
 error message if you pick incompatible options.
@@ -169,11 +171,15 @@ options as single characters. i.e
 "1" plays the video
 "1 md" downloads the music of the video
 
-| **Interactive Arguments** | **Explanation**           |
-|---------------------------|---------------------------|
-| -m, --music               | Play Music only, no video |
-| -f, --full-screen         | Play video in full screen |
-| -d, --download            | Download video or music   |
+| **Interactive Arguments** | **Explanation**                            |
+|---------------------------|--------------------------------------------|
+| -m, --music               | Play Music only, no video                  |
+| -f, --full-screen         | Play video in full screen                  |
+| -d, --download            | Download video or music                    |
+| -a, --auto-play           | Play the next search result (YouTube only) |
+
+Auto-playing videos leads to binge watching. The default option in NimCoon is to
+support auto-play for music only.
 
 ## Development
 
index 17761f390339cc8821d62984d4979acf7601ea37..204c328a26205bed6f93915fca01ed7449c2c465 100644 (file)
@@ -12,7 +12,8 @@ import
 
 import
   config,
-  types
+  types,
+  youtube
 
 
 let
@@ -136,7 +137,11 @@ proc offerSelection(searchResults: SearchResults, options: Table[string, bool],
 
 
 proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) =
-  if options["download"]:
+  if options["autoPlay"]:
+    play(player, options, searchResult.url, searchResult.title)
+    let nextResult = getAutoPlayVideo(searchResult)
+    handleUserInput(nextResult, options, player) # inifinite playlist till user quits
+  elif options["download"]:
     if options["musicOnly"]:
       download(buildMusicDownloadArgs(searchResult.url), searchResult.title)
     else:
@@ -147,24 +152,30 @@ proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], p
 
 proc isValidOptions*(options: Options): bool =
   # Check for invalid combinations of options
-  var invalidCombinations = [("musicOnly", "fullScreen"), ("download", "fullScreen")]
+  var invalidCombinations = [("musicOnly", "fullScreen"), ("download", "fullScreen"), ("download", "autoPlay")]
   result = true
   for combination in invalidCombinations:
     if options[combination[0]] and options[combination[1]]:
      stderr.writeLine fmt"Incompatible options provided: {combination[0]} and {combination[1]}"
      result = false
+  # TODO Make this overridable in configuration
+  if options["autoPlay"] and not options["musicOnly"]:
+    stderr.writeLine "--music-only must be provided with --auto-play. This is to prevent binge-watching."
+    result = false
 
 
 proc updateOptions(options: Options, newOptions: string): Options =
   result = options
 
+  # Interactive options
   for option in newOptions:
     case option
     of 'm': result["musicOnly"] = true
     of 'f': result["fullScreen"] = true
     of 'd': result["download"] = true
+    of 'a': result["autoPlay"] = true
     else:
-      echo "Invalid option provided!"
+      stderr.writeLine "Invalid option provided!"
       quit(2)
 
   if(not isValidOptions(result)):
index 3ad8fdca67fe4741798a3144acfe3af551b48d7e..9df18a8053dbf521956ff6e23b9e749938e03c86 100644 (file)
@@ -19,9 +19,11 @@ proc parseArguments(): CommandLineOptions =
       "feelingLucky": false,
       "fullScreen": false,
       "download": false,
-      "non-interactive": false
+      "nonInteractive": false,
+      "autoPlay": false
     })
 
+  # Non-interactive/Global options
   for kind, key, value in getopt():
     case kind
     of cmdArgument:
@@ -32,7 +34,8 @@ proc parseArguments(): CommandLineOptions =
       of "l", "lucky": options["feelingLucky"] = true
       of "f", "full-screen": options["fullScreen"] = true
       of "d", "download": options["download"] = true
-      of "n", "non-interactive": options["non-interactive"] = true
+      of "n", "non-interactive": options["nonInteractive"] = true
+      of "a", "auto-play": options["autoPlay"] = true
     of cmdEnd: discard
 
   if searchQuery == "":
@@ -58,7 +61,7 @@ proc main() =
     quit(0)
 
   let searchResults = getSearchResults(searchQuery)
-  if options["non-interactive"]:
+  if options["nonInteractive"]:
     for index, (title, url) in searchResults:
       echo title
       echo url
index 3457062f8bf7eeddab312e01e8ec182612f3d2fb..a83e23372fb21b6e66822648812bf60ef062aacf 100644 (file)
@@ -2,6 +2,7 @@ import
   httpClient,
   json,
   strformat,
+  strutils,
   uri
 
 import
@@ -10,13 +11,17 @@ import
 
 
 discard """
-Using Invidious API to retrieve the search results but playing the results directly from YouTube.
 
-API reference:
-https://github.com/iv-org/documentation/blob/master/API.md#get-apiv1search
+Invidious API reference:
+https://github.com/iv-org/documentation/blob/master/API.md
 """
 
+func makeUrl(videoId: string): string =
+  "https://www.youtube.com/watch?v=" & videoId
+
+
 proc getSearchResults*(searchQuery: string): SearchResults =
+  # Using Invidious API to retrieve the search results but playing the results directly from YouTube.
   let queryParam = encodeUrl(searchQuery)
   let client = newHttpClient()
   let response = get(client, &"{invidiousInstance}/api/v1/search?q={queryParam}")
@@ -24,8 +29,17 @@ proc getSearchResults*(searchQuery: string): SearchResults =
   var searchResults: SearchResults = @[]
   for item in jsonData:
     if item["type"].getStr() == "video":
-      searchResults.add((item["title"].getStr(), "https://www.youtube.com/watch?v=" & item["videoId"].getStr()))
+      searchResults.add((item["title"].getStr(), makeUrl(item["videoId"].getStr())))
     elif item["type"].getStr() == "playlist":
-      searchResults.add((item["title"].getStr(), "https://www.youtube.com/watch?v=" & item["playlistId"].getStr()))
+      searchResults.add((item["title"].getStr(), makeUrl(item["playlistId"].getStr())))
     # Not handling type = channel for now
   searchResults
+
+proc getAutoPlayVideo*(searchResult: SearchResult): SearchResult =
+  # Take a search result and fetch its first recommendation
+  let videoId = searchResult.url.split("=")[1]
+  let client = newHttpClient()
+  let response = get(client, &"{invidiousInstance}/api/v1/videos/{videoId}")
+  let jsonData = parseJson($response.body)
+  let firstRecommendation = jsonData["recommendedVideos"][0]
+  (firstRecommendation["title"].getStr(), makeUrl(firstRecommendation["videoId"].getStr()))
index fd44065992227c86e599c42513a6d2229a56e228..b412cb3151d36c49d725ba8229d551e80e80dd3c 100644 (file)
@@ -3,7 +3,6 @@ import
   unittest
 
 import lib
-import nimcoon
 
 suite "Playing direct links":
 
@@ -14,14 +13,20 @@ suite "Playing direct links":
 
   test "validate options":
     let invalidOptionsList = [
-      to_table({"musicOnly": true, "feelingLucky": false, "fullScreen": true, "download": false}),
-      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": true, "download": true})
+      to_table({"musicOnly": true, "feelingLucky": false, "fullScreen": true, "download": false, "autoPlay": false}),
+      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": true, "download": true, "autoPlay": false}),
+      # autoPlay download
+      to_table({"musicOnly": true, "feelingLucky": true, "fullScreen": true, "download": true, "autoPlay": true}),
+      # autoPlay video
+      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": true, "download": false, "autoPlay": true}),
     ]
     for invalidOptions in invalidOptionsList:
       check(not isValidOptions(invalidOptions))
+
     let validOptionsList = [
-      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": false, "download": true}),
-      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": true, "download": false})
+      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": false, "download": true, "autoPlay": false}),
+      to_table({"musicOnly": false, "feelingLucky": true, "fullScreen": true, "download": false, "autoPlay": false}),
+      to_table({"musicOnly": true, "feelingLucky": true, "fullScreen": false, "download": false, "autoPlay": true}),
     ]
     for validOptions in validOptionsList:
       check(isValidOptions(validOptions))