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.
- 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
|