kagu 2.0.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d63ebef06f4a301d40205bd5fb3968605c05a00d6c179839b87623eaea617c7
4
- data.tar.gz: 00add6c458440f5e4b26a5141b22b602c9c5555cec62ceffb61b3c6e40977bef
3
+ metadata.gz: 525d281da8b729c56214956d8fe81f699b105331c15e86976f65ffc983a0b2ea
4
+ data.tar.gz: 9a5200a627a5a6224f0f1d2a6972f9262515a1957ca33e60eebdac20be4303a7
5
5
  SHA512:
6
- metadata.gz: 5e8a3186584bc2d4e29ce6fca5315c7a51bbdfe1af54207ff90544cca5eef14d14aeaa045690e41a14ffd597e4ea71fb3c0f0e1eddddbadc3613626e42d20d94
7
- data.tar.gz: 70c013d695a15f25e853a6efe98d2f074c2de0001b9ab4e604e5baf9a1d51d6d813601517a0f9bc9ab3894a945261fd8f8bebcc033b5ec6801ae07e7f4c96487
6
+ metadata.gz: d54df53f2489cd5d5f6f734c994c89f03ee0fe5c8834547272f8cda4d5b206f6266b9a7474a72be5f734eeae9d0095969762cb5d042b1f600946de9260628670
7
+ data.tar.gz: 928913d1a8454a838ba48fc1c8bfecc992057276de00d22b904864859f66c70db3bf1bf5f2e1028600bacb21e18c5c21f850a7e18efd75fc7f52db41f7260706
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.3
1
+ 3.0.0
data/kagu.gemspec CHANGED
@@ -15,11 +15,10 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.required_ruby_version = '>= 2.0.0'
17
17
 
18
- s.add_dependency 'activesupport', '>= 4.1.0', '< 6.0.0'
18
+ s.add_dependency 'activesupport', '>= 4.1.0', '< 7.0.0'
19
19
  s.add_dependency 'applescript', '>= 1.0', '< 2.0'
20
- s.add_dependency 'htmlentities', '>= 4.3.0', '< 4.4.0'
21
20
 
22
- s.add_development_dependency 'byebug', '>= 3.2.0', '< 10.0.0'
23
- s.add_development_dependency 'rake', '>= 10.3.0', '< 13.0.0'
24
- s.add_development_dependency 'rspec', '>= 3.1.0', '< 3.8.0'
21
+ s.add_development_dependency 'byebug', '>= 3.2.0', '< 12.0.0'
22
+ s.add_development_dependency 'rake', '>= 10.3.0', '< 14.0.0'
23
+ s.add_development_dependency 'rspec', '>= 3.1.0', '< 3.10.0'
25
24
  end
data/lib/kagu/finder.rb CHANGED
@@ -4,8 +4,6 @@ module Kagu
4
4
 
5
5
  MANDATORY_ATTRIBUTES = []
6
6
 
7
- attr_reader :library
8
-
9
7
  delegate :replace, :transliterate, to: 'self.class'
10
8
 
11
9
  def self.replace(value, replacements = {})
@@ -20,9 +18,7 @@ module Kagu
20
18
  ActiveSupport::Inflector.transliterate(value.to_s).squish.downcase.presence
21
19
  end
22
20
 
23
- def initialize(library, options = {})
24
- raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
25
- @library = library
21
+ def initialize(options = {})
26
22
  reload(options)
27
23
  end
28
24
 
@@ -115,7 +111,7 @@ module Kagu
115
111
 
116
112
  def tracks
117
113
  return @tracks if @tracks
118
- (@tracks = library.tracks.to_a).tap do |tracks|
114
+ (@tracks = Tracks.new.to_a).tap do |tracks|
119
115
  Kagu.logger.debug('Kagu') { "Loaded #{tracks.size} track(s) from library" }
120
116
  end
121
117
  end
data/lib/kagu/library.rb CHANGED
@@ -2,31 +2,16 @@ module Kagu
2
2
 
3
3
  class Library
4
4
 
5
- PATH = File.expand_path("#{ENV['HOME']}/Music/iTunes/iTunes Music Library.xml")
6
-
7
- attr_reader :path
8
-
9
- def initialize(path = PATH)
10
- self.path = path
11
- end
12
-
13
5
  def finder(options = {})
14
- Finder.new(self, options)
6
+ Finder.new(options)
15
7
  end
16
8
 
17
9
  def playlists
18
- Playlists.new(self)
10
+ Playlists.new
19
11
  end
