notu 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|