curse_client 0.1.0

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.
@@ -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: []