From 6697cfd20fbad109b18302ab0bb2c094bfdc4b5c Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Sat, 2 Jan 2021 15:26:32 +0530 Subject: [PATCH] Use youtube-dl's ytsearch to feel lucky This makes the search for the top result much faster. Signed-off-by: Joseph Nuthalapati --- src/config.nim | 18 ++--- src/lib.nim | 166 ++++++++++++++++++++++++++--------------------- src/nimcoon.nim | 10 ++- src/peertube.nim | 18 +++++ 4 files changed, 127 insertions(+), 85 deletions(-) create mode 100644 src/peertube.nim diff --git a/src/config.nim b/src/config.nim index 6f1bd72..06ebd63 100644 --- a/src/config.nim +++ b/src/config.nim @@ -1,11 +1,3 @@ -discard """" -Configuration goes through three levels of overrides: - - /etc/nimcoon/config.json - configuration set by system administrator - ~/.config/nimcoon/config.json - per user configuration - default configuration provided in this file -"""" - # Default configuration values # Supported video players in order of preference. @@ -28,7 +20,15 @@ let rewriteInvidiousURLs* = true # Invidious instance for querying # This instance should have a valid public API # Check like this: curl https://invidious.xyz/api/v1/search\?q\=cats -let invidiousInstance* = "https://invidious.xyz" +let invidiousInstance* = "https://invidious.kavin.rocks" + +discard """" +Configuration goes through three levels of overrides: + + /etc/nimcoon/config.json - configuration set by system administrator + ~/.config/nimcoon/config.json - per user configuration + default configuration provided in this file +"""" # import os diff --git a/src/lib.nim b/src/lib.nim index 204c328..25a8016 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -1,6 +1,4 @@ import - httpClient, - json, os, osproc, re, @@ -12,12 +10,13 @@ import import config, + peertube, types, youtube let - processOptions = {poStdErrToStdOut, poUsePath, poEchoCmd} + processOptions = {poStdErrToStdOut, poUsePath} # Add poEchoCmd to debug PEERTUBE_REGEX = re"videos\/watch\/[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}" @@ -34,22 +33,31 @@ proc selectMediaPlayer*(): string = return availablePlayers[0] -proc getPeerTubeMagnetLink(url: string): string = - ## Gets the magnet link of the best possible resolution from PeerTube - let uuid = url.substr(find(url, PEERTUBE_REGEX) + "videos/watch/".len) - let domainName = url.substr(8, find(url, '/', start=8) - 1) - let apiURL = &"https://{domainName}/api/v1/videos/{uuid}" - let client = newHttpClient() - let response = get(client, apiURL) - let jsonNode = parseJson($response.body) - jsonNode["files"][0]["magnetUri"].getStr() +############### +# URL CLEANUP # +############### +func rewriteInvidiousToYouTube*(url: string): string = + {.noSideEffect.}: + if rewriteInvidiousURLs and url.replace(".", "").contains("invidious"): + &"https://www.youtube.com/watch?v={url.split(\"=\")[1]}" + else: url -proc presentVideoOptions*(searchResults: SearchResults) = - eraseScreen() - for index, (title, url) in searchResults: - styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, " ", url, "\n" +func urlLongen(url: string): string = + url.replace("youtu.be/", "www.youtube.com/watch?v=") + + +func stripZshEscaping(url: string): string = url.replace("\\", "") + + +func sanitizeURL*(url: string): string = + rewriteInvidiousToYouTube(urlLongen(stripZshEscaping(url))) + + +######## +# PLAY # +######## func buildPlayerArgs(url: string, options: Table[string, bool], player: string): seq[string] = let musicOnly = if options["musicOnly"]: "--no-video" else: "" @@ -67,6 +75,26 @@ proc play*(player: string, options: Table[string, bool], url: string, title: str discard execProcess(player, args=args, options=processOptions) +proc directPlay*(url: string, player: string, options: Table[string, bool]) = + let url = + if find(url, PEERTUBE_REGEX) != -1 and "webtorrent".isInstalled: + getPeerTubeMagnetLink(url) + else: url + if url.startswith("magnet:") or url.endswith(".torrent"): + if options["musicOnly"]: + # TODO Replace with WebTorrent once it supports media player options + discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video") + else: + # WebTorrent is so much faster! + discard execProcess("webtorrent", args=[url, &"--{player}"], options=processOptions) + else: + play(player, options, url) + + +############ +# DOWNLOAD # +############ + func buildMusicDownloadArgs(url: string): seq[string] = {.noSideEffect.}: let downloadLocation = &"'{expandTilde(musicDownloadDirectory)}/%(title)s.%(ext)s'" @@ -80,75 +108,37 @@ func buildVideoDownloadArgs(url: string): seq[string] = @["-f", "best", "-o", downloadLocation, url] +func buildDownloadArgs(url: string, options: Options): seq[string] = + if options["musicOnly"]: buildMusicDownloadArgs(url) + else: buildVideoDownloadArgs(url) + + proc download*(args: openArray[string], title: string) = styledEcho "\n", fgGreen, "Downloading ", styleBright, fgMagenta, title discard execShellCmd(&"youtube-dl {args.join(\" \")}") -func urlLongen(url: string): string = url.replace("youtu.be/", "www.youtube.com/watch?v=") - - -func rewriteInvidiousToYouTube*(url: string): string = - {.noSideEffect.}: - if rewriteInvidiousURLs and url.replace(".", "").contains("invidious"): - &"https://www.youtube.com/watch?v={url.split(\"=\")[1]}" - else: url - - -func stripZshEscaping(url: string): string = url.replace("\\", "") - - -func sanitizeURL*(url: string): string = - rewriteInvidiousToYouTube(urlLongen(stripZshEscaping(url))) - - -proc directPlay*(url: string, player: string, options: Table[string, bool]) = - let url = - if find(url, PEERTUBE_REGEX) != -1 and isInstalled("webtorrent"): - getPeerTubeMagnetLink(url) - else: url - if url.startswith("magnet:") or url.endswith(".torrent"): - if options["musicOnly"]: - # TODO Replace with WebTorrent once it supports media player options - discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video") - else: - # WebTorrent is so much faster! - discard execProcess("webtorrent", args=[url, &"--{player}"], options=processOptions) - else: - play(player, options, url) - - -proc directDownload*(url: string, musicOnly: bool) = - let args = - if musicOnly: buildMusicDownloadArgs(url) - else: buildVideoDownloadArgs(url) - if isInstalled("aria2c"): +proc directDownload*(url: string, options: Options) = + let args = buildDownloadArgs(url, options) + if "aria2c".isInstalled: discard execShellCmd(&"youtube-dl {args.join(\" \")} --external-downloader aria2c --external-downloader-args '-x 16 -s 16 -k 2M'") else: discard execShellCmd(&"youtube-dl {args.join(\" \")}") +proc luckyDownload*(searchQuery: string, options: Options) = + let args = @[&"ytsearch:\"{searchQuery}\""] & buildDownloadArgs("", options) + styledEcho "\n", fgGreen, "Searching and downloading using youtube-dl" + discard execShellCmd(&"youtube-dl {args.join(\" \")}") -proc offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string = - if options["feelingLucky"]: "0" - else: - presentVideoOptions(searchResults[selectionRange.begin .. selectionRange.until]) - stdout.styledWrite(fgYellow, "Choose video number: ") - readLine(stdin) - - -proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) = - 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: - download(buildVideoDownloadArgs(searchResult.url), searchResult.title) - else: - play(player, options, searchResult.url, searchResult.title) +proc luckyPlay*(searchQuery: string, player: string, options: Options) = + let args = @[&"ytsearch:\"{searchQuery}\""] & buildDownloadArgs("", options) + let output = execProcess(&"youtube-dl --get-url {args.join(\" \")}") + let url = output.split("\n")[0] + play(player, options, url) +########### +# OPTIONS # +########### proc isValidOptions*(options: Options): bool = # Check for invalid combinations of options @@ -182,6 +172,34 @@ proc updateOptions(options: Options, newOptions: string): Options = quit(2) +################ +# PRESENTATION # +################ + +proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) = + if options["autoPlay"]: + play(player, options, searchResult.url, searchResult.title) + handleUserInput(getAutoPlayVideo(searchResult), options, player) # inifinite playlist till user quits + elif options["download"]: + download(buildDownloadArgs(searchResult.url, options), searchResult.title) + else: + play(player, options, searchResult.url, searchResult.title) + + +proc presentVideoOptions(searchResults: SearchResults) = + eraseScreen() + for index, (title, url) in searchResults: + styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, " ", url, "\n" + + +proc offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string = + if options["feelingLucky"]: "0" + else: + presentVideoOptions(searchResults[selectionRange.begin .. selectionRange.until]) + stdout.styledWrite(fgYellow, "Choose video number: ") + readLine(stdin) + + proc present*(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange, player: string) = ##[ Continuously present options till the user quits the application diff --git a/src/nimcoon.nim b/src/nimcoon.nim index 9df18a8..1fd3ba6 100644 --- a/src/nimcoon.nim +++ b/src/nimcoon.nim @@ -55,13 +55,19 @@ proc main() = if searchQuery.startswith("http") or searchQuery.startswith("magnet"): if options["download"]: - directDownload(sanitizeURL(searchQuery), options["musicOnly"]) + directDownload(sanitizeURL(searchQuery), options) else: directPlay(sanitizeURL(searchQuery), player, options) quit(0) + # Take a shortcut and search directly with youtube-dl + if options["feelingLucky"]: + if options["download"]: luckyDownload(searchQuery, options) + else: luckyPlay(searchQuery, player, options) + quit(0) + let searchResults = getSearchResults(searchQuery) - if options["nonInteractive"]: + if options["nonInteractive"]: # Present in machine-readable format for index, (title, url) in searchResults: echo title echo url diff --git a/src/peertube.nim b/src/peertube.nim new file mode 100644 index 0000000..36f1054 --- /dev/null +++ b/src/peertube.nim @@ -0,0 +1,18 @@ +import + httpClient, + json, + re, + strformat, + strutils + +let PEERTUBE_REGEX = re"videos\/watch\/[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}" + +proc getPeerTubeMagnetLink*(url: string): string = + ## Gets the magnet link of the best possible resolution from PeerTube + let uuid = url.substr(find(url, PEERTUBE_REGEX) + "videos/watch/".len) + let domainName = url.substr(8, find(url, '/', start=8) - 1) + let apiURL = &"https://{domainName}/api/v1/videos/{uuid}" + let client = newHttpClient() + let response = get(client, apiURL) + let jsonNode = parseJson($response.body) + jsonNode["files"][0]["magnetUri"].getStr() -- 2.43.0