- [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) |
| Stream Video Playlist | ✅ | | |
| Download Music Playlist | | | |
| Download Video Playlist | | | |
+| Play Recommended Music | ✅ | | |
## Installation
| -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.
"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
import
config,
- types
+ types,
+ youtube
let
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:
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)):
"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:
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 == "":
quit(0)
let searchResults = getSearchResults(searchQuery)
- if options["non-interactive"]:
+ if options["nonInteractive"]:
for index, (title, url) in searchResults:
echo title
echo url
httpClient,
json,
strformat,
+ strutils,
uri
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}")
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()))
unittest
import lib
-import nimcoon
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))