kagu 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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