notu 3.0.0 → 5.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: 43f1b5f5623c11b9b3ecf2bb19a64a65681ff6b4414a06eb26967d4655095d27
4
+ data.tar.gz: 4b581b8a75c6211194c3d0564edbd11c88b83fe66623fc1890fe3af7b54e148d
5
5
  SHA512:
6
- metadata.gz: 1864ecdf4fdccd1629f11c6f789bb354736512c026a55825544087474e4ef0b4c5e73aae863efbed1c8a9cfd84bb06cf9538388068049ed324d36ed4f753c834
7
- data.tar.gz: 6c3f3c4041d49547898cad9ef090e2d54711cac2b304ad9e30d829917709302ab6b09aef9272fa4662a6edf7615189d88aca0cc4470bf5f10dd7d394a80e4f17
6
+ metadata.gz: e6fae0ce54a0bce085f50531e859e9438a90745884271222600559b90dafcd39a5d0cbf270d80f959703f6108d268a1fe314f96d788a40ad818d26984f5c5c98
7
+ data.tar.gz: f2c0d4527a7eb783dcf728fa490d6a89669257ff3b628b9287ba54fd135f6ecf293a6fc2f5ab69e06314d176cd561586ead4938c057e087dbe3edd9e387b7aa4
data/README.mdown CHANGED
@@ -1,6 +1,6 @@
1
1
  # Notu
2
2
 
3
- API to get Last.fm tracks (most played, loved, etc.).
3
+ API to get Last.fm tracks (top, loved, etc.).
4
4
 
5
5
  ## Installation
6
6
 
@@ -15,17 +15,17 @@ Then, just run `bundle install`.
15
15
  ## Example
16
16
 
17
17
  ```ruby
18
- library = Notu::Library.new(username: 'johndoe')
18
+ user_api = Notu::UserApi.new(username: 'johndoe')
19
19
 
20
- library.loved_tracks.each do |track|
20
+ user_api.loved_tracks.each do |track|
21
21
  puts track.artist
22
22
  end
23
23
 
24
- library.most_played_tracks(period: 'last_month').each do |track|
24
+ user_api.top_tracks(period: '3month').each do |track|
25
25
  puts "#{track.artist}: #{track.plays_count}"
26
26
  end
27
27
 
28
- library.played_tracks.each do |track|
28
+ user_api.recent_tracks.each do |track|
29
29
  puts track.title
30
30
  end
31
31
  ```
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.0
1
+ 5.0.0
data/lib/notu/api.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Notu
2
+
3
+ class Api
4
+
5
+ DEFAULT_API_KEY = '91f5d6a201de58e0c0a0d858573dddf0'.freeze
6
+ FORMAT = 'json'.freeze
7
+ HOST = 'ws.audioscrobbler.com'.freeze
8
+ VERSION = '2.0'.freeze
9
+
10
+ attr_reader :api_key
11
+
12
+ def initialize(api_key: DEFAULT_API_KEY)
13
+ @api_key = api_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:, 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
@@ -2,25 +2,33 @@ module Notu
2
2
 
3
3
  class LovedTracks
4
4
 
