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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in henchman.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/henchman ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "henchman"
4
+ Henchman.run
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Henchman
2
+ VERSION = "0.1.0"
3
+ 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: []