Skip to content

Instantly share code, notes, and snippets.

@coneybeare
Created October 2, 2012 15:14
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save coneybeare/3819972 to your computer and use it in GitHub Desktop.
Save coneybeare/3819972 to your computer and use it in GitHub Desktop.
A script for Transmission that runs after a completed download. It scans for downloaded television shows then attempts to place them in the correct organizational directory on the local machine, removing the torrent as well.
#!/usr/bin/env ruby
require 'fileutils'
# SETUP
# Details about the local transmission service
transmission_remote_location = "/usr/sbin/transmission-remote"
transmission_server_port = "mini.local:9091"
# The place where downloaded but unsorted tv show torrents land
downloads_television_directory = "/Volumes/Drobo/Downloads/Television/"
# The final resingplace for shows - Assumes a directory structure of /Path/To/Television > [SHOW NAME] > [SEASON X] > [VIDEO FILE]
show_television_directory = "/Volumes/Drobo/Television/"
# Get the shows on the system
# Should correlate to a listing of the show directories on the system
shows = Dir.entries(show_television_directory).select{|d| File.directory?(show_television_directory + d)}.reject{|d| d[0] == "."}
# Words to ignore in the show titles
ignored_keywords = [ "the", "and", "an", "a", "in", "of" ]
# Files to ignore in the directory
ignored_files = [ ".DS_Store" ]
# Get a list of the files in here that are not folders
episodes = Dir.entries(downloads_television_directory).select { |f| File.file?(downloads_television_directory + f) }
# Iterate over each downloaded episode
episodes.each do |episode_name|
# skip the ignored files
next if ignored_files.include? episode_name
puts "\n----------------------------------------------------------------------------------------------------------------------------"
puts "Inspecting \"#{episode_name}...\""
# The following two checks only affect systems where the incomplete folder is not on the same drive as the final file folder
# This means that when the torrent is done, it has to be moved from one drive to the other, something that is not instaneous
# Check if the filesize is 0.
# This happens when the file has started to transfer but they filesystem hasnt actually moved anything yet.
downloaded_file = downloads_television_directory + episode_name
filesize_1 = File.size(downloaded_file);
if filesize_1 == 0
puts " -> Ignoring: File has not started transferring (filesize: #{filesize_1})"
next
end
# Check if the file is done transferring.
# This doesn't seem like the best way to do this, but in practice it works.
# Could produce a false positive if the system is hung or something else causes no data to transfer in 10 seconds
sleep(10)
filesize_2 = File.size(downloaded_file);
if filesize_2 > filesize_1
puts " -> Ignoring: File is not done transferring (#{filesize_2} > #{filesize_1} )"
next
end
# Try to figure out which show the file belongs to by ensuring that each word of the show is in the torrent.
# There may be a better way to do this using torrent metadata of some sort, but this seems to work for the way torrent names have
# kind of been standardized
matches = []
matched_show = nil
shows.each do |show|
matched = false
keywords = show.downcase.split.reject { |kw| ignored_keywords.include? kw.downcase }
match = keywords.all? { |kw| episode_name.downcase.include? kw }
if match
matches << show
end
end
if matches.length == 1
# A single match is good :)
matched_show = matches.first
elsif matches.length > 1
# multiple matches, need to select the right one, most likely is the one with the longer multiword title
# as usually false positives happen to something like "Girls" and "2 Broke Girls"
puts " -> Found multiple matches: #{matches}"
length_so_far = 0
matches.each do |show|
keywords = show.downcase.split.reject { |kw| ignored_keywords.include? kw.downcase }
if keywords.length > length_so_far
length_so_far = keywords.length
matched_show = show
end
end
end
# A nil matched show here does nothing. Will have to manually sort the episode
if matched_show
show = matched_show
puts " -> Matched: \"#{show}\""
# /Path/To/Television > [SHOW NAME]
show_directory = show_television_directory + show + "/"
# Parse season out of episode filename
season, episode = episode_name.scan(/[S|s](\d+)[E|e](\d+)/).flatten # handles S01E01 style
if !season
season, episode = episode_name.scan(/(\d+)[X|x](\d+)/).flatten # handles 1x01 style
end
# A nil season here does nothing. Will have to manually sort the episode
if season
# makes the season "06" become "6"
season = season.slice(1) if season[0] == "0"
# Look for a matching season to drop the episode.
# Iterate over the season folders: /Path/To/Television > [SHOW NAME] > [SEASON X] folders
# If there is no match, we will have to manually create the season and sort the file.
# This is to prevent a parsing error from creating tons of errant season folders
season_folders = Dir.entries(show_directory)
season_folders.each do |season_folder|
if season_folder == "Season #{season}"
# located the correct episode season
puts " -> Matched: \"#{season_folder}\""
# /Path/To/Television > [SHOW NAME] > [SEASON X] > [VIDEO FILE]
target_file = show_directory + season_folder + "/" + episode_name
puts " -> Moving #{downloaded_file} to #{target_file}"
# Move the file
FileUtils.mv(downloaded_file, target_file)
# Now that we moved the file, lets remove it from transmission
# Assumes that transmission remote is installed on the running machine and that transmission remote is enabled in settings
puts " -> Removing Torrent"
cleaned_episode_name = episode_name.gsub('[', '\[').gsub(']', '\]')
#find the torrent id
cmd = "#{transmission_remote_location} #{transmission_server_port} -l | grep -i '#{cleaned_episode_name}' | awk " "'{print $1}'"
puts " -> Running: #{cmd}"
torrent_id = `#{cmd}`.strip.gsub('*','')
# if torrent id is not found, do nothing. will have to manually remove torrent from transmission
if !torrent_id.empty?
puts " -> Torrent ID: #{torrent_id}"
puts " -> Double checking name matches id: '#{torrent_id}'"
# Sanity check. Make sure that the torrent name for the id we found matches the episode name
cmd = "#{transmission_remote_location} #{transmission_server_port} -t '#{torrent_id}' -i | grep -i 'Name\\:' | awk " "'{print $2}'"
puts " -> Running: #{cmd}"
name = `#{cmd}`.strip
# if not matching, do nothing. will have to manually remove torrent from transmission
if name == episode_name
puts " -> Matched: #{episode_name} == #{name}"
puts " -> Removing Torrent: \"#{episode_name}\" == \"#{name}\""
# Remove the torrent file from transmission
cmd = "#{transmission_remote_location} #{transmission_server_port} -t '#{torrent_id}' --remove-and-delete"
puts " -> Running: #{cmd}"
output = `#{cmd}`.strip
puts " -> #{output}"
end
else
puts " -> NO MATCH"
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment