Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<version>2.1.9.RELEASE</version>
<relativePath/>
</parent>

Expand All @@ -39,6 +39,39 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.1.3.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand All @@ -51,5 +84,6 @@
</configuration>
</plugin>
</plugins>
<testSourceDirectory>${project.basedir}/src/test</testSourceDirectory>
</build>
</project>
257 changes: 8 additions & 249 deletions src/main/java/org/nuvola/tvshowtime/ApplicationLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,268 +18,27 @@

package org.nuvola.tvshowtime;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.stream.Collectors;

import org.nuvola.tvshowtime.business.plex.MediaContainer;
import org.nuvola.tvshowtime.business.plex.User;
import org.nuvola.tvshowtime.business.plex.Video;
import org.nuvola.tvshowtime.business.tvshowtime.AccessToken;
import org.nuvola.tvshowtime.business.tvshowtime.AuthorizationCode;
import org.nuvola.tvshowtime.business.tvshowtime.Message;
import org.nuvola.tvshowtime.config.PMSConfig;
import org.nuvola.tvshowtime.config.TVShowTimeConfig;
import org.nuvola.tvshowtime.util.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.client.RestTemplate;

import static org.nuvola.tvshowtime.util.Constants.MINUTE_IN_MILIS;
import static org.nuvola.tvshowtime.util.Constants.PMS_WATCH_HISTORY;
import static org.nuvola.tvshowtime.util.Constants.TVST_ACCESS_TOKEN_URI;
import static org.nuvola.tvshowtime.util.Constants.TVST_AUTHORIZE_URI;
import static org.nuvola.tvshowtime.util.Constants.TVST_CHECKIN_URI;
import static org.nuvola.tvshowtime.util.Constants.TVST_CLIENT_ID;
import static org.nuvola.tvshowtime.util.Constants.TVST_CLIENT_SECRET;
import static org.nuvola.tvshowtime.util.Constants.TVST_RATE_REMAINING_HEADER;
import static org.nuvola.tvshowtime.util.Constants.TVST_USER_AGENT;
import static org.springframework.http.HttpMethod.POST;

@SpringBootApplication
@EnableScheduling
public class ApplicationLauncher {
private static final Logger LOG = LoggerFactory.getLogger(ApplicationLauncher.class);

@Autowired
private TVShowTimeConfig tvShowTimeConfig;
@Autowired
private PMSConfig pmsConfig;

private RestTemplate tvShowTimeTemplate;
private RestTemplate pmsTemplate;
private AccessToken accessToken;
private Timer tokenTimer;

public static void main(String[] args) {
SpringApplication.run(ApplicationLauncher.class, args);
}

@Scheduled(fixedDelay = Long.MAX_VALUE)
public void init() {
tvShowTimeTemplate = new RestTemplate();
private TvTimeService m_tvTimeService;

File storeToken = new File(tvShowTimeConfig.getTokenFile());
if (storeToken.exists()) {
try {
FileInputStream fileInputStream = new FileInputStream(storeToken);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
accessToken = (AccessToken) objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();

LOG.info("AccessToken loaded from file with success : " + accessToken);
} catch (Exception e) {
LOG.error("Error parsing the AccessToken stored in 'session_token'.");
LOG.error("Please remove the 'session_token' file, and try again.");
LOG.error(e.getMessage());

System.exit(1);
}

try {
processWatchedEpisodes();
} catch (Exception e) {
LOG.error("Error during marking episodes as watched.");
LOG.error(e.getMessage());

System.exit(1);
}
} else {
requestAccessToken();
}
public static void main(String[] args) {
SpringApplication.run(ApplicationLauncher.class, args);
}

private void requestAccessToken() {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<String> entity = new HttpEntity<>("client_id=" + TVST_CLIENT_ID, headers);

ResponseEntity<AuthorizationCode> content = tvShowTimeTemplate.exchange(TVST_AUTHORIZE_URI, POST, entity,
AuthorizationCode.class);
AuthorizationCode authorizationCode = content.getBody();

if (authorizationCode.getResult().equals("OK")) {
LOG.info("Linking with your TVShowTime account using the code " + authorizationCode.getDevice_code());
LOG.info("Please open the URL " + authorizationCode.getVerification_url() + " in your browser");
LOG.info("Connect with your TVShowTime account and type in the following code : ");
LOG.info(authorizationCode.getUser_code());
LOG.info("Waiting for you to type in the code in TVShowTime :-D ...");

tokenTimer = new Timer();
tokenTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
loadAccessToken(authorizationCode.getDevice_code());
}
}, 1000 * authorizationCode.getInterval(), 1000 * authorizationCode.getInterval());
} else {
LOG.error("OAuth authentication TVShowTime failed.");

System.exit(1);
}
} catch (Exception e) {
LOG.error("OAuth authentication TVShowTime failed.");
LOG.error(e.getMessage());

System.exit(1);
}
@EventListener(ApplicationReadyEvent.class)
public void init() throws Exception {
m_tvTimeService.init();
}

