notu 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 +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/lib/notu.rb +18 -0
- data/lib/notu/error.rb +15 -0
- data/lib/notu/html_document.rb +20 -0
- data/lib/notu/http_download.rb +39 -0
- data/lib/notu/library.rb +52 -0
- data/lib/notu/loved_tracks.rb +40 -0
- data/lib/notu/most_played_tracks.rb +46 -0
- data/lib/notu/network_error.rb +6 -0
- data/lib/notu/parse_error.rb +6 -0
- data/lib/notu/track.rb +30 -0
- data/notu.gemspec +27 -0
- data/spec/cassettes/Notu_HtmlDocument/_get/follows_redirects.yml +178 -0
- data/spec/cassettes/Notu_HtmlDocument/_get/raise_a_NetworkError_on_404.yml +80 -0
- data/spec/cassettes/Notu_HtmlDocument/_get/raise_a_ParseError_if_not_a_valid_document.yml +369 -0
- data/spec/cassettes/Notu_HtmlDocument/_get/returns_document_parsed.yml +134 -0
- data/spec/cassettes/Notu_HttpDownload/_get/accepts_HTTPS_URL.yml +134 -0
- data/spec/cassettes/Notu_HttpDownload/_get/follow_redirects.yml +178 -0
- data/spec/cassettes/Notu_HttpDownload/_get/raise_a_NetworkError_if_too_many_redirects.yml +47 -0
- data/spec/cassettes/Notu_HttpDownload/_get/raise_a_NetworkError_on_404.yml +80 -0
- data/spec/cassettes/Notu_HttpDownload/_get/retrives_document_from_given_URL.yml +134 -0
- data/spec/cassettes/Notu_LovedTracks/_each/returns_nil.yml +2568 -0
- data/spec/cassettes/Notu_LovedTracks/_each/returns_some_tracks.yml +5113 -0
- data/spec/cassettes/Notu_LovedTracks/_page_urls/is_correct.yml +5088 -0
- data/spec/cassettes/Notu_LovedTracks/_pages_count/is_correct.yml +2546 -0
- data/spec/cassettes/Notu_MostPlayedTracks/_each/returns_nil.yml +6532 -0
- data/spec/cassettes/Notu_MostPlayedTracks/_each/returns_some_tracks.yml +13455 -0
- data/spec/notu/error_spec.rb +40 -0
- data/spec/notu/html_document_spec.rb +31 -0
- data/spec/notu/http_download_spec.rb +39 -0
- data/spec/notu/library_spec.rb +130 -0
- data/spec/notu/loved_tracks_spec.rb +61 -0
- data/spec/notu/most_played_tracks_spec.rb +64 -0
- data/spec/notu/network_error_spec.rb +15 -0
- data/spec/notu/parse_error_spec.rb +15 -0
- data/spec/notu/track_spec.rb +67 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/vcr.rb +8 -0
- metadata +252 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e624e1e0724b52253ee45953acadf9f0614dcbb3
|
4
|
+
data.tar.gz: 7540301e160396bf7630cb8398ea29f5a7959917
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ac2cef63ae77bd529389076774f156855ede52dd5d2bdd4e85ffd7a8bfce1a7b6ec0319b14cc3f18cbde4a7f346fe43b1682813bcefeacf4ce3a4e3767544051
|
7
|
+
data.tar.gz: 2d272e01ccfcecfafabc99b09c17155b1861346cc60004ed028ee3efdc8734f109050c16def7540d9ff8be07c61968f0a23dfbe88befe5e441cbe731e9aa00a8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Alexis Toulotte
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/notu.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
require 'byebug' if ENV['DEBUGGER']
|
3
|
+
require 'cgi'
|
4
|
+
require 'net/https'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
lib_path = "#{__dir__}/notu"
|
8
|
+
|
9
|
+
require "#{lib_path}/error"
|
10
|
+
require "#{lib_path}/network_error"
|
11
|
+
require "#{lib_path}/parse_error"
|
12
|
+
|
13
|
+
require "#{lib_path}/html_document"
|
14
|
+
require "#{lib_path}/http_download"
|
15
|
+
require "#{lib_path}/library"
|
16
|
+
require "#{lib_path}/loved_tracks"
|
17
|
+
require "#{lib_path}/most_played_tracks"
|
18
|
+
require "#{lib_path}/track"
|
data/lib/notu/error.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
class Error < StandardError
|
4
|
+
|
5
|
+
attr_reader :original
|
6
|
+
|
7
|
+
def initialize(message)
|
8
|
+
@original = message.is_a?(Exception) ? message : nil
|
9
|
+
message = original.message if original.is_a?(Exception)
|
10
|
+
super(message.to_s.squish.presence)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
module HtmlDocument
|
4
|
+
|
5
|
+
def self.get(url, options = {})
|
6
|
+
parse(HttpDownload.get(url, options))
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def self.parse(data)
|
12
|
+
data = data.gsub(/ /i, ' ').gsub(/\s+/, ' ')
|
13
|
+
document = Nokogiri::HTML.parse(data, nil, 'UTF-8')
|
14
|
+
raise ParseError.new('Invalid HTML document') if (document/'head').empty?
|
15
|
+
document
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
module HttpDownload
|
4
|
+
|
5
|
+
def self.get(url, options = {})
|
6
|
+
uri = url.is_a?(URI) ? url : URI.parse(url)
|
7
|
+
raise "Invalid URL: #{url.inspect}" unless uri.is_a?(URI::HTTP)
|
8
|
+
options.reverse_merge!(max_redirects: 10, timeout: 10, max_retries: 3, retry_sleep: 2)
|
9
|
+
connection = Net::HTTP.new(uri.host, uri.port)
|
10
|
+
connection.open_timeout = options[:timeout]
|
11
|
+
connection.read_timeout = options[:timeout]
|
12
|
+
if uri.is_a?(URI::HTTPS)
|
13
|
+
connection.use_ssl = true
|
14
|
+
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
15
|
+
end
|
16
|
+
connection.start do |http|
|
17
|
+
http.request_get(uri.request_uri, (options[:headers] || {}).stringify_keys) do |response|
|
18
|
+
begin
|
19
|
+
response.value # raise if not success
|
20
|
+
rescue Net::HTTPRetriableError
|
21
|
+
raise 'Max redirects has been reached' if options[:max_redirects] < 1
|
22
|
+
options[:max_redirects] -= 1
|
23
|
+
return get(response['Location'], options)
|
24
|
+
end
|
25
|
+
return response.body
|
26
|
+
end
|
27
|
+
end
|
28
|
+
rescue Net::ReadTimeout, TimeoutError, Timeout::Error, Zlib::BufError => exception
|
29
|
+
raise NetworkError.new(exception) if options[:max_retries] < 1
|
30
|
+
options[:max_retries] -= 1
|
31
|
+
sleep(options[:retry_sleep])
|
32
|
+
get(url, options)
|
33
|
+
rescue => exception
|
34
|
+
raise NetworkError.new(exception)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/lib/notu/library.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
class Library
|
4
|
+
|
5
|
+
DEFAULT_HOST = 'www.last.fm'
|
6
|
+
|
7
|
+
attr_reader :host, :username
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
options = options.stringify_keys
|
11
|
+
self.host = options['host']
|
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 url(options = {})
|
24
|
+
options = options.stringify_keys
|
25
|
+
path = options['path'].presence
|
26
|
+
query = options['query'].presence
|
27
|
+
query = options['query'].map { |name, value| "#{CGI.escape(name.to_s)}=#{CGI.escape(value.to_s)}" }.join('&') if options['query'].is_a?(Hash)
|
28
|
+
"http://#{host}/user/#{username}".tap do |url|
|
29
|
+
if path.present?
|
30
|
+
url << '/' unless path.starts_with?('/')
|
31
|
+
url << path
|
32
|
+
end
|
33
|
+
if query.present?
|
34
|
+
url << '?' << query
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def host=(value)
|
42
|
+
@host = value.presence || DEFAULT_HOST
|
43
|
+
end
|
44
|
+
|
45
|
+
def username=(value)
|
46
|
+
@username = value.to_s.strip.downcase
|
47
|
+
raise Error.new("Invalid Last.fm username: #{value.inspect}") if username !~ /^[a-z0-9_]+$/
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
class LovedTracks
|
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(&block)
|
15
|
+
return unless block_given?
|
16
|
+
page_urls.each do |url|
|
17
|
+
document = HtmlDocument.get(url)
|
18
|
+
(document/'#lovedTracks td.subjectCell').each do |element|
|
19
|
+
yield(Track.new(artist: (element/'a').first.text, title: (element/'a').last.text))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def page_urls
|
28
|
+
@loved_pages ||= (1..pages_count).map do |index|
|
29
|
+
library.url(path: 'library/loved', query: { 'sortBy' => 'date', 'sortOrder' => 'desc', 'page' => index })
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pages_count
|
34
|
+
document = HtmlDocument.get(library.url(path: 'library/loved'))
|
35
|
+
[1, (document/'div.whittle-pagination a').map { |link| link.text.to_i }].flatten.max
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
class MostPlayedTracks
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
PERIODS = {
|
8
|
+
'last_week' => 'week',
|
9
|
+
'last_month' => '1month',
|
10
|
+
'last_3_months' => '3month',
|
11
|
+
'last_6_months' => '6month',
|
12
|
+
'last_year' => 'year',
|
13
|
+
'overall' => 'overall',
|
14
|
+
}
|
15
|
+
|
16
|
+
attr_reader :library, :period
|
17
|
+
|
18
|
+
def initialize(library, options = {})
|
19
|
+
raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
|
20
|
+
@library = library
|
21
|
+
options = options.stringify_keys.reverse_merge('period' => PERIODS.keys.first)
|
22
|
+
self.period = options['period']
|
23
|
+
end
|
24
|
+
|
25
|
+
def each(&block)
|
26
|
+
return unless block_given?
|
27
|
+
document = HtmlDocument.get(library.url(path: 'charts', query: { 'rangetype' => PERIODS[period], 'subtype' => 'tracks' }))
|
28
|
+
(document/'table.chart tbody tr').each do |element|
|
29
|
+
artist = (element/'td.subjectCell a').first.text
|
30
|
+
plays_count = (element/'td.chartbarCell a span').text.strip
|
31
|
+
title = (element/'td.subjectCell a').last.text
|
32
|
+
yield(Track.new(artist: artist, plays_count: plays_count, title: title))
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def period=(value)
|
40
|
+
raise ArgumentError.new("Notu::MostPlayedTracks#period is invalid: #{value.inspect}") unless PERIODS.key?(value.to_s)
|
41
|
+
@period = value.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/notu/track.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Notu
|
2
|
+
|
3
|
+
class Track
|
4
|
+
|
5
|
+
attr_reader :artist, :plays_count, :title
|
6
|
+
|
7
|
+
def initialize(attributes = {})
|
8
|
+
attributes = attributes.stringify_keys
|
9
|
+
self.artist = attributes['artist']
|
10
|
+
self.plays_count = attributes['plays_count']
|
11
|
+
self.title = attributes['title']
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def artist=(value)
|
17
|
+
@artist = value.to_s.squish.presence || raise(Error.new("#{self.class}#artist must be specified, #{value.inspect} given"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def plays_count=(value)
|
21
|
+
@plays_count = value.is_a?(Integer) || value.is_a?(String) && value =~ /\A[0-9]+\z/ ? [0, value.to_i].max : nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def title=(value)
|
25
|
+
@title = value.to_s.squish.presence || raise(Error.new("#{self.class}#title must be specified, #{value.inspect} given"))
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/notu.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'notu'
|
3
|
+
s.version = File.read("#{File.dirname(__FILE__)}/VERSION").strip
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.author = 'Alexis Toulotte'
|
6
|
+
s.email = 'al@alweb.org'
|
7
|
+
s.homepage = 'https://github.com/alexistoulotte/notu'
|
8
|
+
s.summary = 'API for Last.fm'
|
9
|
+
s.description = 'API to get Last.fm most played and loved tracks'
|
10
|
+
s.license = 'MIT'
|
11
|
+
|
12
|
+
s.rubyforge_project = 'notu'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
|
19
|
+
s.add_dependency 'activesupport', '>= 4.1.0', '< 4.2.0'
|
20
|
+
s.add_dependency 'nokogiri', '>= 1.6.0', '< 1.7.0'
|
21
|
+
|
22
|
+
s.add_development_dependency 'byebug', '>= 3.2.0', '< 3.3.0'
|
23
|
+
s.add_development_dependency 'rake', '>= 10.3.0', '< 10.4.0'
|
24
|
+
s.add_development_dependency 'rspec', '>= 3.0.0', '< 3.1.0'
|
25
|
+
s.add_development_dependency 'vcr', '>= 2.9.0', '< 2.10.0'
|
26
|
+
s.add_development_dependency 'webmock', '>= 1.18.0', '< 1.19.0'
|
27
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: http://www.alweb.org/
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept-Encoding:
|
11
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
12
|
+
Accept:
|
13
|
+
- "*/*"
|
14
|
+
User-Agent:
|
15
|
+
- Ruby
|
16
|
+
response:
|
17
|
+
status:
|
18
|
+
code: 301
|
19
|
+
message: Moved Permanently
|
20
|
+
headers:
|
21
|
+
Date:
|
22
|
+
- Thu, 28 Aug 2014 22:42:20 GMT
|
23
|
+
Server:
|
24
|
+
- Apache/2.2.16 (Debian)
|
25
|
+
Location:
|
26
|
+
- http://alweb.org/
|
27
|
+
Vary:
|
28
|
+
- Accept-Encoding
|
29
|
+
Content-Length:
|
30
|
+
- '241'
|
31
|
+
Content-Type:
|
32
|
+
- text/html; charset=iso-8859-1
|
33
|
+
body:
|
34
|
+
encoding: UTF-8
|
35
|
+
string: |
|
36
|
+
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
37
|
+
<html><head>
|
38
|
+
<title>301 Moved Permanently</title>
|
39
|
+
</head><body>
|
40
|
+
<h1>Moved Permanently</h1>
|
41
|
+
<p>The document has moved <a href="http://alweb.org/">here</a>.</p>
|
42
|
+
<hr>
|
43
|
+
<address>Apache/2.2.16 (Debian) Server at www.alweb.org Port 80</address>
|
44
|
+
</body></html>
|
45
|
+
http_version:
|
46
|
+
recorded_at: Thu, 28 Aug 2014 22:42:21 GMT
|
47
|
+
- request:
|
48
|
+
method: get
|
49
|
+
uri: http://alweb.org/
|
50
|
+
body:
|
51
|
+
encoding: US-ASCII
|
52
|
+
string: ''
|
53
|
+
headers:
|
54
|
+
Accept-Encoding:
|
55
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
56
|
+
Accept:
|
57
|
+
- "*/*"
|
58
|
+
User-Agent:
|
59
|
+
- Ruby
|
60
|
+
response:
|
61
|
+
status:
|
62
|
+
code: 200
|
63
|
+
message: OK
|
64
|
+
headers:
|
65
|
+
Date:
|
66
|
+
- Thu, 28 Aug 2014 22:42:21 GMT
|
67
|
+
Server:
|
68
|
+
- Apache/2.2.16 (Debian)
|
69
|
+
X-Frame-Options:
|
70
|
+
- SAMEORIGIN
|
71
|
+
X-Xss-Protection:
|
72
|
+
- 1; mode=block
|
73
|
+
X-Content-Type-Options:
|
74
|
+
- nosniff
|
75
|
+
Etag:
|
76
|
+
- '"f24a7f595f7d88d135193e6f203bfbd5"'
|
77
|
+
Cache-Control:
|
78
|
+
- public
|
79
|
+
X-Request-Id:
|
80
|
+
- 005d51c9-30e8-45bc-886a-476b46509307
|
81
|
+
X-Runtime:
|
82
|
+
- '0.005857'
|
83
|
+
X-Powered-By:
|
84
|
+
- Phusion Passenger 4.0.49
|
85
|
+
Status:
|
86
|
+
- 200 OK
|
87
|
+
Vary:
|
88
|
+
- Accept-Encoding
|
89
|
+
Content-Length:
|
90
|
+
- '929'
|
91
|
+
Content-Type:
|
92
|
+
- text/html; charset=utf-8
|
93
|
+
body:
|
94
|
+
encoding: UTF-8
|
95
|
+
string: |
|
96
|
+
<!DOCTYPE html>
|
97
|
+
|
98
|
+
<html lang="fr">
|
99
|
+
|
100
|
+
<head>
|
101
|
+
<meta charset="utf-8" />
|
102
|
+
<title>Alexis Toulotte</title>
|
103
|
+
<meta name="author" content="Alexis Toulotte" />
|
104
|
+
<meta name="description" content="Page personnelle d'Alexis Toulotte." />
|
105
|
+
<meta name="robots" content="all" />
|
106
|
+
<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
|
107
|
+
<meta name="DC.format" content="text/html" />
|
108
|
+
<meta name="DC.language" content="fr" />
|
109
|
+
<meta name="DC.title" content="Alexis Toulotte" />
|
110
|
+
<meta name="DC.creator" content="Alexis Toulotte" />
|
111
|
+
<meta name="DC.description" content="Page personnelle d'Alexis Toulotte." />
|
112
|
+
<meta name="google-site-verification" content="1dodRvC0Zp-iAZilnTeT5PaY4bPKHwSzs9994-3g304" />
|
113
|
+
<script src="/assets/application-2faf062622c53593fc90ee0ba5667a48.js"></script>
|
114
|
+
<link href="/assets/application-a374b3c70bad2d98bffceac38d81f6d5.css" media="screen" rel="stylesheet" />
|
115
|
+
<link href="/avatar?size=128" rel="icon" type="image/png" />
|
116
|
+
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Dancing Script" />
|
117
|
+
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Raleway" />
|
118
|
+
</head>
|
119
|
+
|
120
|
+
<body>
|
121
|
+
|
122
|
+
<h1>Alexis Toulotte</h1>
|
123
|
+
|
124
|
+
<ul class="services">
|
125
|
+
<li class="github">
|
126
|
+
<a href="https://github.com/alexistoulotte"><span>GitHub</span></a>
|
127
|
+
</li>
|
128
|
+
<li class="rubygems">
|
129
|
+
<a href="https://rubygems.org/profiles/alexistoulotte"><span>RubyGems</span></a>
|
130
|
+
</li>
|
131
|
+
<li class="cv">
|
132
|
+
<a href="https://dl.dropbox.com/u/5601946/CV.pdf"><span>CV</span></a>
|
133
|
+
</li>
|
134
|
+
<li class="email">
|
135
|
+
<a href="#"><span>Email</span></a>
|
136
|
+
</li>
|
137
|
+
<li class="phone">
|
138
|
+
<a href="callto://+687817612"><span>+687 81 76 12</span></a>
|
139
|
+
</li>
|
140
|
+
<li class="skype">
|
141
|
+
<a href="skype:alexistoulotte"><span>Skype</span></a>
|
142
|
+
</li>
|
143
|
+
<li class="twitter">
|
144
|
+
<a href="https://twitter.com/alexistoulotte"><span>Twitter</span></a>
|
145
|
+
</li>
|
146
|
+
<li class="facebook">
|
147
|
+
<a href="https://www.facebook.com/alexis.toulotte"><span>Facebook</span></a>
|
148
|
+
</li>
|
149
|
+
<li class="linked-in">
|
150
|
+
<a href="https://www.linkedin.com/in/alexistoulotte"><span>LinkedIn</span></a>
|
151
|
+
</li>
|
152
|
+
<li class="google-plus">
|
153
|
+
<a href="https://plus.google.com/111956742901852825014/posts"><span>Google+</span></a>
|
154
|
+
</li>
|
155
|
+
<li class="youtube">
|
156
|
+
<a href="https://www.youtube.com/alexistoulotte"><span>YouTube</span></a>
|
157
|
+
</li>
|
158
|
+
<li class="mixcloud">
|
159
|
+
<a href="https://www.mixcloud.com/alexistoulotte"><span>Mixcloud</span></a>
|
160
|
+
</li>
|
161
|
+
<li class="last-fm">
|
162
|
+
<a href="http://www.lastfm.fr/user/alexistoulotte"><span>Last.fm</span></a>
|
163
|
+
</li>
|
164
|
+
<li class="sound-cloud">
|
165
|
+
<a href="https://soundcloud.com/alexistoulotte"><span>SoundCloud</span></a>
|
166
|
+
</li>
|
167
|
+
<li class="delicious">
|
168
|
+
<a href="https://delicious.com/alexistoulotte"><span>Delicious</span></a>
|
169
|
+
</li>
|
170
|
+
</ul>
|
171
|
+
|
172
|
+
|
173
|
+
</body>
|
174
|
+
|
175
|
+
</html>
|
176
|
+
http_version:
|
177
|
+
recorded_at: Thu, 28 Aug 2014 22:42:21 GMT
|
178
|
+
recorded_with: VCR 2.9.2
|