├── .imdb.swp ├── LICENSE ├── README.md ├── bin └── imdb ├── imdb-terminal-0.7.4.gem ├── img └── screenshot.png └── rsh.gemspec /.imdb.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isene/IMDB/682ff7a39fb6be842a32f21dbdd18c4c11f7f900/.imdb.swp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Ruby](https://img.shields.io/badge/language-Ruby-red) [![Gem Version](https://badge.fury.io/rb/imdb-terminal.svg)](https://badge.fury.io/rb/imdb-terminal) ![Unlicense](https://img.shields.io/badge/license-Unlicense-green) ![Stay Amazing](https://img.shields.io/badge/Stay-Amazing-important) 3 | 4 | # IMDB for the terminal 5 | Finding the right moviie to watch or series to binge can be time consuming. 6 | [My girlfriend](https://isene.org/2022/04/Siv.html) asked if I could create an 7 | application that would cut down the time spent on searching in favor of time 8 | spent watching and cuddling. Meet IMDB for the terminal. 9 | 10 | Narrow down your preferences from a 1000 movies and almost 500 series. Select 11 | a minimum IMDB rating, range of production years, genres you like and dislike 12 | to get your preferred list. Get detailed information on movies and series and 13 | where you can stream them. Even the movie poster in the terminal. 14 | 15 | IMDB for the terminal uses three APIs - one to scrape IMDB for the top 1000 16 | lists, one to give you details on the movies and series and one to show you 17 | where you can stream it. When you first run the application, it will guide you 18 | through the process of registering free accounts and get the three API keys 19 | needed. I would be happy to keys in the application, but there is a limit to 20 | the number of calls each day, so you need your own keys. The key to scrape the 21 | IMDB top 1000 is via `page2api`. You will get a certain amounts of credits for 22 | free when you sign up, and it will last you a very long time since the program 23 | caches the movies and series lists. You could probably do well with refreshing 24 | the lists once per month or so (using the key `I`). 25 | 26 | As for the application itself, it's a terminal curses program that is best 27 | installed with `gem install imdb-terminal` and run the program `imdb`in the 28 | terminal. Or you can clone this repo and run the single file `imdb`. As usual, 29 | I try to keep the requirements to a minimum and the program to a single file. 30 | 31 | # Screenshot 32 | The screenshot gives you an overview of the layout of the panes: 33 | ![IMDB-terminal screenshot](img/screenshot.png) 34 | 35 | # Keymap 36 | Keys | Function 37 | -------------|-------------------- 38 | TAB or S-TAB | Cycle panes (active is gray) or backwards 39 | Arrow keys | UP, DOWN, PgUP, PgDOWN, HOME, END in lists 40 | \+ or - | Depends on pane (intuitive) 41 | I | Load fresh IMDB data (be patient) 42 | m or s | Show MOVIES or SERIES 43 | r | Select MINimum IMDB rating (in bottom line) 44 | y or Y | Select MINimum or MAXimum production year 45 | / or \\ | Enter or clear search for movie/series title 46 | G | Set genres to match every movie/series 47 | d | Get details on selected movie/series 48 | D | Show where you can stream the movie/series 49 | R | Refresh all panes 50 | v | Show version (and latest RybyGems version) 51 | w or W | Write changes to config or save IMDB data 52 | q or Q | Quit w/saving config or w/o saving config 53 | 54 | # Suggestions and improvements 55 | If you have any suggestions for improvement or cool features, create an issue 56 | in this repository. 57 | 58 | For more stuff, check out https://isene.com 59 | -------------------------------------------------------------------------------- /bin/imdb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | # PROGRAM INFO 5 | # Name: IMDB-term 6 | # Language: Pure Ruby, best viewed in VIM 7 | # Author: Geir Isene 8 | # Web_site: http://isene.com/ 9 | # Github: https://github.com/isene/imdb-term 10 | # License: I release all copyright claims. This code is in the public domain. 11 | # Permission is granted to use, copy modify, distribute, and sell 12 | # this software for any purpose. I make no guarantee about the 13 | # suitability of this software for any purpose and I am not liable 14 | # for any damages resulting from its use. Further, I am under no 15 | # obligation to maintain or extend this software. It is provided 16 | # on an 'as is' basis without any expressed or implied warranty. 17 | @version = "0.7.4" 18 | 19 | # PRELIMINARIES 20 | @help = < /dev/null 2>&1") 53 | end 54 | if cmd?('/usr/lib/w3m/w3mimgdisplay') 55 | @w3mimgdisplay = "/usr/lib/w3m/w3mimgdisplay" 56 | @showimage = true 57 | else 58 | @showimage = false 59 | end 60 | @showimage = false unless (cmd?('xwininfo') and cmd?('xdotool')) 61 | 62 | begin # Check if network is available 63 | URI.open("https://www.google.com/", :open_timeout=>5) 64 | rescue 65 | puts "\nNo network. Running offline.\n\n" 66 | end 67 | 68 | # INITIALIZE BASIC VARIABLES 69 | ## These can be set in .imdb.conf 70 | @rating = 0 71 | @yearMin = 0 72 | @yearMax = 2100 73 | @myMY = [] 74 | @myMN = [] 75 | @mySY = [] 76 | @mySN = [] 77 | ## These are IMDB specific 78 | @urlmovies = "https://www.imdb.com/search/title/?groups=top_1000&start=[1, 1001, 50]" 79 | @urlseries = "https://www.imdb.com/search/title/?title_type=tv_series&start=[1, 1001, 50]" 80 | @country = "no" 81 | @lang = "en" 82 | @genres = ["Action", "Adult", "Adventure", "Animation", "Biography", "Comedy", "Crime", "Documentary", "Drama", 83 | "Family", "Fantasy", "Film Noir", "Game Show", "History", "Horror", "Musical", "Music", "Mystery", 84 | "News", "Reality-TV", "Romance", "Sci-Fi", "Short", "Sport", "Talk-Show", "Thriller", "War", "Western"] 85 | @genY = @genres.map(&:clone) 86 | @genN = [] 87 | ## These are internal variables 88 | @imdbmovies = [] 89 | @imdbseries = [] 90 | @imdbsel = [] 91 | @search = '' 92 | @new = false 93 | @movies = true 94 | @noimage = false 95 | 96 | ## Color constants (e.g. Window x fg) 97 | WtMfg = 232 98 | WtSfg = 255 99 | WtMbg = 214 100 | WtMbgS = 202 101 | WtSbg = 23 102 | WtSbgS = 58 103 | Wifg = 230 104 | Wgfg = 195 105 | WgYfg = 34 106 | WgNfg = 241 107 | Wmfg = 117 108 | Wnfg = 204 109 | Wdfg = 255 110 | Wpfg = 242 111 | Wbfg = 252 112 | Wbbg = 238 113 | Mark = 240 114 | WAbg = 233 115 | WIbg = 232 116 | end 117 | 118 | # CLASSES 119 | class Curses::Window # CLASS EXTENSION 120 | # General extensions (see https://github.com/isene/Ruby-Curses-Class-Extension) 121 | # This is a class extension to Ruby Curses - a class in dire need of such. 122 | # self.pair keeps a registry of colors as they are encountered - defined with: 123 | # init_pair(index, foreground, background) 124 | # self.fg is set for the foreground color 125 | # self.bg is set for the background color 126 | # self.attr is set for text attributes like Curses::A_BOLD 127 | # self.update can be used to indicate if a window should be updated (true/false) 128 | # self.index can be used to keep track of the current list item in a window 129 | attr_accessor :fg, :bg, :attr, :text, :update, :index, :list 130 | def self.pair(fg, bg) 131 | @p = [[]] if @p == nil 132 | fg = fg.to_i; bg = bg.to_i 133 | if @p.include?([fg,bg]) 134 | @p.index([fg,bg]) 135 | else 136 | @p.push([fg,bg]) 137 | cp = @p.index([fg,bg]) 138 | init_pair(cp, fg, bg) 139 | @p.index([fg,bg]) 140 | end 141 | end 142 | def clr # Clears the whole window 143 | self.setpos(0, 0) 144 | self.maxy.times {self.deleteln()} 145 | self.refresh 146 | self.setpos(0, 0) 147 | end 148 | def fill # Fill window with color as set by self.color (or self.bg if not set) 149 | self.setpos(0, 0) 150 | self.fill_from_cur_pos 151 | end 152 | def fill_to_cur_pos # Fills the window up to current line 153 | x = self.curx 154 | y = self.cury 155 | self.setpos(0, 0) 156 | self.bg = 0 if self.bg == nil 157 | self.fg = 255 if self.fg == nil 158 | blank = " " * self.maxx 159 | cp = Curses::Window.pair(self.fg, self.bg) 160 | y.times {self.attron(color_pair(cp)) {self << blank}} 161 | self.refresh 162 | self.setpos(y, x) 163 | end 164 | def fill_from_cur_pos # Fills the rest of the window from current line 165 | x = self.curx 166 | y = self.cury 167 | self.setpos(y, 0) 168 | self.bg = 0 if self.bg == nil 169 | self.fg = 255 if self.fg == nil 170 | blank = " " * self.maxx 171 | cp = Curses::Window.pair(self.fg, self.bg) 172 | self.maxy.times {self.attron(color_pair(cp)) {self << blank}} 173 | self.refresh 174 | self.setpos(y, x) 175 | end 176 | def p(fg = self.fg, bg = self.bg, attr = self.attr, text) # Puts text to window with full set of attributes 177 | fg = 255 if fg == nil 178 | bg = 0 if bg == nil 179 | attr = 0 if attr == nil 180 | cp = Curses::Window.pair(fg, bg) 181 | self.attron(color_pair(cp) | attr) { self << text } 182 | self.refresh 183 | end 184 | def nl(bg = self.bg) 185 | bg = 232 if bg == nil 186 | f = " " * (self.maxx - self.curx) 187 | self.p(self.fg, bg, self.attr, f) 188 | end 189 | def format(text) # Format text so that it linebreaks neatly inside window 190 | return "\n" + text.gsub(/(.{1,#{self.maxx-1}})( +|$\n?)|(.{1,#{self.maxx-1}})/, "\\1\\3\n") 191 | end 192 | alias :puts :p 193 | end 194 | 195 | # GENERIC FUNCTIONS 196 | def firstrun 197 | puts "Welcome to IMDB-term, the IMDB application for the terminal." 198 | puts "\nFind your next movie or series to binge. Narrow down your preferences from a 1000 movies and almost 500 series." 199 | puts "Select a minimum IMDB rating, range of production years, genres you like and dislike to get your preferred list." 200 | puts "Get detailed information on movies and series and where you can stream them. Even the movie poster in the terminal." 201 | puts "\nLet's first look at the help text (accessible in the program via the key '?'):\n\n" 202 | puts @help 203 | print "\nPress any key... "; STDIN.getch 204 | system("clear") 205 | puts "We will now walk you through the steps you need to do to make use of this application:" 206 | puts "\n 1. Go to the website https://www.page2api.com/" 207 | print " Create a free account and paste your API_KEY here (then press ENTER): " 208 | conf = "@imdbkey = '" 209 | conf += gets.chomp + "'\n" 210 | puts "\n 2. Go to the website https://www.omdbapi.com/apikey.aspx" 211 | print " Create a free account and paste your API KEY here (then press ENTER): " 212 | conf += "@omdbkey = '" 213 | conf += gets.chomp + "'\n" 214 | puts "\n 3. Go to the website https://rapidapi.com/movie-of-the-night-movie-of-the-night-default/api/streaming-availability" 215 | print " Create a free account and paste your X-RapidAPI-Key here (then press ENTER): " 216 | conf += "@streamkey = '" 217 | conf += gets.chomp + "'\n" 218 | File.write(Dir.home+'/.imdb.conf', conf) 219 | puts "\n\nYour keys have now been written to the configuration file (.imdb.conf). You can edit this file manually if needed." 220 | print "\nPress 'y' to start imdb-term "; y = STDIN.getch 221 | exit if y != "y" 222 | end 223 | def getchr # PROCESS KEY PRESSES 224 | c = STDIN.getch #(min: 0, time: 1) 225 | case c 226 | when "\e" # ANSI escape sequences 227 | case $stdin.getc 228 | when '[' # CSI 229 | case $stdin.getc 230 | when 'A' then chr = "UP" 231 | when 'B' then chr = "DOWN" 232 | when 'C' then chr = "RIGHT" 233 | when 'D' then chr = "LEFT" 234 | when 'Z' then chr = "S-TAB" 235 | when '2' then chr = "INS" ; STDIN.getc 236 | when '3' then chr = "DEL" ; STDIN.getc 237 | when '5' then chr = "PgUP" ; STDIN.getc 238 | when '6' then chr = "PgDOWN" ; STDIN.getc 239 | when '7' then chr = "HOME" ; STDIN.getc 240 | when '8' then chr = "END" ; STDIN.getc 241 | end 242 | end 243 | when "", "" then chr = "BACK" 244 | when "" then chr = "C-C" 245 | when "" then chr = "C-G" 246 | when "" then chr = "C-T" 247 | when "" then chr = "LDEL" 248 | when "" then chr = "WBACK" 249 | when "\r" then chr = "ENTER" 250 | when "\t" then chr = "TAB" 251 | when /./ then chr = c 252 | end 253 | return chr 254 | end 255 | def getkey # GET KEY FROM USER 256 | chr = getchr 257 | case chr 258 | when '?' # Show helptext in right window 259 | @w_d.fill 260 | @w_d.p(@help) 261 | @w_d.update = false 262 | when 'UP' 263 | @active.index = @active.index <= 0 ? @active.list.size - 1 : @active.index - 1 264 | when 'DOWN' 265 | @active.index = @active.index >= @active.list.size - 1 ? 0 : @active.index + 1 266 | when 'PgUP' 267 | @active.index -= @active.maxy - 2 268 | @active.index = 0 if @active.index < 0 269 | when 'PgDOWN' 270 | @active.index += @active.maxy - 2 271 | @active.index = @active.list.size - 1 if @active.index > @active.list.size - 1 272 | when 'HOME' 273 | @active.index = 0 274 | when 'END' 275 | @active.index = @active.list.size - 1 276 | when 'TAB' 277 | case @active 278 | when @w_i 279 | @active = @w_g 280 | when @w_g 281 | @active = @w_m 282 | when @w_m 283 | @active = @w_n 284 | when @w_n 285 | @active = @w_i 286 | end 287 | when 'S-TAB' 288 | case @active 289 | when @w_i 290 | @active = @w_n 291 | when @w_n 292 | @active = @w_m 293 | when @w_m 294 | @active = @w_g 295 | when @w_g 296 | @active = @w_i 297 | end 298 | when 'I' 299 | w_b("Loading IMDB data...") 300 | loadimdb 301 | when 'm' 302 | @movies = true 303 | when 's' 304 | @movies = false 305 | when 'r' 306 | r = w_b_getstr(" Set MINimum Rating: ", "") 307 | @rating = r.to_f unless r == "" 308 | when 'y' 309 | y = w_b_getstr(" Set MINimum Year: ", "") 310 | @yearMin = y.to_i unless y == "" 311 | when 'Y' 312 | y = w_b_getstr(" Set MAXimum Year: ", "") 313 | @yearMax = y.to_i unless y == "" 314 | when '/' 315 | @search = w_b_getstr(" Search for title: ", "") 316 | when '\\' 317 | @search = '' 318 | when 'G' 319 | @genY = @genres.map(&:clone) 320 | @genN = [] 321 | when '+' 322 | case @active 323 | when @w_i 324 | @myY.push(@active.list[@active.index]) 325 | @active.index = 0 if @active.index > @active.list.size - 2 326 | when @w_g 327 | @genY.push(@active.list[@active.index]) unless @genN.include?(@active.list[@active.index]) 328 | @genN.delete(@active.list[@active.index]) 329 | @active.index = @active.index >= @active.list.size - 1 ? 0 : @active.index + 1 330 | when @w_m 331 | @myN.delete(@active.list[@active.index]) 332 | when @w_n 333 | @myY.push(@active.list[@active.index]) 334 | @myN.delete(@active.list[@active.index]) 335 | @active.index -= 1 if @active.index == @active.list.size 336 | end 337 | when '-' 338 | case @active 339 | when @w_i 340 | @myN.push(@active.list[@active.index]) 341 | @active.index = 0 if @active.index > @active.list.size - 2 342 | when @w_g 343 | @genN.push(@active.list[@active.index]) unless @genY.include?(@active.list[@active.index]) 344 | @genY.delete(@active.list[@active.index]) 345 | @active.index = @active.index >= @active.list.size - 1 ? 0 : @active.index + 1 346 | when @w_m 347 | @myY.delete(@active.list[@active.index]) 348 | @active.index -= 1 if @active.index == @active.list.size 349 | when @w_n 350 | @myN.delete(@active.list[@active.index]) 351 | @active.index -= 1 if @active.index == @active.list.size 352 | end 353 | when 'd' 354 | @w_d.fill 355 | w_d(1) 356 | @w_d.update = false 357 | when 'D' 358 | @w_d.fill 359 | w_d(2) 360 | @w_d.update = false 361 | when 'R' # Refresh all windows 362 | @break = true 363 | when '@' # Enter "Ruby debug" 364 | cmd = w_b_getstr("◆ ", "") 365 | begin 366 | @w_d.fill 367 | @w_d.p(eval(cmd)) 368 | rescue StandardError => e 369 | w_b("Error: #{e.inspect}") 370 | end 371 | @w_b.update = false 372 | when 'v' 373 | w_b("Version = #{@version} (latest RubyGems version is #{Gem.latest_version_for("imdb-terminal").version} - https://github.com/isene/IMDB)") 374 | when 'w' 375 | saveconf 376 | when 'W' 377 | saveimdb 378 | when 'q' # Exit 379 | saveconf 380 | exit 0 381 | when 'Q' # EXIT 382 | exit 0 383 | end 384 | end 385 | def getimdb(url) 386 | api_url = "https://www.page2api.com/api/v1/scrape" 387 | payload = { 388 | api_key: @imdbkey, 389 | batch: { 390 | urls: url, 391 | concurrency: 1, 392 | merge_results: true 393 | }, 394 | parse: { 395 | movies: [ 396 | { 397 | title: ".lister-item-header >> text", 398 | url: ".lister-item-header a >> href", 399 | id: ".userRatingValue >> data-tconst", 400 | year: ".lister-item-year >> text", 401 | genre: ".genre >> text", 402 | votes: "[name=nv] >> text", 403 | rating: ".ratings-imdb-rating >> data-value", 404 | _parent: ".lister-item", 405 | runtime: ".runtime >> text", 406 | certificate: ".certificate >> text" 407 | } 408 | ] 409 | }, 410 | datacenter_proxy: "us" 411 | } 412 | response = RestClient::Request.execute( 413 | method: :post, 414 | payload: payload.to_json, 415 | url: api_url, 416 | headers: { "Content-type" => "application/json" }, 417 | ).body 418 | res = JSON.parse(response) 419 | imdb = [] 420 | res["result"]["movies"].each {|m| imdb.push [m["title"].sub(/^\d+\. /, ''), m["rating"].to_f, m["year"].delete("^0-9").to_i, m["genre"], m["id"]]} 421 | imdb 422 | end 423 | def loadimdb 424 | @imdbmovies = getimdb(@urlmovies) 425 | @imdbseries = getimdb(@urlseries) 426 | @new = true 427 | end 428 | def imdbmovies 429 | if @search == '' 430 | @imdbsel = @imdbmovies.map{|m| m if (m[1] >= @rating) and ((@yearMin..@yearMax) === m[2])} 431 | else 432 | @imdbsel = @imdbmovies.map{|m| m if (m[0] =~ /#{@search}/)} 433 | end 434 | @imdbsel.select! do |i| 435 | ig = i[3].split(", ") unless i == nil 436 | (ig & @genY).any? unless ig == nil 437 | end 438 | @imdbsel.select! {|i| (i[3].split(", ") & @genN).empty?} 439 | @imdbsel = @imdbsel - @myMN - @myMY 440 | @myMYsel = @myMY - @myMN 441 | [@imdbsel, @myMYsel, @myMY, @myMN, @genY, @genN].each do |arr| 442 | arr.uniq! 443 | arr.compact! 444 | arr.sort_by! {|m| m[0]} 445 | end 446 | @w_i.list = @imdbsel 447 | @w_i.index = 0 if @w_i.index > @w_i.list.size - 1 448 | @w_m.list = @myMYsel 449 | @w_n.list = @myMN 450 | @myN = @myMN 451 | @myY = @myMY 452 | end 453 | def imdbseries 454 | if @search == '' 455 | @imdbsel = @imdbseries.map{|m| m if (m[1] > @rating) and ((@yearMin..@yearMax) === m[2])} 456 | else 457 | @imdbsel = @imdbseries.map{|m| m if (m[0] =~ /#{@search}/)} 458 | end 459 | @imdbsel.select! do |i| 460 | ig = i[3].split(", ") unless i == nil 461 | (ig & @genY).any? unless ig == nil 462 | end 463 | @imdbsel.select! {|i| (i[3].split(", ") & @genN).empty?} 464 | @imdbsel = @imdbsel - @mySN - @mySY 465 | @mySYsel = @mySY - @mySN 466 | [@imdbsel, @mySYsel, @mySY, @mySN, @genY, @genN].each do |arr| 467 | arr.uniq! 468 | arr.compact! 469 | arr.sort_by! {|m| m[0]} 470 | end 471 | @w_i.list = @imdbsel 472 | @w_i.index = 0 if @w_i.index > @w_i.list.size - 2 473 | @w_m.list = @mySYsel 474 | @w_n.list = @mySN 475 | @myN = @mySN 476 | @myY = @mySY 477 | end 478 | def getomdb(id) 479 | @urldetails = "http://www.omdbapi.com/?apikey=#{@omdbkey}&i=#{id}" 480 | details = Net::HTTP.get(URI(@urldetails)) 481 | det = JSON.parse(details) 482 | return det 483 | end 484 | def getstreaming(id) # Returns array of outlets 485 | url = URI("https://streaming-availability.p.rapidapi.com/v2/get/basic?country=#{@country}&imdb_id=#{id}&output_language=#{@lang}") 486 | http = Net::HTTP.new(url.host, url.port) 487 | http.use_ssl = true 488 | request = Net::HTTP::Get.new(url) 489 | request["X-RapidAPI-Key"] = @streamkey 490 | request["X-RapidAPI-Host"] = 'streaming-availability.p.rapidapi.com' 491 | response = http.request(request) 492 | res = JSON.parse(response.read_body) 493 | outlets = [] 494 | begin 495 | res["result"]["streamingInfo"][@country].each{|k,v| outlets.push(k)} 496 | return outlets 497 | rescue 498 | return [outlets.to_s] 499 | end 500 | end 501 | def saveconf 502 | if File.exist?(Dir.home+'/.imdb.conf') 503 | conf = File.read(Dir.home+'/.imdb.conf') 504 | else 505 | conf = "" 506 | end 507 | conf.gsub!(/^@rating.*\n/, "") 508 | conf += "@rating = #{@rating}\n" 509 | conf.gsub!(/^@yearMin.*\n/, "") 510 | conf += "@yearMin = #{@yearMin}\n" 511 | conf.gsub!(/^@yearMax.*\n/, "") 512 | conf += "@yearMax = #{@yearMax}\n" 513 | conf.gsub!(/^@genY.*\n/, "") 514 | conf += "@genY = #{@genY}\n" 515 | conf.gsub!(/^@genN.*\n/, "") 516 | conf += "@genN = #{@genN}\n" 517 | conf.gsub!(/^@myMY.*\n/, "") 518 | conf += "@myMY = #{@myMY}\n" 519 | conf.gsub!(/^@myMN.*\n/, "") 520 | conf += "@myMN = #{@myMN}\n" 521 | conf.gsub!(/^@mySY.*\n/, "") 522 | conf += "@mySY = #{@mySY}\n" 523 | conf.gsub!(/^@mySN.*\n/, "") 524 | conf += "@mySN = #{@mySN}\n" 525 | w_b("Configuration written to .imdb.conf") 526 | File.write(Dir.home+'/.imdb.conf', conf) 527 | end 528 | def saveimdb 529 | if File.exist?(Dir.home+'/.imdb') 530 | data = File.read(Dir.home+'/.imdb') 531 | else 532 | data = "" 533 | end 534 | data.gsub!(/^@imdbmovies.*\n/, "") 535 | data += "@imdbmovies = #{@imdbmovies}\n" 536 | data.gsub!(/^@imdbseries.*\n/, "") 537 | data += "@imdbseries = #{@imdbseries}\n" 538 | w_b("IMDB data written to .imdb") 539 | File.write(Dir.home+'/.imdb', data) 540 | end 541 | 542 | # BASIC WINDOW FUNCTIONS 543 | def w_t # SHOW INFO IN @w_t 544 | @w_t.clr 545 | @movies ? text = " MOVIES :: " : text = " SERIES :: " 546 | text += "Rating MIN: #{@rating} - Year MIN: #{@yearMin} - Year MAX: #{@yearMax} :: Selection = #{@imdbsel.size}" 547 | @w_t.p(text) 548 | @w_t.nl 549 | end 550 | def w_list(win) # LIST IN WINDOW 551 | win.attr = 0 552 | win == @active ? win.bg = WAbg : win.bg = WIbg 553 | win.fill 554 | ix = 0; t = 0 555 | ix = win.index - win.maxy/2 if win.index > win.maxy/2 and win.list.size > win.maxy - 1 556 | while ix < win.list.size and t < win.maxy do 557 | str = win.list[ix][0] 558 | str = win.list[ix] if win == @w_g 559 | if ix == win.index and win == @active 560 | win.p("→ ") 561 | else 562 | win.p(" ") 563 | end 564 | if win == @w_g 565 | if @genY.include?(str) 566 | win.fg = WgYfg 567 | win.attr = Curses::A_BOLD 568 | win.p("+") 569 | elsif @genN.include?(str) 570 | win.fg = WgNfg 571 | win.p("-") 572 | else 573 | win.fg = Wgfg 574 | win.p(" ") 575 | end 576 | end 577 | win.attr = win.attr | Curses::A_UNDERLINE if ix == win.index 578 | str = str[0..(win.maxx - 6)] + "…" if str.length > win.maxx - 4 579 | win.p(str) 580 | win.attr = 0 581 | win.nl 582 | ix += 1; t += 1 583 | end 584 | if win.index > win.maxy/2 585 | win.setpos(0, win.maxx - 1) 586 | win.p(Mark,"∆") 587 | end 588 | if win.list.length > win.maxy - 1 and win.list.length > win.index + win.maxy/2 - 1 589 | win.setpos(win.maxy - 1, win.maxx - 1) 590 | win.p(Mark,"∇") 591 | end 592 | end 593 | def w_d(ext = 0) # SHOW INFO IN @w_d and @w_p 594 | return if @active == @w_g 595 | list = @active.list 596 | return if list.empty? # Skip if list is empty 597 | id = list[@active.index][4] 598 | @w_d.clr 599 | @w_d.attr = Curses::A_BOLD 600 | @w_d.p("#{list[@active.index][0]}\n\n") 601 | @w_d.attr = 0 602 | @w_d.p(153,232,Curses::A_BOLD,"Rating: " + list[@active.index][1].to_s.ljust(14) + "Genres: #{list[@active.index][3]}\n") 603 | return unless ext > 0 # Skip if no details are to be displayed 604 | det = getomdb(id) 605 | text = "\nRated: " + det["Rated"].ljust(14) + "Runtime: #{det["Runtime"]}" 606 | text += " (#{det["totalSeasons"]})" unless det["totalSeasons"] == nil 607 | text += "\n" 608 | text += "Released: " + det["Released"].ljust(14) 609 | @w_d.p(230,text) 610 | @w_d.p(244,"(#{id})\n") 611 | width = Curses.cols - 104 612 | @w_d.p(228,@w_d.format(det["Plot"])) 613 | text = "\n" 614 | text += "Awards: " + det["Awards"] + "\n" 615 | text += "Director: " + det["Director"] + "\n" 616 | text += "Actors: " + det["Actors"] + "\n\n" 617 | @w_d.p(223,text) 618 | text = "Metascore: " + det["Metascore"] + "\n\n" 619 | @w_d.p(230,text) 620 | # Display the poster 621 | poster = det["Poster"] 622 | `curl -s "#{poster}" > /tmp/imdb.jpg` 623 | imageshow("/tmp/imdb.jpg") 624 | return unless ext > 1 # Skip if no outlets are to be displayed 625 | outlets = "" 626 | otl = getstreaming(id) 627 | otl.each{|o| outlets += "#{o} "} 628 | text = "Outlets: " + outlets 629 | @w_d.p(112,text) 630 | end 631 | def imageshow(image) 632 | begin 633 | terminfo = `xwininfo -id $(xdotool getactivewindow)` 634 | term_w = terminfo.match(/Width: (\d+)/)[1].to_i 635 | term_h = terminfo.match(/Height: (\d+)/)[1].to_i 636 | char_w = term_w / Curses.cols 637 | char_h = term_h / Curses.lines 638 | img_x = char_w * 105 639 | img_y = char_h * (Curses.lines / 2 + 2) 640 | img_max_w = char_w * (Curses.cols - (Curses.cols - 104) - 2) 641 | img_max_h = char_h * (@w_d.maxy - 2) 642 | # Clear previous images 643 | `echo "6;#{img_x};#{img_y};#{img_max_w+2};#{img_max_h+2};\n4;\n3;" | #{@w3mimgdisplay} 2>/dev/null` 644 | return if image == "" 645 | img_w,img_h = `identify -format "%[fx:w]x%[fx:h]" #{image} 2>/dev/null`.split('x') 646 | img_w = img_w.to_i 647 | img_h = img_h.to_i 648 | if img_w > img_max_w 649 | img_h = img_h * img_max_w / img_w 650 | img_w = img_max_w 651 | end 652 | if img_h > img_max_h 653 | img_w = img_w * img_max_h / img_h 654 | img_h = img_max_h 655 | end 656 | `echo "0;1;#{img_x};#{img_y};#{img_w};#{img_h};;;;;\"#{image}\"\n4;\n3;" | #{@w3mimgdisplay} 2>/dev/null` 657 | rescue 658 | w_b("Error showing image") 659 | end 660 | end 661 | 662 | # BOTTOM WINDOW FUNCTIONS 663 | def w_b(info) # SHOW INFO IN @W_B 664 | @w_b.clr 665 | info = "Use TAB to cycle through windows. Press ? for help. " if info == nil 666 | info = info[1..(@w_b.maxx - 3)] + "…" if info.length + 3 > @w_b.maxx 667 | info += " " * (@w_b.maxx - info.length) if info.length < @w_b.maxx 668 | @w_b.p(info) 669 | @w_b.update = false 670 | end 671 | def w_b_getstr(pretext, text) # A SIMPLE READLINE-LIKE ROUTINE 672 | Curses.curs_set(1) 673 | Curses.echo 674 | stk = 0 675 | pos = text.length 676 | chr = "" 677 | while chr != "ENTER" 678 | @w_b.setpos(0,0) 679 | @w_b.p(pretext + text) 680 | @w_b.nl 681 | @w_b.setpos(0,pretext.length + pos) 682 | @w_b.refresh 683 | chr = getchr 684 | case chr 685 | when 'C-C', 'C-G' 686 | return "" 687 | when 'RIGHT' 688 | pos += 1 unless pos > text.length 689 | when 'LEFT' 690 | pos -= 1 unless pos == 0 691 | when 'HOME' 692 | pos = 0 693 | when 'END' 694 | pos = text.length 695 | when 'DEL' 696 | text[pos] = "" 697 | when 'BACK' 698 | unless pos == 0 699 | pos -= 1 700 | text[pos] = "" 701 | end 702 | when 'LDEL' 703 | text = "" 704 | pos = 0 705 | when /^.$/ 706 | text.insert(pos,chr) 707 | pos += 1 708 | end 709 | end 710 | Curses.curs_set(0) 711 | Curses.noecho 712 | return text 713 | end 714 | 715 | # BEFORE WE START 716 | begin 717 | if File.exist?(Dir.home+'/.imdb.conf') 718 | load(Dir.home+'/.imdb.conf') 719 | else 720 | firstrun 721 | end 722 | if File.exist?(Dir.home+'/.imdb') 723 | load(Dir.home+'/.imdb') 724 | else 725 | puts "Loading IMDB data... (this may take som time, go get some coffee)" 726 | loadimdb 727 | saveimdb 728 | end 729 | ## Curses setup 730 | Curses.init_screen 731 | Curses.start_color 732 | Curses.curs_set(0) 733 | Curses.noecho 734 | Curses.cbreak 735 | Curses.stdscr.keypad = true 736 | end 737 | 738 | # MAIN PROGRAM 739 | loop do # OUTER LOOP - (catching refreshes via 'r') 740 | @break = false # Initialize @break variable (set if user hits 'r') 741 | begin # Create the windows/panels 742 | maxx = Curses.cols 743 | maxy = Curses.lines 744 | init_pair(255, 0, 234) 745 | maxy.times {Curses.stdscr.attron(color_pair(255)) {Curses.stdscr << " " * maxx}} 746 | Curses.stdscr.refresh 747 | # Curses::Window.new( h, w, y, x ) 748 | @w_t = Curses::Window.new( 1, maxx, 0, 0 ) 749 | @w_i = Curses::Window.new( maxy-2, 40, 1, 0 ) 750 | @w_g = Curses::Window.new( maxy-2, 20, 1, 41 ) 751 | @w_m = Curses::Window.new( maxy/2-1, 40, 1, 62 ) 752 | @w_n = Curses::Window.new( maxy/2-1, 40, maxy/2+1, 62 ) 753 | @w_d = Curses::Window.new( maxy/2-1, maxx-103, 1, 103 ) 754 | @w_p = Curses::Window.new( maxy/2-1, maxx-103, maxy/2+1, 103 ) 755 | @w_b = Curses::Window.new( 1, maxx, maxy-1, 0 ) 756 | @w_i.fg, @w_i.bg = Wifg, WIbg 757 | @w_g.fg, @w_g.bg = Wgfg, WIbg 758 | @w_m.fg, @w_m.bg = Wmfg, WIbg 759 | @w_n.fg, @w_n.bg = Wnfg, WIbg 760 | @w_d.fg, @w_d.bg = Wdfg, WIbg 761 | @w_p.fg, @w_p.bg = Wpfg, WIbg 762 | @w_b.fg, @w_b.bg = Wbfg, Wbbg 763 | [@w_i, @w_g, @w_m, @w_n].each{|w| w.index = 0} 764 | @w_g.list = @genres 765 | @w_b.update = true 766 | @w_d.update = true 767 | @active = @w_i 768 | loop do # INNER, CORE LOOP 769 | if @movies 770 | imdbmovies 771 | @w_t.fg = WtMfg 772 | @search == '' ? @w_t.bg = WtMbg : @w_t.bg = WtMbgS 773 | @w_t.attr = Curses::A_BOLD 774 | else 775 | imdbseries 776 | @w_t.fg = WtSfg 777 | @search == '' ? @w_t.bg = WtSbg : @w_t.bg = WtSbgS 778 | @w_t.attr = Curses::A_BOLD 779 | end 780 | @w_i.fill; @w_g.fill; @w_m.fill; @w_n.fill; @w_p.fill 781 | w_t; w_list(@w_i); w_list(@w_g); w_list(@w_m); w_list(@w_n) 782 | if @w_d.update 783 | @w_d.fill 784 | w_d 785 | end 786 | imageshow("") if @w_d.update 787 | @w_d.update = true 788 | w_b(nil) if @w_b.update 789 | @w_b.update = true 790 | getkey # Get key from user 791 | break if @break # Break to outer loop, redrawing windows, if user hit 'r' 792 | break if Curses.cols != maxx or Curses.lines != maxy # break on terminal resize 793 | end 794 | ensure # On exit: clear image, close curses 795 | imageshow("") 796 | close_screen 797 | end 798 | end 799 | 800 | # vim: set sw=2 sts=2 et fdm=syntax fdn=2 fcs=fold\:\ : 801 | -------------------------------------------------------------------------------- /imdb-terminal-0.7.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isene/IMDB/682ff7a39fb6be842a32f21dbdd18c4c11f7f900/imdb-terminal-0.7.4.gem -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isene/IMDB/682ff7a39fb6be842a32f21dbdd18c4c11f7f900/img/screenshot.png -------------------------------------------------------------------------------- /rsh.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'imdb-terminal' 3 | s.version = '0.7.4' 4 | s.licenses = ['Unlicense'] 5 | s.summary = "IMDB for the terminal" 6 | s.description = "Narrow down your preferences from a 1000 movies and almost 500 series. Select a minimum IMDB rating, range of production years, genres you like and dislike to get your preferred list. Get detailed information on movies and series and where you can stream them. Even the movie poster in the terminal. New in 0.2: Added 'v' to show version and RubyGem version. Code cleanup. 0.7.4: Fixed info string in bottom window." 7 | s.authors = ["Geir Isene"] 8 | s.email = 'g@isene.com' 9 | s.files = ["bin/imdb", "README.md"] 10 | s.add_runtime_dependency 'curses', '~> 1.3', '>= 1.3.2' 11 | s.add_runtime_dependency 'rest-client', '~> 2.0' 12 | s.executables << 'imdb' 13 | s.homepage = 'https://isene.com/' 14 | s.metadata = { "source_code_uri" => "https://github.com/isene/imdb" } 15 | end 16 | --------------------------------------------------------------------------------