itc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bbb1fbc96993d0b0cb77574b931c1ea62d2dbee8
4
+ data.tar.gz: 5d5b693c549eb9e7e01ca6364ce6fc09569dd3a7
5
+ SHA512:
6
+ metadata.gz: db27c20f1615babc9460ea5c33ee0412384190382b244fb58c6cc405bfc0766b27605c86311bbb82dee18d578db94af81cd34e0fe52099390aad0d726fa12cf7
7
+ data.tar.gz: 561aec5e3943d49608fd9ff6933c37a7c26da316f2f798b0a2a6ada4082bf877ee94d44425109d3ed86cecdeec9e46e427b2dfffb00a0782e735006b3f4feede
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ *.swp
24
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014, Ragnar B. Johannsson
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,74 @@
1
+ ## Itc
2
+
3
+ Itc is an iTunes command line client. It indexes iTunes' Music Library XML file into a SQLite3 database for faster lookup and provides shell tab completion for metadata.
4
+
5
+
6
+ ### Installation
7
+
8
+ Install the gem:
9
+
10
+ $ gem install itc
11
+
12
+ Copy the tab completion definitions under `extra/completion/` to the appropriate paths for your system and shell.
13
+
14
+
15
+ ### Usage
16
+
17
+ After installing, start by indexing the iTunes Music Library:
18
+
19
+ $ itc index
20
+
21
+ This generates a SQLite3 database in `$HOME/.itc.db` which will act as a cache for the iTunes Music Library XML files. Rerun the index command after adding new content to iTunes. Alternatively, add it to crontab to keep things in sync.
22
+
23
+ After indexing you can start querying the library:
24
+
25
+ $ itc list artist
26
+
27
+ will return a list of all artists in the library.
28
+
29
+ $ itc play artist=Daft\ Punk
30
+
31
+ will start to play all songs and albums by Daft Punk in the Library.
32
+
33
+ $ itc play artist=Ibrahim\ Maalouf album=Wind
34
+
35
+ will play the album Wind by Ibrahim Maalouf. Notice that if the tab completions are installed there is no need to laboriously spell out full names for the filters as they will tab complete just fine.
36
+
37
+ Recognized filters for the **list** and **play** commands are `artist`, `album`, `song`, `year` and `genre`.
38
+
39
+ Following are all recognized commands:
40
+
41
+ * **index** - (Re)index the music database
42
+ * **list** - Search for tracks
43
+ * **next** - Skip to next song
44
+ * **pause** - Pause playing
45
+ * **play** - Commence playing
46
+ * **previous** - Go back one song
47
+ * **status** - Show playing status, playlist position
48
+
49
+ To gauge the position within the current album or playlist use the **status** command:
50
+
51
+ $ itc play artist=Emika album=DVA
52
+ $ itc next
53
+ $ itc status
54
+ Emika - DVA - Hush (Interlude) feat. Michaela Srumova
55
+ ▶ Emika - DVA - Young Minds
56
+ Emika - DVA - She Beats
57
+ Emika - DVA - Filters
58
+ Emika - DVA - After The Fall
59
+ Emika - DVA - Sing To Me
60
+ Emika - DVA - Dem Worlds
61
+ Emika - DVA - Primary Colours
62
+ Emika - DVA - Sleep With My Enemies
63
+ Emika - DVA - Wicked Game
64
+ Emika - DVA - Fight For Your Love
65
+ Emika - DVA - Mouth To Mouth
66
+ Emika - DVA - Searching
67
+ Emika - DVA - Centuries
68
+ Emika - DVA - Criminal Gift
69
+ $
70
+
71
+
72
+ ### License
73
+
74
+ BSD 2-Clause. See the LICENSE file for details.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/itc ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'itc'
4
+
5
+ def usage(msg="")
6
+ puts "Error: #{msg}\n\n" unless msg == ""
7
+
8
+ puts <<-USAGE.gsub(/^ /, "")
9
+ Usage: itc <command> [filters...]
10
+
11
+ Commands:
12
+ help - This message
13
+ index - (Re)index the music database
14
+ list - Search for tracks
15
+ next - Next song
16
+ pause - Pause playing
17
+ play - Play
18
+ previous - Previous song
19
+ status - Playing status, playlist position
20
+
21
+ Filters (valid with play and list commands):
22
+ artist - Artist name
23
+ album - Album name
24
+ song - Song name
25
+ year - Release yeaar
26
+ genre - Genre name
27
+
28
+ USAGE
29
+
30
+ exit msg == "" ? 0 : 1
31
+ end
32
+
33
+ usage "No command specified" if ARGV.size() < 1
34
+
35
+ begin
36
+ cmd = ARGV.shift
37
+ filters = {}
38
+
39
+ usage if cmd == "help"
40
+
41
+ while action = ARGV.shift do
42
+ sp = action.split("=", 2)
43
+ filters[sp[0]] = sp.size() == 1 ? "" : sp[1]
44
+ end
45
+
46
+ Command.execute cmd, filters
47
+ rescue InvalidFilterNumberException => e
48
+ usage e.message
49
+ rescue UnknownFilterException => e
50
+ usage "Unknown filter #{e.message}"
51
+ rescue UnknownCommandException
52
+ usage "Unknown command #{cmd}"
53
+ end
54
+
@@ -0,0 +1,53 @@
1
+ #compdef itc
2
+
3
+ local context state state_descr line expl list_item
4
+ local -a commands
5
+ local -A opt_args
6
+
7
+ function _itc_list {
8
+ local i filters=""
9
+ for i in {2..$#line}; do
10
+ if [[ ! $line[$i] == $list_item* ]]; then
11
+ filters="${filters}$line[$i] "
12
+ fi
13
+ done
14
+
15
+ local cmd item IFS=$'\n'
16
+ cmd="itc list $filters $list_item"
17
+ for item in $(eval ${cmd}); do
18
+ compadd $expl[@] -- $item
19
+ done
20
+
21
+ return 0
22
+ }
23
+
24
+ _arguments '1:command:->cmds' '*:filter:->filters' && ret=0
25
+
26
+ case "$state" in
27
+ (cmds)
28
+ commands=(
29
+ 'list:Search for tracks' \
30
+ 'status:Show playing status, playlist position' \
31
+ 'play:Play' \
32
+ 'index:(Re)index the music database' \
33
+ 'next:Next song' \
34
+ 'previous:Previous song' \
35
+ 'pause:Pause playing')
36
+ _describe 'values' commands
37
+ ;;
38
+ (filters)
39
+ case "$line[1]" in
40
+ (play)
41
+ ;&
42
+ (list)
43
+ _values -w 'option' \
44
+ 'artist[Artist name]:artist:{list_item=artist _itc_list}' \
45
+ 'album[Album name]:album:{list_item=album _itc_list}' \
46
+ 'song[Song name]:song:{list_item=song _itc_list}' \
47
+ 'genre[Genre name]:genre:{list_item=genre _itc_list}' \
48
+ 'year[Release year]:year:{list_item=year _itc_list}'
49
+ ;;
50
+ esac
51
+ ;;
52
+ esac
53
+
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'itc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "itc"
8
+ spec.version = Itc::VERSION
9
+ spec.authors = ["Ragnar B. Johannsson"]
10
+ spec.email = ["ragnar@igo.is"]
11
+ spec.summary = %q{iTunes CLI}
12
+ spec.description = %q{iTunes CLI with tab completions}
13
+ spec.homepage = "https://github.com/ragnar-johannsson/itc"
14
+ spec.license = "BSD"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "sqlite3", "~> 1.3"
22
+ spec.add_runtime_dependency "htmlentities", "~> 4.3"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.6"
25
+ spec.add_development_dependency "rake"
26
+ end
@@ -0,0 +1,8 @@
1
+ require 'itc/version'
2
+
3
+ module Itc
4
+ require 'itc/musiclib'
5
+ require 'itc/database'
6
+ require 'itc/player'
7
+ require 'itc/command'
8
+ end
@@ -0,0 +1,49 @@
1
+ # coding: utf-8
2
+
3
+ class Command
4
+ def self.execute(action, filters)
5
+ db = Database.new
6
+ player = Player.new
7
+ xml = ENV["HOME"] + "/Music/iTunes/iTunes Music Library.xml"
8
+
9
+ supported_filters = ("artist" "album" "song" "genre" "year")
10
+ filters.keys.each do |filter|
11
+ raise UnknownFilterException, filter unless supported_filters.include? filter
12
+ end
13
+
14
+ case action
15
+ when "index"
16
+ db.reindex MusicLibrary.new(xml)
17
+ when "list"
18
+ raise InvalidFilterNumberException, "No filter specified" if filters.empty?
19
+ puts db.list(filters).collect { |f| f.last }
20
+ when "play"
21
+ player.play filters.empty? ? {} : db.list_ids(filters)
22
+ when "pause"
23
+ player.pause
24
+ when "next"
25
+ player.next
26
+ when "previous", "prev"
27
+ player.previous
28
+ when "status"
29
+ if player.state == "stopped"
30
+ puts "iTunes is #{player.state}\n"
31
+ return
32
+ end
33
+
34
+ curr_song = player.current_track
35
+ state_icon = player.state == "playing" ? "▶" : "‖"
36
+ playlist = player.current_playlist_tracks.split(", ")
37
+ db.list_by_ids(playlist.size < 1000 ? playlist : [curr_song]).collect do |track|
38
+ printf " %s %s - %s - %s\n", track[0].to_s == curr_song ? state_icon : " ",
39
+ track[1], track[2], track[3]
40
+ end
41
+ else
42
+ raise UnknownCommandException
43
+ end
44
+ end
45
+ end
46
+
47
+ class InvalidFilterNumberException < StandardError; end
48
+ class UnknownFilterException < StandardError; end
49
+ class UnknownCommandException < StandardError; end
@@ -0,0 +1,83 @@
1
+ # coding: utf-8
2
+
3
+ require 'sqlite3'
4
+
5
+ class Database
6
+ def initialize
7
+ @db = SQLite3::Database.new(ENV["HOME"] + "/.itc.db")
8
+ end
9
+
10
+ def reset
11
+ @db.execute_batch <<-SQL
12
+ drop index if exists tracks_artist_index;
13
+ drop index if exists tracks_album_index;
14
+ drop index if exists tracks_song_index;
15
+ drop index if exists tracks_genre_index;
16
+ drop index if exists tracks_year_index;
17
+ drop table if exists tracks;
18
+
19
+ create table tracks (
20
+ track_id INTEGER PRIMARY KEY,
21
+ artist VARCHAR DEFAULT NULL,
22
+ album VARCHAR DEFAULT NULL,
23
+ song VARCHAR DEFAULT NULL,
24
+ genre VARCHAR DEFAULT NULL,
25
+ year VARCHAR DEFAULT NULL,
26
+ disc_number INTEGER DEFAULT NULL,
27
+ track_number INTEGER DEFAULT NULL
28
+ );
29
+
30
+ create index tracks_artist_index on tracks (artist);
31
+ create index tracks_album_index on tracks (album);
32
+ create index tracks_song_index on tracks (song);
33
+ create index tracks_genre_index on tracks (genre);
34
+ create index tracks_year_index on tracks (year);
35
+ SQL
36
+ end
37
+
38
+ def add_tracks(tracks)
39
+ @db.transaction do |db|
40
+ db.prepare("insert into tracks values (?, ?, ?, ?, ?, ?, ?, ?)") do |stmt|
41
+ tracks.each do |t|
42
+ stmt.execute t[:track_id].to_i, t[:artist], t[:album], t[:name],
43
+ t[:genre], t[:year], t[:disc_number].to_i, t[:track_number].to_i
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def reindex(music)
50
+ reset
51
+ add_tracks music.tracks
52
+ end
53
+
54
+ def list(filters)
55
+ c = filters.keys.collect { |k| "#{k} like \"%#{filters[k]}%\"" }
56
+ condition = c.empty? ? "" : "where #{c.join(' and ')}"
57
+ fields = filters.keys.join(",")
58
+ @db.execute <<-SQL
59
+ select distinct #{fields} from tracks #{condition} order by artist, album, disc_number, track_number
60
+ SQL
61
+ end
62
+
63
+ def list_ids(filters)
64
+ c = filters.keys.collect { |k| "#{k} like \"%#{filters[k]}%\"" }
65
+ condition = c.empty? ? "" : "where #{c.join(' and ')}"
66
+ @db.execute <<-SQL
67
+ select track_id from tracks #{condition} order by artist, album, disc_number, track_number
68
+ SQL
69
+ end
70
+
71
+ def list_by_ids(ids)
72
+ c = ids.collect { |i| "track_id = #{i}" }
73
+ condition = c.empty? ? "" : "where #{c.join(' or ')}"
74
+ @db.execute <<-SQL
75
+ select
76
+ track_id, artist, album, song
77
+ from
78
+ tracks #{condition}
79
+ order by
80
+ artist, album, disc_number, track_number
81
+ SQL
82
+ end
83
+ end
@@ -0,0 +1,46 @@
1
+ # coding: utf-8
2
+
3
+ require 'htmlentities'
4
+
5
+ class MusicLibrary
6
+ attr_accessor :file, :tracks
7
+
8
+ def initialize(file)
9
+ @file = file
10
+ @tracks = []
11
+
12
+ parse_xml
13
+ end
14
+
15
+ def parse_xml
16
+ entities = HTMLEntities.new
17
+ in_tracks = false
18
+ track = {}
19
+
20
+ IO.foreach(File.expand_path(@file)) do |line|
21
+ line = line.force_encoding("UTF-8")
22
+
23
+ if line =~ /^\t<key>Tracks<\/key>$/
24
+ in_tracks = true
25
+ next
26
+ end
27
+
28
+ if in_tracks then
29
+ if line =~ /^\t\t\t<key>(.*)<\/key><(date|integer|string)>(.*)<\/.*>$/
30
+ key = $1.downcase.split.join("_").intern
31
+ track[key] = entities.decode($3.to_s)
32
+ next
33
+ elsif line =~ /^\t\t<\/dict>$/ then
34
+ @tracks.push(track.dup)
35
+ track.clear
36
+ next
37
+ elsif in_tracks && line =~ /^\t<\/dict>$/
38
+ in_tracks = false
39
+ next
40
+ else
41
+ next
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ # coding: utf-8
2
+
3
+ class Player
4
+ def execute(script)
5
+ `osascript -e '#{script}'`.gsub(/\n$/, "")
6
+ end
7
+
8
+ def play(tracks)
9
+ if tracks.empty?
10
+ execute 'tell application "iTunes" to play'
11
+ else
12
+ tracks = tracks.collect {|track| "database ID is #{track}"}
13
+ execute <<-SCRIPT
14
+ set pName to "itc"
15
+ tell application "iTunes"
16
+ if not ((name of playlists) contains pName) then
17
+ set itcPlaylist to make new playlist with properties {name:pName}
18
+ else
19
+ set itcPlaylist to playlist pName
20
+ delete every track of itcPlaylist
21
+ end if
22
+
23
+ duplicate (every track whose #{
24
+ tracks.join(" or ")
25
+ }) to itcPlaylist
26
+
27
+ play itcPlaylist
28
+ end tell
29
+ SCRIPT
30
+ end
31
+ end
32
+
33
+ def pause
34
+ execute 'tell application "iTunes" to pause'
35
+ end
36
+
37
+ def next
38
+ execute 'tell application "iTunes" to next track'
39
+ end
40
+
41
+ def previous
42
+ execute 'tell application "iTunes" to previous track'
43
+ end
44
+
45
+ def state
46
+ execute 'tell application "iTunes" to return player state'
47
+ end
48
+
49
+ def current_track
50
+ execute 'tell application "iTunes" to return database ID of current track'
51
+ end
52
+
53
+ def current_playlist_tracks
54
+ execute <<-SCRIPT
55
+ tell application "iTunes"
56
+ return database ID of tracks of current playlist
57
+ end tell
58
+ SCRIPT
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Itc
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: itc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ragnar B. Johannsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sqlite3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: htmlentities
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: iTunes CLI with tab completions
70
+ email:
71
+ - ragnar@igo.is
72
+ executables:
73
+ - itc
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - bin/itc
83
+ - extra/completion/zsh/_itc
84
+ - itc.gemspec
85
+ - lib/itc.rb
86
+ - lib/itc/command.rb
87
+ - lib/itc/database.rb
88
+ - lib/itc/musiclib.rb
89
+ - lib/itc/player.rb
90
+ - lib/itc/version.rb
91
+ homepage: https://github.com/ragnar-johannsson/itc
92
+ licenses:
93
+ - BSD
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.2.2
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: iTunes CLI
115
+ test_files: []