nehm 1.6.1 → 2.0.1
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/CHANGELOG.md +10 -0
- data/README.md +52 -35
- data/Rakefile +63 -13
- data/bin/nehm +0 -1
- data/lib/nehm.rb +33 -11
- data/lib/nehm/cfg.rb +19 -6
- data/lib/nehm/client.rb +30 -33
- data/lib/nehm/command.rb +19 -34
- data/lib/nehm/command_manager.rb +8 -18
- data/lib/nehm/commands/configure_command.rb +17 -14
- data/lib/nehm/commands/dl_command.rb +13 -11
- data/lib/nehm/commands/get_command.rb +36 -12
- data/lib/nehm/commands/help_command.rb +5 -5
- data/lib/nehm/commands/search_command.rb +41 -0
- data/lib/nehm/commands/select_command.rb +56 -0
- data/lib/nehm/http_client.rb +66 -0
- data/lib/nehm/menu.rb +96 -0
- data/lib/nehm/option_parser.rb +0 -2
- data/lib/nehm/os.rb +2 -2
- data/lib/nehm/path_manager.rb +5 -24
- data/lib/nehm/playlist.rb +2 -2
- data/lib/nehm/playlist_manager.rb +3 -5
- data/lib/nehm/track.rb +4 -2
- data/lib/nehm/track_manager.rb +135 -0
- data/lib/nehm/tracks_view_command.rb +123 -0
- data/lib/nehm/ui.rb +18 -2
- data/lib/nehm/user_manager.rb +5 -4
- data/lib/nehm/version.rb +1 -1
- data/nehm.gemspec +4 -7
- metadata +13 -64
- data/lib/nehm/tracks.rb +0 -153
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Nehm
|
5
|
+
|
6
|
+
class HTTPClient
|
7
|
+
|
8
|
+
##
|
9
|
+
# SoundCloud API client ID
|
10
|
+
|
11
|
+
CLIENT_ID = '11a37feb6ccc034d5975f3f803928a32'
|
12
|
+
|
13
|
+
##
|
14
|
+
# Exception classes
|
15
|
+
|
16
|
+
class Status404 < StandardError; end
|
17
|
+
class ConnectionError < StandardError; end
|
18
|
+
|
19
|
+
def get(api_version, uri_string)
|
20
|
+
uri = form_uri(api_version, uri_string)
|
21
|
+
get_hash(uri)
|
22
|
+
end
|
23
|
+
|
24
|
+
def resolve(url)
|
25
|
+
response = get(1, "/resolve?url=#{url}")
|
26
|
+
|
27
|
+
errors = response['errors']
|
28
|
+
if errors
|
29
|
+
if errors[0]['error_message'] =~ /404/
|
30
|
+
raise Status404
|
31
|
+
else
|
32
|
+
raise ConnectionError # HACK
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if response['status'] =~ /302/
|
37
|
+
get_hash(response['location'])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def form_uri(api_version, uri_string)
|
44
|
+
uri =
|
45
|
+
case api_version
|
46
|
+
when 1
|
47
|
+
'https://api.soundcloud.com'
|
48
|
+
when 2
|
49
|
+
'https://api-v2.soundcloud.com'
|
50
|
+
end
|
51
|
+
uri += uri_string
|
52
|
+
uri += "&client_id=#{CLIENT_ID}" if api_version == 1
|
53
|
+
|
54
|
+
uri
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_hash(uri)
|
58
|
+
uri = URI.parse(URI.escape(uri))
|
59
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
60
|
+
http.use_ssl = true
|
61
|
+
res = http.get(uri.request_uri)
|
62
|
+
JSON.parse(res.body)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/lib/nehm/menu.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module Nehm
|
2
|
+
module UI
|
3
|
+
class Menu
|
4
|
+
|
5
|
+
attr_writer :msg_bar, :header
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@choices = {}
|
9
|
+
@inc_index = 1
|
10
|
+
@items = []
|
11
|
+
|
12
|
+
yield self
|
13
|
+
select
|
14
|
+
end
|
15
|
+
|
16
|
+
def choice(index, desc, &block)
|
17
|
+
# Visual index - index that you see in menu
|
18
|
+
# Select index - index than can be selected
|
19
|
+
# For example, if you use ':added' index
|
20
|
+
# In menu you see 'A', but you can select it by number
|
21
|
+
# You receive a warning though
|
22
|
+
|
23
|
+
visual_index = select_index = index
|
24
|
+
|
25
|
+
if index == :inc
|
26
|
+
visual_index = select_index = @inc_index.to_s
|
27
|
+
@inc_index += 1
|
28
|
+
end
|
29
|
+
|
30
|
+
if index == :added
|
31
|
+
visual_index = 'A'.green
|
32
|
+
select_index = @inc_index.to_s
|
33
|
+
@inc_index += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
@choices[select_index] = block
|
37
|
+
@items << "#{visual_index} #{desc}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def show_header
|
41
|
+
UI.say @header
|
42
|
+
UI.newline
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Message bar used to show messages of
|
47
|
+
# last successful or not operations
|
48
|
+
# Shown before menu
|
49
|
+
|
50
|
+
def show_msg_bar
|
51
|
+
UI.say 'Message:'
|
52
|
+
UI.say " #{@msg_bar}"
|
53
|
+
UI.newline
|
54
|
+
@msg_bar.clear
|
55
|
+
end
|
56
|
+
|
57
|
+
def newline
|
58
|
+
@items << "\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
def select
|
62
|
+
show_header unless @header.to_s.empty?
|
63
|
+
show_msg_bar unless @msg_bar.to_s.empty?
|
64
|
+
|
65
|
+
# Add exit option
|
66
|
+
newline
|
67
|
+
choice('e', 'Exit'.red) { UI.term }
|
68
|
+
|
69
|
+
# Output items
|
70
|
+
@items.each do |item|
|
71
|
+
UI.say item
|
72
|
+
end
|
73
|
+
|
74
|
+
UI.newline
|
75
|
+
|
76
|
+
selected = UI.ask('Enter option'.yellow.freeze)
|
77
|
+
call_selected_block(selected)
|
78
|
+
|
79
|
+
UI.newline
|
80
|
+
end
|
81
|
+
|
82
|
+
def call_selected_block(selected)
|
83
|
+
loop do
|
84
|
+
if @choices.keys.include? selected
|
85
|
+
block = @choices[selected]
|
86
|
+
block.call
|
87
|
+
break
|
88
|
+
else
|
89
|
+
selected = UI.ask "You must choose one of [#{@choices.keys.join(', ')}]"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/nehm/option_parser.rb
CHANGED
data/lib/nehm/os.rb
CHANGED
@@ -6,8 +6,8 @@ module Nehm
|
|
6
6
|
# OS module returns information about OS on this computer
|
7
7
|
|
8
8
|
module OS
|
9
|
-
def self.
|
10
|
-
RbConfig::CONFIG['host_os'] =~ /
|
9
|
+
def self.mac?
|
10
|
+
RbConfig::CONFIG['host_os'] =~ /darwin|mac os/ ? true : false
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
data/lib/nehm/path_manager.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
module Nehm
|
2
2
|
|
3
3
|
##
|
4
|
-
# Path manager works with download
|
4
|
+
# Path manager works with download paths
|
5
5
|
|
6
6
|
module PathManager
|
7
7
|
|
8
|
-
##
|
9
|
-
# Returns default download path (contains in ~/.nehmconfig)
|
10
|
-
|
11
8
|
def self.default_dl_path
|
12
9
|
Cfg[:dl_path]
|
13
10
|
end
|
@@ -16,13 +13,10 @@ module Nehm
|
|
16
13
|
# Checks path for validation and returns it if valid
|
17
14
|
|
18
15
|
def self.get_path(path)
|
19
|
-
# If path begins with ~
|
20
|
-
path = tilde_to_home(path) if tilde_at_top?(path)
|
21
|
-
|
22
16
|
# Check path for existence
|
23
17
|
UI.term 'Invalid download path! Please enter correct path' unless Dir.exist?(path)
|
24
18
|
|
25
|
-
path
|
19
|
+
File.expand_path(path)
|
26
20
|
end
|
27
21
|
|
28
22
|
def self.set_dl_path
|
@@ -31,21 +25,18 @@ module Nehm
|
|
31
25
|
default_path = File.join(ENV['HOME'], '/Music')
|
32
26
|
|
33
27
|
if Dir.exist?(default_path)
|
34
|
-
ask_sentence << " (press
|
28
|
+
ask_sentence << " (press Enter to set it to #{default_path.magenta})"
|
35
29
|
else
|
36
30
|
default_path = nil
|
37
31
|
end
|
38
32
|
|
39
|
-
path =
|
33
|
+
path = UI.ask(ask_sentence + ':')
|
40
34
|
|
41
35
|
# If user press enter, set path to default
|
42
36
|
path = default_path if path == '' && default_path
|
43
37
|
|
44
|
-
# If tilde at top of the line of path
|
45
|
-
path = PathManager.tilde_to_home(path) if PathManager.tilde_at_top?(path)
|
46
|
-
|
47
38
|
if Dir.exist?(path)
|
48
|
-
Cfg[:dl_path] = path
|
39
|
+
Cfg[:dl_path] = File.expand_path(path)
|
49
40
|
UI.say "#{'Download directory set up to'.green} #{path.magenta}"
|
50
41
|
break
|
51
42
|
else
|
@@ -54,15 +45,5 @@ module Nehm
|
|
54
45
|
end
|
55
46
|
end
|
56
47
|
|
57
|
-
module_function
|
58
|
-
|
59
|
-
def tilde_at_top?(path)
|
60
|
-
path[0] == '~'
|
61
|
-
end
|
62
|
-
|
63
|
-
def tilde_to_home(path)
|
64
|
-
File.join(ENV['HOME'], path[1..-1])
|
65
|
-
end
|
66
|
-
|
67
48
|
end
|
68
49
|
end
|
data/lib/nehm/playlist.rb
CHANGED
@@ -11,9 +11,9 @@ module Nehm
|
|
11
11
|
@name = name.chomp
|
12
12
|
end
|
13
13
|
|
14
|
-
def add_track(
|
14
|
+
def add_track(track)
|
15
15
|
UI.say 'Adding to iTunes'
|
16
|
-
AppleScript.add_track_to_playlist(
|
16
|
+
AppleScript.add_track_to_playlist(track.file_path, @name)
|
17
17
|
end
|
18
18
|
|
19
19
|
def to_s
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'nehm/applescript'
|
1
2
|
require 'nehm/playlist'
|
2
3
|
|
3
4
|
module Nehm
|
@@ -7,11 +8,8 @@ module Nehm
|
|
7
8
|
|
8
9
|
module PlaylistManager
|
9
10
|
|
10
|
-
##
|
11
|
-
# Returns default iTunes playlist (contains in ~/.nehmconfig)
|
12
|
-
|
13
11
|
def self.default_playlist
|
14
|
-
default_user_playlist || music_master_library
|
12
|
+
default_user_playlist || music_master_library if OS.mac?
|
15
13
|
end
|
16
14
|
|
17
15
|
##
|
@@ -27,7 +25,7 @@ module Nehm
|
|
27
25
|
|
28
26
|
def self.set_playlist
|
29
27
|
loop do
|
30
|
-
playlist =
|
28
|
+
playlist = UI.ask('Enter name of default iTunes playlist to that you want add tracks (press Enter to set it to default iTunes Music library):')
|
31
29
|
|
32
30
|
# If entered nothing, unset iTunes playlist
|
33
31
|
if playlist == ''
|
data/lib/nehm/track.rb
CHANGED
@@ -42,7 +42,7 @@ module Nehm
|
|
42
42
|
|
43
43
|
separators = [' - ', ' ~ ']
|
44
44
|
separators.each do |sep|
|
45
|
-
return title.split(sep) if title.include? sep
|
45
|
+
return title.split(sep, 2) if title.include? sep
|
46
46
|
end
|
47
47
|
|
48
48
|
[@hash['user']['username'], title]
|
@@ -53,7 +53,9 @@ module Nehm
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def url
|
56
|
-
|
56
|
+
# API V2 track hash has no 'stream_url' but 'uri'
|
57
|
+
dl_url = @hash['uri'] ? "#{@hash['uri']}/stream" : @hash['stream_url']
|
58
|
+
"#{dl_url}?client_id=#{HTTPClient::CLIENT_ID}"
|
57
59
|
end
|
58
60
|
|
59
61
|
def year
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'taglib'
|
2
|
+
|
3
|
+
require 'nehm/track'
|
4
|
+
|
5
|
+
module Nehm
|
6
|
+
|
7
|
+
class TrackManager
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
setup_environment(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_tracks(tracks)
|
14
|
+
tracks.reverse_each do |track|
|
15
|
+
UI.newline
|
16
|
+
dl(track)
|
17
|
+
tag(track)
|
18
|
+
track.artwork.suicide
|
19
|
+
@playlist.add_track(track) if @playlist
|
20
|
+
UI.newline
|
21
|
+
end
|
22
|
+
UI.success 'Done!'
|
23
|
+
end
|
24
|
+
|
25
|
+
def likes(limit, offset)
|
26
|
+
likes = Client.tracks(limit, offset, :likes, @uid)
|
27
|
+
return nil if likes.empty?
|
28
|
+
|
29
|
+
filter(likes)
|
30
|
+
convert(likes)
|
31
|
+
end
|
32
|
+
|
33
|
+
def posts(limit, offset)
|
34
|
+
posts = Client.tracks(limit, offset, :posts, @uid)
|
35
|
+
return nil if posts.empty?
|
36
|
+
|
37
|
+
posts.reject! { |hash| hash['type'] == 'playlist' }
|
38
|
+
posts.map! { |hash| hash['track'] }
|
39
|
+
filter(posts)
|
40
|
+
convert(posts)
|
41
|
+
end
|
42
|
+
|
43
|
+
def track_from_url(url)
|
44
|
+
track = [Client.track(url)]
|
45
|
+
return nil if track.empty?
|
46
|
+
|
47
|
+
filter(track)
|
48
|
+
convert(track)
|
49
|
+
end
|
50
|
+
|
51
|
+
def search(query, limit, offset)
|
52
|
+
found = Client.search(query, limit, offset)
|
53
|
+
return nil if found.empty?
|
54
|
+
|
55
|
+
filter(found)
|
56
|
+
convert(found)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def dl(track)
|
62
|
+
# Downloading track
|
63
|
+
UI.say 'Downloading ' + track.full_name
|
64
|
+
`curl -# -o \"#{track.file_path}\" -L #{track.url}`
|
65
|
+
|
66
|
+
# Downloading artwork
|
67
|
+
UI.say 'Downloading artwork'
|
68
|
+
artwork = track.artwork
|
69
|
+
`curl -# -o \"#{artwork.file_path}\" -L #{artwork.url}`
|
70
|
+
end
|
71
|
+
|
72
|
+
def tag(track)
|
73
|
+
UI.say 'Setting tags'
|
74
|
+
TagLib::MPEG::File.open(track.file_path) do |file|
|
75
|
+
tag = file.id3v2_tag
|
76
|
+
tag.artist = track.artist
|
77
|
+
tag.title = track.title
|
78
|
+
tag.year = track.year
|
79
|
+
|
80
|
+
# Adding artwork
|
81
|
+
apic = TagLib::ID3v2::AttachedPictureFrame.new
|
82
|
+
apic.mime_type = 'image/jpeg'
|
83
|
+
apic.description = 'Cover'
|
84
|
+
apic.type = TagLib::ID3v2::AttachedPictureFrame::FrontCover
|
85
|
+
apic.picture = File.open(track.artwork.file_path, 'rb') { |f| f.read }
|
86
|
+
tag.add_frame(apic)
|
87
|
+
|
88
|
+
file.save
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def setup_environment(options)
|
93
|
+
# Setting up user id
|
94
|
+
permalink = options[:from]
|
95
|
+
@uid = permalink ? UserManager.get_uid(permalink) : UserManager.default_uid
|
96
|
+
unless @uid
|
97
|
+
UI.error "You didn't logged in"
|
98
|
+
UI.say "Login from #{'nehm configure'.yellow} or use #{'[from PERMALINK]'.yellow} option"
|
99
|
+
UI.term
|
100
|
+
end
|
101
|
+
|
102
|
+
# Setting up download path
|
103
|
+
temp_path = options[:to]
|
104
|
+
dl_path = temp_path ? PathManager.get_path(temp_path) : PathManager.default_dl_path
|
105
|
+
if dl_path
|
106
|
+
ENV['dl_path'] = dl_path
|
107
|
+
else
|
108
|
+
UI.error "You don't set up download path!"
|
109
|
+
UI.say "Set it up from #{'nehm configure'.yellow} or use #{'[to PATH_TO_DIRECTORY]'.yellow} option"
|
110
|
+
UI.term
|
111
|
+
end
|
112
|
+
|
113
|
+
# Setting up iTunes playlist
|
114
|
+
@playlist = nil
|
115
|
+
if !options[:dl] && OS.mac?
|
116
|
+
playlist_name = options[:pl]
|
117
|
+
@playlist = playlist_name ? PlaylistManager.get_playlist(playlist_name) : PlaylistManager.default_playlist
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def filter(tracks)
|
122
|
+
# Removing unstreamable tracks
|
123
|
+
first_length = tracks.length
|
124
|
+
tracks.select! { |hash| hash['streamable'] }
|
125
|
+
diff = first_length - tracks.length
|
126
|
+
|
127
|
+
UI.warning "Was skipped #{diff} undownloadable track(s)" if diff > 0
|
128
|
+
end
|
129
|
+
|
130
|
+
def convert(tracks)
|
131
|
+
tracks.map! { |hash| Track.new(hash) }
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|