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