xwax_playlist 0.1.0
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 +28 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +10 -0
- data/Rakefile +2 -0
- data/bin/itunes2xwax +5 -0
- data/lib/xwax_playlist.rb +16 -0
- data/lib/xwax_playlist/command_line_parser.rb +68 -0
- data/lib/xwax_playlist/parser.rb +30 -0
- data/lib/xwax_playlist/playlist.rb +78 -0
- data/lib/xwax_playlist/run.rb +51 -0
- data/lib/xwax_playlist/track.rb +92 -0
- data/spec/command_line_spec.rb +78 -0
- data/spec/itunes/iTunes Library.xml +295 -0
- data/spec/itunes/iTunes Media/Music/Empty Artist/Unknown Album/Empty Song.mp3 +0 -0
- data/spec/library_spec.rb +96 -0
- data/spec/run_spec.rb +95 -0
- data/spec/spec_helper.rb +73 -0
- data/spec/support/update_path.rb +25 -0
- data/xwax_playlist.gemspec +25 -0
- metadata +172 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: f8e55d5d38bfcf72809b14aadf886c4c61d6d51d
|
|
4
|
+
data.tar.gz: fc651721dee7e3184ec161decf77c7677154b8a4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 264e3902099022128447db20268589b30eefdf45efa652557a280a3434d1a9cdbfe8d38da68c9c41315ebe8a7bb0b19c19e074bb7aafaf9f759548dbeca61c8c
|
|
7
|
+
data.tar.gz: 3fc9201d01418f8eb346cf98b507e1e41eade2603898c945a73918ccd50739e6c019144d0f80b04f69c19decba383c3d276120289a6c2df7bbdaf7e3a92125d3
|
data/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
.idea/
|
|
24
|
+
*.iml
|
|
25
|
+
.DS_Store
|
|
26
|
+
.rvmrc
|
|
27
|
+
.ruby-version
|
|
28
|
+
.ruby-gemset
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Chris Parkinson
|
|
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,10 @@
|
|
|
1
|
+
# xwax_playlist
|
|
2
|
+
|
|
3
|
+
This is a small, stand-alone application to convert iTunes Library XML files into playlists suitable for reading by [xwax](http://xwax.org/).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
Run itunes2xwax -h for usage details.
|
|
7
|
+
|
|
8
|
+
## License
|
|
9
|
+
|
|
10
|
+
This project is licensed by under the terms of the MIT license. See LICENSE.txt for details.
|
data/Rakefile
ADDED
data/bin/itunes2xwax
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'plist'
|
|
2
|
+
require 'optparse'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'addressable/uri'
|
|
7
|
+
|
|
8
|
+
require 'xwax_playlist/playlist'
|
|
9
|
+
require 'xwax_playlist/track'
|
|
10
|
+
require 'xwax_playlist/parser'
|
|
11
|
+
require 'xwax_playlist/command_line_parser'
|
|
12
|
+
require 'xwax_playlist/run'
|
|
13
|
+
|
|
14
|
+
module XwaxPlaylist
|
|
15
|
+
VERSION = '0.1.0'
|
|
16
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module XwaxPlaylist
|
|
2
|
+
class CommandLineParser
|
|
3
|
+
def self.parse(args)
|
|
4
|
+
options = {
|
|
5
|
+
ignored_genres: [],
|
|
6
|
+
playlists: [],
|
|
7
|
+
create_genre_playlists: true,
|
|
8
|
+
playlist_dir: ENV['PWD'],
|
|
9
|
+
min_rating: 0,
|
|
10
|
+
stdout: false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# TODO Add a music directory option to allow XML parsing from a different filesystem (ie an HFS filesystem shared between OSX and Linux)
|
|
14
|
+
opts = OptionParser.new do |o|
|
|
15
|
+
o.banner = "Usage: #{File.basename($0)} [options] file"
|
|
16
|
+
|
|
17
|
+
o.on('-c', '--copy DIR', 'Copy matching files to DIR') do |dir|
|
|
18
|
+
options[:copy_dir] = dir
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
o.on('-C', '--copy-pattern PATTERN', 'Use PATTERN for naming the copied files') do |pattern|
|
|
22
|
+
options[:copy_pattern] = pattern
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
o.on('-n', '--no-genre-playlists', 'Do not create a playlist for each genre') do
|
|
26
|
+
options[:create_genre_playlists] = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
o.on('-i', '--include-playlist PLAYLIST', 'Include PLAYLIST in the output') do |playlist|
|
|
30
|
+
options[:playlists] << playlist
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
o.on('-I', '--ignore-genre GENRE', 'Ignore GENRE when creating genre specific playlists') do |genre|
|
|
34
|
+
options[:ignored_genres] << genre
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
o.on('-o', '--stdout', 'Write playlists to STDOUT instead of files') do
|
|
38
|
+
options[:stdout] = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
o.on('-p', '--playlist DIR', 'Create playlist files in DIR') do |dir|
|
|
42
|
+
options[:playlist_dir] = dir
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
o.on('-r', '--rating RATING', OptionParser::OctalInteger,
|
|
46
|
+
'Only include files with a rating of RATING or higher (0-5)') do |rating|
|
|
47
|
+
options[:min_rating] = rating
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
o.on_tail('-h', '--help', 'Show this message') do
|
|
51
|
+
puts o
|
|
52
|
+
exit
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.parse!(args)
|
|
57
|
+
|
|
58
|
+
# Expect the remaining argument to be the XML file.
|
|
59
|
+
options[:file] = args[0]
|
|
60
|
+
unless options[:file] and File.exist?(options[:file])
|
|
61
|
+
puts opts
|
|
62
|
+
return false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
options
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module XwaxPlaylist
|
|
2
|
+
class Parser
|
|
3
|
+
attr_accessor :tracks, :playlists
|
|
4
|
+
|
|
5
|
+
def self.from_xml(xml_file)
|
|
6
|
+
new(Plist::parse_xml(xml_file))
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(plist)
|
|
10
|
+
@tracks = Track.load_from_plist(plist['Tracks'])
|
|
11
|
+
@playlists = Playlist.load_from_plist(plist['Playlists'])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def intersect_playlists(included)
|
|
15
|
+
(@playlists.keys - included).each { |k| @playlists.delete(k) }
|
|
16
|
+
@playlists
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_genre_playlists(ignored = [])
|
|
20
|
+
@tracks.each do |t|
|
|
21
|
+
next if ignored.include?(t.genre)
|
|
22
|
+
|
|
23
|
+
@playlists[t.genre] ||= Playlist.new(t.genre)
|
|
24
|
+
@playlists[t.genre] << t
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@playlists
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module XwaxPlaylist
|
|
2
|
+
class Playlist
|
|
3
|
+
attr_accessor :tracks
|
|
4
|
+
|
|
5
|
+
IGNORED = [
|
|
6
|
+
'Genius',
|
|
7
|
+
'Library',
|
|
8
|
+
'Music',
|
|
9
|
+
'Movies',
|
|
10
|
+
'TV Shows',
|
|
11
|
+
'90’s Music',
|
|
12
|
+
'Classical Music',
|
|
13
|
+
'My Top Rated',
|
|
14
|
+
'Recently Added',
|
|
15
|
+
'Recently Played',
|
|
16
|
+
'Top 25 Most Played'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def self.load_from_plist(plist)
|
|
20
|
+
lists = {}
|
|
21
|
+
|
|
22
|
+
plist.each do |plist_entry|
|
|
23
|
+
name = plist_entry['Name']
|
|
24
|
+
next if IGNORED.include?(name)
|
|
25
|
+
|
|
26
|
+
playlist = new(name)
|
|
27
|
+
lists[name] = playlist
|
|
28
|
+
|
|
29
|
+
if plist_entry['Playlist Items']
|
|
30
|
+
plist_entry['Playlist Items'].each do |t|
|
|
31
|
+
track = Track.get_by_id(t['Track ID'])
|
|
32
|
+
playlist << track if track
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
lists
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(name)
|
|
41
|
+
@name = name
|
|
42
|
+
@tracks = []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def <<(track)
|
|
46
|
+
@tracks << track
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def prune_by_rating(rating)
|
|
50
|
+
@tracks.delete_if { |t| t.rating < rating }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def write_to_file(dir)
|
|
54
|
+
# Don't write empty playlists
|
|
55
|
+
return false if @tracks.size == 0
|
|
56
|
+
|
|
57
|
+
tracks = 0
|
|
58
|
+
File.open(File.join(dir, @name), 'w') do |f|
|
|
59
|
+
tracks = write(f)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
tracks
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def write(io)
|
|
66
|
+
# Don't write empty playlists
|
|
67
|
+
return false if @tracks.size == 0
|
|
68
|
+
|
|
69
|
+
track_count = 0
|
|
70
|
+
@tracks.each do |track|
|
|
71
|
+
track_count += 1
|
|
72
|
+
io.puts("#{track.path}\t#{track.artist}\t#{track.title}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
track_count
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module XwaxPlaylist
|
|
2
|
+
class Run
|
|
3
|
+
def initialize(options, xml = nil)
|
|
4
|
+
@options = options
|
|
5
|
+
if xml
|
|
6
|
+
@parser = Parser.new(xml)
|
|
7
|
+
else
|
|
8
|
+
@parser = Parser.from_xml(options[:file])
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse_options
|
|
13
|
+
@parser.create_genre_playlists(@options[:ignored_genres]) if @options[:create_genre_playlists]
|
|
14
|
+
|
|
15
|
+
# If we've been passed a list of playlists, exclude the others
|
|
16
|
+
@parser.intersect_playlists(@options[:playlists]) if @options[:playlists].size > 0
|
|
17
|
+
|
|
18
|
+
if @options[:min_rating] > 0
|
|
19
|
+
@parser.playlists.each do |name, p|
|
|
20
|
+
p.prune_by_rating(@options[:min_rating])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write
|
|
26
|
+
@parser.playlists.each do |name, p|
|
|
27
|
+
p.tracks.each { |t| t.copy(@options[:copy_dir], @options[:copy_pattern]) } if @options[:copy_dir]
|
|
28
|
+
|
|
29
|
+
if @options[:stdout]
|
|
30
|
+
p.write($stdout)
|
|
31
|
+
else
|
|
32
|
+
p.write_to_file(@options[:playlist_dir])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
options = CommandLineParser.parse(ARGV)
|
|
42
|
+
exit 1 unless options
|
|
43
|
+
execute(options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def execute(options, xml = nil)
|
|
47
|
+
o = Run.new(options, xml)
|
|
48
|
+
o.parse_options
|
|
49
|
+
o.write
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module XwaxPlaylist
|
|
2
|
+
class Track
|
|
3
|
+
@@id_map = {}
|
|
4
|
+
|
|
5
|
+
def self.load_from_plist(plist)
|
|
6
|
+
tracks = []
|
|
7
|
+
plist.each { |id, track| tracks << new(track) }
|
|
8
|
+
tracks
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.get_by_id(id)
|
|
12
|
+
@@id_map[id]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_accessor :id, :name, :artist, :genre, :location, :path, :rating, :year
|
|
16
|
+
alias_method :title, :name
|
|
17
|
+
|
|
18
|
+
def initialize(properties)
|
|
19
|
+
@id = properties['Track ID']
|
|
20
|
+
@name = properties['Name']
|
|
21
|
+
@artist = properties['Artist']
|
|
22
|
+
@genre = properties['Genre']
|
|
23
|
+
@rating = properties['Rating'] && properties['Rating'] / 20 || 0
|
|
24
|
+
@year = properties['Year']
|
|
25
|
+
@comments = properties['Comments']
|
|
26
|
+
@location = properties['Location']
|
|
27
|
+
@path = Addressable::URI.unescape(URI(@location).path)
|
|
28
|
+
|
|
29
|
+
@copied = false
|
|
30
|
+
|
|
31
|
+
@@id_map[@id] = self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def evaluate_pattern(pattern)
|
|
35
|
+
pattern % build_attr_hash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def copy(dst_dir, pattern = nil)
|
|
39
|
+
# Don't copy the file more than once.
|
|
40
|
+
return false if @copied
|
|
41
|
+
|
|
42
|
+
src = Addressable::URI.unescape(URI(@location).path)
|
|
43
|
+
dst_filename = pattern && evaluate_pattern(pattern) || File.basename(src)
|
|
44
|
+
dst_path = File.join(dst_dir, dst_filename)
|
|
45
|
+
|
|
46
|
+
# If the destination file exists, append _1, _2, etc until we get a unique filename
|
|
47
|
+
if File.exist?(dst_path)
|
|
48
|
+
ext = File.extname(dst_filename)
|
|
49
|
+
base = File.basename(dst_filename, ext)
|
|
50
|
+
|
|
51
|
+
count = 1
|
|
52
|
+
loop do
|
|
53
|
+
rename = "#{base}_#{count}#{ext}"
|
|
54
|
+
rename_path = File.join(dst_dir, rename)
|
|
55
|
+
|
|
56
|
+
unless File.exist?(rename_path)
|
|
57
|
+
dst_path = rename_path
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
count += 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
FileUtils.cp(src, dst_path)
|
|
66
|
+
@copied = true
|
|
67
|
+
|
|
68
|
+
# Update the location
|
|
69
|
+
uri = URI::Generic.build({
|
|
70
|
+
scheme: 'file',
|
|
71
|
+
host: 'localhost',
|
|
72
|
+
path: Addressable::URI.escape(File.absolute_path(dst_path))
|
|
73
|
+
})
|
|
74
|
+
@location = uri.to_s
|
|
75
|
+
@path = dst_path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def build_attr_hash
|
|
81
|
+
{
|
|
82
|
+
file: @location,
|
|
83
|
+
title: @name,
|
|
84
|
+
artist: @artist,
|
|
85
|
+
genre: @genre,
|
|
86
|
+
year: @year,
|
|
87
|
+
rating: @rating,
|
|
88
|
+
comments: @comments
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
describe 'CommandLineParser' do
|
|
2
|
+
before do
|
|
3
|
+
$stdout.stub(:write)
|
|
4
|
+
$stderr.stub(:write)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
let(:empty) { [] }
|
|
8
|
+
let(:missing_file) { ['missing_file.321.no'] }
|
|
9
|
+
let(:this_file) { [ __FILE__ ] }
|
|
10
|
+
let(:copy_dir) { [ '-c', '/tmp', __FILE__ ] }
|
|
11
|
+
let(:copy_pattern) { [ '-C', '%{file}', __FILE__ ] }
|
|
12
|
+
let(:no_genre) { [ '-n', __FILE__ ] }
|
|
13
|
+
let(:playlists) { [ '-i', 'Test Playlist', '-i', 'Empty Playlist', __FILE__ ] }
|
|
14
|
+
let(:ignored_genres) { [ '-I', 'Ignored1', '-I', 'Ignored2', __FILE__ ] }
|
|
15
|
+
let(:playlist_dir) { [ '-p', '/tmp', __FILE__ ] }
|
|
16
|
+
let(:min_rating) { [ '-r', '5', __FILE__ ] }
|
|
17
|
+
let(:bad_min_rating) { [ '-r', 'S', __FILE__] }
|
|
18
|
+
let(:stdout) { [ '-o', __FILE__ ] }
|
|
19
|
+
|
|
20
|
+
it 'fails with no file' do
|
|
21
|
+
options = XwaxPlaylist::CommandLineParser.parse(empty)
|
|
22
|
+
expect(options).to eq(false)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'fails with a missing file' do
|
|
26
|
+
options = XwaxPlaylist::CommandLineParser.parse(missing_file)
|
|
27
|
+
expect(options).to eq(false)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'succeeds with an existent file' do
|
|
31
|
+
options = XwaxPlaylist::CommandLineParser.parse(this_file)
|
|
32
|
+
expect(options).not_to eq(false)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'captures the copy dir switch' do
|
|
36
|
+
options = XwaxPlaylist::CommandLineParser.parse(copy_dir.clone)
|
|
37
|
+
expect(options[:copy_dir]).to eq(copy_dir[1])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'captures the copy pattern switch' do
|
|
41
|
+
options = XwaxPlaylist::CommandLineParser.parse(copy_pattern.clone)
|
|
42
|
+
expect(options[:copy_pattern]).to eq(copy_pattern[1])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'captures the no genre playlists switch' do
|
|
46
|
+
options = XwaxPlaylist::CommandLineParser.parse(no_genre.clone)
|
|
47
|
+
expect(options[:create_genre_playlists]).to eq(false)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'captures the playlists switch' do
|
|
51
|
+
options = XwaxPlaylist::CommandLineParser.parse(playlists.clone)
|
|
52
|
+
expect(options[:playlists]).to eq([playlists[1], playlists[3]])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'captures the ignored genres switch' do
|
|
56
|
+
options = XwaxPlaylist::CommandLineParser.parse(ignored_genres.clone)
|
|
57
|
+
expect(options[:ignored_genres]).to eq([ignored_genres[1], ignored_genres[3]])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'captures the playlist directory switch' do
|
|
61
|
+
options = XwaxPlaylist::CommandLineParser.parse(playlist_dir.clone)
|
|
62
|
+
expect(options[:playlist_dir]).to eq(playlist_dir[1])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'captures the min rating switch' do
|
|
66
|
+
options = XwaxPlaylist::CommandLineParser.parse(min_rating.clone)
|
|
67
|
+
expect(options[:min_rating]).to eq(5)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'fails with a non-integer passed to the min rating switch' do
|
|
71
|
+
expect{XwaxPlaylist::CommandLineParser.parse(bad_min_rating.clone)}.to raise_error(OptionParser::InvalidArgument)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'captures the stdout switch' do
|
|
75
|
+
options = XwaxPlaylist::CommandLineParser.parse(stdout)
|
|
76
|
+
expect(options[:stdout]).to eq(true)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Major Version</key><integer>1</integer>
|
|
6
|
+
<key>Minor Version</key><integer>1</integer>
|
|
7
|
+
<key>Date</key><date>2014-06-12T19:01:22Z</date>
|
|
8
|
+
<key>Application Version</key><string>11.2.2</string>
|
|
9
|
+
<key>Features</key><integer>5</integer>
|
|
10
|
+
<key>Show Content Ratings</key><true/>
|
|
11
|
+
<key>Music Folder</key><string>file://localhost/iTunes%20Media/</string>
|
|
12
|
+
<key>Library Persistent ID</key><string>D44B7E580245E78A</string>
|
|
13
|
+
<key>Tracks</key>
|
|
14
|
+
<dict>
|
|
15
|
+
<key>109</key>
|
|
16
|
+
<dict>
|
|
17
|
+
<key>Track ID</key><integer>109</integer>
|
|
18
|
+
<key>Name</key><string>Empty Song</string>
|
|
19
|
+
<key>Artist</key><string>Empty Artist</string>
|
|
20
|
+
<key>Genre</key><string>Progressive House</string>
|
|
21
|
+
<key>Kind</key><string>MPEG audio file</string>
|
|
22
|
+
<key>Size</key><integer>4224</integer>
|
|
23
|
+
<key>Total Time</key><integer>156</integer>
|
|
24
|
+
<key>Year</key><integer>2014</integer>
|
|
25
|
+
<key>Date Modified</key><date>2014-06-12T19:00:40Z</date>
|
|
26
|
+
<key>Date Added</key><date>2014-06-12T14:52:39Z</date>
|
|
27
|
+
<key>Bit Rate</key><integer>128</integer>
|
|
28
|
+
<key>Sample Rate</key><integer>44100</integer>
|
|
29
|
+
<key>Comments</key><string>1A</string>
|
|
30
|
+
<key>Rating</key><integer>60</integer>
|
|
31
|
+
<key>Album Rating</key><integer>60</integer>
|
|
32
|
+
<key>Album Rating Computed</key><true/>
|
|
33
|
+
<key>Persistent ID</key><string>AABD423D8BA0257A</string>
|
|
34
|
+
<key>Track Type</key><string>File</string>
|
|
35
|
+
<key>Location</key><string>file://localhost/iTunes%20Media/Music/Empty%20Artist/Unknown%20Album/Empty%20Song.mp3</string>
|
|
36
|
+
<key>File Folder Count</key><integer>5</integer>
|
|
37
|
+
<key>Library Folder Count</key><integer>1</integer>
|
|
38
|
+
</dict>
|
|
39
|
+
</dict>
|
|
40
|
+
<key>Playlists</key>
|
|
41
|
+
<array>
|
|
42
|
+
<dict>
|
|
43
|
+
<key>Name</key><string>Library</string>
|
|
44
|
+
<key>Master</key><true/>
|
|
45
|
+
<key>Playlist ID</key><integer>111</integer>
|
|
46
|
+
<key>Playlist Persistent ID</key><string>B87D63FFC13183AF</string>
|
|
47
|
+
<key>Visible</key><false/>
|
|
48
|
+
<key>All Items</key><true/>
|
|
49
|
+
<key>Playlist Items</key>
|
|
50
|
+
<array>
|
|
51
|
+
<dict>
|
|
52
|
+
<key>Track ID</key><integer>109</integer>
|
|
53
|
+
</dict>
|
|
54
|
+
</array>
|
|
55
|
+
</dict>
|
|
56
|
+
<dict>
|
|
57
|
+
<key>Name</key><string>Music</string>
|
|
58
|
+
<key>Playlist ID</key><integer>115</integer>
|
|
59
|
+
<key>Playlist Persistent ID</key><string>0DA4BC72D9C31EBA</string>
|
|
60
|
+
<key>Distinguished Kind</key><integer>4</integer>
|
|
61
|
+
<key>Music</key><true/>
|
|
62
|
+
<key>All Items</key><true/>
|
|
63
|
+
<key>Playlist Items</key>
|
|
64
|
+
<array>
|
|
65
|
+
<dict>
|
|
66
|
+
<key>Track ID</key><integer>109</integer>
|
|
67
|
+
</dict>
|
|
68
|
+
</array>
|
|
69
|
+
</dict>
|
|
70
|
+
<dict>
|
|
71
|
+
<key>Name</key><string>Movies</string>
|
|
72
|
+
<key>Playlist ID</key><integer>128</integer>
|
|
73
|
+
<key>Playlist Persistent ID</key><string>AF7EF9A1E2D01BDF</string>
|
|
74
|
+
<key>Distinguished Kind</key><integer>2</integer>
|
|
75
|
+
<key>Movies</key><true/>
|
|
76
|
+
<key>All Items</key><true/>
|
|
77
|
+
</dict>
|
|
78
|
+
<dict>
|
|
79
|
+
<key>Name</key><string>TV Shows</string>
|
|
80
|
+
<key>Playlist ID</key><integer>131</integer>
|
|
81
|
+
<key>Playlist Persistent ID</key><string>B7D2F99A3145FC56</string>
|
|
82
|
+
<key>Distinguished Kind</key><integer>3</integer>
|
|
83
|
+
<key>TV Shows</key><true/>
|
|
84
|
+
<key>All Items</key><true/>
|
|
85
|
+
</dict>
|
|
86
|
+
<dict>
|
|
87
|
+
<key>Name</key><string>Genius</string>
|
|
88
|
+
<key>Playlist ID</key><integer>150</integer>
|
|
89
|
+
<key>Playlist Persistent ID</key><string>65434ECC909D5C84</string>
|
|
90
|
+
<key>Distinguished Kind</key><integer>26</integer>
|
|
91
|
+
<key>All Items</key><true/>
|
|
92
|
+
</dict>
|
|
93
|
+
<dict>
|
|
94
|
+
<key>Name</key><string>90’s Music</string>
|
|
95
|
+
<key>Playlist ID</key><integer>153</integer>
|
|
96
|
+
<key>Playlist Persistent ID</key><string>A004723A197972D4</string>
|
|
97
|
+
<key>Distinguished Kind</key><integer>200</integer>
|
|
98
|
+
<key>All Items</key><true/>
|
|
99
|
+
<key>Smart Info</key>
|
|
100
|
+
<data>
|
|
101
|
+
AQEAAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
102
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
103
|
+
AAAAAA==
|
|
104
|
+
</data>
|
|
105
|
+
<key>Smart Criteria</key>
|
|
106
|
+
<data>
|
|
107
|
+
U0xzdAABAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
108
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
109
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAEAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
110
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEAAAAAAAAB8YAAAAAAAAAAAAAAAAAAAAB
|
|
111
|
+
AAAAAAAAB88AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEA
|
|
112
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgFNMc3QAAQAB
|
|
113
|
+
AAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
114
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
115
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
116
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAB
|
|
117
|
+
AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAABAAAAAAAAAAAAAAA
|
|
118
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAIAAAAAAAAAAA
|
|
119
|
+
AAAAAAAAAAEAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
|
120
|
+
</data>
|
|
121
|
+
</dict>
|
|
122
|
+
<dict>
|
|
123
|
+
<key>Name</key><string>Classical Music</string>
|
|
124
|
+
<key>Playlist ID</key><integer>156</integer>
|
|
125
|
+
<key>Playlist Persistent ID</key><string>A8598F78E40BC778</string>
|
|
126
|
+
<key>Distinguished Kind</key><integer>206</integer>
|
|
127
|
+
<key>All Items</key><true/>
|
|
128
|
+
<key>Smart Info</key>
|
|
129
|
+
<data>
|
|
130
|
+
AQEAAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
131
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
132
|
+
AAAAAA==
|
|
133
|
+
</data>
|
|
134
|
+
<key>Smart Criteria</key>
|
|
135
|
+
<data>
|
|
136
|
+
U0xzdAABAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
137
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
138
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAA
|
|
139
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAU0xzdAABAAEAAAACAAAAAQAAAAAAAAAA
|
|
140
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
141
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
142
|
+
AAAAAAAAADwAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
143
|
+
AAAAAABEAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAB
|
|
144
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
145
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAg
|
|
146
|
+
AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAAAAAAAAAAA
|
|
147
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVxTTHN0AAEAAQAAABEAAAAB
|
|
148
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
149
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
150
|
+
AAAAAAAAAAAAAAAAAAAACAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
151
|
+
AAAAAAAAAAAAAAAAABIAQwBsAGEAcwBzAGkAYwBhAGwAAAAIAQAAAQAAAAAAAAAAAAAAAAAA
|
|
152
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEABLAGwAYQBzAHMAaQBlAGsAAAAI
|
|
153
|
+
AQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgBD
|
|
154
|
+
AGwAYQBzAHMAaQBxAHUAZQAAAAgBAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
155
|
+
AAAAAAAAAAAAAAAAAAAAAAAOAEsAbABhAHMAcwBpAGsAAAAIAQAAAQAAAAAAAAAAAAAAAAAA
|
|
156
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEABDAGwAYQBzAHMAaQBjAGEAAAAI
|
|
157
|
+
AQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACjCv
|
|
158
|
+
MOkwtzDDMK8AAAAIAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
159
|
+
AAAAAAAAAAAADgBDAGwA4QBzAGkAYwBhAAAACAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
160
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAASwBsAGEAcwBzAGkAcwBrAAAACAEAAAEAAAAA
|
|
161
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAQwBsAOEAcwBz
|
|
162
|
+
AGkAYwBhAAAACAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
163
|
+
AAAAAAAAABIASwBsAGEAcwBzAGkAcwBrAHQAAAAIAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
164
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAQaBDsEMARBBEEEOARHBDUEQQQ6BDAETwAA
|
|
165
|
+
AAgBAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS
|
|
166
|
+
AEsAbABhAHMAeQBjAHoAbgBhAAAACAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
167
|
+
AAAAAAAAAAAAAAAAAAAAAAAAABIASwBsAGEAcwBzAGkAbgBlAG4AAAAIAQAAAQAAAAAAAAAA
|
|
168
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADABLAGwAYQBzAGkAawAA
|
|
169
|
+
AAgBAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU
|
|
170
|
+
AEsAbABhAHMAcwB6AGkAawB1AHMAAAAIAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
171
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAFgBWAOEBfgBuAOEAIABoAHUAZABiAGEAAAAIAQAAAQAA
|
|
172
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgZDBkQGJwYz
|
|
173
|
+
BkoGQwZK
|
|
174
|
+
</data>
|
|
175
|
+
</dict>
|
|
176
|
+
<dict>
|
|
177
|
+
<key>Name</key><string>My Top Rated</string>
|
|
178
|
+
<key>Playlist ID</key><integer>159</integer>
|
|
179
|
+
<key>Playlist Persistent ID</key><string>97B6597AD593D22E</string>
|
|
180
|
+
<key>Distinguished Kind</key><integer>201</integer>
|
|
181
|
+
<key>All Items</key><true/>
|
|
182
|
+
<key>Smart Info</key>
|
|
183
|
+
<data>
|
|
184
|
+
AQEAAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
185
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
186
|
+
AAAAAA==
|
|
187
|
+
</data>
|
|
188
|
+
<key>Smart Criteria</key>
|
|
189
|
+
<data>
|
|
190
|
+
U0xzdAABAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
191
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
192
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkAAAAQAAAAAAAAAAAAAAAAAAAAAAAA
|
|
193
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAB
|
|
194
|
+
AAAAAAAAADwAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|
195
|
+
</data>
|
|
196
|
+
</dict>
|
|
197
|
+
<dict>
|
|
198
|
+
<key>Name</key><string>Recently Added</string>
|
|
199
|
+
<key>Playlist ID</key><integer>163</integer>
|
|
200
|
+
<key>Playlist Persistent ID</key><string>67CEE90F7D35C249</string>
|
|
201
|
+
<key>Distinguished Kind</key><integer>204</integer>
|
|
202
|
+
<key>All Items</key><true/>
|
|
203
|
+
<key>Smart Info</key>
|
|
204
|
+
<data>
|
|
205
|
+
AQEAAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
206
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
207
|
+
AAAAAA==
|
|
208
|
+
</data>
|
|
209
|
+
<key>Smart Criteria</key>
|
|
210
|
+
<data>
|
|
211
|
+
U0xzdAABAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
212
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
213
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAIAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
214
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABELa4tri2uLa7//////////gAAAAAACTqA
|
|
215
|
+
La4tri2uLa4AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5AgAAAQAA
|
|
216
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAB
|
|
217
|
+
AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA
|
|
218
|
+
AAAAAAAA
|
|
219
|
+
</data>
|
|
220
|
+
<key>Playlist Items</key>
|
|
221
|
+
<array>
|
|
222
|
+
<dict>
|
|
223
|
+
<key>Track ID</key><integer>109</integer>
|
|
224
|
+
</dict>
|
|
225
|
+
</array>
|
|
226
|
+
</dict>
|
|
227
|
+
<dict>
|
|
228
|
+
<key>Name</key><string>Recently Played</string>
|
|
229
|
+
<key>Playlist ID</key><integer>167</integer>
|
|
230
|
+
<key>Playlist Persistent ID</key><string>DA11DF676EFDA95E</string>
|
|
231
|
+
<key>Distinguished Kind</key><integer>203</integer>
|
|
232
|
+
<key>All Items</key><true/>
|
|
233
|
+
<key>Smart Info</key>
|
|
234
|
+
<data>
|
|
235
|
+
AQEAAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
236
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
237
|
+
AAAAAA==
|
|
238
|
+
</data>
|
|
239
|
+
<key>Smart Criteria</key>
|
|
240
|
+
<data>
|
|
241
|
+
U0xzdAABAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
242
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
243
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcAAAIAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
244
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABELa4tri2uLa7//////////gAAAAAACTqA
|
|
245
|
+
La4tri2uLa4AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5AgAAAQAA
|
|
246
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAB
|
|
247
|
+
AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA
|
|
248
|
+
AAAAAAAA
|
|
249
|
+
</data>
|
|
250
|
+
</dict>
|
|
251
|
+
<dict>
|
|
252
|
+
<key>Name</key><string>Top 25 Most Played</string>
|
|
253
|
+
<key>Playlist ID</key><integer>170</integer>
|
|
254
|
+
<key>Playlist Persistent ID</key><string>58021E26AFA4D306</string>
|
|
255
|
+
<key>Distinguished Kind</key><integer>202</integer>
|
|
256
|
+
<key>All Items</key><true/>
|
|
257
|
+
<key>Smart Info</key>
|
|
258
|
+
<data>
|
|
259
|
+
AQEBAwAAABkAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
260
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
261
|
+
AAAAAA==
|
|
262
|
+
</data>
|
|
263
|
+
<key>Smart Criteria</key>
|
|
264
|
+
<data>
|
|
265
|
+
U0xzdAABAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
266
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
267
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADkCAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
|
268
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAB
|
|
269
|
+
AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAAAEAAA
|
|
270
|
+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAA
|
|
271
|
+
AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA
|
|
272
|
+
AAAAAAAA
|
|
273
|
+
</data>
|
|
274
|
+
</dict>
|
|
275
|
+
<dict>
|
|
276
|
+
<key>Name</key><string>Empty Playlist</string>
|
|
277
|
+
<key>Playlist ID</key><integer>173</integer>
|
|
278
|
+
<key>Playlist Persistent ID</key><string>EEB4C05E775E537E</string>
|
|
279
|
+
<key>All Items</key><true/>
|
|
280
|
+
</dict>
|
|
281
|
+
<dict>
|
|
282
|
+
<key>Name</key><string>Test Playlist</string>
|
|
283
|
+
<key>Playlist ID</key><integer>176</integer>
|
|
284
|
+
<key>Playlist Persistent ID</key><string>943ADBC2F96D82BC</string>
|
|
285
|
+
<key>All Items</key><true/>
|
|
286
|
+
<key>Playlist Items</key>
|
|
287
|
+
<array>
|
|
288
|
+
<dict>
|
|
289
|
+
<key>Track ID</key><integer>109</integer>
|
|
290
|
+
</dict>
|
|
291
|
+
</array>
|
|
292
|
+
</dict>
|
|
293
|
+
</array>
|
|
294
|
+
</dict>
|
|
295
|
+
</plist>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
describe 'Library' do
|
|
2
|
+
before(:all) do
|
|
3
|
+
@plist = update_path(File.join('spec', 'itunes', 'iTunes Library.xml'))
|
|
4
|
+
@parser = XwaxPlaylist::Parser.new(@plist)
|
|
5
|
+
@tmp_dir = Dir.mktmpdir
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
after(:all) do
|
|
9
|
+
FileUtils.remove_entry(@tmp_dir)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe 'Track' do
|
|
13
|
+
subject(:track) { XwaxPlaylist::Track.new(@plist['Tracks'].first[1]) }
|
|
14
|
+
|
|
15
|
+
describe '#copy' do
|
|
16
|
+
let(:bad_dir) { 'invalid_directory_name' }
|
|
17
|
+
let(:tmp_dir) { @tmp_dir }
|
|
18
|
+
let(:dst_filename) { File.join(@tmp_dir, File.basename(subject.path)) }
|
|
19
|
+
let(:dst_uri) { URI::Generic.build({scheme: 'file', host: 'localhost', path: Addressable::URI.escape(dst_filename)}) }
|
|
20
|
+
|
|
21
|
+
it 'will fail if the directory doesn\'t exist' do
|
|
22
|
+
expect { subject.copy(bad_dir) }.to raise_error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'will succeed if the directory exists' do
|
|
26
|
+
subject.copy(tmp_dir)
|
|
27
|
+
expect(File.exist?(dst_filename)).to eq(true)
|
|
28
|
+
expect(subject.location).to eq(dst_uri.to_s)
|
|
29
|
+
expect(subject.path).to eq(dst_filename)
|
|
30
|
+
FileUtils.rm dst_filename
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'will rename the file if there is a conflict' do
|
|
34
|
+
filename = 'Empty Song.mp3'
|
|
35
|
+
|
|
36
|
+
ext = File.extname(filename)
|
|
37
|
+
name = File.basename(filename, ext)
|
|
38
|
+
renamed = File.join(tmp_dir, "#{name}_1#{ext}")
|
|
39
|
+
|
|
40
|
+
path = File.join(tmp_dir, filename)
|
|
41
|
+
|
|
42
|
+
FileUtils.touch(path)
|
|
43
|
+
|
|
44
|
+
subject.copy(tmp_dir, '%{title}.mp3')
|
|
45
|
+
expect(File.exist?(renamed)).to eq(true)
|
|
46
|
+
|
|
47
|
+
FileUtils.rm path
|
|
48
|
+
FileUtils.rm renamed
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#evaluate_pattern' do
|
|
53
|
+
let(:pattern) { '%{genre} - %{artist} - %{title}.mp3' }
|
|
54
|
+
let(:correct) { 'Progressive House - Empty Artist - Empty Song.mp3' }
|
|
55
|
+
|
|
56
|
+
it 'evaluates to the correct pattern' do
|
|
57
|
+
expect(subject.evaluate_pattern(pattern)).to eq(correct)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe 'Playlist' do
|
|
63
|
+
let(:playlists) { XwaxPlaylist::Playlist.load_from_plist(@plist['Playlists']) }
|
|
64
|
+
|
|
65
|
+
describe 'Empty playlist' do
|
|
66
|
+
it 'contains no tracks' do
|
|
67
|
+
expect(playlists['Empty Playlist'].tracks.size).to eq(0)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe 'Test playlist' do
|
|
72
|
+
it 'contains one track' do
|
|
73
|
+
expect(playlists['Test Playlist'].tracks.size).to eq(1)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe 'Parser' do
|
|
79
|
+
let(:parser) { XwaxPlaylist::Parser.new(@plist) }
|
|
80
|
+
|
|
81
|
+
it 'will intersect the playlist hash with an array of playlist names' do
|
|
82
|
+
playlist_name = 'Test Playlist'
|
|
83
|
+
playlist = @parser.playlists[playlist_name]
|
|
84
|
+
expect(@parser.intersect_playlists([playlist_name]).values).to eq([playlist])
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe 'Genre playlist' do
|
|
89
|
+
subject { @parser.create_genre_playlists['Progressive House'] }
|
|
90
|
+
describe 'Progressive house' do
|
|
91
|
+
it 'contains one track' do
|
|
92
|
+
expect(subject.tracks.size).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/spec/run_spec.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
describe 'Run' do
|
|
2
|
+
TEST_PLAYLIST = 'Test Playlist'
|
|
3
|
+
EMPTY_PLAYLIST = 'Empty Playlist'
|
|
4
|
+
PROGRESSIVE_HOUSE = 'Progressive House'
|
|
5
|
+
|
|
6
|
+
before(:each) do
|
|
7
|
+
# Set up a temporary area
|
|
8
|
+
@tmp_dir = Dir.mktmpdir
|
|
9
|
+
options[:playlist_dir] = @tmp_dir
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:options) do
|
|
13
|
+
{
|
|
14
|
+
file: File.join('spec', 'itunes', 'iTunes Library.xml'),
|
|
15
|
+
min_rating: 0,
|
|
16
|
+
playlists: [],
|
|
17
|
+
create_genre_playlists: true,
|
|
18
|
+
ignored_genres: [],
|
|
19
|
+
stdout: false
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'playlists' do
|
|
24
|
+
it 'will create playlists that contain tracks' do
|
|
25
|
+
XwaxPlaylist.execute(options)
|
|
26
|
+
expect(File.exist?(File.join(@tmp_dir, TEST_PLAYLIST))).to eq(true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'will not create empty playlists' do
|
|
30
|
+
XwaxPlaylist.execute(options)
|
|
31
|
+
expect(File.exist?(File.join(@tmp_dir, EMPTY_PLAYLIST))).to eq(false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'will create genre playlists by default' do
|
|
35
|
+
XwaxPlaylist.execute(options)
|
|
36
|
+
expect(File.exist?(File.join(@tmp_dir, PROGRESSIVE_HOUSE))).to eq(true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'will not create genre playlists when the -n switch is passed' do
|
|
40
|
+
options[:create_genre_playlists] = false
|
|
41
|
+
XwaxPlaylist.execute(options)
|
|
42
|
+
expect(File.exist?(File.join(@tmp_dir, PROGRESSIVE_HOUSE))).to eq(false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'will not create playlists for ignored genres' do
|
|
46
|
+
options[:ignored_genres] << PROGRESSIVE_HOUSE
|
|
47
|
+
XwaxPlaylist.execute(options)
|
|
48
|
+
expect(File.exist?(File.join(@tmp_dir, PROGRESSIVE_HOUSE))).to eq(false)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'will not add tracks below the minimum rating' do
|
|
52
|
+
options[:min_rating] = 5
|
|
53
|
+
XwaxPlaylist.execute(options)
|
|
54
|
+
expect(File.exist?(File.join(@tmp_dir, TEST_PLAYLIST))).to eq(false)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'will copy tracks using the specified naming pattern' do
|
|
58
|
+
pattern = '%{artist} - %{title}.mp3'
|
|
59
|
+
filename = 'Empty Artist - Empty Song.mp3'
|
|
60
|
+
|
|
61
|
+
options[:copy_dir] = @tmp_dir
|
|
62
|
+
options[:copy_pattern] = pattern
|
|
63
|
+
XwaxPlaylist.execute(options, update_path(options[:file]))
|
|
64
|
+
expect(File.exist?(File.join(@tmp_dir, filename))).to eq(true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'will write to stdout when passed the -o switch' do
|
|
68
|
+
playlist_name = 'Test Playlist'
|
|
69
|
+
|
|
70
|
+
file_path = File.join(__dir__, 'itunes', 'iTunes Media', 'Music', 'Empty Artist', 'Unknown Album', 'Empty Song.mp3')
|
|
71
|
+
expected_line = "#{file_path}\tEmpty Artist\tEmpty Song\n"
|
|
72
|
+
options[:stdout] = true
|
|
73
|
+
options[:playlists] << playlist_name
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
out = $stdout
|
|
77
|
+
buffer = StringIO.new
|
|
78
|
+
$stdout = buffer
|
|
79
|
+
|
|
80
|
+
XwaxPlaylist.execute(options, update_path(options[:file]))
|
|
81
|
+
expect(buffer.string).to eq(expected_line)
|
|
82
|
+
ensure
|
|
83
|
+
$stdout = out
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
after(:each) do
|
|
88
|
+
FileUtils.rm_rf("#{@tmp_dir}/.", secure: true)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
after(:each) do
|
|
93
|
+
FileUtils.remove_entry(@tmp_dir)
|
|
94
|
+
end
|
|
95
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
|
|
2
|
+
|
|
3
|
+
require 'xwax_playlist'
|
|
4
|
+
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
config.order = :random
|
|
7
|
+
Kernel.srand config.seed
|
|
8
|
+
|
|
9
|
+
config.expect_with :rspec do |expectations|
|
|
10
|
+
expectations.syntax = :expect
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The settings below are suggested to provide a good initial experience
|
|
14
|
+
# with RSpec, but feel free to customize to your heart's content.
|
|
15
|
+
=begin
|
|
16
|
+
# These two settings work together to allow you to limit a spec run
|
|
17
|
+
# to individual examples or groups you care about by tagging them with
|
|
18
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
|
19
|
+
# get run.
|
|
20
|
+
config.filter_run :focus
|
|
21
|
+
config.run_all_when_everything_filtered = true
|
|
22
|
+
|
|
23
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
|
24
|
+
# file, and it's useful to allow more verbose output when running an
|
|
25
|
+
# individual spec file.
|
|
26
|
+
if config.files_to_run.one?
|
|
27
|
+
# Use the documentation formatter for detailed output,
|
|
28
|
+
# unless a formatter has already been configured
|
|
29
|
+
# (e.g. via a command-line flag).
|
|
30
|
+
config.default_formatter = 'doc'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Print the 10 slowest examples and example groups at the
|
|
34
|
+
# end of the spec run, to help surface which specs are running
|
|
35
|
+
# particularly slow.
|
|
36
|
+
config.profile_examples = 10
|
|
37
|
+
|
|
38
|
+
# Run specs in random order to surface order dependencies. If you find an
|
|
39
|
+
# order dependency and want to debug it, you can fix the order by providing
|
|
40
|
+
# the seed, which is printed after each run.
|
|
41
|
+
# --seed 1234
|
|
42
|
+
config.order = :random
|
|
43
|
+
|
|
44
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
|
45
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
|
46
|
+
# test failures related to randomization by passing the same `--seed` value
|
|
47
|
+
# as the one that triggered the failure.
|
|
48
|
+
Kernel.srand config.seed
|
|
49
|
+
|
|
50
|
+
# rspec-expectations config goes here. You can use an alternate
|
|
51
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
|
52
|
+
# assertions if you prefer.
|
|
53
|
+
config.expect_with :rspec do |expectations|
|
|
54
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
|
55
|
+
# For more details, see:
|
|
56
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
|
57
|
+
expectations.syntax = :expect
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
|
61
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
|
62
|
+
config.mock_with :rspec do |mocks|
|
|
63
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
|
64
|
+
# For more details, see:
|
|
65
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
|
66
|
+
mocks.syntax = :expect
|
|
67
|
+
|
|
68
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
|
69
|
+
# a real object. This is generally recommended.
|
|
70
|
+
mocks.verify_partial_doubles = true
|
|
71
|
+
end
|
|
72
|
+
=end
|
|
73
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
|
|
3
|
+
def update_path(xml_file)
|
|
4
|
+
plist = Plist.parse_xml(xml_file)
|
|
5
|
+
music_folder_path = Addressable::URI.unescape(URI(plist['Music Folder']).path)
|
|
6
|
+
track_path = Addressable::URI.unescape(URI(plist['Tracks'].first[1]['Location']).path)
|
|
7
|
+
relative_path = Pathname.new(track_path).relative_path_from(Pathname.new(music_folder_path))
|
|
8
|
+
|
|
9
|
+
new_music_folder_path = File.join(File.dirname(File.realpath(xml_file)), 'iTunes Media', File::SEPARATOR)
|
|
10
|
+
new_track_path = File.join(new_music_folder_path, relative_path)
|
|
11
|
+
|
|
12
|
+
plist['Music Folder'] = URI::Generic.build({
|
|
13
|
+
scheme: 'file',
|
|
14
|
+
host: 'localhost',
|
|
15
|
+
path: Addressable::URI.escape(new_music_folder_path)
|
|
16
|
+
}).to_s
|
|
17
|
+
|
|
18
|
+
plist['Tracks'].first[1]['Location'] = URI::Generic.build({
|
|
19
|
+
scheme: 'file',
|
|
20
|
+
host: 'localhost',
|
|
21
|
+
path: Addressable::URI.escape(new_track_path)
|
|
22
|
+
}).to_s
|
|
23
|
+
|
|
24
|
+
plist
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = 'xwax_playlist'
|
|
5
|
+
spec.version = '0.1.0'
|
|
6
|
+
spec.authors = ['Christian Parkinson']
|
|
7
|
+
spec.email = ['chris@parkie.ca']
|
|
8
|
+
spec.summary = %q{A small utility to parse an iTunes Library XML file and export xwax compatible playlists.}
|
|
9
|
+
spec.homepage = 'http://github.com/cmparkinson/xwax_playlist'
|
|
10
|
+
spec.license = 'MIT'
|
|
11
|
+
|
|
12
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
13
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
14
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
15
|
+
spec.require_paths = ['lib']
|
|
16
|
+
|
|
17
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
|
18
|
+
spec.add_development_dependency 'rake', '~> 0'
|
|
19
|
+
spec.add_development_dependency 'rspec', '~> 2.14'
|
|
20
|
+
spec.add_development_dependency 'debase', '~> 0'
|
|
21
|
+
spec.add_development_dependency 'ruby-debug-ide', '~> 0'
|
|
22
|
+
|
|
23
|
+
spec.add_dependency 'plist', '~> 3.1'
|
|
24
|
+
spec.add_dependency 'addressable', '~> 2.3'
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: xwax_playlist
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Christian Parkinson
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2014-09-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.6'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.14'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.14'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: debase
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: ruby-debug-ide
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: plist
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.1'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.1'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: addressable
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '2.3'
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '2.3'
|
|
111
|
+
description:
|
|
112
|
+
email:
|
|
113
|
+
- chris@parkie.ca
|
|
114
|
+
executables:
|
|
115
|
+
- itunes2xwax
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- ".gitignore"
|
|
120
|
+
- ".rspec"
|
|
121
|
+
- Gemfile
|
|
122
|
+
- LICENSE.txt
|
|
123
|
+
- README.md
|
|
124
|
+
- Rakefile
|
|
125
|
+
- bin/itunes2xwax
|
|
126
|
+
- lib/xwax_playlist.rb
|
|
127
|
+
- lib/xwax_playlist/command_line_parser.rb
|
|
128
|
+
- lib/xwax_playlist/parser.rb
|
|
129
|
+
- lib/xwax_playlist/playlist.rb
|
|
130
|
+
- lib/xwax_playlist/run.rb
|
|
131
|
+
- lib/xwax_playlist/track.rb
|
|
132
|
+
- spec/command_line_spec.rb
|
|
133
|
+
- spec/itunes/iTunes Library.xml
|
|
134
|
+
- spec/itunes/iTunes Media/Music/Empty Artist/Unknown Album/Empty Song.mp3
|
|
135
|
+
- spec/library_spec.rb
|
|
136
|
+
- spec/run_spec.rb
|
|
137
|
+
- spec/spec_helper.rb
|
|
138
|
+
- spec/support/update_path.rb
|
|
139
|
+
- xwax_playlist.gemspec
|
|
140
|
+
homepage: http://github.com/cmparkinson/xwax_playlist
|
|
141
|
+
licenses:
|
|
142
|
+
- MIT
|
|
143
|
+
metadata: {}
|
|
144
|
+
post_install_message:
|
|
145
|
+
rdoc_options: []
|
|
146
|
+
require_paths:
|
|
147
|
+
- lib
|
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
158
|
+
requirements: []
|
|
159
|
+
rubyforge_project:
|
|
160
|
+
rubygems_version: 2.2.2
|
|
161
|
+
signing_key:
|
|
162
|
+
specification_version: 4
|
|
163
|
+
summary: A small utility to parse an iTunes Library XML file and export xwax compatible
|
|
164
|
+
playlists.
|
|
165
|
+
test_files:
|
|
166
|
+
- spec/command_line_spec.rb
|
|
167
|
+
- spec/itunes/iTunes Library.xml
|
|
168
|
+
- spec/itunes/iTunes Media/Music/Empty Artist/Unknown Album/Empty Song.mp3
|
|
169
|
+
- spec/library_spec.rb
|
|
170
|
+
- spec/run_spec.rb
|
|
171
|
+
- spec/spec_helper.rb
|
|
172
|
+
- spec/support/update_path.rb
|