xxx_rename 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/codeql-analysis.yml +42 -0
  3. data/.github/workflows/ruby.yml +44 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +41 -0
  7. data/.ruby-version +1 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +174 -0
  10. data/README.md +319 -0
  11. data/Rakefile +12 -0
  12. data/bin/console +23 -0
  13. data/bin/install +22 -0
  14. data/bin/setup +8 -0
  15. data/codecov.yml +2 -0
  16. data/docs/DEVELOPMENT.md +42 -0
  17. data/exe/xxx_rename +12 -0
  18. data/lib/xxx_rename/actions/base_action.rb +20 -0
  19. data/lib/xxx_rename/actions/log_new_filename.rb +40 -0
  20. data/lib/xxx_rename/actions/resolver.rb +32 -0
  21. data/lib/xxx_rename/actions/stash_app_post_movie.rb +62 -0
  22. data/lib/xxx_rename/actors_helper.rb +117 -0
  23. data/lib/xxx_rename/cli.rb +211 -0
  24. data/lib/xxx_rename/client.rb +110 -0
  25. data/lib/xxx_rename/constants.rb +96 -0
  26. data/lib/xxx_rename/contract/config_contract.rb +241 -0
  27. data/lib/xxx_rename/contract/config_generator.rb +207 -0
  28. data/lib/xxx_rename/contract/file_rename_op_contract.rb +54 -0
  29. data/lib/xxx_rename/contract/types.rb +10 -0
  30. data/lib/xxx_rename/core_extensions/string.rb +39 -0
  31. data/lib/xxx_rename/data/base.rb +34 -0
  32. data/lib/xxx_rename/data/config.rb +97 -0
  33. data/lib/xxx_rename/data/file_rename_op.rb +42 -0
  34. data/lib/xxx_rename/data/file_rename_op_datastore.rb +111 -0
  35. data/lib/xxx_rename/data/naughty_america_database.rb +22 -0
  36. data/lib/xxx_rename/data/query_interface.rb +78 -0
  37. data/lib/xxx_rename/data/scene_data.rb +71 -0
  38. data/lib/xxx_rename/data/scene_datastore.rb +401 -0
  39. data/lib/xxx_rename/data/site_config.rb +84 -0
  40. data/lib/xxx_rename/data/types.rb +13 -0
  41. data/lib/xxx_rename/errors.rb +28 -0
  42. data/lib/xxx_rename/file_scanner.rb +49 -0
  43. data/lib/xxx_rename/file_utilities.rb +38 -0
  44. data/lib/xxx_rename/filename_generator.rb +173 -0
  45. data/lib/xxx_rename/integrations/base.rb +20 -0
  46. data/lib/xxx_rename/integrations/stash_app.rb +316 -0
  47. data/lib/xxx_rename/log.rb +26 -0
  48. data/lib/xxx_rename/migration_client.rb +139 -0
  49. data/lib/xxx_rename/processed_file.rb +203 -0
  50. data/lib/xxx_rename/search.rb +166 -0
  51. data/lib/xxx_rename/site_client_matcher.rb +299 -0
  52. data/lib/xxx_rename/site_clients/adult_time.rb +31 -0
  53. data/lib/xxx_rename/site_clients/algolia_common.rb +48 -0
  54. data/lib/xxx_rename/site_clients/algolia_v2.rb +181 -0
  55. data/lib/xxx_rename/site_clients/babes.rb +15 -0
  56. data/lib/xxx_rename/site_clients/base.rb +61 -0
  57. data/lib/xxx_rename/site_clients/blacked.rb +12 -0
  58. data/lib/xxx_rename/site_clients/blacked_raw.rb +12 -0
  59. data/lib/xxx_rename/site_clients/brazzers.rb +15 -0
  60. data/lib/xxx_rename/site_clients/configuration.rb +55 -0
  61. data/lib/xxx_rename/site_clients/digital_playground.rb +15 -0
  62. data/lib/xxx_rename/site_clients/elegant_angel.rb +168 -0
  63. data/lib/xxx_rename/site_clients/errors.rb +103 -0
  64. data/lib/xxx_rename/site_clients/evil_angel.rb +59 -0
  65. data/lib/xxx_rename/site_clients/goodporn.rb +109 -0
  66. data/lib/xxx_rename/site_clients/jules_jordan.rb +22 -0
  67. data/lib/xxx_rename/site_clients/jules_jordan_media.rb +175 -0
  68. data/lib/xxx_rename/site_clients/manuel_ferrara.rb +24 -0
  69. data/lib/xxx_rename/site_clients/mg_premium.rb +247 -0
  70. data/lib/xxx_rename/site_clients/mofos.rb +15 -0
  71. data/lib/xxx_rename/site_clients/naughty_america.rb +272 -0
  72. data/lib/xxx_rename/site_clients/nfbusty.rb +84 -0
  73. data/lib/xxx_rename/site_clients/query_generator/base.rb +89 -0
  74. data/lib/xxx_rename/site_clients/query_generator/evil_angel.rb +36 -0
  75. data/lib/xxx_rename/site_clients/query_generator/goodporn.rb +27 -0
  76. data/lib/xxx_rename/site_clients/query_generator/mg_premium.rb +26 -0
  77. data/lib/xxx_rename/site_clients/query_generator/naughty_america.rb +24 -0
  78. data/lib/xxx_rename/site_clients/query_generator/stash_db.rb +21 -0
  79. data/lib/xxx_rename/site_clients/query_generator/vixen.rb +27 -0
  80. data/lib/xxx_rename/site_clients/query_generator/whale.rb +39 -0
  81. data/lib/xxx_rename/site_clients/reality_kings.rb +14 -0
  82. data/lib/xxx_rename/site_clients/stash_db.rb +257 -0
  83. data/lib/xxx_rename/site_clients/tushy.rb +12 -0
  84. data/lib/xxx_rename/site_clients/tushy_raw.rb +12 -0
  85. data/lib/xxx_rename/site_clients/twistys.rb +15 -0
  86. data/lib/xxx_rename/site_clients/vixen.rb +12 -0
  87. data/lib/xxx_rename/site_clients/vixen_media.rb +130 -0
  88. data/lib/xxx_rename/site_clients/whale.rb +106 -0
  89. data/lib/xxx_rename/site_clients/wicked.rb +52 -0
  90. data/lib/xxx_rename/site_clients/x_empire.rb +51 -0
  91. data/lib/xxx_rename/site_clients/zero_tolerance.rb +36 -0
  92. data/lib/xxx_rename/utils.rb +81 -0
  93. data/lib/xxx_rename/version.rb +5 -0
  94. data/lib/xxx_rename.rb +60 -0
  95. data/output.png +0 -0
  96. data/xxx_rename.gemspec +42 -0
  97. metadata +411 -0
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "nokogiri"
5
+
6
+ module XxxRename
7
+ module SiteClients
8
+ module AlgoliaCommon
9
+ CLIENT_KEY_JS_REGEX_1 = /var\sclient\s=\salgoliasearch\('(?<application_id>\w+)',\s'(?<api_key>[\w=]+)'\);/x.freeze
10
+ CLIENT_KEY_JS_REGEX_2 = /
11
+ window\.env\s*=\s*{
12
+ "api":{
13
+ "algolia":{
14
+ "applicationID":"(?<application_id>\w+)",
15
+ "apiKey":"(?<api_key>[\w=]+)"
16
+ }
17
+ }/x.freeze
18
+
19
+ ALGOLIA_RATE_LIMIT_MESSAGE = "Too many requests"
20
+ ALGOLIA_RATE_LIMIT_STATUS = 429
21
+ ALGOLIA_EXPIRY_MESSAGE = "\"validUntil\" parameter expired (less than current date)"
22
+ ALGOLIA_EXPIRY_STATUS = 400
23
+
24
+ AlgoliaParams = Struct.new(:application_id, :api_key, keyword_init: true)
25
+
26
+ def algolia_params!(site)
27
+ doc = Nokogiri::HTML HTTParty.get(site, headers: Constants::DEFAULT_HEADERS).parsed_response
28
+ js = doc.search("script").text
29
+ match = js.match(CLIENT_KEY_JS_REGEX_1) || js.match(CLIENT_KEY_JS_REGEX_2)
30
+ raise "Unable to fetch algolia credentials" if match.nil?
31
+
32
+ AlgoliaParams.new(application_id: match[:application_id], api_key: match[:api_key])
33
+ end
34
+
35
+ def actors_contained?(actors_from_file, actors_from_response)
36
+ # All actors in actors_from_file should be contained in actors_from_response
37
+ get_actor = ->(hsh) { hsh[:name] }
38
+ actors_from_file_set = actors_from_file.map(&:normalize).to_set
39
+ actors_from_response_set = actors_from_response.map(&get_actor).map(&:normalize).to_set
40
+ actors_from_file_set.subset? actors_from_response_set
41
+ end
42
+
43
+ def date_released(resp)
44
+ Time.strptime(resp[:release_date].strip, "%Y-%m-%d")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "algolia"
4
+
5
+ require "xxx_rename/site_clients/algolia_common"
6
+
7
+ module XxxRename
8
+ module SiteClients
9
+ class AlgoliaV2 < Base
10
+ include AlgoliaCommon
11
+
12
+ def client(refresh: false)
13
+ if refresh
14
+ XxxRename.logger.debug "Refreshing Algolia Token...".colorize(:blue)
15
+
16
+ @client = nil
17
+ @scenes_index = nil
18
+ @movies_index = nil
19
+ @actor_index = nil
20
+ end
21
+
22
+ @client = client! if @client.nil?
23
+
24
+ @client
25
+ end
26
+
27
+ def scenes_index
28
+ @scenes_index ||= client.init_index(self.class::SCENES_INDEX_NAME)
29
+ end
30
+
31
+ def movies_index
32
+ @movies_index ||= client.init_index(self.class::MOVIES_INDEX_NAME)
33
+ end
34
+
35
+ def actors_index
36
+ @actors_index ||= client.init_index(self.class::ACTORS_INDEX_NAME)
37
+ end
38
+
39
+ def fetch_scenes_from_api(str)
40
+ with_retry { scenes_index.search(str, default_query)&.[](:hits) }
41
+ end
42
+
43
+ def fetch_actor_from_api(str)
44
+ with_retry { actors_index.search(str)&.[](:hits) }
45
+ end
46
+
47
+ private
48
+
49
+ def client!
50
+ Algolia::Search::Client.new(algolia_config, logger: XxxRename.logger)
51
+ end
52
+
53
+ def algolia_config
54
+ params = algolia_params!(@site_url)
55
+ algolia_config = Algolia::Search::Config.new(application_id: params.application_id,
56
+ api_key: params.api_key)
57
+ algolia_config.set_extra_header("Referer", @site_url)
58
+ algolia_config
59
+ end
60
+
61
+ def with_retry(current_attempt: 1, max_attempts: 5, &block)
62
+ if current_attempt > max_attempts
63
+ raise XxxRename::Errors::FatalError, "Retry exceeded #{self.class.name} ran exceeded retry attempts #{max_attempts}"
64
+ end
65
+
66
+ block.call
67
+ rescue Algolia::AlgoliaHttpError => e
68
+ case e.code
69
+ when 429
70
+ XxxRename.logger.error "[RATE LIMIT EXCEEDED] Sleeping for 3 minutes. Cancel to run the app at a different time."
71
+ 6.times do |counter|
72
+ sleep(30)
73
+ XxxRename.logger.info "[SLEEP ELAPSED] #{counter * 30}s"
74
+ end
75
+ else
76
+ XxxRename.logger.error "#{e.class}: code:#{e.code} message:#{e.message}"
77
+ end
78
+ client(refresh: true)
79
+ with_retry(current_attempt: current_attempt + 1, max_attempts: max_attempts, &block)
80
+ end
81
+
82
+ def default_query
83
+ {
84
+ attributesToRetrieve: %w[clip_id title actors release_date description network_name movie_id movie_title],
85
+ hitsPerPage: 50
86
+ }
87
+ end
88
+
89
+ def default_facet_filters
90
+ %w[upcoming:0 content_tags:straight]
91
+ end
92
+
93
+ def find_matched_scene!(search_results, match)
94
+ scenes = search_results.reject { |x| x[:release_date].nil? }
95
+ .uniq { |x| x[:clip_id] }
96
+ .select { |x| actors_contained?(match.actors, x[:actors]) }
97
+ .select { |x| x[:title].normalize.start_with?(match.title.normalize) }
98
+
99
+ raise Errors::NoMatchError.new(Errors::NoMatchError::ERR_NO_RESULT, match.title) if scenes.length != 1
100
+
101
+ make_scene_data(scenes.first)
102
+ end
103
+
104
+ def make_scene_data(scene)
105
+ hash = {}.tap do |h|
106
+ h[:female_actors] = female_actors(scene)
107
+ h[:male_actors] = male_actors(scene)
108
+ h[:actors] = female_actors(scene) + male_actors(scene)
109
+ h[:collection] = scene[:network_name]&.strip&.titleize
110
+ h[:collection_tag] = site_config.collection_tag
111
+ h[:title] = scene[:title]&.strip
112
+ h[:id] = scene[:clip_id].to_s
113
+ h[:date_released] = date_released(scene[:release_date])
114
+ movie_hash = movie_details(scene[:movie_id]) || nil
115
+ h[:movie] = movie_hash unless movie_hash.nil?
116
+ end
117
+
118
+ Data::SceneData.new(hash)
119
+ end
120
+
121
+ def find_movie(movie_id)
122
+ options = {
123
+ filters: "movie_id:#{movie_id}",
124
+ attributesToRetrieve: %w[movie_id title description date_created
125
+ studio_name directors network_name
126
+ url_title cover_path],
127
+ hitsPerPage: 1
128
+ }
129
+ with_retry { movies_index.search("", options)&.[](:hits)&.first }
130
+ end
131
+
132
+ def movie_details(movie_id)
133
+ return movies[movie_id] if movies.key?(movie_id)
134
+
135
+ movie = find_movie(movie_id)
136
+ return if movie.nil?
137
+
138
+ movie_hash = {}.tap do |h|
139
+ h[:name] = movie[:title]&.strip&.titleize
140
+ date = date_released(movie[:date_created])
141
+ h[:date] = date if date
142
+ h[:url] = URI.join(@site_url, "/en/movie/", "#{movie[:url_title]}/", (movie[:movie_id]).to_s).to_s
143
+ h[:front_image] = "#{self.class::CDN_BASE_URL}/movies#{movie[:cover_path]}_front_400x625.jpg?width=900&height=1272&format=webp"
144
+ h[:back_image] = "#{self.class::CDN_BASE_URL}/movies#{movie[:cover_path]}_back_400x625.jpg?width=900&height=1272&format=webp"
145
+ h[:studio] = movie[:network_name]&.strip
146
+ h[:synopsis] = movie[:description]
147
+ end
148
+
149
+ movies[movie_id] = movie_hash
150
+ movie_hash
151
+ end
152
+
153
+ def female_actors(resp)
154
+ resp[:actors]
155
+ .select { |actor| actor[:gender] == "female" }
156
+ .map { |hash| hash[:name] }
157
+ .map(&:strip)
158
+ .sort
159
+ end
160
+
161
+ def male_actors(resp)
162
+ resp[:actors]
163
+ .select { |actor| actor[:gender] == "male" }
164
+ .map { |hash| hash[:name] }
165
+ .map(&:strip)
166
+ .sort
167
+ end
168
+
169
+ def date_released(str, format = "%Y-%m-%d")
170
+ Time.strptime(str.strip, format)
171
+ rescue ArgumentError => e
172
+ XxxRename.logger.error "[DATE PARSING ERROR] #{e.message}"
173
+ nil
174
+ end
175
+
176
+ def movies
177
+ @movies ||= {}
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/site_clients/mg_premium"
4
+
5
+ module XxxRename
6
+ module SiteClients
7
+ class Babes < MGPremium
8
+ site_client_name :babes
9
+
10
+ def initialize(config)
11
+ super(config, site_url: "https://www.babes.com")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "xxx_rename/site_clients/configuration"
5
+ require "xxx_rename/utils"
6
+
7
+ module XxxRename
8
+ module SiteClients
9
+ class Base
10
+ include Utils
11
+ include SiteClients::Configuration
12
+
13
+ attr_reader :source_format, :config
14
+
15
+ # @param [XxxRename::Data::Config] config
16
+ def initialize(config)
17
+ @actors_helper = ActorsHelper.instance
18
+ @config = config
19
+ @source_format = site_config.file_source_format
20
+ return unless self.class.include?(HTTParty)
21
+
22
+ self.class.logger(XxxRename.logger, :debug)
23
+ self.class.headers(Constants::DEFAULT_HEADERS)
24
+ end
25
+
26
+ def search(_filename, **_opts)
27
+ raise "Not Implemented."
28
+ end
29
+
30
+ private
31
+
32
+ def match?(str1, str2)
33
+ str1.normalize == str2.normalize
34
+ end
35
+
36
+ def contains?(str1, str2)
37
+ str1.normalize.include?(str2.normalize)
38
+ end
39
+
40
+ def actors_hash(actors)
41
+ actors.each { |x| @actors_helper.auto_fetch! x }
42
+ female_actors = actors.select { |x| @actors_helper.female? x }
43
+ male_actors = actors.select { |x| @actors_helper.male? x }
44
+ resp = { female_actors: female_actors,
45
+ male_actors: male_actors,
46
+ actors: female_actors + male_actors }
47
+ diff = (resp[:female_actors] + resp[:male_actors]) - actors
48
+ raise "Actors #{diff.join(", ")} removed from response even after being processed." if diff.length > 1
49
+
50
+ resp
51
+ rescue XxxRename::Errors::UnprocessedEntity => e
52
+ XxxRename.logger.warn "Unable to fetch details of actor #{e.message}"
53
+ {
54
+ female_actors: [],
55
+ male_actors: [],
56
+ actors: actors
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/site_clients/vixen_media"
4
+
5
+ module XxxRename
6
+ module SiteClients
7
+ class Blacked < VixenMedia
8
+ base_uri "https://www.blacked.com"
9
+ site_client_name :blacked
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/site_clients/vixen_media"
4
+
5
+ module XxxRename
6
+ module SiteClients
7
+ class BlackedRaw < VixenMedia
8
+ base_uri "https://www.blackedraw.com/"
9
+ site_client_name :blacked_raw
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/site_clients/mg_premium"
4
+
5
+ module XxxRename
6
+ module SiteClients
7
+ class Brazzers < MGPremium
8
+ site_client_name :brazzers
9
+
10
+ def initialize(config)
11
+ super(config, site_url: "https://www.brazzers.com")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module SiteClients
5
+ module Configuration
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+
9
+ # The `ModuleInheritableAttributes` allows us to use some ruby magic
10
+ base.send :include, HTTParty::ModuleInheritableAttributes
11
+ base.send(:mattr_inheritable, :constants)
12
+ base.instance_variable_set("@constants", {})
13
+
14
+ base.class_eval do
15
+ def site_config
16
+ config.site.send(self.class.site_client_name.to_sym)
17
+ end
18
+
19
+ def site_client_datastore
20
+ @site_client_datastore ||=
21
+ begin
22
+ datastore_name = site_config.database.presence ? site_config.database : self.class.name.demodulize.underscore
23
+ qualified_name = datastore_name.end_with?(".store") ? datastore_name : "#{datastore_name}.store"
24
+ path = File.join(config.generated_files_dir, self.class.site_client_name.to_s)
25
+ FileUtils.mkpath(path)
26
+ store = Data::SceneDatastore.new(path, qualified_name).store
27
+ Data::SceneDatastoreQuery.new(store, config.mutex)
28
+ end
29
+ end
30
+
31
+ # @return [Hash]
32
+ def metadata
33
+ site_client_datastore.metadata
34
+ end
35
+
36
+ # @param [Hash] opts
37
+ # @return [Hash]
38
+ def update_metadata(**opts)
39
+ site_client_datastore.update_metadata(opts)
40
+ end
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ def site_client_name(name = nil)
46
+ return @constants[:site_client_name] if @constants.key?(:site_client_name)
47
+
48
+ raise XxxRename::Errors::FatalError, "#{self.name} did not set its :site_client_name" if name.nil?
49
+
50
+ @constants[:site_client_name] = name
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/site_clients/mg_premium"
4
+
5
+ module XxxRename
6
+ module SiteClients
7
+ class DigitalPlayground < MGPremium
8
+ site_client_name :digital_playground
9
+
10
+ def initialize(config)
11
+ super(config, site_url: "https://www.digitalplayground.com")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "xxx_rename/site_clients/query_generator/base"
5
+
6
+ module XxxRename
7
+ module SiteClients
8
+ class ElegantAngel < Base
9
+ include HTTParty
10
+
11
+ base_uri "https://www.elegantangel.com"
12
+ site_client_name :elegant_angel
13
+ OLDEST_PROCESSABLE_MOVIE_YEAR = 2007
14
+ COLLECTION = "Elegant Angel"
15
+ MOVIES_ENDPOINT = "/streaming-elegant-angel-dvds-on-video.html?page=$page$"
16
+
17
+ def search(filename)
18
+ refresh_datastore(1) if datastore_refresh_required?
19
+ match = SiteClients::QueryGenerator::Base.generic_generate(filename, source_format)
20
+ lookup_in_datastore!(match)
21
+ end
22
+
23
+ def all_scenes_processed?
24
+ all_scenes_processed
25
+ end
26
+
27
+ def oldest_processable_date?
28
+ oldest_processable_date
29
+ end
30
+
31
+ private
32
+
33
+ def lookup_in_datastore!(match)
34
+ msg = "requires both movie title(%collection) and scene title(%title)"
35
+ raise Errors::NoMatchError.new(Errors::NoMatchError::ERR_NO_METADATA, msg) if match.nil?
36
+
37
+ raise Errors::NoMatchError.new(Errors::NoMatchError::ERR_NO_METADATA, msg) unless match.collection.presence && match.title.presence
38
+
39
+ index_key = site_client_datastore.generate_lookup_key(match.collection, match.title)
40
+ scene_data_key = site_client_datastore.find_by_key?(index_key)
41
+ raise Errors::NoMatchError.new(Errors::NoMatchError::ERR_NO_RESULT, index_key) if scene_data_key.nil?
42
+
43
+ scene_data = site_client_datastore.find_by_key?(scene_data_key)
44
+ raise Errors::NoMatchError.new(Errors::NoMatchError::ERR_NO_RESULT, scene_data_key) if scene_data.nil?
45
+
46
+ scene_data
47
+ end
48
+
49
+ def all_scenes_processed
50
+ @all_scenes_processed ||= false
51
+ end
52
+
53
+ def oldest_processable_date
54
+ @oldest_processable_date ||= false
55
+ end
56
+
57
+ def datastore_refresh_required?
58
+ if config.force_refresh_datastore
59
+ XxxRename.logger.info "#{"[FORCE REFRESH]".colorize(:green)} #{self.class.name}"
60
+ true
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ def refresh_datastore(page)
67
+ movie_links = movie_links(page)
68
+ @all_scenes_processed = true if movie_links.blank?
69
+
70
+ movie_links.map do |path|
71
+ movie_doc = doc(path)
72
+ movie_hash = movie_hash(movie_doc, path)
73
+ if movie_hash[:date].year < OLDEST_PROCESSABLE_MOVIE_YEAR
74
+ @oldest_processable_date = true
75
+ break
76
+ end
77
+ scenes = movie_scenes(movie_doc, movie_hash)
78
+ scenes.map { |scene_data| site_client_datastore.create!(scene_data, force: true) }
79
+ end
80
+
81
+ stop_processing? ? true : refresh_datastore(page + 1)
82
+ end
83
+
84
+ def stop_processing?
85
+ if all_scenes_processed?
86
+ XxxRename.logger.info "#{"[DATASTORE REFRESH COMPLETE]".colorize(:green)} #{self.class.site_client_name}"
87
+ true
88
+ elsif oldest_processable_date?
89
+ XxxRename.logger.info "#{"[OLDEST PROCESSABLE MOVIE REACHED]".colorize(:green)} #{self.class.site_client_name}"
90
+ true
91
+ else
92
+ false
93
+ end
94
+ end
95
+
96
+ def movie_links(page)
97
+ doc = doc(MOVIES_ENDPOINT.gsub("$page$", page.to_s))
98
+ doc.css(".item-grid .grid-item .boxcover")
99
+ .map { |x| x["href"] }
100
+ .uniq
101
+ .map { |x| x.gsub(self.class.base_uri, "") }
102
+ end
103
+
104
+ def movie_scenes(doc, movie_hash)
105
+ XxxRename.logger.info "#{"[PROCESSING MOVIE]".colorize(:green)} #{movie_hash[:name]}"
106
+
107
+ scenes(doc).map do |scene_doc|
108
+ next if scene_unavailable?(scene_doc)
109
+
110
+ hash = {}.tap do |h|
111
+ h[:actors] = scene_doc.css(".scene-performer-names a").map { |x| x.text&.strip }
112
+ h[:collection] = movie_hash[:name]
113
+ h[:collection_tag] = site_config.collection_tag
114
+ h[:title] = scene_doc.at('//a[@class="scene-title"]//h6/text()[last()]')&.text&.strip
115
+ h[:date_released] = movie_hash[:date]
116
+ scene_path = scene_doc.at('//a[@class="scene-title"]/@href').value
117
+ h[:scene_link] = URI.join(self.class.base_uri, scene_path).to_s
118
+ h[:movie] = movie_hash
119
+ end
120
+ XxxRename.logger.info "#{"[PROCESSING SCENE]".colorize(:green)} #{hash[:title]}"
121
+ Data::SceneData.new(hash)
122
+ end.compact
123
+ end
124
+
125
+ def scene_unavailable?(scene_doc)
126
+ # scene title is un-clickable
127
+ scene_doc.at('//a[@class="scene-title"]').nil? ||
128
+ # buying scene is disabled
129
+ scene_doc.at('//div[contains(@class, "scene-buy-options")]//button[contains(@class, "disabled")]').present? # scene title is un-clickable
130
+ end
131
+
132
+ def scenes(doc)
133
+ doc.xpath('//div[@id="scenes"]//div[@class="scene-details"]').presence ||
134
+ doc.xpath('//div[@id="scenes"]//div[@class="grid-item"]').presence ||
135
+ []
136
+ end
137
+
138
+ def movie_hash(doc, path)
139
+ image_css = doc.xpath('//div[@id="viewLargeBoxcoverCarousel"]//div[contains(@class, "carousel-item")]//img/@data-src')
140
+ date_str = doc.xpath('//div[contains(@class, "video-details")]' \
141
+ '//div[@class="release-date"]' \
142
+ '//span[contains(text(),"Released")]/../text()').text&.strip
143
+ synopsis = doc.xpath('//div[contains(@class, "video-details")]//div[@class="synopsis"]').text
144
+ {}.tap do |h|
145
+ h[:name] = doc.css(".video-title h1.description").text&.strip
146
+ h[:date] = date_released(date_str, "%b %d, %Y")
147
+ h[:url] = URI.join(self.class.base_uri, path).to_s
148
+ h[:front_image] = image_css.first.text
149
+ h[:back_image] = image_css.last.text if image_css.length == 2
150
+ h[:studio] = "Elegant Angel"
151
+ h[:synopsis] = synopsis if synopsis.presence
152
+ end
153
+ end
154
+
155
+ def date_released(str, format = "%Y-%m-%d")
156
+ Time.strptime(str.strip, format)
157
+ rescue ArgumentError => e
158
+ XxxRename.logger.error "[DATE PARSING ERROR] #{e.message}"
159
+ nil
160
+ end
161
+
162
+ def doc(path)
163
+ res = handle_response!(return_raw: true) { self.class.get(path) }
164
+ Nokogiri::HTML res.parsed_response
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module SiteClients
5
+ module Errors
6
+ class NoMatchError < StandardError
7
+ attr_reader :code, :data
8
+
9
+ ERR_NO_METADATA = 0
10
+ ERR_NO_RESULT = 1
11
+ ERR_NW_REDIRECT = 2
12
+ ERR_CUSTOM = 3
13
+
14
+ def initialize(code, data)
15
+ @code = code
16
+ @data = data
17
+ super(make_message)
18
+ end
19
+
20
+ def make_message
21
+ case code
22
+ when ERR_NO_RESULT then "No results from API #{"using query #{data}" if data}"
23
+ when ERR_NO_METADATA then "No metadata parsed from file"
24
+ when ERR_NW_REDIRECT then "Network redirect from search #{data}"
25
+ when ERR_CUSTOM then data || "No metadata parsed from file"
26
+ else "Unhandled error when trying to process file (Code #{code})"
27
+ end
28
+ end
29
+ end
30
+
31
+ class SiteClientUnavailableError < StandardError
32
+ def initialize(site)
33
+ super("Network #{site} unreachable. Check if site is accessible.")
34
+ end
35
+ end
36
+
37
+ class InvalidCredentialsError < StandardError
38
+ def initialize(site)
39
+ super("Missing/Invalid credentials provided for #{site}")
40
+ end
41
+ end
42
+
43
+ class SearchError < StandardError
44
+ attr_reader :entity, :request_options, :response_code, :response_body
45
+
46
+ def initialize(entity, object)
47
+ @entity = entity
48
+ @request_options = object[:request_options]
49
+ @response_code = object[:response_code]
50
+ @response_body = object[:response_body]
51
+ super("Network Error while fetching details for file #{@entity}")
52
+ end
53
+
54
+ def dump_error
55
+ File.open(File.join($pwd, "error_dump.txt"), "a") do |dump| # rubocop:disable Style/GlobalVars
56
+ dump << "--------ERROR BEGIN--------\n#{message}\n"
57
+ dump << "--------ENTITY--------\n#{@entity}\n"
58
+ dump << "--------REQUEST--------\n#{@request_options}\n"
59
+ dump << "--------CODE--------\n#{@response_code}\n"
60
+ dump << "--------BODY--------\n#{@response_body}\n"
61
+ dump << "--------ERROR END--------\n\n\n"
62
+ end
63
+ end
64
+ end
65
+
66
+ class APIError < StandardError
67
+ attr_reader :endpoint, :code, :body, :headers
68
+
69
+ def initialize(endpoint:, code:, body:, headers:)
70
+ @endpoint = endpoint
71
+ @code = code
72
+ @body = body
73
+ @headers = headers
74
+ super(message)
75
+ end
76
+
77
+ def fetch_error_message
78
+ case code
79
+ when 302 then "unexpected redirection to #{headers["location"]}"
80
+ else (body&.[]("error") || body&.[]("message") || body).to_s[0..150]
81
+ end
82
+ end
83
+
84
+ def message
85
+ "API Failure:\n" \
86
+ "\tURL: #{endpoint}\n" \
87
+ "\tRESPONSE CODE: #{code}\n" \
88
+ "\tERROR MESSAGE: #{fetch_error_message}"
89
+ end
90
+ end
91
+
92
+ class BadGatewayError < APIError; end
93
+ class BadRequestError < APIError; end
94
+ class ForbiddenError < APIError; end
95
+ class InternalServerError < APIError; end
96
+ class NotFoundError < APIError; end
97
+ class RedirectedError < APIError; end
98
+ class TooManyRequestsError < APIError; end
99
+ class UnauthorizedError < APIError; end
100
+ class UnhandledError < APIError; end
101
+ end
102
+ end
103
+ end