notu 3.0.0 → 4.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: 1e13444a530e59b3049d159ff447ba19c9525be2a861563e47dbe2fed586d728
4
- data.tar.gz: b982c666ad9eac6e8fe23be668f7e3270fe528c0f1511fb0db63ebb46ddd0028
3
+ metadata.gz: efbddd2bb568278da60c542fd1046d39a61d200bf1ca1a20614e7135ac07e66f
4
+ data.tar.gz: 4c14831c8b1526a402cf47870e64d17fb5e3ffa4b04d0f2ecaefb30d493de4e3
5
5
  SHA512:
6
- metadata.gz: 1864ecdf4fdccd1629f11c6f789bb354736512c026a55825544087474e4ef0b4c5e73aae863efbed1c8a9cfd84bb06cf9538388068049ed324d36ed4f753c834
7
- data.tar.gz: 6c3f3c4041d49547898cad9ef090e2d54711cac2b304ad9e30d829917709302ab6b09aef9272fa4662a6edf7615189d88aca0cc4470bf5f10dd7d394a80e4f17
6
+ metadata.gz: 60c8869371be7e6b80a4b8f8b26ebfa2406f63cb576d7788cf6aeacb6d963c3f4d534455a5ae33bca7ea0a33236a0bb6ab529980fc3697ba302062e764f96d0c
7
+ data.tar.gz: 8343035fb3d9a5ce7e9bba05676f13b0265ace6f514e2e492aa5280eeae0ddc25990eb553adf39f698f66767f4da2d5a5d55523e6b04d2fef428cb47129ece3b
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.0
1
+ 4.0.0
data/lib/notu/api.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Notu
2
+
3
+ class Api
4
+
5
+ DEFAULT_KEY = '91f5d6a201de58e0c0a0d858573dddf0'.freeze
6
+ FORMAT = 'json'.freeze
7
+ HOST = 'ws.audioscrobbler.com'.freeze
8
+ VERSION = '2.0'.freeze
9
+
10
+ attr_reader :key
11
+
12
+ def initialize(key: DEFAULT_KEY)
13
+ @key = key.try(:squish).presence || raise(Error.new('API key must be specified'))
14
+ end
15
+
16
+ def url(params = {})
17
+ params = (params || {}).symbolize_keys
18
+ params.merge!(api_key: key, format: FORMAT)
19
+ query_string = params.map { |name, value| "#{CGI.escape(name.to_s)}=#{CGI.escape(value.to_s)}" }.join('&')
20
+ "https://#{HOST}/#{VERSION}?#{query_string}"
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,11 @@
1
+ module Notu
2
+
3
+ module JsonDocument
4
+
5
+ def self.get(url, options = {})
6
+ JSON.parse(HttpDownload.get(url, options))
7
+ end
8
+
9
+ end
10
+
11
+ end
data/lib/notu/library.rb CHANGED
@@ -2,14 +2,11 @@ module Notu
2
2
 
3
3
  class Library
4
4
 
5
- HOST = 'www.last.fm'.freeze
5
+ attr_reader :api, :username
6
6
 
7
- attr_reader :username
8
-
9
- def initialize(options = {})
10
- @semaphore = Mutex.new
11
- options = options.symbolize_keys
12
- self.username = options[:username]
7
+ def initialize(username:, api: Api.new)
8
+ @api = api.presence || raise(Error.new('API must be specified'))
9
+ @username = username.try(:squish).presence || raise(Error.new('Username must be specified'))
13
10
  end
14
11
 
15
12
  def loved_tracks
@@ -20,38 +17,12 @@ module Notu
20
17
  MostPlayedTracks.new(self, options)
21
18
  end
22
19
 
