X-Git-Url: http://njoseph.me/gitweb/nimcoon.git/blobdiff_plain/13a4017d99baaf7919263edaf05b13063e91cb8d..HEAD:/src/lib.nim diff --git a/src/lib.nim b/src/lib.nim index 3d3f515..4064ae4 100644 --- a/src/lib.nim +++ b/src/lib.nim @@ -1,109 +1,200 @@ import - htmlparser, - httpClient, os, osproc, + re, sequtils, - sugar, - strformat, std/[terminal], strformat, - strtabs, strutils, - tables, - uri, - xmltree + tables + +import + config, + peertube, + types, + youtube + -import config +let + processOptions = {poStdErrToStdOut, poUsePath} # Add poEchoCmd to debug + PEERTUBE_REGEX = re"w\/[0-9a-zA-z]{22}" -type - Options* = Table[string, bool] - SearchResult* = tuple[title: string, url: string] - SearchResults* = seq[tuple[title: string, url: string]] - CommandLineOptions* = tuple[searchQuery: string, options: Options] - SelectionRange* = tuple[begin: int, until: int] -# poEchoCmd can be added to options for debugging -let processOptions = {poStdErrToStdOut, poUsePath} +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") 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}") - return $response.body -func extractTitlesAndUrls*(html: string): SearchResults = +proc printTitle(action: string, title: string) = + styledEcho "\n", fgGreen, &"{action} ", styleBright, fgMagenta, title + + +############### +# URL CLEANUP # +############### + +func rewriteInvidiousToYouTube*(url: string): string = {.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"])) + 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" -proc play*(player: string, args: openArray[string], title: string) = - styledEcho "\n", fgGreen, "Playing ", styleBright, fgMagenta, title +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: "" + 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 != "": + printTitle("Playing", title) if "--no-video" in args: discard execShellCmd(&"{player} {args.join(\" \")}") else: discard execProcess(player, args=args, options=processOptions) -func buildMusicDownloadArgs*(url: string): seq[string] = + +proc directPlay*(url: string, player: string, options: Table[string, bool]) = + let url = + if find(url, PEERTUBE_REGEX) != -1 and "webtorrent".isInstalled: + getPeerTubeMagnetLink(url, options["musicOnly"]) + else: url + if url.startswith("magnet:") or url.endswith(".torrent"): + if options["musicOnly"]: + discard execShellCmd(&"webtorrent '{url}' --{player} --player-args='--no-video'") + else: + discard execProcess("webtorrent", args=[url, &"--{player}"], options=processOptions) + else: + play(player, options, url) + + +############ +# DOWNLOAD # +############ + +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 + @["--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.}: - var args = @["-f", "best", "-o"] let downloadLocation = &"'{expandTilde(videoDownloadDirectory)}/%(title)s.%(ext)s'" - args.add(downloadLocation) - args.add(url) - return args + @["-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 buildDownloadArgs(url: string, options: Options): seq[string] = + if options["musicOnly"]: buildMusicDownloadArgs(url) + else: buildVideoDownloadArgs(url) -func stripZshEscaping(url: string): string = - url.replace("\\", "") -func sanitizeURL*(url: string): string = - urlLongen(stripZshEscaping(url)) +proc download*(args: openArray[string], title: string) = + printTitle("Downloading", title) + discard execShellCmd(&"yt-dlp {args.join(\" \")}") -proc directPlay*(url: string, player: string, musicOnly: bool) = - if url.startswith("magnet:"): - if musicOnly: - discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video") - else: - discard execProcess("peerflix", args=[url, &"--{player}"], options=processOptions) + +proc directDownload*(url: string, options: Options) = + let args = buildDownloadArgs(url, options) + if "aria2c".isInstalled: + discard execShellCmd(&"yt-dlp {args.join(\" \")} --external-downloader aria2c --external-downloader-args '-x 16 -s 16 -k 2M'") else: - if musicOnly: - discard execShellCmd(&"{player} --no-video {url}") + discard execShellCmd(&"yt-dlp {args.join(\" \")}") + +proc luckyDownload*(searchQuery: string, options: Options) = + let args = @[&"ytsearch1:\"{searchQuery}\""] & buildDownloadArgs("", options) + let title = execProcess(&"yt-dlp --get-title {args.join(\" \")}").split("\n")[0] + download(args, title) + +proc luckyPlay*(searchQuery: string, player: string, options: Options) = + let args = @[&"ytsearch:\"{searchQuery}\""] & buildDownloadArgs("", options) + let output = execProcess(&"yt-dlp --get-url --get-title {args.join(\" \")}").split("\n") + let + title = output[0] + url = &"\"{output[1]}\"" + play(player, options, url, title) + +########### +# OPTIONS # +########### + +proc isValidOptions*(options: Options): bool = + # Check for invalid combinations of options + 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: - discard execProcess(player, args=[url], options=processOptions) + stderr.writeLine "Invalid option provided!" + quit(2) + + if(not isValidOptions(result)): + 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 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" @@ -112,26 +203,12 @@ 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"]: - download(buildMusicDownloadArgs(searchResult.url), searchResult.title) - else: - download(buildVideoDownloadArgs(searchResult.url), searchResult.title) - else: - play(player, buildPlayerArgs(searchResult, options), 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) @@ -144,14 +221,25 @@ proc present*(searchResults: SearchResults, options: Table[string, bool], select if selectionRange.until + 1 < len(searchResults): let newSelectionRange = (selectionRange.until + 1, min(len(searchResults) - 1, selectionRange.until + limit)) present(searchResults, options, newSelectionRange, player) + else: + present(searchResults, options, selectionRange, player) of "p": if selectionRange.begin > 0: let newSelectionRange = (selectionRange.begin - limit, selectionRange.until - limit) present(searchResults, options, newSelectionRange, player) + else: + present(searchResults, options, selectionRange, player) of "q": quit(0) else: - handleUserInput(searchResults[parseInt(userInput)], options, player) + if " " in userInput: + let selection = parseInt(userInput.split(" ")[0]) + let updatedOptions = updateOptions(options, userInput.split(" ")[1]) + let searchResult = searchResults[selectionRange.begin .. selectionRange.until][selection] + handleUserInput(searchResult, updatedOptions, player) + else: + let searchResult = searchResults[selectionRange.begin .. selectionRange.until][parseInt(userInput)] + handleUserInput(searchResult, options, player) if options["feelingLucky"]: quit(0) else: