diff options
author | Kelly Rauchenberger <fefferburbia@gmail.com> | 2018-03-01 16:03:16 -0500 |
---|---|---|
committer | Kelly Rauchenberger <fefferburbia@gmail.com> | 2018-03-01 16:03:16 -0500 |
commit | 473b327ceed3afb5e5683002b39fd9c1947cb25a (patch) | |
tree | 0dd2eac68605ad8cf34a2e2e54c6a44ad3ab14c1 | |
parent | d85fed8541a9580e820a907d83a2184b020572ba (diff) | |
download | lunatic-473b327ceed3afb5e5683002b39fd9c1947cb25a.tar.gz lunatic-473b327ceed3afb5e5683002b39fd9c1947cb25a.tar.bz2 lunatic-473b327ceed3afb5e5683002b39fd9c1947cb25a.zip |
Redesigned persistent data formta
This is the start of a project to add imagery to the bot's output. We began by rewriting the scraper to use a SQLite datafile instead of dumping achievement names to a text file. This allows storage of additional information about each achievement, and allows for more sophisticated scraping. Profiles to be scraped can be added on the command line using the scraper script, instead of being specified in a config file. The scraper can conduct full or delta scrapes; in a delta scrape, only each profile's recent games are scraped, whereas they are all scraped in a full scrape. When a game is scraped for the first time, images from the store page of that game are saved locally to be used by the bot. The bot has been altered to not use Twitter, and instead generate a pixelated image based on an image from the game of the chosen achievement. This is just for development purposes. It also crashes occasionally due to picking an achievement from a game that does not have any images saved. Sprites of the moons from Odyssey have been included in the repository. A short message denoting their copyright is included.
-rw-r--r-- | CMakeLists.txt | 11 | ||||
-rw-r--r-- | Gemfile | 5 | ||||
-rw-r--r-- | Gemfile.lock | 19 | ||||
-rw-r--r-- | database.cpp | 122 | ||||
-rw-r--r-- | database.h | 53 | ||||
-rw-r--r-- | lunatic.cpp | 50 | ||||
-rw-r--r-- | moons/README.md | 1 | ||||
-rw-r--r-- | moons/blue.png | bin | 0 -> 37618 bytes | |||
-rw-r--r-- | moons/brown.png | bin | 0 -> 38150 bytes | |||
-rw-r--r-- | moons/cyan.png | bin | 0 -> 33139 bytes | |||
-rw-r--r-- | moons/green.png | bin | 0 -> 36760 bytes | |||
-rw-r--r-- | moons/orange.png | bin | 0 -> 36465 bytes | |||
-rw-r--r-- | moons/pink.png | bin | 0 -> 30838 bytes | |||
-rw-r--r-- | moons/purple.png | bin | 0 -> 32331 bytes | |||
-rw-r--r-- | moons/red.png | bin | 0 -> 37165 bytes | |||
-rw-r--r-- | moons/star.png | bin | 0 -> 27829 bytes | |||
-rw-r--r-- | moons/white.png | bin | 0 -> 29878 bytes | |||
-rw-r--r-- | moons/yellow.png | bin | 0 -> 32939 bytes | |||
-rw-r--r-- | schema.sql | 32 | ||||
-rw-r--r-- | scrape.rb | 181 |
20 files changed, 442 insertions, 32 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 55a671b..00c0ad9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -5,12 +5,17 @@ add_subdirectory(vendor/libtwittercpp) | |||
5 | 5 | ||
6 | find_package(PkgConfig) | 6 | find_package(PkgConfig) |
7 | pkg_check_modules(yaml-cpp yaml-cpp REQUIRED) | 7 | pkg_check_modules(yaml-cpp yaml-cpp REQUIRED) |
8 | pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED) | ||
9 | pkg_check_modules(sqlite3 sqlite3>=3.8.3 REQUIRED) | ||
8 | 10 | ||
9 | include_directories( | 11 | include_directories( |
10 | vendor/libtwittercpp/src | 12 | vendor/libtwittercpp/src |
11 | ${yaml-cpp_INCLUDE_DIRS}) | 13 | ${yaml-cpp_INCLUDE_DIRS} |
14 | ${GraphicsMagick_INCLUDE_DIRS}) | ||
12 | 15 | ||
13 | add_executable(lunatic lunatic.cpp) | 16 | link_directories(${GraphicsMagick_LIBRARY_DIRS}) |
17 | |||
18 | add_executable(lunatic lunatic.cpp database.cpp) | ||
14 | set_property(TARGET lunatic PROPERTY CXX_STANDARD 11) | 19 | set_property(TARGET lunatic PROPERTY CXX_STANDARD 11) |
15 | set_property(TARGET lunatic PROPERTY CXX_STANDARD_REQUIRED ON) | 20 | set_property(TARGET lunatic PROPERTY CXX_STANDARD_REQUIRED ON) |
16 | target_link_libraries(lunatic ${yaml-cpp_LIBRARIES} twitter++) \ No newline at end of file | 21 | target_link_libraries(lunatic ${yaml-cpp_LIBRARIES} ${GraphicsMagick_LIBRARIES} ${sqlite3_LIBRARIES} twitter++) \ No newline at end of file |
diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..57a02a2 --- /dev/null +++ b/Gemfile | |||
@@ -0,0 +1,5 @@ | |||
1 | source 'https://rubygems.org' | ||
2 | |||
3 | gem 'sqlite3' | ||
4 | gem 'sequel' | ||
5 | gem 'nokogiri' | ||
diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..58ec121 --- /dev/null +++ b/Gemfile.lock | |||
@@ -0,0 +1,19 @@ | |||
1 | GEM | ||
2 | remote: https://rubygems.org/ | ||
3 | specs: | ||
4 | mini_portile2 (2.3.0) | ||
5 | nokogiri (1.8.2) | ||
6 | mini_portile2 (~> 2.3.0) | ||
7 | sequel (5.5.0) | ||
8 | sqlite3 (1.3.13) | ||
9 | |||
10 | PLATFORMS | ||
11 | ruby | ||
12 | |||
13 | DEPENDENCIES | ||
14 | nokogiri | ||
15 | sequel | ||
16 | sqlite3 | ||
17 | |||
18 | BUNDLED WITH | ||
19 | 1.16.1 | ||
diff --git a/database.cpp b/database.cpp new file mode 100644 index 0000000..f8b5016 --- /dev/null +++ b/database.cpp | |||
@@ -0,0 +1,122 @@ | |||
1 | #include "database.h" | ||
2 | #include <sqlite3.h> | ||
3 | #include <stdexcept> | ||
4 | |||
5 | database::database(std::string path) | ||
6 | { | ||
7 | if (sqlite3_open_v2( | ||
8 | path.c_str(), | ||
9 | &ppdb_, | ||
10 | SQLITE_OPEN_READONLY, | ||
11 | NULL) != SQLITE_OK) | ||
12 | { | ||
13 | // We still have to free the resources allocated. In the event that | ||
14 | // allocation failed, ppdb will be null and sqlite3_close_v2 will just | ||
15 | // ignore it. | ||
16 | std::string errmsg(sqlite3_errmsg(ppdb_)); | ||
17 | sqlite3_close_v2(ppdb_); | ||
18 | |||
19 | throw std::logic_error(errmsg); | ||
20 | } | ||
21 | } | ||
22 | |||
23 | database::database(database&& other) : database() | ||
24 | { | ||
25 | swap(*this, other); | ||
26 | } | ||
27 | |||
28 | database& database::operator=(database&& other) | ||
29 | { | ||
30 | swap(*this, other); | ||
31 | |||
32 | return *this; | ||
33 | } | ||
34 | |||
35 | void swap(database& first, database& second) | ||
36 | { | ||
37 | std::swap(first.ppdb_, second.ppdb_); | ||
38 | } | ||
39 | |||
40 | database::~database() | ||
41 | { | ||
42 | sqlite3_close_v2(ppdb_); | ||
43 | } | ||
44 | |||
45 | achievement database::getRandomAchievement() const | ||
46 | { | ||
47 | std::string queryString = "SELECT achievements.achievement_id, achievements.game_id, achievements.title, games.moon_image FROM achievements INNER JOIN games ON games.game_id = achievements.game_id ORDER BY RANDOM() LIMIT 1"; | ||
48 | |||
49 | sqlite3_stmt* ppstmt; | ||
50 | if (sqlite3_prepare_v2( | ||
51 | ppdb_, | ||
52 | queryString.c_str(), | ||
53 | queryString.length(), | ||
54 | &ppstmt, | ||
55 | NULL) != SQLITE_OK) | ||
56 | { | ||
57 | std::string errorMsg = sqlite3_errmsg(ppdb_); | ||
58 | sqlite3_finalize(ppstmt); | ||
59 | |||
60 | throw std::logic_error(errorMsg); | ||
61 | } | ||
62 | |||
63 | if (sqlite3_step(ppstmt) != SQLITE_ROW) | ||
64 | { | ||
65 | std::string errorMsg = sqlite3_errmsg(ppdb_); | ||
66 | sqlite3_finalize(ppstmt); | ||
67 | |||
68 | throw std::logic_error(errorMsg); | ||
69 | } | ||
70 | |||
71 | achievement result; | ||
72 | |||
73 | result.achievementId = sqlite3_column_int(ppstmt, 0); | ||
74 | result.gameId = sqlite3_column_int(ppstmt, 1); | ||
75 | result.title = reinterpret_cast<const char*>(sqlite3_column_text(ppstmt, 2)); | ||
76 | result.moonImage = reinterpret_cast<const char*>(sqlite3_column_text(ppstmt, 3)); | ||
77 | |||
78 | sqlite3_finalize(ppstmt); | ||
79 | |||
80 | return result; | ||
81 | } | ||
82 | |||
83 | std::string database::getRandomImageForGame(int gameId) const | ||
84 | { | ||
85 | std::string queryString = "SELECT filename FROM images WHERE game_id = ? ORDER BY RANDOM() LIMIT 1"; | ||
86 | |||
87 | sqlite3_stmt* ppstmt; | ||
88 | if (sqlite3_prepare_v2( | ||
89 | ppdb_, | ||
90 | queryString.c_str(), | ||
91 | queryString.length(), | ||
92 | &ppstmt, | ||
93 | NULL) != SQLITE_OK) | ||
94 | { | ||
95 | std::string errorMsg = sqlite3_errmsg(ppdb_); | ||
96 | sqlite3_finalize(ppstmt); | ||
97 | |||
98 | throw std::logic_error(errorMsg); | ||
99 | } | ||
100 | |||
101 | if (sqlite3_bind_int(ppstmt, 1, gameId) != SQLITE_OK) | ||
102 | { | ||
103 | std::string errorMsg = sqlite3_errmsg(ppdb_); | ||
104 | sqlite3_finalize(ppstmt); | ||
105 | |||
106 | throw std::logic_error(errorMsg); | ||
107 | } | ||
108 | |||
109 | if (sqlite3_step(ppstmt) != SQLITE_ROW) | ||
110 | { | ||
111 | std::string errorMsg = sqlite3_errmsg(ppdb_); | ||
112 | sqlite3_finalize(ppstmt); | ||
113 | |||
114 | throw std::logic_error(errorMsg); | ||
115 | } | ||
116 | |||
117 | std::string result = reinterpret_cast<const char*>(sqlite3_column_text(ppstmt, 0)); | ||
118 | |||
119 | sqlite3_finalize(ppstmt); | ||
120 | |||
121 | return result; | ||
122 | } | ||
diff --git a/database.h b/database.h new file mode 100644 index 0000000..560eeda --- /dev/null +++ b/database.h | |||
@@ -0,0 +1,53 @@ | |||
1 | #ifndef DATABASE_H_75C3CE0F | ||
2 | #define DATABASE_H_75C3CE0F | ||
3 | |||
4 | #include <string> | ||
5 | |||
6 | struct sqlite3; | ||
7 | |||
8 | struct achievement { | ||
9 | int achievementId; | ||
10 | int gameId; | ||
11 | std::string title; | ||
12 | std::string moonImage; | ||
13 | }; | ||
14 | |||
15 | class database { | ||
16 | public: | ||
17 | |||
18 | // Constructor | ||
19 | |||
20 | explicit database(std::string path); | ||
21 | |||
22 | // Disable copying | ||
23 | |||
24 | database(const database& other) = delete; | ||
25 | database& operator=(const database& other) = delete; | ||
26 | |||
27 | // Move constructor and move assignment | ||
28 | |||
29 | database(database&& other); | ||
30 | database& operator=(database&& other); | ||
31 | |||
32 | // Swap | ||
33 | |||
34 | friend void swap(database& first, database& second); | ||
35 | |||
36 | // Destructor | ||
37 | |||
38 | ~database(); | ||
39 | |||
40 | // Accessors | ||
41 | |||
42 | achievement getRandomAchievement() const; | ||
43 | |||
44 | std::string getRandomImageForGame(int gameId) const; | ||
45 | |||
46 | private: | ||
47 | |||
48 | database() = default; | ||
49 | |||
50 | sqlite3* ppdb_ = nullptr; | ||
51 | }; | ||
52 | |||
53 | #endif /* end of include guard: DATABASE_H_75C3CE0F */ | ||
diff --git a/lunatic.cpp b/lunatic.cpp index 09dcc41..ca3140a 100644 --- a/lunatic.cpp +++ b/lunatic.cpp | |||
@@ -5,6 +5,8 @@ | |||
5 | #include <twitter.h> | 5 | #include <twitter.h> |
6 | #include <chrono> | 6 | #include <chrono> |
7 | #include <thread> | 7 | #include <thread> |
8 | #include <Magick++.h> | ||
9 | #include "database.h" | ||
8 | 10 | ||
9 | int main(int argc, char** argv) | 11 | int main(int argc, char** argv) |
10 | { | 12 | { |
@@ -14,6 +16,8 @@ int main(int argc, char** argv) | |||
14 | return -1; | 16 | return -1; |
15 | } | 17 | } |
16 | 18 | ||
19 | Magick::InitializeMagick(nullptr); | ||
20 | |||
17 | std::string configfile(argv[1]); | 21 | std::string configfile(argv[1]); |
18 | YAML::Node config = YAML::LoadFile(configfile); | 22 | YAML::Node config = YAML::LoadFile(configfile); |
19 | 23 | ||
@@ -23,7 +27,9 @@ int main(int argc, char** argv) | |||
23 | auth.setAccessKey(config["access_key"].as<std::string>()); | 27 | auth.setAccessKey(config["access_key"].as<std::string>()); |
24 | auth.setAccessSecret(config["access_secret"].as<std::string>()); | 28 | auth.setAccessSecret(config["access_secret"].as<std::string>()); |
25 | 29 | ||
26 | twitter::client client(auth); | 30 | //twitter::client client(auth); |
31 | |||
32 | database db(config["database"].as<std::string>()); | ||
27 | 33 | ||
28 | std::random_device randomDevice; | 34 | std::random_device randomDevice; |
29 | std::mt19937 rng(randomDevice()); | 35 | std::mt19937 rng(randomDevice()); |
@@ -32,6 +38,40 @@ int main(int argc, char** argv) | |||
32 | { | 38 | { |
33 | std::cout << "Generating tweet" << std::endl; | 39 | std::cout << "Generating tweet" << std::endl; |
34 | 40 | ||
41 | achievement ach = db.getRandomAchievement(); | ||
42 | std::string imageName = db.getRandomImageForGame(ach.gameId); | ||
43 | std::string imagePath = config["images"].as<std::string>() | ||
44 | + "/" + imageName; | ||
45 | |||
46 | |||
47 | try | ||
48 | { | ||
49 | Magick::Image image; | ||
50 | image.read(imagePath); | ||
51 | image.transform("1600x900"); | ||
52 | image.scale("160x90"); | ||
53 | image.scale("1600x900"); | ||
54 | image.magick("png"); | ||
55 | image.write("output.png"); | ||
56 | } catch (const Magick::WarningCoder& ex) | ||
57 | { | ||
58 | // Ok | ||
59 | } | ||
60 | |||
61 | |||
62 | |||
63 | |||
64 | |||
65 | |||
66 | |||
67 | |||
68 | |||
69 | |||
70 | |||
71 | |||
72 | |||
73 | /* | ||
74 | |||
35 | // Reload achievements list every time in case it has been updated | 75 | // Reload achievements list every time in case it has been updated |
36 | std::vector<std::string> achievements; | 76 | std::vector<std::string> achievements; |
37 | std::ifstream datafile(config["achievements"].as<std::string>()); | 77 | std::ifstream datafile(config["achievements"].as<std::string>()); |
@@ -53,7 +93,7 @@ int main(int argc, char** argv) | |||
53 | } | 93 | } |
54 | 94 | ||
55 | std::uniform_int_distribution<int> dist(0, achievements.size() - 1); | 95 | std::uniform_int_distribution<int> dist(0, achievements.size() - 1); |
56 | std::string achievement = achievements[dist(rng)]; | 96 | std::string achievement = achievements[dist(rng)];*/ |
57 | 97 | ||
58 | std::string header; | 98 | std::string header; |
59 | if (std::bernoulli_distribution(1.0 / 50.0)(rng)) | 99 | if (std::bernoulli_distribution(1.0 / 50.0)(rng)) |
@@ -63,16 +103,16 @@ int main(int argc, char** argv) | |||
63 | header = "YOU GOT A MOON!"; | 103 | header = "YOU GOT A MOON!"; |
64 | } | 104 | } |
65 | 105 | ||
66 | std::string action = header + "\n" + achievement; | 106 | std::string action = header + "\n" + ach.title; |
67 | action.resize(140); | 107 | action.resize(140); |
68 | 108 | ||
69 | try | 109 | /*try |
70 | { | 110 | { |
71 | client.updateStatus(action); | 111 | client.updateStatus(action); |
72 | } catch (const twitter::twitter_error& e) | 112 | } catch (const twitter::twitter_error& e) |
73 | { | 113 | { |
74 | std::cout << "Twitter error: " << e.what() << std::endl; | 114 | std::cout << "Twitter error: " << e.what() << std::endl; |
75 | } | 115 | }*/ |
76 | 116 | ||
77 | std::cout << action << std::endl; | 117 | std::cout << action << std::endl; |
78 | std::cout << "Waiting" << std::endl; | 118 | std::cout << "Waiting" << std::endl; |
diff --git a/moons/README.md b/moons/README.md new file mode 100644 index 0000000..876f0db --- /dev/null +++ b/moons/README.md | |||
@@ -0,0 +1 @@ | |||
The images in this directory are originally from the game Super Mario Odyssey, which is copyrighted by Nintendo. \ No newline at end of file | |||
diff --git a/moons/blue.png b/moons/blue.png new file mode 100644 index 0000000..5e4ca02 --- /dev/null +++ b/moons/blue.png | |||
Binary files differ | |||
diff --git a/moons/brown.png b/moons/brown.png new file mode 100644 index 0000000..6a31b16 --- /dev/null +++ b/moons/brown.png | |||
Binary files differ | |||
diff --git a/moons/cyan.png b/moons/cyan.png new file mode 100644 index 0000000..8984126 --- /dev/null +++ b/moons/cyan.png | |||
Binary files differ | |||
diff --git a/moons/green.png b/moons/green.png new file mode 100644 index 0000000..5e7d311 --- /dev/null +++ b/moons/green.png | |||
Binary files differ | |||
diff --git a/moons/orange.png b/moons/orange.png new file mode 100644 index 0000000..06ca595 --- /dev/null +++ b/moons/orange.png | |||
Binary files differ | |||
diff --git a/moons/pink.png b/moons/pink.png new file mode 100644 index 0000000..3b1521b --- /dev/null +++ b/moons/pink.png | |||
Binary files differ | |||
diff --git a/moons/purple.png b/moons/purple.png new file mode 100644 index 0000000..9eeacf9 --- /dev/null +++ b/moons/purple.png | |||
Binary files differ | |||
diff --git a/moons/red.png b/moons/red.png new file mode 100644 index 0000000..2c0ef77 --- /dev/null +++ b/moons/red.png | |||
Binary files differ | |||
diff --git a/moons/star.png b/moons/star.png new file mode 100644 index 0000000..29263d5 --- /dev/null +++ b/moons/star.png | |||
Binary files differ | |||
diff --git a/moons/white.png b/moons/white.png new file mode 100644 index 0000000..fae666b --- /dev/null +++ b/moons/white.png | |||
Binary files differ | |||
diff --git a/moons/yellow.png b/moons/yellow.png new file mode 100644 index 0000000..8c012df --- /dev/null +++ b/moons/yellow.png | |||
Binary files differ | |||
diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..61fdc45 --- /dev/null +++ b/schema.sql | |||
@@ -0,0 +1,32 @@ | |||
1 | CREATE TABLE `profiles` ( | ||
2 | `profile_id` INTEGER PRIMARY KEY, | ||
3 | `profile_path` VARCHAR(255) NOT NULL | ||
4 | ); | ||
5 | |||
6 | CREATE UNIQUE INDEX `profile_by_path` ON `profiles`(`profile_path`); | ||
7 | |||
8 | CREATE TABLE `games` ( | ||
9 | `game_id` INTEGER PRIMARY KEY, | ||
10 | `steam_appid` INTEGER NOT NULL, | ||
11 | `moon_image` VARCHAR(255) NOT NULL | ||
12 | ); | ||
13 | |||
14 | CREATE UNIQUE INDEX `game_by_appid` ON `games`(`steam_appid`); | ||
15 | |||
16 | CREATE TABLE `achievements` ( | ||
17 | `achievement_id` INTEGER PRIMARY KEY, | ||
18 | `game_id` INTEGER NOT NULL, | ||
19 | `title` VARCHAR(255) NOT NULL | ||
20 | ); | ||
21 | |||
22 | CREATE TABLE `dids` ( | ||
23 | `profile_id` INTEGER NOT NULL, | ||
24 | `achievement_id` INTEGER NOT NULL, | ||
25 | `achieved_at` DATETIME NOT NULL | ||
26 | ); | ||
27 | |||
28 | CREATE TABLE `images` ( | ||
29 | `image_id` INTEGER PRIMARY KEY, | ||
30 | `game_id` INTEGER NOT NULL, | ||
31 | `filename` VARCHAR(255) NOT NULL | ||
32 | ); | ||
diff --git a/scrape.rb b/scrape.rb index a28f4c5..6f3a8e4 100644 --- a/scrape.rb +++ b/scrape.rb | |||
@@ -1,42 +1,175 @@ | |||
1 | require 'json' | 1 | require 'json' |
2 | require 'nokogiri' | ||
3 | require 'open-uri' | 2 | require 'open-uri' |
4 | require 'yaml' | 3 | require 'yaml' |
5 | 4 | ||
6 | config = YAML.load(open(ARGV[0])) | 5 | require 'rubygems' |
7 | usernames = config["usernames"] | 6 | require 'bundler/setup' |
7 | Bundler.require :default | ||
8 | 8 | ||
9 | achieves = usernames.map do |username| | 9 | @config = YAML.load(open(ARGV[0])) |
10 | page = Nokogiri::HTML(open("https://steamcommunity.com/#{username}/games/?tab=all")) | 10 | db_existed = File.exists?(@config["database"]) |
11 | db = Sequel.connect("sqlite://" + @config["database"]) | ||
12 | |||
13 | if ARGV[1] == "init" | ||
14 | if db_existed | ||
15 | raise "Datafile already exists" | ||
16 | end | ||
17 | |||
18 | schema = File.read("schema.sql") | ||
19 | |||
20 | db.run schema | ||
21 | |||
22 | puts "Initialized datafile" | ||
23 | |||
24 | exit | ||
25 | end | ||
26 | |||
27 | class Profile < Sequel::Model | ||
28 | many_to_many :achievements, join_table: :dids | ||
29 | end | ||
30 | |||
31 | class Game < Sequel::Model | ||
32 | one_to_many :achievements | ||
33 | one_to_many :images | ||
34 | end | ||
35 | |||
36 | class Achievement < Sequel::Model | ||
37 | many_to_one :game | ||
38 | many_to_many :profiles, join_table: :dids | ||
39 | end | ||
40 | |||
41 | class Image < Sequel::Model | ||
42 | many_to_one :game | ||
43 | end | ||
44 | |||
45 | class Did < Sequel::Model | ||
46 | many_to_one :profile | ||
47 | many_to_one :achievement | ||
48 | end | ||
49 | |||
50 | @moonimgs = Dir.entries(@config["moon_images"]).select do |img| | ||
51 | img.end_with? ".png" | ||
52 | end | ||
53 | |||
54 | def scrape_profile(profile, full) | ||
55 | if full | ||
56 | url = "https://steamcommunity.com/#{profile.profile_path}/games/?tab=all" | ||
57 | else | ||
58 | url = "https://steamcommunity.com/#{profile.profile_path}/games/" | ||
59 | end | ||
60 | |||
61 | page = Nokogiri::HTML(open(url)) | ||
11 | script = page.css(".responsive_page_template_content script").text[18..-1] | 62 | script = page.css(".responsive_page_template_content script").text[18..-1] |
12 | data = JSON.parse(script[0..script.index(";\r\n\t\t")-1]) | 63 | data = JSON.parse(script[0..script.index(";\r\n\t\t")-1]) |
13 | ids = data.map { |d| d["appid"] } | 64 | ids = data.map { |d| d["appid"] } |
14 | 65 | ||
15 | index = 0 | 66 | index = 0 |
16 | ids.map do |id| | 67 | ids.each do |id| |
17 | index += 1 | 68 | index += 1 |
18 | puts "#{username} - #{index}/#{ids.count}" | 69 | puts "#{profile.profile_path} - #{index}/#{ids.count}" |
19 | 70 | ||
20 | achsp = Nokogiri::HTML(open("https://steamcommunity.com/#{username}/stats/#{id}/")) | 71 | achsp = Nokogiri::HTML( |
21 | achsp.css(".achieveTxt .achieveUnlockTime + h3").map { |d| d.text } | 72 | open("https://steamcommunity.com/#{profile.profile_path}/stats/#{id}/")) |
22 | end | ||
23 | end.flatten | ||
24 | 73 | ||
25 | if File.exists?(config["achievements"]) | 74 | achsp.css(".achieveTxt").each do |node| |
26 | already = File.read(config["achievements"]).split("\n") | 75 | unless node.css(".achieveUnlockTime").empty? |
27 | all_achieves = achieves + already | 76 | if Game.where(steam_appid: id).count > 0 |
28 | else | 77 | game = Game.where(steam_appid: id).first |
29 | all_achieves = achieves | 78 | else |
30 | end | 79 | moon_index = Random.rand(@moonimgs.size) |
80 | |||
81 | game = Game.new(steam_appid: id, moon_image: @moonimgs[moon_index]) | ||
82 | game.save | ||
83 | |||
84 | storepage = Nokogiri::HTML( | ||
85 | open("http://store.steampowered.com/app/#{id}")) | ||
86 | |||
87 | img_id = 0 | ||
88 | storepage.css(".highlight_screenshot_link").each do |node| | ||
89 | begin | ||
90 | imagepage = open(node["href"]).read | ||
91 | |||
92 | img_id += 1 | ||
93 | img_filename = "#{id}-#{img_id}.jpg" | ||
94 | img_filepath = File.join(@config["images"], img_filename) | ||
95 | |||
96 | img_file = File.open(img_filepath, "w") | ||
97 | img_file.write(imagepage) | ||
98 | img_file.close | ||
99 | |||
100 | image = Image.new(game: game, filename: img_filename) | ||
101 | image.save | ||
102 | rescue OpenURI::HTTPError | ||
103 | puts "Error downloading an image" | ||
104 | end | ||
105 | |||
106 | sleep 2 | ||
107 | end | ||
108 | end | ||
109 | |||
110 | title = node.at_css("h3").text | ||
31 | 111 | ||
32 | all_achieves.sort! | 112 | if game.achievements_dataset.where(title: title).count > 0 |
33 | all_achieves.uniq! | 113 | achievement = game.achievements_dataset.where(title: title).first |
114 | else | ||
115 | achievement = Achievement.new(game: game, title: title) | ||
116 | achievement.save | ||
117 | end | ||
34 | 118 | ||
35 | if config.key? "blacklist" | 119 | unless Did.where(profile: profile, achievement: achievement).count > 0 |
36 | blacklist = File.read(config["blacklist"]).split("\n") | 120 | begin |
37 | all_achieves.reject! { |l| blacklist.include? l } | 121 | unlock = DateTime.strptime( |
122 | node.css(".achieveUnlockTime").text.lstrip[9..-1], | ||
123 | "%b %d, %Y @ %l:%M%P") | ||
124 | rescue ArgumentError | ||
125 | unlock = DateTime.strptime( | ||
126 | node.css(".achieveUnlockTime").text.lstrip[9..-1], | ||
127 | "%b %d @ %l:%M%P") | ||
128 | end | ||
129 | |||
130 | join = Did.new( | ||
131 | profile: profile, | ||
132 | achievement: achievement, | ||
133 | achieved_at: unlock) | ||
134 | join.save | ||
135 | end | ||
136 | end | ||
137 | end | ||
138 | end | ||
38 | end | 139 | end |
39 | 140 | ||
40 | File.open(config["achievements"], "w") do |f| | 141 | if ARGV[1] == "add" |
41 | f << all_achieves.join("\n") | 142 | userpath = ARGV[2] |
143 | |||
144 | if Profile.where(profile_path: userpath).count > 0 | ||
145 | raise "Profile " + userpath + " already exists" | ||
146 | end | ||
147 | |||
148 | profile = Profile.new(profile_path: userpath) | ||
149 | profile.save | ||
150 | |||
151 | scrape_profile profile, true | ||
152 | elsif ARGV[1] == "update" | ||
153 | if ARGV.size == 3 | ||
154 | scrape_profile Profile.where(profile_path: ARGV[2]).first, false | ||
155 | else | ||
156 | Profile.all.each do |profile| | ||
157 | scrape_profile profile, false | ||
158 | end | ||
159 | end | ||
160 | elsif ARGV[1] == "full" | ||
161 | if ARGV.size == 3 | ||
162 | scrape_profile Profile.where(profile_path: ARGV[2]).first, true | ||
163 | else | ||
164 | Profile.all.each do |profile| | ||
165 | scrape_profile profile, true | ||
166 | end | ||
167 | end | ||
168 | elsif ARGV[1] == "recolor" | ||
169 | Game.all.each do |game| | ||
170 | moon_index = Random.rand(@moonimgs.size) | ||
171 | |||
172 | game.moon_image = @moonimgs[moon_index] | ||
173 | game.save | ||
174 | end | ||
42 | end | 175 | end |