23
- def played_tracks
24
- PlayedTracks.new(self)
25
- end
26
-
27
- def url(options = {})
28
- options = options.symbolize_keys
29
- path = options[:path].presence
30
- query = options[:query].presence
31
- query = options[:query].map { |name, value| "#{CGI.escape(name.to_s)}=#{CGI.escape(value.to_s)}" }.join('&') if options[:query].is_a?(Hash)
32
- "https://#{HOST}/user/#{username}".tap do |url|
33
- if path.present?
34
- url << '/' unless path.starts_with?('/')
35
- url << path
36
- end
37
- if query.present?
38
- url << '?' << query
39
- end
40
- end
20
+ def recent_tracks
21
+ RecentTracks.new(self)
41
22
  end
42
23
 
43
- private
44
-
45
- def username=(value)
46
- @semaphore.synchronize do
47
- @username = value.to_s.strip.downcase
48
- raise UnknownUsernameError.new(value) if username !~ /^[a-z0-9_]+$/
49
- begin
50
- HtmlDocument.get(url)
51
- rescue
52
- raise UnknownUsernameError.new(value)
53
- end
54
- end
24
+ def url(params = {})
25
+ api.url((params || {}).symbolize_keys.merge(user: username))
55
26
  end
56
27
 
57
28
  end
@@ -2,25 +2,33 @@ module Notu
2
2
 
3
3
  class LovedTracks
4
4
 
5
- include Listing
5
+ include Enumerable
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
6
13
 
7
14
  def each
8
15
  return unless block_given?
9
- page_urls.each do |url|
10
- document = HtmlDocument.get(url)
11
- (document / 'table.chartlist tbody tr').each do |element|
12
- artist = (element / 'td.chartlist-artist a').first.try(:text) || next
13
- title = (element / 'td.chartlist-name a').first.try(:text) || next
16
+ pages_count = nil
17
+ page = 1
18
+ loop do
19
+ json = JsonDocument.get(library.url(limit: 50, method: 'user.getLovedTracks', page:))
20
+ pages_count = json['lovedtracks']['@attr']['totalPages'].to_i
21
+ json['lovedtracks']['track'].each do |track_json|
22
+ artist = track_json['artist']['name'] || next
23
+ title = track_json['name'] || next
14
24
  yield(Track.new(artist:, title:))
15
25
  end
26
+ page += 1
27
+ break if page > pages_count
16
28
  end
17
29
  nil
18
30
  end
19
31
 
20
- def path
21
- 'loved'
22
- end
23
-
24
32
  end
25
33
 
26
34
  end
@@ -2,51 +2,44 @@ module Notu
2
2
 
3
3
  class MostPlayedTracks
4
4
 
5
- include Listing
5
+ include Enumerable
6
6
 
7
- PERIODS = {
8
- '7 days' => 'LAST_7_DAYS',
9
- '30 days' => 'LAST_30_DAYS',
10
- '90 days' => 'LAST_90_DAYS',
11
- '365 days' => 'LAST_365_DAYS',
12
- 'Overall' => '',
13
- }.freeze
7
+ PERIODS = %w(overall 7day 1month 3month 6month 12month).freeze
14
8
 
15
- attr_reader :period
9
+ attr_reader :library, :period
16
10
 
17
11
  def initialize(library, options = {})
18
- super(library)
19
- options = options.stringify_keys.reverse_merge('period' => PERIODS.keys.first)
20
- self.period = options['period']
12
+ raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
13
+ @library = library
14
+ options = options.symbolize_keys.reverse_merge(period: PERIODS.first)
15
+ self.period = options[:period]
21
16
  end
22
17
 
23
18
  def each
24
19
  return unless block_given?
