music_blender 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +67 -0
  4. data/ID3TAGS.txt +26 -0
  5. data/LICENSE +20 -0
  6. data/README.md +41 -0
  7. data/Rakefile +27 -0
  8. data/bin/blend +5 -0
  9. data/db/migrate/001_create_root_folders.rb +10 -0
  10. data/db/migrate/002_create_tracks.rb +15 -0
  11. data/db/migrate/003_rename_root_folders_table_to_music_folders.rb +6 -0
  12. data/db/migrate/004_create_artists.rb +11 -0
  13. data/db/migrate/005_add_missing_column_to_tracks.rb +5 -0
  14. data/db/schema.rb +47 -0
  15. data/lib/music_blender.rb +26 -0
  16. data/lib/music_blender/artist.rb +6 -0
  17. data/lib/music_blender/bootstrap.rb +32 -0
  18. data/lib/music_blender/db_adapter.rb +58 -0
  19. data/lib/music_blender/id3_adapter.rb +70 -0
  20. data/lib/music_blender/music_folder.rb +51 -0
  21. data/lib/music_blender/player.rb +83 -0
  22. data/lib/music_blender/player_monitor.rb +77 -0
  23. data/lib/music_blender/shell.rb +59 -0
  24. data/lib/music_blender/track.rb +68 -0
  25. data/lib/music_blender/version.rb +3 -0
  26. data/music_blender.gemspec +28 -0
  27. data/test/factories/artists.rb +7 -0
  28. data/test/factories/music_folders.rb +15 -0
  29. data/test/factories/tracks.rb +12 -0
  30. data/test/music/point1sec.mp3 +0 -0
  31. data/test/music/subfolder/insubfolder.mp3 +0 -0
  32. data/test/music/test1.txt +0 -0
  33. data/test/music/test2.txt +0 -0
  34. data/test/test_helper.rb +53 -0
  35. data/test/unit/artist_test.rb +9 -0
  36. data/test/unit/bootstrap_test.rb +32 -0
  37. data/test/unit/db_adapter_test.rb +37 -0
  38. data/test/unit/id3_adapter_test.rb +42 -0
  39. data/test/unit/music_folder_test.rb +92 -0
  40. data/test/unit/player_monitor_test.rb +70 -0
  41. data/test/unit/player_test.rb +76 -0
  42. data/test/unit/shell_test.rb +71 -0
  43. data/test/unit/track_test.rb +48 -0
  44. metadata +250 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ coverage
2
+ *.swp
3
+ log/*
4
+ db/*.db
5
+ *.backup
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
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,5 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/music_blender'
3
+
4
+ MusicBlender::MUSIC_PATH = ARGV.pop
5
+ MusicBlender::Bootstrap.new.call
@@ -0,0 +1,10 @@
1
+ class CreateRootFolders < ActiveRecord::Migration
2
+ def change
3
+ create_table :root_folders do |t|
4
+ t.column :path, :string, :null => false
5
+
6
+ t.timestamps
7
+ end
8
+ add_index :root_folders, :path
9
+ end
10
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ class RenameRootFoldersTableToMusicFolders < ActiveRecord::Migration
2
+ def change
3
+ rename_table :root_folders, :music_folders
4
+ rename_column :tracks, :root_folder_id, :music_folder_id
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class CreateArtists < ActiveRecord::Migration
2
+ def change
3
+ create_table :artists do |t|
4
+ t.column :name, :string, :null => false
5
+
6
+ t.timestamps
7
+ end
8
+ add_index :artists, :name
9
+ add_column :tracks, :artist_id, :integer
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class AddMissingColumnToTracks < ActiveRecord::Migration
2
+ def change
3
+ add_column :tracks, :missing, :boolean, :default => false
4
+ end
5
+ 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,6 @@
1
+ module MusicBlender
2
+ class Artist < ActiveRecord::Base
3
+ has_many :tracks
4
+ validates_uniqueness_of :name
5
+ end
6
+ 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