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