All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tinymediamanager.scraper.trakttv.TraktTv Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2012 - 2019 Manuel Laggner
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.tinymediamanager.scraper.trakttv;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.threeten.bp.DateTimeUtils;
import org.threeten.bp.OffsetDateTime;
import org.threeten.bp.ZoneId;
import org.tinymediamanager.Globals;
import org.tinymediamanager.core.Constants;
import org.tinymediamanager.core.movie.MovieList;
import org.tinymediamanager.core.movie.entities.Movie;
import org.tinymediamanager.core.tvshow.TvShowList;
import org.tinymediamanager.core.tvshow.entities.TvShow;
import org.tinymediamanager.core.tvshow.entities.TvShowEpisode;
import org.tinymediamanager.core.tvshow.entities.TvShowSeason;
import org.tinymediamanager.scraper.MediaProviderInfo;
import org.tinymediamanager.scraper.http.TmmHttpClient;
import org.tinymediamanager.scraper.util.ApiKey;

import com.uwetrottmann.trakt5.TraktV2;
import com.uwetrottmann.trakt5.TraktV2Interceptor;
import com.uwetrottmann.trakt5.entities.AccessToken;
import com.uwetrottmann.trakt5.entities.BaseEpisode;
import com.uwetrottmann.trakt5.entities.BaseMovie;
import com.uwetrottmann.trakt5.entities.BaseSeason;
import com.uwetrottmann.trakt5.entities.BaseShow;
import com.uwetrottmann.trakt5.entities.MovieIds;
import com.uwetrottmann.trakt5.entities.ShowIds;
import com.uwetrottmann.trakt5.entities.SyncEpisode;
import com.uwetrottmann.trakt5.entities.SyncErrors;
import com.uwetrottmann.trakt5.entities.SyncItems;
import com.uwetrottmann.trakt5.entities.SyncMovie;
import com.uwetrottmann.trakt5.entities.SyncResponse;
import com.uwetrottmann.trakt5.entities.SyncSeason;
import com.uwetrottmann.trakt5.entities.SyncShow;
import com.uwetrottmann.trakt5.entities.SyncStats;

import okhttp3.OkHttpClient;
import retrofit2.Response;

