music_blender 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +67 -0
- data/ID3TAGS.txt +26 -0
- data/LICENSE +20 -0
- data/README.md +41 -0
- data/Rakefile +27 -0
- data/bin/blend +5 -0
- data/db/migrate/001_create_root_folders.rb +10 -0
- data/db/migrate/002_create_tracks.rb +15 -0
- data/db/migrate/003_rename_root_folders_table_to_music_folders.rb +6 -0
- data/db/migrate/004_create_artists.rb +11 -0
- data/db/migrate/005_add_missing_column_to_tracks.rb +5 -0
- data/db/schema.rb +47 -0
- data/lib/music_blender.rb +26 -0
- data/lib/music_blender/artist.rb +6 -0
- data/lib/music_blender/bootstrap.rb +32 -0
- data/lib/music_blender/db_adapter.rb +58 -0
- data/lib/music_blender/id3_adapter.rb +70 -0
- data/lib/music_blender/music_folder.rb +51 -0
- data/lib/music_blender/player.rb +83 -0
- data/lib/music_blender/player_monitor.rb +77 -0
- data/lib/music_blender/shell.rb +59 -0
- data/lib/music_blender/track.rb +68 -0
- data/lib/music_blender/version.rb +3 -0
- data/music_blender.gemspec +28 -0
- data/test/factories/artists.rb +7 -0
- data/test/factories/music_folders.rb +15 -0
- data/test/factories/tracks.rb +12 -0
- data/test/music/point1sec.mp3 +0 -0
- data/test/music/subfolder/insubfolder.mp3 +0 -0
- data/test/music/test1.txt +0 -0
- data/test/music/test2.txt +0 -0
- data/test/test_helper.rb +53 -0
- data/test/unit/artist_test.rb +9 -0
- data/test/unit/bootstrap_test.rb +32 -0
- data/test/unit/db_adapter_test.rb +37 -0
- data/test/unit/id3_adapter_test.rb +42 -0
- data/test/unit/music_folder_test.rb +92 -0
- data/test/unit/player_monitor_test.rb +70 -0
- data/test/unit/player_test.rb +76 -0
- data/test/unit/shell_test.rb +71 -0
- data/test/unit/track_test.rb +48 -0
- metadata +250 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
music_blender (0.0.1)
|
5
|
+
activerecord
|
6
|
+
sqlite3
|
7
|
+
taglib-ruby
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.0.0)
|
13
|
+
activesupport (= 4.0.0)
|
14
|
+
builder (~> 3.1.0)
|
15
|
+
activerecord (4.0.0)
|
16
|
+
activemodel (= 4.0.0)
|
17
|
+
activerecord-deprecated_finders (~> 1.0.2)
|
18
|
+
activesupport (= 4.0.0)
|
19
|
+
arel (~> 4.0.0)
|
20
|
+
activerecord-deprecated_finders (1.0.3)
|
21
|
+
activesupport (4.0.0)
|
22
|
+
i18n (~> 0.6, >= 0.6.4)
|
23
|
+
minitest (~> 4.2)
|
24
|
+
multi_json (~> 1.3)
|
25
|
+
thread_safe (~> 0.1)
|
26
|
+
tzinfo (~> 0.3.37)
|
27
|
+
arel (4.0.0)
|
28
|
+
assert_difference (0.5.0)
|
29
|
+
atomic (1.1.13)
|
30
|
+
builder (3.1.4)
|
31
|
+
coderay (1.0.9)
|
32
|
+
factory_girl (4.2.0)
|
33
|
+
activesupport (>= 3.0.0)
|
34
|
+
fakefs (0.4.2)
|
35
|
+
i18n (0.6.5)
|
36
|
+
metaclass (0.0.1)
|
37
|
+
method_source (0.8.1)
|
38
|
+
minitest (4.7.5)
|
39
|
+
mocha (0.14.0)
|
40
|
+
metaclass (~> 0.0.1)
|
41
|
+
multi_json (1.7.4)
|
42
|
+
pry (0.9.12.1)
|
43
|
+
coderay (~> 1.0.5)
|
44
|
+
method_source (~> 0.8)
|
45
|
+
slop (~> 3.4)
|
46
|
+
simplecov (0.7.1)
|
47
|
+
multi_json (~> 1.0)
|
48
|
+
simplecov-html (~> 0.7.1)
|
49
|
+
simplecov-html (0.7.1)
|
50
|
+
slop (3.4.4)
|
51
|
+
sqlite3 (1.3.7)
|
52
|
+
taglib-ruby (0.6.0)
|
53
|
+
thread_safe (0.1.2)
|
54
|
+
atomic
|
55
|
+
tzinfo (0.3.37)
|
56
|
+
|
57
|
+
PLATFORMS
|
58
|
+
ruby
|
59
|
+
|
60
|
+
DEPENDENCIES
|
61
|
+
assert_difference
|
62
|
+
factory_girl
|
63
|
+
fakefs
|
64
|
+
mocha
|
65
|
+
music_blender!
|
66
|
+
pry
|
67
|
+
simplecov
|
data/ID3TAGS.txt
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Adding ID3 Comments
|
2
|
+
|
3
|
+
gem 'taglib-ruby', :require => 'taglib'
|
4
|
+
require 'taglib'
|
5
|
+
file = TagLib::MPEG::File.new('/path/to/file.mp3')
|
6
|
+
tag = file.id3v2_tag(true)
|
7
|
+
|
8
|
+
comment = TagLib::ID3v2::CommentsFrame.new()
|
9
|
+
comment.description = 'MMP Rating'
|
10
|
+
comment.text = '10'
|
11
|
+
|
12
|
+
tag.add_frame(comment)
|
13
|
+
|
14
|
+
file.save
|
15
|
+
##############
|
16
|
+
|
17
|
+
tag.frame_list #=> array of frames.
|
18
|
+
|
19
|
+
# Get array of comment frames
|
20
|
+
tag.frame_list.select {|frame| frame.is_a?(TagLib::ID3v2::CommentsFrame) }
|
21
|
+
|
22
|
+
# Get MMP Rating Frame
|
23
|
+
tag.frame_list.detect { |frame|
|
24
|
+
frame.is_a?(TagLib::ID3v2::CommentsFrame) and
|
25
|
+
frame.description == 'MMP Rating'
|
26
|
+
}
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2013 Jonathan S Garvin <jon(at)(numeral-five)valleys(dot)com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
copy of this software and associated documentation files (the "Software"),
|
5
|
+
to deal in the Software without restriction, including without limitation
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be
|
11
|
+
included in all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# MusicBlender
|
2
|
+
|
3
|
+
A ruby wrapper around [mpg123](http://www.mpg123.de/ "mpg123") providing some additional features.
|
4
|
+
|
5
|
+
## Motivation
|
6
|
+
|
7
|
+
I was unsatisfied with how every other mp3 player I'd tried randomized music. So, I wrote my own to satisfy my desires. Maybe it will satisy yours, too.
|
8
|
+
|
9
|
+
MusicBlender...
|
10
|
+
|
11
|
+
1. allows you to rate songs on any scale you choose (but I recommend using 1-11).
|
12
|
+
2. writes ratings to custom field in ID3 tags, for persistance across devices.
|
13
|
+
3. plays those with a higher rating more frequently.
|
14
|
+
4. is more likely to play a song that it hasn't played in a while than another of equal rating that it's played more recently.
|
15
|
+
5. will not play songs by the same artist too close to each other.
|
16
|
+
6. supports relative volume adjustments embeded in ID3 tags with mpg123's --rva-mix flag.
|
17
|
+
7. currently is operated from it's own command shell. Maybe someday I'll create a GUI. Maybe I won't.
|
18
|
+
|
19
|
+
## Dependencies
|
20
|
+
|
21
|
+
MusicBlender requires you to have [mpg123](http://www.mpg123.de/ "mpg123") installed and in your path. MusicBlender does *not* play well with mpg321.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
1. install [mpg123](http://www.mpg123.de/ "mpg123")
|
26
|
+
2. `gem install music_blender`
|
27
|
+
|
28
|
+
## Quick Start
|
29
|
+
|
30
|
+
1. From the console, run `blender /path/to/music/folder`
|
31
|
+
2. From MusicBlender's command prompt, enter `play`
|
32
|
+
|
33
|
+
## Additional Commands
|
34
|
+
|
35
|
+
1. `pause` - pause and restart playback.
|
36
|
+
2. `play` - play a new song (aborts current song if already playing).
|
37
|
+
3. `stop` - stop playback.
|
38
|
+
4. `exit` - stop playback and exit.
|
39
|
+
5. `info` - print out info about currently play song.
|
40
|
+
6. `rate` # - set current songs rating to #.
|
41
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require File.expand_path('../lib/music_blender', __FILE__)
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'lib'
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :db do
|
14
|
+
desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"
|
15
|
+
task :migrate => :environment do
|
16
|
+
dba.migrate_db
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
task :environment do
|
21
|
+
dba.establish_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
def dba
|
25
|
+
@dba ||= MyMusicPlayer::DbAdapter.new
|
26
|
+
end
|
27
|
+
|
data/bin/blend
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateTracks < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :tracks do |t|
|
4
|
+
t.column :relative_path, :string, :null => false
|
5
|
+
t.column :last_played_at, :datetime
|
6
|
+
t.column :rating, :integer
|
7
|
+
t.column :title, :string
|
8
|
+
t.column :root_folder_id, :integer
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
add_index :tracks, [:root_folder_id, :relative_path]
|
13
|
+
add_index :tracks, [:root_folder_id, :last_played_at]
|
14
|
+
end
|
15
|
+
end
|
data/db/schema.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# This file is auto-generated from the current state of the database. Instead
|
3
|
+
# of editing this file, please use the migrations feature of Active Record to
|
4
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
5
|
+
#
|
6
|
+
# Note that this schema.rb definition is the authoritative source for your
|
7
|
+
# database schema. If you need to create the application database on another
|
8
|
+
# system, you should be using db:schema:load, not running all the migrations
|
9
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
10
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
11
|
+
#
|
12
|
+
# It's strongly recommended that you check this file into your version control system.
|
13
|
+
|
14
|
+
ActiveRecord::Schema.define(version: 5) do
|
15
|
+
|
16
|
+
create_table "artists", force: true do |t|
|
17
|
+
t.string "name", null: false
|
18
|
+
t.datetime "created_at"
|
19
|
+
t.datetime "updated_at"
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index "artists", ["name"], name: "index_artists_on_name"
|
23
|
+
|
24
|
+
create_table "music_folders", force: true do |t|
|
25
|
+
t.string "path", null: false
|
26
|
+
t.datetime "created_at"
|
27
|
+
t.datetime "updated_at"
|
28
|
+
end
|
29
|
+
|
30
|
+
add_index "music_folders", ["path"], name: "index_music_folders_on_path"
|
31
|
+
|
32
|
+
create_table "tracks", force: true do |t|
|
33
|
+
t.string "relative_path", null: false
|
34
|
+
t.datetime "last_played_at"
|
35
|
+
t.integer "rating"
|
36
|
+
t.string "title"
|
37
|
+
t.integer "music_folder_id"
|
38
|
+
t.datetime "created_at"
|
39
|
+
t.datetime "updated_at"
|
40
|
+
t.integer "artist_id"
|
41
|
+
t.boolean "missing", default: false
|
42
|
+
end
|
43
|
+
|
44
|
+
add_index "tracks", ["music_folder_id", "last_played_at"], name: "index_tracks_on_music_folder_id_and_last_played_at"
|
45
|
+
add_index "tracks", ["music_folder_id", "relative_path"], name: "index_tracks_on_music_folder_id_and_relative_path"
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'etc'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'find'
|
6
|
+
require 'io/wait'
|
7
|
+
require 'singleton'
|
8
|
+
require 'sqlite3'
|
9
|
+
require 'taglib'
|
10
|
+
require 'open3'
|
11
|
+
|
12
|
+
require_relative 'music_blender/artist'
|
13
|
+
require_relative 'music_blender/bootstrap'
|
14
|
+
require_relative 'music_blender/db_adapter'
|
15
|
+
require_relative 'music_blender/id3_adapter'
|
16
|
+
require_relative 'music_blender/music_folder'
|
17
|
+
require_relative 'music_blender/player'
|
18
|
+
require_relative 'music_blender/player_monitor'
|
19
|
+
require_relative 'music_blender/shell'
|
20
|
+
require_relative 'music_blender/track'
|
21
|
+
|
22
|
+
Thread.abort_on_exception = true
|
23
|
+
|
24
|
+
module MusicBlender
|
25
|
+
BLENDER_ROOT = File.expand_path('../../', __FILE__)
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module MusicBlender
|
2
|
+
class Bootstrap
|
3
|
+
|
4
|
+
def call
|
5
|
+
spin_up_db
|
6
|
+
update_tracks
|
7
|
+
launch_shell
|
8
|
+
end
|
9
|
+
|
10
|
+
#######
|
11
|
+
private
|
12
|
+
#######
|
13
|
+
|
14
|
+
def spin_up_db
|
15
|
+
DbAdapter.new.spin_up
|
16
|
+
end
|
17
|
+
|
18
|
+
def update_tracks
|
19
|
+
music_folder.load_tracks
|
20
|
+
music_folder.update_missing_flags
|
21
|
+
end
|
22
|
+
|
23
|
+
def launch_shell
|
24
|
+
puts catch(:exited) { Shell.new.run(*ARGV) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def music_folder
|
28
|
+
MusicFolder.current
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module MusicBlender
|
2
|
+
class DbAdapter
|
3
|
+
attr_reader :connection
|
4
|
+
|
5
|
+
def spin_up
|
6
|
+
establish_connection
|
7
|
+
silence_stream(STDOUT) { initialize_or_migrate_db }
|
8
|
+
setup_db_log
|
9
|
+
end
|
10
|
+
|
11
|
+
def establish_connection
|
12
|
+
FileUtils.mkdir_p(db_folder)
|
13
|
+
@connection ||= ActiveRecord::Base.establish_connection(
|
14
|
+
:adapter => 'sqlite3',
|
15
|
+
:database => path_to_db
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def migrate_db
|
20
|
+
ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
|
21
|
+
File.open(path_to_schema, 'w:utf-8') do |file|
|
22
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#######
|
27
|
+
private
|
28
|
+
#######
|
29
|
+
|
30
|
+
def initialize_or_migrate_db
|
31
|
+
if File.exist?(path_to_db)
|
32
|
+
migrate_db
|
33
|
+
else
|
34
|
+
initialize_db
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize_db
|
39
|
+
load(path_to_schema) #load and run schema.rb
|
40
|
+
end
|
41
|
+
|
42
|
+
def path_to_db
|
43
|
+
@path_to_db ||= "#{db_folder}/music_blender.db"
|
44
|
+
end
|
45
|
+
|
46
|
+
def db_folder
|
47
|
+
"#{Etc.getpwuid.dir}/.music_blender/db"
|
48
|
+
end
|
49
|
+
|
50
|
+
def path_to_schema
|
51
|
+
@path_to_schema ||= "#{BLENDER_ROOT}/db/schema.rb"
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup_db_log
|
55
|
+
ActiveRecord::Base.logger ||= Logger.new("#{BLENDER_ROOT}/log/database.log",'daily')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module MusicBlender
|
2
|
+
class Id3Adapter
|
3
|
+
RATING_FRAME_DESCRIPTION = 'MMP Rating'
|
4
|
+
attr_reader :initial_rating, :path
|
5
|
+
|
6
|
+
def initialize(path,initial_rating)
|
7
|
+
@path = path
|
8
|
+
@initial_rating = initial_rating
|
9
|
+
end
|
10
|
+
|
11
|
+
def title
|
12
|
+
v2tag.title || v1tag.title
|
13
|
+
end
|
14
|
+
|
15
|
+
def artist
|
16
|
+
v2tag.artist || v1tag.artist
|
17
|
+
end
|
18
|
+
|
19
|
+
def rating
|
20
|
+
rating_frame.text.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_rating(value)
|
24
|
+
rating_frame.text = value.to_s
|
25
|
+
tag_file.save
|
26
|
+
end
|
27
|
+
|
28
|
+
#######
|
29
|
+
private
|
30
|
+
#######
|
31
|
+
|
32
|
+
def v1tag
|
33
|
+
@v1tag ||= tag_file.id3v1_tag(true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def v2tag
|
37
|
+
@v2tag ||= tag_file.id3v2_tag(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def tag_file
|
41
|
+
@tag_file ||= TagLib::MPEG::File.new(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def rating_frame
|
45
|
+
@rating_frame ||= (find_rating_frame || create_and_find_rating_frame)
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_rating_frame
|
49
|
+
v2tag.frame_list.detect { |frame|
|
50
|
+
frame.is_a?(TagLib::ID3v2::CommentsFrame) and
|
51
|
+
frame.description == RATING_FRAME_DESCRIPTION
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_and_find_rating_frame
|
56
|
+
TagLib::ID3v2::CommentsFrame.new.tap do |new_frame|
|
57
|
+
new_frame.description = RATING_FRAME_DESCRIPTION
|
58
|
+
new_frame.text = (initial_rating || 1).to_s
|
59
|
+
v2tag.add_frame(new_frame)
|
60
|
+
tag_file.save
|
61
|
+
end
|
62
|
+
# Due to quirk in TagLib, the new_frame
|
63
|
+
# from the above block is not accessible
|
64
|
+
# anymore, so need to re-find an instance
|
65
|
+
# of it that we can read and manipulate.
|
66
|
+
find_rating_frame
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|