├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md └── duplicates.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'plex-ruby' 3 | gem 'colored' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | colored (1.2) 5 | mini_portile (0.5.2) 6 | nokogiri (1.6.1) 7 | mini_portile (~> 0.5.0) 8 | plex-ruby (1.5.1) 9 | nokogiri 10 | 11 | PLATFORMS 12 | ruby 13 | 14 | DEPENDENCIES 15 | colored 16 | plex-ruby 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 by Patrick Stadler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plex Duplicates 2 | 3 | Scan your [Plex Media Server](http://www.plexapp.com/) for duplicate files. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | # Install dependencies 9 | $ bundle install 10 | 11 | # Scan 12 | $ ./duplicates.rb hostname[:port] 13 | ``` 14 | -------------------------------------------------------------------------------- /duplicates.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'bundler' 4 | Bundler.require(:default, (ENV['RACK_ENV'] ||= :development.to_s).to_sym) 5 | 6 | host, port = (ARGV[0] || '').split(':') 7 | host ||= 'localhost' 8 | port ||= 32400 9 | 10 | def pretty_filesize n 11 | count = 0 12 | while n >= 1024 and count < 4 13 | n /= 1024.0 14 | count += 1 15 | end 16 | format("%.2f",n) + %w(B KB MB GB TB)[count] 17 | end 18 | 19 | puts "Connecting to #{host}:#{port}" 20 | server = Plex::Server.new(host, port) 21 | 22 | server.library.sections.each do |section| 23 | case section.type 24 | when 'show' then 25 | videos = [] 26 | section.all.each do |s| 27 | s.seasons.each do |m| 28 | m.episodes.each do |e| 29 | videos << e 30 | end 31 | end 32 | end 33 | when 'movie' then 34 | videos = section.all 35 | else 36 | next 37 | end 38 | 39 | print "Analyzing #{section.title.white}: 0/#{videos.length}" 40 | out = '' 41 | videos.each_with_index do |item, index| 42 | if not ['movie', 'episode'].include?(item.type) 43 | next 44 | end 45 | 46 | medias = item.medias 47 | if medias.length > 1 48 | out << "#{item.grandparent_title + ': ' if item.type == 'episode'}#{item.title}\n".white 49 | last_dir = nil 50 | medias.each do |m| 51 | m.parts.each do |part| 52 | dir = File.dirname(part.file) 53 | name = File.basename(part.file) 54 | size = pretty_filesize(part.size.to_i) 55 | unless last_dir.nil? or last_dir == dir 56 | out << " #{dir.red}/#{name} (#{size})\n" 57 | else 58 | out << " #{part.file} (#{size})\n" 59 | end 60 | last_dir = dir 61 | end 62 | end 63 | end 64 | print "\rAnalyzing #{section.title.white}: #{index+1}/#{videos.length}" 65 | end 66 | puts "\n\n#{out}\n" 67 | end 68 | 69 | --------------------------------------------------------------------------------