]> njoseph.me Git - nimcoon.git/blame - src/lib.nim
Use Invidio.us to retrieve search results
[nimcoon.git] / src / lib.nim
CommitLineData
d65a1dcf
JN
1import
2 htmlparser,
3 httpClient,
c12cc641 4 json,
e9f0c7d0 5 os,
d65a1dcf 6 osproc,
c12cc641 7 re,
d65a1dcf 8 sequtils,
d65a1dcf 9 std/[terminal],
e9f0c7d0 10 strformat,
d65a1dcf
JN
11 strtabs,
12 strutils,
c12cc641 13 sugar,
6f161e0b 14 tables,
d65a1dcf
JN
15 uri,
16 xmltree
17
18import config
19
b44b6494 20
d65a1dcf 21type
6f161e0b 22 Options* = Table[string, bool]
e9f0c7d0 23 SearchResult* = tuple[title: string, url: string]
13a4017d 24 SearchResults* = seq[tuple[title: string, url: string]]
6f161e0b 25 CommandLineOptions* = tuple[searchQuery: string, options: Options]
e6561dc9 26 SelectionRange* = tuple[begin: int, until: int]
d65a1dcf 27
b44b6494 28
b44b6494 29let
86e6cb72 30 processOptions = {poStdErrToStdOut, poUsePath} # poEchoCmd can be added to options for debugging
b44b6494
JN
31 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}"
32
c12cc641
JN
33
34proc isInstalled(program: string): bool =
35 execProcess("which " & program).len != 0
9e6b8568 36
b44b6494 37
d65a1dcf 38proc selectMediaPlayer*(): string =
c12cc641 39 let availablePlayers = supportedPlayers.filter(isInstalled)
d65a1dcf
JN
40 if len(availablePlayers) == 0:
41 stderr.writeLine &"Please install one of the supported media players: {supportedPlayers}"
42 raise newException(OSError, "No supported media player found")
43 else:
44 return availablePlayers[0]
45
b44b6494 46
d65a1dcf
JN
47proc getYoutubePage*(searchQuery: string): string =
48 let queryParam = encodeUrl(searchQuery)
49 let client = newHttpClient()
30843c18 50 let response = get(client, &"https://invidio.us/search?q={queryParam}")
55b0cae7 51 $response.body
d65a1dcf 52
b44b6494 53
c12cc641 54proc getPeerTubeMagnetLink(url: string): string =
86e6cb72 55 ## Gets the magnet link of the best possible resolution from PeerTube
c12cc641
JN
56 let uuid = url.substr(find(url, PEERTUBE_REGEX) + "videos/watch/".len)
57 let domainName = url.substr(8, find(url, '/', start=8) - 1)
58 let apiURL = &"https://{domainName}/api/v1/videos/{uuid}"
59 let client = newHttpClient()
60 let response = get(client, apiURL)
61 let jsonNode = parseJson($response.body)
62 jsonNode["files"][0]["magnetUri"].getStr()
63
b44b6494 64
13a4017d 65func extractTitlesAndUrls*(html: string): SearchResults =
4a0587e2 66 {.noSideEffect.}:
30843c18 67 # TODO Pick an invidious instance from config. Using YouTube directly for now.
4a0587e2 68 parseHtml(html).findAll("a").
30843c18
JN
69 filter(a => "watch" in a.attrs["href"] and len(a) == 1).
70 map(a => (innerText(a), "https://www.youtube.com" & a.attrs["href"]))
d65a1dcf 71
b44b6494 72
13a4017d 73proc presentVideoOptions*(searchResults: SearchResults) =
17955bba 74 eraseScreen()
d65a1dcf 75 for index, (title, url) in searchResults:
86e6cb72 76 styledEcho $index, ". ", styleBright, fgMagenta, title, "\n", resetStyle, fgCyan, " ", url, "\n"
d65a1dcf 77
b44b6494 78
d36e2201 79func isPlaylist(url: string): bool =
b44b6494
JN
80 ##[ Identifies if video is part of a playlist.
81 Only YouTube playlists are supported for now. ]##
d36e2201
JN
82 "www.youtube.com" in url and "&list=" in url
83
b44b6494 84
d36e2201 85func buildPlayerArgs(url: string, options: Table[string, bool], player: string): seq[string] =
046c2cc3 86 let url =
b44b6494 87 # Playlists are only supported by MPV player. VLC needs a plugin.
046c2cc3
JN
88 if isPlaylist(url) and player == "mpv":
89 "https://www.youtube.com/playlist?" & url.split('&')[1]
90 else: url
91 let musicOnly = if options["musicOnly"]: "--no-video" else: ""
92 let fullScreen = if options["fullScreen"]: "--fullscreen" else: ""
55b0cae7 93 filterIt([url, musicOnly, fullScreen], it != "")
d36e2201 94
b44b6494 95
d36e2201
JN
96proc play*(player: string, options: Table[string, bool], url: string, title: string = "") =
97 let args = buildPlayerArgs(url, options, player)
98 if title != "":
99 styledEcho "\n", fgGreen, "Playing ", styleBright, fgMagenta, title
e71f26f3
JN
100 if "--no-video" in args:
101 discard execShellCmd(&"{player} {args.join(\" \")}")
102 else:
103 discard execProcess(player, args=args, options=processOptions)
d65a1dcf 104
b44b6494
JN
105
106func buildMusicDownloadArgs(url: string): seq[string] =
9a8ef4ad 107 {.noSideEffect.}:
9a8ef4ad 108 let downloadLocation = &"'{expandTilde(musicDownloadDirectory)}/%(title)s.%(ext)s'"
b44b6494
JN
109 @["--ignore-errors", "-f", "bestaudio", "--extract-audio", "--audio-format", "mp3",
110 "--audio-quality", "0", "-o", downloadLocation, url]
111
9a8ef4ad 112
b44b6494 113func buildVideoDownloadArgs(url: string): seq[string] =
9a8ef4ad 114 {.noSideEffect.}:
9a8ef4ad 115 let downloadLocation = &"'{expandTilde(videoDownloadDirectory)}/%(title)s.%(ext)s'"
55b0cae7 116 @["-f", "best", "-o", downloadLocation, url]
9a8ef4ad 117
b44b6494 118
e9f0c7d0
JN
119proc download*(args: openArray[string], title: string) =
120 styledEcho "\n", fgGreen, "Downloading ", styleBright, fgMagenta, title
121 discard execShellCmd(&"youtube-dl {args.join(\" \")}")
122
b44b6494 123
55b0cae7 124func urlLongen(url: string): string = url.replace("youtu.be/", "www.youtube.com/watch?v=")
d65a1dcf 125
b44b6494
JN
126
127func rewriteInvidiousToYouTube(url: string): string =
128 {.noSideEffect.}:
129 if rewriteInvidiousURLs: url.replace("invidio.us", "www.youtube.com") else: url
130
131
55b0cae7 132func stripZshEscaping(url: string): string = url.replace("\\", "")
d65a1dcf 133
b44b6494
JN
134
135func sanitizeURL*(url: string): string =
136 rewriteInvidiousToYouTube(urlLongen(stripZshEscaping(url)))
137
d65a1dcf 138
d36e2201 139proc directPlay*(url: string, player: string, options: Table[string, bool]) =
c12cc641
JN
140 let url =
141 if find(url, PEERTUBE_REGEX) != -1 and isInstalled("webtorrent"):
142 getPeerTubeMagnetLink(url)
143 else: url
d7688e97 144 if url.startswith("magnet:") or url.endswith(".torrent"):
d36e2201 145 if options["musicOnly"]:
8d06ad69 146 # TODO Replace with WebTorrent once it supports media player options
811928a7
JN
147 discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video")
148 else:
8d06ad69
JN
149 # WebTorrent is so much faster!
150 discard execProcess("webtorrent", args=[url, &"--{player}"], options=processOptions)
fe1a5856 151 else:
d36e2201 152 play(player, options, url)
811928a7 153
b44b6494 154
811928a7
JN
155proc directDownload*(url: string, musicOnly: bool) =
156 let args =
157 if musicOnly: buildMusicDownloadArgs(url)
158 else: buildVideoDownloadArgs(url)
0c2f0385
JN
159 if isInstalled("aria2c"):
160 discard execShellCmd(&"youtube-dl {args.join(\" \")} --external-downloader aria2c --external-downloader-args '-x 16 -s 16 -k 2M'")
161 else:
162 discard execShellCmd(&"youtube-dl {args.join(\" \")}")
13a4017d 163
b44b6494 164
13a4017d
JN
165proc offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string =
166 if options["feelingLucky"]: "0"
167 else:
168 presentVideoOptions(searchResults[selectionRange.begin .. selectionRange.until])
169 stdout.styledWrite(fgYellow, "Choose video number: ")
170 readLine(stdin)
171
b44b6494 172
13a4017d
JN
173proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) =
174 if options["download"]:
175 if options["musicOnly"]:
176 download(buildMusicDownloadArgs(searchResult.url), searchResult.title)
177 else:
178 download(buildVideoDownloadArgs(searchResult.url), searchResult.title)
179 else:
d36e2201 180 play(player, options, searchResult.url, searchResult.title)
13a4017d 181
b44b6494 182
13a4017d 183proc present*(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange, player: string) =
55b0cae7
JN
184 ##[ Continuously present options till the user quits the application
185
186 selectionRange: Currently available range to choose from depending on pagination
187 ]##
13a4017d
JN
188
189 let userInput = offerSelection(searchResults, options, selectionRange)
190
191 case userInput
192 of "all":
193 for selection in selectionRange.begin .. selectionRange.until:
194 handleUserInput(searchResults[selection], options, player)
195 quit(0)
196 of "n":
197 if selectionRange.until + 1 < len(searchResults):
198 let newSelectionRange = (selectionRange.until + 1, min(len(searchResults) - 1, selectionRange.until + limit))
199 present(searchResults, options, newSelectionRange, player)
ebae91b4
JN
200 else:
201 present(searchResults, options, selectionRange, player)
13a4017d
JN
202 of "p":
203 if selectionRange.begin > 0:
204 let newSelectionRange = (selectionRange.begin - limit, selectionRange.until - limit)
205 present(searchResults, options, newSelectionRange, player)
ebae91b4
JN
206 else:
207 present(searchResults, options, selectionRange, player)
13a4017d
JN
208 of "q":
209 quit(0)
210 else:
db34bbff
JN
211 let searchResult = searchResults[selectionRange.begin .. selectionRange.until][parseInt(userInput)]
212 handleUserInput(searchResult, options, player)
13a4017d
JN
213 if options["feelingLucky"]:
214 quit(0)
215 else:
216 present(searchResults, options, selectionRange, player)