xwax_playlist 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: f8e55d5d38bfcf72809b14aadf886c4c61d6d51d
4
+ data.tar.gz: fc651721dee7e3184ec161decf77c7677154b8a4
5
+ SHA512:
6
+ metadata.gz: 264e3902099022128447db20268589b30eefdf45efa652557a280a3434d1a9cdbfe8d38da68c9c41315ebe8a7bb0b19c19e074bb7aafaf9f759548dbeca61c8c
7
+ data.tar.gz: 3fc9201d01418f8eb346cf98b507e1e41eade2603898c945a73918ccd50739e6c019144d0f80b04f69c19decba383c3d276120289a6c2df7bbdaf7e3a92125d3
@@ -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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in xwax_playlist.gemspec
4
+ gemspec
@@ -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.
@@ -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.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'xwax_playlist'
4
+
5
+ XwaxPlaylist.run
@@ -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
@@ -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
@@ -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