5
- include Listing
5
+ include Enumerable
6
+
7
+ attr_reader :user_api
8
+
9
+ def initialize(user_api)
10
+ raise ArgumentError.new("#{self.class}#user_api must be specified") unless user_api
11
+ @user_api = user_api
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(user_api.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
@@ -0,0 +1,34 @@
1
+ module Notu
2
+
3
+ class RecentTracks
4
+
5
+ include Enumerable
6
+
7
+ attr_reader :user_api
8
+
9
+ def initialize(user_api)
10
+ raise ArgumentError.new("#{self.class}#user_api must be specified") unless user_api
11
+ @user_api = user_api
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(user_api.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
@@ -0,0 +1,47 @@
1
+ module Notu
2
+
3
+ class TopTracks
4
+
5
+ include Enumerable
6
+
7
+ PERIODS = %w(overall 7day 1month 3month 6month 12month).freeze
8
+
9
+ attr_reader :period, :user_api
10
+
11
+ def initialize(user_api, options = {})
12
+ raise ArgumentError.new("#{self.class}#user_api must be specified") unless user_api
13
+ @user_api = user_api
14
+ options = options.symbolize_keys.reverse_merge(period: PERIODS.first)
15
+ self.period = options[:period]
16
+ end
17
+
18
+ def each
19
+ return unless block_given?
20
+ pages_count = nil
21
+ page = 1
22
+ loop do
23
+ json = JsonDocument.get(user_api.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
29
+ yield(Track.new(artist:, plays_count:, title:))
30
+ end
31
+ page += 1
32
+ break if page > pages_count
33
+ end
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def period=(value)
40
+ string_value = value.to_s
41
+ raise ArgumentError.new("#{self.class.name}#period is invalid: #{value.inspect}") unless PERIODS.include?(string_value)
42
+ @period = string_value
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,30 @@
1
+ module Notu
2
+
3
+ class UserApi < Api
4
+
5
+ attr_reader :username
6
+
7
+ def initialize(username:, api_key: DEFAULT_API_KEY)
8
+ super(api_key:)
9
+ @username = username.try(:squish).presence || raise(Error.new('Username must be specified'))
10
+ end
11
+
12
+ def loved_tracks
13
+ LovedTracks.new(self)
14
+ end
15
+
16
+ def recent_tracks
17
+ RecentTracks.new(self)
18
+ end
19
+
20
+ def top_tracks(options = {})
21
+ TopTracks.new(self, options)
22
+ end
23
+
24
+ def url(params = {})
25
+ super((params || {}).symbolize_keys.merge(user: username))
26
+ end
27
+
28
+ end
29
+
30
+ 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"
23
- require "#{lib_path}/library"
24
- require "#{lib_path}/listing"
22
+ require "#{lib_path}/json_document"
25
23
  require "#{lib_path}/loved_tracks"
26
- require "#{lib_path}/most_played_tracks"
27
- require "#{lib_path}/played_tracks"
24
+ require "#{lib_path}/recent_tracks"
25
+ require "#{lib_path}/top_tracks"
28
26
  require "#{lib_path}/track"
29
- require "#{lib_path}/unknown_username_error"
27
+ require "#{lib_path}/user_api"
data/notu.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.email = 'al@alweb.org'
7
7
  s.homepage = 'https://github.com/alexistoulotte/notu'
8
8
  s.summary = 'API for Last.fm'
9
- s.description = 'API to get Last.fm tracks (most played, loved, etc.)'
9
+ s.description = 'API to get Last.fm tracks (top, loved, etc.)'
10
10
  s.license = 'MIT'
11
11
 
12
12
  s.files = %x(git ls-files | grep -vE '^(spec/|test/|\\.|Gemfile|Rakefile)').split("\n")
@@ -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: 5.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
@@ -210,7 +190,7 @@ dependencies:
210
190
  - - "<"
211
191
  - !ruby/object:Gem::Version
212
192
  version: 4.0.0
213
- description: API to get Last.fm tracks (most played, loved, etc.)
193
+ description: API to get Last.fm tracks (top, loved, etc.)
214
194
  email: al@alweb.org
215
195
  executables: []
216
196
  extensions: []
@@ -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
226
- - lib/notu/library.rb
227
- - lib/notu/listing.rb
206
+ - lib/notu/json_document.rb
228
207
  - lib/notu/loved_tracks.rb
229
- - lib/notu/most_played_tracks.rb
230
208
  - lib/notu/network_error.rb
231
209
  - lib/notu/parse_error.rb
232
- - lib/notu/played_tracks.rb
210
+ - lib/notu/recent_tracks.rb
211
+ - lib/notu/top_tracks.rb
233
212
  - lib/notu/track.rb
234
- - lib/notu/unknown_username_error.rb
213
+ - lib/notu/user_api.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/library.rb DELETED
@@ -1,59 +0,0 @@
1
- module Notu
2
-
3
- class Library
4
-
5
- HOST = 'www.last.fm'.freeze
6
-
7
- attr_reader :username
8
-
9
- def initialize(options = {})
10
- @semaphore = Mutex.new
11
- options = options.symbolize_keys
12
- self.username = options[:username]
13
- end
14
-
15
- def loved_tracks
16
- LovedTracks.new(self)
17
- end
18
-
19
- def most_played_tracks(options = {})
20
- MostPlayedTracks.new(self, options)
21
- end
22
-
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
41
- end
42
-
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
55
- end
56
-
57
- end
58
-
59
- 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,54 +0,0 @@
1
- module Notu
2
-
3
- class MostPlayedTracks
4
-
5
- include Listing
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
14
-
15
- attr_reader :period
16
-
17
- def initialize(library, options = {})
18
- super(library)
19
- options = options.stringify_keys.reverse_merge('period' => PERIODS.keys.first)
20
- self.period = options['period']
21
- end
22
-
23
- def each
24
- 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
31
- yield(Track.new(artist:, plays_count:, title:))
32
- end
33
- end
34
- nil
35
- end
36
-
37
- def params
38
- { 'date_preset' => PERIODS[period] }
39
- end
40
-
41
- def path
42
- 'library/tracks'
43
- end
44
-
45
- private
46
-
47
- 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
50
- end
51
-
52
- end
53
-
54
- 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