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.
- checksums.yaml +7 -0
- data/.github/workflows/codeql-analysis.yml +42 -0
- data/.github/workflows/ruby.yml +44 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +41 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +174 -0
- data/README.md +319 -0
- data/Rakefile +12 -0
- data/bin/console +23 -0
- data/bin/install +22 -0
- data/bin/setup +8 -0
- data/codecov.yml +2 -0
- data/docs/DEVELOPMENT.md +42 -0
- data/exe/xxx_rename +12 -0
- data/lib/xxx_rename/actions/base_action.rb +20 -0
- data/lib/xxx_rename/actions/log_new_filename.rb +40 -0
- data/lib/xxx_rename/actions/resolver.rb +32 -0
- data/lib/xxx_rename/actions/stash_app_post_movie.rb +62 -0
- data/lib/xxx_rename/actors_helper.rb +117 -0
- data/lib/xxx_rename/cli.rb +211 -0
- data/lib/xxx_rename/client.rb +110 -0
- data/lib/xxx_rename/constants.rb +96 -0
- data/lib/xxx_rename/contract/config_contract.rb +241 -0
- data/lib/xxx_rename/contract/config_generator.rb +207 -0
- data/lib/xxx_rename/contract/file_rename_op_contract.rb +54 -0
- data/lib/xxx_rename/contract/types.rb +10 -0
- data/lib/xxx_rename/core_extensions/string.rb +39 -0
- data/lib/xxx_rename/data/base.rb +34 -0
- data/lib/xxx_rename/data/config.rb +97 -0
- data/lib/xxx_rename/data/file_rename_op.rb +42 -0
- data/lib/xxx_rename/data/file_rename_op_datastore.rb +111 -0
- data/lib/xxx_rename/data/naughty_america_database.rb +22 -0
- data/lib/xxx_rename/data/query_interface.rb +78 -0
- data/lib/xxx_rename/data/scene_data.rb +71 -0
- data/lib/xxx_rename/data/scene_datastore.rb +401 -0
- data/lib/xxx_rename/data/site_config.rb +84 -0
- data/lib/xxx_rename/data/types.rb +13 -0
- data/lib/xxx_rename/errors.rb +28 -0
- data/lib/xxx_rename/file_scanner.rb +49 -0
- data/lib/xxx_rename/file_utilities.rb +38 -0
- data/lib/xxx_rename/filename_generator.rb +173 -0
- data/lib/xxx_rename/integrations/base.rb +20 -0
- data/lib/xxx_rename/integrations/stash_app.rb +316 -0
- data/lib/xxx_rename/log.rb +26 -0
- data/lib/xxx_rename/migration_client.rb +139 -0
- data/lib/xxx_rename/processed_file.rb +203 -0
- data/lib/xxx_rename/search.rb +166 -0
- data/lib/xxx_rename/site_client_matcher.rb +299 -0
- data/lib/xxx_rename/site_clients/adult_time.rb +31 -0
- data/lib/xxx_rename/site_clients/algolia_common.rb +48 -0
- data/lib/xxx_rename/site_clients/algolia_v2.rb +181 -0
- data/lib/xxx_rename/site_clients/babes.rb +15 -0
- data/lib/xxx_rename/site_clients/base.rb +61 -0
- data/lib/xxx_rename/site_clients/blacked.rb +12 -0
- data/lib/xxx_rename/site_clients/blacked_raw.rb +12 -0
- data/lib/xxx_rename/site_clients/brazzers.rb +15 -0
- data/lib/xxx_rename/site_clients/configuration.rb +55 -0
- data/lib/xxx_rename/site_clients/digital_playground.rb +15 -0
- data/lib/xxx_rename/site_clients/elegant_angel.rb +168 -0
- data/lib/xxx_rename/site_clients/errors.rb +103 -0
- data/lib/xxx_rename/site_clients/evil_angel.rb +59 -0
- data/lib/xxx_rename/site_clients/goodporn.rb +109 -0
- data/lib/xxx_rename/site_clients/jules_jordan.rb +22 -0
- data/lib/xxx_rename/site_clients/jules_jordan_media.rb +175 -0
- data/lib/xxx_rename/site_clients/manuel_ferrara.rb +24 -0
- data/lib/xxx_rename/site_clients/mg_premium.rb +247 -0
- data/lib/xxx_rename/site_clients/mofos.rb +15 -0
- data/lib/xxx_rename/site_clients/naughty_america.rb +272 -0
- data/lib/xxx_rename/site_clients/nfbusty.rb +84 -0
- data/lib/xxx_rename/site_clients/query_generator/base.rb +89 -0
- data/lib/xxx_rename/site_clients/query_generator/evil_angel.rb +36 -0
- data/lib/xxx_rename/site_clients/query_generator/goodporn.rb +27 -0
- data/lib/xxx_rename/site_clients/query_generator/mg_premium.rb +26 -0
- data/lib/xxx_rename/site_clients/query_generator/naughty_america.rb +24 -0
- data/lib/xxx_rename/site_clients/query_generator/stash_db.rb +21 -0
- data/lib/xxx_rename/site_clients/query_generator/vixen.rb +27 -0
- data/lib/xxx_rename/site_clients/query_generator/whale.rb +39 -0
- data/lib/xxx_rename/site_clients/reality_kings.rb +14 -0
- data/lib/xxx_rename/site_clients/stash_db.rb +257 -0
- data/lib/xxx_rename/site_clients/tushy.rb +12 -0
- data/lib/xxx_rename/site_clients/tushy_raw.rb +12 -0
- data/lib/xxx_rename/site_clients/twistys.rb +15 -0
- data/lib/xxx_rename/site_clients/vixen.rb +12 -0
- data/lib/xxx_rename/site_clients/vixen_media.rb +130 -0
- data/lib/xxx_rename/site_clients/whale.rb +106 -0
- data/lib/xxx_rename/site_clients/wicked.rb +52 -0
- data/lib/xxx_rename/site_clients/x_empire.rb +51 -0
- data/lib/xxx_rename/site_clients/zero_tolerance.rb +36 -0
- data/lib/xxx_rename/utils.rb +81 -0
- data/lib/xxx_rename/version.rb +5 -0
- data/lib/xxx_rename.rb +60 -0
- data/output.png +0 -0
- data/xxx_rename.gemspec +42 -0
- 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,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
|