movie_organizer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +75 -0
  5. data/CHANGELOG +3 -0
  6. data/Gemfile +9 -0
  7. data/Guardfile +37 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +81 -0
  10. data/Rakefile +14 -0
  11. data/bin/movie_organizer +9 -0
  12. data/lib/movie_organizer.rb +58 -0
  13. data/lib/movie_organizer/file_copier.rb +69 -0
  14. data/lib/movie_organizer/logger.rb +47 -0
  15. data/lib/movie_organizer/media.rb +95 -0
  16. data/lib/movie_organizer/media_list.rb +31 -0
  17. data/lib/movie_organizer/movie.rb +41 -0
  18. data/lib/movie_organizer/organizer.rb +68 -0
  19. data/lib/movie_organizer/settings.rb +47 -0
  20. data/lib/movie_organizer/string.rb +13 -0
  21. data/lib/movie_organizer/tv_show.rb +96 -0
  22. data/lib/movie_organizer/version.rb +5 -0
  23. data/lib/movie_organizer/video.rb +47 -0
  24. data/movie_organizer.gemspec +43 -0
  25. data/spec/files/The.Walking.Dead.S04E08.HDTV.x264-2HD.mp4 +0 -0
  26. data/spec/files/movies/Dunkirk.2017.BluRay.1080p/Dunkirk.2017.BluRay.1080p.mp4 +0 -0
  27. data/spec/files/movies/The Matrix (1999) [BluRay] [1080p]/The Matrix (1999) [BluRay] [1080p].mp4 b/data/spec/files/movies/The Matrix (1999) [BluRay] [1080p]/The Matrix (1999) [BluRay] → [1080p].mp4 +0 -0
  28. data/spec/files/short_video.mp4 +0 -0
  29. data/spec/fixtures/.blank_settings.yml +0 -0
  30. data/spec/fixtures/.movie_organizer.yml +19 -0
  31. data/spec/fixtures/.no_source_directories.yml +15 -0
  32. data/spec/lib/movie_organizer/file_copier_spec.rb +38 -0
  33. data/spec/lib/movie_organizer/logger_spec.rb +34 -0
  34. data/spec/lib/movie_organizer/media_list_spec.rb +49 -0
  35. data/spec/lib/movie_organizer/media_spec.rb +64 -0
  36. data/spec/lib/movie_organizer/movie_spec.rb +54 -0
  37. data/spec/lib/movie_organizer/organizer_spec.rb +35 -0
  38. data/spec/lib/movie_organizer/tv_show_spec.rb +80 -0
  39. data/spec/lib/movie_organizer_spec.rb +66 -0
  40. data/spec/spec_helper.rb +94 -0
  41. data/spec/support/filename_mappings.yml +10 -0
  42. data/spec/support/shared_contexts/media_shared.rb +50 -0
  43. data/tmdb-logo-primary-green.png +0 -0
  44. metadata +331 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 56db1180e21190c3099cc45f75066d9a27c9fb34d74364ed64c87c22f229da18
