syoboi_calendar 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +60 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +26 -0
- data/example/search_by_keyword.rb +29 -0
- data/example/search_saisoku.rb +34 -0
- data/lib/syoboi_calendar.rb +9 -0
- data/lib/syoboi_calendar/agent.rb +55 -0
- data/lib/syoboi_calendar/client.rb +87 -0
- data/lib/syoboi_calendar/program.rb +86 -0
- data/lib/syoboi_calendar/title.rb +138 -0
- data/lib/syoboi_calendar/version.rb +3 -0
- data/spec/fixtures/response_from_login +173 -0
- data/spec/fixtures/response_from_search_programs +724 -0
- data/spec/fixtures/response_from_search_titles +234 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/syoboi_calendar/agent_spec.rb +36 -0
- data/spec/syoboi_calendar/client_spec.rb +69 -0
- data/spec/syoboi_calendar/program_spec.rb +19 -0
- data/spec/syoboi_calendar/title_spec.rb +41 -0
- data/syoboi_calendar.gemspec +24 -0
- metadata +149 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
syoboi_calendar (0.0.1)
|
5
|
+
mechanize (>= 2.3)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
addressable (2.2.7)
|
11
|
+
crack (0.3.1)
|
12
|
+
diff-lcs (1.1.3)
|
13
|
+
domain_name (0.5.2)
|
14
|
+
unf (~> 0.0.3)
|
15
|
+
mechanize (2.3)
|
16
|
+
domain_name (~> 0.5, >= 0.5.1)
|
17
|
+
mime-types (~> 1.17, >= 1.17.2)
|
18
|
+
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
19
|
+
net-http-persistent (~> 2.5, >= 2.5.2)
|
20
|
+
nokogiri (~> 1.4)
|
21
|
+
ntlm-http (~> 0.1, >= 0.1.1)
|
22
|
+
webrobots (~> 0.0, >= 0.0.9)
|
23
|
+
mime-types (1.18)
|
24
|
+
multi_json (1.2.0)
|
25
|
+
net-http-digest_auth (1.2)
|
26
|
+
net-http-persistent (2.6)
|
27
|
+
nokogiri (1.5.2)
|
28
|
+
ntlm-http (0.1.1)
|
29
|
+
rake (0.9.2.2)
|
30
|
+
rspec (2.9.0)
|
31
|
+
rspec-core (~> 2.9.0)
|
32
|
+
rspec-expectations (~> 2.9.0)
|
33
|
+
rspec-mocks (~> 2.9.0)
|
34
|
+
rspec-core (2.9.0)
|
35
|
+
rspec-expectations (2.9.1)
|
36
|
+
diff-lcs (~> 1.1.3)
|
37
|
+
rspec-mocks (2.9.0)
|
38
|
+
simplecov (0.6.1)
|
39
|
+
multi_json (~> 1.0)
|
40
|
+
simplecov-html (~> 0.5.3)
|
41
|
+
simplecov-html (0.5.3)
|
42
|
+
simplecov-vim (0.0.1)
|
43
|
+
unf (0.0.5)
|
44
|
+
unf_ext
|
45
|
+
unf_ext (0.0.4)
|
46
|
+
webmock (1.8.6)
|
47
|
+
addressable (>= 2.2.7)
|
48
|
+
crack (>= 0.1.7)
|
49
|
+
webrobots (0.0.13)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
rake (>= 0.9.2)
|
56
|
+
rspec (>= 2.9.0)
|
57
|
+
simplecov (>= 0.6.1)
|
58
|
+
simplecov-vim (>= 0.0.1)
|
59
|
+
syoboi_calendar!
|
60
|
+
webmock (>= 1.8.6)
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Ryo NAKAMURA
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# SyoboiCalendar
|
2
|
+
|
3
|
+
Simple gem for [SyoboiCalendar](http://cal.syoboi.jp/) to search Japanese anime lineup
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'syoboi_calendar'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install syoboi_calendar
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```
|
22
|
+
require "syoboi_calender"
|
23
|
+
|
24
|
+
client = SyoboiCalendar::Client.new(
|
25
|
+
:user => "r7kamura",
|
26
|
+
:pass => "********"
|
27
|
+
)
|
28
|
+
programs = client.search(
|
29
|
+
:first => true,
|
30
|
+
:range => "2012/4/1-2012/4/30"
|
31
|
+
)
|
32
|
+
|
33
|
+
p programs[0].name #=> "さんかれあ #1 私が…ゾンビに…なったら"
|
34
|
+
p programs[0].start_time #=> 2012-04-06 02:50:00 +0900
|
35
|
+
p programs[0].channel_name #=> "TBS"
|
36
|
+
```
|
37
|
+
|
38
|
+
## Contributing
|
39
|
+
|
40
|
+
1. Fork it
|
41
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
42
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
43
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
44
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
|
5
|
+
begin
|
6
|
+
require "bundler"
|
7
|
+
rescue LoadError
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `gem install bundler` to install bundler"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
Bundler.setup(:default, :development)
|
15
|
+
rescue Bundler::BundlerError => e
|
16
|
+
$stderr.puts e.message
|
17
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
18
|
+
exit e.status_code
|
19
|
+
end
|
20
|
+
|
21
|
+
require "rspec/core"
|
22
|
+
require "rspec/core/rake_task"
|
23
|
+
|
24
|
+
RSpec::Core::RakeTask.new(:spec)
|
25
|
+
|
26
|
+
task :default => :spec
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
4
|
+
require "syoboi_calendar"
|
5
|
+
|
6
|
+
require "slop"
|
7
|
+
args = Slop.parse :help => true do
|
8
|
+
banner "usage: \n ruby #{__FILE__}"
|
9
|
+
on :k, :keyword=, "Keyword for title (ex. ひだまりスケッチ)"
|
10
|
+
on :r, :range=, "Date range (ex. 2012/4/1-2012/4/30)"
|
11
|
+
end
|
12
|
+
unless args[:keyword]
|
13
|
+
puts args.to_s.gsub(/\n\n/, "\n")
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
|
17
|
+
client = SyoboiCalendar::Client.new
|
18
|
+
titles = client.search(
|
19
|
+
:mode => :title,
|
20
|
+
:keyword => args[:keyword],
|
21
|
+
)
|
22
|
+
|
23
|
+
require "awesome_print"
|
24
|
+
titles.each do |title|
|
25
|
+
ap title.name
|
26
|
+
ap title.url
|
27
|
+
ap title.voice_actor_map
|
28
|
+
puts
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
4
|
+
require "syoboi_calendar"
|
5
|
+
|
6
|
+
require "slop"
|
7
|
+
args = Slop.parse :help => true do
|
8
|
+
on :u, :user=, "Username on SyoboiCalendar"
|
9
|
+
on :p, :pass=, "Password on SyoboiCalendar"
|
10
|
+
on :r, :range=, "Date range (e.g. 2012/4/1-2012/4/30)"
|
11
|
+
end
|
12
|
+
|
13
|
+
client = SyoboiCalendar::Client.new(
|
14
|
+
:user => args[:user],
|
15
|
+
:pass => args[:pass]
|
16
|
+
)
|
17
|
+
programs = client.search(
|
18
|
+
:first => true,
|
19
|
+
:range => args[:range] || [ # today .. today + 1.month
|
20
|
+
Time.now + 0,
|
21
|
+
Time.now + 60 * 60 * 24 * 31
|
22
|
+
].map { |i| Time.at(i).strftime("%Y/%m/%d") }.join("-")
|
23
|
+
)
|
24
|
+
|
25
|
+
programs.uniq!(&:name)
|
26
|
+
programs.sort! { |a, b| a.start_time.to_i <=> b.start_time.to_i}
|
27
|
+
programs.each { |program|
|
28
|
+
puts "|%s|%-9.9s|%s|%s|" % [
|
29
|
+
program.start_time.strftime("%Y-%m-%d %H:%M"),
|
30
|
+
program.channel_name.tr("A-Z", "A-Z").tr("a-z", "a-z").tr(" ", ""),
|
31
|
+
program.name.gsub(/( #\d+).*/, '\1'),
|
32
|
+
{true => "✔", false => " "}[program.saisoku?]
|
33
|
+
]
|
34
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module SyoboiCalendar
|
5
|
+
class Agent
|
6
|
+
BASE_URL = "http://cal.syoboi.jp"
|
7
|
+
SEARCH_URL = BASE_URL + "/find"
|
8
|
+
JSON_URL = BASE_URL + "/json.php"
|
9
|
+
LOGIN_URL = BASE_URL + "/usr"
|
10
|
+
|
11
|
+
# login is option to search with user channel setting
|
12
|
+
def initialize(opts = {})
|
13
|
+
if opts[:user] && opts[:pass]
|
14
|
+
login(opts[:user], opts[:pass])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def login(user, pass)
|
19
|
+
page = get LOGIN_URL
|
20
|
+
form = page.forms[1]
|
21
|
+
form.usr = user
|
22
|
+
form.pass = pass
|
23
|
+
mechanize.submit(form)
|
24
|
+
@logined = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def login?
|
28
|
+
!!@logined
|
29
|
+
end
|
30
|
+
|
31
|
+
def json(query)
|
32
|
+
page = get JSON_URL, query
|
33
|
+
JSON.parse(page.content)
|
34
|
+
end
|
35
|
+
|
36
|
+
def search(query)
|
37
|
+
get SEARCH_URL, query
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def get(url, hash = {})
|
43
|
+
mechanize.get(url + querinize(hash))
|
44
|
+
end
|
45
|
+
|
46
|
+
# change hash into URL query string
|
47
|
+
def querinize(hash)
|
48
|
+
"?" + hash.map { |k, v| "#{k}=#{URI.encode(v.to_s)}" }.join("&")
|
49
|
+
end
|
50
|
+
|
51
|
+
def mechanize
|
52
|
+
@mechanize ||= Mechanize.new
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module SyoboiCalendar
|
2
|
+
class Client
|
3
|
+
# user and pass are optional to search with user's channel setting
|
4
|
+
def initialize(opts = {})
|
5
|
+
@agent = Agent.new(:user => opts[:user], :pass => opts[:pass])
|
6
|
+
end
|
7
|
+
|
8
|
+
def login?
|
9
|
+
@agent.login?
|
10
|
+
end
|
11
|
+
|
12
|
+
# search programs
|
13
|
+
def search(args)
|
14
|
+
query = create_search_query(args)
|
15
|
+
page = @agent.search(query)
|
16
|
+
|
17
|
+
args[:mode] == :title ?
|
18
|
+
extract_titles(page) :
|
19
|
+
extract_programs(page)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# return Array of #<SyoboiCalendar::Program> by #<Mechanize::Page>
|
25
|
+
def extract_programs(page, opts = {})
|
26
|
+
page.search(".tframe tr").map do |tr|
|
27
|
+
args = {}
|
28
|
+
|
29
|
+
tr.search("td:nth-child(2) a").each do |a|
|
30
|
+
if match = a.attributes["href"].value.match(%r|/tid/(\d+)/time#(\d+)$|)
|
31
|
+
args[:tid] = match[1]
|
32
|
+
args[:pid] = match[2]
|
33
|
+
args[:name] = a.text
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
tr.search("td:nth-child(3)").each do |td|
|
38
|
+
args[:channel_name] = td.text
|
39
|
+
end
|
40
|
+
|
41
|
+
if args.has_key?(:tid)
|
42
|
+
Program.new(args)
|
43
|
+
else
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
# return Array of #<SyoboiCalendar::Title> by #<Mechanize::Page>
|
50
|
+
def extract_titles(page, opts = {})
|
51
|
+
page.search(".tframe tr").map do |tr|
|
52
|
+
args = {}
|
53
|
+
|
54
|
+
tr.search("td:nth-child(1) a").each do |a|
|
55
|
+
if match = a.attributes["href"].value.match(%r|/tid/(\d+)$|)
|
56
|
+
args[:tid] = match[1]
|
57
|
+
args[:name] = a.text
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if args.has_key?(:tid)
|
62
|
+
Title.new(args)
|
63
|
+
else
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end.compact
|
67
|
+
end
|
68
|
+
|
69
|
+
# create hash for search query
|
70
|
+
def create_search_query(opts)
|
71
|
+
{
|
72
|
+
:sd => { nil => 2, :program => 2, :title => 0 }[opts[:mode]],
|
73
|
+
:r => { nil => 0, :all => 0, :past => 1, :future => 2}[opts[:range]] || 3,
|
74
|
+
:rd => opts[:range],
|
75
|
+
:kw => opts[:keyword],
|
76
|
+
:ch => opts[:channel],
|
77
|
+
:st => opts[:subtitle],
|
78
|
+
:cm => opts[:comment],
|
79
|
+
:uuc => opts[:uuc] || 1, # use user's channel setting
|
80
|
+
:v => opts[:v] || 0, # list view
|
81
|
+
:pfn => opts[:first] && 2, # first episode
|
82
|
+
:pfl => opts[:final] && 4, # final episode
|
83
|
+
:pfs => opts[:special] && 1, # special program
|
84
|
+
}.select { |k, v| v }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module SyoboiCalendar
|
2
|
+
class Program
|
3
|
+
attr_reader(
|
4
|
+
:pid,
|
5
|
+
:tid,
|
6
|
+
:name,
|
7
|
+
:channel_name
|
8
|
+
)
|
9
|
+
|
10
|
+
# (1) :method_name => "NameInSyoboiCalendarResponse"
|
11
|
+
# (2) :method_name => ["NameInSyoboiCalendarResponse", callback]
|
12
|
+
EXT_PARAM_MAP = {
|
13
|
+
:channel_epg_url => "ChEPGURL",
|
14
|
+
:channel_id => "ChID",
|
15
|
+
:comment => "Comment",
|
16
|
+
:config_flag => "ConfFlag",
|
17
|
+
:count => "Count",
|
18
|
+
:end_time => ["EdTime", proc { |val| Time.at(val.to_i) }],
|
19
|
+
:start_time => ["StTime", proc { |val| Time.at(val.to_i) }],
|
20
|
+
:subtitle => "SubTitle",
|
21
|
+
:subtitle2 => "SubTitle2"
|
22
|
+
}
|
23
|
+
|
24
|
+
# Example:
|
25
|
+
# program.count
|
26
|
+
# program.start_time
|
27
|
+
# ...
|
28
|
+
EXT_PARAM_MAP.keys.each do |key|
|
29
|
+
define_method(key) do
|
30
|
+
update_detail unless @blob.has_key?(key)
|
31
|
+
@blob[key]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# NOTICE: @blob is empty hash at first
|
36
|
+
# For example,
|
37
|
+
# If self.start_time is called,
|
38
|
+
# then update_detail is called automatically,
|
39
|
+
# and @blob is updated to fill up @blob[:start_time]
|
40
|
+
def initialize(args)
|
41
|
+
@pid = args[:pid]
|
42
|
+
@tid = args[:tid]
|
43
|
+
@name = args[:name]
|
44
|
+
@channel_name = args[:channel_name]
|
45
|
+
@blob = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# SAISOKU means "the most earliest broadcasting in the World"
|
49
|
+
def saisoku?
|
50
|
+
!!title.first_channel.match(channel_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# update params from detail data
|
54
|
+
def update_detail
|
55
|
+
hash = get_detail
|
56
|
+
EXT_PARAM_MAP.each do |k, v|
|
57
|
+
if v.kind_of?(Array)
|
58
|
+
@blob[k] = v[1].call(hash[v[0]])
|
59
|
+
else
|
60
|
+
@blob[k] = hash[v]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def title
|
66
|
+
@title ||= Title.new(:tid => @tid)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# request detail data
|
72
|
+
def get_detail
|
73
|
+
hash = self.class.agent.json(
|
74
|
+
:Req => "ProgramByPID",
|
75
|
+
:PID => @pid,
|
76
|
+
)
|
77
|
+
hash = hash[hash.keys.first]
|
78
|
+
hash = hash[hash.keys.first]
|
79
|
+
hash
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.agent
|
83
|
+
@agent ||= Agent.new
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|