]> njoseph.me Git - nimcoon.git/blame - src/lib.nim
VLC is better than CVLC
[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,
c12cc641 11 strformat,
d65a1dcf
JN
12 strtabs,
13 strutils,
c12cc641 14 sugar,
6f161e0b 15 tables,
d65a1dcf
JN
16 uri,
17 xmltree
18
19import config
20
b44b6494 21
d65a1dcf 22type
6f161e0b 23 Options* = Table[string, bool]
e9f0c7d0 24 SearchResult* = tuple[title: string, url: string]
13a4017d 25 SearchResults* = seq[tuple[title: string, url: string]]
6f161e0b 26 CommandLineOptions* = tuple[searchQuery: string, options: Options]
e6561dc9 27 SelectionRange* = tuple[begin: int, until: int]
d65a1dcf 28
b44b6494 29
b44b6494 30let
86e6cb72 31 processOptions = {poStdErrToStdOut, poUsePath} # poEchoCmd can be added to options for debugging
b44b6494
JN
32 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}"
33
c12cc641
JN
34
35proc isInstalled(program: string): bool =
36 execProcess("which " & program).len != 0
9e6b8568 37
b44b6494 38
d65a1dcf 39proc selectMediaPlayer*(): string =
c12cc641 40 let availablePlayers = supportedPlayers.filter(isInstalled)
d65a1dcf
JN
41 if len(availablePlayers) == 0:
42 stderr.writeLine &"Please install one of the supported media players: {supportedPlayers}"
43 raise newException(OSError, "No supported media player found")
44 else:
45 return availablePlayers[0]
46
b44b6494 47
d65a1dcf
JN
48proc getYoutubePage*(searchQuery: string): string =
49 let queryParam = encodeUrl(searchQuery)
50 let client = newHttpClient()
51 let response = get(client, &"https://www.youtube.com/results?hl=en&search_query={queryParam}")
55b0cae7 52 $response.body
d65a1dcf 53
b44b6494 54
c12cc641 55proc getPeerTubeMagnetLink(url: string): string =
86e6cb72 56 ## Gets the magnet link of the best possible resolution from PeerTube
c12cc641
JN
57 let uuid = url.substr(find(url, PEERTUBE_REGEX) + "videos/watch/".len)
58 let domainName = url.substr(8, find(url, '/', start=8) - 1)
59 let apiURL = &"https://{domainName}/api/v1/videos/{uuid}"
60 let client = newHttpClient()
61 let response = get(client, apiURL)
62 let jsonNode = parseJson($response.body)
63 jsonNode["files"][0]["magnetUri"].getStr()
64
b44b6494 65
13a4017d 66func extractTitlesAndUrls*(html: string): SearchResults =
4a0587e2
JN
67 {.noSideEffect.}:
68 parseHtml(html).findAll("a").
69 filter(a => "watch" in a.attrs["href"] and a.attrs.hasKey "title").
72720bec 70 map(a => (a.attrs["title"], "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)
9a8ef4ad 159 discard execShellCmd(&"youtube-dl {args.join(\" \")}")
13a4017d 160
b44b6494 161
13a4017d
JN
162proc offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string =
163 if options["feelingLucky"]: "0"
164 else:
165 presentVideoOptions(searchResults[selectionRange.begin .. selectionRange.until])
166 stdout.styledWrite(fgYellow, "Choose video number: ")
167 readLine(stdin)
168
b44b6494 169
13a4017d
JN
170proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) =
171 if options["download"]:
172 if options["musicOnly"]:
173 download(buildMusicDownloadArgs(searchResult.url), searchResult.title)
174 else:
175 download(buildVideoDownloadArgs(searchResult.url), searchResult.title)
176 else:
d36e2201 177 play(player, options, searchResult.url, searchResult.title)
13a4017d 178
b44b6494 179
13a4017d 180proc present*(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange, player: string) =
55b0cae7
JN
181 ##[ Continuously present options till the user quits the application
182
183 selectionRange: Currently available range to choose from depending on pagination
184 ]##
13a4017d
JN
185
186 let userInput = offerSelection(searchResults, options, selectionRange)
187
188 case userInput
189 of "all":
190 for selection in selectionRange.begin .. selectionRange.until:
191 handleUserInput(searchResults[selection], options, player)
192 quit(0)
193 of "n":
194 if selectionRange.until + 1 < len(searchResults):
195 let newSelectionRange = (selectionRange.until + 1, min(len(searchResults) - 1, selectionRange.until + limit))
196 present(searchResults, options, newSelectionRange, player)
ebae91b4
JN
197 else:
198 present(searchResults, options, selectionRange, player)
13a4017d
JN
199 of "p":
200 if selectionRange.begin > 0:
201 let newSelectionRange = (selectionRange.begin - limit, selectionRange.until - limit)
202 present(searchResults, options, newSelectionRange, player)
ebae91b4
JN
203 else:
204 present(searchResults, options, selectionRange, player)
13a4017d
JN
205 of "q":
206 quit(0)
207 else:
db34bbff
JN
208 let searchResult = searchResults[selectionRange.begin .. selectionRange.until][parseInt(userInput)]
209 handleUserInput(searchResult, options, player)
13a4017d
JN
210 if options["feelingLucky"]:
211 quit(0)
212 else:
213 present(searchResults, options, selectionRange, player)