4
+ data.tar.gz: 34a39c981eb1cc29a7fdff7bb7bbd53ea464b9d0ebf973685bfe716e5da824af
5
+ SHA512:
6
+ metadata.gz: 9fbd33cf4a225627e99925e09f5d4a0cb3e2f13cc24cc2ef7da4485547079f07996ab6531491d8c1025d7394b0766d501e58a2ab312fbf09919efcccd8fd2616
7
+ data.tar.gz: 3db9a4aecbf719ceb5185f2ad00f78c853bc13df6c0ce400d4c8f954baa75ae9a98a1a0eb5b8c74dfb45170a0fe0b4c6a944000824c60c634a25ecfb2fb91b55
@@ -0,0 +1,19 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .env*
16
+ /stubs
17
+ /vendor
18
+ /spec/.DS_Store
19
+ /.DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --format doc
@@ -0,0 +1,75 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
4
+ # Include gemspec and Rakefile
5
+ Include:
6
+ - '**/*.gemspec'
7
+ - '**/*.podspec'
8
+ - '**/*.jbuilder'
9
+ - '**/*.rake'
10
+ - '**/Gemfile'
11
+ - '**/Rakefile'
12
+ - '**/Capfile'
13
+ - '**/Guardfile'
14
+ - '**/Podfile'
15
+ - '**/Thorfile'
16
+ - '**/Vagrantfile'
17
+ Exclude:
18
+ - 'vendor/**/*'
19
+ - 'stubs/**/*'
20
+ - 'spec/support/shared_contexts/*'
21
+
22
+ # Checks formatting of special comments
23
+ CommentAnnotation:
24
+ Keywords:
25
+ - TODO
26
+ - FIXME
27
+ - OPTIMIZE
28
+ - HACK
29
+ - REVIEW
30
+
31
+ ########################################
32
+ # Style Cops
33
+
34
+ Style/Documentation:
35
+ Enabled: false
36
+
37
+ Style/FileName:
38
+ Enabled: false
39
+
40
+ Style/AlignParameters:
41
+ EnforcedStyle: with_fixed_indentation
42
+
43
+ Style/RegexpLiteral:
44
+ Enabled: false
45
+
46
+ Style/EmptyLinesAroundBlockBody:
47
+ Enabled: false
48
+
49
+ Style/RaiseArgs:
50
+ Enabled: false
51
+
52
+ Style/DoubleNegation:
53
+ Enabled: false
54
+
55
+ Style/PerlBackrefs:
56
+ Enabled: false
57
+
58
+ ########################################
59
+ # Lint Cops
60
+
61
+ Lint/Eval:
62
+ Enabled: false
63
+
64
+ ########################################
65
+ # Metrics Cops
66
+
67
+ Metrics/LineLength:
68
+ Max: 110
69
+
70
+ Metrics/MethodLength:
71
+ CountComments: false # count full line comments?
72
+ Max: 20
73
+
74
+ Metrics/ClassLength:
75
+ Max: 120
@@ -0,0 +1,3 @@
1
+ *0.0.1* (March 08, 2015)
2
+
3
+ * Initial Commit
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
+
6
+ gem 'themoviedb', github: 'midwire/themoviedb'
7
+
8
+ # Specify your gem's dependencies in movie_organizer.gemspec
9
+ gemspec
@@ -0,0 +1,37 @@
1
+ # rubocop:disable all
2
+ # clearing :on
3
+
4
+ guard :bundler do
5
+ require 'guard/bundler'
6
+ require 'guard/bundler/verify'
7
+ helper = Guard::Bundler::Verify.new
8
+
9
+ files = ['Gemfile']
10
+ files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
11
+
12
+ # Assume files are symlinked from somewhere
13
+ files.each { |file| watch(helper.real_path(file)) }
14
+ end
15
+
16
+ CMD = 'bundle exec rspec -f doc --color'
17
+
18
+ guard :rspec, cmd: CMD do
19
+ require 'guard/rspec/dsl'
20
+ dsl = Guard::RSpec::Dsl.new(self)
21
+
22
+ # RSpec files
23
+ rspec = dsl.rspec
24
+ watch(rspec.spec_helper) { rspec.spec_dir }
25
+ watch(rspec.spec_support) { rspec.spec_dir }
26
+ watch(rspec.spec_files)
27
+
28
+ # Ruby files
29
+ ruby = dsl.ruby
30
+ dsl.watch_spec_files_for(ruby.lib_files)
31
+ end
32
+
33
+ # guard :rubocop, cli: ['-D'] do
34
+ # watch(%r{.+\.rb$})
35
+ # watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
36
+ # end
37
+ # rubocop:enable all
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Chris Blackburn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,81 @@
1
+ # MovieOrganizer
2
+
3
+ Automatically organize movies, tv shows and home videos.
4
+
5
+ I use [Plex](https://www.plex.tv/) as the centerpiece of my home entertainment center, which requires that I rip my BluRay movies and TV shows, and copy them into a certain location so my [Plex server](https://www.plex.tv/downloads/) can recognize them.
6
+
7
+ MovieOrganizer makes the job of organizing my ripped movies, tv shows and home videos as simple as:
8
+
9
+ ```bash
10
+ movie_organizer
11
+
12
+ Starting MovieOrganizer...
13
+ Processing [/Users/midwire/Downloads/The Matrix (1990).mp4] - MovieOrganizer::Movie
14
+ target dir: [ssh://username@plex_server/media/movies/The Matrix (1990)]
15
+ target file: [ssh://username@plex_server/media/movies/The Matrix (1990)/The Matrix (1990).mp4]
16
+
17
+ ...
18
+ ```
19
+
20
+ ## Features
21
+
22
+ * Copies media to remote hosts
23
+ * Can differentiate between most Movies, TV Shows and Home Video if they are named properly, and copy each to the appropriately configured directory
24
+ * Uses [The Movie Database](https://www.themoviedb.org) for movie identification
25
+
26
+ ## Caveats
27
+
28
+ Note that this is beta software. I wrote it as a hack because I got tired of constantly copying my media over to Plex by hand. Having said that, it works but there are edge cases that are probably not covered. If you find a bug or problem, please file an issue and I'll address it ASAP.
29
+
30
+ ## Installation
31
+
32
+ $ gem install movie_organizer
33
+
34
+ ## Configuration
35
+
36
+ Sign up for an account at [The Movie Database](https://www.themoviedb.org/) and get an API key.
37
+
38
+ Create a file in your home directory named `.movie_organizer.yml` as follows:
39
+
40
+ ```yaml
41
+ ---
42
+ :space_warning: 20GB
43
+ :new_media_directories:
44
+ - "/Users/midwire/media_rips" # <- new media directory
45
+ :tv_shows:
46
+ :directory: "/Volumes/Genesis/TV Series" # <- a local directory
47
+ :movies:
48
+ :tmdb_key: df08efec9f01985d401a3cfedf5628a2 # <- use your own API key (this one is fake)
49
+ :directory: ssh://plex_admin@plex.local/media/media1/movies # <- remote directory
50
+ :videos:
51
+ :directory: "ssh://plex_admin@plex.local/media/media1/Family Videos" # <- remote directory
52
+ ```
53
+
54
+ **NOTE:** If you want to use a remote host, you will need to configure passwordless logins using your ssh-key, which is outside the scope of this document. If you don't know how, please do a search for
55
+
56
+ Remote hosts are specified in this format:
57
+
58
+ ```bash
59
+ ssh://username@hostname/path/to/remote/destination
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ You should be able to simply run `movie_organizer`
65
+
66
+ Here are the command line options:
67
+
68
+ ```bash
69
+ Options:
70
+ -s, --source-dir=<s> Source directories containing media files. Colon (:) separated. (Default: /Users/midwire/media_rips)
71
+ -d, --dry-run Do not actually move or copy files
72
+ -p, --preserve-episode-name Preserve episode names if they exist (experimental)
73
+ -v, --verbose Be verbose with output
74
+ -h, --help Show this message
75
+ ```
76
+
77
+ ## Attribution
78
+
79
+ ### The Movie Database
80
+
81
+ <img src="tmdb-logo-primary-green.png" alt="TMDB Logo" style="float:left; padding: 0 10px 0 0;"/> We are profoundly pleased to have access to [The Movie Database](https://www.themoviedb.org) API. This product uses the TMDb API but is not endorsed or certified by TMDb.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task default: :spec
8
+
9
+ begin
10
+ require 'midwire_common/rake_tasks'
11
+ rescue LoadError
12
+ puts ">>> Could not load 'midwire_common' gem."
13
+ exit
14
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ require 'movie_organizer'
7
+ require 'movie_organizer/organizer'
8
+
9
+ MovieOrganizer::Organizer.instance.start
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Create a .test.env and a .development.env for your different local
4
+ # environments
5
+ require 'dotenv'
6
+ require 'colored'
7
+ require 'readline'
8
+
9
+ paths = %W(.env .env.#{ENV['APP_ENV']}).map { |name| "#{Dir.pwd}/#{name}" }
10
+ Dotenv.load(*paths).each { |k, v| ENV[k] = v }
11
+
12
+ require 'movie_organizer/version'
13
+ require 'midwire_common/string'
14
+
15
+ module MovieOrganizer
16
+ def self.root
17
+ Pathname.new(File.dirname(__FILE__)).parent
18
+ end
19
+
20
+ def self.current_environment
21
+ ENV.fetch('APP_ENV', 'development')
22
+ end
23
+
24
+ def self.config_file(filename = '.movie_organizer.yml')
25
+ return root.join('spec', 'fixtures', filename) if current_environment == 'test'
26
+ #:nocov:
27
+ home = ENV.fetch('HOME')
28
+ file = ENV.fetch('MO_CONFIG_FILE', File.join(home, '.movie_organizer.yml'))
29
+ FileUtils.touch(file)
30
+ file
31
+ #:nocov:
32
+ end
33
+
34
+ def self.source_directories(settings = Settings.new, test_response = nil)
35
+ settings[:new_media_directories] || begin
36
+ strings = prompt_for('Media source directories (separated by a colon)', test_response)
37
+ strings.split(':')
38
+ end
39
+ end
40
+
41
+ #:nocov:
42
+ def self.prompt_for(message = '', test_response = nil)
43
+ prompt = "#{message.dup}\n? "
44
+ return test_response if test_response
45
+ Readline.readline(prompt, true).squeeze(' ').strip
46
+ end
47
+ #:nocov:
48
+
49
+ autoload :FileCopier, 'movie_organizer/file_copier'
50
+ autoload :Logger, 'movie_organizer/logger'
51
+ autoload :Media, 'movie_organizer/media'
52
+ autoload :MediaList, 'movie_organizer/media_list'
53
+ autoload :Movie, 'movie_organizer/movie'
54
+ autoload :Organizer, 'movie_organizer/organizer'
55
+ autoload :Settings, 'movie_organizer/settings'
56
+ autoload :TvShow, 'movie_organizer/tv_show'
57
+ autoload :Video, 'movie_organizer/video'
58
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MovieOrganizer
4
+ class FileCopier
5
+ attr_accessor :filename, :target_file, :options
6
+ attr_reader :username, :hostname, :remote_filename
7
+
8
+ def initialize(filename, target_file, options)
9
+ @filename = filename
10
+ @target_file = target_file
11
+ @options = options
12
+ end
13
+
14
+ def copy
15
+ ssh? ? remote_copy : local_copy
16
+ end
17
+
18
+ private
19
+
20
+ def local_copy
21
+ FileUtils.mkdir_p(File.dirname(target_file))
22
+ FileUtils.copy(
23
+ filename,
24
+ target_file,
25
+ noop: options[:dry_run]
26
+ )
27
+ end
28
+
29
+ def remote_copy
30
+ parse_target
31
+ return do_dry_run if options[:dry_run]
32
+ Net::SSH.start(hostname, username) do |ssh|
33
+ ssh.exec("mkdir -p '#{target_dir}'")
34
+ end
35
+ Net::SCP.start(hostname, username) do |scp|
36
+ scp.upload!(filename, remote_filename)
37
+ end
38
+ end
39
+
40
+ def do_dry_run
41
+ puts("Would remotely execute: [#{"mkdir -p '#{target_dir}'"}] on #{hostname}")
42
+ puts("Would execute: [#{"scp '#{filename}' '#{remote_filename}'"}]")
43
+ end
44
+
45
+ def target_dir
46
+ @target_dir ||= begin
47
+ parts = @remote_filename.split('/')
48
+ parts[0..parts.length - 2].join('/').to_s
49
+ end
50
+ end
51
+
52
+ def parse_target
53
+ return nil if @parse_target
54
+ @parse_target = true
55
+ temp ||= target_file.to_s.split('/')[2..99]
56
+ md = temp.join('/').match(/([\w\-\.]+)@([^\/]+)(\/.+)$/)
57
+ @username = md[1]
58
+ @hostname = md[2]
59
+ @remote_filename = md[3]
60
+ if @username.nil? || @hostname.nil? || @remote_filename.nil?
61
+ fail 'SSH path not formatted properly. Use [ssh://username@hostname/absolute/path]'
62
+ end
63
+ end
64
+
65
+ def ssh?
66
+ target_file.match?(/^ssh:/)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ require 'singleton'
2
+ require 'logger'
3
+
4
+ module MovieOrganizer
5
+ class Logger
6
+ include Singleton
7
+
8
+ attr_accessor :log_provider
9
+
10
+ def initialize(provider = default_logger)
11
+ @log_provider = provider
12
+ end
13
+
14
+ def log_exception(e, data = {})
15
+ msg = "EXCEPTION : #{e.class.name} : #{e.message}"
16
+ msg += "\n data : #{data.inspect}" if data && !data.empty?
17
+ msg += "\n #{e.backtrace[0, 6].join("\n ")}"
18
+ log_provider.error(msg)
19
+ end
20
+
21
+ def method_missing(meth, *args, &block)
22
+ if log_provider.respond_to?(meth)
23
+ log_provider.send(meth, *args, &block)
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def respond_to?(meth, include_private = false)
30
+ if log_provider.respond_to?(meth)
31
+ true
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def default_logger
40
+ logger = ::Logger.new(STDOUT)
41
+ logger.formatter = proc do |_, _, _, msg|
42
+ "#{msg}\n"
43
+ end
44
+ logger
45
+ end
46
+ end
47
+ end