001package ball.tv.epg.sd; 002/*- 003 * ########################################################################## 004 * TV H/W, EPGs, and Recording 005 * $Id: SDClient.java 7215 2021-01-03 18:39:51Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-tv/trunk/src/main/java/ball/tv/epg/sd/SDClient.java $ 007 * %% 008 * Copyright (C) 2013 - 2021 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import ball.http.ProtocolClient; 024import ball.tv.ObjectMapperConfiguration; 025import ball.tv.epg.entity.Headend; 026import ball.tv.epg.entity.Lineup; 027import ball.tv.epg.entity.Program; 028import com.fasterxml.jackson.databind.JsonNode; 029import com.fasterxml.jackson.databind.node.ObjectNode; 030import java.io.IOException; 031import java.math.BigInteger; 032import java.net.URI; 033import java.nio.file.Files; 034import java.nio.file.Path; 035import java.nio.file.Paths; 036import java.security.MessageDigest; 037import java.time.Duration; 038import java.time.Instant; 039import java.util.Collection; 040import java.util.LinkedHashMap; 041import java.util.List; 042import java.util.Map; 043import org.apache.http.HttpEntity; 044import org.apache.http.HttpRequest; 045import org.apache.http.HttpResponse; 046import org.apache.http.ProtocolException; 047import org.apache.http.impl.client.DefaultRedirectStrategy; 048import org.apache.http.impl.client.HttpClientBuilder; 049import org.apache.http.protocol.HttpContext; 050 051import static java.nio.charset.StandardCharsets.UTF_8; 052 053/** 054 * {@link SDProtocol} client. 055 * 056 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 057 * @version $Revision: 7215 $ 058 */ 059public class SDClient extends ProtocolClient<SDProtocol> { 060 private static final Path PARENT = 061 Paths.get(System.getProperty("user.home"), ".tv", "epg", "sd") 062 .normalize(); 063 064 private static final HttpClientBuilder BUILDER = 065 HttpClientBuilder.create() 066 .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE); 067 068 private final Path parent; 069 private final Path credentials; 070 private final Path token; 071 072 /** 073 * No-argument constructor. 074 */ 075 public SDClient() { this(null); } 076 077 /** 078 * Constructor to specify local parent directory. 079 * 080 * @param parent The local parent {@link Path}. 081 */ 082 public SDClient(Path parent) { this(parent, null); } 083 084 /** 085 * Protected constructor. 086 * 087 * @param parent The local parent {@link Path}. 088 * @param builder A configured {@link HttpClientBuilder}. 089 */ 090 protected SDClient(Path parent, HttpClientBuilder builder) { 091 super((builder != null) ? builder : BUILDER, 092 null, SDProtocol.class); 093 super.mapper = ObjectMapperConfiguration.MAPPER; 094 095 this.parent = (parent != null) ? parent : PARENT; 096 this.credentials = this.parent.resolve("credentials.json"); 097 this.token = this.parent.resolve("token"); 098 } 099 100 /** 101 * Method to get the local parent {@link Path}. 102 * 103 * @return The parent {@link Path}. 104 */ 105 public Path getParent() { return parent; } 106 107 /** 108 * Method to initialize the user configuration. 109 * 110 * @param username The user's name. 111 * @param password The user's password. 112 * 113 * @throws Exception If the hierarchy cannot be initialized. 114 */ 115 public void initialize(String username, String password) throws Exception { 116 ObjectNode node = mapper.createObjectNode(); 117 118 node.put("username", username); 119 node.put("password", hash(password)); 120 121 Files.createDirectories(credentials.getParent()); 122 mapper.writeValue(credentials.toFile(), node); 123 } 124 125 /** 126 * Method to invoke {@link SDProtocol#postToken(File)}. 127 * 128 * @return The token {@link String} if successful; {@code null} 129 * otherwise. 130 * 131 * @throws Exception If an exception is encountered invoking 132 * {@link SDProtocol#postToken(File)}. 133 */ 134 public String getToken() throws Exception { 135 String value = null; 136 137 synchronized (this) { 138 if (Files.exists(token) && ageOf(token).toMinutes() > (24 * 60)) { 139 Files.deleteIfExists(token); 140 } 141 142 if (Files.exists(token)) { 143 value = new String(Files.readAllBytes(token), UTF_8); 144 } else { 145 SDProtocol.PostTokenResponse response = 146 proxy().postToken(credentials.toFile()); 147 148 if (response.getCode() == 0) { 149 value = response.getToken(); 150 151 if (value != null) { 152 Files.write(token, value.getBytes(UTF_8)); 153 } 154 } else { 155 throw new ProtocolException(response.getMessage()); 156 } 157 } 158 } 159 160 return value; 161 } 162 163 private Duration ageOf(Path path) throws Exception { 164 return Duration.between(Files.getLastModifiedTime(path).toInstant(), 165 Instant.now()); 166 } 167 168 /** 169 * Method to invoke {@link SDProtocol#getStatus(String)}. 170 * 171 * @return The {@link SDProtocol.GetStatusResponse}. 172 * 173 * @throws Exception If an exception is encountered invoking 174 * {@link SDProtocol#getStatus(String)}. 175 */ 176 public SDProtocol.GetStatusResponse getStatus() throws Exception { 177 return proxy().getStatus(getToken()); 178 } 179 180 /** 181 * Method to invoke {@link SDProtocol#getVersion(String)}. 182 * 183 * @return The {@link SDProtocol.GetVersionResponse}. 184 * 185 * @throws Exception If an exception is encountered invoking 186 * {@link SDProtocol#getVersion(String)}. 187 */ 188 public SDProtocol.GetVersionResponse getVersion() throws Exception { 189 return proxy().getVersion(getClass().getPackage().getName()); 190 } 191 192 /** 193 * Method to invoke 194 * {@link SDProtocol#getHeadends(String,String,String)}. 195 * 196 * @param country The country. 197 * @param postalcode The postal (ZIP) code. 198 * 199 * @return The {@link Headend}s. 200 * 201 * @throws Exception If an exception is encountered invoking 202 * {@link SDProtocol#getHeadends(String,String,String)}. 203 */ 204 public List<Headend> getHeadends(String country, 205 String postalcode) throws Exception { 206 return proxy().getHeadends(getToken(), country, postalcode); 207 } 208 209 /** 210 * Method to invoke {@link SDProtocol#putLineup(String,String)}. 211 * 212 * @param lineup The line-up to add. 213 * 214 * @return The {@link JsonNode}. 215 * 216 * @throws Exception If an exception is encountered invoking 217 * {@link SDProtocol#putLineup(String,String)}. 218 */ 219 public JsonNode putLineup(String lineup) throws Exception { 220 return proxy().putLineup(getToken(), lineup); 221 } 222 223 /** 224 * Method to invoke {@link SDProtocol#getLineups(String)}. 225 * 226 * @return The {@link SDProtocol.GetLineupsResponse}. 227 * 228 * @throws Exception If an exception is encountered invoking 229 * {@link SDProtocol#getLineups(String)}. 230 */ 231 public SDProtocol.GetLineupsResponse getLineups() throws Exception { 232 return proxy().getLineups(getToken()); 233 } 234 235 /** 236 * Method to invoke {@link SDProtocol#deleteLineup(String,String)}. 237 * 238 * @param lineup The line-up to delete. 239 * 240 * @return The {@link JsonNode}. 241 * 242 * @throws Exception If an exception is encountered invoking 243 * {@link SDProtocol#deleteLineup(String,String)}. 244 */ 245 public JsonNode deleteLineup(String lineup) throws Exception { 246 return proxy().deleteLineup(getToken(), lineup); 247 } 248 249 /** 250 * Method to invoke 251 * {@link SDProtocol#getLineup(String,Boolean,String)}. 252 * 253 * @param lineup The line-up to get. 254 * 255 * @return The {@link Lineup}. 256 * 257 * @throws Exception If an exception is encountered invoking 258 * {@link SDProtocol#getLineup(String,Boolean,String)}. 259 */ 260 public Lineup getLineup(String lineup) throws Exception { 261 return proxy().getLineup(getToken(), true, lineup); 262 } 263 264 /** 265 * Method to invoke 266 * {@link SDProtocol#postLineup(String,HttpEntity)} to attempt to 267 * auto-map a discovered HDHR Prime line-up. 268 * 269 * @param entity The JSON entity retrieved from the HDHR 270 * Prime. 271 * 272 * @return The {@link JsonNode}. 273 * 274 * @throws Exception If an exception is encountered invoking 275 * {@link SDProtocol#postLineup(String,HttpEntity)}. 276 * 277 * @see silicondust.HDHRPrimeClient#getLineup() 278 */ 279 public JsonNode postLineup(HttpEntity entity) throws Exception { 280 return proxy().postLineup(getToken(), entity); 281 } 282 283 /** 284 * Method to invoke 285 * {@link SDProtocol#postLineup(String,String,HttpEntity)} to attempt to 286 * post a discovered HDHR Prime line-up to the server. 287 * 288 * @param lineup The name of the line-up. 289 * @param entity The JSON entity retrieved from the HDHR 290 * Prime. 291 * 292 * @return The {@link JsonNode}. 293 * 294 * @throws Exception If an exception is encountered invoking 295 * {@link SDProtocol#postLineup(String,String,HttpEntity)}. 296 * 297 * @see silicondust.HDHRPrimeClient#getLineup() 298 */ 299 public JsonNode postLineup(String lineup, 300 HttpEntity entity) throws Exception { 301 return proxy().postLineup(getToken(), lineup, entity); 302 } 303 304 /** 305 * Method to invoke {@link SDProtocol#postPrograms(String,Collection)}. 306 * 307 * @param ids The {@link Collection} of program IDs, 308 * 309 * @return The {@link List} of {@link Program}s. 310 * 311 * @throws Exception If an exception is encountered invoking 312 * {@link SDProtocol#postPrograms(String,Collection)}. 313 */ 314 public List<Program> getPrograms(Collection<String> ids) throws Exception { 315 return proxy().postPrograms(getToken(), ids); 316 } 317 318 /** 319 * Method to invoke 320 * {@link SDProtocol#postProgramsDescription(String,Collection)}. 321 * 322 * @param ids The {@link Collection} of program IDs, 323 * 324 * @return The {@link JsonNode}. 325 * 326 * @throws Exception If an exception is encountered invoking 327 * {@link SDProtocol#postProgramsDescription(String,Collection)}. 328 */ 329 public JsonNode getProgramsDescription(Collection<String> ids) throws Exception { 330 return proxy().postProgramsDescription(getToken(), ids); 331 } 332 333 /** 334 * Method to invoke 335 * {@link SDProtocol#postProgramsMetadata(String,Collection)}. 336 * 337 * @param ids The {@link Collection} of program IDs, 338 * 339 * @return The {@link JsonNode}. 340 * 341 * @throws Exception If an exception is encountered invoking 342 * {@link SDProtocol#postProgramsMetadata(String,Collection)}. 343 */ 344 public JsonNode getProgramsMetadata(Collection<String> ids) throws Exception { 345 return proxy().postProgramsMetadata(getToken(), ids); 346 } 347 348 /** 349 * Method to invoke {@link SDProtocol#getProgramMetadata(String)}. 350 * 351 * @param id The root ID, 352 * 353 * @return The {@link JsonNode}. 354 * 355 * @throws Exception If an exception is encountered invoking 356 * {@link SDProtocol#getProgramMetadata(String)}. 357 */ 358 public JsonNode getProgramMetadata(String id) throws Exception { 359 return proxy().getProgramMetadata(id); 360 } 361 362 /** 363 * Method to invoke 364 * {@link SDProtocol#postSchedules(String,Collection)}. 365 * 366 * @param ids The {@link Collection} of station IDs, 367 * 368 * @return The {@link List} of {@link SDProtocol.Schedules}. 369 * 370 * @throws Exception If an exception is encountered invoking 371 * {@link SDProtocol#postSchedules(String,Collection)}. 372 */ 373 public List<SDProtocol.Schedules> getSchedules(Collection<?> ids) throws Exception { 374 return proxy().postSchedules(getToken(), 375 new PostSchedulesMap(ids).values()); 376 } 377 378 /** 379 * {@link #getSchedules(Collection)} and 380 * {@link #getSchedulesMD5(Collection)} parameter: Use {@link #values()} 381 * as argument. 382 */ 383 public static class PostSchedulesMap 384 extends LinkedHashMap<Object,Map<String,Object>> { 385 private static final long serialVersionUID = 6106598221772498302L; 386 387 /** 388 * Sole constructor. 389 * 390 * @param ids The station IDs. 391 */ 392 public PostSchedulesMap(Collection<?> ids) { 393 super(); 394 395 for (Object key : ids) { 396 if (! containsKey(key)) { 397 put(key, new LinkedHashMap<String,Object>()); 398 } 399 400 get(key).put("stationID", key); 401 } 402 } 403 } 404 405 /** 406 * Method to invoke 407 * {@link SDProtocol#postSchedulesMD5(String,Collection)}. 408 * 409 * @param ids The {@link Collection} of station IDs, 410 * 411 * @return The {@link JsonNode}. 412 * 413 * @throws Exception If an exception is encountered invoking 414 * {@link SDProtocol#postSchedulesMD5(String,Collection)}. 415 */ 416 public JsonNode getSchedulesMD5(Collection<?> ids) throws Exception { 417 return proxy().postSchedulesMD5(getToken(), 418 new PostSchedulesMap(ids).values()); 419 } 420 421 /** 422 * Method to invoke {@link SDProtocol#getCelebrityMetadata(String)}. 423 * 424 * @param id The root ID, 425 * 426 * @return The {@link JsonNode}. 427 * 428 * @throws Exception If an exception is encountered invoking 429 * {@link SDProtocol#getCelebrityMetadata(String)}. 430 */ 431 public JsonNode getCelebrityMetadata(String id) throws Exception { 432 return proxy().getCelebrityMetadata(id); 433 } 434 435 /** 436 * Method to invoke {@link SDProtocol#getImage(URI)} or 437 * {@link SDProtocol#getImage(String)} (as appropriate). 438 * 439 * @param string The image {@link URI} 440 * (as {@link String}). 441 * 442 * @return The {@link HttpEntity}. 443 * 444 * @throws Exception If an exception is encountered invoking 445 * {@link SDProtocol#getImage(URI)} or 446 * {@link SDProtocol#getImage(String)}. 447 */ 448 public HttpEntity getImage(String string) throws Exception { 449 URI uri = URI.create(string); 450 HttpResponse response = 451 uri.isAbsolute() 452 ? proxy().getImage(uri) 453 : proxy().getImage(string); 454 455 return response.getEntity(); 456 } 457 458 @Override 459 public void process(HttpRequest request, 460 HttpContext context) throws IOException { 461 super.process(request, context); 462 } 463 464 @Override 465 public void process(HttpResponse response, 466 HttpContext context) throws IOException { 467 super.process(response, context); 468 } 469 470 @Override 471 public String toString() { return super.toString(); } 472 473 /** 474 * Static method to generate a 40-digit {@code SHA1} hash of a user 475 * password. 476 * 477 * @param password The user password. 478 * 479 * @return The hash (as a 40-{@link Character} {@link String}. 480 */ 481 public static String hash(String password) { 482 BigInteger hash = null; 483 484 try { 485 hash = 486 new BigInteger(1, 487 MessageDigest.getInstance("SHA1") 488 .digest(password.getBytes(UTF_8))); 489 } catch (Exception exception) { 490 throw new Error(exception); 491 } 492 493 return String.format("%040x", hash); 494 } 495}