/**
 * Sync your collection and watched status with Trakt.tv
* Using best practice 2-way-sync according to http://trakt.tv/api-docs/sync
* https://github.com/UweTrottmann/trakt-java * * @author Myron Boyle * */ public class TraktTv { private static final String CLIENT_ID = "a8e7e30fd7fd3f397b6e079f9f023e790f9cbd80a2be57c104089174fa8c6d89"; private static final Logger LOGGER = LoggerFactory.getLogger(TraktTv.class); private static final TraktV2 TRAKT = createTraktApi(); private static TraktTv instance; private static MediaProviderInfo providerInfo = new MediaProviderInfo(Constants.TRAKT, "Trakt.tv", "Scraper for Trakt.tv; yes, we can scrape here too :)"); private static TraktV2 createTraktApi() { TraktV2 api = new TraktV2(CLIENT_ID, ApiKey.decryptApikey("VD2h4jmnrrYWnP1Nk49UtTNRILiWsuelJKdza7DAw+ROh1wtVf2U6PQScm7QWCOTsxN0K3QluIykKs2ZT1af1GcPz1401005bDBDss1Pz2c="), "urn:ietf:wg:oauth:2.0:oob") { // tell the trakt api to use our OkHttp client @Override protected synchronized OkHttpClient okHttpClient() { OkHttpClient.Builder builder = TmmHttpClient.newBuilder(); builder.addInterceptor(new TraktV2Interceptor(this)); return builder.build(); } }; return api; } public static synchronized TraktTv getInstance() { if (instance == null) { instance = new TraktTv(); } return instance; } public TraktTv() { } public static Map authenticateViaPin(String pin) throws Exception { Map result = new HashMap<>(); // OAuthAccessTokenResponse response = TraktV2.getAccessToken(CLIENT_ID, // ApiKey.decryptApikey("VD2h4jmnrrYWnP1Nk49UtTNRILiWsuelJKdza7DAw+ROh1wtVf2U6PQScm7QWCOTsxN0K3QluIykKs2ZT1af1GcPz1401005bDBDss1Pz2c="), // "urn:ietf:wg:oauth:2.0:oob", pin); Response response = TRAKT.exchangeCodeForAccessToken(pin); // get tokens String accessToken = response.body().access_token; String refreshToken = response.body().refresh_token; if (StringUtils.isNoneBlank(accessToken, refreshToken)) { result.put("accessToken", accessToken); result.put("refreshToken", refreshToken); } return result; } /** * get a new accessToken with the refreshToken */ public static void refreshAccessToken() throws Exception { if (StringUtils.isBlank(Globals.settings.getTraktRefreshToken())) { throw new Exception("not trakt.tv refresh token found"); } // OAuthAccessTokenResponse response = TraktV2.refreshAccessToken(CLIENT_ID, // ApiKey.decryptApikey("VD2h4jmnrrYWnP1Nk49UtTNRILiWsuelJKdza7DAw+ROh1wtVf2U6PQScm7QWCOTsxN0K3QluIykKs2ZT1af1GcPz1401005bDBDss1Pz2c="), // "urn:ietf:wg:oauth:2.0:oob", Globals.settings.getTraktRefreshToken()); Response response = TRAKT.refreshToken(Globals.settings.getTraktRefreshToken()) .refreshAccessToken(Globals.settings.getTraktRefreshToken()); if (StringUtils.isNoneBlank(response.body().access_token, response.body().refresh_token)) { Globals.settings.setTraktAccessToken(response.body().access_token); Globals.settings.setTraktRefreshToken(response.body().refresh_token); TRAKT.accessToken(Globals.settings.getTraktAccessToken()); } } /** * do we have values for user/pass/api and are we a donator?! * * @return true/false if trakt could be called */ private boolean isEnabled() { if (StringUtils.isNoneBlank(Globals.settings.getTraktAccessToken(), Globals.settings.getTraktRefreshToken())) { // everything seems fine; also set the access token TRAKT.accessToken(Globals.settings.getTraktAccessToken()); return true; } return false; } // @formatter:off // ███╗ ███╗ ██████╗ ██╗ ██╗██╗███████╗███████╗ // ████╗ ████║██╔═══██╗██║ ██║██║██╔════╝██╔════╝ // ██╔████╔██║██║ ██║██║ ██║██║█████╗ ███████╗ // ██║╚██╔╝██║██║ ██║╚██╗ ██╔╝██║██╔══╝ ╚════██║ // ██║ ╚═╝ ██║╚██████╔╝ ╚████╔╝ ██║███████╗███████║ // ╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚═╝╚══════╝╚══════╝ // @formatter:on /** * Syncs Trakt.tv collection (specified movies)
* Gets all Trakt movies from collection, matches them to ours, and sends ONLY the new ones back to Trakt */ public void syncTraktMovieCollection(List moviesInTmm) { if (!isEnabled()) { return; } // create a local copy of the list List tmmMovies = new ArrayList<>(moviesInTmm); // ***************************************************************************** // 1) get diff of TMM <-> Trakt collection // ***************************************************************************** LOGGER.info("got up to " + tmmMovies.size() + " movies for Trakt.tv collection sync"); // get ALL Trakt movies in collection List traktMovies; try { // Extended.DEFAULT adds url, poster, fanart, banner, genres // Extended.MAX adds certs, runtime, and other stuff (useful for scraper!) Response> response = TRAKT.sync().collectionMovies(null).execute(); if (!response.isSuccessful() && response.code() == 401) { // try to re-auth refreshAccessToken(); response = TRAKT.sync().collectionMovies(null).execute(); } if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } traktMovies = response.body(); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } LOGGER.info("You have " + traktMovies.size() + " movies in your Trakt.tv collection"); // loop over all movies on trakt for (BaseMovie traktMovie : traktMovies) { // loop over TMM movies, and check if IMDBID match for (int i = tmmMovies.size() - 1; i >= 0; i--) { Movie tmmMovie = tmmMovies.get(i); if (matches(tmmMovie, traktMovie.movie.ids)) { // we have a movie match // update missing IDs (we get them for free :) boolean dirty = updateIDs(tmmMovie, traktMovie.movie.ids); if (traktMovie.collected_at != null) { Date collectedAt = DateTimeUtils.toDate(traktMovie.collected_at.toInstant()); { if (!collectedAt.equals(tmmMovie.getDateAdded())) // always set from trakt, if not matched (Trakt = master) LOGGER.trace( "Marking movie '" + tmmMovie.getTitle() + "' as collected on " + collectedAt + " (was " + tmmMovie.getDateAddedAsString() + ")"); tmmMovie.setDateAdded(collectedAt); dirty = true; } } if (dirty) { tmmMovie.writeNFO(); tmmMovie.saveToDb(); } // remove it from our list (no need to add) tmmMovies.remove(i); } } } if (tmmMovies.size() == 0) { LOGGER.info("Already up-to-date - no need to add anything :)"); return; } // ***************************************************************************** // 2) add remaining TMM movies to Trakt collection // ***************************************************************************** LOGGER.debug("prepare " + tmmMovies.size() + " movies for Trakt.tv collection sync"); List movies = new ArrayList<>(); int nosync = 0; for (Movie tmmMovie : tmmMovies) { if (tmmMovie.getIdAsInt(providerInfo.getId()) != 0 || !tmmMovie.getIdAsString(Constants.IMDB).isEmpty() || tmmMovie.getIdAsInt(Constants.TMDB) != 0) { movies.add(toSyncMovie(tmmMovie, false)); } else { // do not add to Trakt if we do not have at least one ID nosync++; } } if (nosync > 0) { LOGGER.debug("skipping " + nosync + " movies, because they have not been scraped yet!"); } if (movies.size() == 0) { LOGGER.info("no new movies for Trakt collection sync found."); return; } try { LOGGER.info("Adding " + movies.size() + " movies to Trakt.tv collection"); SyncItems items = new SyncItems().movies(movies); Response response = TRAKT.sync().addItemsToCollection(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.info("Trakt add-to-library status:"); printStatus(response.body()); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); } } /** * Syncs Trakt.tv collection (all movies you have)
* Gets all Trakt movies from collection, matches them to ours, and sends ONLY the new ones back to Trakt */ public void syncTraktMovieCollection() { if (!isEnabled()) { return; } syncTraktMovieCollection(new ArrayList<>(MovieList.getInstance().getMovies())); } /** * clears the whole Trakt.tv movie collection. Gets all Trakt.tv movies from your collection and removes them from the collection and the watched * state; a little helper to initialize the collection */ public void clearTraktMovies() { // ***************************************************************************** // 1) get ALL Trakt movies in collection / watched // ***************************************************************************** List traktCollection; List traktWatched; try { // collection Response> traktCollectionResponse = TRAKT.sync().collectionMovies(null).execute(); if (!traktCollectionResponse.isSuccessful() && traktCollectionResponse.code() == 401) { // try to re-auth refreshAccessToken(); traktCollectionResponse = TRAKT.sync().collectionMovies(null).execute(); } if (!traktCollectionResponse.isSuccessful()) { LOGGER.error("failed syncing trakt: " + traktCollectionResponse.message()); return; } traktCollection = traktCollectionResponse.body(); // watched Response> traktWatchedResponse = TRAKT.sync().watchedMovies(null).execute(); if (!traktWatchedResponse.isSuccessful() && traktWatchedResponse.code() == 401) { // try to re-auth refreshAccessToken(); traktWatchedResponse = TRAKT.sync().watchedMovies(null).execute(); } if (!traktWatchedResponse.isSuccessful()) { LOGGER.error("failed syncing trakt: " + traktWatchedResponse.message()); return; } traktWatched = traktWatchedResponse.body(); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } LOGGER.info("You have " + traktCollection.size() + " movies in your Trakt.tv collection"); LOGGER.info("You have " + traktWatched.size() + " movies watched"); // ***************************************************************************** // 2) remove every movie from the COLLECTION state // ***************************************************************************** List movieToRemove = new ArrayList<>(); for (BaseMovie traktMovie : traktCollection) { movieToRemove.add(toSyncMovie(traktMovie)); } if (!movieToRemove.isEmpty()) { try { SyncItems items = new SyncItems().movies(movieToRemove); Response response = TRAKT.sync().deleteItemsFromCollection(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.info("removed " + movieToRemove.size() + " movies from your trakt.tv collection"); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } } // ***************************************************************************** // 3) remove every movie from the WATCHED state // ***************************************************************************** movieToRemove.clear(); for (BaseMovie traktMovie : traktWatched) { movieToRemove.add(toSyncMovie(traktMovie)); } if (!movieToRemove.isEmpty()) { try { SyncItems items = new SyncItems().movies(movieToRemove); Response response = TRAKT.sync().deleteItemsFromWatchedHistory(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.info("removed " + movieToRemove.size() + " movies from your trakt.tv watched"); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); } } } /** * Syncs Trakt.tv "seen" flag (all gives you have already marked as watched)
* Gets all watched movies from Trakt, and sets the "watched" flag on TMM movies.
* Then update the remaining TMM movies on Trakt as 'seen'. */ public void syncTraktMovieWatched(List moviesInTmm) { if (!isEnabled()) { return; } // create a local copy of the list List tmmMovies = new ArrayList<>(moviesInTmm); // ***************************************************************************** // 1) get all Trakt watched movies and update our "watched" status // ***************************************************************************** List traktMovies; try { // Extended.DEFAULT adds url, poster, fanart, banner, genres // Extended.MAX adds certs, runtime, and other stuff (useful for scraper!) Response> traktWatchedResponse = TRAKT.sync().watchedMovies(null).execute(); if (!traktWatchedResponse.isSuccessful() && traktWatchedResponse.code() == 401) { // try to re-auth refreshAccessToken(); traktWatchedResponse = TRAKT.sync().watchedMovies(null).execute(); } if (!traktWatchedResponse.isSuccessful()) { LOGGER.error("failed syncing trakt: " + traktWatchedResponse.message()); return; } traktMovies = traktWatchedResponse.body(); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } LOGGER.info("You have " + traktMovies.size() + " movies marked as 'watched' in your Trakt.tv collection"); // loop over all watched movies on trakt for (BaseMovie traktWatched : traktMovies) { // loop over TMM movies, and check if IMDBID match for (Movie tmmMovie : tmmMovies) { if (matches(tmmMovie, traktWatched.movie.ids)) { // we have a movie match // update missing IDs (we get them for free :) boolean dirty = updateIDs(tmmMovie, traktWatched.movie.ids); if (!tmmMovie.isWatched()) { // save Trakt watched status LOGGER.info("Marking movie '" + tmmMovie.getTitle() + "' as watched"); tmmMovie.setWatched(true); dirty = true; } if (traktWatched.last_watched_at != null) { Date lastWatchedAt = DateTimeUtils.toDate(traktWatched.last_watched_at.toInstant()); if (!lastWatchedAt.equals(tmmMovie.getLastWatched())) { // always set from trakt, if not matched (Trakt = master) LOGGER.trace("Marking movie '" + tmmMovie.getTitle() + "' as watched on " + lastWatchedAt + " (was " + tmmMovie.getLastWatched() + ")"); tmmMovie.setLastWatched(lastWatchedAt); // dirty = true; // we do not write date to NFO. But just mark for syncing back... } } if (dirty) { tmmMovie.writeNFO(); tmmMovie.saveToDb(); } } } } // ***************************************************************************** // 2) mark additionally "watched" movies as 'seen' on Trakt // ***************************************************************************** // Now get all TMM watched movies... List tmmWatchedMovies = new ArrayList<>(); for (Movie movie : tmmMovies) { if (movie.isWatched()) { tmmWatchedMovies.add(movie); } } LOGGER.info("You have now " + tmmWatchedMovies.size() + " movies marked as 'watched' in your TMM database"); // ...and subtract the already watched from Trakt for (int i = tmmWatchedMovies.size() - 1; i >= 0; i--) { for (BaseMovie traktWatched : traktMovies) { Movie tmmMovie = tmmWatchedMovies.get(i); if (matches(tmmMovie, traktWatched.movie.ids)) { tmmWatchedMovies.remove(i); break; } } } if (tmmWatchedMovies.size() == 0) { LOGGER.info("no new watched movies for Trakt sync found."); return; } LOGGER.debug("prepare " + tmmWatchedMovies.size() + " movies for Trakt.tv sync"); List movies = new ArrayList<>(); int nosync = 0; for (Movie tmmMovie : tmmWatchedMovies) { if (tmmMovie.getIdAsInt(providerInfo.getId()) != 0 || !tmmMovie.getIdAsString(Constants.IMDB).isEmpty() || tmmMovie.getIdAsInt(Constants.TMDB) != 0) { movies.add(toSyncMovie(tmmMovie, true)); } else { // do not add to Trakt if we do not have at least one ID nosync++; } } if (nosync > 0) { LOGGER.debug("skipping " + nosync + " movies, because they have not been scraped yet!"); } if (movies.size() == 0) { LOGGER.info("no new watched movies for Trakt sync found."); return; } try { LOGGER.info("Marking " + movies.size() + " movies as 'watched' to Trakt.tv collection"); SyncItems items = new SyncItems().movies(movies); Response response = TRAKT.sync().addItemsToWatchedHistory(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.info("Trakt mark-as-watched status:"); printStatus(response.body()); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); } } /** * Syncs Trakt.tv "seen" flag (all movies you have already marked as watched)
* Gets all watched movies from Trakt, and sets the "watched" flag on TMM movies.
* Then update the remaining TMM movies on Trakt as 'seen'. */ public void syncTraktMovieWatched() { if (!isEnabled()) { return; } syncTraktMovieWatched(MovieList.getInstance().getMovies()); } // @formatter:off // ████████╗██╗ ██╗███████╗██╗ ██╗ ██████╗ ██╗ ██╗███████╗ // ╚══██╔══╝██║ ██║██╔════╝██║ ██║██╔═══██╗██║ ██║██╔════╝ // ██║ ██║ ██║███████╗███████║██║ ██║██║ █╗ ██║███████╗ // ██║ ╚██╗ ██╔╝╚════██║██╔══██║██║ ██║██║███╗██║╚════██║ // ██║ ╚████╔╝ ███████║██║ ██║╚██████╔╝╚███╔███╔╝███████║ // ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝ // @formatter:on /** * Syncs Trakt.tv collection (gets all IDs & dates, and adds all TMM shows to Trakt)
* Do not send diffs, since this is too complicated currently :| */ public void syncTraktTvShowCollection(List tvShowsInTmm) { if (!isEnabled()) { return; } // create a local copy of the list List tvShows = new ArrayList<>(tvShowsInTmm); // ***************************************************************************** // 1) sync ALL missing show IDs & dates from trakt // ***************************************************************************** List traktShows; try { // Extended.DEFAULT adds url, poster, fanart, banner, genres // Extended.MAX adds certs, runtime, and other stuff (useful for scraper!) Response> response = TRAKT.sync().collectionShows(null).execute(); if (!response.isSuccessful() && response.code() == 401) { // try to re-auth refreshAccessToken(); response = TRAKT.sync().collectionShows(null).execute(); } if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } traktShows = response.body(); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } LOGGER.info("You have " + traktShows.size() + " TvShows in your Trakt.tv collection"); // remember which episodes are already in trakt Set episodesInTrakt = new HashSet<>(); for (BaseShow traktShow : traktShows) { for (TvShow tmmShow : tvShows) { if (matches(tmmShow, traktShow.show.ids)) { // ok, we have a show match // update show IDs from trakt boolean dirty = updateIDs(tmmShow, traktShow.show.ids); // update collection date from trakt (show) if (traktShow.last_collected_at != null) { Date collectedAt = DateTimeUtils.toDate(traktShow.last_collected_at.toInstant()); if (!collectedAt.equals(tmmShow.getDateAdded())) { // always set from trakt, if not matched (Trakt = master) LOGGER.trace( "Marking TvShow '" + tmmShow.getTitle() + "' as collected on " + collectedAt + " (was " + tmmShow.getDateAddedAsString() + ")"); tmmShow.setDateAdded(collectedAt); dirty = true; } } // update collection date from trakt (episodes) for (BaseSeason bs : traktShow.seasons) { for (BaseEpisode be : bs.episodes) { TvShowEpisode tmmEP = tmmShow.getEpisode(bs.number, be.number); if (tmmEP == null) { continue; } episodesInTrakt.add(tmmEP); // update ep IDs - NOT YET POSSIBLE // boolean epDirty = updateIDs(tmmEP, be.ids); if (be.collected_at != null) { Date collectedAt = DateTimeUtils.toDate(be.collected_at.toInstant()); if (!collectedAt.equals(tmmEP.getDateAdded())) { tmmEP.setDateAdded(collectedAt); tmmEP.writeNFO(); tmmEP.saveToDb(); // epDirty = true; } } } } if (dirty) { tmmShow.writeNFO(); tmmShow.saveToDb(); } } } } // ***************************************************************************** // 2) add all our shows to Trakt collection (we have the physical file) // ***************************************************************************** LOGGER.info("Adding " + tvShows.size() + " TvShows to Trakt.tv collection"); // send show per show; sending all together may result too often in a timeout for (TvShow tvShow : tvShows) { SyncShow show = toSyncShow(tvShow, false, episodesInTrakt); if (show == null) { continue; } try { SyncItems items = new SyncItems().shows(show); Response response = TRAKT.sync().addItemsToCollection(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.debug("Trakt add-to-library status: " + tvShow.getTitle()); printStatus(response.body()); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } } } /** * Syncs Trakt.tv collection (all TvShows you have)
* Gets all Trakt shows/episodes from collection, matches them to ours, and sends ONLY the new ones back to Trakt */ public void syncTraktTvShowCollection() { if (!isEnabled()) { return; } syncTraktTvShowCollection(new ArrayList<>(TvShowList.getInstance().getTvShows())); } public void syncTraktTvShowWatched(List tvShowsInTmm) { if (!isEnabled()) { return; } // create a local copy of the list List tvShows = new ArrayList<>(tvShowsInTmm); List traktShows; try { // Extended.DEFAULT adds url, poster, fanart, banner, genres // Extended.MAX adds certs, runtime, and other stuff (useful for scraper!) Response> response = TRAKT.sync().watchedShows(null).execute(); if (!response.isSuccessful() && response.code() == 401) { // try to re-auth refreshAccessToken(); response = TRAKT.sync().watchedShows(null).execute(); } if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } traktShows = response.body(); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } LOGGER.info("You have " + traktShows.size() + " TvShows marked as watched on Trakt.tv"); for (BaseShow traktShow : traktShows) { for (TvShow tmmShow : tvShows) { if (matches(tmmShow, traktShow.show.ids)) { // ok, we have a show match // update show IDs from trakt boolean dirty = updateIDs(tmmShow, traktShow.show.ids); // update watched date from trakt (show) if (traktShow.last_watched_at != null) { Date lastWatchedAt = DateTimeUtils.toDate(traktShow.last_watched_at.toInstant()); if (!lastWatchedAt.equals(tmmShow.getLastWatched())) { // always set from trakt, if not matched (Trakt = master) LOGGER.trace("Marking TvShow '" + tmmShow.getTitle() + "' as watched on " + lastWatchedAt + " (was " + tmmShow.getLastWatched() + ")"); tmmShow.setLastWatched(lastWatchedAt); // dirty = true; // we do not write date to NFO. But just mark for syncing back... } } // update collection date from trakt (episodes) for (BaseSeason bs : traktShow.seasons) { for (BaseEpisode be : bs.episodes) { TvShowEpisode tmmEP = tmmShow.getEpisode(bs.number, be.number); if (tmmEP == null) { continue; } // update ep IDs - NOT YET POSSIBLE // boolean epDirty = updateIDs(tmmEP, be.ids); if (!tmmEP.isWatched()) { tmmEP.setWatched(true); tmmEP.writeNFO(); tmmEP.saveToDb(); } if (be.last_watched_at != null) { Date lastWatchedAt = DateTimeUtils.toDate(be.last_watched_at.toInstant()); if (!lastWatchedAt.equals(tmmEP.getLastWatched())) { tmmEP.setLastWatched(lastWatchedAt); } } } } if (dirty) { tmmShow.writeNFO(); tmmShow.saveToDb(); } } } } // ***************************************************************************** // 2) add all our shows to Trakt watched // ***************************************************************************** LOGGER.info("Adding up to " + tvShows.size() + " TvShows as watched on Trakt.tv"); // send show per show; sending all together may result too often in a timeout for (TvShow show : tvShows) { // get items to sync SyncShow sync = toSyncShow(show, true, new HashSet<>()); if (sync == null) { continue; } try { SyncItems items = new SyncItems().shows(sync); Response response = TRAKT.sync().addItemsToWatchedHistory(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.debug("Trakt add-to-library status: " + show.getTitle()); printStatus(response.body()); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } } } public void syncTraktTvShowWatched() { if (!isEnabled()) { return; } syncTraktTvShowWatched(new ArrayList<>(TvShowList.getInstance().getTvShows())); } /** * clears the whole Trakt.tv movie collection. Gets all Trakt.tv movies from your collection and removes them from the collection and the watched * state; a little helper to initialize the collection */ public void clearTraktTvShows() { // ***************************************************************************** // 1) get ALL Trakt shows in collection / watched // ***************************************************************************** List traktCollection; List traktWatched; try { // collection Response> traktCollectionResponse = TRAKT.sync().collectionShows(null).execute(); if (!traktCollectionResponse.isSuccessful() && traktCollectionResponse.code() == 401) { // try to re-auth refreshAccessToken(); traktCollectionResponse = TRAKT.sync().collectionShows(null).execute(); } if (!traktCollectionResponse.isSuccessful()) { LOGGER.error("failed syncing trakt: " + traktCollectionResponse.message()); return; } traktCollection = traktCollectionResponse.body(); // watched Response> traktWatchedResponse = TRAKT.sync().watchedShows(null).execute(); if (!traktWatchedResponse.isSuccessful() && traktWatchedResponse.code() == 401) { // try to re-auth refreshAccessToken(); traktWatchedResponse = TRAKT.sync().watchedShows(null).execute(); } if (!traktWatchedResponse.isSuccessful()) { LOGGER.error("failed syncing trakt: " + traktWatchedResponse.message()); return; } traktWatched = traktWatchedResponse.body(); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } LOGGER.info("You have " + traktCollection.size() + " shows in your Trakt.tv collection"); LOGGER.info("You have " + traktWatched.size() + " shows watched"); // ***************************************************************************** // 2) remove every shows from the COLLECTION state // ***************************************************************************** List showToRemove = new ArrayList<>(); for (BaseShow traktShow : traktCollection) { showToRemove.add(toSyncShow(traktShow)); } if (!showToRemove.isEmpty()) { try { SyncItems items = new SyncItems().shows(showToRemove); Response response = TRAKT.sync().deleteItemsFromCollection(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.debug("removed " + showToRemove.size() + " shows from your trakt.tv collection"); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); return; } } // ***************************************************************************** // 3) remove every shows from the WATCHED state // ***************************************************************************** showToRemove.clear(); for (BaseShow traktShow : traktWatched) { showToRemove.add(toSyncShow(traktShow)); } if (!showToRemove.isEmpty()) { try { SyncItems items = new SyncItems().shows(showToRemove); Response response = TRAKT.sync().deleteItemsFromWatchedHistory(items).execute(); if (!response.isSuccessful()) { LOGGER.error("failed syncing trakt: " + response.message()); return; } LOGGER.debug("removed " + showToRemove.size() + " shows from your trakt.tv watched"); } catch (Exception e) { LOGGER.error("failed syncing trakt: " + e.getMessage()); } } } // @formatter:off // ██╗ ██╗████████╗██╗██╗ ███████╗ // ██║ ██║╚══██╔══╝██║██║ ██╔════╝ // ██║ ██║ ██║ ██║██║ ███████╗ // ██║ ██║ ██║ ██║██║ ╚════██║ // ╚██████╔╝ ██║ ██║███████╗███████║ // ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ // @formatter:on private boolean updateIDs(TvShow tmmShow, ShowIds ids) { boolean dirty = false; if (tmmShow.getIdAsString(Constants.IMDB).isEmpty() && !StringUtils.isEmpty(ids.imdb)) { tmmShow.setId(Constants.IMDB, ids.imdb); dirty = true; } if (tmmShow.getIdAsInt(Constants.TMDB) == 0 && ids.tmdb != null && ids.tmdb != 0) { tmmShow.setId(Constants.TMDB, ids.tmdb); dirty = true; } if (tmmShow.getIdAsInt(providerInfo.getId()) == 0 && ids.trakt != null && ids.trakt != 0) { tmmShow.setId(providerInfo.getId(), ids.trakt); dirty = true; } if (tmmShow.getIdAsInt(Constants.TVDB) == 0 && ids.tvdb != null && ids.tvdb != 0) { tmmShow.setId(Constants.TVDB, ids.tvdb); dirty = true; } // not used atm // if (tmmShow.getIdAsInt(Constants.TVRAGEID) == 0 && ids.tvrage != null && ids.tvrage != 0) { // tmmShow.setId(Constants.TVRAGEID, ids.tvrage); // dirty = true; // } return dirty; } private boolean updateIDs(Movie tmmMovie, MovieIds ids) { boolean dirty = false; if (tmmMovie.getIdAsString(Constants.IMDB).isEmpty() && !StringUtils.isEmpty(ids.imdb)) { tmmMovie.setId(Constants.IMDB, ids.imdb); dirty = true; } if (tmmMovie.getIdAsInt(Constants.TMDB) == 0 && ids.tmdb != null && ids.tmdb != 0) { tmmMovie.setId(Constants.TMDB, ids.tmdb); dirty = true; } if (tmmMovie.getIdAsInt(providerInfo.getId()) == 0 && ids.trakt != null && ids.trakt != 0) { tmmMovie.setId(providerInfo.getId(), ids.trakt); dirty = true; } return dirty; } private boolean matches(TvShow tmmShow, ShowIds ids) { if (ids.trakt != null && ids.trakt != 0 && ids.trakt == tmmShow.getIdAsInt(providerInfo.getId())) { return true; } if (StringUtils.isNotEmpty(ids.imdb) && ids.imdb.equals(tmmShow.getIdAsString(Constants.IMDB))) { return true; } if (ids.tmdb != null && ids.tmdb != 0 && ids.tmdb == tmmShow.getIdAsInt(Constants.TMDB)) { return true; } if (ids.tvdb != null && ids.tvdb != 0 && ids.tvdb == tmmShow.getIdAsInt(Constants.TVDB)) { return true; } // not used atm // if (ids.tvrage != null && ids.tvrage != 0 && ids.tvrage == tmmShow.getIdAsInt(Constants.TVRAGEID)) { // return true; // } return false; } private boolean matches(Movie tmmMovie, MovieIds ids) { if (ids.trakt != null && ids.trakt != 0 && ids.trakt == tmmMovie.getIdAsInt(providerInfo.getId())) { return true; } if (StringUtils.isNotEmpty(ids.imdb) && ids.imdb.equals(tmmMovie.getIdAsString(Constants.IMDB))) { return true; } if (ids.tmdb != null && ids.tmdb != 0 && ids.tmdb == tmmMovie.getIdAsInt(Constants.TMDB)) { return true; } return false; } private SyncMovie toSyncMovie(Movie tmmMovie, boolean watched) { boolean hasId = false; SyncMovie movie = null; MovieIds ids = new MovieIds(); if (!tmmMovie.getIdAsString(Constants.IMDB).isEmpty()) { ids.imdb = tmmMovie.getIdAsString(Constants.IMDB); hasId = true; } if (tmmMovie.getIdAsInt(Constants.TMDB) != 0) { ids.tmdb = tmmMovie.getIdAsInt(Constants.TMDB); hasId = true; } if (tmmMovie.getIdAsInt(providerInfo.getId()) != 0) { ids.trakt = tmmMovie.getIdAsInt(providerInfo.getId()); hasId = true; } if (!hasId) { return movie; } // we have to decide what we send; trakt behaves differently when sending data to sync collection and sync history. if (watched) { // sync history if (tmmMovie.isWatched() && tmmMovie.getLastWatched() == null) { // watched in tmm and not in trakt -> sync OffsetDateTime watchedAt = OffsetDateTime.ofInstant(DateTimeUtils.toInstant(new Date()), ZoneId.systemDefault()); movie = new SyncMovie().id(ids).watchedAt(watchedAt); } } else { // sync collection OffsetDateTime collectedAt = OffsetDateTime.ofInstant(DateTimeUtils.toInstant(tmmMovie.getDateAdded()), ZoneId.systemDefault()); movie = new SyncMovie().id(ids).collectedAt(collectedAt); } return movie; } private SyncMovie toSyncMovie(BaseMovie baseMovie) { return new SyncMovie().id(baseMovie.movie.ids).collectedAt(baseMovie.collected_at).watchedAt(baseMovie.last_watched_at); } private SyncShow toSyncShow(TvShow tmmShow, boolean watched, Set episodesInTrakt) { boolean hasId = false; SyncShow show = null; ShowIds ids = new ShowIds(); if (!tmmShow.getIdAsString(Constants.IMDB).isEmpty()) { ids.imdb = tmmShow.getIdAsString(Constants.IMDB); hasId = true; } if (tmmShow.getIdAsInt(Constants.TMDB) != 0) { ids.tmdb = tmmShow.getIdAsInt(Constants.TMDB); hasId = true; } if (tmmShow.getIdAsInt(Constants.TVDB) != 0) { ids.tvdb = tmmShow.getIdAsInt(Constants.TVDB); hasId = true; } if (tmmShow.getIdAsInt(providerInfo.getId()) != 0) { ids.trakt = tmmShow.getIdAsInt(providerInfo.getId()); hasId = true; } // not used atm // if (tmmShow.getIdAsInt(Constants.TVRAGEID) != 0) { // ids.tvrage = tmmShow.getIdAsInt(Constants.TVRAGEID); // hasId = true; // } if (!hasId) { return show; } ArrayList ss = new ArrayList<>(); boolean foundS = false; for (TvShowSeason tmmSeason : tmmShow.getSeasons()) { boolean foundEP = false; ArrayList se = new ArrayList<>(); for (TvShowEpisode tmmEp : tmmSeason.getEpisodes()) { // we have to decide what we send; trakt behaves differenty when sending data to // sync collection and sync history. if (watched) { // sync history if (tmmEp.isWatched() && tmmEp.getLastWatched() == null) { // watched in tmm and not in trakt -> sync OffsetDateTime watchedAt = OffsetDateTime.ofInstant(DateTimeUtils.toInstant(new Date()), ZoneId.systemDefault()); se.add(new SyncEpisode().number(tmmEp.getEpisode()).watchedAt(watchedAt)); foundEP = true; } } else { // sync collection if (!episodesInTrakt.contains(tmmEp)) { OffsetDateTime collectedAt = OffsetDateTime.ofInstant(DateTimeUtils.toInstant(tmmEp.getDateAdded()), ZoneId.systemDefault()); se.add(new SyncEpisode().number(tmmEp.getEpisode()).collectedAt(collectedAt)); foundEP = true; } } } if (foundEP) { // do not send empty seasons foundS = true; ss.add(new SyncSeason().number(tmmSeason.getSeason()).episodes(se)); } } if (foundS) { // we have at least one season/episode, so add it OffsetDateTime collectedAt = OffsetDateTime.ofInstant(DateTimeUtils.toInstant(tmmShow.getDateAdded()), ZoneId.systemDefault()); show = new SyncShow().id(ids).collectedAt(collectedAt).seasons(ss); } // if nothing added, do NOT send an empty show (to add all) return show; } private SyncShow toSyncShow(BaseShow baseShow) { // TODO: used only on clear() - so we don't need the episodes? TBC ArrayList ss = new ArrayList<>(); for (BaseSeason baseSeason : baseShow.seasons) { ArrayList se = new ArrayList<>(); for (BaseEpisode baseEp : baseSeason.episodes) { se.add(new SyncEpisode().number(baseEp.number).collectedAt(baseEp.collected_at).watchedAt(baseEp.collected_at)); } ss.add(new SyncSeason().number(baseSeason.number).episodes(se)); } return new SyncShow().id(baseShow.show.ids).collectedAt(baseShow.last_collected_at).watchedAt(baseShow.last_watched_at).seasons(ss); } /** * prints some trakt response status * * @param resp * the response */ private void printStatus(SyncResponse resp) { if (resp != null) { String info = getStatusString(resp.added); if (!info.isEmpty()) { LOGGER.debug("Added : " + info); } info = getStatusString(resp.existing); if (!info.isEmpty()) { LOGGER.debug("Existing : " + info); } info = getStatusString(resp.deleted); if (!info.isEmpty()) { LOGGER.debug("Deleted : " + info); } info = getStatusString(resp.not_found); if (!info.isEmpty()) { LOGGER.debug("Errors : " + info); } } } private String getStatusString(SyncStats ss) { if (ss == null) { return ""; } StringBuilder sb = new StringBuilder(50); if (ss.movies != null && ss.movies > 0) { sb.append(ss.movies + " Movies "); } if (ss.shows != null && ss.shows > 0) { sb.append(ss.shows + " Shows "); } if (ss.seasons != null && ss.seasons > 0) { sb.append(ss.seasons + " Seasons "); } if (ss.episodes != null && ss.episodes > 0) { sb.append(ss.episodes + " Episodes"); } return sb.toString(); } private String getStatusString(SyncErrors ss) { if (ss == null) { return ""; } StringBuilder sb = new StringBuilder(50); // TODO: iterate over error array and display which did not work if (ss.movies != null && ss.movies.size() > 0) { sb.append(ss.movies.size() + " Movies "); } if (ss.shows != null && ss.shows.size() > 0) { sb.append(ss.shows.size() + " Shows "); } if (ss.seasons != null && ss.seasons.size() > 0) { sb.append(ss.seasons.size() + " Seasons "); } if (ss.episodes != null && ss.episodes.size() > 0) { sb.append(ss.episodes.size() + " Episodes"); } return sb.toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy