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