movie_organizer 0.1.0

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 (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,43 @@
1
+ # frozen_string_literal: true
2
+ # coding: utf-8
3
+
4
+ lib = File.expand_path('../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'movie_organizer/version'
7
+
8
+ # rubocop:disable Metrics/BlockLength
9
+ Gem::Specification.new do |spec|
10
+ spec.name = 'movie_organizer'
11
+ spec.version = MovieOrganizer::VERSION
12
+ spec.authors = ['Chris Blackburn']
13
+ spec.email = ['87a1779b@opayq.com']
14
+ spec.summary = 'Automatically organize movies, tv shows and home videos.'
15
+ spec.description = spec.summary
16
+ spec.homepage = 'https://github.com/midwire/movie_organizer'
17
+ spec.license = 'MIT'
18
+
19
+ spec.required_ruby_version = '>= 2.3.0'
20
+ spec.files = `git ls-files -z`.split("\x0")
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.7'
26
+ spec.add_development_dependency 'faker'
27
+ spec.add_development_dependency 'midwire_common', '~> 1.1'
28
+ spec.add_development_dependency 'pry', '~> 0.10'
29
+ spec.add_development_dependency 'pry-nav', '~> 0.2'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.2'
32
+ spec.add_development_dependency 'rubocop'
33
+ spec.add_development_dependency 'simplecov'
34
+
35
+ spec.add_runtime_dependency 'colored'
36
+ spec.add_runtime_dependency 'dotenv', '~> 1.0'
37
+ spec.add_runtime_dependency 'mime-types'
38
+ spec.add_runtime_dependency 'net-scp', '~> 1.2'
39
+ spec.add_runtime_dependency 'streamio-ffmpeg'
40
+ spec.add_runtime_dependency 'titleize', '~> 1.3'
41
+ spec.add_runtime_dependency 'trollop'
42
+ end
43
+ # rubocop:enable Metrics/BlockLength
File without changes
@@ -0,0 +1,19 @@
1
+ ---
2
+ :eztv_host: https://eztv.ag/
3
+ :space_warning: 20GB
4
+ :tv_shows:
5
+ :directory: "/Volumes/Genesis/TV Series"
6
+ :my_shows:
7
+ - Arrow
8
+ - Fear the Walking Dead
9
+ - Last Man Standing
10
+ - Star Trek
11
+ - The Flash 2014
12
+ - The Last Ship
13
+ - The Walking Dead
14
+ - Vikings
15
+ - The Blacklist
16
+ :movies:
17
+ :directory: "/Users/cblackburn/Code/ruby/Gems/movie_organizer/tmp/files/movies"
18
+ :new_media_directories:
19
+ - "/Users/djohnson/Downloads"
@@ -0,0 +1,15 @@
1
+ ---
2
+ :eztv_host: https://eztv.ag/
3
+ :space_warning: 20GB
4
+ :tv_shows:
5
+ :directory: "/Volumes/Genesis/TV Series"
6
+ :my_shows:
7
+ - Fear the Walking Dead
8
+ - Last Man Standing
9
+ - Star Trek
10
+ - The Last Ship
11
+ - The Walking Dead
12
+ - Vikings
13
+ - The Blacklist
14
+ :movies:
15
+ :directory: "/Volumes/Qo'noS/Media/- Feature Films 3"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # rubocop:disable Metrics/BlockLength
6
+ module MovieOrganizer
7
+ RSpec.describe FileCopier, type: :lib do
8
+ let(:tmp_movie_src) { MovieOrganizer.root.join('spec', 'files', 'movies') }
9
+ let(:tmp_dest) { MovieOrganizer.root.join('tmp', 'files', 'movies') }
10
+ test_files = {}
11
+ Dir[MovieOrganizer.root.join('spec', 'files', 'movies', '**', '*')].each do |src|
12
+ next if File.directory?(src)
13
+ dir, filename = src.split('/')[-2..999]
14
+ dest = MovieOrganizer.root.join('tmp', 'files', 'movies').join(dir, filename)
15
+ test_files[src] = dest.to_s
16
+ end
17
+
18
+ before(:each) do
19
+ test_files.each_pair do |_src, dst|
20
+ FileUtils.rm_rf(File.dirname(dst))
21
+ end
22
+ end
23
+
24
+ context '#copy' do
25
+ context 'with a local target' do
26
+ test_files.each_pair do |src, dst|
27
+ it "copies [#{src}] to [#{dst}]" do
28
+ expect(File.exist?(dst)).to eq(false)
29
+ file_copier = FileCopier.new(src, dst, dry_run: false)
30
+ file_copier.copy
31
+ expect(File.exist?(dst)).to eq(true)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ module MovieOrganizer
4
+ RSpec.describe Logger, type: :lib do
5
+
6
+ let(:buffer) { StringIO.new }
7
+ let(:default_logger) { ::Logger.new(buffer) }
8
+ let(:logger) { Logger.send(:new, default_logger) }
9
+ let(:data) { { key: 'value' } }
10
+
11
+ context '.log_exception' do
12
+ it 'logs a formatted exception' do
13
+ exception = StandardError.new('bogus')
14
+ exception.set_backtrace(caller)
15
+ expect(buffer.string).to be_empty
16
+ logger.log_exception(exception, data)
17
+ expect(buffer.string).to match(/StandardError\s+:\s+bogus/)
18
+ end
19
+ end
20
+
21
+ context '.method_missing' do
22
+ it 'does not call bad_method' do
23
+ expect { logger.bogus }.to raise_error(NoMethodError)
24
+ end
25
+ end
26
+
27
+ context '.respond_to?' do
28
+ it 'does not respond to bad_method' do
29
+ expect(logger.respond_to?(:bad_method)).to eq(false)
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module MovieOrganizer
5
+ RSpec.describe MediaList, type: :lib do
6
+ include_context 'media_shared'
7
+
8
+ let(:media_list) { MediaList.new([tmpdir]) }
9
+
10
+ context '#new' do
11
+ %w(m4v mov mkv mp4 avi).each do |extension|
12
+ it "includes #{extension} files" do
13
+ create_test_file(count: 3, extension: extension)
14
+ expect(media_list.file_collection.count).to eq(3)
15
+ end
16
+ end
17
+ end
18
+
19
+ context '.video?' do
20
+ it 'returns true if passed filename has a video extension' do
21
+ expect(media_list.video?('bogus.mp4')).to eq(true)
22
+ end
23
+
24
+ it "returns false if passed filename isn't a video extension" do
25
+ expect(media_list.video?('bogus.mp3')).to eq(false)
26
+ end
27
+ end
28
+
29
+ context '.subtitle?' do
30
+ it 'returns true if passed filename has a subtitle extension' do
31
+ expect(media_list.subtitle?('bogus.sub')).to eq(true)
32
+ expect(media_list.subtitle?('bogus.srt')).to eq(true)
33
+ end
34
+
35
+ it "returns false if passed filename isn't a subtitle extension" do
36
+ expect(media_list.subtitle?('bogus.mp3')).to eq(false)
37
+ end
38
+ end
39
+
40
+ context '.media?' do
41
+ it 'returns true if passed filename has a media extension' do
42
+ expect(media_list.media?('bogus.srt')).to eq(true)
43
+ expect(media_list.media?('bogus.mov')).to eq(true)
44
+ expect(media_list.media?('bogus.mp3')).to eq(false)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module MovieOrganizer
5
+ RSpec.describe Media, type: :lib do
6
+ include_context 'media_shared'
7
+
8
+ context '.subtype' do
9
+ tv_shows = {
10
+ 'SnnEnn' => 'The.Walking.Dead.S04E08.HDTV.x264-2HD',
11
+ 'nxn' => 'The.Walking.Dead.4x0.HDTV.x264-2HD',
12
+ 'nnxn' => 'The.Walking.Dead.12x8.HDTV.x264-2HD',
13
+ 'nnxnn' => 'The.Walking.Dead.12x08.HDTV.x264-2HD',
14
+ 'nxnn' => 'The.Walking.Dead.2x18.HDTV.x264-2HD'
15
+ }
16
+
17
+ tv_shows.each_pair do |syntax, filename|
18
+ it "returns a TvShow with season and episode syntax: #{syntax}" do
19
+ filename = create_test_file(
20
+ filename: filename,
21
+ extension: 'mp4'
22
+ ).first
23
+ expect(Media.subtype(filename, default_options)).to be_a(TvShow)
24
+ end
25
+ end
26
+
27
+ movies = [
28
+ 'Coco.2017.1080p.BluRay.x264-[BOGUS.IT]',
29
+ 'Beetlejuice',
30
+ 'Justice.League.2017.1080p.BluRay.x264-[YTS.AM]'
31
+ ]
32
+
33
+ movies.each do |filename|
34
+ it "returns a Movie for '#{filename}'" do
35
+ expect(Tmdb::Movie).to receive(:find).at_least(1).times.and_return([1])
36
+ filename = create_test_file(
37
+ filename: filename
38
+ ).first
39
+ expect(Media.subtype(filename, default_options)).to be_a(Movie)
40
+ end
41
+ end
42
+
43
+ videos = [
44
+ 'IMG_2052',
45
+ 'IMG_3322',
46
+ 'VID_3322',
47
+ 'VIDEO_3322',
48
+ 'Our Summer Vacation',
49
+ 'Tom and Jerry on the Beach'
50
+ ]
51
+
52
+ videos.each do |filename|
53
+ it "returns a Video for '#{filename}'" do
54
+ expect(Tmdb::Movie).to receive(:find).at_least(1).times.and_return([])
55
+ filename = create_test_file(
56
+ filename: filename
57
+ ).first
58
+ expect(Media.subtype(filename, default_options)).to be_a(Video)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module MovieOrganizer
5
+ RSpec.describe Movie, type: :lib do
6
+ include_context 'media_shared'
7
+
8
+ let(:filename) do
9
+ create_test_file(
10
+ filename: 'Foreign+Correspondent+(1940)+1080p', extension: 'mp4'
11
+ ).first
12
+ end
13
+ let(:movie) { Movie.new(filename, default_options) }
14
+
15
+ context '#new' do
16
+ it 'returns a child of the Media class' do
17
+ expect(movie).to be_a(Media)
18
+ end
19
+
20
+ it 'is a Movie' do
21
+ expect(movie).to be_a(Movie)
22
+ end
23
+
24
+ it 'sets filename accessor' do
25
+ expect(movie.filename).to eq(filename)
26
+ end
27
+ end
28
+
29
+ context '.processed_filename' do
30
+ it 'correctly processes the filename' do
31
+ expect(
32
+ movie.processed_filename
33
+ ).to eq('Foreign Correspondent (1940).mp4')
34
+ end
35
+ end
36
+
37
+ context '.year' do
38
+ it 'returns the year' do
39
+ expect(movie.year).to eq('1940')
40
+ end
41
+ end
42
+
43
+ context '.process!' do
44
+ it 'moves the file to the configured location' do
45
+ settings = Settings.new
46
+ settings[:movies][:directory] = MovieOrganizer.root.join('tmp', 'files', 'movies').to_s
47
+ settings.save
48
+ movie.process!
49
+ expect(File.exist?(filename)).to eq(true)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,35 @@
1
+ module MovieOrganizer
2
+ RSpec.describe Organizer, type: :lib do
3
+ include_context 'media_shared'
4
+
5
+ let(:organizer) { Organizer.instance }
6
+
7
+ context '#instance' do
8
+ it "calls 'initialize'" do
9
+ expect(Organizer).to receive(:new)
10
+ organizer
11
+ end
12
+
13
+ it 'instantiates the logger' do
14
+ organizer.logger
15
+ end
16
+ end
17
+
18
+ context '.start' do
19
+ let(:args) do
20
+ {
21
+ source_dir: tmpdir, help: false, source_dir_given: true
22
+ }
23
+ end
24
+
25
+ before do
26
+ create_test_file(filename: 'Beetlejuice', extension: 'mp4')
27
+ end
28
+
29
+ # it 'collects the arguments' do
30
+ # expect(organizer).to receive(:collect_args).and_return(args)
31
+ # organizer.start
32
+ # end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module MovieOrganizer
5
+ RSpec.describe TvShow, type: :lib do
6
+ include_context 'media_shared'
7
+
8
+ let(:filename) do
9
+ create_test_file(
10
+ filename: 'The.Walking.Dead.S05E02.720p.HDTV.x264-KILLERS',
11
+ extension: 'mkv'
12
+ ).first
13
+ end
14
+ let(:tv_show) { TvShow.new(filename, default_options) }
15
+
16
+ context '#new' do
17
+ it 'returns a child of the Media class' do
18
+ expect(tv_show).to be_a(Media)
19
+ end
20
+
21
+ it 'is a TvShow' do
22
+ expect(tv_show).to be_a(TvShow)
23
+ end
24
+
25
+ it 'sets filename accessor' do
26
+ expect(tv_show.filename).to eq(filename)
27
+ end
28
+ end
29
+
30
+ context '.processed_filename' do
31
+ file = File.join(MovieOrganizer.root, 'spec', 'support', 'filename_mappings.yml')
32
+
33
+ context 'correctly maps the following with preserve_episode_name == false' do
34
+ mapped = YAML.load_file(file)['tvshows_no_preserve']
35
+ mapped.each do |from, to|
36
+ it "maps '#{from}' to [#{to}]" do
37
+ tv_show = TvShow.new(
38
+ from,
39
+ default_options.merge(preserve_episode_name: false)
40
+ )
41
+ expect(
42
+ tv_show.processed_filename
43
+ ).to eq(to)
44
+ end
45
+ end
46
+ end
47
+
48
+ context 'correctly maps the following with preserve_episode_name == true' do
49
+ mapped = YAML.load_file(file)['tvshows_preserve']
50
+ mapped.each do |from, to|
51
+ it "maps '#{from}' to [#{to}]" do
52
+ tv_show = TvShow.new(
53
+ from,
54
+ default_options.merge(preserve_episode_name: true)
55
+ )
56
+ expect(
57
+ tv_show.processed_filename
58
+ ).to eq(to)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ context '.process!' do
65
+ it 'moves the file to the configured location' do
66
+ settings = Settings.new
67
+ binding.pry if settings.nil?
68
+ target_dir = File.join(
69
+ settings[:tv_shows][:directory],
70
+ tv_show.title,
71
+ "Season #{tv_show.season.sub(/^0+/, '')}"
72
+ )
73
+ expect(FileUtils).to receive(:mkdir_p).with(target_dir, noop: true).and_return(nil)
74
+ expect(FileUtils).to receive(:move).and_return(nil)
75
+ tv_show.process!
76
+ end
77
+ end
78
+ end
79
+ end
80
+ # rubocop:enable Metrics/BlockLength