private void loadAccessToken(String deviceCode) {
String query = new StringBuilder("client_id=")
.append(TVST_CLIENT_ID)
.append("&client_secret=")
.append(TVST_CLIENT_SECRET)
.append("&code=")
.append(deviceCode)
.toString();

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<String> entity = new HttpEntity<>(query, headers);

ResponseEntity<AccessToken> content = tvShowTimeTemplate.exchange(TVST_ACCESS_TOKEN_URI, POST, entity,
AccessToken.class);
accessToken = content.getBody();

if (accessToken.getResult().equals("OK")) {
LOG.info("AccessToken from TVShowTime with success : " + accessToken);
tokenTimer.cancel();

storeAccessToken();
processWatchedEpisodes();
} else {
if (!accessToken.getMessage().equals("Authorization pending")
&& !accessToken.getMessage().equals("Slow down")) {
LOG.error("Unexpected error did arrive, please reload the service :-(");
tokenTimer.cancel();

System.exit(1);
}
}
}

private void storeAccessToken() {
try {
File storeToken = new File(tvShowTimeConfig.getTokenFile());
FileOutputStream fileOutputStream = new FileOutputStream(storeToken);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(accessToken);
objectOutputStream.close();
fileOutputStream.close();

LOG.info("AccessToken store successfully inside a file...");
} catch (Exception e) {
LOG.error("Unexpected error did arrive when trying to store the AccessToken in a file ");
LOG.error(e.getMessage());

System.exit(1);
}
}

private void processWatchedEpisodes() {
pmsTemplate = new RestTemplate();
String watchHistoryUrl = pmsConfig.getPath() + PMS_WATCH_HISTORY;

if (pmsConfig.getToken() != null && !pmsConfig.getToken().isEmpty()) {
watchHistoryUrl += "?X-Plex-Token=" + pmsConfig.getToken();
LOG.info("Calling Plex with a X-Plex-Token...");
}

ResponseEntity<MediaContainer> response = pmsTemplate.getForEntity(watchHistoryUrl, MediaContainer.class);
MediaContainer mediaContainer = response.getBody();

for (Video video : mediaContainer.getVideo()) {
LocalDateTime date = DateUtils.getDateTimeFromTimestamp(video.getViewedAt());

// Mark as watched only episodes for configured user
if (pmsConfig.getUsername() != null && video.getUser() != null) {
List<User> users = video.getUser().stream().filter(user -> user.getName().equals(pmsConfig.getUsername())).collect(Collectors.toList());

if (users.stream().count() == 0) {
continue;
}
}

// Mark as watched only today and yesterday episodes
if (DateUtils.isTodayOrYesterday(date) || pmsConfig.getMarkall() == true) {
if (video.getType().equals("episode")) {
String episode = new StringBuilder(video.getGrandparentTitle())
.append(" - S")
.append(video.getParentIndex())
.append("E").append(video.getIndex())
.toString();

markEpisodeAsWatched(episode);
} else {
continue;
}
} else {
LOG.info("All episodes are processed successfully ...");
System.exit(0);
}
}
}

private void markEpisodeAsWatched(String episode) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("User-Agent", TVST_USER_AGENT);
HttpEntity<String> entity = new HttpEntity<>("filename=" + episode, headers);

String checkinUrl = new StringBuilder(TVST_CHECKIN_URI)
.append("?access_token=")
.append(accessToken.getAccess_token())
.toString();

ResponseEntity<Message> content = tvShowTimeTemplate.exchange(checkinUrl, POST, entity, Message.class);
Message message = content.getBody();

if (message.getResult().equals("OK")) {
LOG.info("Mark " + episode + " as watched in TVShowTime");
} else {
LOG.error("Error while marking [" + episode + "] as watched in TVShowTime ");
}

// Check if we are below the Rate-Limit of the API
int remainingApiCalls = Integer.parseInt(content.getHeaders().get(TVST_RATE_REMAINING_HEADER).get(0));
if (remainingApiCalls == 0) {
try {
LOG.info("Consumed all available TVShowTime API calls slots, waiting for new slots ...");
Thread.sleep(MINUTE_IN_MILIS);
} catch (Exception e) {
LOG.error(e.getMessage());

System.exit(1);
}
}
}
}
Loading