From 5f9cdfeff96bb2848e3dc72bab410859d78b9c65 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Sat, 26 Dec 2020 20:12:46 +0530 Subject: [PATCH] YouTube auto-play Signed-off-by: Joseph Nuthalapati --- README.md | 18 ++++++++++++------ src/lib.nim | 19 +++++++++++++++---- src/nimcoon.nim | 9 ++++++--- src/youtube.nim | 24 +++++++++++++++++++----- tests/tests.nim | 15 ++++++++++----- 5 files changed, 62 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7de80d0..2b09f97 100644 --- 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 diff --git a/src/lib.nim b/src/lib.nim index 17761f3..204c328 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -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)): diff --git a/src/nimcoon.nim b/src/nimcoon.nim index 3ad8fdc..9df18a8 100644 --- a/src/nimcoon.nim +++ b/src/nimcoon.nim @@ -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 diff --git a/src/youtube.nim b/src/youtube.nim index 3457062..a83e233 100644 --- a/src/youtube.nim +++ b/src/youtube.nim @@ -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())) diff --git a/tests/tests.nim b/tests/tests.nim index fd44065..b412cb3 100644 --- a/tests/tests.nim +++ b/tests/tests.nim @@ -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)) -- 2.43.0