rmd 0.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 +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +2 -0
- data/bin/rmd +4 -0
- data/fixtures/vcr_cassettes/song_download.yml +186 -0
- data/lib/rmd.rb +8 -0
- data/lib/rmd/downloader.rb +51 -0
- data/lib/rmd/factory.rb +29 -0
- data/lib/rmd/main.rb +12 -0
- data/lib/rmd/nct/getter/base.rb +31 -0
- data/lib/rmd/nct/getter/key_from_page.rb +50 -0
- data/lib/rmd/nct/getter/key_from_url.rb +44 -0
- data/lib/rmd/nct/playlist.rb +63 -0
- data/lib/rmd/nct/song.rb +36 -0
- data/lib/rmd/processor.rb +44 -0
- data/lib/rmd/song_playlist_adapter.rb +20 -0
- data/lib/rmd/version.rb +3 -0
- data/rmd.gemspec +31 -0
- data/spec/rmd/downloader_spec.rb +33 -0
- data/spec/rmd/factory_spec.rb +31 -0
- data/spec/rmd/nct/getter/key_from_page_spec.rb +140 -0
- data/spec/rmd/nct/getter/key_from_url_spec.rb +113 -0
- data/spec/rmd/nct/playlist_spec.rb +100 -0
- data/spec/rmd/nct/song_spec.rb +57 -0
- data/spec/rmd/processor_spec.rb +62 -0
- data/spec/rmd/song_playlist_adapter_spec.rb +35 -0
- data/spec/spec_helper.rb +102 -0
- metadata +211 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rmd/nct/getter/base'
|
2
|
+
|
3
|
+
module RMD
|
4
|
+
module NCT
|
5
|
+
module Getter
|
6
|
+
class KeyFromPage < Base
|
7
|
+
|
8
|
+
def fetch
|
9
|
+
if key && element
|
10
|
+
@data_link = element.text.strip
|
11
|
+
else
|
12
|
+
@errors = "The page does not contain the key."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def uncached_key
|
19
|
+
page.search('script').each do |element|
|
20
|
+
if match_data = /NCTNowPlaying.intFlashPlayer\("flashPlayer", "song", "(.+)"\)\;/.match(element.text)
|
21
|
+
return match_data.to_a.last
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def new_link
|
29
|
+
"http://www.nhaccuatui.com/flash/xml?key1=#{key}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def page
|
33
|
+
@page ||= agent.get(url)
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_page
|
37
|
+
@new_page ||= agent.get(new_link)
|
38
|
+
end
|
39
|
+
|
40
|
+
def response
|
41
|
+
@response ||= Nokogiri::XML(new_page.body)
|
42
|
+
end
|
43
|
+
|
44
|
+
def element
|
45
|
+
@element ||= response.at_xpath('.//tracklist//location')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rmd/nct/getter/base'
|
2
|
+
|
3
|
+
module RMD
|
4
|
+
module NCT
|
5
|
+
module Getter
|
6
|
+
class KeyFromUrl < Base
|
7
|
+
ROOT_URL = 'http://www.nhaccuatui.com/download/song'
|
8
|
+
|
9
|
+
def fetch
|
10
|
+
if key
|
11
|
+
if response['error_message'] == 'Success'
|
12
|
+
@data_link = response['data']['stream_url']
|
13
|
+
else
|
14
|
+
@errors = "#{response['error_message']}: #{new_link}"
|
15
|
+
end
|
16
|
+
else
|
17
|
+
@errors = "The url does not contain the key."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def response
|
24
|
+
@response ||= JSON.parse(page.body)
|
25
|
+
end
|
26
|
+
|
27
|
+
def uncached_key
|
28
|
+
uri = URI.parse(url)
|
29
|
+
if data = /\.(\S+)\./.match(uri.path)
|
30
|
+
data.to_a.last
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_link
|
35
|
+
"#{ROOT_URL}/#{key}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def page
|
39
|
+
@page ||= agent.get(new_link)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
|
3
|
+
module RMD
|
4
|
+
module NCT
|
5
|
+
class Playlist
|
6
|
+
attr_reader :errors, :link, :songs
|
7
|
+
|
8
|
+
def initialize(link)
|
9
|
+
@link = link
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch
|
13
|
+
if song_elements.count > 0
|
14
|
+
@errors = []
|
15
|
+
progress_bar = ProgressBar.create(
|
16
|
+
starting_at: 0,
|
17
|
+
total: song_elements.count,
|
18
|
+
format: '%c / %C Songs %B %p%%'
|
19
|
+
)
|
20
|
+
|
21
|
+
@songs = song_elements.inject([]) do |result, element|
|
22
|
+
link = element.attr('href')
|
23
|
+
song = RMD::NCT::Song.new(link)
|
24
|
+
song.fetch
|
25
|
+
if song.success?
|
26
|
+
result << song.data_link
|
27
|
+
else
|
28
|
+
@errors << song.errors
|
29
|
+
end
|
30
|
+
|
31
|
+
progress_bar.increment
|
32
|
+
result
|
33
|
+
end
|
34
|
+
else
|
35
|
+
@songs = []
|
36
|
+
@errors = ['Can not get song lists from this playlist page.']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def success?
|
41
|
+
!!songs && songs.count > 0
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def song_css
|
47
|
+
'.item_content .name_song'
|
48
|
+
end
|
49
|
+
|
50
|
+
def song_elements
|
51
|
+
@song_element ||= page.search(song_css)
|
52
|
+
end
|
53
|
+
|
54
|
+
def page
|
55
|
+
@page ||= agent.get(link)
|
56
|
+
end
|
57
|
+
|
58
|
+
def agent
|
59
|
+
agent ||= Mechanize.new
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/rmd/nct/song.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rmd/nct/getter/key_from_page'
|
2
|
+
require 'rmd/nct/getter/key_from_url'
|
3
|
+
|
4
|
+
module RMD
|
5
|
+
module NCT
|
6
|
+
class Song
|
7
|
+
attr_reader :errors, :link, :data_link
|
8
|
+
|
9
|
+
def initialize(link)
|
10
|
+
@link = link
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch
|
14
|
+
getters.each do |getter|
|
15
|
+
getter.fetch
|
16
|
+
@data_link = getter.data_link
|
17
|
+
@errors = getter.errors
|
18
|
+
break unless @errors
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def success?
|
23
|
+
!!data_link
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def getters
|
29
|
+
[
|
30
|
+
RMD::NCT::Getter::KeyFromUrl.new(link),
|
31
|
+
RMD::NCT::Getter::KeyFromPage.new(link)
|
32
|
+
]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rmd/downloader'
|
2
|
+
require 'rmd/factory'
|
3
|
+
require 'ruby-progressbar'
|
4
|
+
|
5
|
+
module RMD
|
6
|
+
class Processor
|
7
|
+
attr_reader :link
|
8
|
+
|
9
|
+
def initialize(link)
|
10
|
+
@link = link
|
11
|
+
end
|
12
|
+
|
13
|
+
def process
|
14
|
+
puts "Start processing #{link}...".green
|
15
|
+
playlist = RMD::Factory.build(link)
|
16
|
+
playlist.fetch
|
17
|
+
|
18
|
+
if playlist.success?
|
19
|
+
playlist.songs.each do |song|
|
20
|
+
download(song)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
playlist.errors.each do |error|
|
25
|
+
puts error.red
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.process(link)
|
30
|
+
begin
|
31
|
+
new(link).process
|
32
|
+
rescue => e
|
33
|
+
puts e.message.red
|
34
|
+
puts 'Errors! Can not continue!'.red
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def download(data_link)
|
41
|
+
RMD::Downloader.download(data_link)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RMD
|
2
|
+
class SongPlaylistAdapter
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
attr_reader :song
|
6
|
+
def_delegators :song, :fetch, :success?
|
7
|
+
|
8
|
+
def initialize(song)
|
9
|
+
@song = song
|
10
|
+
end
|
11
|
+
|
12
|
+
def songs
|
13
|
+
[song.data_link].compact
|
14
|
+
end
|
15
|
+
|
16
|
+
def errors
|
17
|
+
[song.errors].compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/rmd/version.rb
ADDED
data/rmd.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rmd/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rmd"
|
8
|
+
spec.version = Rmd::VERSION
|
9
|
+
spec.authors = ["Hieu Nguyen"]
|
10
|
+
spec.email = ["hieuk09@gmail.com"]
|
11
|
+
spec.summary = %q{RMD: Ruby Music Dowloader}
|
12
|
+
spec.description = %q{RMD: a music downloader written in pure ruby.}
|
13
|
+
spec.homepage = "https://github.com/hieuk09/rmd"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = 'rmd'
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'thor', '~> 0.19'
|
22
|
+
spec.add_dependency 'mechanize', '~> 2.7'
|
23
|
+
spec.add_dependency 'ruby-progressbar', '~> 1.7'
|
24
|
+
spec.add_dependency 'colorize', '~> 0.7'
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rspec", '~> 3.1'
|
28
|
+
spec.add_development_dependency "webmock", '~> 1.20'
|
29
|
+
spec.add_development_dependency "vcr", '~> 2.9'
|
30
|
+
spec.add_development_dependency "byebug", '~> 3.5'
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RMD::Downloader do
|
4
|
+
describe '.download' do
|
5
|
+
let(:link) { 'www.example/xyz/abc.mp3' }
|
6
|
+
let(:downloader) { instance_double('RMD::Downloader') }
|
7
|
+
|
8
|
+
it 'downloads' do
|
9
|
+
expect(described_class).to receive(:new).with(link).and_return(downloader)
|
10
|
+
expect(downloader).to receive(:download)
|
11
|
+
described_class.download(link)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#download' do
|
16
|
+
let(:downloader) { described_class.new(link) }
|
17
|
+
let(:link) { 'http://db.gamefaqs.com/portable/3ds/file/fire_emblem_awakening_shop.txt' }
|
18
|
+
let(:file_name) { 'fire_emblem_awakening_shop.txt' }
|
19
|
+
let(:content_length) { body.length }
|
20
|
+
let(:file_path) { File.expand_path("../../../#{file_name}", __FILE__) }
|
21
|
+
let(:result) { File.read(file_path) }
|
22
|
+
|
23
|
+
it 'downloads' do
|
24
|
+
VCR.use_cassette("song download") do
|
25
|
+
expect {
|
26
|
+
downloader.download
|
27
|
+
}.to output("\e[0;32;49m#{file_name}\e[0m\n").to_stdout
|
28
|
+
expect(File.exists?(file_path)).to eq true
|
29
|
+
end
|
30
|
+
File.delete(file_path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RMD::Factory do
|
4
|
+
describe '.build' do
|
5
|
+
let(:factory) { double('RMD::Factory') }
|
6
|
+
let(:link) { 'link' }
|
7
|
+
|
8
|
+
it 'builds adapter' do
|
9
|
+
expect(described_class).to receive(:new).with(link).and_return(factory)
|
10
|
+
expect(factory).to receive(:build)
|
11
|
+
described_class.build(link)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#build' do
|
16
|
+
let(:factory) { described_class.new(link) }
|
17
|
+
subject { factory.build }
|
18
|
+
|
19
|
+
context 'when link belongs to nhaccuatui website' do
|
20
|
+
context 'when link is song link' do
|
21
|
+
let(:link) { 'www.nhaccuatui/bai-hat/song.mp3' }
|
22
|
+
it { is_expected.to be_a(RMD::SongPlaylistAdapter) }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when link is playlist link' do
|
26
|
+
let(:link) { 'www.nhaccuatui/playlist/bang-kieu' }
|
27
|
+
it { is_expected.to be_a(RMD::NCT::Playlist) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RMD::NCT::Getter::KeyFromPage do
|
4
|
+
let(:url) { 'url' }
|
5
|
+
let(:getter) { described_class.new(url) }
|
6
|
+
|
7
|
+
describe '#fetch' do
|
8
|
+
let(:element) { double('Element', text: data_link) }
|
9
|
+
let(:data_link) { 'data_link' }
|
10
|
+
let(:key) { 'key' }
|
11
|
+
let(:errors) { 'The page does not contain the key.' }
|
12
|
+
|
13
|
+
before do
|
14
|
+
allow(getter).to receive(:key).and_return(key)
|
15
|
+
allow(getter).to receive(:element).and_return(element)
|
16
|
+
getter.fetch
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when key is not present' do
|
20
|
+
let(:key) { nil }
|
21
|
+
|
22
|
+
it 'returns errors' do
|
23
|
+
expect(getter.data_link).to eq nil
|
24
|
+
expect(getter.errors).to eq errors
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when element is not present' do
|
29
|
+
let(:element) { nil }
|
30
|
+
|
31
|
+
it 'returns errors' do
|
32
|
+
expect(getter.data_link).to eq nil
|
33
|
+
expect(getter.errors).to eq errors
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'otherwise' do
|
38
|
+
it 'gets data' do
|
39
|
+
expect(getter.data_link).to eq data_link
|
40
|
+
expect(getter.errors).to eq nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#response' do
|
46
|
+
let(:body) { '<body>abc</body>' }
|
47
|
+
let(:new_page) { double('Page', body: body) }
|
48
|
+
let(:response) { getter.send(:response).child }
|
49
|
+
|
50
|
+
before do
|
51
|
+
expect(getter).to receive(:new_page).and_return(new_page)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'parse the response from the page' do
|
55
|
+
expect(response.name).to eq 'body'
|
56
|
+
expect(response.text).to eq 'abc'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#key' do
|
61
|
+
let(:page) { double('Page') }
|
62
|
+
let(:elements) { [element] }
|
63
|
+
let(:element) { double('Element', text: text) }
|
64
|
+
subject { getter.send(:key) }
|
65
|
+
|
66
|
+
before do
|
67
|
+
expect(getter).to receive(:page).and_return(page)
|
68
|
+
expect(page).to receive(:search).with('script').and_return(elements)
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when match data exists' do
|
72
|
+
let(:text) { 'NCTNowPlaying.intFlashPlayer("flashPlayer", "song", "key");' }
|
73
|
+
it { is_expected.to eq 'key' }
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when match data is not exists' do
|
77
|
+
let(:text) { 'NCTNowPlaying.initFlashPlayer();' }
|
78
|
+
it { is_expected.to eq nil }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#element' do
|
83
|
+
let(:response) { Nokogiri::XML(xml) }
|
84
|
+
|
85
|
+
before do
|
86
|
+
expect(getter).to receive(:response).and_return(response)
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when the path exists' do
|
90
|
+
let(:xml) { '<tracklist><location>abc</location></tracklist>' }
|
91
|
+
let(:subject) { getter.send(:element).text }
|
92
|
+
it { is_expected.to eq 'abc' }
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'when the path is not exists' do
|
96
|
+
let(:xml) { '<location>abc</location>' }
|
97
|
+
subject { getter.send(:element) }
|
98
|
+
it { is_expected.to eq nil }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#new_link' do
|
103
|
+
let(:key) { 'key' }
|
104
|
+
subject { getter.send(:new_link) }
|
105
|
+
|
106
|
+
before do
|
107
|
+
expect(getter).to receive(:key).and_return(key)
|
108
|
+
end
|
109
|
+
|
110
|
+
it { is_expected.to eq 'http://www.nhaccuatui.com/flash/xml?key1=key' }
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#page' do
|
114
|
+
let(:agent) { instance_double('Mechanize') }
|
115
|
+
let(:page) { double('Page') }
|
116
|
+
subject { getter.send(:page) }
|
117
|
+
|
118
|
+
before do
|
119
|
+
expect(getter).to receive(:agent).and_return(agent)
|
120
|
+
expect(agent).to receive(:get).with(url).and_return(page)
|
121
|
+
end
|
122
|
+
|
123
|
+
it { is_expected.to eq page }
|
124
|
+
end
|
125
|
+
|
126
|
+
describe '#new_page' do
|
127
|
+
let(:new_link) { 'new_link' }
|
128
|
+
let(:agent) { instance_double('Mechanize') }
|
129
|
+
let(:page) { double('Page') }
|
130
|
+
subject { getter.send(:new_page) }
|
131
|
+
|
132
|
+
before do
|
133
|
+
expect(getter).to receive(:new_link).and_return(new_link)
|
134
|
+
expect(getter).to receive(:agent).and_return(agent)
|
135
|
+
expect(agent).to receive(:get).with(new_link).and_return(page)
|
136
|
+
end
|
137
|
+
|
138
|
+
it { is_expected.to eq page }
|
139
|
+
end
|
140
|
+
end
|