20
12
 
21
13
  def tracks
22
- Tracks.new(self)
23
- end
24
-
25
- private
26
-
27
- def path=(path)
28
- raise Error.new("No such file: #{path.inspect}") unless File.file?(path)
29
- @path = path
14
+ Tracks.new
30
15
  end
31
16
 
32
17
  end
data/lib/kagu/playlist.rb CHANGED
@@ -28,15 +28,16 @@ module Kagu
28
28
  private
29
29
 
30
30
  def add_tracks
31
+ return if tracks.empty?
31
32
  Kagu.logger.info('Kagu') { "Adding #{tracks.size} track(s) to playlist #{name.inspect}" }
32
- tracks.map(&:id).each_slice(500) do |ids|
33
+ Dir.mktmpdir do |directory|
34
+ path = "#{directory}/#{name}.m3u"
35
+ File.open(path, 'w') do |io|
36
+ tracks.each { |track| io << "file://#{URI.escape(track.path.to_s)}\n" }
37
+ end
33
38
  AppleScript.execute(%Q{
34
39
  tell application #{Kagu::OSX_APP_NAME.inspect}
35
- set playlistToPush to user playlist #{name.inspect}
36
- set idsToAdd to {#{ids.join(',')}}
37
- repeat with idToAdd in idsToAdd
38
- duplicate (tracks of library playlist 1 whose database ID is idToAdd) to playlistToPush
39
- end repeat
40
+ open #{path.inspect}
40
41
  end tell
41
42
  })
42
43
  end
@@ -79,11 +80,6 @@ module Kagu
79
80
  @tracks = [values].flatten.select { |value| value.is_a?(Track) }
80
81
  end
81
82
 
82
- def xml_name=(value)
83
- @@html_entities ||= HTMLEntities.new
84
- self.name = @@html_entities.decode(value)
85
- end
86
-
87
83
  end
88
84
 
89
85
  end
@@ -4,13 +4,6 @@ module Kagu
4
4
 
5
5
  include Enumerable
6
6
 
7
- attr_reader :library
8
-
9
- def initialize(library)
10
- raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
11
- @library = library
12
- end
13
-
14
7
  def build(attributes = {})
15
8
  Playlist.new(attributes)
16
9
  end
@@ -21,42 +14,34 @@ module Kagu
21
14
 
22
15
  def each(&block)
23
16
  return unless block_given?
17
+ Kagu.logger.debug('Kagu') { 'Loading library playlists' }
24
18
  tracks = {}.tap do |tracks|
25
- library.tracks.each { |track| tracks[track.id] = track }
19
+ Tracks.new.each { |track| tracks[track.id] = track }
26
20
  end
27
- Kagu.logger.debug('Kagu') { "Reading library playlists from #{library.path.inspect}" }
28
- File.open(library.path, 'r') do |file|
29
- begin
30
- line = file.readline.strip
31
- end while !line.starts_with?('<key>Playlists</key>')
32
- playlist_name = nil
33
- playlist_tracks = []
34
- skip_next = false
35
- while !file.eof? && (line = file.readline.strip)
36
- if line == '<key>Master</key><true/>'
37
- playlist_name = nil
38
- skip_next = true
39
- next
40
- end
41
- if line == '</array>'
42
- yield(Playlist.new(tracks: playlist_tracks, xml_name: playlist_name)) if playlist_name.present? && playlist_tracks.any?
43
- playlist_name = nil
44
- playlist_tracks = []
45
- next
46
- end
47
- match = line.match(/<key>(.+)<\/key><(\w+)>(.*)<\/\2>/)
48
- next unless match
49
- name = match[1]
50
- value = match[3]
51
- if name == 'Name'
52
- if skip_next
53
- skip_next = false
54
- else
55
- playlist_name = value
56
- end
57
- elsif name == 'Track ID'
58
- playlist_tracks << tracks[value.to_i]
59
- end
21
+ playlist_name = nil
22
+ playlist_tracks = []
23
+ SwiftHelper.execute(%Q{
24
+ import iTunesLibrary
25
+
26
+ let library = try! ITLibrary(apiVersion: "1")
27
+ for playlist in library.allPlaylists.filter({ !$0.isMaster }) {
28
+ print("BEGIN_PLAYLIST")
29
+ print(playlist.name)
30
+ for track in playlist.items.filter({ $0.mediaKind == ITLibMediaItemMediaKind.kindSong }) {
31
+ print(String(format: "%02X", track.persistentID.intValue))
32
+ }
33
+ print("END_PLAYLIST")
34
+ }
35
+ }) do |line|
36
+ if line == 'BEGIN_PLAYLIST'
37
+ playlist_name = nil
38
+ playlist_tracks = []
39
+ elsif line == 'END_PLAYLIST'
40
+ yield(Playlist.new(name: playlist_name, tracks: playlist_tracks)) if playlist_name.present?
41
+ elsif playlist_name.nil?
42
+ playlist_name = line
43
+ else
44
+ playlist_tracks << tracks[line]
60
45
  end
61
46
  end
62
47
  end
@@ -0,0 +1,28 @@
1
+ module Kagu
2
+
3
+ module SwiftHelper
4
+
5
+ def self.execute(code, &block)
6
+ tempfile = Tempfile.new
7
+ begin
8
+ tempfile << code
9
+ ensure
10
+ tempfile.close
11
+ end
12
+ begin
13
+ stdout, stderr, result = Open3.capture3("swift #{tempfile.path.inspect}")
14
+ raise(stderr.presence || "Swift command returned with code: #{result.exitstatus}") unless result.success?
15
+ if block_given?
16
+ stdout.lines.each { |line| yield(line.chomp) }
17
+ nil
18
+ else
19
+ stdout
20
+ end
21
+ ensure
22
+ tempfile.unlink
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
data/lib/kagu/track.rb CHANGED
@@ -9,10 +9,6 @@ module Kagu
9
9
 
10
10
  attr_reader :added_at, :album, :artist, :bpm, :genre, :id, :length, :path, :title, :year
11
11
 
12
- def initialize(attributes = {})
13
- super
14
- end
15
-
16
12
  def <=>(other)
17
13
  return nil unless other.is_a?(self.class)
18
14
  length <=> other.length
@@ -27,7 +23,7 @@ module Kagu
27
23
  end
28
24
 
29
25
  def exists?
30
- File.file?(path)
26
+ path.file?
31
27
  end
32
28
 
33
29
  def hash
@@ -35,7 +31,8 @@ module Kagu
35
31
  end
36
32
 
37
33
  def relative_path(directory)
38
- directory.present? && directory.starts_with?(directory) ? path.gsub(/\A#{Regexp.escape(directory)}\//, '') : path
34
+ directory = directory.to_s
35
+ directory.present? ? Pathname.new(path.to_s.gsub(/\A#{Regexp.escape(directory)}\//, '')) : path
39
36
  end
40
37
 
41
38
  def to_s
@@ -45,6 +42,11 @@ module Kagu
45
42
  private
46
43
 
47
44
  def added_at=(value)
45
+ if value.is_a?(String)
46
+ value = Time.parse(value)
47
+ elsif value.is_a?(Integer)
48
+ value = Time.at(value)
49
+ end
48
50
  @added_at = value.is_a?(Time) ? value.utc : nil
49
51
  end
50
52
 
@@ -64,13 +66,8 @@ module Kagu
64
66
  @genre = value.to_s.squish.presence
65
67
  end
66
68
 
67
- def html_entities_decode(value)
68
- @@html_entities ||= HTMLEntities.new
69
- @@html_entities.decode(value.to_s)
70
- end
71
-
72
69
  def id=(value)
73
- @id = value.to_s =~ /\A[0-9]+\z/ ? value.to_i : nil
70
+ @id = value.to_s.presence
74
71
  end
75
72
 
76
73
  def length=(value)
@@ -78,8 +75,11 @@ module Kagu
78
75
  end
79
76
 
80
77
  def path=(value)
81
- @path = value.to_s.presence
82
- raise Error.new("No such file: #{path.inspect}") if File.exists?(path) && !exists?
78
+ value = value.to_s.presence
79
+ value = URI.unescape(URI.parse(value).path) if value.is_a?(String) && value.starts_with?('file://')
80
+ value = value.encode('UTF-8', 'UTF-8-MAC') if Kagu::IS_MAC_OS
81
+ @path = Pathname.new(value)
82
+ raise Error.new("No such file: #{path.to_s.inspect}") if path.exist? && !exists?
83
83
  Kagu.logger.error('Kagu') { "No such track: #{path.inspect}" } unless exists?
84
84
  end
85
85
 
@@ -87,48 +87,6 @@ module Kagu
87
87
  @title = value.to_s.squish.presence
88
88
  end
89
89
 
90
- def xml_album=(value)
91
- self.album = html_entities_decode(value)
92
- end
93
-
94
- def xml_artist=(value)
95
- self.artist = html_entities_decode(value)
96
- end
97
-
98
- def xml_bpm=(value)
99
- self.bpm = value
100
- end
101
-
102
- def xml_date_added=(value)
103
- self.added_at = value.present? ? Time.parse(value.to_s) : nil
104
- end
105
-
106
- def xml_genre=(value)
107
- self.genre = html_entities_decode(value)
108
- end
109
-
110
- def xml_location=(value)
111
- path = CGI.unescape(html_entities_decode(value).gsub('+', '%2B')).gsub(/\Afile:\/\/(localhost)?/, '')
112
- path = path.encode('UTF-8', 'UTF-8-MAC') if Kagu::IS_MAC_OS
113
- self.path = path
114
- end
115
-
116
- def xml_name=(value)
117
- self.title = html_entities_decode(value)
118
- end
119
-
120
- def xml_total_time=(value)
121
- self.length = value.to_s =~ /\A[0-9]+\z/ ? (value.to_i / 1000.0).round : nil
122
- end
123
-
124
- def xml_track_id=(value)
125
- self.id = value
126
- end
127
-
128
- def xml_year=(value)
129
- self.year = value
130
- end
131
-
132
90
  def year=(value)
133
91
  @year = value.to_s =~ /\A\d{,4}\z/ ? value.to_i : nil
134
92
  end
data/lib/kagu/tracks.rb CHANGED
@@ -6,28 +6,42 @@ module Kagu
6
6
 
7
7
  EXTENSIONS = %w(.aac .flac .mp3 .wav).freeze
8
8
 
9
- attr_reader :library
10
-
11
- def initialize(library)
12
- raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
13
- @library = library
14
- end
15
-
16
9
  def each(&block)
17
10
  return unless block_given?
18
- Kagu.logger.debug('Kagu') { "Loading library tracks from #{library.path.inspect}" }
19
- File.open(library.path, 'r') do |file|
20
- while !file.eof? && (line = file.readline.strip)
21
- next unless line.starts_with?('<key>Track ID</key>')
11
+ Kagu.logger.debug('Kagu') { 'Loading library tracks' }
12
+ attributes = {}
13
+ SwiftHelper.execute(%Q{
14
+ import iTunesLibrary
15
+
16
+ func printObjectProperty<T: Encodable>(name: String, value: T?) {
17
+ let jsonEncoder = JSONEncoder()
18
+ let jsonData = try! jsonEncoder.encode(value)
19
+ let json = String(data: jsonData, encoding: String.Encoding.utf8)
20
+ print("\\(name)=\\(json!)")
21
+ }
22
+
23
+ let library = try! ITLibrary(apiVersion: "1")
24
+ for track in library.allMediaItems.filter({ $0.mediaKind == ITLibMediaItemMediaKind.kindSong }) {
25
+ print("BEGIN_TRACK")
26
+ printObjectProperty(name: "added_at", value: track.addedDate!.timeIntervalSince1970)
27
+ printObjectProperty(name: "album", value: track.album.title)
28
+ printObjectProperty(name: "artist", value: track.artist!.name)
29
+ printObjectProperty(name: "bpm", value: track.beatsPerMinute)
30
+ printObjectProperty(name: "genre", value: track.genre)
31
+ printObjectProperty(name: "id", value: String(track.persistentID.uint64Value, radix: 16).uppercased())
32
+ printObjectProperty(name: "length", value: track.totalTime)
33
+ printObjectProperty(name: "path", value: track.location)
34
+ printObjectProperty(name: "title", value: track.title)
35
+ printObjectProperty(name: "year", value: track.year)
36
+ print("END_TRACK")
37
+ }
38
+ }) do |line|
39
+ if line == 'BEGIN_TRACK'
22
40
  attributes = {}
23
- begin
24
- match = line.match(/<key>(.+)<\/key><(\w+)>(.*)<\/\2>/)
25
- next unless match
26
- name = "xml_#{match[1].downcase.gsub(' ', '_')}"
27
- value = match[3]
28
- attributes[name] = value
29
- end while (line = file.readline.strip) != '</dict>'
30
- yield(Track.new(attributes)) if attributes['xml_track_type'] == 'File' && attributes['xml_podcast'].blank? && EXTENSIONS.include?(File.extname(attributes['xml_location'].try(:downcase)))
41
+ elsif line == 'END_TRACK'
42
+ yield(Track.new(attributes))
43
+ elsif match = /(^\w+)=(.*)/.match(line)
44
+ attributes[match[1]] = JSON.parse(match[2])
31
45
  end
32
46
  end
33
47
  end
data/lib/kagu.rb CHANGED
@@ -2,8 +2,10 @@ require 'active_support'
2
2
  require 'active_support/core_ext'
3
3
  require 'applescript'
4
4
  require 'byebug' if ENV['DEBUGGER']
5
- require 'htmlentities'
6
5
  require 'logger'
6
+ require 'open3'
7
+ require 'pathname'
8
+ require 'tempfile'
7
9
 
8
10
  lib_path = "#{__dir__}/kagu"
9
11
 
@@ -29,5 +31,6 @@ require "#{lib_path}/finder"
29
31
  require "#{lib_path}/library"
30
32
  require "#{lib_path}/playlist"
31
33
  require "#{lib_path}/playlists"
34
+ require "#{lib_path}/swift_helper"
32
35
  require "#{lib_path}/track"
33
36
  require "#{lib_path}/tracks"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kagu
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Toulotte
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-09 00:00:00.000000000 Z
11
+ date: 2019-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 4.1.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: 6.0.0
22
+ version: 7.0.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 4.1.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: 6.0.0
32
+ version: 7.0.0
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: applescript
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -50,26 +50,6 @@ dependencies:
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '2.0'
53
- - !ruby/object:Gem::Dependency
54
- name: htmlentities
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: 4.3.0
60
- - - "<"
61
- - !ruby/object:Gem::Version
62
- version: 4.4.0
63
- type: :runtime
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: 4.3.0
70
- - - "<"
71
- - !ruby/object:Gem::Version
72
- version: 4.4.0
73
53
  - !ruby/object:Gem::Dependency
74
54
  name: byebug
75
55
  requirement: !ruby/object:Gem::Requirement
@@ -79,7 +59,7 @@ dependencies:
79
59
  version: 3.2.0
80
60
  - - "<"
81
61
  - !ruby/object:Gem::Version
82
- version: 10.0.0
62
+ version: 12.0.0
83
63
  type: :development
84
64
  prerelease: false
85
65
  version_requirements: !ruby/object:Gem::Requirement
@@ -89,7 +69,7 @@ dependencies:
89
69
  version: 3.2.0
90
70
  - - "<"
91
71
  - !ruby/object:Gem::Version
92
- version: 10.0.0
72
+ version: 12.0.0
93
73
  - !ruby/object:Gem::Dependency
94
74
  name: rake
95
75
  requirement: !ruby/object:Gem::Requirement
@@ -99,7 +79,7 @@ dependencies:
99
79
  version: 10.3.0
100
80
  - - "<"
101
81
  - !ruby/object:Gem::Version
102
- version: 13.0.0
82
+ version: 14.0.0
103
83
  type: :development
104
84
  prerelease: false
105
85
  version_requirements: !ruby/object:Gem::Requirement
@@ -109,7 +89,7 @@ dependencies:
109
89
  version: 10.3.0
110
90
  - - "<"
111
91
  - !ruby/object:Gem::Version
112
- version: 13.0.0
92
+ version: 14.0.0
113
93
  - !ruby/object:Gem::Dependency
114
94
  name: rspec
115
95
  requirement: !ruby/object:Gem::Requirement
@@ -119,7 +99,7 @@ dependencies:
119
99
  version: 3.1.0
120
100
  - - "<"
121
101
  - !ruby/object:Gem::Version
122
- version: 3.8.0
102
+ version: 3.10.0
123
103
  type: :development
124
104
  prerelease: false
125
105
  version_requirements: !ruby/object:Gem::Requirement
@@ -129,7 +109,7 @@ dependencies:
129
109
  version: 3.1.0
130
110
  - - "<"
131
111
  - !ruby/object:Gem::Version
132
- version: 3.8.0
112
+ version: 3.10.0
133
113
  description: API to manage macOS Music tracks and playlists
134
114
  email: al@alweb.org
135
115
  executables: []
@@ -147,6 +127,7 @@ files:
147
127
  - lib/kagu/library.rb
148
128
  - lib/kagu/playlist.rb
149
129
  - lib/kagu/playlists.rb
130
+ - lib/kagu/swift_helper.rb
150
131
  - lib/kagu/track.rb
151
132
  - lib/kagu/tracks.rb
152
133
  homepage: https://github.com/alexistoulotte/kagu