curse_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTcxNTc4MTNiYTU4YjUzOTkyMTJhYTc1NTdlN2I1NzgyZTQ0YTljZQ==
5
+ data.tar.gz: !binary |-
6
+ OWRjNmI2NTk5ZDUzMjhmOTZlY2NlMDY5ZDAwZTMwNmUwMDllYjFmMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ N2E1Y2UzNGZiYmVkMzhkMmU0ODA5NThiMDUyMDg1YjI1ZWM2ZTJiZmIxZjM4
10
+ ZTcwZjhkM2VjZDkxZDJhYTdkNTVmMzE4ZDUyNDkxZTQ1YzM3ZDRhMDZmODdi
11
+ MmVmM2FkZjdjZjE1MmJhMGE2YTkwMTNkOTNkYTg3MjcxNjgxOTM=
12
+ data.tar.gz: !binary |-
13
+ YzhhNWUxNGNjZmVjOTIzMTUwNzQzYmUyNDAwMjJlNTc4ZTg0N2I1MzIyNzQ2
14
+ ODdmZWY1NmY4M2JhZDI5Y2M5MzRkMDJjMGI4MzllMTM3M2U2MjNkNTNiZDI0
15
+ ZDZkZDJhZDNmMmJiNjg0OTY4OTEzODA4ZmJmYmQ4YjE3ZTYzODM=
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ 1.9.3
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in curse_client.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Andy Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,123 @@
1
+ # Curse Client
2
+
3
+ Curse Client is a command line client for curse-hosted Minecraft modpacks.
4
+
5
+ ## Prerequisites
6
+
7
+ You must have Ruby 1.9.3 or greater installed.
8
+ https://www.ruby-lang.org/en/documentation/installation/
9
+
10
+ ## Installation
11
+
12
+ Install the gem.
13
+
14
+ $ gem install curse_client
15
+
16
+ ## Usage
17
+
18
+ ### Help
19
+
20
+ Get a list of commands
21
+
22
+ $ curse help
23
+
24
+ ### List
25
+ List all modpacks
26
+
27
+ $ curse list
28
+
29
+ ### Search
30
+
31
+ Search for a modpack
32
+
33
+ $ curse search "<regexp>"
34
+
35
+ Example:
36
+
37
+ $ curse search "ftb"
38
+
39
+ FTB Departed: Travel to distant worlds and dimensions, encounter ghoulish and strange creatures, and craft legendary tools and equipment in...
40
+ FTB Horizons: Created to showcase some lesser-known 1.6.4 mods in the Minecraft world that don’t often get put into larger packs. You’ve ce...
41
+ FTB Horizons: Daybreaker: Daybreaker is the sequel to the original Horizons, but updated and remastered for 1.7.10. It aims to show off new mods to the...
42
+ FTB Infinity Evolved: Infinity Evolved adds game modes! Two modes are currently included; 'normal' and 'expert'. New and existing worlds are auto...
43
+ FTB Lite: A lightweight, simple-to-use 1.4.7 modpack designed for both users who are either unfamiliar with mods or those with compute...
44
+ --- snip ---
45
+
46
+ ### Show
47
+
48
+ Show details for a modpack
49
+
50
+ $ curse show "<modpack name>"
51
+
52
+ Example:
53
+
54
+ $ curse show "FTB Infinity Evolved"
55
+ FTB Infinity Evolved
56
+ Summary: Infinity Evolved adds game modes! Two modes are currently included; 'normal' and 'expert'. New and existing worlds are auto...
57
+ Authors: FTB, FTBTeam
58
+ Url: http://www.curse.com/modpacks/minecraft/227724-ftb-infinity-evolved
59
+ Categories: Exploration, Extra Large, FTB Official Pack, Magic, Tech
60
+ Downloads: 602881.0
61
+ Popularity: 5792.97021484375
62
+ Files:
63
+ 2283980 2016-02-26T17:02:28 1.7.10 FTBInfinity-2.4.1-1.7.10.zip (Beta)
64
+ 2283863 2016-02-25T22:22:09 1.7.10 FTBInfinity-2.4.0-1.7.10.zip (Beta)
65
+ 2275596 2016-01-15T17:42:51 1.7.10 FTBInfinity-2.3.5-1.7.10.zip (Release)
66
+ 2275249 2016-01-13T21:03:15 1.7.10 FTBInfinity-2.3.4-1.7.10.zip (Beta)
67
+ 2273009 2015-12-30T21:12:24 1.7.10 FTBInfinity-2.3.3-1.7.10.zip (Beta)
68
+ 2272982 2015-12-30T19:22:22 1.7.10 FTBInfinity-2.3.2-1.7.10.zip (Beta)
69
+
70
+ ### Install
71
+
72
+ Install the specified modpack.
73
+
74
+ $ curse install "<modpack name>" [path] [options]
75
+
76
+ Options:
77
+ `--version <version>`: `<version>` can be one of
78
+ - release: The latest release version (default)
79
+ - latest: The latest version, including betas
80
+ - file id: The id of the file
81
+ - file date: The date of the file
82
+ - file name: The name of the file
83
+
84
+ Example:
85
+
86
+ $ curse install "FTB Infinity Evolved" "/Users/amcoder/Applications/MultiMC/instances/Infinity/minecraft" --version latest
87
+ Installing FTB Infinity Evolved
88
+ Downloading http://addons.curse.cursecdn.com/files/2226/936/FTBInfinity-1.0.0-1.7.10.zip 100%
89
+ Downloading http://addons.curse.cursecdn.com/files/2225/549/AOBD-2.4.0.jar 100%
90
+ Downloading http://addons.curse.cursecdn.com/files/2225/85/AgriCraft-1.7.10-1.2.1.jar 100%
91
+ Downloading http://addons.curse.cursecdn.com/files/2219/248/Aroma1997Core-1.7.10-1.0.2.13.jar 100%
92
+ -- snip --
93
+ Installed FTB Infinity Evolved to /Users/amcoder/Applications/MultiMC/instances/Infinity/minecraft
94
+ Requires minecraft 1.7.10 and forge-10.13.2.1291
95
+
96
+ ### MultiMC Install
97
+
98
+ 1. Create a new MultiMC instance for the required minecraft version
99
+ 2. Run `curse install "<modpack name>" "<MultiMC instance folder>/minecraft"`
100
+ 3. Install the specified version of forge in your instance
101
+ 4. Play! :)
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
106
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
107
+ prompt that will allow you to experiment.
108
+
109
+ To install this gem onto your local machine, run `bundle exec rake install`. To
110
+ release a new version, update the version number in `version.rb`, and then run
111
+ `bundle exec rake release`, which will create a git tag for the version, push
112
+ git commits and tags, and push the `.gem` file to
113
+ [rubygems.org](https://rubygems.org).
114
+
115
+ ## Contributing
116
+
117
+ Bug reports and pull requests are welcome on GitHub at https://github.com/amcoder/curse_client.
118
+
119
+
120
+ ## License
121
+
122
+ The gem is available as open source under the terms of the
123
+ [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "curse_client"
5
+
6
+ require "pry"
7
+ Pry.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'curse_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "curse_client"
8
+ spec.version = CurseClient::VERSION
9
+ spec.authors = ["Andy Miller"]
10
+ spec.email = ["amcoder@gmail.com"]
11
+
12
+ spec.homepage = "https://github.com/amcoder/curse_client"
13
+ spec.summary = %q{A Minecraft curse client for the command line}
14
+ spec.description = %q{This is an unofficial client for curse-hosted Minecraft modpacks.}
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.required_ruby_version = ">= 1.9.3"
23
+
24
+ spec.add_runtime_dependency "thor", "~> 0.19"
25
+ spec.add_runtime_dependency "bzip2-ffi", "~> 1.0"
26
+ spec.add_runtime_dependency "rubyzip", "~> 1.2"
27
+ spec.add_runtime_dependency "http-cookie", "~> 1.0"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.10"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "pry"
33
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'curse_client/cli'
4
+
5
+ CurseClient::CLI.start(ARGV)
@@ -0,0 +1,10 @@
1
+ require 'curse_client/version'
2
+ require 'curse_client/feed'
3
+ require 'curse_client/api'
4
+ require 'curse_client/client'
5
+ require 'curse_client/downloader'
6
+ require 'curse_client/installer'
7
+ require 'curse_client/http'
8
+
9
+ module CurseClient
10
+ end
@@ -0,0 +1,63 @@
1
+ module CurseClient
2
+ class API
3
+
4
+ class UnauthorizedError < StandardError; end
5
+
6
+ DEFAULT_URL = "https://curse-rest-proxy.azurewebsites.net/api"
7
+
8
+ attr_accessor :token
9
+
10
+ def initialize(url = DEFAULT_URL)
11
+ @url = url
12
+ end
13
+
14
+ def authenticated?
15
+ !token.nil?
16
+ end
17
+
18
+ def authenticate(username, password)
19
+ response = HTTP.post("#{url}/authenticate",
20
+ { username: username, password: password },
21
+ headers: { "Content-Type" => "application/json" },
22
+ format: :json)
23
+ case response
24
+ when Net::HTTPUnauthorized
25
+ raise UnauthorizedError, "Invalid username or password"
26
+ when Net::HTTPSuccess
27
+ data = JSON.parse(response.body, symbolize_names: true)
28
+ @token = "#{data[:session][:user_id]}:#{data[:session][:token]}"
29
+ else
30
+ response.error!
31
+ end
32
+ end
33
+
34
+ def addon(id)
35
+ get_json("addon/#{id}")
36
+ end
37
+
38
+ def addon_files(id)
39
+ get_json("addon/#{id}/files")
40
+ end
41
+
42
+ def addon_file(addon_id, file_id)
43
+ get_json("addon/#{addon_id}/file/#{file_id}")
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :url
49
+
50
+ def get_json(path)
51
+ response = HTTP.get("#{url}/#{path}",
52
+ headers: { "Authorization" => "Token #{token}" })
53
+ case response
54
+ when Net::HTTPUnauthorized
55
+ raise UnauthorizedError, "Invalid token"
56
+ when Net::HTTPSuccess
57
+ return JSON.parse(response.body, symbolize_names: true)
58
+ else
59
+ response.error!
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,64 @@
1
+ require 'thor'
2
+ require 'curse_client'
3
+
4
+ module CurseClient
5
+ class CLI < Thor
6
+
7
+ desc "list", "List the available modpacks"
8
+ def list
9
+ client.modpacks.each do |modpack|
10
+ puts simple_description(modpack)
11
+ end
12
+ end
13
+
14
+ desc "search <regexp>", "Search for a modpack"
15
+ def search(regexp)
16
+ client.modpacks.
17
+ select { |m| m[:name] =~ /#{regexp}/i }.
18
+ each { |m| puts simple_description(m) }
19
+ end
20
+
21
+ desc "show <modpack>", "Show details for a modpack"
22
+ def show(modpack_name)
23
+ modpack = client.find_by_name(modpack_name)
24
+
25
+ if modpack
26
+ puts modpack[:name]
27
+ puts "Summary: #{modpack[:summary]}"
28
+ puts "Authors: #{modpack[:authors].map{|a| a[:name]}.join(", ")}"
29
+ puts "Url: #{modpack[:web_site_url]}"
30
+ puts "Categories: #{modpack[:categories].map{|c| c[:name]}.join(", ")}"
31
+ puts "Downloads: #{modpack[:download_count]}"
32
+ puts "Popularity: #{modpack[:popularity_score]}"
33
+ puts "Files:"
34
+ client.addon_files(modpack[:id]).each do |f|
35
+ puts " #{f[:id]} #{f[:file_date]} #{f[:game_version].first} #{f[:file_name]} (#{f[:release_type]})"
36
+ end
37
+ else
38
+ puts "Cannot find modpack #{modpack_name}"
39
+ end
40
+ end
41
+
42
+ desc "install <modpack> [path] [options]", "Install the modpack to [path]"
43
+ option :version, alias: "-v", default: "release",
44
+ desc: "One of 'latest', 'release', file id, file date, or file name"
45
+ def install(modpack_name, path = modpack_name)
46
+ modpack = client.find_by_name(modpack_name)
47
+ if modpack
48
+ client.install(modpack, path, options[:version])
49
+ else
50
+ puts "Cannot find modpack #{modpack_name}"
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def simple_description(modpack)
57
+ "#{modpack[:name]}: #{modpack[:summary]}"
58
+ end
59
+
60
+ def client
61
+ @client ||= CurseClient::Client.new
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,147 @@
1
+ require 'fileutils'
2
+ require 'zip'
3
+ require 'pstore'
4
+
5
+ module CurseClient
6
+ class Client
7
+
8
+ HOME_DIR = "#{Dir.home}/.curse_client"
9
+ CACHE_DIR = "#{HOME_DIR}/cache"
10
+
11
+ def initialize(feed = CurseClient::Feed.new, api = CurseClient::API.new)
12
+ @feed = feed
13
+ @api = api
14
+
15
+ FileUtils::mkpath(CACHE_DIR)
16
+
17
+ api.token = token
18
+ end
19
+
20
+ def projects
21
+ @projects ||= load_projects
22
+ end
23
+
24
+ def modpacks
25
+ projects.
26
+ select { |d| d[:category_section] == :modpacks }.
27
+ sort_by { |d| d[:name] }
28
+ end
29
+
30
+ def find_by_name(name)
31
+ modpack = modpacks.
32
+ select { |m| m[:name] == name }.
33
+ first
34
+ addon(modpack[:id])
35
+ end
36
+
37
+ def addon(id)
38
+ with_authentication do
39
+ api.addon(id)
40
+ end
41
+ end
42
+
43
+ def addon_files(addon_id)
44
+ with_authentication do
45
+ api.addon_files(addon_id)[:files].
46
+ sort_by{|f| f[:file_date] }.
47
+ reverse
48
+ end
49
+ end
50
+
51
+ def addon_file(addon_id, file_id)
52
+ with_authentication do
53
+ api.addon_file(addon_id, file_id)
54
+ end
55
+ end
56
+
57
+ def install(modpack, path, version)
58
+ Installer.new(self).install(modpack, path, version)
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :feed,
64
+ :api
65
+
66
+ def with_authentication
67
+ yield
68
+ rescue API::UnauthorizedError
69
+ retry if authenticate
70
+ end
71
+
72
+ def authenticate
73
+ print "Curse username: "
74
+ username = $stdin.gets.chomp
75
+ print "Curse Password: "
76
+ password = $stdin.noecho(&:gets).chomp
77
+ puts ""
78
+ api.authenticate(username, password)
79
+ self.token = api.token
80
+ true
81
+ rescue API::UnauthorizedError
82
+ puts "Inavlid username or password"
83
+ false
84
+ end
85
+
86
+ def load_projects
87
+ puts "Loading project information from curse"
88
+
89
+ store = load_store
90
+
91
+ if store[:complete_timestamp] < feed.complete_timestamp
92
+ complete = feed.complete
93
+ store[:complete_timestamp] = complete[:timestamp]
94
+ store[:projects] = strip_data(complete[:data])
95
+ save_store(store)
96
+ end
97
+
98
+ store[:projects]
99
+ end
100
+
101
+ def strip_data(data)
102
+ data.map do |project|
103
+ {
104
+ id: project[:id],
105
+ name: project[:name],
106
+ summary: project[:summary],
107
+ category_section: project[:category_section][:name].downcase.gsub(/ /, "_").to_sym,
108
+ }
109
+ end
110
+ end
111
+
112
+ def load_store
113
+ store = PStore.new("#{CACHE_DIR}/projects.pstore")
114
+ store.transaction(true) do
115
+ {
116
+ complete_timestamp: store[:complete_timestamp] || 0,
117
+ projects: store[:projects]
118
+ }
119
+ end
120
+ end
121
+
122
+ def save_store(data)
123
+ store = PStore.new("#{CACHE_DIR}/projects.pstore")
124
+ store.transaction do
125
+ store[:complete_timestamp] = data[:complete_timestamp]
126
+ store[:projects] = data[:projects]
127
+ end
128
+ end
129
+
130
+ def token
131
+ @token ||=
132
+ begin
133
+ store = PStore.new("#{HOME_DIR}/token.pstore")
134
+ store.transaction(true) do
135
+ store[:token]
136
+ end
137
+ end
138
+ end
139
+
140
+ def token=(t)
141
+ store = PStore.new("#{HOME_DIR}/token.pstore")
142
+ store.transaction do
143
+ store[:token] = t
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,48 @@
1
+ require 'tempfile'
2
+
3
+ module CurseClient
4
+ class Downloader
5
+
6
+ class DownloadError < StandardError; end
7
+
8
+ def fetch(uri, path, &block)
9
+ HTTP.get(uri) do |response|
10
+ case response
11
+ when Net::HTTPSuccess
12
+ save_response(response, path, &block)
13
+ when Net::HTTPRedirection
14
+ url = URI.escape(response['location'], "[]")
15
+ return fetch(url, path, &block)
16
+ else
17
+ response.error!
18
+ end
19
+ end
20
+ rescue Exception => e
21
+ raise DownloadError, e.message
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :logger
27
+
28
+ def save_response(response, path)
29
+ length = response['Content-Length'].to_i
30
+ done = 0
31
+
32
+ FileUtils.mkpath(File.dirname(path))
33
+ temp_file_name = "#{path}.#{Time.now.to_f}"
34
+ File.open(temp_file_name, "w+") do |file|
35
+ response.read_body do |chunk|
36
+ file << chunk
37
+ done += chunk.length
38
+ progress = (done.quo(length) * 100).to_i
39
+ yield progress if block_given?
40
+ end
41
+ end
42
+
43
+ FileUtils.move(temp_file_name, path)
44
+ ensure
45
+ FileUtils.remove(temp_file_name) if File.exists?(temp_file_name)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,69 @@
1
+ require 'bzip2/ffi'
2
+ require 'open-uri'
3
+ require 'json'
4
+
5
+ module CurseClient
6
+ class Feed
7
+
8
+ DEFAULT_URL = "http://clientupdate-v6.cursecdn.com/feed/addons/432/v10/"
9
+ COMPLETE_URL = DEFAULT_URL + "complete.json.bz2"
10
+ HOURLY_URL = DEFAULT_URL + "hourly.json.bz2"
11
+
12
+ def initialize(url = DEFAULT_URL)
13
+ @url = url
14
+ end
15
+
16
+ def hourly_timestamp
17
+ get_timestamp(HOURLY_URL)
18
+ end
19
+
20
+ def hourly
21
+ download_bz2(HOURLY_URL + "?t=#{hourly_timestamp}")
22
+ end
23
+
24
+ def complete_timestamp
25
+ get_timestamp(COMPLETE_URL)
26
+ end
27
+
28
+ def complete
29
+ download_bz2(COMPLETE_URL)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :url
35
+
36
+ def download_bz2(url)
37
+ open(COMPLETE_URL) do |request|
38
+ Bzip2::FFI::Reader.open(request) do |bz2|
39
+ fix_keys(JSON.parse(bz2.read))
40
+ end
41
+ end
42
+ end
43
+
44
+ def get_timestamp(url)
45
+ open(url + ".txt") do |request|
46
+ Integer(request.read)
47
+ end
48
+ end
49
+
50
+ def fix_keys(value)
51
+ case value
52
+ when Array
53
+ value.map{ |v| fix_keys(v) }
54
+ when Hash
55
+ Hash[value.map { |k, v| [underscore(k).to_sym, fix_keys(v)] }]
56
+ else
57
+ value
58
+ end
59
+ end
60
+
61
+ def underscore(name)
62
+ return name.downcase if name.match(/\A[A-Z]+\z/)
63
+ name.
64
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
65
+ gsub(/([a-z])([A-Z])/, '\1_\2').
66
+ downcase
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,45 @@
1
+ require 'net/http'
2
+
3
+ module CurseClient
4
+ class HTTP
5
+
6
+ def self.get(uri, options = {}, &block)
7
+ uri = create_uri(uri)
8
+ request = Net::HTTP::Get.new(uri.request_uri)
9
+ request.initialize_http_header(options[:headers]) if options[:headers]
10
+ send_request(uri, request, &block)
11
+ end
12
+
13
+ def self.post(uri, body, options = {}, &block)
14
+ uri = create_uri(uri)
15
+ request = Net::HTTP::Post.new(uri.request_uri)
16
+ request.initialize_http_header(options[:headers]) if options[:headers]
17
+ request.body = parse_request_body(body, options[:format] || :json)
18
+ send_request(uri, request, &block)
19
+ end
20
+
21
+ private
22
+
23
+ def self.create_uri(uri)
24
+ return uri if uri.is_a?(URI)
25
+ URI.parse(uri)
26
+ end
27
+
28
+ def self.parse_request_body(body, format)
29
+ case format
30
+ when :json
31
+ JSON.generate(body)
32
+ else
33
+ raise NotImplementedError, "Format #{format} is not implemented"
34
+ end
35
+ end
36
+
37
+ def self.send_request(uri, request)
38
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
39
+ return http.request(request) do |response|
40
+ yield response if block_given?
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,115 @@
1
+ module CurseClient
2
+ class Installer
3
+ def initialize(client)
4
+ @client = client
5
+ end
6
+
7
+ def install(modpack, path, version)
8
+ modpack_file = find_file(modpack, version)
9
+ unless modpack_file
10
+ puts "Could not find #{modpack[:name]} version #{version}"
11
+ return
12
+ end
13
+
14
+ puts "Installing #{modpack[:name]}"
15
+ file = download(modpack_file[:download_url])
16
+
17
+ path = File.expand_path(path)
18
+ FileUtils::mkpath(path) unless File.exists?(path)
19
+ unless File.directory?(path)
20
+ puts "#{path} is not a directory"
21
+ return
22
+ end
23
+
24
+ manifest = unzip(file, path)
25
+ download_mods(manifest, path)
26
+ write_configuration(path, modpack, modpack_file)
27
+
28
+ puts "\nInstalled #{modpack[:name]} to #{File.expand_path(path)}"
29
+ puts "Requires minecraft #{manifest["minecraft"]["version"]} and #{manifest["minecraft"]["modLoaders"][0]["id"]}"
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :client
35
+
36
+ def find_file(modpack, version)
37
+ client.addon_files(modpack[:id]).
38
+ select{|f| file_eligible?(f, version) }.
39
+ first
40
+ end
41
+
42
+ def file_eligible?(file, version)
43
+ case version
44
+ when "release"
45
+ file[:release_type] == "Release"
46
+ when "latest"
47
+ true
48
+ else
49
+ file[:id].to_s == version ||
50
+ file[:file_date] == version ||
51
+ file[:file_name] =~ /\A#{version}/
52
+ end
53
+ end
54
+
55
+ def unzip(file, path)
56
+ Zip::File.open(file) do |zip|
57
+ # manifest
58
+ entry = zip.find_entry('manifest.json')
59
+ manifest = entry.get_input_stream do |stream|
60
+ JSON.parse(stream.read.gsub("\r", "").gsub("\n", ""))
61
+ end
62
+ overrides_name = manifest["overrides"]
63
+
64
+ # extract
65
+ zip.each do |entry|
66
+ file_path = "#{path}/#{entry.name.gsub(/\A#{overrides_name}\//, "")}"
67
+ entry.extract(file_path) { true }
68
+ end
69
+ manifest
70
+ end
71
+ end
72
+
73
+ def download_mods(manifest, path)
74
+ modpath = "#{path}/mods"
75
+ FileUtils.mkpath(modpath)
76
+ manifest["files"].each do |manifest_mod|
77
+ mod_file = client.addon_file(manifest_mod["projectID"], manifest_mod["fileID"])
78
+ file = download(mod_file[:download_url])
79
+ FileUtils.copy(file, modpath)
80
+ end
81
+ end
82
+
83
+ def write_configuration(path, modpack, modpack_file)
84
+ File.open("#{path}/curse_client.json", "w+") do |file|
85
+ file << JSON.pretty_generate({
86
+ id: modpack[:id],
87
+ name: modpack[:name],
88
+ file: modpack_file
89
+ })
90
+ end
91
+ end
92
+
93
+ def download(url)
94
+
95
+ uri = URI.parse(escape(url))
96
+ path = "#{Client::CACHE_DIR}/#{uri.host}/#{URI.unescape(uri.path)}"
97
+
98
+ print "Downloading #{url} "
99
+ unless File.exists?(path)
100
+ downloader = CurseClient::Downloader.new
101
+ downloader.fetch(uri, path) do |progress|
102
+ print "\b\b\b\b% 3d%%" % progress
103
+ end
104
+ end
105
+ puts "\b\b\b\b100%"
106
+
107
+ path
108
+ end
109
+
110
+ def escape(url)
111
+ URI.escape(URI.escape(url), "[]")
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,3 @@
1
+ module CurseClient
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: curse_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Miller
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.19'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bzip2-ffi
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubyzip
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: http-cookie
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: This is an unofficial client for curse-hosted Minecraft modpacks.
126
+ email:
127
+ - amcoder@gmail.com
128
+ executables:
129
+ - curse
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .rspec
135
+ - .ruby-version
136
+ - .travis.yml
137
+ - Gemfile
138
+ - LICENSE.txt
139
+ - README.md
140
+ - Rakefile
141
+ - bin/console
142
+ - bin/setup
143
+ - curse_client.gemspec
144
+ - exe/curse
145
+ - lib/curse_client.rb
146
+ - lib/curse_client/api.rb
147
+ - lib/curse_client/cli.rb
148
+ - lib/curse_client/client.rb
149
+ - lib/curse_client/downloader.rb
150
+ - lib/curse_client/feed.rb
151
+ - lib/curse_client/http.rb
152
+ - lib/curse_client/installer.rb
153
+ - lib/curse_client/version.rb
154
+ homepage: https://github.com/amcoder/curse_client
155
+ licenses:
156
+ - MIT
157
+ metadata: {}
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ! '>='
165
+ - !ruby/object:Gem::Version
166
+ version: 1.9.3
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ! '>='
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.5.2
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: A Minecraft curse client for the command line
178
+ test_files: []