spinna 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +67 -0
- data/Rakefile +16 -0
- data/bin/spinna +7 -0
- data/lib/spinna.rb +12 -0
- data/lib/spinna/cli.rb +23 -0
- data/lib/spinna/client.rb +44 -0
- data/lib/spinna/config.rb +86 -0
- data/lib/spinna/history.rb +76 -0
- data/lib/spinna/music_library.rb +36 -0
- data/lib/spinna/picker.rb +52 -0
- data/lib/spinna/version.rb +3 -0
- data/spec/fixtures/data_dir/config.yml +3 -0
- data/spec/fixtures/data_dir/config1.yml +2 -0
- data/spec/fixtures/data_dir/config2.yml +3 -0
- data/spec/fixtures/data_dir/config3.yml +4 -0
- data/spec/fixtures/data_dir/config4.yml +4 -0
- data/spec/fixtures/data_dir/history.log +3 -0
- data/spec/fixtures/source_dir/album1/.gitkeep +0 -0
- data/spec/fixtures/source_dir/album2/.gitkeep +0 -0
- data/spec/fixtures/source_dir/album3/.gitkeep +0 -0
- data/spec/functional/get_spec.rb +99 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/unit/client_spec.rb +115 -0
- data/spec/unit/config_spec.rb +125 -0
- data/spec/unit/history_spec.rb +82 -0
- data/spec/unit/music_library_spec.rb +59 -0
- data/spec/unit/picker_spec.rb +250 -0
- data/spinna.gemspec +30 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/bin/spinna
ADDED
data/lib/spinna.rb
ADDED
@@ -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
|
data/lib/spinna/cli.rb
ADDED
@@ -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
|