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.
@@ -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