]> njoseph.me Git - nimcoon.git/blame - src/lib.nim
PeerTube: Pick magnet link of the best resolution
[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}")
47 return $response.body
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: ""
84 return 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'"
046c2cc3 98 return @["--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'"
046c2cc3 103 return @["-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
d65a1dcf
JN
109func urlLongen(url: string): string =
110 url.replace("youtu.be/", "www.youtube.com/watch?v=")
111
112func stripZshEscaping(url: string): string =
113 url.replace("\\", "")
114
115func sanitizeURL*(url: string): string =
116 urlLongen(stripZshEscaping(url))
117
d36e2201 118proc directPlay*(url: string, player: string, options: Table[string, bool]) =
c12cc641
JN
119 let url =
120 if find(url, PEERTUBE_REGEX) != -1 and isInstalled("webtorrent"):
121 getPeerTubeMagnetLink(url)
122 else: url
d7688e97 123 if url.startswith("magnet:") or url.endswith(".torrent"):
d36e2201 124 if options["musicOnly"]:
8d06ad69 125 # TODO Replace with WebTorrent once it supports media player options
811928a7
JN
126 discard execShellCmd(&"peerflix '{url}' -a --{player} -- --no-video")
127 else:
8d06ad69
JN
128 # WebTorrent is so much faster!
129 discard execProcess("webtorrent", args=[url, &"--{player}"], options=processOptions)
fe1a5856 130 else:
d36e2201 131 play(player, options, url)
811928a7
JN
132
133proc directDownload*(url: string, musicOnly: bool) =
134 let args =
135 if musicOnly: buildMusicDownloadArgs(url)
136 else: buildVideoDownloadArgs(url)
9a8ef4ad 137 discard execShellCmd(&"youtube-dl {args.join(\" \")}")
13a4017d
JN
138
139proc offerSelection(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange): string =
140 if options["feelingLucky"]: "0"
141 else:
142 presentVideoOptions(searchResults[selectionRange.begin .. selectionRange.until])
143 stdout.styledWrite(fgYellow, "Choose video number: ")
144 readLine(stdin)
145
13a4017d
JN
146proc handleUserInput(searchResult: SearchResult, options: Table[string, bool], player: string) =
147 if options["download"]:
148 if options["musicOnly"]:
149 download(buildMusicDownloadArgs(searchResult.url), searchResult.title)
150 else:
151 download(buildVideoDownloadArgs(searchResult.url), searchResult.title)
152 else:
d36e2201 153 play(player, options, searchResult.url, searchResult.title)
13a4017d
JN
154
155proc present*(searchResults: SearchResults, options: Table[string, bool], selectionRange: SelectionRange, player: string) =
156 #[ Continuously present options till the user quits the application
157 selectionRange: Currently available range to choose from depending on pagination
158 ]#
159
160 let userInput = offerSelection(searchResults, options, selectionRange)
161
162 case userInput
163 of "all":
164 for selection in selectionRange.begin .. selectionRange.until:
165 handleUserInput(searchResults[selection], options, player)
166 quit(0)
167 of "n":
168 if selectionRange.until + 1 < len(searchResults):
169 let newSelectionRange = (selectionRange.until + 1, min(len(searchResults) - 1, selectionRange.until + limit))
170 present(searchResults, options, newSelectionRange, player)
ebae91b4
JN
171 else:
172 present(searchResults, options, selectionRange, player)
13a4017d
JN
173 of "p":
174 if selectionRange.begin > 0:
175 let newSelectionRange = (selectionRange.begin - limit, selectionRange.until - limit)
176 present(searchResults, options, newSelectionRange, player)
ebae91b4
JN
177 else:
178 present(searchResults, options, selectionRange, player)
13a4017d
JN
179 of "q":
180 quit(0)
181 else:
db34bbff
JN
182 let searchResult = searchResults[selectionRange.begin .. selectionRange.until][parseInt(userInput)]
183 handleUserInput(searchResult, options, player)
13a4017d
JN
184 if options["feelingLucky"]:
185 quit(0)
186 else:
187 present(searchResults, options, selectionRange, player)