From d36e22010231d8bd139b906fc66e060575decb2d Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Mon, 1 Jun 2020 22:22:22 +0530 Subject: [PATCH 01/16] Detect and play playlists (MPV only) --- README.md | 3 +++ src/lib.nim | 38 ++++++++++++++++++++++---------------- src/nimcoon.nim | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 82dad57..7af3591 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ only the standard library. - [x] Stream video and music from magnet links - [x] Download music - [x] Download video +- [x] Play playlists (MPV only) +- [ ] Download playlists +- [ ] Autoplay next video/audio - [ ] Configuration options ## Installation diff --git a/src/lib.nim b/src/lib.nim index d77cbe1..67095ac 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -51,8 +51,24 @@ proc presentVideoOptions*(searchResults: SearchResults) = for index, (title, url) in searchResults: styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, url, "\n" -proc play*(player: string, args: openArray[string], title: string) = - styledEcho "\n", fgGreen, "Playing ", styleBright, fgMagenta, title +func isPlaylist(url: string): bool = + # Only YouTube playlists are supported for now + "www.youtube.com" in url and "&list=" in url + +# This is a pure function with no side effects +func buildPlayerArgs(url: string, options: Table[string, bool], player: string): seq[string] = + var args = @[url] + if options["musicOnly"]: args.add("--no-video") + if options["fullScreen"]: args.add("--fullscreen") + # Playlists are only supported for MPV player + if isPlaylist(url) and player == "mpv": + args.add("--ytdl-raw-options=\"yes-playlist=\"") + return args + +proc play*(player: string, options: Table[string, bool], url: string, title: string = "") = + let args = buildPlayerArgs(url, options, player) + if title != "": + styledEcho "\n", fgGreen, "Playing ", styleBright, fgMagenta, title if "--no-video" in args: discard execShellCmd(&"{player} {args.join(\" \")}") else: @@ -87,17 +103,14 @@ func stripZshEscaping(url: string): string = func sanitizeURL*(url: string): string = urlLongen(stripZshEscaping(url)) -proc directPlay*(url: string, player: string, musicOnly: bool) = +proc directPlay*(url: string, player: string, options: Table[string, bool]) = if url.startswith("magnet:"): - if musicOnly: + if options["musicOnly"]: discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video") else: discard execProcess("peerflix", args=[url, &"--{player}"], options=processOptions) else: - if musicOnly: - discard execShellCmd(&"{player} --no-video {url}") - else: - discard execProcess(player, args=[url], options=processOptions) + play(player, options, url) proc directDownload*(url: string, musicOnly: bool) = let args = @@ -112,13 +125,6 @@ proc offerSelection(searchResults: SearchResults, options: Table[string, bool], stdout.styledWrite(fgYellow, "Choose video number: ") readLine(stdin) -# This is a pure function with no side effects -func buildPlayerArgs(searchResult: SearchResult, options: Table[string, bool]): seq[string] = - var args = @[searchResult.url] - if options["musicOnly"]: args.add("--no-video") - if options["fullScreen"]: args.add("--fullscreen") - return args - proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) = if options["download"]: if options["musicOnly"]: @@ -126,7 +132,7 @@ proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], p else: download(buildVideoDownloadArgs(searchResult.url), searchResult.title) else: - play(player, buildPlayerArgs(searchResult, options), searchResult.title) + play(player, options, searchResult.url, searchResult.title) 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 cc0bd7e..1bb0b74 100644 --- a/src/nimcoon.nim +++ b/src/nimcoon.nim @@ -49,7 +49,7 @@ proc main() = if options["download"]: directDownload(sanitizeURL(searchQuery), options["musicOnly"]) else: - directPlay(sanitizeURL(searchQuery), player, options["musicOnly"]) + directPlay(sanitizeURL(searchQuery), player, options) quit(0) let searchResults = extractTitlesAndUrls(getYoutubePage(searchQuery)) -- 2.43.0 From e4f082d920c20319371df852923d488a2f3b701f Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Mon, 1 Jun 2020 22:34:32 +0530 Subject: [PATCH 02/16] Bump version and update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47fb0fb..42272a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.3.0 + +- Add support for playing playlists of videos and songs from YouTube +- Playlists can be played by providing the first video in the list or by directly providing the playlist url + # 0.2.2 - Fix bug with paginated result selection -- 2.43.0 From 000327f010f60ae7137f75668bece1ee311da754 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Mon, 1 Jun 2020 22:44:58 +0530 Subject: [PATCH 03/16] Bump version in nimble file --- nimcoon.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimcoon.nimble b/nimcoon.nimble index cada593..a10c3f9 100644 --- a/nimcoon.nimble +++ b/nimcoon.nimble @@ -1,6 +1,6 @@ # Package -version = "0.2.2" +version = "0.3.0" author = "Joseph Nuthalapati" description = "A command-line YouTube player and more" license = "GPL-3.0" -- 2.43.0 From ede339f5fd328fd22e320a087f5b594b2c7b2038 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Tue, 2 Jun 2020 07:39:09 +0530 Subject: [PATCH 04/16] Bump version to fix tagging mistake --- CHANGELOG.md | 4 ++++ nimcoon.nimble | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42272a6..afb2915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.3.1 + +- Fix mistake with tagging. + # 0.3.0 - Add support for playing playlists of videos and songs from YouTube diff --git a/nimcoon.nimble b/nimcoon.nimble index a10c3f9..6daa081 100644 --- a/nimcoon.nimble +++ b/nimcoon.nimble @@ -1,6 +1,6 @@ # Package -version = "0.3.0" +version = "0.3.1" author = "Joseph Nuthalapati" description = "A command-line YouTube player and more" license = "GPL-3.0" -- 2.43.0 From 28d7042ef9a94c77a2de54902ecd46591940555e Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Wed, 3 Jun 2020 00:14:47 +0530 Subject: [PATCH 05/16] Reimplement streaming of playlists --- src/lib.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.nim b/src/lib.nim index 67095ac..4305b2c 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -52,6 +52,7 @@ proc presentVideoOptions*(searchResults: SearchResults) = styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, url, "\n" func isPlaylist(url: string): bool = + # Identifies if video is part of a playlist # Only YouTube playlists are supported for now "www.youtube.com" in url and "&list=" in url @@ -62,7 +63,8 @@ func buildPlayerArgs(url: string, options: Table[string, bool], player: string): if options["fullScreen"]: args.add("--fullscreen") # Playlists are only supported for MPV player if isPlaylist(url) and player == "mpv": - args.add("--ytdl-raw-options=\"yes-playlist=\"") + let list_arg = url.split('&')[1] + args[0] = "https://www.youtube.com/playlist?" & list_arg return args proc play*(player: string, options: Table[string, bool], url: string, title: string = "") = -- 2.43.0 From 59e0f961b4181f52ef8648b16d425518640bdd27 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Wed, 3 Jun 2020 00:16:25 +0530 Subject: [PATCH 06/16] Bump version and update changelog --- CHANGELOG.md | 4 ++++ nimcoon.nimble | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afb2915..9e4e72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.3.2 + +- Reimplement streaming of YouTube playlists + # 0.3.1 - Fix mistake with tagging. diff --git a/nimcoon.nimble b/nimcoon.nimble index 6daa081..2748c36 100644 --- a/nimcoon.nimble +++ b/nimcoon.nimble @@ -1,6 +1,6 @@ # Package -version = "0.3.1" +version = "0.3.2" author = "Joseph Nuthalapati" description = "A command-line YouTube player and more" license = "GPL-3.0" -- 2.43.0 From 8d06ad6913cf2c0a0714523d6925d3038950f50a Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Wed, 3 Jun 2020 00:58:10 +0530 Subject: [PATCH 07/16] Start replacing PeerFlix with WebTorrent --- README.md | 8 ++++---- src/lib.nim | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7af3591..75adeaa 100644 --- a/README.md +++ b/README.md @@ -26,18 +26,18 @@ only the standard library. Nim Coon depends on the following: - youtube-dl - mpv (recommended) or vlc -- peerflix (for magnet links) +- peerflix and webtorrent (for magnet links) -Install VLC or MPV using your distribution's package manager. +Install MPV or VLC using your distribution's package manager. Install YouTube-dl ``` sh pip3 install --user youtube-dl ``` -Install PeerFlix +Install PeerFlix and WebTorrent ```sh -npm install --global peerflix +npm install --global peerflix webtorrent ``` ### Installing using Nimble diff --git a/src/lib.nim b/src/lib.nim index 4305b2c..0a008b8 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -108,9 +108,11 @@ func sanitizeURL*(url: string): string = proc directPlay*(url: string, player: string, options: Table[string, bool]) = if url.startswith("magnet:"): if options["musicOnly"]: + # TODO Replace with WebTorrent once it supports media player options discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video") else: - discard execProcess("peerflix", args=[url, &"--{player}"], options=processOptions) + # WebTorrent is so much faster! + discard execProcess("webtorrent", args=[url, &"--{player}"], options=processOptions) else: play(player, options, url) -- 2.43.0 From d7688e977df2d65d032a5535318a4a913bee625f Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Thu, 4 Jun 2020 23:34:21 +0530 Subject: [PATCH 08/16] Allow http urls to .torrent files to be streamed --- README.md | 2 +- src/lib.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75adeaa..2968502 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ only the standard library. - [x] Search for videos using keywords - [x] Stream videos and music from YouTube - [x] Play direct links from YouTube and PeerTube -- [x] Stream video and music from magnet links +- [x] Stream video and music from magnet links and hyperlinks to torrent files - [x] Download music - [x] Download video - [x] Play playlists (MPV only) diff --git a/src/lib.nim b/src/lib.nim index 0a008b8..24aa930 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -106,7 +106,7 @@ func sanitizeURL*(url: string): string = urlLongen(stripZshEscaping(url)) proc directPlay*(url: string, player: string, options: Table[string, bool]) = - if url.startswith("magnet:"): + 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") -- 2.43.0 From 046c2cc3b1dc773cf9e1b92f2c4fb41e2d6d8eb0 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Thu, 4 Jun 2020 23:38:21 +0530 Subject: [PATCH 09/16] Make all sequences immutable --- src/lib.nim | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/lib.nim b/src/lib.nim index 24aa930..6153992 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -58,14 +58,14 @@ func isPlaylist(url: string): bool = # This is a pure function with no side effects func buildPlayerArgs(url: string, options: Table[string, bool], player: string): seq[string] = - var args = @[url] - if options["musicOnly"]: args.add("--no-video") - if options["fullScreen"]: args.add("--fullscreen") - # Playlists are only supported for MPV player - if isPlaylist(url) and player == "mpv": - let list_arg = url.split('&')[1] - args[0] = "https://www.youtube.com/playlist?" & list_arg - return args + let url = + # Playlists are only supported for MPV player + if isPlaylist(url) and player == "mpv": + "https://www.youtube.com/playlist?" & url.split('&')[1] + else: url + let musicOnly = if options["musicOnly"]: "--no-video" else: "" + let fullScreen = if options["fullScreen"]: "--fullscreen" else: "" + return filterIt([url, musicOnly, fullScreen], it != "") proc play*(player: string, options: Table[string, bool], url: string, title: string = "") = let args = buildPlayerArgs(url, options, player) @@ -78,19 +78,13 @@ proc play*(player: string, options: Table[string, bool], url: string, title: str func buildMusicDownloadArgs*(url: string): seq[string] = {.noSideEffect.}: - var args = @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3", "--audio-quality", "0", "-o"] let downloadLocation = &"'{expandTilde(musicDownloadDirectory)}/%(title)s.%(ext)s'" - args.add(downloadLocation) - args.add(url) - return args + return @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3", "--audio-quality", "0", "-o", downloadLocation, url] func buildVideoDownloadArgs*(url: string): seq[string] = {.noSideEffect.}: - var args = @["-f", "best", "-o"] let downloadLocation = &"'{expandTilde(videoDownloadDirectory)}/%(title)s.%(ext)s'" - args.add(downloadLocation) - args.add(url) - return args + return @["-f", "best", "-o", downloadLocation, url] proc download*(args: openArray[string], title: string) = styledEcho "\n", fgGreen, "Downloading ", styleBright, fgMagenta, title -- 2.43.0 From c12cc641fe659e7e584a589bde9444904018b3b5 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 5 Jun 2020 01:14:15 +0530 Subject: [PATCH 10/16] PeerTube: Pick magnet link of the best resolution --- README.md | 3 +++ src/lib.nim | 26 +++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2968502..ddba7a1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ only the standard library. - [x] Download video - [x] Play playlists (MPV only) - [ ] Download playlists +- [x] Stream video from torrent file URLs +- [x] BitTorrent is preferred for PeerTube video links +- [ ] Search PeerTube (3.0 or later) - [ ] Autoplay next video/audio - [ ] Configuration options diff --git a/src/lib.nim b/src/lib.nim index 6153992..26df762 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -1,15 +1,17 @@ import htmlparser, httpClient, + json, os, osproc, + re, sequtils, - sugar, - strformat, std/[terminal], strformat, + strformat, strtabs, strutils, + sugar, tables, uri, xmltree @@ -25,9 +27,13 @@ type # poEchoCmd can be added to options for debugging let processOptions = {poStdErrToStdOut, poUsePath} +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 isInstalled(program: string): bool = + execProcess("which " & program).len != 0 proc selectMediaPlayer*(): string = - let availablePlayers = filterIt(supportedPlayers, execProcess("which " & it).len != 0) + let availablePlayers = supportedPlayers.filter(isInstalled) if len(availablePlayers) == 0: stderr.writeLine &"Please install one of the supported media players: {supportedPlayers}" raise newException(OSError, "No supported media player found") @@ -40,6 +46,16 @@ proc getYoutubePage*(searchQuery: string): string = let response = get(client, &"https://www.youtube.com/results?hl=en&search_query={queryParam}") return $response.body +proc getPeerTubeMagnetLink(url: string): string = + # Gets the magnet link of the best possible resolutino 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() + func extractTitlesAndUrls*(html: string): SearchResults = {.noSideEffect.}: parseHtml(html).findAll("a"). @@ -100,6 +116,10 @@ func sanitizeURL*(url: string): string = 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 -- 2.43.0 From c9e986cae6624ca4300bddcd8fd73b6074145da7 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 5 Jun 2020 01:20:28 +0530 Subject: [PATCH 11/16] 0.4.0 - Bump version and update changelog --- CHANGELOG.md | 5 +++++ nimcoon.nimble | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4e72c..8360931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.4.0 + +- Allow picking up torrent links from http urls (PeerTube provides these in download options) +- Given a PeerTube link, automatically pick the best resolution video and stream it using WebTorrent + # 0.3.2 - Reimplement streaming of YouTube playlists diff --git a/nimcoon.nimble b/nimcoon.nimble index 2748c36..29d0054 100644 --- a/nimcoon.nimble +++ b/nimcoon.nimble @@ -1,6 +1,6 @@ # Package -version = "0.3.2" +version = "0.4.0" author = "Joseph Nuthalapati" description = "A command-line YouTube player and more" license = "GPL-3.0" -- 2.43.0 From 55b0cae73065832a9bce15e7921822fa4172684e Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 5 Jun 2020 22:18:45 +0530 Subject: [PATCH 12/16] Delete unnecessary return statements --- src/lib.nim | 24 +++++++++++------------- src/nimcoon.nim | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/lib.nim b/src/lib.nim index 26df762..62d0553 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -44,7 +44,7 @@ proc getYoutubePage*(searchQuery: string): string = let queryParam = encodeUrl(searchQuery) let client = newHttpClient() let response = get(client, &"https://www.youtube.com/results?hl=en&search_query={queryParam}") - return $response.body + $response.body proc getPeerTubeMagnetLink(url: string): string = # Gets the magnet link of the best possible resolutino from PeerTube @@ -81,7 +81,7 @@ func buildPlayerArgs(url: string, options: Table[string, bool], player: string): else: url let musicOnly = if options["musicOnly"]: "--no-video" else: "" let fullScreen = if options["fullScreen"]: "--fullscreen" else: "" - return filterIt([url, musicOnly, fullScreen], it != "") + filterIt([url, musicOnly, fullScreen], it != "") proc play*(player: string, options: Table[string, bool], url: string, title: string = "") = let args = buildPlayerArgs(url, options, player) @@ -95,25 +95,22 @@ proc play*(player: string, options: Table[string, bool], url: string, title: str func buildMusicDownloadArgs*(url: string): seq[string] = {.noSideEffect.}: let downloadLocation = &"'{expandTilde(musicDownloadDirectory)}/%(title)s.%(ext)s'" - return @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3", "--audio-quality", "0", "-o", downloadLocation, url] + @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3", "--audio-quality", "0", "-o", downloadLocation, url] func buildVideoDownloadArgs*(url: string): seq[string] = {.noSideEffect.}: let downloadLocation = &"'{expandTilde(videoDownloadDirectory)}/%(title)s.%(ext)s'" - return @["-f", "best", "-o", downloadLocation, url] + @["-f", "best", "-o", downloadLocation, 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 urlLongen(url: string): string = url.replace("youtu.be/", "www.youtube.com/watch?v=") -func stripZshEscaping(url: string): string = - url.replace("\\", "") +func stripZshEscaping(url: string): string = url.replace("\\", "") -func sanitizeURL*(url: string): string = - urlLongen(stripZshEscaping(url)) +func sanitizeURL*(url: string): string = urlLongen(stripZshEscaping(url)) proc directPlay*(url: string, player: string, options: Table[string, bool]) = let url = @@ -153,9 +150,10 @@ proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], p play(player, options, searchResult.url, searchResult.title) proc present*(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange, player: string) = - #[ Continuously present options till the user quits the application - selectionRange: Currently available range to choose from depending on pagination - ]# + ##[ Continuously present options till the user quits the application + + selectionRange: Currently available range to choose from depending on pagination + ]## let userInput = offerSelection(searchResults, options, selectionRange) diff --git a/src/nimcoon.nim b/src/nimcoon.nim index 1bb0b74..20b9280 100644 --- a/src/nimcoon.nim +++ b/src/nimcoon.nim @@ -25,7 +25,7 @@ proc parseArguments(): CommandLineOptions = of "d", "download": options["download"] = true of cmdEnd: discard - return (searchQuery, options) + (searchQuery, options) proc isValidOptions*(options: Options): bool = @@ -35,7 +35,7 @@ proc isValidOptions*(options: Options): bool = if options[combination[0]] and options[combination[1]]: stderr.writeLine fmt"Incompatible options provided: {combination[0]} and {combination[1]}" return false - return true + true proc main() = let -- 2.43.0 From b44b64940a9a5708a58decce73634c81ef0b9783 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Sat, 6 Jun 2020 00:20:08 +0530 Subject: [PATCH 13/16] Add config option to rewrite invidio.us URLs --- src/config.nim | 4 ++++ src/lib.nim | 49 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/config.nim b/src/config.nim index 3593fe6..de8be3f 100644 --- a/src/config.nim +++ b/src/config.nim @@ -12,3 +12,7 @@ let videoDownloadDirectory* = "~/Videos" # Download music to this directory let musicDownloadDirectory* = "~/Music" + +# Rewrite Invidious URLs to YouTube +# Using Invidious as a proxy makes loading YouTube videos much slower +let rewriteInvidiousURLs* = false diff --git a/src/lib.nim b/src/lib.nim index 62d0553..ccd1a39 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -18,6 +18,7 @@ import import config + type Options* = Table[string, bool] SearchResult* = tuple[title: string, url: string] @@ -25,13 +26,17 @@ type CommandLineOptions* = tuple[searchQuery: string, options: Options] SelectionRange* = tuple[begin: int, until: int] + # poEchoCmd can be added to options for debugging -let processOptions = {poStdErrToStdOut, poUsePath} -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}" +let + processOptions = {poStdErrToStdOut, poUsePath} + 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 isInstalled(program: string): bool = execProcess("which " & program).len != 0 + proc selectMediaPlayer*(): string = let availablePlayers = supportedPlayers.filter(isInstalled) if len(availablePlayers) == 0: @@ -40,12 +45,14 @@ proc selectMediaPlayer*(): string = else: return availablePlayers[0] + proc getYoutubePage*(searchQuery: string): string = let queryParam = encodeUrl(searchQuery) let client = newHttpClient() let response = get(client, &"https://www.youtube.com/results?hl=en&search_query={queryParam}") $response.body + proc getPeerTubeMagnetLink(url: string): string = # Gets the magnet link of the best possible resolutino from PeerTube let uuid = url.substr(find(url, PEERTUBE_REGEX) + "videos/watch/".len) @@ -56,26 +63,29 @@ proc getPeerTubeMagnetLink(url: string): string = let jsonNode = parseJson($response.body) jsonNode["files"][0]["magnetUri"].getStr() + func extractTitlesAndUrls*(html: string): SearchResults = {.noSideEffect.}: parseHtml(html).findAll("a"). filter(a => "watch" in a.attrs["href"] and a.attrs.hasKey "title"). map(a => (a.attrs["title"], "https://www.youtube.com" & a.attrs["href"])) + proc presentVideoOptions*(searchResults: SearchResults) = eraseScreen() for index, (title, url) in searchResults: styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, url, "\n" + func isPlaylist(url: string): bool = - # Identifies if video is part of a playlist - # Only YouTube playlists are supported for now + ##[ Identifies if video is part of a playlist. + Only YouTube playlists are supported for now. ]## "www.youtube.com" in url and "&list=" in url -# This is a pure function with no side effects + func buildPlayerArgs(url: string, options: Table[string, bool], player: string): seq[string] = let url = - # Playlists are only supported for MPV player + # Playlists are only supported by MPV player. VLC needs a plugin. if isPlaylist(url) and player == "mpv": "https://www.youtube.com/playlist?" & url.split('&')[1] else: url @@ -83,6 +93,7 @@ func buildPlayerArgs(url: string, options: Table[string, bool], player: string): let fullScreen = if options["fullScreen"]: "--fullscreen" else: "" filterIt([url, musicOnly, fullScreen], it != "") + proc play*(player: string, options: Table[string, bool], url: string, title: string = "") = let args = buildPlayerArgs(url, options, player) if title != "": @@ -92,25 +103,39 @@ proc play*(player: string, options: Table[string, bool], url: string, title: str else: discard execProcess(player, args=args, options=processOptions) -func buildMusicDownloadArgs*(url: string): seq[string] = + +func buildMusicDownloadArgs(url: string): seq[string] = {.noSideEffect.}: let downloadLocation = &"'{expandTilde(musicDownloadDirectory)}/%(title)s.%(ext)s'" - @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3", "--audio-quality", "0", "-o", downloadLocation, url] + @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3", + "--audio-quality", "0", "-o", downloadLocation, url] + -func buildVideoDownloadArgs*(url: string): seq[string] = +func buildVideoDownloadArgs(url: string): seq[string] = {.noSideEffect.}: let downloadLocation = &"'{expandTilde(videoDownloadDirectory)}/%(title)s.%(ext)s'" @["-f", "best", "-o", downloadLocation, 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: url.replace("invidio.us", "www.youtube.com") else: url + + func stripZshEscaping(url: string): string = url.replace("\\", "") -func sanitizeURL*(url: string): string = urlLongen(stripZshEscaping(url)) + +func sanitizeURL*(url: string): string = + rewriteInvidiousToYouTube(urlLongen(stripZshEscaping(url))) + proc directPlay*(url: string, player: string, options: Table[string, bool]) = let url = @@ -127,12 +152,14 @@ proc directPlay*(url: string, player: string, options: Table[string, bool]) = else: play(player, options, url) + proc directDownload*(url: string, musicOnly: bool) = let args = if musicOnly: buildMusicDownloadArgs(url) else: buildVideoDownloadArgs(url) discard execShellCmd(&"youtube-dl {args.join(\" \")}") + proc offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string = if options["feelingLucky"]: "0" else: @@ -140,6 +167,7 @@ proc offerSelection(searchResults: SearchResults, options: Table[string, bool], stdout.styledWrite(fgYellow, "Choose video number: ") readLine(stdin) + proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) = if options["download"]: if options["musicOnly"]: @@ -149,6 +177,7 @@ proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], p else: play(player, options, searchResult.url, searchResult.title) + proc present*(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange, player: string) = ##[ Continuously present options till the user quits the application -- 2.43.0 From 86e6cb72964db0443c6b5527892f49721079f72a Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Tue, 16 Jun 2020 07:26:22 +0530 Subject: [PATCH 14/16] Minor formatting changes --- src/lib.nim | 7 +++---- src/nimcoon.nim | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lib.nim b/src/lib.nim index ccd1a39..d36e2c2 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -27,9 +27,8 @@ type SelectionRange* = tuple[begin: int, until: int] -# poEchoCmd can be added to options for debugging let - processOptions = {poStdErrToStdOut, poUsePath} + processOptions = {poStdErrToStdOut, poUsePath} # poEchoCmd can be added to options for debugging 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}" @@ -54,7 +53,7 @@ proc getYoutubePage*(searchQuery: string): string = proc getPeerTubeMagnetLink(url: string): string = - # Gets the magnet link of the best possible resolutino from PeerTube + ## 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}" @@ -74,7 +73,7 @@ func extractTitlesAndUrls*(html: string): SearchResults = proc presentVideoOptions*(searchResults: SearchResults) = eraseScreen() for index, (title, url) in searchResults: - styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, url, "\n" + styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, " ", url, "\n" func isPlaylist(url: string): bool = diff --git a/src/nimcoon.nim b/src/nimcoon.nim index 20b9280..265409d 100644 --- a/src/nimcoon.nim +++ b/src/nimcoon.nim @@ -31,11 +31,11 @@ proc parseArguments(): CommandLineOptions = proc isValidOptions*(options: Options): bool = # Check for invalid combinations of options var invalidCombinations = [("musicOnly", "fullScreen"), ("download", "fullScreen")] + 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]}" - return false - true + result = false proc main() = let -- 2.43.0 From bd7117ada0e5f895e38c0476683e9adcae6dbd04 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Tue, 16 Jun 2020 07:26:47 +0530 Subject: [PATCH 15/16] VLC is better than CVLC --- src/config.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.nim b/src/config.nim index de8be3f..b231a83 100644 --- a/src/config.nim +++ b/src/config.nim @@ -2,7 +2,7 @@ # Supported video players in order of preference. # Should be able to play YouTube videos directly. -let supportedPlayers* = ["mpv", "cvlc"] +let supportedPlayers* = ["mpv", "vlc"] # Only show these many results let limit* = 10 -- 2.43.0 From 0c2f03858ecba0812c1b92bd5d2ace75e676ddaa Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Tue, 16 Jun 2020 07:30:53 +0530 Subject: [PATCH 16/16] Use aria2c download manager if available --- README.md | 4 +++- src/lib.nim | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddba7a1..3043dea 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,11 @@ pip3 install --user youtube-dl Install PeerFlix and WebTorrent ```sh -npm install --global peerflix webtorrent +npm install --global peerflix webtorrent-cli ``` +(Optional) If you want your YouTube downloads to be faster, install `aria2` download manager. + ### Installing using Nimble NimCoon can be installed from Nimble repositories: diff --git a/src/lib.nim b/src/lib.nim index d36e2c2..3033458 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -156,7 +156,10 @@ proc directDownload*(url: string, musicOnly: bool) = let args = if musicOnly: buildMusicDownloadArgs(url) else: buildVideoDownloadArgs(url) - discard execShellCmd(&"youtube-dl {args.join(\" \")}") + if isInstalled("aria2c"): + 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 offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string = -- 2.43.0