henchman-sync 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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/henchman +4 -0
- data/henchman.gemspec +35 -0
- data/lib/applescript.rb +130 -0
- data/lib/configure.rb +155 -0
- data/lib/core.rb +125 -0
- data/lib/dropbox.rb +90 -0
- data/lib/henchman/version.rb +3 -0
- data/lib/henchman.rb +47 -0
- data/lib/launchd_handler.rb +55 -0
- data/lib/templates.rb +47 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1f864ccdbb1adb044b16389e17adf74cc7757d26
|
4
|
+
data.tar.gz: 0ed7300956a883823681114bca11bb6985557c4b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43a0697682a79bd283b855ec7c1c59dc7f6de7a6547a9b523effeb8e66dce7f7775283e3c93ee961be6ce4fb15f7238adb2f6f96192625c25ecaf21ebb8e9a1f
|
7
|
+
data.tar.gz: b38df428d8700ac444c63ba55fc2dffa778e9b7cbd06f0732c02385e05e41ffaac9ccc1c4c8ce930b165e3beaa4f2c60b7ba8055dc857e3f955d3610dbfe7523
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Greg Merritt
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Henchman
|
2
|
+
|
3
|
+
Henchman is an application which sits on top of iTunes on OS X. It dynamically syncs in music from Dropbox. This way, all music can be kept in the cloud yet you can still use iTunes as your music player.
|
4
|
+
|
5
|
+
When you (single) click on a track in iTunes the application will check if that track is available locally. If it is not the application will check if the file is available in Dropbox, and it will be downloaded automatically.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
Or install it yourself as:
|
12
|
+
|
13
|
+
$ gem install henchman
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Run:
|
18
|
+
|
19
|
+
$ henchman configure
|
20
|
+
|
21
|
+
This will guide you through setting up the application, including linking your Dropbox. From here you can run:
|
22
|
+
|
23
|
+
$ henchman start
|
24
|
+
|
25
|
+
This starts the henchman daemon. You can now select a track in iTunes which is not available locally, and you will be asked if you'd like to fetch the track from your Dropbox. The application will also automatically download the rest of the album that contains the track you selected.
|
26
|
+
|
27
|
+
## Troubleshooting
|
28
|
+
|
29
|
+
Running the `configure` command creates a `~/.henchman` direction. This directory contains your configuration file, as well as log files and a shell script used for running the application. If nothing appears to be happening in iTunes after running `henchman start` your best bet is to check the log files.
|
30
|
+
|
31
|
+
By default, the application checks if iTunes is open every 10 seconds. It it is, it continues to poll iTunes every 3 seconds to see if a track is selected. If you'd like to change these, you can edit the `config` YAML file. If you edit the `poll_itunes_open` setting, you'll need to stop and re-start the henchman daemon.
|
32
|
+
|
33
|
+
## Development
|
34
|
+
|
35
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
36
|
+
|
37
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
38
|
+
|
39
|
+
## Contributing
|
40
|
+
|
41
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/gremerritt/henchman.
|
42
|
+
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "henchman"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/exe/henchman
ADDED
data/henchman.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'henchman/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "henchman-sync"
|
8
|
+
spec.version = Henchman::VERSION
|
9
|
+
spec.authors = ["Greg Merritt"]
|
10
|
+
spec.email = ["gremerritt@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Cloud music syncing for iTunes on OS X}
|
13
|
+
spec.description = %q{OS X app that sits on top of iTunes to sync music with Dropbox}
|
14
|
+
spec.homepage = "https://www.github.com/gremerritt/henchman"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org/"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "commander", "~> 4.4"
|
31
|
+
spec.add_dependency "dropbox-sdk", "~> 1.6"
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
end
|
data/lib/applescript.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
module Henchman
|
2
|
+
|
3
|
+
class AppleScript
|
4
|
+
|
5
|
+
def setup config
|
6
|
+
@delimiter = config[:delimiter]
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_active_app_script
|
10
|
+
"tell application \"System Events\"\n"\
|
11
|
+
" set activeApp to name of first application process whose frontmost is true\n"\
|
12
|
+
" return activeApp\n"\
|
13
|
+
"end tell"
|
14
|
+
end
|
15
|
+
|
16
|
+
def prompt_script
|
17
|
+
"tell application \"iTunes\"\n"\
|
18
|
+
" display dialog \"Fetch?\"\n"\
|
19
|
+
"end tell"
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_track_location_script selection, local_file
|
23
|
+
"tell application \"iTunes\"\n"\
|
24
|
+
" try\n"\
|
25
|
+
" set data_tracks to "\
|
26
|
+
" (every track whose artist is \"#{selection[:artist].gsub(/'/){ %q('"'"') }}\" and "\
|
27
|
+
" album is \"#{selection[:album].gsub(/'/){ %q('"'"') }}\" and "\
|
28
|
+
" name is \"#{selection[:track].gsub(/'/){ %q('"'"') }}\")\n"\
|
29
|
+
" if (count of data_tracks) is 1 then\n"\
|
30
|
+
" set location of (item 1 of data_tracks) to "\
|
31
|
+
" (POSIX file \"#{local_file.gsub(/'/){ %q('"'"') }}\")\n"\
|
32
|
+
" return 1\n"\
|
33
|
+
" else\n"\
|
34
|
+
" return 0\n"\
|
35
|
+
" end if\n"\
|
36
|
+
" on error\n"\
|
37
|
+
" return 0\n"\
|
38
|
+
" end try\n"\
|
39
|
+
"end tell"
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_selection_script
|
43
|
+
"tell application \"iTunes\"\n"\
|
44
|
+
" try\n"\
|
45
|
+
" if class of selection as string is \"file track\" then\n"\
|
46
|
+
" set data_artist to artist of selection as string\n"\
|
47
|
+
" set data_album to album of selection as string\n"\
|
48
|
+
" set data_title to name of selection as string\n"\
|
49
|
+
" set data_location to POSIX path of (location of selection as string)\n"\
|
50
|
+
" set str to data_artist & \"#{@delimiter}\" & "\
|
51
|
+
" data_album & \"#{@delimiter}\" & "\
|
52
|
+
" data_title & \"#{@delimiter}\" & "\
|
53
|
+
" data_location\n"\
|
54
|
+
" return str\n"\
|
55
|
+
" end if\n"\
|
56
|
+
" on error\n"\
|
57
|
+
" return \"\"\n"\
|
58
|
+
" end try\n"\
|
59
|
+
"end tell"
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_album_tracks_script artist, album
|
63
|
+
"tell application \"iTunes\"\n"\
|
64
|
+
" try\n"\
|
65
|
+
" set data_tracks to "\
|
66
|
+
" (every track whose artist is \"#{artist}\" "\
|
67
|
+
" and album is \"#{album}\")\n"\
|
68
|
+
" set data_tracks_str to \"\"\n"\
|
69
|
+
" repeat with data_track in data_tracks\n"\
|
70
|
+
" set data_tracks_str to data_tracks_str & (name of data_track) as string\n"\
|
71
|
+
" if (name of (last item of data_tracks)) is not (name of data_track) then\n"\
|
72
|
+
" set data_tracks_str to data_tracks_str & \"#{@delimiter}\"\n"\
|
73
|
+
" end if\n"\
|
74
|
+
" end repeat\n"\
|
75
|
+
" return data_tracks_str\n"\
|
76
|
+
" on error\n"\
|
77
|
+
" return 0\n"\
|
78
|
+
" end try\n"\
|
79
|
+
"end tell"
|
80
|
+
end
|
81
|
+
|
82
|
+
def applescript_command(script)
|
83
|
+
"osascript -e '#{script}' 2> /dev/null"
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_selection ret
|
87
|
+
selection = %x( #{applescript_command(get_selection_script)} ).chomp
|
88
|
+
info = selection.split(@delimiter)
|
89
|
+
if info.empty?
|
90
|
+
false
|
91
|
+
elsif info[3] == "/missing value" || !File.exists?(info[3])
|
92
|
+
ret[:artist] = info[0]
|
93
|
+
ret[:album] = info[1]
|
94
|
+
ret[:track] = info[2]
|
95
|
+
true
|
96
|
+
else
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_album_tracks_of selection
|
102
|
+
artist = selection[:artist]
|
103
|
+
album = selection[:album]
|
104
|
+
tracks = %x(#{applescript_command(get_album_tracks_script artist, album)}).chomp
|
105
|
+
tracks = tracks.split(@delimiter)
|
106
|
+
tracks.delete selection[:track]
|
107
|
+
tracks
|
108
|
+
end
|
109
|
+
|
110
|
+
def fetch?
|
111
|
+
(%x(#{applescript_command(prompt_script)}).chomp == "button returned:OK") ? true : false
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_active_app
|
115
|
+
%x(#{applescript_command(get_active_app_script)}).chomp
|
116
|
+
end
|
117
|
+
|
118
|
+
def set_track_location selection, local_file
|
119
|
+
ret = %x(#{applescript_command(update_track_location_script selection, local_file)}).chomp
|
120
|
+
if ret.empty? || ret == '0'
|
121
|
+
puts "Could not update location of #{selection.values.join(':')} to #{local_file}"
|
122
|
+
false
|
123
|
+
else
|
124
|
+
true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
data/lib/configure.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require "templates"
|
2
|
+
require "yaml"
|
3
|
+
require "dropbox_sdk"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Henchman
|
7
|
+
def self.configure
|
8
|
+
config_file = File.expand_path('~/.henchman/config')
|
9
|
+
if !File.exists?(config_file)
|
10
|
+
config = Henchman::Templates.config
|
11
|
+
else
|
12
|
+
config = YAML.load_file(config_file)
|
13
|
+
end
|
14
|
+
|
15
|
+
if config[:dropbox][:key].empty? ||
|
16
|
+
config[:dropbox][:secret].empty? ||
|
17
|
+
config[:dropbox][:access_token].empty? ||
|
18
|
+
agree("Update Dropbox configuration? (y/n) ")
|
19
|
+
begin
|
20
|
+
get_dropbox_credentials config
|
21
|
+
rescue StandardError => err
|
22
|
+
puts err
|
23
|
+
return
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
client = connect config
|
28
|
+
return if !client
|
29
|
+
|
30
|
+
if config[:dropbox][:root].empty? || agree("\nUpdate music directory in Dropbox? (y/n) ")
|
31
|
+
get_dropbox_root config, client
|
32
|
+
end
|
33
|
+
|
34
|
+
if config[:root].empty? || agree("\nUpdate local music directory? (y/n) ")
|
35
|
+
get_local_root config
|
36
|
+
end
|
37
|
+
|
38
|
+
Dir.mkdir(File.dirname(config_file)) if !File.exists?(File.dirname(config_file))
|
39
|
+
File.open(config_file, "w") { |f| f.write( config.to_yaml ) }
|
40
|
+
puts "\nConfiguration complete! Run `henchman start` to start the daemon."
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.connect config={}
|
44
|
+
begin
|
45
|
+
config = YAML.load_file(File.expand_path("~/.henchman/config")) if config.empty?
|
46
|
+
client = DropboxClient.new(config[:dropbox][:access_token])
|
47
|
+
account_info = client.account_info()
|
48
|
+
puts "Successfully connected to Dropbox: "
|
49
|
+
puts " #{account_info['display_name']} [#{account_info['email']}]"
|
50
|
+
return client
|
51
|
+
rescue StandardError => err
|
52
|
+
puts "Error connecting to Dropbox account (#{err}). Try deleting the "\
|
53
|
+
"henchman configuration file (`rm ~/.henchman`) and rerunning "\
|
54
|
+
"`henchman configure`"
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.get_dropbox_credentials config
|
60
|
+
puts "\nYou'll need to create your own Dropbox app to integrate. "\
|
61
|
+
"Head over to https://www.dropbox.com/developers/apps. If "\
|
62
|
+
"you have an app already that you'd like to use, click on "\
|
63
|
+
"that app. If not, click on the 'Create App' link. From "\
|
64
|
+
"here, choose 'Dropbox API', 'Full Dropbox', and finally "\
|
65
|
+
"choose an app name.\n\n"\
|
66
|
+
"Note your app key and app secret, and enter them below. "\
|
67
|
+
"You will then be asked to login and give access to the app. "\
|
68
|
+
"Once you have done this, copy the Authorization Code. You "\
|
69
|
+
"will be prompted to enter it here.\n\n"
|
70
|
+
|
71
|
+
dbx_cfg = config[:dropbox]
|
72
|
+
dbx_cfg[:key] = ask("Dropbox App Key: ")
|
73
|
+
dbx_cfg[:secret] = ask("Dropbox App Secret: ")
|
74
|
+
|
75
|
+
flow = DropboxOAuth2FlowNoRedirect.new(dbx_cfg[:key], dbx_cfg[:secret])
|
76
|
+
authorize_url = flow.start()
|
77
|
+
|
78
|
+
puts '1. Go to: ' + authorize_url
|
79
|
+
puts '2. Click "Allow" (you might have to log in first)'
|
80
|
+
puts '3. Copy the authorization code'
|
81
|
+
|
82
|
+
code = ask("Enter the authorization code here: ")
|
83
|
+
print "\n"
|
84
|
+
begin
|
85
|
+
dbx_cfg[:access_token], dbx_cfg[:user_id] = flow.finish(code)
|
86
|
+
rescue StandardError => msg
|
87
|
+
dbx_cfg[:key] = ''
|
88
|
+
dbx_cfg[:secret] = ''
|
89
|
+
raise "Invalid authorization code (#{msg})"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.get_dropbox_root config, client
|
94
|
+
# paths = Hash.new
|
95
|
+
# build_dropbox_dirs(paths, client, '/', 0)
|
96
|
+
not_done = true
|
97
|
+
while not_done
|
98
|
+
path = ask("Enter the path to your music directory in Dropbox: (? for help)" )
|
99
|
+
if path == '?'
|
100
|
+
puts "The path to your music directory is a unix-like path. For example: "\
|
101
|
+
"/Some/Directory/Music\n\n"
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
path = path.chomp('/')
|
106
|
+
begin
|
107
|
+
metadata = client.metadata(path)
|
108
|
+
config[:dropbox][:root] = path
|
109
|
+
puts "Dropbox music path set!\n"
|
110
|
+
not_done = false
|
111
|
+
rescue StandardError => err
|
112
|
+
print "Invalid path. (#{err})"
|
113
|
+
not_done = agree("Try again? (y/n) ")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# def self.build_dropbox_dirs paths, client, path, level
|
119
|
+
# return if level == 2
|
120
|
+
# metadata = client.metadata(path)
|
121
|
+
# metadata['contents'].each do |elem|
|
122
|
+
# next if !elem['is_dir']
|
123
|
+
# paths[elem['path']] = Hash.new
|
124
|
+
# build_dropbox_dirs(paths[elem['path']], client, elem['path'], level+1)
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
|
128
|
+
def self.get_local_root config
|
129
|
+
not_done = true
|
130
|
+
while not_done
|
131
|
+
path = ask("Enter the path to your local music directory: (? for help)" )
|
132
|
+
if path == '?'
|
133
|
+
puts "This is the directory in which local music files will be stored. "\
|
134
|
+
"For example: /Users/yourusernam/Music\n\n"
|
135
|
+
next
|
136
|
+
end
|
137
|
+
|
138
|
+
path = File.expand_path(path.chomp('/'))
|
139
|
+
if File.directory? path
|
140
|
+
config[:root] = path
|
141
|
+
puts "Local music path set!\n"
|
142
|
+
not_done = false
|
143
|
+
elsif File.directory? File.dirname(path)
|
144
|
+
if agree("Directory doesn't exist. Create it? (y/n) ")
|
145
|
+
Dir.mkdir(path)
|
146
|
+
config[:root] = path
|
147
|
+
puts "Local music path set!\n"
|
148
|
+
not_done = false
|
149
|
+
end
|
150
|
+
else
|
151
|
+
puts "Invalid path."
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/core.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require "dropbox.rb"
|
2
|
+
require "applescript.rb"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Henchman
|
6
|
+
|
7
|
+
class Core
|
8
|
+
|
9
|
+
def self.run
|
10
|
+
@appleScript = Henchman::AppleScript.new
|
11
|
+
|
12
|
+
begin
|
13
|
+
cache_file = File.expand_path("~/.henchman/cache")
|
14
|
+
@ignore = YAML.load_file(cache_file)
|
15
|
+
rescue StandardError => err
|
16
|
+
puts "Error opening cache file (#{err})"
|
17
|
+
@ignore = Hash.new
|
18
|
+
end
|
19
|
+
@ignore.default = 0
|
20
|
+
|
21
|
+
threads = []
|
22
|
+
update_cache = false
|
23
|
+
|
24
|
+
while itunes_is_active?
|
25
|
+
begin
|
26
|
+
@config = YAML.load_file(File.expand_path('~/.henchman/config'))
|
27
|
+
rescue StandardError => err
|
28
|
+
puts "Error opening config file. Try rerunning `henchman configure`"
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
@appleScript.setup @config
|
33
|
+
begin
|
34
|
+
@dropbox = Henchman::DropboxAssistant.new @config
|
35
|
+
rescue
|
36
|
+
puts "Error connecting to Dropbox. Try rerunning `henchman configure`"
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
selection = Hash.new
|
41
|
+
track_selected = @appleScript.get_selection selection
|
42
|
+
|
43
|
+
if track_selected && @ignore[selection[:artist]] < (Time.now.to_i - @config[:reprompt_timeout])
|
44
|
+
update_cache = true
|
45
|
+
@ignore.delete selection[:artist]
|
46
|
+
if @appleScript.fetch?
|
47
|
+
puts "searching"
|
48
|
+
begin
|
49
|
+
# first download the selected track
|
50
|
+
dropbox_path = @dropbox.search_for selection
|
51
|
+
file_save_path = @dropbox.download selection, dropbox_path
|
52
|
+
|
53
|
+
# if we downloaded it, update the location of the track in iTunes
|
54
|
+
unless !file_save_path
|
55
|
+
updated = @appleScript.set_track_location selection, file_save_path
|
56
|
+
# if the update failed, cleanup that directory and don't bother
|
57
|
+
# doing the rest of the album
|
58
|
+
if !updated
|
59
|
+
cleanup file_save_path
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
# now that we've gotten the selected track, spawn off another process
|
64
|
+
# to download the rest of the tracks on the album - spatial locality FTW
|
65
|
+
album_tracks = @appleScript.get_album_tracks_of selection
|
66
|
+
threads << Thread.new{ download_album_tracks selection, album_tracks }
|
67
|
+
end
|
68
|
+
rescue StandardError => err
|
69
|
+
puts err
|
70
|
+
next
|
71
|
+
end
|
72
|
+
else
|
73
|
+
@ignore[selection[:artist]] = Time.now.to_i
|
74
|
+
end
|
75
|
+
end
|
76
|
+
sleep @config[:poll_track]
|
77
|
+
end
|
78
|
+
|
79
|
+
threads.each { |thr| thr.join }
|
80
|
+
File.open(cache_file, "w") { |f| f.write( @ignore.to_yaml ) } if update_cache
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.itunes_is_active?
|
84
|
+
@appleScript.get_active_app == 'iTunes'
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.download_album_tracks selection, album_tracks
|
88
|
+
album_tracks.each do |album_track|
|
89
|
+
selection[:track] = album_track
|
90
|
+
begin
|
91
|
+
# first download the selected track
|
92
|
+
dropbox_path = @dropbox.search_for selection
|
93
|
+
file_save_path = @dropbox.download selection, dropbox_path
|
94
|
+
|
95
|
+
# if we downloaded it, update the location of the track in iTunes
|
96
|
+
unless !file_save_path
|
97
|
+
updated = @appleScript.set_track_location selection, file_save_path
|
98
|
+
# if the update failed, remove that file
|
99
|
+
if !updated
|
100
|
+
cleanup file_save_path
|
101
|
+
next
|
102
|
+
end
|
103
|
+
end
|
104
|
+
rescue StandardError => err
|
105
|
+
puts err
|
106
|
+
next
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.cleanup file_save_path
|
112
|
+
File.delete file_save_path
|
113
|
+
while File.dirname(file_save_path) != @config[:root]
|
114
|
+
file_save_path = File.dirname(file_save_path)
|
115
|
+
begin
|
116
|
+
Dir.rmdir(file_save_path)
|
117
|
+
rescue SystemCallError => msg
|
118
|
+
break
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
data/lib/dropbox.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'dropbox_sdk'
|
2
|
+
require 'yaml'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Henchman
|
6
|
+
|
7
|
+
class DropboxAssistant
|
8
|
+
|
9
|
+
def initialize config
|
10
|
+
begin
|
11
|
+
@config = config
|
12
|
+
@client = DropboxClient.new(@config[:dropbox][:access_token])
|
13
|
+
true
|
14
|
+
rescue DropboxError => msg
|
15
|
+
puts "Couldn't connect to Dropbox (#{msg}). \n"\
|
16
|
+
"Run `henchman stop` then `henchman configure` \n"\
|
17
|
+
"to configure Dropbox connection."
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def download selection, dropbox_path
|
23
|
+
puts "downloading #{selection[:track]}"
|
24
|
+
begin
|
25
|
+
# download the file
|
26
|
+
content = @client.get_file(dropbox_path)
|
27
|
+
|
28
|
+
# make sure we have the directory to put it in
|
29
|
+
trgt_dir = File.join @config[:root], selection[:artist], selection[:album]
|
30
|
+
system 'mkdir', '-p', trgt_dir
|
31
|
+
|
32
|
+
# save the file
|
33
|
+
file_save_path = File.join trgt_dir, File.basename(dropbox_path)
|
34
|
+
open(file_save_path, 'w') {|f| f.puts content }
|
35
|
+
file_save_path
|
36
|
+
rescue DropboxError => msg
|
37
|
+
puts "Error downloading Dropbox file #{dropbox_path}: #{msg}"
|
38
|
+
false
|
39
|
+
rescue StandardError => msg
|
40
|
+
puts "Error saving Dropbox file #{dropbox_path} to #{trgt_dir}: #{msg}"
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def search_for selection
|
46
|
+
# search Dropbox for the file
|
47
|
+
results = @client.search(@config[:dropbox][:root], selection[:track])
|
48
|
+
|
49
|
+
# get rid of any results that are directories
|
50
|
+
results.reject! { |result| result['is_dir'] }
|
51
|
+
|
52
|
+
# if we still don't have any results, try dropping any brackets and paranthesis
|
53
|
+
if results.empty? && (selection[:track].match(%r( *\[.*\] *)) || selection[:track].match(%r( *\(.*\) *)))
|
54
|
+
track = selection[:track].gsub(%r( *\[.*\] *), " ").gsub(%r( *\(.*\) *), " ")
|
55
|
+
results = @client.search(@config[:dropbox][:root], track)
|
56
|
+
results.reject! { |result| result['is_dir'] }
|
57
|
+
end
|
58
|
+
|
59
|
+
# if there were no results, raise err
|
60
|
+
if results.empty?
|
61
|
+
raise "Track not found in Dropbox: #{selection.inspect}"
|
62
|
+
|
63
|
+
# if there's only one result, return it
|
64
|
+
elsif results.length == 1
|
65
|
+
results[0]['path']
|
66
|
+
|
67
|
+
# if there are multiple results, score them based on artist + album
|
68
|
+
else
|
69
|
+
scores = Hash.new 0
|
70
|
+
results.each do |result|
|
71
|
+
[:artist, :album].each do |identifier|
|
72
|
+
tokens = selection[identifier].downcase
|
73
|
+
.gsub(%r( +), " ")
|
74
|
+
.gsub(%r(-+), "-")
|
75
|
+
.strip
|
76
|
+
.split(/[\s-]/)
|
77
|
+
tokens.each do |token|
|
78
|
+
scores[result['path']] += 1 if result['path'].downcase.include? token
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# return the path that has the highest score
|
84
|
+
(scores.sort_by { |path, score| score })[-1][0]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
data/lib/henchman.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require "henchman/version"
|
2
|
+
require "configure"
|
3
|
+
require "launchd_handler"
|
4
|
+
require "core"
|
5
|
+
require "commander/import"
|
6
|
+
require "yaml"
|
7
|
+
|
8
|
+
module Henchman
|
9
|
+
def Henchman.run
|
10
|
+
program :name, 'Henchman'
|
11
|
+
program :version, Henchman::VERSION
|
12
|
+
program :description, 'Cloud music syncing for iTunes on OS X'
|
13
|
+
|
14
|
+
command :start do |c|
|
15
|
+
c.syntax = 'henchman start'
|
16
|
+
c.description = 'Starts the henchman daemon'
|
17
|
+
c.action do |args, options|
|
18
|
+
Henchman::LaunchdHandler.start
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
command :stop do |c|
|
23
|
+
c.syntax = 'henchman stop'
|
24
|
+
c.description = 'Stops the henchman daemon'
|
25
|
+
c.action do |args, options|
|
26
|
+
Henchman::LaunchdHandler.stop
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
command :configure do |c|
|
31
|
+
c.syntax = 'henchman configure'
|
32
|
+
c.description = 'Configures the henchman client'
|
33
|
+
c.action do |args, options|
|
34
|
+
Henchman.configure
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
command :run do |c|
|
39
|
+
c.syntax = 'henchman run'
|
40
|
+
c.description = 'Main interface into henchman. Should not be ran manually.'
|
41
|
+
c.action do |args, options|
|
42
|
+
Henchman::Core.run
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module Henchman
|
4
|
+
|
5
|
+
class LaunchdHandler
|
6
|
+
|
7
|
+
def self.start
|
8
|
+
if !internet_connection?
|
9
|
+
puts "No internet connection detected - unable to verify correct configuration."
|
10
|
+
return if !agree("Launch henchman anyways? (y/n) ")
|
11
|
+
else
|
12
|
+
puts "Checking configuration"
|
13
|
+
return if !Henchman.connect
|
14
|
+
end
|
15
|
+
|
16
|
+
puts "Creating agent"
|
17
|
+
plist = Henchman::Templates.plist
|
18
|
+
plist_path = File.expand_path("~/Library/LaunchAgents/com.henchman.plist")
|
19
|
+
shell_script_path = File.expand_path("~/.henchman/run.sh")
|
20
|
+
cache_path = File.expand_path("~/.henchman/cache")
|
21
|
+
File.write(plist_path, plist)
|
22
|
+
File.write(shell_script_path, Henchman::Templates.shell_script)
|
23
|
+
File.open(cache_path, "w") { |f| f.write( {}.to_yaml ) }
|
24
|
+
|
25
|
+
puts "Launching agent"
|
26
|
+
`chmod +x #{shell_script_path}`
|
27
|
+
`launchctl load #{plist_path}`
|
28
|
+
|
29
|
+
puts "Launched successful! You are now running henchman."
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.stop
|
33
|
+
puts "Stopping agents"
|
34
|
+
plist_path = File.expand_path("~/Library/LaunchAgents/com.henchman.plist")
|
35
|
+
`launchctl unload #{plist_path}`
|
36
|
+
`rm #{plist_path}`
|
37
|
+
`rm #{File.expand_path("~/.henchman/run.sh")}`
|
38
|
+
`rm #{File.expand_path("~/.henchman/cache")}`
|
39
|
+
`rm #{File.expand_path("~/.henchman/stdout.log")}`
|
40
|
+
`rm #{File.expand_path("~/.henchman/stderr.log")}`
|
41
|
+
|
42
|
+
puts "Successfully stopped henchman"
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.internet_connection?
|
46
|
+
begin
|
47
|
+
true if open("https://www.dropbox.com/")
|
48
|
+
rescue
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/lib/templates.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Henchman
|
2
|
+
class Templates
|
3
|
+
def self.plist
|
4
|
+
config = YAML.load_file(File.expand_path('~/.henchman/config'))
|
5
|
+
|
6
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"\
|
7
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "\
|
8
|
+
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"\
|
9
|
+
"<plist version=\"1.0\">\n"\
|
10
|
+
"<dict>\n"\
|
11
|
+
"\t<key>Label</key>\n"\
|
12
|
+
"\t<string>henchman</string>\n"\
|
13
|
+
"\t<key>ProgramArguments</key>\n"\
|
14
|
+
"\t<array>\n"\
|
15
|
+
"\t\t<string>#{File.expand_path('~/.henchman/run.sh')}</string>\n"\
|
16
|
+
"\t</array>\n"\
|
17
|
+
"\t<key>StartInterval</key>\n"\
|
18
|
+
"\t<integer>#{config[:poll_itunes_open]}</integer>\n"\
|
19
|
+
"\t<key>StandardOutPath</key>\n"\
|
20
|
+
"\t<string>#{File.expand_path('~/.henchman/stdout.log')}</string>\n"\
|
21
|
+
"\t<key>StandardErrorPath</key>\n"\
|
22
|
+
"\t<string>#{File.expand_path('~/.henchman/stderr.log')}</string>\n"\
|
23
|
+
"</dict>\n"\
|
24
|
+
"</plist>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.shell_script
|
28
|
+
"#!/bin/sh\n"\
|
29
|
+
"#{`which henchman`.chomp} run"
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.config
|
33
|
+
yml = {
|
34
|
+
:dropbox => {:key => '',
|
35
|
+
:secret => '',
|
36
|
+
:access_token => '',
|
37
|
+
:user_id => '',
|
38
|
+
:root => ''},
|
39
|
+
:root => '',
|
40
|
+
:poll_itunes_open => 10,
|
41
|
+
:poll_track => 3,
|
42
|
+
:reprompt_timeout => 300,
|
43
|
+
:delimiter => '|~|'
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: henchman-sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Greg Merritt
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: commander
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dropbox-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
description: OS X app that sits on top of iTunes to sync music with Dropbox
|
84
|
+
email:
|
85
|
+
- gremerritt@gmail.com
|
86
|
+
executables:
|
87
|
+
- henchman
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- LICENSE.txt
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- exe/henchman
|
101
|
+
- henchman.gemspec
|
102
|
+
- lib/applescript.rb
|
103
|
+
- lib/configure.rb
|
104
|
+
- lib/core.rb
|
105
|
+
- lib/dropbox.rb
|
106
|
+
- lib/henchman.rb
|
107
|
+
- lib/henchman/version.rb
|
108
|
+
- lib/launchd_handler.rb
|
109
|
+
- lib/templates.rb
|
110
|
+
homepage: https://www.github.com/gremerritt/henchman
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata:
|
114
|
+
allowed_push_host: https://rubygems.org/
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.5.1
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Cloud music syncing for iTunes on OS X
|
135
|
+
test_files: []
|