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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module Contract
5
+ class FileRenameOpValidationFailure < StandardError
6
+ # @param [Hash] errors
7
+ def initialize(errors)
8
+ @errors = errors
9
+ super(message)
10
+ end
11
+
12
+ def message
13
+ ers = []
14
+ @errors.each_pair { |code, value| ers << "#{code}: #{value.join(" ")}" }
15
+ ers.join(", ")
16
+ end
17
+ end
18
+
19
+ class FileRenameOpContract < Dry::Validation::Contract
20
+ include FileUtilities
21
+
22
+ schema do
23
+ required(:key).value(:string)
24
+ required(:directory).value(:string)
25
+ required(:source_filename).filled(:string)
26
+ required(:output_filename).filled(:string)
27
+ required(:mtime).maybe(Types::Time)
28
+ end
29
+
30
+ rule(:directory) do
31
+ key(:directory_not_exists).failure(value) unless valid_dir?(value)
32
+ key(:non_absolute_path).failure(value) unless Pathname.new(value).absolute?
33
+ end
34
+
35
+ rule(:directory, :source_filename) do
36
+ unless rule_error?(:directory_not_exists)
37
+ path = File.join(values[:directory], values[:source_filename])
38
+ key(:file_not_found).failure(path) unless valid_file?(path)
39
+ end
40
+ end
41
+
42
+ rule(:output_filename) do
43
+ key(:output_filename_too_long).failure(value) if value.length > MAX_FILENAME_LEN
44
+ end
45
+
46
+ rule(:directory, :output_filename) do
47
+ unless rule_error?(:directory_not_exists)
48
+ path = File.join(values[:directory], values[:output_filename])
49
+ key(:output_file_already_exists).failure(path) if valid_file?(path)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module Contract
5
+ module Types
6
+ include Dry.Types()
7
+ SanitizedString = Types::String.constructor(&:strip)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module CoreExtensions
5
+ module String
6
+ # Remove the following characters from the input string and returns
7
+ # it in lower case
8
+ #
9
+ # \s : whitespace characters
10
+ # \W : any non-word character
11
+ # _ : underscore
12
+ #
13
+ # @return [String]
14
+ def normalize
15
+ gsub(/[\s\W_]/, "").downcase
16
+ end
17
+
18
+ # @return [String]
19
+ def titleize_custom
20
+ re = /([A-Z]\d[A-Z]|[A-Z][a-zA-Z])/
21
+ res = []
22
+ scan(re) { |_| res << Regexp.last_match.offset(0)[0] }
23
+ res.reverse.each { |index| insert(index, " ") }
24
+ strip
25
+ end
26
+
27
+ # Remove any characters not included in the following list
28
+ # \w [a-zA-Z0-9_].
29
+ # \s Whitespace character
30
+ # - hyphen
31
+ # , comma
32
+ # [ brackets
33
+ # ] brackets
34
+ def remove_special_characters
35
+ gsub(/[^\w\s\-,\[\]']/, "").gsub(/\s{2,}/, " ")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module Data
5
+ class Base < Dry::Struct
6
+ transform_keys(&:to_sym)
7
+
8
+ # resolve default types on nil
9
+ # https://dry-rb.org/gems/dry-struct/1.0/recipes/#resolving-default-values-on-code-nil-code
10
+ transform_types do |type|
11
+ if type.default?
12
+ type.constructor do |value|
13
+ value.nil? ? Dry::Types::Undefined : value
14
+ end
15
+ else
16
+ type
17
+ end
18
+ end
19
+
20
+ # Until there's an easier way to convert a hash keys to string
21
+ # without using any external library, the performance penalty
22
+ # that comes from using `JSON.parse(hash.to_json)` is the
23
+ # better alternative to adding more dependencies to this project
24
+ def to_h(stringify_keys: false)
25
+ if stringify_keys
26
+ hash = super()
27
+ JSON.parse(hash.to_json)
28
+ else
29
+ super()
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/data/scene_datastore"
4
+
5
+ module XxxRename
6
+ module Data
7
+ class Config < Base
8
+ attribute :global do
9
+ attribute :female_actors_prefix, Types::String
10
+ attribute :male_actors_prefix, Types::String
11
+ attribute :actors_prefix, Types::String
12
+ attribute :title_prefix, Types::String
13
+ attribute :id_prefix, Types::String
14
+ attribute :output_format, Types::Array.of(Types::String)
15
+ end
16
+
17
+ attribute :site do
18
+ attribute? :adult_time, Data::AdultTimeConfig
19
+ attribute? :babes, Data::MgPremiumConfig
20
+ attribute? :blacked, Data::VixenMediaConfig
21
+ attribute? :blacked_raw, Data::VixenMediaConfig
22
+ attribute? :brazzers, Data::MgPremiumConfig
23
+ attribute? :digital_playground, Data::MgPremiumConfig
24
+ attribute? :elegant_angel, Data::ElegantAngelConfig
25
+ attribute? :evil_angel, Data::EvilAngelConfig
26
+ attribute? :goodporn, Data::GoodpornConfig
27
+ attribute? :jules_jordan, Data::JulesJordanMediaConfig
28
+ attribute? :manuel_ferrara, Data::JulesJordanMediaConfig
29
+ attribute? :mofos, Data::MgPremiumConfig
30
+ attribute? :naughty_america, Data::NaughtyAmericaConfig
31
+ attribute? :nf_busty, Data::NfBustyConfig
32
+ attribute? :reality_kings, Data::MgPremiumConfig
33
+ attribute? :stash, Data::StashDBConfig
34
+ attribute? :tushy, Data::VixenMediaConfig
35
+ attribute? :tushy_raw, Data::VixenMediaConfig
36
+ attribute? :twistys, Data::MgPremiumConfig
37
+ attribute? :vixen, Data::VixenMediaConfig
38
+ attribute? :whale_media, Data::WhaleMediaConfig
39
+ attribute? :wicked, Data::WickedConfig
40
+ attribute? :x_empire, Data::XEmpireConfig
41
+ attribute? :zero_tolerance, Data::ZeroToleranceConfig
42
+ end
43
+
44
+ attribute :stash_app do
45
+ attribute :url, Types::String.optional
46
+ attribute :api_token, Types::String.optional
47
+ end
48
+
49
+ attribute :generated_files_dir, Types::String
50
+ # CLI Flags
51
+ attribute :force_refresh_datastore, Types::Bool
52
+ attribute :force_refresh, Types::Bool
53
+ attribute :actions, Types::Array.of(Types::String)
54
+ attribute? :override_site, Types::String
55
+
56
+ def prefix_hash
57
+ {
58
+ female_actors_prefix: global.female_actors_prefix,
59
+ male_actors_prefix: global.male_actors_prefix,
60
+ actors_prefix: global.actors_prefix,
61
+ title_prefix: global.title_prefix,
62
+ id_prefix: global.id_prefix
63
+ }
64
+ end
65
+
66
+ def collection_tag_to_site_client
67
+ @collection_tag_to_site_client ||= {}.tap do |h|
68
+ site.attributes.each_pair do |site_name, site_config|
69
+ h[site_config.collection_tag] = site_name
70
+ end
71
+ end
72
+ end
73
+
74
+ def mutex
75
+ @mutex ||= Mutex.new
76
+ end
77
+
78
+ def scene_datastore
79
+ @scene_datastore ||=
80
+ begin
81
+ store = Data::SceneDatastore.new(File.join(generated_files_dir, "..")).store
82
+ SceneDatastoreQuery.new(store, mutex)
83
+ end
84
+ end
85
+
86
+ def output_recorder
87
+ @output_recorder ||=
88
+ begin
89
+ store = Data::OutputDatastore.new(generated_files_dir).store
90
+ datastore = Data::FileRenameOpDatastore.new(store, mutex)
91
+ datastore.migration_status = 0
92
+ datastore
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xxx_rename/file_utilities"
4
+
5
+ module XxxRename
6
+ module Data
7
+ class FileRenameOp < Base
8
+ include FileUtilities
9
+
10
+ attribute :key, Types::String
11
+ attribute :directory, Types::String
12
+ attribute :source_filename, Types::String
13
+ attribute :output_filename, Types::String
14
+ attribute :mtime, Types::Time.optional
15
+
16
+ def rename
17
+ Dir.chdir(directory) do
18
+ File.rename(source_filename, output_filename)
19
+
20
+ # This has been deprecated in favour of storing timestamps in filename
21
+ # File.utime(File.atime(output_filename), mtime, output_filename) if mtime
22
+
23
+ XxxRename.logger.info "[RENAME COMPLETE]".colorize(:blue)
24
+ XxxRename.logger.info "\t#{"WAS:".colorize(:light_magenta)} #{source_filename}"
25
+ XxxRename.logger.info "\t#{"NOW:".colorize(:green)} #{output_filename}"
26
+ true
27
+ end
28
+ end
29
+
30
+ def reverse_rename
31
+ Dir.chdir(directory) do
32
+ File.rename(output_filename, source_filename)
33
+
34
+ XxxRename.logger.info "[RENAME ROLLBACK COMPLETE]".colorize(:blue)
35
+ XxxRename.logger.info "\t#{"WAS:".colorize(:light_magenta)} #{output_filename}"
36
+ XxxRename.logger.info "\t#{"NOW:".colorize(:green)} #{source_filename}"
37
+ true
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml/store"
4
+
5
+ require "xxx_rename/data/query_interface"
6
+ require "xxx_rename/contract/file_rename_op_contract"
7
+ require "xxx_rename/data/file_rename_op"
8
+
9
+ module XxxRename
10
+ module Data
11
+ OUTPUT_KEY = "___RENAME_ACTIONS___"
12
+ STATUS_KEY = "___MIGRATION_STATUS___"
13
+ FAILURES_KEY = "___FAILURES___"
14
+
15
+ class OutputDatastore
16
+ attr_reader :store
17
+
18
+ OUTPUT = "output"
19
+
20
+ def initialize(dir)
21
+ file = "#{OUTPUT}_#{Time.now.strftime("%Y%m%d%H%M")}.yml"
22
+
23
+ Dir.chdir(dir) { FileUtils.mkdir OUTPUT unless Dir.exist?(OUTPUT) }
24
+
25
+ path = File.join(dir, OUTPUT, file)
26
+ @store = YAML::Store.new path
27
+ XxxRename.logger.info "Output will be recorded in #{file}"
28
+ end
29
+ end
30
+
31
+ class FileRenameOpDatastore < QueryInterface
32
+ def create!(scene_data, source_file, output_file, dir = Dir.pwd)
33
+ hash = {
34
+ key: scene_data.key,
35
+ directory: dir,
36
+ source_filename: source_file,
37
+ output_filename: output_file,
38
+ mtime: scene_data.date_released
39
+ }
40
+ data = make_file_rename_op!(hash)
41
+ semaphore.synchronize do
42
+ store.transaction do
43
+ store[OUTPUT_KEY] ||= []
44
+ store[OUTPUT_KEY] << data
45
+ end
46
+ end
47
+ end
48
+
49
+ def length
50
+ semaphore.synchronize do
51
+ store.transaction(true) do
52
+ store.fetch(OUTPUT_KEY, []).length
53
+ end
54
+ end
55
+ end
56
+
57
+ def all
58
+ semaphore.synchronize do
59
+ store.transaction(true) do
60
+ store.fetch(OUTPUT_KEY, [])
61
+ end
62
+ end
63
+ end
64
+
65
+ def failures
66
+ semaphore.synchronize do
67
+ store.transaction(true) do
68
+ store.fetch(FAILURES_KEY, {})
69
+ end
70
+ end
71
+ end
72
+
73
+ def add_failure(key, error)
74
+ semaphore.synchronize do
75
+ store.transaction do
76
+ store[FAILURES_KEY] ||= {}
77
+ store[FAILURES_KEY][key] = error
78
+ end
79
+ end
80
+ end
81
+
82
+ def migration_status
83
+ semaphore.synchronize do
84
+ store.transaction(true) do
85
+ val = store.fetch(STATUS_KEY, 0)
86
+ val == 1
87
+ end
88
+ end
89
+ end
90
+
91
+ def migration_status=(status)
92
+ semaphore.synchronize do
93
+ store.transaction do
94
+ store[STATUS_KEY] = status
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def make_file_rename_op!(hash)
102
+ contract = Contract::FileRenameOpContract.new.call(hash)
103
+
104
+ raise Contract::FileRenameOpValidationFailure, contract.errors.to_h unless contract.errors.empty?
105
+
106
+ valid_hash = contract.to_h.transform_keys(&:to_s)
107
+ FileRenameOp.new(valid_hash)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pstore"
4
+
5
+ module XxxRename
6
+ module Data
7
+ class NaughtyAmericaDatabase
8
+ attr_reader :store
9
+
10
+ DEFAULT_STORE_FILE = "naughtyamerica.store"
11
+
12
+ def initialize(store_file)
13
+ store_file = DEFAULT_STORE_FILE if store_file.nil?
14
+
15
+ path = File.join(Dir.pwd, store_file)
16
+ XxxRename.logger.info "Initialising database in #{path}"
17
+
18
+ @store = PStore.new path
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XxxRename
4
+ module Data
5
+ class QueryError < StandardError
6
+ def initialize(scene_data)
7
+ @scene_data = scene_data
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class UniqueRecordViolation < QueryError
13
+ def message
14
+ "Scene already present in database: #{@scene_data.inspect}"
15
+ end
16
+ end
17
+
18
+ class RecordNotFound < QueryError
19
+ def message
20
+ "Scene not saved in database #{@scene_data.inspect}"
21
+ end
22
+ end
23
+
24
+ class QueryInterface
25
+ # @param [PStore] store
26
+ # @param [Mutex] semaphore
27
+ def initialize(store, semaphore)
28
+ @store = store
29
+ @semaphore = semaphore
30
+ end
31
+
32
+ def find(*)
33
+ raise "Not Implemented"
34
+ end
35
+
36
+ def create!(*)
37
+ raise "Not Implemented"
38
+ end
39
+
40
+ def upsert(*)
41
+ raise "Not Implemented"
42
+ end
43
+
44
+ def destroy!(*)
45
+ raise "Not Implemented"
46
+ end
47
+
48
+ def count(*)
49
+ raise "Not Implemented"
50
+ end
51
+
52
+ def metadata(*)
53
+ raise "Not Implemented"
54
+ end
55
+
56
+ def update_metadata(*)
57
+ raise "Not Implemented"
58
+ end
59
+
60
+ # @param [Array[String|Integer]] strs
61
+ def generate_lookup_key(*strs)
62
+ strs.map(&:to_s).map { |x| sanitize(x) }.join("$")
63
+ end
64
+
65
+ attr_reader :store
66
+
67
+ private
68
+
69
+ attr_reader :semaphore
70
+
71
+ # @param [String] str
72
+ # @return [String]
73
+ def sanitize(str)
74
+ str.to_s.normalize
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module XxxRename
6
+ module Data
7
+ class SceneData < Base
8
+ attribute :female_actors, Types::Array.of(Types::String).default([].freeze)
9
+ attribute :male_actors, Types::Array.of(Types::String).default([].freeze)
10
+ attribute :actors, Types::Array.of(Types::String)
11
+ attribute :collection, Types::String.default("")
12
+ attribute :collection_tag, Types::String.default("")
13
+ attribute :title, Types::String
14
+ attribute? :id, Types::Coercible::String.optional
15
+ attribute? :date_released, Types::Time.optional
16
+ attribute :scene_link, Types::String.default("")
17
+ attribute :original_filenames, Types::Set.default(Set.new.freeze)
18
+ attribute? :movie do
19
+ attribute :name, Types::String
20
+ attribute? :date, Types::Time
21
+ attribute? :url, Types::String
22
+ attribute :front_image, Types::String
23
+ attribute? :back_image, Types::String
24
+ attribute? :studio, Types::String
25
+ attribute? :synopsis, Types::String
26
+ end
27
+
28
+ def yyyy_mm_dd
29
+ date_released&.strftime("%Y_%m_%d")
30
+ end
31
+
32
+ def day
33
+ d = date_released&.day.to_s
34
+ return if d.empty?
35
+
36
+ d.length == 1 ? "0#{d}" : d
37
+ end
38
+ alias dd day
39
+
40
+ def month
41
+ m = date_released&.month.to_s
42
+ return if m.empty?
43
+
44
+ m.length == 1 ? "0#{m}" : m
45
+ end
46
+ alias mm month
47
+
48
+ def year
49
+ date_released&.year&.to_s
50
+ end
51
+ alias yyyy year
52
+
53
+ #
54
+ # Takes a scene information and uses as much information
55
+ # as possible to generate a unique key to minimize the
56
+ # chances of key collision
57
+ #
58
+ # @return [String]
59
+ def key
60
+ @key ||=
61
+ begin
62
+ str = "<#{title.normalize}" \
63
+ "$#{collection.to_s.normalize}" \
64
+ "$#{actors.join("-").normalize}" \
65
+ ">"
66
+ Digest::MD5.hexdigest(str)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end