kbl 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +15 -21
- data/bin/kbl +8 -0
- data/kbl.gemspec +4 -2
- data/lib/kbl/cli.rb +170 -0
- data/lib/kbl/package.rb +27 -0
- data/lib/kbl/playlist.rb +21 -0
- data/lib/kbl/serializable/package.rb +37 -0
- data/lib/kbl/serializable/playlist.rb +24 -0
- data/lib/kbl/serializable/song.rb +26 -0
- data/lib/kbl/song.rb +9 -0
- data/lib/kbl/version.rb +2 -2
- metadata +39 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7675c0becfd84f2bc39016316d19c9cca200daeb
|
4
|
+
data.tar.gz: f3df0ab1a649a300f7ac996f342160cd47f34aa9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a405a118a81a11dcf0a564fe3060358e117bd2d643a72013430ffb95ea69c6cf198f16f2ca31f34ee8b871112965bb3fe0dda9a26333bba877e6b9bff06b2e32
|
7
|
+
data.tar.gz: de489185b4a7ce05520c8b0f4959c207d37f4886eba8b198095923584188a19a143fdf6763ef27ee15fe92ca0d86b193f61d51a9548f6dc3512147434b1c6943
|
data/README.md
CHANGED
@@ -42,6 +42,20 @@ end
|
|
42
42
|
puts kbl.to_kbl #=> the KBL source code (in XML)
|
43
43
|
```
|
44
44
|
|
45
|
+
### CLI Tool
|
46
|
+
|
47
|
+
This gem provides a CLI tool `kbl` that simplifies playlist importing / dumping via Song IDs (pathname).
|
48
|
+
|
49
|
+
Usage:
|
50
|
+
|
51
|
+
```txt
|
52
|
+
# Import KKBOX Songs from a list of IDs through [FILENAME] or STDIN.
|
53
|
+
kbl import [INPUT_FILE or STDIN]
|
54
|
+
|
55
|
+
# Dump meta data of KKBOX songs from your local KKBOX client database.
|
56
|
+
kbl dump [INPUT_FILE or STDIN] [-o=OUTPUT_FILE or STDOUT] [-f=tsv]
|
57
|
+
```
|
58
|
+
|
45
59
|
## Required Song Meta Data
|
46
60
|
|
47
61
|
To generate a working KBL file, the following data for every song must be provided.
|
@@ -94,24 +108,4 @@ Can exist multiple times across multiple playlists.
|
|
94
108
|
|
95
109
|
## License
|
96
110
|
|
97
|
-
The MIT License (MIT)
|
98
|
-
|
99
|
-
Copyright (c) 2014 Yu-Cheng Chuang
|
100
|
-
|
101
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
102
|
-
of this software and associated documentation files (the "Software"), to deal
|
103
|
-
in the Software without restriction, including without limitation the rights
|
104
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
105
|
-
copies of the Software, and to permit persons to whom the Software is
|
106
|
-
furnished to do so, subject to the following conditions:
|
107
|
-
|
108
|
-
The above copyright notice and this permission notice shall be included in
|
109
|
-
all copies or substantial portions of the Software.
|
110
|
-
|
111
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
112
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
113
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
114
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
115
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
116
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
117
|
-
THE SOFTWARE.
|
111
|
+
The MIT License (MIT). See LICENSE.txt.
|
data/bin/kbl
ADDED
data/kbl.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'kbl/version'
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "kbl"
|
8
|
-
spec.version =
|
8
|
+
spec.version = KBL::VERSION
|
9
9
|
spec.authors = ["Yu-Cheng Chuang"]
|
10
10
|
spec.email = ["ducksteven@gmail.com"]
|
11
11
|
spec.summary = %q{KKBOX Playlist Builder}
|
@@ -14,11 +14,13 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
|
-
spec.executables =
|
17
|
+
spec.executables = %w(kbl)
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_dependency 'nokogiri', '~> 1.6.1'
|
22
|
+
spec.add_dependency 'thor', '~> 0.19.1'
|
23
|
+
spec.add_dependency 'sqlite3', '~> 1.3.9'
|
22
24
|
|
23
25
|
spec.add_development_dependency "bundler", "~> 1.5"
|
24
26
|
spec.add_development_dependency "rake"
|
data/lib/kbl/cli.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'readline'
|
3
|
+
require 'sqlite3'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module KBL
|
7
|
+
class CLI < Thor
|
8
|
+
class MetaData < Struct.new(:song_id, :song_name, :artist_id, :artist_name, :album_id, :album_name)
|
9
|
+
def to_a
|
10
|
+
[
|
11
|
+
self.song_id,
|
12
|
+
self.song_name,
|
13
|
+
self.artist_id,
|
14
|
+
self.artist_name,
|
15
|
+
self.album_id,
|
16
|
+
self.album_name
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_tsv
|
21
|
+
to_a.join "\t"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "import [INPUT_FILE or STDIN]", "Import KKBOX Songs from a list of IDs through [FILENAME] or STDIN."
|
26
|
+
long_desc <<-EOS
|
27
|
+
Given a list of KKBOX Song IDs (pathname), import them to your KKBOX client's
|
28
|
+
Temporary Playlist, by brute-forcely `open`ing a bunch of
|
29
|
+
`kkbox://play_song_xxxx` URLs.
|
30
|
+
|
31
|
+
This will affect your current Temporary Playlist and make "now playing"
|
32
|
+
song to the last song in the list.
|
33
|
+
EOS
|
34
|
+
|
35
|
+
option :yes, :type => :boolean, :default => false, :aliases => :y,
|
36
|
+
:desc => "Skip warning for overriding KKBOX now playing."
|
37
|
+
|
38
|
+
def import(filename=nil)
|
39
|
+
input = open_file(filename) || STDIN
|
40
|
+
|
41
|
+
ids = input.readlines
|
42
|
+
ids.each(&:chomp!).keep_if {|id| id.match(/\d+/) }#.delete("")
|
43
|
+
|
44
|
+
if ids.empty?
|
45
|
+
puts "No songs to import. Exit."
|
46
|
+
exit 2
|
47
|
+
end
|
48
|
+
|
49
|
+
if !options[:yes]
|
50
|
+
puts "\e[1;31mThis will import #{ids.size} songs into KKBOX and play the last song.\e[m"
|
51
|
+
|
52
|
+
while answer = Readline.readline("Continue? [Y/n] ", false)
|
53
|
+
case answer.downcase
|
54
|
+
when 'n', 'no'
|
55
|
+
puts "Cancelled."
|
56
|
+
exit 3
|
57
|
+
when 'y', 'yes', ''
|
58
|
+
break # stop asking and continue importing
|
59
|
+
else
|
60
|
+
puts "Please answer 'y' or 'n', or hit enter for 'y'."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "Importing..."
|
66
|
+
|
67
|
+
system "open #{ids.map { |id| "kkbox://play_song_#{id}" }.join(' ')}"
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "dump [INPUT_FILE or STDIN] [-o=OUTPUT_FILE or STDOUT] [-f=tsv]",
|
71
|
+
"Dump meta data of KKBOX songs from your local KKBOX client database."
|
72
|
+
long_desc <<-EOS
|
73
|
+
Given a list of KKBOX Song IDs (pathname), find their meta data from your
|
74
|
+
KKBOX client's local database, and if found, export them to the file
|
75
|
+
set in --output option, or the STDOUT stream if not given.
|
76
|
+
|
77
|
+
If you have multiple accounts logged in to KKBOX client,
|
78
|
+
it will look into all of them.
|
79
|
+
|
80
|
+
The output contains the following fields:
|
81
|
+
|
82
|
+
name: Song Title
|
83
|
+
|
84
|
+
pathname: Song ID
|
85
|
+
|
86
|
+
artist: Artist Name
|
87
|
+
|
88
|
+
artist_id: Artist ID
|
89
|
+
|
90
|
+
album: Album Name
|
91
|
+
|
92
|
+
album_id: Album ID
|
93
|
+
|
94
|
+
For tabular format like TSV and CSV, these columns will be printed
|
95
|
+
in the above order.
|
96
|
+
EOS
|
97
|
+
|
98
|
+
option :format, :type => :string, :default => "tsv", :aliases => :f,
|
99
|
+
:desc => "Format of output data.",
|
100
|
+
:banner => "tsv"
|
101
|
+
|
102
|
+
option :output, :type => :string, :default => nil, :aliases => :o,
|
103
|
+
:desc => "The output file to write. Uses STDOUT if absent.",
|
104
|
+
:banner => "OUTPUT_FILE"
|
105
|
+
|
106
|
+
def dump(input_filename=nil)
|
107
|
+
input = open_file(input_filename) || STDIN
|
108
|
+
|
109
|
+
if output_filename = options[:output]
|
110
|
+
output = open_file(output_filename, 'w')
|
111
|
+
if output.nil?
|
112
|
+
puts "Cannot open file #{output_filename} for output."
|
113
|
+
exit 1
|
114
|
+
end
|
115
|
+
else
|
116
|
+
output = STDOUT
|
117
|
+
end
|
118
|
+
|
119
|
+
ids = input.readlines
|
120
|
+
ids.each(&:chomp!).keep_if {|id| id.match(/\d+/) }#.delete("")
|
121
|
+
|
122
|
+
if ids.empty?
|
123
|
+
puts "No songs to export. Exit."
|
124
|
+
exit 2
|
125
|
+
end
|
126
|
+
|
127
|
+
meta_data = {}
|
128
|
+
|
129
|
+
db_files = Dir.glob(File.expand_path("~/Library/Application Support/KKBOX/*/Playlists2.db"))
|
130
|
+
|
131
|
+
db_files.each do |db_file|
|
132
|
+
db = SQLite3::Database.new db_file
|
133
|
+
|
134
|
+
sql = <<-SQL
|
135
|
+
SELECT song_id, song_name, artist_id, artist_name, album_id, album_name
|
136
|
+
FROM song_tracks
|
137
|
+
WHERE song_id IN (#{ids.map {|id|"'#{id}'"}.join(',')});
|
138
|
+
SQL
|
139
|
+
|
140
|
+
db.execute(sql) do |row|
|
141
|
+
data = MetaData.new(*row)
|
142
|
+
|
143
|
+
meta_data[data.song_id] = data
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
ids.each do |id|
|
148
|
+
if data = meta_data[id]
|
149
|
+
output.puts data.send(:"to_#{options[:format]}")
|
150
|
+
else
|
151
|
+
STDERR.puts "ID Not Found: #{id}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
def open_file(filename, mode='r')
|
158
|
+
if filename
|
159
|
+
begin
|
160
|
+
input = File.open(filename, mode)
|
161
|
+
rescue Errno::ENOENT => e
|
162
|
+
puts "Cannot Open File #{filename} in #{mode} mode"
|
163
|
+
exit 1
|
164
|
+
end
|
165
|
+
else
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/lib/kbl/package.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module KBL
|
2
|
+
class Package
|
3
|
+
include KBL::Serializable::Package
|
4
|
+
|
5
|
+
attr_reader :kkbox_version, :version, :description, :date, :playlists
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@date = Time.now
|
9
|
+
@playlists = []
|
10
|
+
@kkbox_version = "04000016"
|
11
|
+
|
12
|
+
block.call(self) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_playlist(&block)
|
16
|
+
playlist = Playlist.new
|
17
|
+
playlist.id = @playlists.size + 1
|
18
|
+
block.call(playlist)
|
19
|
+
|
20
|
+
@playlists << playlist
|
21
|
+
end
|
22
|
+
|
23
|
+
def total_songs
|
24
|
+
@playlists.map { |playlist| playlist.songs.size }.reduce(:+)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/kbl/playlist.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module KBL
|
2
|
+
class Playlist
|
3
|
+
include KBL::Serializable::Playlist
|
4
|
+
|
5
|
+
attr_reader :songs
|
6
|
+
attr_accessor :name, :description, :id
|
7
|
+
|
8
|
+
def initialize(&block)
|
9
|
+
@songs = []
|
10
|
+
|
11
|
+
block.call(self) if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_song(&block)
|
15
|
+
song = Song.new
|
16
|
+
block.call(song)
|
17
|
+
|
18
|
+
@songs << song
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module KBL::Serializable
|
2
|
+
module Package
|
3
|
+
def to_kbl
|
4
|
+
to_kbl_node.to_xml({
|
5
|
+
:encoding => 'UTF-8',
|
6
|
+
:indent => 2,
|
7
|
+
:indent_text => " "
|
8
|
+
}).gsub(/<(.+)\/>/, '<\1></\1>')
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_kbl_node
|
12
|
+
doc = Nokogiri::XML.parse(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS)
|
13
|
+
<utf-8_data><kkbox_package></kkbox_package></utf-8_data>
|
14
|
+
XML
|
15
|
+
|
16
|
+
package_node = doc.css("kkbox_package").first
|
17
|
+
|
18
|
+
package_node << "<kkbox_ver>#{self.kkbox_version}</kkbox_ver>"
|
19
|
+
|
20
|
+
@playlists.each do |playlist|
|
21
|
+
package_node << playlist.to_kbl_node
|
22
|
+
end
|
23
|
+
|
24
|
+
package_node << Nokogiri::XML.parse(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root
|
25
|
+
<package>
|
26
|
+
<ver>1.0</ver>
|
27
|
+
<descr>包裝說明</descr>
|
28
|
+
<packdate>#{ self.date.strftime("%Y%m%d%H%M%S") }</packdate>
|
29
|
+
<playlistcnt>#{ self.playlists.size }</playlistcnt>
|
30
|
+
<songcnt>#{ self.total_songs }</songcnt>
|
31
|
+
</package>
|
32
|
+
XML
|
33
|
+
|
34
|
+
doc.root
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module KBL::Serializable
|
4
|
+
module Playlist
|
5
|
+
def to_kbl_node
|
6
|
+
node = Nokogiri::XML.parse(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS)
|
7
|
+
<playlist>
|
8
|
+
<playlist_id>#{ self.id }</playlist_id>
|
9
|
+
<playlist_name>#{ self.name }</playlist_name>
|
10
|
+
<playlist_descr>#{ self.description }</playlist_descr>
|
11
|
+
<playlist_data></playlist_data>
|
12
|
+
</playlist>
|
13
|
+
XML
|
14
|
+
|
15
|
+
data_node = node.css("playlist_data").first
|
16
|
+
|
17
|
+
@songs.each do |song|
|
18
|
+
data_node << song.to_kbl_node
|
19
|
+
end
|
20
|
+
|
21
|
+
node.root
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module KBL::Serializable
|
4
|
+
module Song
|
5
|
+
def to_kbl_node
|
6
|
+
doc = Nokogiri::XML.parse(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS)
|
7
|
+
<song_data>
|
8
|
+
<song_name>#{ self.name }</song_name>
|
9
|
+
<song_artist>#{ self.artist }</song_artist>
|
10
|
+
<song_album>#{ self.album }</song_album>
|
11
|
+
<song_genre>#{ self.genre }</song_genre>
|
12
|
+
<song_preference>#{ self.preference }</song_preference>
|
13
|
+
<song_playcnt>#{ self.play_count }</song_playcnt>
|
14
|
+
<song_pathname>#{ self.pathname }</song_pathname>
|
15
|
+
<song_type>#{ self.type }</song_type>
|
16
|
+
<song_lyricsexist>#{ self.has_lyrics }</song_lyricsexist>
|
17
|
+
<song_artist_id>#{ self.artist_id }</song_artist_id>
|
18
|
+
<song_album_id>#{ self.album_id }</song_album_id>
|
19
|
+
<song_song_idx>#{ self.song_index }</song_song_idx>
|
20
|
+
</song_data>
|
21
|
+
XML
|
22
|
+
|
23
|
+
doc.root
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/kbl/song.rb
ADDED
data/lib/kbl/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module
|
2
|
-
VERSION = "0.0
|
1
|
+
module KBL
|
2
|
+
VERSION = "0.1.0"
|
3
3
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kbl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yu-Cheng Chuang
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - ~>
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.6.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.19.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.19.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.3.9
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.3.9
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: bundler
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -55,7 +83,8 @@ dependencies:
|
|
55
83
|
description: Building KKBOX Playlist and serialize to KBL format.
|
56
84
|
email:
|
57
85
|
- ducksteven@gmail.com
|
58
|
-
executables:
|
86
|
+
executables:
|
87
|
+
- kbl
|
59
88
|
extensions: []
|
60
89
|
extra_rdoc_files: []
|
61
90
|
files:
|
@@ -64,8 +93,16 @@ files:
|
|
64
93
|
- LICENSE.txt
|
65
94
|
- README.md
|
66
95
|
- Rakefile
|
96
|
+
- bin/kbl
|
67
97
|
- kbl.gemspec
|
68
98
|
- lib/kbl.rb
|
99
|
+
- lib/kbl/cli.rb
|
100
|
+
- lib/kbl/package.rb
|
101
|
+
- lib/kbl/playlist.rb
|
102
|
+
- lib/kbl/serializable/package.rb
|
103
|
+
- lib/kbl/serializable/playlist.rb
|
104
|
+
- lib/kbl/serializable/song.rb
|
105
|
+
- lib/kbl/song.rb
|
69
106
|
- lib/kbl/version.rb
|
70
107
|
homepage: ''
|
71
108
|
licenses:
|