diff options
| -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 |
