nyaa 0.3.0 → 1.0.2

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.
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  coverage
6
+ Gemfile.lock
6
7
  InstalledFiles
7
8
  lib/bundler/man
8
9
  pkg
@@ -16,3 +17,5 @@ tmp
16
17
  .yardoc
17
18
  _yardoc
18
19
  doc/
20
+
21
+ TODO
data/Gemfile CHANGED
@@ -1,2 +1,3 @@
1
- source 'http://rubygems.org'
1
+ source :rubygems
2
+
2
3
  gemspec
data/README.md CHANGED
@@ -1,29 +1,49 @@
1
1
  # Nyaa
2
2
 
3
- Nyaa is a CLI to NyaaTorrents. You can browse, search, and download. Nifty.
3
+ Nyaa is a CLI to Nyaa.eu. You can browse, search, and download. Nifty.
4
+
5
+ ## Features
6
+
7
+ * Ncurses interface
8
+ * Search by category, filter, page, and query
9
+ * Browsing with pagination
10
+ * Download it or open a browser window from the interface
11
+ * Nyaa status aware: (aplus, trusted, remake, etc.)
12
+ * Batch mode for scripts (first page only atm)
13
+ * Supports unicode characters (requires `libncursesw5-dev` and `ruby1.9`)
4
14
 
5
15
  ## Installation
6
16
 
17
+ Stable release:
18
+
7
19
  gem install nyaa
8
20
 
9
- ## Usage
21
+ Development release:
10
22
 
11
- To start browsing immediately, simply run `nyaa`. The default category is english anime.
23
+ git clone git://github.com/mistofvongola/nyaa.git
12
24
 
13
- ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_1.png)
25
+ ## Browser Usage
26
+
27
+ To browse, simply run `nyaa`. The default category is english anime.
14
28
 
15
- Nyaa supports all the aspects of search of the main site. You can search by category and/or filters. Nyaa also shows a summary of seeders, leechers, total filesize, and number of downloads. To download an item, simply enter the number of the result.
29
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/v1.0.0_browse.png)
16
30
 
17
- nyaa -c anime_english -f trusted_only 'guilty crown'
31
+ Nyaa supports all the aspects of search of the main site. You can search by category and/or filters. To download an item, highlight it, and type `g`. To open the description page in a browser, type `i`. A sample query:
18
32
 
19
- ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_2.png)
33
+ nyaa -f trusted_only psycho pass
34
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/v1.0.0_search.png)
20
35
 
21
36
  For a list of categories and filters, see `nyaa -h`.
22
37
 
23
- ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_3.png)
38
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/v1.0.0_help.png)
39
+
40
+ ## The old interface
41
+
42
+ The old nyaa interface is deprecated, but is still included. You can use the old interface using the `--classic` option.
43
+
44
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_1.png)
24
45
 
25
46
  ## Contributing
26
- 0. See HACKING file for style guidelines
27
47
  1. Fork it
28
48
  2. Create your feature branch (`git checkout -b my-new-feature`)
29
49
  3. Commit your changes (`git commit -am 'Added some feature'`)
@@ -32,8 +52,8 @@ For a list of categories and filters, see `nyaa -h`.
32
52
 
33
53
  ## License
34
54
 
35
- See LICENSE file.
55
+ MIT License. See LICENSE file for details.
36
56
 
37
57
  ## Authors
38
58
 
39
- See AUTHORS file.
59
+ David Palma
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ task :default => :test
6
6
 
7
7
  Rake::TestTask.new do |t|
8
8
  t.libs << 'lib/nyaa'
9
- t.test_files = FileList['test/lib/nyaa/*_test.rb']
9
+ t.test_files = FileList['spec/lib/nyaa/*_spec.rb']
10
10
  t.verbose = true
11
11
  end
12
12
 
@@ -17,19 +17,22 @@ task :gemspec do
17
17
  gemspec.validate
18
18
  end
19
19
 
20
- desc "Build gem locally"
21
20
  task :build => :gemspec do
22
21
  system "gem build #{gemspec.name}.gemspec"
23
22
  FileUtils.mkdir_p "pkg"
24
23
  FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg"
25
24
  end
26
25
 
27
- desc "Install gem locally"
28
26
  task :install => :build do
29
27
  system "gem install pkg/#{gemspec.name}-#{gemspec.version}"
30
28
  end
31
29
 
32
- desc "Clean automatically generated files"
30
+ desc "Purge the pkg directory"
33
31
  task :clean do
34
32
  FileUtils.rm_rf "pkg"
35
33
  end
34
+
35
+ desc "Publish gem to rubygems.org"
36
+ task :publish do
37
+ system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
38
+ end
data/bin/nyaa CHANGED
@@ -1,49 +1,78 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
4
+
3
5
  require 'nyaa'
4
- require 'trollop'
5
-
6
- opts = Trollop::options do
7
- version "nyaa v#{Nyaa::VERSION} (c) 2012 David Palma http://github.com/mistofvongola"
8
- banner <<-EOM
9
- The nyaa gem is a simple CLI browser for NyaaTorrents.
10
- Usage:
11
- nyaa [options] "my search"
12
-
13
- Categories:
14
- anime_all, anime_raw, anime_english, anime_nonenglish, anime_music_video
15
- books_all, books_raw, books_english, books_nonenglish
16
- audio_all, audio_lossless, audio_lossy
17
- pictures_all, pictures_photos, pictures_graphics,
18
- live_all, live_raw, live_english, live_nonenglish, live_promo
19
- software_all, software_apps, software_games
20
-
21
- Filters:
22
- show_all, filter_remakes, trusted_only, aplus_only
23
-
24
- Options:
25
- EOM
26
- opt :category, "Select a category to search in. See below for valid options.", :default => 'anime_english'
27
- opt :filter, "Select a filter for your search. See below for valid options.", :default => 'show_all'
28
- opt :outdir, "Select the download directory.", :default => File.expand_path('~/Downloads')
29
- opt :size, "Show <i> results at a time. Must be between 1 and 100.", :type => :int, :default => 4
30
- opt :page, "Start by showing the <i>th result page.", :type => :int, :default => 1
31
- opt :version, "Print the version and exit."
32
- opt :help, "Show this information and exit."
6
+ include Curses
7
+
8
+
9
+ @opts = Nyaa::CLI.parse(ARGV)
10
+ @search = Nyaa::Search.new(@opts[:query], @opts[:category], @opts[:filter])
11
+
12
+ def batch_mode
13
+ results = @search.more.get_results
14
+ results.each do |r|
15
+ puts "o #{r.name}"
16
+ puts "#{r.link}"
17
+ puts
18
+ end
19
+ exit
33
20
  end
34
21
 
35
- unless Nyaa::Browser::CATS.has_key?(opts[:category])
36
- Trollop::die :category, "is not a valid category"
22
+ def browser_mode
23
+ begin
24
+ nyaa = Nyaa::Browser.new(@opts, @search)
25
+ rescue Interrupt
26
+ puts "\nInterrupt received. Exiting."
27
+ exit
28
+ end
37
29
  end
38
- unless Nyaa::Browser::FILS.has_key?(opts[:filter])
39
- Trollop::die :filter, "is not a valid filter"
30
+
31
+ def curses_mode
32
+ begin
33
+ yield
34
+ ensure
35
+ nocbreak
36
+ close_screen
37
+ end
40
38
  end
41
- query = ARGV.join(' ')
42
-
43
- begin
44
- n = Nyaa::Browser.new query, opts
45
- n.search
46
- rescue Interrupt
47
- puts "\nInterrupt received. Exiting."
48
- exit
39
+
40
+ # Batch mode
41
+ if @opts[:batch]
42
+ batch_mode
49
43
  end
44
+
45
+ if @opts[:classic] # use old interface
46
+ browser_mode
47
+ else
48
+ nyaa = Nyaa::UI.new(@opts, @search)
49
+ cursor = 1
50
+
51
+ curses_mode do
52
+ #TODO: Gracefully handle window resizing
53
+ #Signal.trap('SIGWINCH', nyaa.status("Window size changed!", :failure))
54
+ loop do
55
+ nyaa.header
56
+ nyaa.status
57
+ nyaa.footer
58
+ nyaa.menu(cursor)
59
+ refresh
60
+
61
+ case getch
62
+ when Key::UP then cursor = nyaa.move(cursor, -1)
63
+ when Key::DOWN then cursor = nyaa.move(cursor, 1)
64
+ when 'k' then cursor = nyaa.move(cursor, -1)
65
+ when 'j' then cursor = nyaa.move(cursor, 1)
66
+ when '?' then nyaa.status('help not implemented!', :failure)
67
+ when 'g' then nyaa.get(cursor)
68
+ when 'i' then nyaa.open(cursor)
69
+ when 'n' then nyaa.next_page
70
+ when 'p' then nyaa.prev_page
71
+ when 'q' then @search.purge && break
72
+ #when Key::RESIZE then nyaa.status("Window size changed!", :failure)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ exit
data/lib/nyaa.rb CHANGED
@@ -1,5 +1,28 @@
1
- require 'nyaa/version'
1
+ # -*- encoding : utf-8 -*-
2
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
3
+ # stdlib
4
+ require 'optparse'
5
+ require 'open-uri'
6
+ require 'curses'
2
7
 
3
- module Nyaa
4
- autoload :Browser, 'nyaa/browser'
8
+ # third party
9
+ begin
10
+ require 'rubygems' # for ruby 1.8 compat
11
+ require 'nokogiri'
12
+ require 'rest_client'
13
+ require 'formatador'
14
+ rescue LoadError => e
15
+ puts "LoadError: #{e.message}"
5
16
  end
17
+
18
+ # internal api
19
+ require 'nyaa/version'
20
+ require 'nyaa/cli'
21
+ require 'nyaa/constants'
22
+ require 'nyaa/torrent'
23
+ require 'nyaa/search'
24
+
25
+ # internal tools
26
+ require 'nyaa/browser' # old ui
27
+ require 'nyaa/ui' # new ui
28
+ require 'nyaa/downloader'
data/lib/nyaa/browser.rb CHANGED
@@ -1,161 +1,98 @@
1
- require 'nokogiri'
2
- require 'rest_client'
3
- require 'formatador'
4
- require 'uri'
5
-
1
+ # -*- encoding : utf-8 -*-
6
2
  module Nyaa
7
3
  class Browser
8
- BASE_URL = 'http://www.nyaa.eu/?page=torrents'
9
- PSIZE = 100
10
- CATS = {
11
- 'anime_all' => '1_0',
12
- 'anime_raw' => '1_11',
13
- 'anime_english' => '1_37',
14
- 'anime_nonenglish' => '1_38',
15
- 'anime_music_video' => '1_32',
16
- 'books_all' => '2_0',
17
- 'books_raw' => '2_13',
18
- 'books_english' => '2_12',
19
- 'books_nonenglish' => '2_39',
20
- 'audio_all' => '3_0',
21
- 'audio_lossless' => '3_14',
22
- 'audio_lossy' => '3_15',
23
- 'pictures_all' => '4_0',
24
- 'pictures_photos' => '4_17',
25
- 'pictures_graphics' => '4_18',
26
- 'live_all' => '5_0',
27
- 'live_raw' => '5_20',
28
- 'live_english' => '5_19',
29
- 'live_nonenglish' => '5_21',
30
- 'live_promo' => '5_22',
31
- 'software_all' => '6_0',
32
- 'software_apps' => '6_23',
33
- 'software_games' => '6_24',
34
- }
35
- FILS = {
36
- 'show_all' => '0',
37
- 'filter_remakes' => '1',
38
- 'trusted_only' => '2',
39
- 'aplus_only' => '3',
40
- }
41
-
42
- def initialize(query, opts)
43
- @query = URI.escape(query)
4
+ def initialize(opts, search)
44
5
  @opts = opts
6
+ @opts[:size] = 4 if opts[:size].nil?
45
7
  @opts[:size] = PSIZE if opts[:size] > PSIZE
46
8
  @opts[:size] = 1 if opts[:size] <= 1
47
9
  @marker = 0
10
+ @format = Formatador.new
11
+ @page = 0 # Current browser page
12
+ @search = search
13
+ start
48
14
  end
49
15
 
50
- def search
51
- data = harvest(@query, @opts[:page])
52
- part = partition(data, 0, @opts[:size])
53
- display(data, part)
54
- end
55
-
56
- def harvest(query, page)
57
- url = "#{BASE_URL}"
58
- url << "&cats=#{CATS[@opts[:category]]}" if @opts[:category]
59
- url << "&filter=#{FILS[@opts[:filter]]}" if @opts[:filter]
60
- url << "&offset=#{page}" if @opts[:page]
61
- url << "&term=#{query}" unless @query.empty?
62
- doc = Nokogiri::HTML(RestClient.get(url))
63
- items = []
64
- rows = doc.css('div#main div.content table.tlist tr.tlistrow')
65
- #puts "DEBUG: Row: #{rows[0].to_s}"
66
- rows.each do |row|
67
- items << {
68
- :cat => row.css('td.tlisticon').at('a')['title'],
69
- :name => row.css('td.tlistname').at('a').text.strip,
70
- :dl => row.css('td.tlistdownload').at('a')['href'],
71
- :size => row.css('td.tlistsize').text,
72
- :se => row.css('td.tlistsn').text,
73
- :le => row.css('td.tlistln').text,
74
- :dls => row.css('td.tlistdn').text,
75
- :msg => row.css('td.tlistmn').text,
76
- # TODO: The status hashkey is broken
77
- :status =>
78
- if row.at('tr.trusted')
79
- 'trusted'
80
- elsif row.at('tr.remake')
81
- 'remake'
82
- elsif row.at('tr.aplus')
83
- 'aplus'
84
- else
85
- 'normal'
86
- end
87
- }
88
- end
89
- items
16
+ def start
17
+ #@search = Search.new(@opts[:query], @opts[:category], @opts[:filter])
18
+ page_results = @search.more.get_results
19
+ @page += 1
20
+ part = partition(page_results, 0, @opts[:size])
21
+ screen(page_results, part)
90
22
  end
91
23
 
92
24
  def partition(ary, start, size)
93
25
  start = 0 if start < 0
94
- # update marker
95
26
  @marker = start
96
27
  size = PSIZE if size > PSIZE
97
-
98
28
  part = ary[start, size]
29
+ part = [] if part.nil?
99
30
  part
100
31
  end
101
32
 
102
- def display(data, results)
103
- f = Formatador.new
104
- f.display_line( "\t[yellow]NyaaTorrents >> "\
105
- "Browse | Anime, manga, and music[/]\n" )
106
-
107
- if data[0].nil? || results[0].nil?
108
- f.display_line( "[normal]No matches found. "\
109
- "Try another category. See --help.[/]\n")
110
- f.display_line("\t[yellow]Exiting.[/]")
111
- exit
112
- end
113
- f.display_line( "[bold]#{data[0][:cat]}\n[/]" )
114
-
115
- results.each do |item|
116
- case item[:status]
117
- when 'aplus'
118
- flag = 'blue'
119
- when 'trusted'
120
- flag = 'green'
121
- when 'remake'
122
- flag = 'red'
123
- else
124
- flag = 'normal'
125
- end
126
- f.display_line( "[#{flag}]#{data.index(item)+1}. #{item[:name]}[/]")
127
-
128
- f.indent {
129
- f.display_line( "[bold]Size: [purple]#{item[:size]}[/] "\
130
- "[bold]SE: [green]#{item[:se]}[/] "\
131
- "[bold]LE: [red]#{item[:le]}[/] "\
132
- "[bold]DLs: [yellow]#{item[:dls]}[/] "\
133
- "[bold]Msg: [blue]#{item[:msg]}[/]" )
134
- f.display_line( "[green]#{item[:dl]}[/]" )
135
- }
33
+ def torrent_info(page_results, torrent)
34
+ case torrent.status
35
+ when 'A+' then flag = 'blue'
36
+ when 'Trusted' then flag = 'green'
37
+ when 'Remake' then flag = 'red'
38
+ else flag = 'yellow'
136
39
  end
137
40
 
41
+ @format.display_line("#{page_results.index(torrent)+1}. "\
42
+ "#{torrent.name[0..70]}[/]")
43
+ @format.indent {
44
+ @format.display_line(
45
+ "[bold]Size: [purple]#{torrent.filesize}[/] "\
46
+ "[bold]SE: [green]#{torrent.seeders}[/] "\
47
+ "[bold]LE: [red]#{torrent.leechers}[/] "\
48
+ "[bold]DLs: [yellow]#{torrent.downloads}[/] "\
49
+ "[bold]Msg: [blue]#{torrent.comments}[/]")
50
+ @format.display_line("[bold]DL:[/] [#{flag}]#{torrent.link}[/]")
51
+ }
52
+ end
53
+
54
+ def header_info
55
+ @format.display_line( "\t[yellow]NyaaTorrents >> "\
56
+ "Browse | Anime, manga, and music[/]\n" )
57
+ @format.display_line(
58
+ "[bold]#{CATS[@opts[:category].to_sym][:title]}\n[/]" )
59
+ end
60
+
61
+ def footer_info
138
62
  start_count = @marker + 1
139
63
  start_count = PSIZE if start_count > PSIZE
140
64
  end_count = @marker + @opts[:size]
141
65
  end_count = PSIZE if end_count > PSIZE
142
66
 
143
- f.display_line("\n\t[yellow]Displaying results "\
67
+ @format.display_line("\n\t[yellow]Displaying results "\
144
68
  "#{start_count} through #{end_count} of #{PSIZE} "\
145
- "#(Page #{@opts[:page]})\n")
69
+ "(Page ##{@page})\n")
70
+ end
71
+
72
+ def screen(page_results, screen_items)
73
+ header_info
74
+
75
+ if screen_items.empty?
76
+ @format.display_line( "[normal]End of results.")
77
+ @format.display_line("For more search options, see --help.[/]\n")
78
+ exit
79
+ end
80
+
81
+ screen_items.each do |torrent|
82
+ torrent_info(page_results, torrent)
83
+ end
146
84
 
147
- prompt(data, results)
85
+ footer_info
86
+ prompt(page_results, screen_items)
148
87
  end
149
88
 
