|
#!/usr/bin/env ruby |
|
require 'rubygems' |
|
require 'uri' |
|
require 'yaml' |
|
require 'open-uri' |
|
|
|
['siskel','tvdb_party','tmdb_party','digest/md5','json','active_record','growl_notifier','launchy'].each do |thegem| |
|
begin |
|
require thegem |
|
rescue LoadError |
|
begin |
|
gem thegem |
|
rescue Gem::LoadError |
|
$stderr.puts "Please install the gem: #{thegem}" |
|
Process.exit |
|
end |
|
end |
|
end |
|
|
|
$log = Growl::Logger.new("Scrobbalot",nil,"icon.png") |
|
$log.level = :info |
|
|
|
if File.exists?('settings.yaml') |
|
$vs = YAML.load(open("settings.yaml")) |
|
else |
|
$vs = { |
|
:endpoint => "http://videoscrobbler.heroku.com/api/1.0/", |
|
:authorize => "http://videoscrobbler.heroku.com/api/auth", |
|
:secret_key => "3bbe2fdcba09020e54ab4eb0e0c35ddcddd95883", |
|
:api_key => "77b6b7a7a9747c16f9c6bdf1651e0d88cf8c3cf7" |
|
} |
|
YAML.dump($vs,open('settings.yaml',"w")) |
|
end |
|
|
|
def request(params) |
|
params.merge!({ |
|
'api_key' => $vs[:api_key], |
|
'sk' => $vs[:session_key], |
|
}) |
|
|
|
params['api_sig'] = Digest::MD5.hexdigest(params.sort.flatten.join()+$vs[:secret_key]) |
|
url = $vs[:endpoint]+'?'+params.collect { |k,v| "#{k}=#{URI::escape(v.to_s)}" }.reverse.join('&') |
|
$log.debug "Getting #{url}" |
|
return JSON.parse(open(url).read) |
|
end |
|
|
|
if $vs[:session_key].nil? |
|
if $vs[:token].nil? |
|
# Get a token: |
|
token = request({'method'=>'auth.getToken'}) |
|
$vs[:token] = token['token'] |
|
YAML.dump($vs,open('settings.yaml',"w")) |
|
|
|
Launchy.open($vs[:authorize]+"?token=#{token['token']}&api_key=#{$vs[:api_key]}") |
|
$log.error "Please allow Scrobbalot access to your VideoScrobbler account and restart", "Action Required!" |
|
Process.exit |
|
end |
|
begin |
|
session_key = request({'method'=>'auth.getSession','token'=>$vs[:token]}) |
|
rescue OpenURI::HTTPError |
|
Launchy.open($vs[:authorize]+"?token=#{$vs[:token]}&api_key=#{$vs[:api_key]}") |
|
$log.error "Please allow Scrobbalot access to your VideoScrobbler account and restart", "Action Required!" |
|
Process.exit |
|
end |
|
$vs.delete(:token) |
|
$vs[:session_key] = session_key['sk'] |
|
YAML.dump($vs,open('settings.yaml',"w")) |
|
end |
|
|
|
$tmdb = TMDBParty::Base.new('3aebb3859a752aa73f9a7c69eb40e7ba') |
|
$tvdb = TvdbParty::Search.new('E319EC33BBD28757') |
|
|
|
dbfile = 'cache.sqlite3' |
|
|
|
ActiveRecord::Base.establish_connection( |
|
:adapter => "sqlite3", |
|
:database => dbfile |
|
) |
|
|
|
if !File.exists?(dbfile) |
|
ActiveRecord::Schema.define do |
|
create_table :vscaches do |table| |
|
table.string :location |
|
table.integer :vsid |
|
table.string :remote_id |
|
end |
|
end |
|
end |
|
|
|
class Vscache < ActiveRecord::Base |
|
def vsid |
|
return read_attribute(:vsid) if !read_attribute(:vsid).nil? |
|
|
|
params = { |
|
'method' => 'video.getInfo', |
|
'id' => read_attribute(:remote_id) |
|
} |
|
|
|
data = request(params) |
|
|
|
write_attribute(:vsid,data['id']) |
|
self.save |
|
return vsid |
|
end |
|
end |
|
|
|
def get_id(filename) |
|
return nil if $unknown.include? filename |
|
|
|
cached = Vscache.find_by_location(filename) |
|
if !cached.nil? and !cached.vsid.nil? |
|
return cached.vsid |
|
end |
|
|
|
$log.debug "Grabbing metadata for video file (#{filename})" |
|
r = Siskel.review(:file => filename) |
|
# Should test to see if this is an mp4/mov really |
|
|
|
if (!r[:general][:tvsh].nil? or r[:general][:stik] == "10") |
|
id = get_tv(r[:general][:tvsh],r[:general][:tvsn],r[:general][:tves]) |
|
|
|
break if id.nil? # TODO: This may not be right |
|
$log.debug "TVDB id found: #{id}" |
|
return Vscache.create(:location => filename, :remote_id => "tvdb:#{id}").vsid |
|
end |
|
|
|
if (!r[:general][:movie_name].nil? or r[:general][:stik] == "9") |
|
id = get_film(r[:general][:movie_name]) |
|
|
|
break if id.nil? # TODO: This may not be right |
|
$log.debug "TMDB id found: #{id}" |
|
return Vscache.create(:location => filename, :remote_id => "tmdb:#{id}").vsid |
|
end |
|
|
|
# Not impletemened… |
|
$log.debug "Can't find useful metadata, attempting filename voodoo", "It's magic!" |
|
case File.basename(filename)[0..File.basename(filename).rindex('.') - 1 ] |
|
when /^(.+?)([\.\ _-])S(\d{2})E(\d{2})/i,/^(.+?)([\.\ _-])(\d{1,2})x(\d{2})/i,/^(.+?)([\.\ _-])(?!20|19)(\d{1,2})(\d{2})/ |
|
season = $3.to_i |
|
episode = $4.to_i |
|
show = $1.gsub($2,' ') |
|
|
|
$log.debug("Looking for TV episode: #{show} #{season}x#{episode.to_s.rjust(2,"0")}","Voodoo Match") |
|
|
|
id = get_tv(show,season,episode) |
|
|
|
if !id.nil? |
|
$log.debug "TVDB id found: #{id}" |
|
return Vscache.create(:location => filename, :remote_id => "tvdb:#{id}").vsid |
|
end |
|
when /^(.+?)([\.\ _-])(?:\(?((?:19|20)\d\d))/,/^.+?-(.+)-/,/^.+?-(.+)/,/^(.+)/ |
|
year = $3.to_i |
|
film = $1 |
|
film.gsub!($2,' ') if !$2.nil? |
|
year = '?' if year == 0 |
|
|
|
$log.debug("Looking for film: #{film} (#{year})","Voodoo Match") |
|
|
|
id = get_film(film,year) |
|
|
|
if !id.nil? |
|
$log.debug "TMDB id found: #{id}" |
|
return Vscache.create(:location => filename, :remote_id => "tmdb:#{id}").vsid |
|
end |
|
else |
|
$stderr.puts file |
|
end |
|
|
|
$log.error "Can't determine what TV episode or film this video is. I can't scrobble it!", "Not scrobbled" |
|
|
|
$unknown.push filename |
|
nil |
|
end |
|
|
|
def get_tv(showname,series,episode,epname = nil) # TODO: check epname, if we can |
|
shows = $tvdb.search(showname).select{|show| show['SeriesName'].downcase == showname.downcase } |
|
case shows.count |
|
when 1 |
|
show = $tvdb.get_series_by_id(shows[0]['seriesid']) |
|
return show.get_episode(series,episode).id rescue nil |
|
else |
|
return nil |
|
end |
|
end |
|
|
|
def get_film(filmname,year = nil) |
|
films = $tmdb.search(year.nil? ? filmname : "#{filmname} (#{year})").select{|film| film.name.downcase == filmname.downcase } |
|
|
|
case films.count |
|
when 1 |
|
return films[0].id rescue nil |
|
when 0 |
|
return get_film(filmname) if !year.nil? # The year may have screwed things up, try without |
|
else |
|
$log.error("I found more than one film called #{filmname}, not much I can do about that right now I'm afraid!","#{filmname} Confusion") |
|
return nil |
|
end |
|
end |
|
|
|
require 'osx/cocoa' |
|
|
|
include OSX |
|
OSX.require_framework 'ScriptingBridge' |
|
|
|
app = SBApplication.applicationWithBundleIdentifier_("com.apple.QuickTimePlayerX") |
|
|
|
$spec = {} |
|
$wait = 2 |
|
$finished = 0.92 |
|
$unknown = [] |
|
|
|
Signal.trap("INT") do |
|
$log.info "Scrobbalot is shutting down","Goodbye!" |
|
Process.exit |
|
end |
|
|
|
$log.info "I'm scrobbling your video!","Scrobbalot" |
|
while true |
|
if app.isRunning |
|
keep = [] |
|
begin |
|
app.documents.each do |doc| |
|
# Prevents us from scrobbling when the video is still loading |
|
if doc.duration != 0.0 |
|
|
|
file = URI.decode(doc.file.to_s) |
|
# Deal with streaming files |
|
if file[0..15] == 'file://localhost' |
|
file = file[16..-1] |
|
video_id = get_id(file) |
|
else |
|
video_id = file |
|
end |
|
|
|
keep.push(file) |
|
|
|
if video_id.nil? |
|
$log.debug("Couldn't figure out what #{File.basename(file)} is", "Unknown video") if $spec[file].nil? |
|
$spec[file] = false |
|
raise IndexError, "Couldn't get id for this video" |
|
end |
|
|
|
state = (doc.currentTime == doc.duration and doc.currentTime != 0.0) ? 'finished' : (doc.playing) ? 'playing' : ($spec[file].nil? or $spec[file][:paused] < (120/$wait)) ? 'paused' : 'stopped' |
|
|
|
# If the file has just been openned lets see if we want to move the ep to the place it was last played |
|
if $spec[file].nil? |
|
$log.debug "Determining catch-up status" |
|
details = request({ |
|
'method' => 'video.getInfo', |
|
'id' => video_id |
|
}) |
|
|
|
if details['user']['plays'] > 0 |
|
$log.info("You've watched this video before, I'll give you #{$wait} seconds to close it if this was a mistake!","Awesome") |
|
state = nil |
|
end |
|
|
|
if !details['user']['position'].nil? and doc.currentTime < details['user']['position'] |
|
$log.info "Pushing the video forwards to #{details['user']['position'].to_i.divmod(60).collect{|n| n.to_s.rjust(2,"0")}.join(":").gsub(/^0/,'')}" |
|
doc.currentTime = details['user']['position'] |
|
(doc.currentTime - details['user']['position']).abs |
|
|
|
if (doc.currentTime - details['user']['position']).abs > 1 and (doc.duration > details['user']['position']) |
|
$log.warn "I can't push forward to your previously scrobbled position, I'd be overwriting your saved position!","Scrobbling temporarily disabled" |
|
$spec[file] = { |
|
:noscrobble => true |
|
} |
|
end |
|
doc.resume if details['user']['state'] == "playing" |
|
end |
|
end |
|
|
|
# Update if this is the first run of this file, if the state has changed or every 30 seconds if it's playing : don't update again if last was finished |
|
if ($spec[file].nil? or state != ($spec[file][:laststate] rescue nil) or (($spec[file][:tick] % 30) == 0 and state == 'playing') and ($spec[file][:laststate] rescue "") != 'finished') and !state.nil? and ($spec[file].nil? or !$spec[file][:noscrobble]) |
|
$log.debug "Found #{File.basename(file)}" |
|
params = { |
|
'method' => 'video.scrobble', |
|
'state' => state, |
|
'id' => video_id, |
|
'position' => doc.currentTime, |
|
'origin' => 'file' |
|
} |
|
|
|
request(params) |
|
$log.debug "Scrobbled" |
|
$log.info("#{File.basename(file)} now #{state}","Video #{state}") if state != ($spec[file][:laststate] rescue nil) |
|
end |
|
|
|
$spec[file] = { |
|
:laststate => state, |
|
:paused => ((state == 'playing') ? 0 : ($spec[file][:paused] rescue 0) + 1), |
|
:position => doc.currentTime, |
|
:tick => (($spec[file][:tick] rescue 0) + 1) |
|
} |
|
end |
|
end |
|
rescue IndexError |
|
# Couldn't find an ID for this error, no need to quit during debug |
|
rescue Exception => e |
|
raise e if $log.level == :debug |
|
end |
|
|
|
$spec = Hash[*$spec.select{|k,v| |
|
if keep.include? k |
|
true |
|
else # Mark as stopped |
|
begin |
|
if (['paused','playing'].include? v[:laststate]) |
|
params = { |
|
'method' => 'video.scrobble', |
|
'state' => 'stopped', |
|
'id' => get_id(k), |
|
'position' => v[:position], |
|
'origin' => 'file' |
|
} |
|
|
|
request(params) |
|
$log.info("#{File.basename(k)} now stopped","Video stopped") |
|
end |
|
rescue # Just incase something goes wrong |
|
end |
|
false |
|
end |
|
}.flatten] |
|
else |
|
$spec = {} |
|
end |
|
|
|
sleep($wait) |
|
end |
This has somewhat outgrown the space I've put it in here - I'm going to formalise it at some point, and throw it in a proper repo.