1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
:- module(episode_data, [retract_episode/1,
episode_count/1,
lookup_episode/3,
lookup_episode_local/3,
lookup_episode_remote/3]).
:- use_module(library(clpfd)).
:- use_module(library(dcg/basics)).
:- use_module(library(http/http_open)).
:- use_module(library(sgml)).
:- use_module(library(xpath)).
:- use_module(library(persistency)).
:- use_module(atom_dcg).
:- persistent episode_name_data(episode:integer, name:atom, data:list).
attach :-
absolute_file_name('episode_data.db', F, [access(write)]),
db_attach(F, []).
detach :-
db_detach.
% Interface.
episode_count(N) :-
ensure,
setof(E, N^D^lookup_episode_local(E,N,D), Es),
last(Es, N).
lookup_episode(Ep, Name, Data) :- lookup_episode_local(Ep, Name, Data), !.
lookup_episode(Ep, Name, Data) :- lookup_episode_remote(Ep, Name, Data).
lookup_episode_local(Ep, Name, Data) :-
episode_name_data(Ep, Name, Data).
lookup_episode_remote(Ep, Name, Data) :-
update, !,
episode_name_data(Ep, Name, Data).
retract_episode(Ep) :-
( episode_name_data(Ep, _, _)
-> retractall_episode_name_data(Ep, _, _)
; true
).
% Parsing.
padding(Ep) --> { Ep #< 10 }, "00".
padding(Ep) --> { Ep #>= 10, Ep #< 100 }, "0".
padding(Ep) --> { Ep #>= 100 }.
episode_number(Ep) --> padding(Ep), integer(Ep).
episode_number(Ep) --> padding(Ep), integer(Ep), "WPS", integer(_).
% Database updating.
ensure :- episode_name_data(_, _, _), !.
ensure :- update.
update :-
findall(Ep-Name-Data, (remote_row(R),
row_episode(R, Ep),
row_episode_name_data(R, Ep, Name, Data)),
ENDs), !,
maplist(update, ENDs).
update(Ep-Name-Data) :- episode_name_data(Ep, Name, Data), !.
update(Ep-Name-Data) :- assert_episode_name_data(Ep, Name, Data).
% Remote retrieval.
remote_row(R) :-
catch(http_load_html(
'https://www.detectiveconanworld.com/wiki/Next_Conan%27s_Hint',
R0),
_,
fail), !,
xpath(R0, //tr, R).
row_episode(R, Ep) :-
xpath(R, td(index(1),text), T),
atom_phrase(episode_number(Ep), T).
row_episode_name_data(R, Ep, Name, Data) :-
xpath(R, td(index(1),text), T),
atom_phrase(episode_number(Ep), T),
xpath(R, td(index(2),text), Name),
xpath(R, td(index(3),text), Hint),
Data = ['Hint'(Hint)].
http_load_html(URL, DOM) :-
setup_call_cleanup(http_open(URL, In,
[ timeout(60)
]),
( dtd(html, DTD),
load_structure(stream(In),
DOM,
[ dtd(DTD),
dialect(sgml),
shorttag(false),
max_errors(-1),
syntax_errors(quiet)
])
),
close(In)).
|