150
- def prompt(data, results)
151
- f = Formatador.new
152
- f.display_line("[yellow]Help: q to quit, "\
89
+ def prompt(page_results, screen_items)
90
+ @format.display_line("[yellow]Help: q to quit, "\
91
+ "h for display help, "\
153
92
  "n/p for pagination, "\
154
93
  "or a number to download that choice.")
155
- # prompt
156
- f.display("[bold]>[/] ")
94
+ @format.display("[bold]>[/] ")
157
95
 
158
- # handle input
159
96
  choice = STDIN.gets
160
97
  if choice.nil?
161
98
  choice = ' '
@@ -164,48 +101,70 @@ module Nyaa
164
101
  end
165
102
 
166
103
  case
167
- when choice[0] == 'q'
168
- exit
169
- when choice[0] == 'n'
170
- if @marker + @opts[:size] == 100
171
- @opts[:page] += 1
172
- f.indent { f.display_line("[purple][blink_fast]! "\
173
- "Loading more results...[/]") }
174
- data = harvest(@query, @opts[:page])
175
- part = partition(data, 0, @opts[:size])
176
- else
177
- part = partition(data, @marker + @opts[:size], @opts[:size])
178
- end
179
- display(data, part)
180
- when choice[0] == 'p'
181
- if @marker < 1
182
- f.indent { f.display_line("[purple]! Already at page one.[/]") }
183
- input(data, results)
184
- else
185
- part = partition(data, @marker - @opts[:size], @opts[:size])
186
- display(data, part)
187
- end
188
- when choice[0].match(/\d/)
189
- /(\d+)(\s*\|(.*))*/.match(choice) do |str|
190
- num = str[1].to_i - 1
191
- download(data[num][:dl], @opts[:outdir])
192
- end
104
+ when choice[0] == 'q' then @search.purge && exit
105
+ when choice[0] == 'n' then paginate(page_results)
106
+ when choice[0] == 'p' then reverse_paginate(page_results, screen_items)
107
+ when choice[0].match(/\d/) then retrieve(choice, page_results, screen_items)
108
+ when choice[0] == 'h'
109
+ @format.display_line("[normal]The color of an entry's DL link "\
110
+ "represents its status:[/]")
111
+ @format.display_line("[blue]A+[/], [green]Trusted[/], "\
112
+ "[yellow]Normal[/], or [red]Remake[/]")
113
+ prompt(page_results, screen_items)
114
+ else
115
+ @format.display_line("[red]Unrecognized option.[/]")
116
+ prompt(page_results, screen_items)
193
117
  end
194
118
  end
195
119
 
196
- def download(url, output_path)
197
- resp = RestClient.get(url)
120
+ def paginate(page_results)
121
+ if @marker + @opts[:size] == 100
122
+ @format.display_line("[yellow]Loading more results...[/]")
123
+ if @page == @search.offset
124
+ page_results = @search.more.get_results
125
+ else # @page < @search.offset
126
+ page_results = @search.cached(@page + 1)
127
+ end
128
+ @page += 1
129
+ part = partition(page_results, 0, @opts[:size])
130
+ else
131
+ part = partition(page_results, @marker + @opts[:size], @opts[:size])
132
+ end
133
+ screen(page_results, part)
134
+ end
198
135
 
199
- # Get filename from Content-Disposition header
200
- disp_fname = resp.headers[:content_disposition].
201
- split(/;\s+/).
202
- select { |v| v =~ /filename\s*=/ }[0]
203
- local_fname = /([""'])(?:(?=(\\?))\2.)*?\1/.
204
- match(disp_fname).
205
- to_s.gsub(/\A['"]+|['"]+\Z/, "")
136
+ def reverse_paginate(page_results, screen_items)
137
+ if @marker < 1
138
+ if @page == 1
139
+ @format.display_line("[red]Already at page one.[/]")
140
+ prompt(page_results, screen_items)
141
+ else # @page > 1
142
+ @format.display_line("[yellow]Loading results...[/]")
143
+ # TODO Bug: This fails with --size 100
144
+ # TODO Bug: reverse paginate sometimes only returns p1 cache
145
+ page_results = @search.cached(@page - 1)
146
+ @page -= 1
147
+ part = partition(page_results, PSIZE - @opts[:size], @opts[:size])
148
+ screen(page_results, part)
149
+ end
150
+ else
151
+ part = partition(page_results, @marker - @opts[:size], @opts[:size])
152
+ screen(page_results, part)
153
+ end
154
+ end
206
155
 
207
- File.open("#{output_path}/#{local_fname}", 'w') do
208
- |f| f.write(resp.body)
156
+ def retrieve(choice, page_results, screen_items)
157
+ /(\d+)(\s*\|(.*))*/.match(choice) do |str|
158
+ num = str[1].to_i - 1
159
+ download = Downloader.new(page_results[num].link, @opts[:outdir])
160
+ download.save
161
+ unless download.failed?
162
+ @format.display_line(
163
+ "[green]Downloaded '#{download.filename}' successfully.[/]")
164
+ else
165
+ @format.display_line("[red]Download failed (3 attempts).[/]")
166
+ end
167
+ prompt(page_results, screen_items)
209
168
  end
210
169
  end
211
170
  end