imdb-terminal 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/imdb +77 -56
- metadata +26 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b26870720d0e55ae88bc797de5e902b24bca1deac0514c3c29058b90df31d08
|
|
4
|
+
data.tar.gz: 39c9433b18a602fa2f7b16493ae94841896da14493b24256852c1bd70253a403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92396d67a67433e37f25fa135577f012f1259c88e7cc859f868eec153705a35cd138fb57aeb26132927d82b502d3ebf40dab9ae0ee6999a6fb0e951b71a2ed0c
|
|
7
|
+
data.tar.gz: 556c17abd3c6e6b2b5aaebb6a5b1e179cb16374f82945ce2fa484185bebfb9eaf0a2329d892a9ed0cd3ee12987a22536e6dba767034a241feee07eda0d9af66f
|
data/bin/imdb
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# for any damages resulting from its use. Further, I am under no
|
|
15
15
|
# obligation to maintain or extend this software. It is provided
|
|
16
16
|
# on an 'as is' basis without any expressed or implied warranty.
|
|
17
|
-
# Version: 2.
|
|
17
|
+
# Version: 2.1.1: Add HTTP timeouts and external command error handling
|
|
18
18
|
|
|
19
19
|
# REQUIRES AND CONSTANTS {{{1
|
|
20
20
|
require 'io/console'
|
|
@@ -32,6 +32,7 @@ require 'nokogiri'
|
|
|
32
32
|
require 'fileutils'
|
|
33
33
|
require 'concurrent'
|
|
34
34
|
require 'rcurses'
|
|
35
|
+
require 'termpix'
|
|
35
36
|
|
|
36
37
|
include Rcurses
|
|
37
38
|
include Rcurses::Cursor
|
|
@@ -84,8 +85,9 @@ class IMDBApp
|
|
|
84
85
|
@list.border = true
|
|
85
86
|
@last_poster_id = nil
|
|
86
87
|
@current_image = nil
|
|
88
|
+
@termpix = Termpix::Display.new unless WINDOWS
|
|
87
89
|
@last_active_window = nil
|
|
88
|
-
|
|
90
|
+
|
|
89
91
|
run_loop
|
|
90
92
|
ensure
|
|
91
93
|
save_config
|
|
@@ -409,7 +411,8 @@ class IMDBApp
|
|
|
409
411
|
uri = "https://www.imdb.com/#{path}/"
|
|
410
412
|
|
|
411
413
|
begin
|
|
412
|
-
raw = `curl -sfL -H "User-Agent: #{ua}" "#{uri}"`
|
|
414
|
+
raw = `curl -sfL --max-time 15 --connect-timeout 10 -H "User-Agent: #{ua}" "#{uri}"`
|
|
415
|
+
return [] unless $?.success?
|
|
413
416
|
html = raw.force_encoding('UTF-8').scrub('')
|
|
414
417
|
doc = Nokogiri::HTML(html)
|
|
415
418
|
|
|
@@ -452,10 +455,19 @@ class IMDBApp
|
|
|
452
455
|
|
|
453
456
|
def scrape_json_ld(path, ua) #{{{2
|
|
454
457
|
uri = "https://www.imdb.com/#{path}/"
|
|
455
|
-
|
|
458
|
+
begin
|
|
459
|
+
raw = `curl -sfL --max-time 15 --connect-timeout 10 -H "User-Agent: #{ua}" "#{uri}"`
|
|
460
|
+
unless $?.success?
|
|
461
|
+
@progress.say("Warning: Failed to fetch #{path}") if defined?(@progress) && @progress
|
|
462
|
+
return []
|
|
463
|
+
end
|
|
464
|
+
rescue => e
|
|
465
|
+
@progress.say("Warning: curl error for #{path}: #{e.message}") if defined?(@progress) && @progress
|
|
466
|
+
return []
|
|
467
|
+
end
|
|
456
468
|
html = raw.force_encoding('UTF-8').scrub('')
|
|
457
469
|
doc = Nokogiri::HTML(html)
|
|
458
|
-
|
|
470
|
+
|
|
459
471
|
if (ld = doc.at_css('script[type="application/ld+json"]'))
|
|
460
472
|
begin
|
|
461
473
|
data = JSON.parse(ld.text)
|
|
@@ -473,7 +485,7 @@ class IMDBApp
|
|
|
473
485
|
# fall back to HTML scrape
|
|
474
486
|
end
|
|
475
487
|
end
|
|
476
|
-
|
|
488
|
+
|
|
477
489
|
doc.css('.lister-list tr').map do |tr|
|
|
478
490
|
rating = tr.at_css('td.ratingColumn strong')&.text.to_f || 0.0
|
|
479
491
|
a = tr.at_css('td.titleColumn a') or next
|
|
@@ -767,16 +779,27 @@ class IMDBApp
|
|
|
767
779
|
def download_poster(tconst, cache) #{{{2
|
|
768
780
|
url = fetch_imdb_poster_url(tconst)
|
|
769
781
|
return unless url
|
|
770
|
-
|
|
782
|
+
|
|
771
783
|
dest = File.join(cache, "#{tconst}.jpg")
|
|
772
784
|
return if File.exist?(dest) && File.size(dest) > 1000
|
|
773
|
-
|
|
785
|
+
|
|
774
786
|
cmd = [
|
|
775
|
-
'curl', '-sfL', '--max-time', '10', '--
|
|
787
|
+
'curl', '-sfL', '--max-time', '10', '--connect-timeout', '10',
|
|
788
|
+
'--retry', '2', '--retry-delay', '1',
|
|
776
789
|
'-H', 'User-Agent: Mozilla/5.0', '-o', dest, url
|
|
777
790
|
].map { |arg| Shellwords.escape(arg) }.join(' ')
|
|
778
|
-
|
|
779
|
-
|
|
791
|
+
|
|
792
|
+
begin
|
|
793
|
+
system(cmd, err: File::NULL)
|
|
794
|
+
# Remove incomplete downloads
|
|
795
|
+
if File.exist?(dest) && File.size(dest) < 100
|
|
796
|
+
File.delete(dest) rescue nil
|
|
797
|
+
end
|
|
798
|
+
rescue => e
|
|
799
|
+
File.open("/tmp/imdb_fetch_errors.log", "a") do |f|
|
|
800
|
+
f.puts "[#{Time.now.iso8601}] Poster download failed for #{tconst}: #{e.message}"
|
|
801
|
+
end
|
|
802
|
+
end
|
|
780
803
|
end
|
|
781
804
|
|
|
782
805
|
def fetch_imdb_poster_url(tconst) #{{{2
|
|
@@ -784,9 +807,13 @@ class IMDBApp
|
|
|
784
807
|
html = URI.open(
|
|
785
808
|
"https://www.imdb.com/title/#{tconst}/",
|
|
786
809
|
"User-Agent" => "Mozilla/5.0",
|
|
787
|
-
"Accept-Encoding" => "identity"
|
|
810
|
+
"Accept-Encoding" => "identity",
|
|
811
|
+
open_timeout: 10,
|
|
812
|
+
read_timeout: 10
|
|
788
813
|
).read
|
|
789
|
-
rescue SocketError, Errno::ECONNREFUSED, OpenURI::HTTPError,
|
|
814
|
+
rescue SocketError, Errno::ECONNREFUSED, OpenURI::HTTPError,
|
|
815
|
+
Net::HTTPBadResponse, Net::OpenTimeout, Net::ReadTimeout,
|
|
816
|
+
Timeout::Error, Errno::ETIMEDOUT
|
|
790
817
|
return nil
|
|
791
818
|
end
|
|
792
819
|
html[/<meta property="og:image" content="([^"]+)"/,1]
|
|
@@ -1188,12 +1215,14 @@ class IMDBApp
|
|
|
1188
1215
|
uri.query = URI.encode_www_form(q: query, s: 'tt')
|
|
1189
1216
|
html = nil
|
|
1190
1217
|
begin
|
|
1191
|
-
Timeout.timeout(
|
|
1218
|
+
Timeout.timeout(15) do
|
|
1192
1219
|
html = URI.open(uri.to_s,
|
|
1193
1220
|
'User-Agent' => 'Mozilla/5.0',
|
|
1194
1221
|
'Accept' => 'text/html,application/xhtml+xml',
|
|
1195
1222
|
'Accept-Language' => 'en-US,en;q=0.9',
|
|
1196
|
-
'Accept-Encoding' => 'identity'
|
|
1223
|
+
'Accept-Encoding' => 'identity',
|
|
1224
|
+
open_timeout: 10,
|
|
1225
|
+
read_timeout: 10
|
|
1197
1226
|
).read
|
|
1198
1227
|
# Fix encoding issues
|
|
1199
1228
|
html = html.force_encoding('UTF-8').scrub('?')
|
|
@@ -1324,7 +1353,7 @@ class IMDBApp
|
|
|
1324
1353
|
help_text << " • Dump list removes items from main view permanently\n".fg(230)
|
|
1325
1354
|
help_text << " • TMDb provides streaming provider information\n".fg(230)
|
|
1326
1355
|
unless WINDOWS
|
|
1327
|
-
help_text << " • Posters are displayed
|
|
1356
|
+
help_text << " • Posters are displayed using termpix (Sixel or w3m protocols)\n".fg(230)
|
|
1328
1357
|
else
|
|
1329
1358
|
help_text << " • Poster display not available on Windows\n".fg(230)
|
|
1330
1359
|
end
|
|
@@ -1811,43 +1840,33 @@ class IMDBApp
|
|
|
1811
1840
|
|
|
1812
1841
|
def show_poster(tconst) #{{{2
|
|
1813
1842
|
return if WINDOWS # Skip image display on Windows for now
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
return unless File.executable?(w3m)
|
|
1843
|
+
return unless @termpix&.supported?
|
|
1844
|
+
|
|
1817
1845
|
cache = cache_dir
|
|
1818
1846
|
file = File.join(cache, "#{tconst}.jpg")
|
|
1819
1847
|
|
|
1820
1848
|
begin
|
|
1821
1849
|
Timeout.timeout(2) do
|
|
1822
|
-
info = `xwininfo -id $(xdotool getactivewindow) 2>/dev/null`
|
|
1823
|
-
return unless info =~ /Width:\s*(\d+).*Height:\s*(\d+)/m
|
|
1824
|
-
term_w, term_h = $1.to_i, $2.to_i
|
|
1825
1850
|
rows, cols = IO.console.winsize
|
|
1826
|
-
cw = term_w.to_f / cols
|
|
1827
|
-
ch = term_h.to_f / rows
|
|
1828
|
-
px = ((@detail.x - 1) * cw).to_i
|
|
1829
|
-
py = (25 * ch).to_i
|
|
1830
|
-
max_w = (40 * cw).to_i
|
|
1831
|
-
max_h = ((rows - 28) * ch).to_i
|
|
1832
1851
|
|
|
1833
|
-
|
|
1852
|
+
# Always clear previous poster first to prevent overlapping
|
|
1853
|
+
@termpix.clear(
|
|
1854
|
+
x: @detail.x - 1,
|
|
1855
|
+
y: 25,
|
|
1856
|
+
width: 40,
|
|
1857
|
+
height: rows - 27,
|
|
1858
|
+
term_width: cols,
|
|
1859
|
+
term_height: rows)
|
|
1834
1860
|
|
|
1835
1861
|
if File.exist?(file) && File.size?(file) > 0
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
iw = iw * max_h / ih; ih = max_h
|
|
1843
|
-
end
|
|
1844
|
-
|
|
1845
|
-
`echo "0;1;#{px};#{py};#{iw};#{ih};;;;;\"#{file}\"\n4;\n3;" | #{w3m} 2>/dev/null`
|
|
1846
|
-
|
|
1847
|
-
# Track the current image
|
|
1862
|
+
# Display new poster using termpix
|
|
1863
|
+
@termpix.show(file,
|
|
1864
|
+
x: @detail.x - 1,
|
|
1865
|
+
y: 25,
|
|
1866
|
+
max_width: 40,
|
|
1867
|
+
max_height: rows - 28)
|
|
1848
1868
|
@current_image = file
|
|
1849
1869
|
else
|
|
1850
|
-
# Clear current image if no file exists
|
|
1851
1870
|
@current_image = nil
|
|
1852
1871
|
end
|
|
1853
1872
|
end
|
|
@@ -1858,24 +1877,26 @@ class IMDBApp
|
|
|
1858
1877
|
|
|
1859
1878
|
def check_image_redraw #{{{2
|
|
1860
1879
|
return if WINDOWS # Skip image redraw on Windows
|
|
1880
|
+
return unless @termpix&.supported?
|
|
1861
1881
|
# Only check if we have an image currently displayed
|
|
1862
1882
|
return unless @current_image && File.exist?(@current_image)
|
|
1863
|
-
|
|
1883
|
+
|
|
1864
1884
|
begin
|
|
1865
|
-
#
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1885
|
+
# For w3m protocol, check if image overlay was cleared
|
|
1886
|
+
if @termpix.protocol == :w3m
|
|
1887
|
+
active_window = `xdotool getactivewindow 2>/dev/null`.chomp
|
|
1888
|
+
return if active_window.empty?
|
|
1889
|
+
|
|
1890
|
+
current_window = `xdotool getwindowfocus 2>/dev/null`.chomp
|
|
1891
|
+
|
|
1892
|
+
# Only redraw if we detect a focus change
|
|
1893
|
+
if @last_active_window && @last_active_window != active_window && current_window == active_window
|
|
1894
|
+
show_poster(@last_poster_id) if @last_poster_id
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
@last_active_window = active_window
|
|
1876
1898
|
end
|
|
1877
|
-
|
|
1878
|
-
@last_active_window = active_window
|
|
1899
|
+
# Sixel images persist, no redraw needed
|
|
1879
1900
|
rescue
|
|
1880
1901
|
# Silently fail - we don't want focus checking to break anything
|
|
1881
1902
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: imdb-terminal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-03-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rcurses
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: termpix
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.1'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: nokogiri
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,12 +80,12 @@ dependencies:
|
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
82
|
version: '13.0'
|
|
69
|
-
description: '
|
|
70
|
-
|
|
83
|
+
description: 'IMDB v2.1.1: Modern poster display using termpix gem with Sixel and
|
|
84
|
+
w3m protocol support. Discover and manage movies and TV series from IMDb''s Top
|
|
85
|
+
250 lists. Features smart search with preview mode, advanced filtering by rating/year/genre,
|
|
71
86
|
streaming information via TMDb, wish lists, and terminal poster display. Enhanced
|
|
72
|
-
with jump-to-existing items, duplicate management,
|
|
73
|
-
|
|
74
|
-
handling of platform-specific features.'
|
|
87
|
+
with jump-to-existing items, duplicate management, robust data handling, and improved
|
|
88
|
+
HTTP timeout/error handling.'
|
|
75
89
|
email: g@isene.com
|
|
76
90
|
executables:
|
|
77
91
|
- imdb
|
|
@@ -90,11 +104,11 @@ metadata:
|
|
|
90
104
|
post_install_message: "✓ IMDb Terminal Browser installed successfully!\n\nTo get started:\n1.
|
|
91
105
|
Run: imdb\n2. On first run, the app will scrape IMDb Top 250 lists\n3. Optional:
|
|
92
106
|
Get a free TMDb API key for streaming info\n4. Press '?' for help once running\n\nExternal
|
|
93
|
-
dependencies for full functionality (Linux/macOS only):\n-
|
|
94
|
-
sudo apt install
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
works normally.\n\nEnjoy discovering your next favorite movie! :-)\n"
|
|
107
|
+
dependencies for full functionality (Linux/macOS only):\n- imagemagick (for image
|
|
108
|
+
processing): sudo apt install imagemagick\n- For w3m protocol: w3m-img, xdotool
|
|
109
|
+
(sudo apt install w3m-img xdotool)\n- For Sixel protocol (mlterm, xterm): imagemagick
|
|
110
|
+
convert command\n\nNote: Poster display is not available on Windows, but all other
|
|
111
|
+
\nfunctionality works normally.\n\nEnjoy discovering your next favorite movie! :-)\n"
|
|
98
112
|
rdoc_options: []
|
|
99
113
|
require_paths:
|
|
100
114
|
- "."
|