(no commit message)
[utils] / crawler / kiss / src / org / wamblee / crawler / kiss / KissCrawler.java
1 /*
2  * Copyright 2005 the original author or authors.
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  * 
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * 
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.wamblee.crawler.kiss;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileNotFoundException;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.PrintStream;
26 import java.util.ArrayList;
27 import java.util.Date;
28 import java.util.EnumMap;
29 import java.util.List;
30 import java.util.Properties;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33
34 import javax.mail.Message;
35 import javax.mail.MessagingException;
36 import javax.mail.Session;
37 import javax.mail.Transport;
38 import javax.mail.internet.InternetAddress;
39 import javax.mail.internet.MimeMessage;
40
41 import org.apache.commons.httpclient.HttpClient;
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44 import org.wamblee.conditions.Condition;
45 import org.wamblee.crawler.Action;
46 import org.wamblee.crawler.Configuration;
47 import org.wamblee.crawler.Crawler;
48 import org.wamblee.crawler.Page;
49 import org.wamblee.crawler.PageException;
50 import org.wamblee.crawler.impl.ConfigurationParser;
51 import org.wamblee.crawler.impl.CrawlerImpl;
52 import org.wamblee.crawler.kiss.Program.RecordingResult;
53
54 /**
55  * The KiSS crawler for automatic recording of interesting TV shows.
56  * 
57  */
58 public class KissCrawler {
59
60     private static final Log LOG = LogFactory.getLog(KissCrawler.class);
61
62     /**
63      * Log file name for the crawler.
64      */
65     private static final String LOG_FILE = "kiss.log";
66
67     /**
68      * Start URL of the electronic programme guide.
69      */
70     private static final String START_URL = "http://epg.kml.kiss-technology.com/login_core.php";
71
72     /**
73      * Crawler configuration file.
74      */
75     private static final String CRAWLER_CONFIG = "config.xml";
76
77     /**
78      * Configuration file describing interesting programs.
79      */
80     private static final String PROGRAM_CONFIG = "programs.xml";
81
82     /**
83      * Regular expression for matching time interval strings in the retrieved
84      * pages.
85      */
86     private static final String TIME_REGEX = "([0-9]{2}):([0-9]{2})[^0-9]*([0-9]{2}):([0-9]{2}).*";
87
88     /**
89      * Compiled pattern for the time regular expression.
90      */
91     private Pattern _pattern;
92
93     /**
94      * Runs the KiSS crawler.
95      * 
96      * @param aArgs
97      *            Arguments, currently all ignored because they are hardcoded.
98      * @throws Exception
99      *             In case of problems.
100      */
101     public static void main(String[] aArgs) throws Exception {
102         new KissCrawler(START_URL, CRAWLER_CONFIG, PROGRAM_CONFIG);
103     }
104
105     /**
106      * Constructs the crawler. This retrieves the TV guide by crawling the KiSS
107      * EPG guide, filters the guide for interesting programs, tries to record
108      * them, and sends a summary mail to the user.
109      * 
110      * @param aStartUrl
111      *            Start URL of the electronic programme guide.
112      * @param aCrawlerConfig
113      *            Configuration file for the crawler.
114      * @param aProgramConfig
115      *            Configuration file describing interesting shows.
116      * @throws IOException
117      *             In case of problems reading files.
118      * @throws MessagingException
119      *             In case of problems sending a mail notification.
120      */
121     public KissCrawler(String aStartUrl, String aCrawlerConfig,
122             String aProgramConfig) throws IOException, MessagingException {
123
124         _pattern = Pattern.compile(TIME_REGEX);
125
126         FileOutputStream fos = new FileOutputStream(new File(LOG_FILE));
127         PrintStream os = new PrintStream(fos);
128
129         try {
130             HttpClient client = new HttpClient();
131             // client.getHostConfiguration().setProxy("127.0.0.1", 3128);
132
133             Crawler crawler = createCrawler(aCrawlerConfig, os, client);
134
135             Page page = getStartPage(aStartUrl, crawler);
136             TVGuide guide = createGuide(page);
137             PrintVisitor printer = new PrintVisitor(System.out);
138             guide.accept(printer);
139
140             InputStream programConfigFile = new FileInputStream(new File(
141                     aProgramConfig));
142             Condition<Program> programCondition = new ProgramConfigurationParser()
143                     .parse(programConfigFile);
144             recordInterestingShows(programCondition, guide);
145         } finally {
146             os.flush();
147             os.close();
148             System.out.println("Output written on '" + LOG_FILE + "'");
149         }
150     }
151
152     /**
153      * Records interesting shows.
154      * 
155      * @param aProgramCondition
156      *            Condition determining which shows are interesting.
157      * @param aGuide
158      *            Television guide.
159      * @throws MessagingException
160      *             In case of problems sending a summary mail.
161      */
162     private void recordInterestingShows(Condition<Program> aProgramCondition,
163             TVGuide aGuide) throws MessagingException {
164         MatchVisitor matcher = new MatchVisitor(aProgramCondition);
165         aGuide.accept(matcher);
166         List<Program> programs = matcher.getMatches();
167         EnumMap<RecordingResult, List<Program>> messages = new EnumMap<RecordingResult, List<Program>>(
168                 RecordingResult.class);
169         for (RecordingResult result: RecordingResult.values()) {
170             messages.put(result, new ArrayList<Program>());
171         }
172         for (Program program : programs) {
173             try {
174                 Program.RecordingResult result = program.record();
175                 messages.get(result).add(program);
176             } catch (PageException e) {
177                 LOG.info("Attempt to record " + program + " failed.");
178                 messages.get(RecordingResult.ERROR).add(program);
179             }
180         }
181         String msg = "Summary of KiSS crawler: \n\n\n";
182         
183        
184         for (RecordingResult result: RecordingResult.values()) { 
185             if ( messages.get(result).size() > 0 ) { 
186                 msg += result.getDescription() + "\n\n"; 
187                 for (Program program: messages.get(result)) { 
188                     msg += program + "\n";
189                 }
190             }
191         }
192         if ( programs.size() == 0 ) { 
193             msg += "No suitable programs found"; 
194         }
195         
196         System.out.println(msg);
197         sendMail(msg);
198     }
199
200     /**
201      * Creates the crawler.
202      * 
203      * @param aCrawlerConfig
204      *            Crawler configuration file.
205      * @param aOs
206      *            Logging output stream for the crawler.
207      * @param aClient
208      *            HTTP Client to use.
209      * @return Crawler.
210      * @throws FileNotFoundException
211      *             In case configuration files cannot be found.
212      */
213     private Crawler createCrawler(String aCrawlerConfig, PrintStream aOs,
214             HttpClient aClient) throws FileNotFoundException {
215         ConfigurationParser parser = new ConfigurationParser(aOs);
216         InputStream crawlerConfigFile = new FileInputStream(new File(
217                 aCrawlerConfig));
218         Configuration config = parser.parse(crawlerConfigFile);
219         Crawler crawler = new CrawlerImpl(aClient, config);
220         return crawler;
221     }
222
223     /**
224      * Gets the start page of the electronic programme guide. This involves
225      * login and navigation to a suitable start page after logging in.
226      * 
227      * @param aStartUrl
228      *            URL of the electronic programme guide.
229      * @param aCrawler
230      *            Crawler to use.
231      * @return Starting page.
232      */
233     private Page getStartPage(String aStartUrl, Crawler aCrawler) {
234         try {
235             Page page = aCrawler.getPage(aStartUrl);
236             return page.getAction("channels-favorites").execute();
237         } catch (PageException e) {
238             throw new RuntimeException(
239                     "Could not login to electronic program guide", e);
240         }
241     }
242
243     /**
244      * Creates the TV guide by web crawling.
245      * 
246      * @param aPage
247      *            Starting page.
248      * @return TV guide.
249      */
250     private TVGuide createGuide(Page aPage) {
251         LOG.info("Obtaining full TV guide");
252         Action[] actions = aPage.getActions();
253         List<Channel> channels = new ArrayList<Channel>();
254         for (Action action : actions) {
255             try {
256                 LOG.info("Getting channel info for '" + action.getName() + "'");
257                 Channel channel = createChannel(action.getName(), action
258                         .execute().getAction("right-now").execute());
259                 channels.add(channel);
260                 if ( SystemProperties.isDebugMode() ) { 
261                     break; // Only one channel is crawled. 
262                 }
263             } catch (PageException e) {
264                 LOG.error("Could not create channel information for '"
265                         + action.getName() + "'", e);
266             }
267         }
268         return new TVGuide(channels);
269     }
270
271     /**
272      * Create channel information for a specific channel.
273      * 
274      * @param aChannel
275      *            Channel name.
276      * @param aPage
277      *            Starting page for the channel.
278      * @return Channel.
279      */
280     private Channel createChannel(String aChannel, Page aPage) {
281         LOG.info("Obtaining program for " + aChannel);
282         Action[] programActions = aPage.getActions();
283         List<Program> programs = new ArrayList<Program>();
284         for (Action action : programActions) {
285             String time = action.getContent().element("time").getText().trim();
286             Matcher matcher = _pattern.matcher(time);
287             if (matcher.matches()) {
288                 Time begin = new Time(Integer.parseInt(matcher.group(1)),
289                         Integer.parseInt(matcher.group(2)));
290                 Time end = new Time(Integer.parseInt(matcher.group(3)), Integer
291                         .parseInt(matcher.group(4)));
292                 TimeInterval interval = new TimeInterval(begin, end);
293                 String description = "";
294                 String keywords = "";
295                 if (!SystemProperties.isNoProgramDetailsRequired()) {
296                     try {
297                         Page programInfo = action.execute();
298                         description = programInfo.getContent().element(
299                                 "description").getText().trim();
300                         keywords = programInfo.getContent().element("keywords")
301                                 .getText().trim();
302                     } catch (PageException e) {
303                         LOG
304                                 .warn("Program details coul dnot be determined for '"
305                                         + action.getName() + "'");
306                     }
307                 }
308                 Program program = new Program(aChannel, action.getName(),
309                         description, keywords, interval, action);
310
311                 LOG.debug("Got program " + program);
312                 programs.add(program);
313             }
314         }
315         return new Channel(aChannel, programs);
316     }
317
318     /**
319      * Sends a summary mail to the user.
320      * 
321      * @param aText
322      *            Text of the mail.
323      * @throws MessagingException
324      *             In case of problems sending mail.
325      */
326     private void sendMail(String aText) throws MessagingException {
327         Properties props = new Properties();
328         props.put("mail.transport.protocol", "smtp");
329         props.put("mail.smtp.host", "falcon");
330         props.put("mail.smtp.port", "25");
331
332         Session mailSession = Session.getInstance(props);
333         Message message = new MimeMessage(mailSession);
334
335         message.setFrom(new InternetAddress("erik@brakkee.org"));
336         message.setRecipient(Message.RecipientType.TO, new InternetAddress(
337                 "erik@brakkee.org"));
338         message.setSentDate(new Date());
339         message.setSubject("KiSS crawler update");
340         message.setText(aText);
341         Transport.send(message);
342     }
343
344 }