morris 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fc2c10dc14e7ef184b9c23951c989fab9260bc520a438c460bde6c408c7f0473
4
+ data.tar.gz: 144c9c0696b2dab10e33f46cc5d3205883bb33d16322ffd29c1a14d2a4e75eb2
5
+ SHA512:
6
+ metadata.gz: 011a2eafbd15b9dc41d67a888c1af07ac2275642c44ee897c921a31c763ea30fc48443ded27209a4161417e1da8ecad3d536d7a1466324e04af29adc345f7ae4
7
+ data.tar.gz: 032b3c67f3d4205da554a90dfe99aa4001bb36bfcde1d698114a4eb6cd630cb4c3373421bd7638c06589e135856d2174cd3b6363dc6feaf300e30938869d5a23
data/.byebug_history ADDED
@@ -0,0 +1,85 @@
1
+ exit
2
+ content_element["createTime"]
3
+ c
4
+ username
5
+ c
6
+ up
7
+ n
8
+ username
9
+ quit
10
+ Time.at(content_element["createTime"].to_i)
11
+ content_element["createTime"]
12
+ exit
13
+ pp json["__DEFAULT_SCOPE__"].keys
14
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["author"]
15
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"].keys
16
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["video"]["cover"]
17
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["video"].keys
18
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["video"]
19
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["digged"]
20
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["textExtra"]
21
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["originalItem"]
22
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["stats"]["diggCount"]
23
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["stats"]
24
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["challenges"
25
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["id"]
26
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["desc"]
27
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]["contents"]
28
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"].keys
29
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"].keys
30
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]
31
+ pp json["__DEFAULT_SCOPE__"]["webapp.video-detail"].keys
32
+ pp json["__DEFAULT_SCOPE__"].keys
33
+ pp json
34
+ text
35
+ json
36
+ json["__DEFAULT_SCOPE__"]
37
+ json["__DEFAULT_SCOPE"]
38
+ json.keys
39
+ json
40
+ c
41
+ quit
42
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).first.text(:all)
43
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).first.innerText
44
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).first.text
45
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).first
46
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).text
47
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).innerText
48
+ page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false)
49
+ page.all(:xpath, "*[@id='__UNIVERSAL_DATA_FOR_REHYDRATION__']", visible: false)
50
+ c
51
+ page.all(:xpath, "*[@id='__UNIVERSAL_DATA_FOR_REHYDRATION__']", visible: false)
52
+ page.all(:xpath, "script[@id='__UNIVERSAL_DATA_FOR_REHYDRATION__']", visible: false)
53
+ page.find(:xpath, "script[@id='__UNIVERSAL_DATA_FOR_REHYDRATION__']", visible: false)
54
+ page.find(:xpath, "script[@id='__UNIVERSAL_DATA_FOR_REHYDRATION__']")
55
+ exit
56
+ File.size(result.video_file) > 500000
57
+ File.size(result.video_file)
58
+ File.exist?(result.video_file)
59
+ result.video_file
60
+ result[:path]
61
+ result
62
+ exit
63
+ RUBY_ENV
64
+ video_hash
65
+ up
66
+ exit
67
+ video_hash.transform_keys(&:to_s)
68
+ video_hash.strinify_keys
69
+ video_hash
70
+ up
71
+ exit
72
+ video_hash
73
+ @id
74
+ @iid
75
+ up
76
+ @id
77
+ line
78
+ c
79
+ @id
80
+ e
81
+ c
82
+ e
83
+ exit
84
+ e
85
+ c
data/.rubocop.yml ADDED
@@ -0,0 +1,57 @@
1
+ require:
2
+ - rubocop-rails
3
+ - rubocop-performance
4
+
5
+ inherit_gem:
6
+ rubocop-rails_config:
7
+ - config/rails.yml
8
+
9
+ AllCops:
10
+ Exclude:
11
+ - db/schema.rb
12
+ - 'node_modules/**/*'
13
+ - 'redis-stable/**/*'
14
+ - 'bin/**/*'
15
+ - 'vendor/**/*'
16
+ TargetRubyVersion: 3.2
17
+
18
+ # This sets us to use the standard Rails format instead of Rubocop's
19
+ # opinionated Ruby style.
20
+ Style/FrozenStringLiteralComment:
21
+ Enabled: false
22
+
23
+ # This sets us to use the standard Rails format instead of Rubocop's
24
+ # opinionated Ruby style.
25
+ Style/ClassAndModuleChildren:
26
+ Enabled: false
27
+
28
+ # Temporarily turn this off
29
+ Metrics/AbcSize:
30
+ Enabled: false
31
+
32
+ Metrics/ClassLength:
33
+ Enabled: false
34
+
35
+ Lint/RescueException:
36
+ Enabled: true
37
+
38
+ Lint/Debugger:
39
+ Enabled: true
40
+
41
+ Rails/HasManyOrHasOneDependent:
42
+ Enabled: true
43
+
44
+ Rails/HasAndBelongsToMany:
45
+ Enabled: true
46
+
47
+ Style/NumericPredicate:
48
+ Enabled: true
49
+
50
+ Style/HashSyntax:
51
+ EnforcedShorthandSyntax: 'never'
52
+
53
+ # This sets us to use the standard Rails format instead of Rubocop's
54
+ # opinionated Ruby style.
55
+ Layout/EmptyLinesAroundAccessModifier:
56
+ Enabled: true
57
+ EnforcedStyle: 'around'
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-02-19
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at cguess@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in morris.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "minitest", "~> 5.0"
10
+
11
+ gem "rubocop", "~> 1.27", require: false
12
+ gem "rubocop-rails", require: false # Rails specific styles
13
+ gem "rubocop-rails_config", require: false # More Rails stuff
14
+
15
+ gem "dotenv", "~> 2.7.6"
16
+
17
+ gem "rack"
18
+
19
+ gem "curb"
data/Gemfile.lock ADDED
@@ -0,0 +1,168 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ morris (0.1.0)
5
+ apparition
6
+ capybara
7
+ oj
8
+ selenium-devtools
9
+ selenium-webdriver
10
+ terrapin
11
+ typhoeus
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (7.1.3)
17
+ base64
18
+ bigdecimal
19
+ concurrent-ruby (~> 1.0, >= 1.0.2)
20
+ connection_pool (>= 2.2.5)
21
+ drb
22
+ i18n (>= 1.6, < 2)
23
+ minitest (>= 5.1)
24
+ mutex_m
25
+ tzinfo (~> 2.0)
26
+ addressable (2.8.6)
27
+ public_suffix (>= 2.0.2, < 6.0)
28
+ apparition (0.6.0)
29
+ capybara (~> 3.13, < 4)
30
+ websocket-driver (>= 0.6.5)
31
+ ast (2.4.2)
32
+ base64 (0.2.0)
33
+ bigdecimal (3.1.6)
34
+ capybara (3.40.0)
35
+ addressable
36
+ matrix
37
+ mini_mime (>= 0.1.3)
38
+ nokogiri (~> 1.11)
39
+ rack (>= 1.6.0)
40
+ rack-test (>= 0.6.3)
41
+ regexp_parser (>= 1.5, < 3.0)
42
+ xpath (~> 3.2)
43
+ climate_control (1.2.0)
44
+ concurrent-ruby (1.2.3)
45
+ connection_pool (2.4.1)
46
+ curb (1.0.5)
47
+ debug (1.9.1)
48
+ irb (~> 1.10)
49
+ reline (>= 0.3.8)
50
+ dotenv (2.7.6)
51
+ drb (2.2.0)
52
+ ruby2_keywords
53
+ ethon (0.16.0)
54
+ ffi (>= 1.15.0)
55
+ ffi (1.16.3)
56
+ i18n (1.14.1)
57
+ concurrent-ruby (~> 1.0)
58
+ io-console (0.7.2)
59
+ irb (1.11.2)
60
+ rdoc
61
+ reline (>= 0.4.2)
62
+ json (2.7.1)
63
+ language_server-protocol (3.17.0.3)
64
+ matrix (0.4.2)
65
+ mini_mime (1.1.5)
66
+ minitest (5.22.2)
67
+ mutex_m (0.2.0)
68
+ nokogiri (1.16.2-arm64-darwin)
69
+ racc (~> 1.4)
70
+ oj (3.16.3)
71
+ bigdecimal (>= 3.0)
72
+ parallel (1.24.0)
73
+ parser (3.3.0.5)
74
+ ast (~> 2.4.1)
75
+ racc
76
+ psych (5.1.2)
77
+ stringio
78
+ public_suffix (5.0.4)
79
+ racc (1.7.3)
80
+ rack (3.0.9)
81
+ rack-test (2.1.0)
82
+ rack (>= 1.3)
83
+ rainbow (3.1.1)
84
+ rake (13.1.0)
85
+ rdoc (6.6.2)
86
+ psych (>= 4.0.0)
87
+ regexp_parser (2.9.0)
88
+ reline (0.4.2)
89
+ io-console (~> 0.5)
90
+ rexml (3.2.6)
91
+ rubocop (1.60.2)
92
+ json (~> 2.3)
93
+ language_server-protocol (>= 3.17.0)
94
+ parallel (~> 1.10)
95
+ parser (>= 3.3.0.2)
96
+ rainbow (>= 2.2.2, < 4.0)
97
+ regexp_parser (>= 1.8, < 3.0)
98
+ rexml (>= 3.2.5, < 4.0)
99
+ rubocop-ast (>= 1.30.0, < 2.0)
100
+ ruby-progressbar (~> 1.7)
101
+ unicode-display_width (>= 2.4.0, < 3.0)
102
+ rubocop-ast (1.30.0)
103
+ parser (>= 3.2.1.0)
104
+ rubocop-md (1.2.2)
105
+ rubocop (>= 1.0)
106
+ rubocop-minitest (0.34.5)
107
+ rubocop (>= 1.39, < 2.0)
108
+ rubocop-ast (>= 1.30.0, < 2.0)
109
+ rubocop-packaging (0.5.2)
110
+ rubocop (>= 1.33, < 2.0)
111
+ rubocop-performance (1.20.2)
112
+ rubocop (>= 1.48.1, < 2.0)
113
+ rubocop-ast (>= 1.30.0, < 2.0)
114
+ rubocop-rails (2.23.1)
115
+ activesupport (>= 4.2.0)
116
+ rack (>= 1.1)
117
+ rubocop (>= 1.33.0, < 2.0)
118
+ rubocop-ast (>= 1.30.0, < 2.0)
119
+ rubocop-rails_config (1.16.0)
120
+ rubocop (>= 1.57.0)
121
+ rubocop-ast (>= 1.26.0)
122
+ rubocop-md
123
+ rubocop-minitest (~> 0.22)
124
+ rubocop-packaging (~> 0.5)
125
+ rubocop-performance (~> 1.11)
126
+ rubocop-rails (~> 2.0)
127
+ ruby-progressbar (1.13.0)
128
+ ruby2_keywords (0.0.5)
129
+ rubyzip (2.3.2)
130
+ selenium-devtools (0.121.0)
131
+ selenium-webdriver (~> 4.2)
132
+ selenium-webdriver (4.17.0)
133
+ base64 (~> 0.2)
134
+ rexml (~> 3.2, >= 3.2.5)
135
+ rubyzip (>= 1.2.2, < 3.0)
136
+ websocket (~> 1.0)
137
+ stringio (3.1.0)
138
+ terrapin (1.0.1)
139
+ climate_control
140
+ typhoeus (1.4.1)
141
+ ethon (>= 0.9.0)
142
+ tzinfo (2.0.6)
143
+ concurrent-ruby (~> 1.0)
144
+ unicode-display_width (2.5.0)
145
+ websocket (1.2.10)
146
+ websocket-driver (0.7.6)
147
+ websocket-extensions (>= 0.1.0)
148
+ websocket-extensions (0.1.5)
149
+ xpath (3.2.0)
150
+ nokogiri (~> 1.8)
151
+
152
+ PLATFORMS
153
+ arm64-darwin-22
154
+
155
+ DEPENDENCIES
156
+ curb
157
+ debug
158
+ dotenv (~> 2.7.6)
159
+ minitest (~> 5.0)
160
+ morris!
161
+ rack
162
+ rake (~> 13.0)
163
+ rubocop (~> 1.27)
164
+ rubocop-rails
165
+ rubocop-rails_config
166
+
167
+ BUNDLED WITH
168
+ 2.4.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Christopher Guess
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.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Morris
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/morris`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/morris. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/morris/blob/master/CODE_OF_CONDUCT.md).
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
36
+
37
+ ## Code of Conduct
38
+
39
+ Everyone interacting in the Morris project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/morris/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Borrowed with thanks from https://www.viget.com/articles/easy-gem-configuration-variables-with-defaults/
4
+ module Configuration
5
+ def configuration
6
+ yield self
7
+ end
8
+
9
+ def define_setting(name, default = nil)
10
+ class_variable_set("@@#{name}", default)
11
+
12
+ define_class_method "#{name}=" do |value|
13
+ class_variable_set("@@#{name}", value)
14
+ end
15
+
16
+ define_class_method name do
17
+ class_variable_get("@@#{name}")
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def define_class_method(name, &block)
24
+ (class << self; self; end).instance_eval do
25
+ define_method name, &block
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,52 @@
1
+ require "logger"
2
+ require "selenium-webdriver"
3
+
4
+ # Design taken from https://blog.appsignal.com/2021/08/24/responsible-monkeypatching-in-ruby.html
5
+
6
+ module SeleniumMonkeypatch
7
+ class << self
8
+ @@logger = Logger.new(STDOUT)
9
+ @@logger.level = Logger::INFO
10
+
11
+ def apply_patch
12
+ target_class = find_class
13
+ target_method = find_method(target_class)
14
+
15
+ unless target_method
16
+ raise "Could not find class or method when patching Selenium::WebDriver::DevTools.send_cmd"
17
+ end
18
+
19
+ @@logger.info "#{__FILE__} is monkeypatching Selenium::WebDriver::DevTools.send_cmd"
20
+ target_class.prepend(InstanceMethods)
21
+ end
22
+
23
+ private
24
+
25
+ def find_class
26
+ Kernel.const_get("Selenium::WebDriver::DevTools")
27
+ rescue NameError
28
+ end
29
+
30
+ def find_method(class_)
31
+ return unless class_
32
+ class_.instance_method(:send_cmd)
33
+ rescue NameError
34
+ end
35
+ end
36
+
37
+ module InstanceMethods
38
+ # We're monkeypatching the following method so that Selenium doesn't raise errors when we fail to call `continue` on requests
39
+ def send_cmd(method, **params)
40
+ data = { method: method, params: params.compact }
41
+ data[:sessionId] = @session_id if @session_id
42
+ message = @ws.send_cmd(**data)
43
+ if message.nil? == false && message["error"] && (method != "Fetch.continueRequest")
44
+ raise Error::WebDriverError, error_message(message["error"])
45
+ end
46
+
47
+ message
48
+ end
49
+ end
50
+ end
51
+
52
+ SeleniumMonkeypatch.apply_patch
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Morris
4
+ class Post
5
+ def self.lookup(urls = [])
6
+ # If a single id is passed in we make it the appropriate array
7
+ urls = [urls] unless urls.kind_of?(Array)
8
+ self.scrape(urls)
9
+ end
10
+
11
+ attr_reader :id,
12
+ :text,
13
+ :date,
14
+ :number_of_likes,
15
+ :user,
16
+ :video_file_name,
17
+ :video_preview_image,
18
+ :screenshot_file
19
+
20
+ private
21
+
22
+ def initialize(post_hash = {})
23
+ @id = post_hash[:id]
24
+ @text = post_hash[:text]
25
+ @date = post_hash[:date]
26
+ @number_of_likes = post_hash[:number_of_likes]
27
+ @user = post_hash[:user]
28
+ @video_file_name = post_hash[:video]
29
+ @video_preview_image = post_hash[:video_preview_image]
30
+ @screenshot_file = post_hash[:screenshot_file]
31
+ end
32
+
33
+ class << self
34
+ private
35
+
36
+ def scrape(urls)
37
+ urls.map do |url|
38
+ post_hash = Morris::PostScraper.new.parse(url)
39
+ Post.new(post_hash)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "typhoeus"
4
+
5
+ module Morris
6
+ class PostScraper < Scraper
7
+ def parse(url)
8
+ # Stuff we need to get from the DOM (implemented is starred):
9
+ # - User *
10
+ # - Text *
11
+ # - Image * / Images * / Video *
12
+ # - Date *
13
+ # - Number of likes *
14
+ # - Hashtags
15
+
16
+ Capybara.app_host = "https://tiktok.com"
17
+
18
+ # Get the page
19
+ visit(url)
20
+
21
+ # Grab the JSON
22
+ element = page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).first
23
+ text = element.text(:all) # Gotta get the hiddent text of the element
24
+ json = JSON.parse(text)
25
+
26
+ content_element = json["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]
27
+
28
+ id = content_element["id"]
29
+ text = content_element["desc"]
30
+ number_of_likes = content_element["stats"]["diggCount"]
31
+ date = Time.at(content_element["createTime"].to_i)
32
+ video_preview_image = Morris.retrieve_media(content_element["video"]["cover"])
33
+ video = Morris::VideoScraper.lookup(url)
34
+ username = content_element["author"]["uniqueId"]
35
+
36
+ screenshot_file = take_screenshot()
37
+
38
+ # This has to run last since it switches pages
39
+ user = Morris::UserScraper.new.lookup(username)
40
+
41
+ {
42
+ video: video,
43
+ video_preview_image: video_preview_image,
44
+ screenshot_file: screenshot_file,
45
+ text: text,
46
+ date: date,
47
+ number_of_likes: number_of_likes,
48
+ user: user,
49
+ id: id
50
+ }
51
+ end
52
+
53
+ def take_screenshot
54
+ # First check if a post has a fact check overlay, if so, clear it.
55
+ # The only issue is that this can take *awhile* to search. Not sure what to do about that
56
+ # since it's Instagram's fault for having such a fucked up obfuscated hierarchy
57
+ begin
58
+ find_button("See Post").click
59
+ sleep(0.1)
60
+ rescue Capybara::ElementNotFound
61
+ # Do nothing if the element is not found
62
+ end
63
+
64
+ # Take the screenshot and return it
65
+ save_screenshot("#{Morris.temp_storage_location}/instagram_screenshot_#{SecureRandom.uuid}.png")
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capybara/dsl"
4
+ require "dotenv/load"
5
+ require "oj"
6
+ require "selenium-webdriver"
7
+ require "logger"
8
+ require "securerandom"
9
+ require "selenium/webdriver/remote/http/curb"
10
+ require "debug"
11
+
12
+ # 2022-06-07 14:15:23 WARN Selenium [DEPRECATION] [:browser_options] :options as a parameter for driver initialization is deprecated. Use :capabilities with an Array of value capabilities/options if necessary instead.
13
+
14
+ options = Selenium::WebDriver::Options.chrome(exclude_switches: ["enable-automation"])
15
+ options.add_argument("--start-maximized")
16
+ options.add_argument("--no-sandbox")
17
+ options.add_argument("--disable-dev-shm-usage")
18
+ options.add_argument("–-disable-blink-features=AutomationControlled")
19
+ options.add_argument("--disable-extensions")
20
+ options.add_argument("--enable-features=NetworkService,NetworkServiceInProcess")
21
+ options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
22
+ options.add_preference "password_manager_enabled", false
23
+ options.add_argument("--user-data-dir=/tmp/tarun_morris_#{SecureRandom.uuid}")
24
+
25
+ Capybara.register_driver :selenium_morris do |app|
26
+ client = Selenium::WebDriver::Remote::Http::Curb.new
27
+ # client.read_timeout = 60 # Don't wait 60 seconds to return Net::ReadTimeoutError. We'll retry through Hypatia after 10 seconds
28
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: options, http_client: client)
29
+ end
30
+
31
+ Capybara.threadsafe = true
32
+ Capybara.default_max_wait_time = 60
33
+ Capybara.reuse_server = true
34
+
35
+ module Morris
36
+ class Scraper # rubocop:disable Metrics/ClassLength
37
+ include Capybara::DSL
38
+
39
+ @@logger = Logger.new(STDOUT)
40
+ @@logger.level = Logger::WARN
41
+ @@logger.datetime_format = "%Y-%m-%d %H:%M:%S"
42
+ @@session_id = nil
43
+
44
+ def initialize
45
+ Capybara.default_driver = :selenium_morris
46
+ end
47
+
48
+ private
49
+
50
+ ##########
51
+ # Set the session to use a new user folder in the options!
52
+ # #####################
53
+ def reset_selenium
54
+ options = Selenium::WebDriver::Options.chrome(exclude_switches: ["enable-automation"])
55
+ options.add_argument("--start-maximized")
56
+ options.add_argument("--no-sandbox")
57
+ options.add_argument("--disable-dev-shm-usage")
58
+ options.add_argument("–-disable-blink-features=AutomationControlled")
59
+ options.add_argument("--disable-extensions")
60
+ options.add_argument("--enable-features=NetworkService,NetworkServiceInProcess")
61
+
62
+ options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
63
+ options.add_preference "password_manager_enabled", false
64
+ options.add_argument("--user-data-dir=/tmp/tarun_morris_#{SecureRandom.uuid}")
65
+ # options.add_argument("--user-data-dir=/tmp/tarun")
66
+
67
+ Capybara.register_driver :selenium do |app|
68
+ client = Selenium::WebDriver::Remote::Http::Curb.new
69
+ # client.read_timeout = 60 # Don't wait 60 seconds to return Net::ReadTimeoutError. We'll retry through Hypatia after 10 seconds
70
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: options, http_client: client)
71
+ end
72
+
73
+ Capybara.current_driver = :selenium
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "typhoeus"
4
+
5
+ module Morris
6
+ class UserScraper < Scraper
7
+ def lookup(username)
8
+ Capybara.app_host = "https://tiktok.com"
9
+ url = "https://www.tiktok.com/@#{username}"
10
+
11
+ # Get the page
12
+ visit(url)
13
+
14
+ # Grab the JSON
15
+ element = page.all(:xpath, '//*[@id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]', visible: false).first
16
+ text = element.text(:all) # Gotta get the hiddent text of the element
17
+ json = JSON.parse(text)
18
+
19
+ content_element = json["__DEFAULT_SCOPE__"]["webapp.user-detail"]["userInfo"]
20
+
21
+ name = content_element["user"]["nickname"]
22
+ username = content_element["user"]["uniqueId"]
23
+ number_of_posts = content_element["stats"]["videoCount"]
24
+ number_of_followers = content_element["stats"]["followerCount"]
25
+ number_of_following = content_element["stats"]["followingCount"]
26
+ verified = content_element["user"]["verified"]
27
+ profile = content_element["user"]["signature"]
28
+ profile_link = url
29
+ profile_image = Morris.retrieve_media(content_element["user"]["avatarLarger"])
30
+ profile_image_url = content_element["user"]["avatarLarger"]
31
+
32
+ {
33
+ name: name,
34
+ username: username,
35
+ number_of_posts: number_of_posts,
36
+ number_of_followers: number_of_followers,
37
+ number_of_following: number_of_following,
38
+ verified: verified,
39
+ profile: profile,
40
+ profile_link: profile_link,
41
+ profile_image: profile_image,
42
+ profile_image_url: profile_image_url
43
+ }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "logger"
5
+ require "byebug"
6
+ require "terrapin"
7
+
8
+ module Morris
9
+ class VideoScraper
10
+
11
+ class VideoDownloadError < StandardError
12
+ def initialize(msg = "Morris encountered an error scraping TikTok")
13
+ super
14
+ end
15
+ end
16
+
17
+ @@morris_logger = Logger.new(STDOUT)
18
+ @@morris_logger.level = Logger::INFO
19
+ @@morris_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
20
+
21
+ def self.lookup(url)
22
+ @@morris_logger.debug("Morris started downloading video with id: #{@id}")
23
+
24
+ start_time = Time.now
25
+ filename = "#{Morris.temp_storage_location}/morris_media_#{SecureRandom.uuid}.mp4"
26
+ line = Terrapin::CommandLine.new("yt-dlp", "-f :filetype -o :filename :url")
27
+
28
+ line.run(filename: filename,
29
+ filetype: "mp4",
30
+ url: url)
31
+
32
+ @@morris_logger.debug("YoutubeArchiver finished downloading video with id: #{@id}")
33
+ @@morris_logger.debug("Save location: #{filename}")
34
+ @@morris_logger.debug("Time to download: #{(Time.now - start_time).round(3)} seconds")
35
+
36
+ filename
37
+ rescue Terrapin::ExitStatusError => e # yt-dlp command returns a non-zero exit status
38
+ raise VideoDownloadError.new(e.message) # Retryable error
39
+ end
40
+
41
+ # def self.retrieve_data(ids)
42
+ # api_key = ENV["YOUTUBE_API_KEY"]
43
+ # youtube_base_url = "https://youtube.googleapis.com/youtube/v3/videos/"
44
+ # params = {
45
+ # "part": "contentDetails,snippet,statistics,status",
46
+ # "id": ids.join(","),
47
+ # "key": api_key
48
+ # }
49
+
50
+ # response = video_lookup(youtube_base_url, params)
51
+
52
+ # response
53
+ # end
54
+
55
+ # def self.video_lookup(url, params)
56
+ # options = {
57
+ # method: "get",
58
+ # params:
59
+ # }
60
+
61
+ # request = Typhoeus::Request.new(url, options)
62
+ # response = request.run
63
+
64
+ # raise YoutubeArchiver::YoutubeApiError, "Invalid response code #{response.code}" if response.code > 500 # Retryable (downstream) error
65
+ # raise YoutubeArchiver::AuthorizationError, "Invalid response code #{response.code}" if response.code > 400
66
+
67
+ # response
68
+ # end
69
+
70
+ # # Convert a YouTube duration string to number of seconds
71
+ # # A duration string of "PT0H4M32S" signifies a length of 4 minutes and 32 seconds
72
+ # def self.convert_video_length_to_seconds(duration_string)
73
+ # if /PT((\d+)H)?((\d+)M)?((\d+)S)?/ =~ duration_string # Use regex to capture num_hours, num_minutes, num_seconds
74
+ # $2.to_i * 3600 + $4.to_i * 60 + $6.to_i # To convert to seconds, sum(num_hours*3600, num_minutes*60, num_seconds*1)
75
+ # else
76
+ # 0
77
+ # end
78
+ # end
79
+ end
80
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Morris
4
+ class User
5
+ def self.lookup(usernames = [])
6
+ # If a single id is passed in we make it the appropriate array
7
+ usernames = [usernames] unless usernames.kind_of?(Array)
8
+
9
+ # Check that the usernames are at least real usernames
10
+ # usernames.each { |id| raise Birdsong::Error if !/\A\d+\z/.match(id) }
11
+
12
+ self.scrape(usernames)
13
+ end
14
+
15
+ attr_reader :name,
16
+ :username,
17
+ :number_of_posts,
18
+ :number_of_followers,
19
+ :number_of_following,
20
+ :verified,
21
+ :profile,
22
+ :profile_link,
23
+ :profile_image,
24
+ :profile_image_url
25
+
26
+ private
27
+
28
+ def initialize(user_hash = {})
29
+ @name = user_hash[:name]
30
+ @username = user_hash[:username]
31
+ @number_of_posts = user_hash[:number_of_posts]
32
+ @number_of_followers = user_hash[:number_of_followers]
33
+ @number_of_following = user_hash[:number_of_following]
34
+ @verified = user_hash[:verified]
35
+ @profile = user_hash[:profile]
36
+ @profile_link = user_hash[:profile_link]
37
+ @profile_image = user_hash[:profile_image]
38
+ @profile_image_url = user_hash[:profile_image_url]
39
+ end
40
+
41
+ class << self
42
+ private
43
+
44
+ def scrape(usernames)
45
+ usernames.map do |username|
46
+ user_hash = Morris::UserScraper.new.parse(username)
47
+ User.new(user_hash)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Morris
4
+ VERSION = "0.1.0"
5
+ end
data/lib/morris.rb ADDED
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "morris/version"
4
+ require "helpers/configuration"
5
+
6
+ # Representative objects we create
7
+ require_relative "morris/user"
8
+ require_relative "morris/post"
9
+
10
+ require_relative "morris/scrapers/scraper"
11
+ require_relative "morris/scrapers/video_scraper"
12
+ require_relative "morris/scrapers/post_scraper"
13
+ require_relative "morris/scrapers/user_scraper"
14
+
15
+ module Morris
16
+ extend Configuration
17
+
18
+ class Error < StandardError
19
+ def initialize(msg = "Morris encountered an error scraping TikTok")
20
+ super
21
+ end
22
+ end
23
+
24
+ class ContentUnavailableError < Error
25
+ attr_reader :additional_data
26
+
27
+ def initialize(msg = "Morris could not find content requested", additional_data: {})
28
+ super(msg)
29
+ @additional_data = additional_data
30
+ end
31
+
32
+ def to_honeybadger_context
33
+ additional_data
34
+ end
35
+ end
36
+
37
+ class RetryableError < Error; end
38
+
39
+ class MediaRequestTimedOutError < RetryableError
40
+ def initialize(msg = "Morris encountered a timeout error requesting an media")
41
+ super
42
+ end
43
+ end
44
+
45
+ class MediaRequestFailedError < RetryableError
46
+ def initialize(msg = "Morris received a non-200 response requesting an media")
47
+ super
48
+ end
49
+ end
50
+
51
+ define_setting :temp_storage_location, "tmp/Morris"
52
+
53
+ # Get an image from a URL and save to a temp folder set in the configuration under
54
+ # temp_storage_location
55
+ def self.retrieve_media(url)
56
+ response = Typhoeus.get(url)
57
+
58
+ # Get the file extension if it's in the file
59
+ stripped_url = url.split("?").first # remove URL query params
60
+ extension = stripped_url.split(".").last
61
+
62
+ # Do some basic checks so we just empty out if there's something weird in the file extension
63
+ # that could do some harm.
64
+ if extension.length.positive?
65
+ extension = nil unless /^[a-zA-Z0-9]+$/.match?(extension)
66
+ extension = ".#{extension}" unless extension.nil?
67
+ end
68
+
69
+ temp_file_name = "#{Morris.temp_storage_location}/morris_media_#{SecureRandom.uuid}#{extension}"
70
+
71
+ # We do this in case the folder isn't created yet, since it's a temp folder we'll just do so
72
+ self.create_temp_storage_location
73
+ File.binwrite(temp_file_name, response.body)
74
+ temp_file_name
75
+ end
76
+
77
+ private
78
+
79
+ def self.create_temp_storage_location
80
+ return if File.exist?(Morris.temp_storage_location) && File.directory?(Morris.temp_storage_location)
81
+ FileUtils.mkdir_p Morris.temp_storage_location
82
+ end
83
+ end
data/morris.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/morris/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "morris"
7
+ spec.version = Morris::VERSION
8
+ spec.authors = ["Christopher Guess"]
9
+ spec.email = ["cguess@gmail.com"]
10
+
11
+ spec.summary = "A TikTok media and post scraper"
12
+ spec.description = "The Thing"
13
+ spec.homepage = "https://www.github.com/techandcheck/morris"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://www.github.com/techandcheck/morris"
21
+ spec.metadata["changelog_uri"] = "https://www.github.com/techandcheck/morris/CHANGELOG.md"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_dependency "capybara" # For scraping and running browsers
35
+ spec.add_dependency "apparition" # A Chrome driver for Capybara
36
+ spec.add_dependency "typhoeus" # For making API requests
37
+ spec.add_dependency "oj" # A faster JSON parser/loader than stdlib
38
+ spec.add_dependency "selenium-webdriver" # Webdriver selenium
39
+ spec.add_dependency "selenium-devtools" # Allow us to intercept requests
40
+ spec.add_dependency "terrapin" # For running shell commands
41
+
42
+ spec.add_development_dependency "debug"
43
+ end
data/sig/morris.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Morris
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: morris
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Guess
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-03-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: capybara
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: apparition
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: typhoeus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: oj
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: selenium-webdriver
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: selenium-devtools
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: terrapin
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: debug
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: The Thing
126
+ email:
127
+ - cguess@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".byebug_history"
133
+ - ".rubocop.yml"
134
+ - CHANGELOG.md
135
+ - CODE_OF_CONDUCT.md
136
+ - Gemfile
137
+ - Gemfile.lock
138
+ - LICENSE.txt
139
+ - README.md
140
+ - Rakefile
141
+ - lib/helpers/configuration.rb
142
+ - lib/morris.rb
143
+ - lib/morris/monkeypatch.rb
144
+ - lib/morris/post.rb
145
+ - lib/morris/scrapers/post_scraper.rb
146
+ - lib/morris/scrapers/scraper.rb
147
+ - lib/morris/scrapers/user_scraper.rb
148
+ - lib/morris/scrapers/video_scraper.rb
149
+ - lib/morris/user.rb
150
+ - lib/morris/version.rb
151
+ - morris.gemspec
152
+ - sig/morris.rbs
153
+ homepage: https://www.github.com/techandcheck/morris
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ homepage_uri: https://www.github.com/techandcheck/morris
158
+ source_code_uri: https://www.github.com/techandcheck/morris
159
+ changelog_uri: https://www.github.com/techandcheck/morris/CHANGELOG.md
160
+ post_install_message:
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: 3.1.0
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements: []
175
+ rubygems_version: 3.4.9
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: A TikTok media and post scraper
179
+ test_files: []