movie_organizer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +75 -0
- data/CHANGELOG +3 -0
- data/Gemfile +9 -0
- data/Guardfile +37 -0
- data/LICENSE.txt +22 -0
- data/README.md +81 -0
- data/Rakefile +14 -0
- data/bin/movie_organizer +9 -0
- data/lib/movie_organizer.rb +58 -0
- data/lib/movie_organizer/file_copier.rb +69 -0
- data/lib/movie_organizer/logger.rb +47 -0
- data/lib/movie_organizer/media.rb +95 -0
- data/lib/movie_organizer/media_list.rb +31 -0
- data/lib/movie_organizer/movie.rb +41 -0
- data/lib/movie_organizer/organizer.rb +68 -0
- data/lib/movie_organizer/settings.rb +47 -0
- data/lib/movie_organizer/string.rb +13 -0
- data/lib/movie_organizer/tv_show.rb +96 -0
- data/lib/movie_organizer/version.rb +5 -0
- data/lib/movie_organizer/video.rb +47 -0
- data/movie_organizer.gemspec +43 -0
- data/spec/files/The.Walking.Dead.S04E08.HDTV.x264-2HD.mp4 +0 -0
- data/spec/files/movies/Dunkirk.2017.BluRay.1080p/Dunkirk.2017.BluRay.1080p.mp4 +0 -0
- 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
- data/spec/files/short_video.mp4 +0 -0
- data/spec/fixtures/.blank_settings.yml +0 -0
- data/spec/fixtures/.movie_organizer.yml +19 -0
- data/spec/fixtures/.no_source_directories.yml +15 -0
- data/spec/lib/movie_organizer/file_copier_spec.rb +38 -0
- data/spec/lib/movie_organizer/logger_spec.rb +34 -0
- data/spec/lib/movie_organizer/media_list_spec.rb +49 -0
- data/spec/lib/movie_organizer/media_spec.rb +64 -0
- data/spec/lib/movie_organizer/movie_spec.rb +54 -0
- data/spec/lib/movie_organizer/organizer_spec.rb +35 -0
- data/spec/lib/movie_organizer/tv_show_spec.rb +80 -0
- data/spec/lib/movie_organizer_spec.rb +66 -0
- data/spec/spec_helper.rb +94 -0
- data/spec/support/filename_mappings.yml +10 -0
- data/spec/support/shared_contexts/media_shared.rb +50 -0
- data/tmdb-logo-primary-green.png +0 -0
- metadata +331 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -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
|
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/movie_organizer
ADDED
@@ -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
|