spinna 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ade338315b39f54cf3ba9a615c6dfa12cc361cbe
4
+ data.tar.gz: a0b6a23bd1b0716c67bed3445561d7686c5c86f0
5
+ SHA512:
6
+ metadata.gz: 039b7c87100b437bc5fd2d02aa769a9315637a5b6d4beb15aa1e6e2f339c66791cf9ec5f4f0b3d1f6655b4b87fce3ea58fa8e725a023b16a1b9915b3e9de025d
7
+ data.tar.gz: 843f620f9984395abb340b6e63074bb380abf5f478a4d15ea7c8037f54688efcd03496d6ab558327227e50dd7c32410bc1f33be9fcc766742e1f4cf3b028fad3
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - "2.1.1"
6
+ - jruby-19mode # JRuby in 1.9 mode
7
+
8
+ script: bundle exec rake test
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spinna.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Patrick Paul-Hus
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ # Spinna
2
+
3
+ I have a *lot* of music. This little application helps me make sure that I don’t
4
+ always listen to the same stuff because I seem to be really bad at randomizing
5
+ my album selection.
6
+
7
+ Spinna will simply select some random albums from a given location and copy them
8
+ to my current working directory. It will also keep track of the selections made
9
+ in order to improve the rotation by not allowing a recently selected album to be
10
+ selected again.
11
+
12
+ ## Requirements
13
+
14
+ Spinna requires Ruby 1.9+.
15
+
16
+ ## Installation
17
+
18
+ `gem install spinna`
19
+
20
+ ## Configuration
21
+
22
+ First, spinna needs to be configured. Create a folder named `.spinna` in
23
+ your home directory and create a file named `config.yml`. If you run spinna
24
+ before creating the folder in your home directory, it will be created but
25
+ you will have to create the config file yourself.
26
+
27
+ The configuration file should look like this:
28
+
29
+ ```yaml
30
+ source_dir: '/foo'
31
+ history_size: 50
32
+ number_of_picks: 10
33
+ ```
34
+
35
+ The `source_dir` represents the location of your music library on your filesystem.
36
+ It is assumed that this directory contains a flat hierarchy of folders, your albums.
37
+ It is required.
38
+
39
+ The `history_size` is how many picks do we keep in the history log before we start
40
+ removing the older picks. If you do not supply it, the default value is 50.
41
+
42
+ The `number_of_picks` represents the default number of album picks you want Spinna
43
+ to make when you run it. If you do not supply it, the default value is 10.
44
+
45
+ ## Usage
46
+
47
+ To run, simply do:
48
+
49
+ `spinna get`
50
+
51
+ and it will download picks in the current working directory.
52
+
53
+ You can also request a given number of picks if you want:
54
+
55
+ `spinna get -n 10`
56
+
57
+ and it will pick 10 albums from your music library and download them
58
+ in the current working directory.
59
+
60
+
61
+ ## Contributing
62
+
63
+ 1. Fork it ( https://github.com/hydrozen/spinna/fork )
64
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
65
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
66
+ 4. Push to the branch (`git push origin my-new-feature`)
67
+ 5. Create a new Pull Request
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ task :default => [:test]
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'spec'
8
+ t.pattern = "spec/**/*_spec.rb"
9
+ end
10
+
11
+ task :console do
12
+ require 'pry'
13
+ require 'spinna'
14
+ ARGV.clear
15
+ Pry.start
16
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require 'spinna/cli'
6
+
7
+ Spinna::CLI.start(ARGV)
@@ -0,0 +1,12 @@
1
+ require "spinna/version"
2
+
3
+ module Spinna
4
+ # Base class for all errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the configuration file was not found.
8
+ class ConfigFileNotFoundError < Error; end
9
+
10
+ # Raised when the configuration file is not valid.
11
+ class InvalidConfigFileError < Error; end
12
+ end
@@ -0,0 +1,23 @@
1
+ require 'thor'
2
+ require 'yaml'
3
+ require 'spinna'
4
+ require 'spinna/client'
5
+
6
+ module Spinna
7
+ class CLI < Thor
8
+ desc 'get', 'Downloads some fresh music'
9
+ method_option :number_of_picks, :aliases => '-n', :type => :numeric
10
+ method_option :pattern, :aliases => '-p', :type => :string
11
+
12
+ def get
13
+ begin
14
+ client = Client.new
15
+ client.get(options)
16
+ exit 0
17
+ rescue StandardError => e
18
+ puts e.message
19
+ exit 1
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ require 'fileutils'
2
+ require 'spinna/history'
3
+ require 'spinna/config'
4
+ require 'spinna/picker'
5
+ require 'spinna/music_library'
6
+
7
+ module Spinna
8
+ # The client takes care of initializing the application by reading
9
+ # the configuration and the history. It exposes the various commands
10
+ # that spinna can do.
11
+ class Client
12
+ attr_accessor :library
13
+ attr_accessor :config
14
+ attr_accessor :history
15
+
16
+ # Boots the application.
17
+ def initialize
18
+ init_configuration
19
+ init_history
20
+ init_library
21
+ end
22
+
23
+ # Get a certain number of random albums from the music collection.
24
+ def get(opts = {})
25
+ number_of_picks = opts[:number_of_picks] || config.number_of_picks
26
+ pattern = opts[:pattern] || nil
27
+ Picker.new(config, history, library).pick(number_of_picks, pattern)
28
+ end
29
+
30
+ private
31
+
32
+ def init_configuration
33
+ @config = Config.new
34
+ end
35
+
36
+ def init_history
37
+ @history = Spinna::History.new(config)
38
+ end
39
+
40
+ def init_library
41
+ @library = Spinna::MusicLibrary.new(config, history)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,86 @@
1
+ require 'yaml'
2
+ require 'spinna'
3
+
4
+ module Spinna
5
+ # Takes care of reading the configuration file and exposing
6
+ # the settings.
7
+ class Config
8
+ # Default number of albums to keep in the history.
9
+ DEFAULT_HISTORY_SIZE = 50
10
+
11
+ # Default number of albums to pick when spinna is run.
12
+ DEFAULT_NUMBER_OF_PICKS = 10
13
+
14
+ # The directory where spinna stores its data.
15
+ attr_accessor :data_dir
16
+
17
+ # The directory where all the albums are stored.
18
+ attr_accessor :source_dir
19
+
20
+ # The number previously picked albums to keep in the
21
+ # history. Defaults to DEFAULT_NUMBER_OF_PICKS.
22
+ attr_accessor :history_size
23
+
24
+ # The number of picks to fetch if not explicitly given.
25
+ attr_accessor :number_of_picks
26
+
27
+ # The name of the config file to load. (ex: config.yml)
28
+ attr_accessor :config_file
29
+
30
+ def initialize(opts = {})
31
+ @data_dir = opts[:data_dir] || ENV['SPINNA_DATA_DIR'] || File.join(Dir.home, '.spinna')
32
+ @config_file = opts[:config_file] || 'config.yml'
33
+ ensure_data_dir_exists
34
+ ensure_configuration_exists
35
+ parse_configuration
36
+ end
37
+
38
+ private
39
+
40
+ def parse_configuration
41
+ config = read_configuration_file
42
+ @source_dir = config['source_dir']
43
+ @history_size = config['history_size'] || DEFAULT_HISTORY_SIZE
44
+ @number_of_picks = config['number_of_picks'] || DEFAULT_NUMBER_OF_PICKS
45
+ validate_config
46
+ end
47
+
48
+ def validate_config
49
+ unless source_dir
50
+ raise Spinna::InvalidConfigFileError, "You must set a source_dir in your configuration file."
51
+ end
52
+ end
53
+
54
+ def ensure_data_dir_exists
55
+ if !File.exists?(data_dir)
56
+ Dir.mkdir(data_dir, 0700)
57
+ raise Spinna::ConfigFileNotFoundError, "Could not find the configuration file. Please create it at #{config_path}."
58
+ end
59
+ end
60
+
61
+ def ensure_configuration_exists
62
+ unless config_file_exists?
63
+ raise Spinna::ConfigFileNotFoundError, "Could not find the configuration file. Please create it at #{config_path}."
64
+ end
65
+ end
66
+
67
+ def read_configuration_file
68
+ begin
69
+ config = YAML.load_file(File.join(data_dir, config_file))
70
+ raise StandardError unless config.is_a?(Hash)
71
+ rescue StandardError
72
+ raise Spinna::InvalidConfigFileError, "Could not parse the configuration file. Please make sure it is valid YAML."
73
+ end
74
+ config
75
+ end
76
+
77
+ def config_path
78
+ File.join(data_dir, config_file)
79
+ end
80
+
81
+ def config_file_exists?
82
+ File.exists?(config_path)
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,76 @@
1
+ require 'spinna/config'
2
+
3
+ module Spinna
4
+ # Takes care of tracking the picks that have been recently made
5
+ # so that they are not picked again too soon. The log is persisted
6
+ # in ~/.spinna/history.log by default.
7
+ class History
8
+ # The configuration of the application.
9
+ attr_reader :config
10
+
11
+ # An array that represents the log entries.
12
+ # The oldest entries are first.
13
+ attr_reader :log
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ read_history_log
18
+ end
19
+
20
+ # Add album at the end of the history log unless it’s
21
+ # already in there.
22
+ def append(album)
23
+ @log << album unless @log.include?(album)
24
+ trim if full?
25
+ end
26
+
27
+ # Is the given album in the history already?
28
+ def include?(album)
29
+ @log.include?(album)
30
+ end
31
+
32
+ # Saves the history to file after removing old picks
33
+ # from the log if needed.
34
+ def save
35
+ write_history_to_disk
36
+ end
37
+
38
+ # Returns the path to the history log file on the filesystem.
39
+ def history_path
40
+ File.join(config.data_dir, 'history.log')
41
+ end
42
+
43
+ private
44
+
45
+ # Are there more albums in the log than there should be?
46
+ def full?
47
+ @log.size > config.history_size
48
+ end
49
+
50
+ # Deletes the old albums that are now outdated depending
51
+ # on the history_size.
52
+ def trim
53
+ @log = @log.last(config.history_size)
54
+ end
55
+
56
+ def write_history_to_disk
57
+ File.open(history_path, 'w+') do |f|
58
+ @log.each { |album| f.puts album }
59
+ end
60
+ end
61
+
62
+ # Reads the history.log file and stores all the lines
63
+ # in an array.
64
+ def read_history_log
65
+ if !history_file_exists?
66
+ @log = [] and return
67
+ end
68
+
69
+ @log = File.read(history_path).split("\n")
70
+ end
71
+
72
+ def history_file_exists?
73
+ File.exists?(history_path)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,36 @@
1
+ module Spinna
2
+ # This class encapsulates the access to the music library.
3
+ class MusicLibrary
4
+ def initialize(config, history)
5
+ @config = config
6
+ @history = history
7
+ end
8
+
9
+ # Returns the albums that are pickable. An album is pickable
10
+ # if it isn’t present in the history log. If a pattern is given,
11
+ # then only the albums that match the pattern will be returned.
12
+ def pickable_albums(pattern = nil)
13
+ return available_albums unless pattern
14
+ available_albums.select! { |a| a =~ Regexp.new(pattern)}
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :config
20
+ attr_reader :history
21
+
22
+ def albums
23
+ Dir.entries(config.source_dir).reject! { |i| i =~ /\A\./ }
24
+ end
25
+
26
+ # Returns the list of previously selected albums.
27
+ def previously_selected_albums
28
+ history.log
29
+ end
30
+
31
+ # Returns only the albums that are available for selection.
32
+ def available_albums
33
+ albums - previously_selected_albums
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ module Spinna
2
+ # The picker is responsible for reading the media library, selecting
3
+ # albums from it and updating the history log. Basically, it does the
4
+ # grunt work.
5
+ class Picker
6
+ def initialize(config, history, library)
7
+ @config = config
8
+ @history = history
9
+ @library = library
10
+ end
11
+
12
+ # Picks the requested number of picks from the available albums.
13
+ def pick(number_of_picks, pattern = nil)
14
+ picks = pick_albums(number_of_picks, pattern)
15
+ copy_picks(picks)
16
+ update_history(picks)
17
+ picks
18
+ end
19
+
20
+ private
21
+
22
+ attr_accessor :config
23
+ attr_accessor :history
24
+ attr_accessor :library
25
+
26
+ def pick_albums(number_of_picks, pattern = nil)
27
+ albums = library.pickable_albums(pattern)
28
+ return albums if number_of_picks >= albums.size
29
+ choose_random_picks(number_of_picks, albums)
30
+ end
31
+
32
+ # Does the actual picking of random albums.
33
+ def choose_random_picks(number_of_picks, albums)
34
+ return albums if albums.size < number_of_picks
35
+ albums.sample(number_of_picks)
36
+ end
37
+
38
+ # Copies the picks to the current working directory.
39
+ def copy_picks(picks)
40
+ picks.each do |pick|
41
+ puts "Copying #{pick}..."
42
+ FileUtils.cp_r("#{config.source_dir}/#{pick}", Dir.pwd)
43
+ end
44
+ end
45
+
46
+ # Updates the history.
47
+ def update_history(picks)
48
+ picks.each { |pick| history.append(pick) }
49
+ history.save
50
+ end
51
+ end
52
+ end