plexify 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.plexify.yml ADDED
@@ -0,0 +1,4 @@
1
+ plexify:
2
+ tmdb:
3
+ api_key: a1d640d24a78fb94c297cac5483ba31e
4
+ default_language: fr
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in plexify.gemspec
4
+ gemspec
5
+
6
+ gem "ruby-tmdb", ">= 0.2.1"
7
+ gem "colorize", ">= 0.5.8"
8
+ gem "text", ">= 1.2.1"
9
+ gem "ruby-progressbar", ">= 1.0.2"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Pierre FILSTROFF
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,31 @@
1
+ # Plexify
2
+
3
+ Currently in development. Not working yet.
4
+
5
+ TODO: Write a gem description
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'plexify'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install plexify
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/plexify ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
4
+ require "plexify"
5
+
6
+ plexify = Plexify::Plexify.new(ARGV)
7
+ plexify.run!
@@ -0,0 +1,170 @@
1
+ module Plexify
2
+ module Execute
3
+ class Plexify
4
+ # Plexify::Execute initialize method
5
+ def initialize(args)
6
+ @args = args
7
+ @options = {}
8
+ @missing_args = []
9
+ end
10
+
11
+ # Parses the command-line arguments and execute files processing
12
+ def parse!
13
+ begin
14
+ @opts = OptionParser.new(&method(:set_opts))
15
+ @opts.parse!(@args)
16
+ check_arguments
17
+
18
+ if @args.any?
19
+ @options[:source] = @args[0] unless @options[:source]
20
+ @options[:destination] = @args[1] unless @options[:destination]
21
+ end
22
+
23
+ process_result
24
+
25
+ # Puts missing arguments
26
+ rescue OptionParser::MissingArgument => e
27
+ built_string = e.message
28
+ for arg in @missing_args
29
+ built_string += arg
30
+ unless @missing_args.last == arg
31
+ built_string += ", "
32
+ else
33
+ built_string += "\n\n"
34
+ end
35
+ end
36
+ puts built_string
37
+ puts @opts
38
+
39
+ # Puts exception message
40
+ rescue Exception => e
41
+ raise e if @options[:trace] || e.is_a?(SystemExit)
42
+
43
+ print "\n#{e.class}: " unless e.class == RuntimeError
44
+ puts "#{e.message}"
45
+ puts " Use --trace for backtrace."
46
+ end
47
+ exit 0
48
+ end
49
+
50
+ private
51
+ # Check missing arguments
52
+ def check_arguments
53
+ return if @args.size == 2
54
+
55
+ @missing_args << "-s [source]" if !@options[:source] && !@args[0]
56
+ @missing_args << "-d [destination]" if !@options[:destination] && !@options[:movies_destination] && !@options[:series_destination]
57
+ @missing_args << "--movies-destination [destination]" if !@options[:destination] && !@options[:movies_destination] && !@options[:series_destination]
58
+ @missing_args << "--series-destination [destination]" if !@options[:destination] && !@options[:series_destination] && !@options[:movies_destination]
59
+
60
+ if @missing_args.any?
61
+ raise OptionParser::MissingArgument
62
+ end
63
+ end
64
+
65
+ # Set options definition for command-line execution
66
+ def set_opts(opts)
67
+ opts.banner = "Usage: plexify -s /media/temp --movies-destination /media/Movies --series-destination /media/Series\n\n"
68
+
69
+ opts.on('-s [source]', '--source [source]', 'Source folder which includes media files. Temp folder.') do |source|
70
+ @options[:source] = source
71
+ end
72
+
73
+ opts.on('-d [destination]', '--destination [dest]', 'Destination folder to put organized.') do |destination|
74
+ @options[:destination] = destination
75
+ end
76
+
77
+ opts.on('--movies-destination [dest]', 'Destination folder to put organized movies files.') do |movies_destination|
78
+ @options[:movies_destination] = movies_destination
79
+ end
80
+
81
+ opts.on('--series-destination [dest]', 'Destination folder to put organized series files.') do |series_destination|
82
+ @options[:series_destination] = series_destination
83
+ end
84
+
85
+ opts.on_tail('-t', '--test', 'Test files name searching in media databases: only print results.') do
86
+ @options[:test] = true
87
+ end
88
+
89
+ opts.on_tail('--accept-removing-all', :NONE, 'Accept removing source files. [NOT WORKING YET]') do
90
+ @options[:accept_removing_all] = true
91
+ end
92
+
93
+ opts.on_tail("-?", "-h", "--help", "Show this message.") do
94
+ puts opts
95
+ exit
96
+ end
97
+
98
+ opts.on_tail("-v", "--version", "Print version") do
99
+ puts("Plexify #{VERSION}")
100
+ exit
101
+ end
102
+
103
+ opts.on_tail('--trace', :NONE, 'Show a full traceback on error') do
104
+ @options[:trace] = true
105
+ end
106
+ end
107
+
108
+ # Process result
109
+ def process_result
110
+
111
+ Logger.info("=> Preparing source folder")
112
+ source = Prepare::Source.new(@options)
113
+ source.prepare
114
+ Logger.info(" : #{source.files.size} #{source.files.size > 1 ? 'files' : 'file'} found")
115
+
116
+ unless @options[:test]
117
+ Logger.info("\n=> Preparing destination folder")
118
+ destination = Prepare::Destination.new(@options)
119
+ destination.prepare
120
+ end
121
+
122
+ media_manager = MediaManager.new
123
+ media_organizer = MediaOrganizer.new(source, destination) unless @options[:test]
124
+
125
+ files_processed_count = 0
126
+
127
+ # Process files
128
+ for file in source.files
129
+ basename = File.basename(file.path)
130
+ Logger.info("\n\n=> Analyzing '#{basename}' file...")
131
+ medias = media_manager.analyze(file)
132
+
133
+ # Choose the good media
134
+ if medias.any?
135
+ Logger.info("\nFound medias :")
136
+
137
+ # If the test argument is present, don't move files or prompt the user
138
+ if @options[:test]
139
+ for media in medias
140
+ Logger.success("\n#{media.data.name} #{media.data.released ? "(#{Date.parse(media.data.released).year})" : nil}")
141
+ end
142
+ else
143
+ Logger.success("\n0: The movie isn't in the list, go next without doing anything.")
144
+ medias.each_with_index do |media, index|
145
+ Logger.success("\n#{index+1}: #{media.data.name} #{media.data.released ? "(#{Date.parse(media.data.released).year})" : nil}")
146
+ end
147
+
148
+ Logger.success("\nChoose the movie: ")
149
+ resp = STDIN.gets.chomp.to_i-1
150
+
151
+ if resp >= 0 && resp <= medias.size
152
+ media = medias[resp]
153
+ Logger.success("\nMedia found : #{media.data.name} #{media.data.released ? "(#{Date.parse(media.data.released).year})" : nil}")
154
+ media_organizer.move_to_destination(file, media)
155
+ files_processed_count += 1
156
+ elsif resp == -1
157
+ Logger.info("Aborting...")
158
+ next
159
+ end
160
+ end
161
+ else
162
+ Logger.error("\nMedia not found")
163
+ end
164
+ end
165
+
166
+ Logger.info("\n\n=> Files pocessed #{files_processed_count}/#{source.files.size}") unless @options[:test]
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,19 @@
1
+ module Plexify
2
+ # Logger class
3
+ class Logger
4
+ # print informations
5
+ def self.info(content)
6
+ print content
7
+ end
8
+
9
+ # print success in green
10
+ def self.success(content)
11
+ print content.colorize(:green)
12
+ end
13
+
14
+ # print errors in red
15
+ def self.error(content)
16
+ print content.colorize(:red)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Plexify
2
+ # Media class
3
+ # Will include more methods to test media data
4
+ class Media
5
+ attr_accessor :data
6
+ attr_accessor :found_title
7
+ attr_accessor :score
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,88 @@
1
+ module Plexify
2
+ class MediaManager
3
+ # Initialize variables and load tmdb media manager
4
+ def initialize
5
+ @tmdb_api_key = nil
6
+ @tmdb_default_language = nil
7
+ load_tmdb
8
+ end
9
+
10
+ # Search for a media in the Tmdb media manager database.
11
+ def analyze(file)
12
+ name = File.basename(file.path, File.extname(file.path))
13
+ splitted_name = name.split(/\W/).map(&:downcase)
14
+
15
+ medias = analyse_names(splitted_name, {})
16
+
17
+ flattened_medias = []
18
+ medias.each_pair do |title, media|
19
+ flattened_medias << media if flattened_medias.empty?
20
+ unless flattened_medias.map(&:data).map(&:id).include?(media.data.id)
21
+ media.found_title = title
22
+ flattened_medias << media
23
+ end
24
+ end
25
+
26
+ return flattened_medias
27
+ end
28
+
29
+ private
30
+ # Load tmdb media manager
31
+ def load_tmdb
32
+ load_plexify_config
33
+ Tmdb.api_key = @tmdb_api_key
34
+ Tmdb.default_language = @tmdb_default_language
35
+ end
36
+
37
+ # Load plexify config yaml file in the home directory
38
+ # The file structure is :
39
+ # plexify:
40
+ # tmdb:
41
+ # api_key: xxxx
42
+ # default_language: en
43
+ def load_plexify_config
44
+ config_file_path = File.join(File.dirname(__FILE__), "../../.plexify.yml")
45
+ config_file = File.open config_file_path
46
+ yaml_config = YAML::load(config_file)
47
+
48
+ tmdb_config = yaml_config["plexify"]["tmdb"]
49
+ @tmdb_api_key = tmdb_config["api_key"]
50
+ @tmdb_default_language = tmdb_config["default_language"]
51
+ end
52
+
53
+ def analyse_names(list, medias)
54
+ title = String.new
55
+
56
+ list.each_with_index do |str, index|
57
+ title << " " if title.length > 0
58
+ title << str
59
+ media = find(title)
60
+
61
+ if media.empty?
62
+ if medias.any?
63
+ shift = list.shift
64
+ analyse_names(list, medias)
65
+ end
66
+ else
67
+ medias[title] = Media.new(media)
68
+ end
69
+ end
70
+ return medias
71
+ end
72
+
73
+ # Get Levenshtein distance
74
+ def levenshtein_distance(first, second)
75
+ Text::Levenshtein.distance(first, second)
76
+ end
77
+
78
+ # Find a movie in the Tmdb database with a title and a limit of one item
79
+ def find(title)
80
+ TmdbMovie.find(:title => title, :limit => 1)
81
+ end
82
+
83
+ # Find all movies in the Tmdb database with a title
84
+ def find_all(title)
85
+ TmdbMovie.find(:title => title)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,71 @@
1
+ module Plexify
2
+ # Class MediaOrganizer
3
+ # It manage source and destination folders
4
+ class MediaOrganizer
5
+ attr_reader :source
6
+ attr_reader :destination
7
+
8
+ # Initialize
9
+ def initialize(source, destination)
10
+ @source = source
11
+ @destination = destination
12
+ end
13
+
14
+ # Move a file the his appropriate destination folder
15
+ def move_to_destination(file, media)
16
+ # Consider media is a movie
17
+ destination = @destination.movies_folder.path
18
+
19
+ # Create an appropriate media folder
20
+ media_folder_name = "#{media.data.name} (#{Date.parse(media.data.released).year})"
21
+ media_folder_path = File.join(destination, media_folder_name)
22
+ file_basename = File.basename(file.path)
23
+
24
+ # Create the movie folder
25
+ FileUtils.mkdir(media_folder_path) unless File.exists?(media_folder_path)
26
+ media_folder = File.open(media_folder_path)
27
+
28
+ if media_folder
29
+ renamed_file_basename = media_folder_name + File.extname(file_basename)
30
+ renamed_file_path = File.join(media_folder.path, renamed_file_basename)
31
+
32
+ # Copy the file with a progress bar
33
+ Logger.info("\nCopying file to '#{renamed_file_path}'")
34
+ copy_with_progressbar(file.path, renamed_file_path)
35
+ Logger.success("File '#{renamed_file_path}' copied") if File.exists?(renamed_file_path)
36
+ end
37
+
38
+ Logger.info("\nDo you want to remove the source file '#{file_basename}' ? (y/n) ")
39
+ resp = STDIN.gets.chomp
40
+
41
+ if resp.eql?("y")
42
+ FileUtils.rm file.path
43
+ Logger.success("File '#{file_basename}' removed")
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Copy a file with a great progress bar
50
+ def copy_with_progressbar(source, destination)
51
+ source_file = File.new(source, "r")
52
+ destination_file = File.new(destination, "w")
53
+ source_size = File.size(source)
54
+ batch_bytes = ( source_size / 100 ).ceil
55
+ total = 0
56
+
57
+ p_bar = ProgressBar.create(:title => "Copying '#{File.basename(source_file.path)}'", :format => '|%b>%i| %p%% %t')
58
+ buffer = source_file.sysread(batch_bytes)
59
+
60
+ while total < source_size do
61
+ destination_file.syswrite(buffer)
62
+ p_bar.progress = ((total*100)/source_size).ceil + 1
63
+ total += batch_bytes
64
+ if (source_size - total) < batch_bytes
65
+ batch_bytes = (source_size - total)
66
+ end
67
+ buffer = source_file.sysread(batch_bytes)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,91 @@
1
+ module Plexify
2
+ module Prepare
3
+ # Destination class implementation
4
+ class Destination
5
+ attr_accessor :folder
6
+ attr_accessor :movies_folder
7
+ attr_accessor :series_folder
8
+
9
+ # Plexify::Execute initialize method
10
+ def initialize(options)
11
+ @destination_path = options[:destination]
12
+ @movies_destination_path = options[:movies_destination]
13
+ @series_destination_path = options[:series_destination]
14
+ end
15
+
16
+ def prepare
17
+ if @destination_path
18
+ prepare_destination
19
+ else
20
+ prepare_destinations
21
+ end
22
+ end
23
+
24
+ protected
25
+ # Prepare destination
26
+ def prepare_destination
27
+ # Create destination folder
28
+ destination_path = @destination_path
29
+ @folder = create_or_use_folder(destination_path)
30
+
31
+ # Create "Movies" and "TV Shows" folder
32
+ @movies_folder = create_or_use_folder(destination_path, "Movies")
33
+ @series_folder = create_or_use_folder(destination_path, "TV Shows")
34
+ end
35
+
36
+ # Prepare destinations
37
+ def prepare_destinations
38
+ # Create or use movies and series folders
39
+ movies_path = @movies_destination_path
40
+ series_path = @series_destination_path
41
+
42
+ @movies_folder = create_or_use_folder(movies_path) if movies_path
43
+ @series_folder = create_or_use_folder(series_path) if series_path
44
+ end
45
+
46
+ # Create or use a folder
47
+ def create_or_use_folder(*args)
48
+ path = args.first
49
+
50
+ if args.size > 1
51
+ name = args[1]
52
+ end
53
+
54
+ folder_path = name ? File.join(path, name) : path
55
+
56
+ if File.exists?(folder_path) && File.directory?(folder_path)
57
+ folder = File.open(folder_path)
58
+ else
59
+ Logger.info("\n=> Created missing '#{name ? name : File.basename(folder_path)}' folder in destination folder.")
60
+ FileUtils.mkdir_p folder_path
61
+ folder = File.open(folder_path) if File.exists?(folder_path)
62
+ end
63
+
64
+ folder
65
+ end
66
+ end
67
+
68
+ # Source class implementation
69
+ class Source
70
+ attr_accessor :folder
71
+ attr_accessor :files
72
+
73
+ # Initialize
74
+ def initialize(options)
75
+ @source_path = options[:source]
76
+ @files = Array.new
77
+ end
78
+
79
+ # Prepare source folder
80
+ def prepare
81
+ @folder = File.new @source_path
82
+
83
+ if File.exists?(@folder.path) && File.directory?(@folder.path)
84
+ Dir.glob(@folder.path + "/**/*.{mkv,avi,mov}") do |file|
85
+ @files << File.open(file)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Plexify
2
+ VERSION = "0.1.0"
3
+ end
data/lib/plexify.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'fileutils'
2
+ require 'date'
3
+ require 'ruby-progressbar'
4
+ require 'optparse'
5
+ require 'colorize'
6
+ require 'ruby-tmdb'
7
+ require 'yaml'
8
+ require 'text'
9
+
10
+ Dir[File.dirname(__FILE__) + "/plexify/**/*"].each{ |file| require file }
11
+
12
+ module Plexify
13
+ class Plexify
14
+ def initialize(args)
15
+ @opts = Execute::Plexify.new(args)
16
+ end
17
+
18
+ # Run plexify !
19
+ def run!
20
+ @opts.parse!
21
+ end
22
+ end
23
+ end
data/plexify.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'plexify/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "plexify"
8
+ gem.version = Plexify::VERSION
9
+ gem.authors = ["Pierre FILSTROFF"]
10
+ gem.email = ["pfilstroff@gmail.com"]
11
+ gem.description = %q{Organize media folders to get ready with media managers}
12
+ gem.summary = %q{Media managers files organizer}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plexify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pierre FILSTROFF
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-03 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Organize media folders to get ready with media managers
15
+ email:
16
+ - pfilstroff@gmail.com
17
+ executables:
18
+ - plexify
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - .plexify.yml
24
+ - Gemfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - bin/plexify
29
+ - lib/plexify.rb
30
+ - lib/plexify/execute.rb
31
+ - lib/plexify/logger.rb
32
+ - lib/plexify/media.rb
33
+ - lib/plexify/media_manager.rb
34
+ - lib/plexify/media_organizer.rb
35
+ - lib/plexify/prepare.rb
36
+ - lib/plexify/version.rb
37
+ - plexify.gemspec
38
+ homepage: ''
39
+ licenses: []
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 1.8.24
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: Media managers files organizer
62
+ test_files: []