25
- page_urls.each do |url|
26
- document = HtmlDocument.get(url)
27
- (document / 'table.chartlist tbody tr').each do |element|
28
- artist = (element / 'td.chartlist-artist a').first.try(:text) || next
29
- title = (element / 'td.chartlist-name a').first.try(:text) || next
30
- plays_count = (element / 'td.chartlist-bar .chartlist-count-bar-value').text.gsub(/[^\d]/, '').presence || next
20
+ pages_count = nil
21
+ page = 1
22
+ loop do
23
+ json = JsonDocument.get(library.url(limit: 50, method: 'user.getTopTracks', page:))
24
+ pages_count = json['toptracks']['@attr']['totalPages'].to_i
25
+ json['toptracks']['track'].each do |track_json|
26
+ artist = track_json['artist']['name'] || next
27
+ title = track_json['name'] || next
28
+ plays_count = track_json['playcount'] || next
31
29
  yield(Track.new(artist:, plays_count:, title:))
32
30
  end
31
+ page += 1
32
+ break if page > pages_count
33
33
  end
34
34
  nil
35
35
  end
36
36
 
37
- def params
38
- { 'date_preset' => PERIODS[period] }
39
- end
40
-
41
- def path
42
- 'library/tracks'
43
- end
44
-
45
37
  private
46
38
 
47
39
  def period=(value)
48
- raise ArgumentError.new("Notu::MostPlayedTracks#period is invalid: #{value.inspect}") unless PERIODS.key?(value.to_s)
49
- @period = value.to_s
40
+ string_value = value.to_s
41
+ raise ArgumentError.new("Notu::MostPlayedTracks#period is invalid: #{value.inspect}") unless PERIODS.include?(string_value)
42
+ @period = string_value
50
43
  end
51
44
 
52
45
  end
@@ -0,0 +1,34 @@
1
+ module Notu
2
+
3
+ class RecentTracks
4
+
5
+ include Enumerable
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
+ def each
15
+ return unless block_given?
16
+ pages_count = nil
17
+ page = 1
18
+ loop do
19
+ json = JsonDocument.get(library.url(limit: 50, method: 'user.getRecentTracks', page:))
20
+ pages_count = json['recenttracks']['@attr']['totalPages'].to_i
21
+ json['recenttracks']['track'].each do |track_json|
22
+ artist = track_json['artist']['#text'] || next
23
+ title = track_json['name'] || next
24
+ yield(Track.new(artist:, title:))
25
+ end
26
+ page += 1
27
+ break if page > pages_count
28
+ end
29
+ nil
30
+ end
31
+
32
+ end
33
+
34
+ end
data/lib/notu.rb CHANGED
@@ -3,7 +3,6 @@ require 'active_support/core_ext'
3
3
  require 'byebug' if ENV['DEBUGGER']
4
4
  require 'cgi'
5
5
  require 'net/https'
6
- require 'nokogiri'
7
6
 
8
7
  lib_path = "#{__dir__}/notu"
9
8
 
@@ -18,12 +17,11 @@ require "#{lib_path}/error"
18
17
  require "#{lib_path}/network_error"
19
18
  require "#{lib_path}/parse_error"
20
19
 
21
- require "#{lib_path}/html_document"
20
+ require "#{lib_path}/api"
22
21
  require "#{lib_path}/http_download"
22
+ require "#{lib_path}/json_document"
23
23
  require "#{lib_path}/library"
24
- require "#{lib_path}/listing"
25
24
  require "#{lib_path}/loved_tracks"
26
25
  require "#{lib_path}/most_played_tracks"
27
- require "#{lib_path}/played_tracks"
26
+ require "#{lib_path}/recent_tracks"
28
27
  require "#{lib_path}/track"
29
- require "#{lib_path}/unknown_username_error"
data/notu.gemspec CHANGED
@@ -16,7 +16,6 @@ Gem::Specification.new do |s|
16
16
  s.required_ruby_version = '>= 3.1.0'
17
17
 
18
18
  s.add_dependency 'activesupport', '>= 7.0.0', '< 8.0.0'
19
- s.add_dependency 'nokogiri', '>= 1.6.0', '< 1.14.0'
20
19
 
21
20
  s.add_development_dependency 'byebug', '>= 3.2.0', '< 12.0.0'
22
21
  s.add_development_dependency 'rake', '>= 10.3.0', '< 14.0.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notu
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.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: 2022-02-06 00:00:00.000000000 Z
11
+ date: 2023-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,26 +30,6 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 8.0.0
33
- - !ruby/object:Gem::Dependency
34
- name: nokogiri
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: 1.6.0
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: 1.14.0
43
- type: :runtime
44
- prerelease: false
45
- version_requirements: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 1.6.0
50
- - - "<"
51
- - !ruby/object:Gem::Version
52
- version: 1.14.0
53
33
  - !ruby/object:Gem::Dependency
54
34
  name: byebug
55
35
  requirement: !ruby/object:Gem::Requirement
@@ -220,18 +200,17 @@ files:
220
200
  - README.mdown
221
201
  - VERSION
222
202
  - lib/notu.rb
203
+ - lib/notu/api.rb
223
204
  - lib/notu/error.rb
224
- - lib/notu/html_document.rb
225
205
  - lib/notu/http_download.rb
206
+ - lib/notu/json_document.rb
226
207
  - lib/notu/library.rb
227
- - lib/notu/listing.rb
228
208
  - lib/notu/loved_tracks.rb
229
209
  - lib/notu/most_played_tracks.rb
230
210
  - lib/notu/network_error.rb
231
211
  - lib/notu/parse_error.rb
232
- - lib/notu/played_tracks.rb
212
+ - lib/notu/recent_tracks.rb
233
213
  - lib/notu/track.rb
234
- - lib/notu/unknown_username_error.rb
235
214
  - notu.gemspec
236
215
  homepage: https://github.com/alexistoulotte/notu
237
216
  licenses:
@@ -1,19 +0,0 @@
1
- module Notu
2
-
3
- module HtmlDocument
4
-
5
- def self.get(url, options = {})
6
- parse(HttpDownload.get(url, options))
7
- end
8
-
9
- def self.parse(data)
10
- data = data.gsub(/&nbsp;/i, ' ').gsub(/\s+/, ' ')
11
- document = Nokogiri::HTML.parse(data, nil, 'UTF-8')
12
- raise ParseError.new('Invalid HTML document') if (document / 'head').empty?
13
- document
14
- end
15
- private_class_method :parse
16
-
17
- end
18
-
19
- end
data/lib/notu/listing.rb DELETED
@@ -1,32 +0,0 @@
1
- module Notu
2
-
3
- module Listing
4
-
5
- include Enumerable
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
- def page_urls
15
- (1..pages_count).map do |index|
16
- library.url(path:, query: params.merge('page' => index))
17
- end
18
- end
19
-
20
- def pages_count
21
- document = HtmlDocument.get(library.url(path:, query: params))
22
- [1, (document / 'ul.pagination-list li.pagination-page').text.split(/\s+/).map(&:to_i)].flatten.compact.max
23
- end
24
-
25
- def params
26
- # to be overriden
27
- {}
28
- end
29
-
30
- end
31
-
32
- end
@@ -1,26 +0,0 @@
1
- module Notu
2
-
3
- class PlayedTracks
4
-
5
- include Listing
6
-
7
- def each
8
- return unless block_given?
9
- page_urls.each do |url|
10
- document = HtmlDocument.get(url)
11
- (document / 'table.chartlist tbody tr').each do |element|
12
- artist = (element / 'td.chartlist-artist a').first.try(:text) || next
13
- title = (element / 'td.chartlist-name a').first.try(:text) || next
14
- yield(Track.new(artist:, title:))
15
- end
16
- end
17
- nil
18
- end
19
-
20
- def path
21
- 'library'
22
- end
23
-
24
- end
25
-
26
- end
@@ -1,14 +0,0 @@
1
- module Notu
2
-
3
- class UnknownUsernameError < Error
4
-
5
- attr_reader :username
6
-
7
- def initialize(username)
8
- @username = username
9
- super("No such Last.fm username: #{self.username.inspect}")
10
- end
11
-
12
- end
13
-
14
- end