/*
   * detect which mediafiles has to be parsed and start a thread to do that
   */
  private void gatherMediaInformationForUngatheredMediaFiles(TvShow tvShow) {
    // get mediainfo for tv show (fanart/poster..)
    ArrayList<MediaFile> ungatheredMediaFiles = new ArrayList<MediaFile>();
    for (MediaFile mf : tvShow.getMediaFiles()) {
      if (StringUtils.isBlank(mf.getContainerFormat())) {
        ungatheredMediaFiles.add(mf);
      }
    }

    if (ungatheredMediaFiles.size() > 0) {
      submitTask(new MediaFileInformationFetcherTask(ungatheredMediaFiles, tvShow, false));
    }

    // get mediainfo for all episodes within this tv show
    for (TvShowEpisode episode : new ArrayList<TvShowEpisode>(tvShow.getEpisodes())) {
      ungatheredMediaFiles = new ArrayList<MediaFile>();
      for (MediaFile mf : episode.getMediaFiles()) {
        if (StringUtils.isBlank(mf.getContainerFormat())) {
          ungatheredMediaFiles.add(mf);
        }
      }

      if (ungatheredMediaFiles.size() > 0) {
        submitTask(new MediaFileInformationFetcherTask(ungatheredMediaFiles, episode, false));
      }
    }
  }
  private void cleanup(TvShow tvShow) {
    boolean dirty = false;
    if (!tvShow.isNewlyAdded()) {
      // check and delete all not found MediaFiles
      List<MediaFile> mediaFiles = new ArrayList<MediaFile>(tvShow.getMediaFiles());
      for (MediaFile mf : mediaFiles) {
        if (!mf.getFile().exists()) {
          tvShow.removeFromMediaFiles(mf);
          dirty = true;
        }
      }
      List<TvShowEpisode> episodes = new ArrayList<TvShowEpisode>(tvShow.getEpisodes());
      for (TvShowEpisode episode : episodes) {
        mediaFiles = new ArrayList<MediaFile>(episode.getMediaFiles());
        for (MediaFile mf : mediaFiles) {
          if (!mf.getFile().exists()) {
            episode.removeFromMediaFiles(mf);
            dirty = true;
          }
        }
        // lets have a look if there is at least one video file for this episode
        List<MediaFile> mfs = episode.getMediaFiles(MediaFileType.VIDEO);
        if (mfs.size() == 0) {
          tvShow.removeEpisode(episode);
          dirty = true;
        }
      }
    }

    if (dirty) {
      tvShow.saveToDb();
    }
  }
  @Override
  public void doInBackground() {
    // check if there is at least one DS to update
    Utils.removeEmptyStringsFromList(dataSources);
    if (dataSources.isEmpty() && tvShowFolders.isEmpty()) {
      LOGGER.info("no datasource to update");
      MessageManager.instance.pushMessage(
          new Message(MessageLevel.ERROR, "update.datasource", "update.datasource.nonespecified"));
      return;
    }

    try {
      long start = System.currentTimeMillis();
      start();

      // cleanup newlyadded for a new UDS run
      for (TvShow tvShow : tvShowList.getTvShows()) {
        for (TvShowEpisode episode : tvShow.getEpisodes()) {
          episode.setNewlyAdded(false);
        }
        tvShow.setNewlyAdded(false);
      }

      // here we have 2 ways of updating:
      // - per datasource -> update ds / remove orphaned / update MFs
      // - per TV show -> udpate TV show / update MFs
      if (tvShowFolders.size() == 0) {
        // update ds
        updateDatasource();
      } else {
        // update TV show
        updateTvShows();
      }
      long end = System.currentTimeMillis();
      LOGGER.info("Done updating datasource :) - took " + Utils.MSECtoHHMMSS(end - start));
    } catch (Exception e) {
      LOGGER.error("Thread crashed", e);
      MessageManager.instance.pushMessage(
          new Message(MessageLevel.ERROR, "update.datasource", "message.update.threadcrashed"));
    }
  }
  /**
   * renames the TvSHow root folder and updates all mediaFiles
   *
   * @param show the show
   */
  public static void renameTvShowRoot(TvShow show) {
    LOGGER.debug("TV show year: " + show.getYear());
    LOGGER.debug("TV show path: " + show.getPath());
    String newPathname = generateTvShowDir(SETTINGS.getRenamerTvShowFoldername(), show);
    String oldPathname = show.getPath();

    if (!newPathname.isEmpty()) {
      // newPathname = show.getDataSource() + File.separator + newPathname;
      File srcDir = new File(oldPathname);
      File destDir = new File(newPathname);
      // move directory if needed
      // if (!srcDir.equals(destDir)) {
      if (!srcDir.getAbsolutePath().equals(destDir.getAbsolutePath())) {
        try {
          // FileUtils.moveDirectory(srcDir, destDir);
          boolean ok = Utils.moveDirectorySafe(srcDir, destDir);
          if (ok) {
            show.updateMediaFilePath(srcDir, destDir); // TvShow MFs
            show.setPath(newPathname);
            for (TvShowEpisode episode : new ArrayList<TvShowEpisode>(show.getEpisodes())) {
              episode.replacePathForRenamedFolder(srcDir, destDir);
              episode.updateMediaFilePath(srcDir, destDir);
            }
            show.saveToDb();
            show.writeNFO();
          }
        } catch (Exception e) {
          LOGGER.error("error moving folder: ", e.getMessage());
          MessageManager.instance.pushMessage(
              new Message(
                  MessageLevel.ERROR,
                  srcDir.getPath(),
                  "message.renamer.failedrename",
                  new String[] {":", e.getLocalizedMessage()}));
        }
      }
    }
  }
  /**
   * Rename Episode (PLUS all Episodes having the same MediaFile!!!).
   *
   * @param episode the Episode
   */
  public static void renameEpisode(TvShowEpisode episode) {
    // test for valid season/episode number
    if (episode.getSeason() < 0 || episode.getEpisode() < 0) {
      LOGGER.warn(
          "failed to rename episode "
              + episode.getTitle()
              + " (TV show "
              + episode.getTvShow().getTitle()
              + ") - invalid season/episode number");
      MessageManager.instance.pushMessage(
          new Message(
              MessageLevel.ERROR,
              episode.getTvShow().getTitle(),
              "tvshow.renamer.failedrename",
              new String[] {episode.getTitle()}));
      return;
    }

    LOGGER.info(
        "Renaming TvShow '" + episode.getTvShow().getTitle() + "' Episode " + episode.getEpisode());
    for (MediaFile mf : new ArrayList<MediaFile>(episode.getMediaFiles())) {
      renameMediaFile(mf, episode.getTvShow());
    }
  }
  public static String generateSeasonDir(String template, TvShowEpisode episode) {
    String seasonDir = template;

    // replace $1 and $2 as the only episode specific tokens
    seasonDir = seasonDir.replace("$1", String.valueOf(episode.getSeason()));
    seasonDir = seasonDir.replace("$2", lz(episode.getSeason()));
    seasonDir = seasonDir.replace("$1", String.valueOf(episode.getDvdSeason()));
    seasonDir = seasonDir.replace("$2", lz(episode.getDvdSeason()));

    // replace all other tokens
    seasonDir = createDestination(seasonDir, episode.getTvShow(), new ArrayList<TvShowEpisode>());

    // only allow empty season dir if the season is in the filename
    if (StringUtils.isBlank(seasonDir)
        && !(SETTINGS.getRenamerFilename().contains("$1")
            || SETTINGS.getRenamerFilename().contains("$2")
            || SETTINGS.getRenamerFilename().contains("$3")
            || SETTINGS.getRenamerFilename().contains("$4"))) {
      seasonDir = "Season " + String.valueOf(episode.getSeason());
    }
    return seasonDir;
  }
  private SyncShow toSyncShow(TvShow tmmShow, boolean watched) {
    SyncShow show = null;
    ShowIds ids = new ShowIds();
    if (!tmmShow.getIdAsString(Constants.IMDBID).isEmpty()) {
      ids.imdb = tmmShow.getIdAsString(Constants.IMDBID);
    }
    if (tmmShow.getIdAsInt(Constants.TMDBID) != 0) {
      ids.tmdb = tmmShow.getIdAsInt(Constants.TMDBID);
    }
    if (tmmShow.getIdAsInt(Constants.TVDBID) != 0) {
      ids.tvdb = tmmShow.getIdAsInt(Constants.TVDBID);
    }
    if (tmmShow.getIdAsInt(Constants.TRAKTID) != 0) {
      ids.trakt = tmmShow.getIdAsInt(Constants.TRAKTID);
    }
    if (tmmShow.getIdAsInt(Constants.TVRAGEID) != 0) {
      ids.tvrage = tmmShow.getIdAsInt(Constants.TVRAGEID);
    }

    ArrayList<SyncSeason> ss = new ArrayList<SyncSeason>();
    boolean foundS = false;
    for (TvShowSeason tmmSeason : tmmShow.getSeasons()) {
      boolean foundEP = false;
      ArrayList<SyncEpisode> se = new ArrayList<SyncEpisode>();
      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
            se.add(
                new SyncEpisode()
                    .number(tmmEp.getEpisode())
                    .watchedAt(new DateTime(tmmEp.getLastWatched())));
            foundEP = true;
          }
        } else {
          // sync collection
          se.add(
              new SyncEpisode()
                  .number(tmmEp.getEpisode())
                  .collectedAt(new DateTime(tmmEp.getDateAdded())));
          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
      show = new SyncShow().id(ids).collectedAt(new DateTime(tmmShow.getDateAdded())).seasons(ss);
    }

    // if nothing added, do NOT send an empty show (to add all)
    return show;
  }
  public void syncTraktTvShowWatched(List<TvShow> tvShowsInTmm) {
    if (!isEnabled()) {
      return;
    }

    // create a local copy of the list
    List<TvShow> tvShows = new ArrayList<TvShow>(tvShowsInTmm);

    List<BaseShow> traktShows = new ArrayList<BaseShow>();
    try {
      traktShows = TRAKT.sync().watchedShows(Extended.DEFAULT_MIN);
    } catch (RetrofitError e) {
      handleRetrofitError(e);
      return;
    } catch (UnauthorizedException e) {
      // not authorized - maybe access token revoked - relogin
      if (this.Login()) {
        // ok, it worked, lets try once again :)
        try {
          traktShows = TRAKT.sync().watchedShows(Extended.DEFAULT_MIN);
        } catch (UnauthorizedException e1) {
          return;
        }
      } else {
        handleRetrofitError((RetrofitError) e.getCause());
        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
              && !(traktShow.last_watched_at.toDate().equals(tmmShow.getLastWatched()))) {
            // always set from trakt, if not matched (Trakt = master)
            LOGGER.trace(
                "Marking TvShow '"
                    + tmmShow.getTitle()
                    + "' as watched on "
                    + traktShow.last_watched_at.toDate()
                    + " (was "
                    + tmmShow.getLastWatched()
                    + ")");
            tmmShow.setLastWatched(traktShow.last_watched_at.toDate());
            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);
              // update ep IDs - NOT YET POSSIBLE
              // boolean dirty = updateIDs(tmmEP, be.ids);

              if (tmmEP != null
                  && be.last_watched_at != null
                  && !(be.last_watched_at.toDate().equals(tmmEP.getLastWatched()))) {
                tmmEP.setLastWatched(be.last_watched_at.toDate());
                tmmEP.setWatched(true);
                dirty = true;
              }
            }
          }

          if (dirty) {
            tmmShow.saveToDb();
          }
        }
      }
    }

    // *****************************************************************************
    // 2) add all our shows to Trakt watched
    // *****************************************************************************
    LOGGER.info("Adding " + 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);
      if (sync == null) {
        continue;
      }

      try {
        SyncItems items = new SyncItems().shows(sync);
        response = TRAKT.sync().addItemsToWatchedHistory(items);

        LOGGER.debug("Trakt add-to-library status: " + show.getTitle());
        printStatus(response);
      } catch (RetrofitError e) {
        handleRetrofitError(e);
      } catch (UnauthorizedException e) {
        handleRetrofitError((RetrofitError) e.getCause());
      }
    }
  }
  /**
   * Find additional episode files.<br>
   * adds everything which starts with "videoFile name"<br>
   * scans subs/subtitle directories aswell
   *
   * @param episode the episode
   * @param videoFile the video file
   * @param directoryContents the directory contents
   * @return indicator whether something new has been found or not
   */
  private boolean findAdditionalEpisodeFiles(
      TvShowEpisode episode, File videoFile, File[] directoryContents) {
    boolean newFileFound = false;
    List<MediaFile> existingMediaFiles = episode.getMediaFiles();

    for (File file : directoryContents) {
      if (file.isFile()) {
        MediaFile mf = new MediaFile(file);
        if (existingMediaFiles.contains(mf)) {
          continue;
        }
        if (mf.getType().equals(MediaFileType.VIDEO)
            || !mf.getBasename().startsWith(FilenameUtils.getBaseName(videoFile.getName()))
            || file.getName().startsWith(skipFilesStartingWith)) { // MacOS ignore)
          continue;
        }
        if (mf.getType() == MediaFileType.SUBTITLE) {
          episode.setSubtitles(true);
        }
        // check if it is a poster
        if (mf.getType() == MediaFileType.GRAPHIC) {
          LOGGER.debug("parsing unknown graphic " + mf.getFilename());
          String vfilename = FilenameUtils.getBaseName(videoFile.getName());
          if (vfilename.equals(FilenameUtils.getBaseName(mf.getFilename())) // basename match
              || Utils.cleanStackingMarkers(vfilename)
                  .trim()
                  .equals(FilenameUtils.getBaseName(mf.getFilename())) // basename w/o stacking
              || episode
                  .getTitle()
                  .equals(FilenameUtils.getBaseName(mf.getFilename()))) { // title match
            mf.setType(MediaFileType.THUMB);
          }
        }

        episode.addToMediaFiles(mf);
        newFileFound = true;
      } else {
        if (file.getName().equalsIgnoreCase("subs")
            || file.getName().equalsIgnoreCase("subtitle")) {
          File[] subDirContent = file.listFiles();
          if (subDirContent == null) {
            LOGGER.error("Whops. Cannot access directory: " + file.getName());
          } else {
            for (File subDirFile : subDirContent) {
              if (FilenameUtils.getBaseName(subDirFile.getName())
                  .startsWith(FilenameUtils.getBaseName(videoFile.getName()))) {
                MediaFile mf = new MediaFile(subDirFile);
                if (existingMediaFiles.contains(mf)) {
                  continue;
                }
                if (mf.getType() == MediaFileType.SUBTITLE) {
                  episode.setSubtitles(true);
                }
                episode.addToMediaFiles(mf);
                newFileFound = true;
              }
            }
          }
        }
      }
    }

    return newFileFound;
  }
  /**
   * Find tv episodes.
   *
   * @param tvShow the tv show
   * @param dir the dir
   */
  private void findTvEpisodesAsDisc(TvShow tvShow, File dir) {
    String parentDir = dir.getParent();
    LOGGER.debug("parsing disc structure in " + dir.getPath() + " parent: " + parentDir);
    // crawl this folder and try to find every episode in it

    List<MediaFile> videoFiles = new ArrayList<MediaFile>();
    File firstVideoFile = null;

    File[] content = dir.listFiles();
    if (content == null) {
      LOGGER.error("Whops. Cannot access directory: " + dir.getName());
      return;
    }

    for (File file : content) {
      if (file.isFile()) {
        // check filetype
        if (!Globals.settings
                .getVideoFileType()
                .contains("." + FilenameUtils.getExtension(file.getName()).toLowerCase())
            || file.getName().startsWith(skipFilesStartingWith)) { // MacOS ignore
          continue;
        }

        videoFiles.add(new MediaFile(file));
        if (firstVideoFile == null) {
          firstVideoFile = file;
        }
      }
    }

    List<TvShowEpisode> episodes = tvShowList.getTvEpisodesByFile(tvShow, firstVideoFile);
    if (episodes.size() == 0) {
      String relativePath =
          new File(tvShow.getPath()).toURI().relativize(firstVideoFile.toURI()).getPath();
      EpisodeMatchingResult result =
          TvShowEpisodeAndSeasonParser.detectEpisodeFromFilenameAlternative(
              relativePath, tvShow.getTitle());

      if (result.season == -1) {
        // did the search find a season?
        // no -> search for it in the folder name (relative path between tv show root and the
        // current dir)
        result.season = TvShowEpisodeAndSeasonParser.detectSeason(relativePath);
      }

      if (result.episodes.size() == 0) {
        // try to parse out episodes/season from parent directory
        result =
            TvShowEpisodeAndSeasonParser.detectEpisodeFromDirectory(
                dir.getParentFile(), tvShow.getPath());
      }

      List<TvShowEpisode> episodesInNfo = TvShowEpisode.parseNFO(firstVideoFile);

      // FIXME: Episode root is outside of disc folders ?!
      while (dir.getPath().toUpperCase().contains("BDMV")
          || dir.getPath().toUpperCase().contains("VIDEO_TS")) {
        dir = dir.getParentFile();
      }

      if (result.episodes.size() > 0) {
        // add it
        for (int ep : result.episodes) {
          TvShowEpisode episode = null;
          // search in the NFO list if an episode has been found
          for (int i = episodesInNfo.size() - 1; i >= 0; i--) {
            TvShowEpisode e = episodesInNfo.get(i);
            if (e.getSeason() == result.season && e.getEpisode() == ep) {
              episode = e;
              episodesInNfo.remove(i);
              break;
            }
          }
          if (episode == null) {
            episode = new TvShowEpisode();
            episode.setDvdOrder(Globals.settings.getTvShowSettings().isDvdOrder());
            episode.setEpisode(ep);
            episode.setSeason(result.season);
            episode.setNewlyAdded(true);
            episode.setFirstAired(result.date);
          }

          episode.setPath(dir.getPath());
          episode.setTvShow(tvShow);
          episode.setDisc(true);
          episode.setNewlyAdded(true);
          episode.addToMediaFiles(videoFiles);
          episode.setDateAddedFromMediaFile(new MediaFile(firstVideoFile));
          findAdditionalEpisodeFiles(episode, firstVideoFile, content);
          episode.saveToDb();
          tvShow.addEpisode(episode);
        }
      } else {
        // episode detection found nothing - simply add this file
        if (episodesInNfo.size() > 0) {
          for (TvShowEpisode e : episodesInNfo) {
            e.setPath(dir.getPath());
            e.setTvShow(tvShow);
            e.addToMediaFiles(videoFiles);
            e.setNewlyAdded(true);
            // e.findImages();
            e.setDateAddedFromMediaFile(new MediaFile(firstVideoFile));
            findAdditionalEpisodeFiles(e, firstVideoFile, content);
            e.saveToDb();
            tvShow.addEpisode(e);
          }
        } else {
          TvShowEpisode episode = new TvShowEpisode();
          episode.setPath(dir.getPath());
          episode.setDvdOrder(Globals.settings.getTvShowSettings().isDvdOrder());
          episode.setEpisode(-1);
          episode.setSeason(-1);
          episode.setTvShow(tvShow);
          episode.setFirstAired(result.date);
          episode.setDisc(true);
          episode.setNewlyAdded(true);
          episode.addToMediaFiles(videoFiles);
          episode.setDateAddedFromMediaFile(new MediaFile(firstVideoFile));
          findAdditionalEpisodeFiles(episode, firstVideoFile, content);
          episode.saveToDb();
          tvShow.addEpisode(episode);
        }
      }
    } else {
      // episode already added; look if any new additional files have been added
      for (TvShowEpisode episode : episodes) {
        if (findAdditionalEpisodeFiles(episode, firstVideoFile, content)) {
          episode.saveToDb();
        }
      }
    }
  }
  /**
   * Find tv episodes.
   *
   * @param tvShow the tv show
   * @param dir the dir
   */
  private void findTvEpisodes(TvShow tvShow, File dir) {
    LOGGER.debug("parsing " + dir.getPath());
    // crawl this folder and try to find every episode and its corresponding files in it
    File[] content = dir.listFiles();
    if (content == null) {
      LOGGER.error("Whops. Cannot access directory: " + dir.getName());
      return;
    }

    Arrays.sort(content);
    for (File file : content) {
      if (file.isFile()) {
        if (!file.getName().startsWith(skipFilesStartingWith)) {
          MediaFile mf = new MediaFile(file);
          // check filetype - we only proceed here if it's a video file
          if (!mf.getType().equals(MediaFileType.VIDEO)) {
            continue;
          }

          // is this file already assigned to another episode?
          List<TvShowEpisode> episodes = tvShowList.getTvEpisodesByFile(tvShow, file);
          if (episodes.size() == 0) {
            // try to check what episode//season
            // EpisodeMatchingResult result =
            // TvShowEpisodeAndSeasonParser.detectEpisodeFromFilename(file);
            String relativePath =
                new File(tvShow.getPath()).toURI().relativize(file.toURI()).getPath();
            EpisodeMatchingResult result =
                TvShowEpisodeAndSeasonParser.detectEpisodeFromFilenameAlternative(
                    relativePath, tvShow.getTitle());

            // second check: is the detected episode (>-1; season >-1) already in tmm and any valid
            // stacking markers found?
            if (result.episodes.size() == 1 && result.season > -1 && result.stackingMarkerFound) {
              // get any assigned episode
              TvShowEpisode ep = tvShow.getEpisode(result.season, result.episodes.get(0));
              if (ep != null) {
                ep.setNewlyAdded(true);
                ep.addToMediaFiles(mf);
                continue;
              }
            }

            if (result.episodes.size() == 0) {
              // try to parse out episodes/season from parent directory
              result =
                  TvShowEpisodeAndSeasonParser.detectEpisodeFromDirectory(dir, tvShow.getPath());
            }

            if (result.season == -1) {
              // did the search find a season?
              // no -> search for it in the folder name (relative path between tv show root and the
              // current dir)
              result.season = TvShowEpisodeAndSeasonParser.detectSeason(relativePath);
            }

            List<TvShowEpisode> episodesInNfo = TvShowEpisode.parseNFO(file);

            // did we find any episodes in the NFO?
            if (episodesInNfo.size() > 0) {
              // these have priority!
              for (TvShowEpisode e : episodesInNfo) {
                e.setPath(dir.getPath());
                e.setTvShow(tvShow);
                e.addToMediaFiles(mf);
                e.setDateAddedFromMediaFile(mf);
                findAdditionalEpisodeFiles(e, file, content);
                e.setNewlyAdded(true);
                e.saveToDb();
                tvShow.addEpisode(e);
              }
            } else if (result.episodes.size() > 0) {
              // something found with the season detection?
              for (int ep : result.episodes) {
                TvShowEpisode episode = new TvShowEpisode();
                episode.setDvdOrder(Globals.settings.getTvShowSettings().isDvdOrder());
                episode.setEpisode(ep);
                episode.setSeason(result.season);
                episode.setFirstAired(result.date);

                if (result.name.isEmpty()) {
                  result.name = FilenameUtils.getBaseName(file.getName());
                }
                episode.setTitle(result.name);

                episode.setPath(dir.getPath());
                episode.setTvShow(tvShow);
                episode.addToMediaFiles(mf);
                episode.setDateAddedFromMediaFile(mf);
                findAdditionalEpisodeFiles(episode, file, content);
                episode.setNewlyAdded(true);
                episode.saveToDb();
                tvShow.addEpisode(episode);
              }
            } else {
              // episode detection found nothing - simply add this file
              TvShowEpisode episode = new TvShowEpisode();
              episode.setDvdOrder(Globals.settings.getTvShowSettings().isDvdOrder());
              episode.setEpisode(-1);
              episode.setSeason(-1);
              episode.setPath(dir.getPath());

              episode.setTitle(FilenameUtils.getBaseName(file.getName()));
              episode.setTvShow(tvShow);
              episode.setFirstAired(result.date);
              episode.addToMediaFiles(mf);
              episode.setDateAddedFromMediaFile(mf);
              findAdditionalEpisodeFiles(episode, file, content);
              episode.setNewlyAdded(true);
              episode.saveToDb();
              tvShow.addEpisode(episode);
            }
          } else {
            // episode already added; look if any new additional files have been added
            for (TvShowEpisode episode : episodes) {
              if (findAdditionalEpisodeFiles(episode, file, content)) {
                episode.saveToDb();
              }
            }
          }
        } // end skipFilesStartingWith
      } // end isFile

      if (file.isDirectory()
          && !skipFolders.contains(file.getName().toUpperCase())
          && !file.getName().matches(skipFoldersRegex)
          && !TvShowModuleManager.TV_SHOW_SETTINGS
              .getTvShowSkipFolders()
              .contains(file.getAbsolutePath())) {
        // check if that directory contains a .tmmignore file
        File tmmIgnore = new File(file, ".tmmignore");
        if (!tmmIgnore.exists()) {
          // dig deeper
          if (file.getName().toUpperCase().equals("VIDEO_TS")) {
            findTvEpisodesAsDisc(tvShow, file);
          } else if (file.getName().toUpperCase().equals("BDMV")) {
            findTvEpisodesAsDisc(tvShow, file);
          } else {
            findTvEpisodes(tvShow, file);
          }
        }
      }
    }
  }
  /*
   * update a single TV show
   */
  private void updateTvShows() {
    // one thread here - more threads killed the UI
    initThreadPool(1, "update");

    for (File tvShowFolder : tvShowFolders) {
      // check if the tv show dir is accessible
      File[] filesInDatasourceRoot = tvShowFolder.getParentFile().listFiles();
      if (filesInDatasourceRoot == null || filesInDatasourceRoot.length == 0) {
        LOGGER.warn("TvShow folder not available/empty " + tvShowFolder);
        MessageManager.instance.pushMessage(
            new Message(
                MessageLevel.ERROR,
                "update.datasource",
                "update.datasource.unavailable",
                new String[] {tvShowFolder.getParent()}));
        continue;
      }

      if (tvShowFolder.isDirectory()) {
        submitTask(new FindTvShowTask(tvShowFolder, tvShowFolder.getParent()));
      }
    }

    waitForCompletionOrCancel();

    // cleanup
    setTaskName(BUNDLE.getString("update.cleanup"));
    setTaskDescription(null);
    setProgressDone(0);
    setWorkUnits(0);
    publishState();

    LOGGER.info("removing orphaned movies/files...");
    for (int i = tvShowList.getTvShows().size() - 1; i >= 0; i--) {
      if (cancel) {
        break;
      }
      TvShow tvShow = tvShowList.getTvShows().get(i);

      // check only Tv shows matching datasource
      if (!tvShowFolders.contains(new File(tvShow.getPath()))) {
        continue;
      }

      // check and delete all not found MediaFiles
      cleanup(tvShow);
    }

    // start MI
    setTaskName(BUNDLE.getString("update.mediainfo"));
    publishState();

    initThreadPool(1, "mediainfo");
    LOGGER.info("getting Mediainfo...");
    for (int i = tvShowList.getTvShows().size() - 1; i >= 0; i--) {
      if (cancel) {
        break;
      }
      TvShow tvShow = tvShowList.getTvShows().get(i);

      // check only Tv shows matching datasource
      if (!tvShowFolders.contains(new File(tvShow.getPath()))) {
        continue;
      }

      gatherMediaInformationForUngatheredMediaFiles(tvShow);
    }

    waitForCompletionOrCancel();

    if (cancel) {
      return;
    }

    // build up the image cache
    if (Globals.settings.getTvShowSettings().isBuildImageCacheOnImport()) {
      List<File> imageFiles = new ArrayList<File>();
      for (int i = tvShowList.getTvShows().size() - 1; i >= 0; i--) {
        if (cancel) {
          break;
        }
        TvShow tvShow = tvShowList.getTvShows().get(i);

        // check only Tv shows matching datasource
        if (!tvShowFolders.contains(new File(tvShow.getPath()))) {
          continue;
        }

        for (MediaFile mf : new ArrayList<MediaFile>(tvShow.getMediaFiles())) {
          if (mf.isGraphic()) {
            imageFiles.add(mf.getFile());
          }
        }
        for (TvShowEpisode episode : tvShow.getEpisodes()) {
          for (MediaFile mf : new ArrayList<MediaFile>(episode.getMediaFiles())) {
            if (mf.isGraphic()) {
              imageFiles.add(mf.getFile());
            }
          }
        }
      }

      ImageCacheTask task = new ImageCacheTask(imageFiles);
      TmmTaskManager.getInstance().addUnnamedTask(task);
    }
  }
  /*
   * update one or more datasources
   */
  private void updateDatasource() {
    List<File> imageFiles = new ArrayList<File>();

    for (String path : dataSources) {
      File[] dirs = new File(path).listFiles();
      // check whether the path is accessible (eg disconnected shares)
      if (dirs == null || dirs.length == 0) {
        // error - continue with next datasource
        LOGGER.warn("Datasource not available/empty " + path);
        MessageManager.instance.pushMessage(
            new Message(
                MessageLevel.ERROR,
                "update.datasource",
                "update.datasource.unavailable",
                new String[] {path}));
        continue;
      }

      // one thread here - more threads killed the UI
      initThreadPool(1, "update");

      for (File subdir : dirs) {
        if (cancel) {
          break;
        }

        String directoryName = subdir.getName();
        // check against unwanted dirs
        if (skipFolders.contains(directoryName.toUpperCase())
            || directoryName.matches(skipFoldersRegex)
            || TvShowModuleManager.TV_SHOW_SETTINGS
                .getTvShowSkipFolders()
                .contains(subdir.getAbsolutePath())) {
          LOGGER.info("ignoring directory " + directoryName);
          continue;
        }

        // check this dir as TV show dir
        if (subdir.isDirectory()) {
          // check if there is a .tmmignore in this directory
          File tmmIgnore = new File(subdir, ".tmmignore");
          if (!tmmIgnore.exists()) {
            submitTask(new FindTvShowTask(subdir, path));
          }
        }

        // video FILE in DS root - not supported!
        if (subdir.isFile()
            && Globals.settings
                .getVideoFileType()
                .contains("." + FilenameUtils.getExtension(subdir.getName()))) {
          MessageManager.instance.pushMessage(
              new Message(
                  MessageLevel.ERROR,
                  "update.datasource",
                  "update.datasource.episodeinroot",
                  new String[] {subdir.getName()}));
        }
      }

      waitForCompletionOrCancel();
      if (cancel) {
        break;
      }

      // cleanup
      setTaskName(BUNDLE.getString("update.cleanup"));
      setTaskDescription(null);
      setProgressDone(0);
      setWorkUnits(0);
      publishState();
      LOGGER.info("removing orphaned tv shows/files...");
      for (int i = tvShowList.getTvShows().size() - 1; i >= 0; i--) {
        if (cancel) {
          break;
        }
        TvShow tvShow = tvShowList.getTvShows().get(i);
        if (!new File(path).equals(new File(tvShow.getDataSource()))) {
          // check only Tv shows matching datasource
          continue;
        }

        File tvShowDir = new File(tvShow.getPath());
        if (!tvShowDir.exists()) {
          tvShowList.removeTvShow(tvShow);
        } else {
          // do a cleanup
          cleanup(tvShow);
        }
      }

      // mediainfo
      setTaskName(BUNDLE.getString("update.mediainfo"));
      publishState();

      initThreadPool(1, "mediainfo");
      LOGGER.info("getting Mediainfo...");
      for (int i = tvShowList.getTvShows().size() - 1; i >= 0; i--) {
        if (cancel) {
          break;
        }
        TvShow tvShow = tvShowList.getTvShows().get(i);
        if (!new File(path).equals(new File(tvShow.getDataSource()))) {
          // check only Tv shows matching datasource
          continue;
        }

        gatherMediaInformationForUngatheredMediaFiles(tvShow);
      }

      waitForCompletionOrCancel();
      if (cancel) {
        break;
      }

      // build image cache on import
      if (Globals.settings.getTvShowSettings().isBuildImageCacheOnImport()) {
        for (TvShow tvShow : new ArrayList<TvShow>(tvShowList.getTvShows())) {
          if (!new File(path).equals(new File(tvShow.getDataSource()))) {
            continue;
          }
          for (MediaFile mf : new ArrayList<MediaFile>(tvShow.getMediaFiles())) {
            if (mf.isGraphic()) {
              imageFiles.add(mf.getFile());
            }
          }
          for (TvShowEpisode episode : tvShow.getEpisodes()) {
            for (MediaFile mf : new ArrayList<MediaFile>(episode.getMediaFiles())) {
              if (mf.isGraphic()) {
                imageFiles.add(mf.getFile());
              }
            }
          }
        }
      }
    }

    if (cancel) {
      return;
    }

    if (imageFiles.size() > 0) {
      ImageCacheTask task = new ImageCacheTask(imageFiles);
      TmmTaskManager.getInstance().addUnnamedTask(task);
    }

    if (Globals.settings.getTvShowSettings().getSyncTrakt()) {
      TmmTask task = new SyncTraktTvTask(false, false, true, true);
      TmmTaskManager.getInstance().addUnnamedTask(task);
    }
  }
  /**
   * Creates the new file/folder name according to template string
   *
   * @param template the template
   * @param show the TV show
   * @param episodes the TV show episodes; nullable for TV show root foldername
   * @return the string
   */
  public static String createDestination(
      String template, TvShow show, List<TvShowEpisode> episodes) {
    String newDestination = template;
    TvShowEpisode firstEp = null;

    // replace token show title ($N)
    if (newDestination.contains("$N")) {
      newDestination = replaceToken(newDestination, "$N", show.getTitle());
    }

    // parse out episode depended tokens - for multi EP naming
    if (!episodes.isEmpty()) {
      Matcher matcher = multiEpisodeTokenPattern.matcher(template);
      String episodeTokens = "";

      if (matcher.find()) {
        episodeTokens = matcher.group(0);
      }

      String combinedEpisodeParts = "";
      for (TvShowEpisode episode : episodes) {
        String episodePart = episodeTokens;

        // remember first episode for media file tokens
        if (firstEp == null) {
          firstEp = episode;
        }

        // Season w/o leading zeros ($1)
        if (episodePart.contains("$1")) {
          episodePart = replaceToken(episodePart, "$1", String.valueOf(episode.getSeason()));
        }

        // Season leading zeros ($2)
        if (episodePart.contains("$2")) {
          episodePart = replaceToken(episodePart, "$2", lz(episode.getSeason()));
        }

        // DVD-Season w/o leading zeros ($3)
        if (episodePart.contains("$3")) {
          episodePart = replaceToken(episodePart, "$3", String.valueOf(episode.getDvdSeason()));
        }

        // DVD-Season leading zeros ($4)
        if (episodePart.contains("$4")) {
          episodePart = replaceToken(episodePart, "$4", lz(episode.getDvdSeason()));
        }

        // episode number
        if (episodePart.contains("$E")) {
          episodePart = replaceToken(episodePart, "$E", lz(episode.getEpisode()));
        }

        // DVD-episode number
        if (episodePart.contains("$D")) {
          episodePart = replaceToken(episodePart, "$D", lz(episode.getDvdEpisode()));
        }

        // episode title
        if (episodePart.contains("$T")) {
          episodePart = replaceToken(episodePart, "$T", episode.getTitle());
        }

        combinedEpisodeParts += episodePart + " ";
      }

      // and now fill in the (multiple) episode parts
      if (StringUtils.isNotBlank(episodeTokens)) {
        newDestination = newDestination.replace(episodeTokens, combinedEpisodeParts);
      }
    } else {
      // we're in either TV show folder or season folder generation;
      // strip out episode tokens
      newDestination = newDestination.replace("$E", "");
      newDestination = newDestination.replace("$T", "");
    }

    // replace token year ($Y)
    if (newDestination.contains("$Y")) {
      if (show.getYear().equals("0")) {
        newDestination = newDestination.replace("$Y", "");
      } else {
        newDestination = replaceToken(newDestination, "$Y", show.getYear());
      }
    }

    if (firstEp != null && firstEp.getMediaFiles(MediaFileType.VIDEO).size() > 0) {
      MediaFile mf = firstEp.getMediaFiles(MediaFileType.VIDEO).get(0);
      // replace token resolution ($R)
      if (newDestination.contains("$R")) {
        newDestination = replaceToken(newDestination, "$R", mf.getVideoResolution());
      }

      // replace token audio codec + channels ($A)
      if (newDestination.contains("$A")) {
        newDestination =
            replaceToken(
                newDestination,
                "$A",
                mf.getAudioCodec()
                    + (mf.getAudioCodec().isEmpty() ? "" : "-")
                    + mf.getAudioChannels());
      }

      // replace token video codec + format ($V)
      if (newDestination.contains("$V")) {
        newDestination =
            replaceToken(
                newDestination,
                "$V",
                mf.getVideoCodec()
                    + (mf.getVideoCodec().isEmpty() ? "" : "-")
                    + mf.getVideoFormat());
      }

      // replace token video format ($F)
      if (newDestination.contains("$F")) {
        newDestination = replaceToken(newDestination, "$F", mf.getVideoFormat());
      }
    } else {
      // no mediafiles; remove at least token (if available)
      newDestination = newDestination.replace("$R", "");
      newDestination = newDestination.replace("$A", "");
      newDestination = newDestination.replace("$V", "");
      newDestination = newDestination.replace("$F", "");
    }

    // replace empty brackets
    newDestination = newDestination.replaceAll("\\(\\)", "");
    newDestination = newDestination.replaceAll("\\[\\]", "");

    // if there are multiple file separators in a row - strip them out
    if (SystemUtils.IS_OS_WINDOWS) {
      // we need to mask it in windows
      newDestination = newDestination.replaceAll("\\\\{2,}", "\\\\");
      newDestination = newDestination.replaceAll("^\\\\", "");
    } else {
      newDestination = newDestination.replaceAll(File.separator + "{2,}", File.separator);
      newDestination = newDestination.replaceAll("^" + File.separator, "");
    }

    // ASCII replacement
    if (SETTINGS.isAsciiReplacement()) {
      newDestination = StrgUtils.convertToAscii(newDestination, false);
    }

    // trim out unnecessary whitespaces
    newDestination = newDestination.trim();

    // any whitespace replacements?
    if (SETTINGS.isRenamerSpaceSubstitution()) {
      newDestination = newDestination.replaceAll(" ", SETTINGS.getRenamerSpaceReplacement());
    }

    // replace trailing dots and spaces
    newDestination = newDestination.replaceAll("[ \\.]+$", "");

    return newDestination.trim();
  }
  /**
   * Renames a MediaFiles<br>
   * gets all episodes of it, creates season folder, updates MFs & DB
   *
   * @param mf the MediaFile
   * @param show the tvshow (only needed for path)
   */
  public static void renameMediaFile(MediaFile mf, TvShow show) {
    // #######################################################
    // Assumption: all multi-episodes share the same season!!!
    // #######################################################

    List<TvShowEpisode> eps = TvShowList.getInstance().getTvEpisodesByFile(show, mf.getFile());
    if (eps == null || eps.size() == 0) {
      // this should not happen, but unluckily ODB does it sometimes; try a second time to get the
      // episode
      try {
        Thread.sleep(250);
      } catch (Exception e) {
      }
      eps = TvShowList.getInstance().getTvEpisodesByFile(show, mf.getFile());
    }
    if (eps == null || eps.size() == 0) {
      // FIXME: workaround for r1972
      // when moving video file, all NFOs get deleted and a new gets created.
      // so this OLD NFO is not found anylonger - just delete it
      if (mf.getType() == MediaFileType.NFO) {
        FileUtils.deleteQuietly(mf.getFile());
        return;
      }

      LOGGER.warn("No episodes found for file '" + mf.getFilename() + "' - skipping");
      return;
    }

    // get first, for isDisc and season
    TvShowEpisode ep = eps.get(0);

    // test access rights or return
    LOGGER.debug(
        "testing file S:"
            + ep.getSeason()
            + " E:"
            + ep.getEpisode()
            + " MF:"
            + mf.getFile().getAbsolutePath());
    File f = mf.getFile();
    boolean testRenameOk = false;
    for (int i = 0; i < 5; i++) {
      testRenameOk = f.renameTo(f); // haahaa, try to rename to itself :P
      if (testRenameOk) {
        break; // ok it worked, step out
      }
      try {
        if (!f.exists()) {
          LOGGER.debug("Hmmm... file " + f + " does not even exists; delete from DB");
          // delete from MF
          for (TvShowEpisode e : eps) {
            e.removeFromMediaFiles(mf);
            e.saveToDb();
            e.writeNFO();
          }
          return;
        }
        LOGGER.debug("rename did not work - sleep a while and try again...");
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        LOGGER.warn("I'm so excited - could not sleep");
      }
    }
    if (!testRenameOk) {
      LOGGER.warn("File " + mf.getFile().getAbsolutePath() + " is not accessible!");
      MessageManager.instance.pushMessage(
          new Message(MessageLevel.ERROR, mf.getFilename(), "message.renamer.failedrename"));
      return;
    }

    // create SeasonDir
    // String seasonName = "Season " + String.valueOf(ep.getSeason());
    String seasonName = generateSeasonDir(SETTINGS.getRenamerSeasonFoldername(), ep);
    File seasonDir = null;
    if (StringUtils.isNotBlank(seasonName)) {
      seasonDir = new File(show.getPath(), seasonName);
      if (!seasonDir.exists()) {
        seasonDir.mkdir();
      }
    } else {
      seasonDir = new File(show.getPath());
    }

    // rename epFolder accordingly
    if (ep.isDisc() || mf.isDiscFile()) {
      // \Season 1\S01E02E03\VIDEO_TS\VIDEO_TS.VOB
      // ........ \epFolder \disc... \ file
      File disc = mf.getFile().getParentFile();
      File epFolder = disc.getParentFile();

      // sanity check
      if (!disc.getName().equalsIgnoreCase("BDMV")
          && !disc.getName().equalsIgnoreCase("VIDEO_TS")) {
        LOGGER.error(
            "Episode is labeled as 'on BD/DVD', but structure seems not to match. Better exit and do nothing... o_O");
        return;
      }

      String newFoldername =
          FilenameUtils.getBaseName(generateFolderename(show, mf)); // w/o extension
      if (newFoldername != null && !newFoldername.isEmpty()) {
        File newEpFolder = new File(seasonDir + File.separator + newFoldername);
        File newDisc = new File(newEpFolder + File.separator + disc.getName()); // old disc name

        try {
          // if (!epFolder.equals(newEpFolder)) {
          if (!epFolder.getAbsolutePath().equals(newEpFolder.getAbsolutePath())) {
            boolean ok = false;
            try {
              ok = Utils.moveDirectorySafe(epFolder, newEpFolder);
            } catch (Exception e) {
              LOGGER.error(e.getMessage());
              MessageManager.instance.pushMessage(
                  new Message(
                      MessageLevel.ERROR,
                      epFolder.getName(),
                      "message.renamer.failedrename",
                      new String[] {":", e.getLocalizedMessage()}));
            }
            if (ok) {
              // iterate over all EPs & MFs and fix new path
              LOGGER.debug("updating *all* MFs for new path -> " + newEpFolder);
              for (TvShowEpisode e : eps) {
                e.updateMediaFilePath(disc, newDisc);
                e.setPath(newEpFolder.getPath());
                e.saveToDb();
                e.writeNFO();
              }
            }
            // and cleanup
            cleanEmptyDir(epFolder);
          } else {
            // old and new folder are equal, do nothing
          }
        } catch (Exception e) {
          LOGGER.error("error moving video file " + disc.getName() + " to " + newFoldername, e);
          MessageManager.instance.pushMessage(
              new Message(
                  MessageLevel.ERROR,
                  mf.getFilename(),
                  "message.renamer.failedrename",
                  new String[] {":", e.getLocalizedMessage()}));
        }
      }
    } // end isDisc
    else {
      MediaFile newMF = new MediaFile(mf); // clone MF
      if (mf.getType().equals(MediaFileType.TRAILER)) {
        // move trailer into separate dir - not supported by XBMC
        File sample = new File(seasonDir, "sample");
        if (!sample.exists()) {
          sample.mkdir();
        }
        seasonDir = sample; // change directory storage
      }
      String filename = generateFilename(show, mf);
      LOGGER.debug("new filename should be " + filename);
      if (filename != null && !filename.isEmpty()) {
        File newFile = new File(seasonDir, filename);

        try {
          // if (!mf.getFile().equals(newFile)) {
          if (!mf.getFile().getAbsolutePath().equals(newFile.getAbsolutePath())) {
            File oldMfFile = mf.getFile();
            boolean ok = false;
            try {
              ok = Utils.moveFileSafe(oldMfFile, newFile);
            } catch (Exception e) {
              LOGGER.error(e.getMessage());
              MessageManager.instance.pushMessage(
                  new Message(
                      MessageLevel.ERROR,
                      oldMfFile.getPath(),
                      "message.renamer.failedrename",
                      new String[] {":", e.getLocalizedMessage()}));
            }
            if (ok) {
              newMF.setPath(seasonDir.getAbsolutePath());
              newMF.setFilename(filename);
              // iterate over all EPs and delete old / set new MF
              for (TvShowEpisode e : eps) {
                e.removeFromMediaFiles(mf);
                e.addToMediaFiles(newMF);
                e.setPath(seasonDir.getAbsolutePath());
                e.saveToDb();
                e.writeNFO();
              }
            }
            // and cleanup
            cleanEmptyDir(oldMfFile.getParentFile());
          } else {
            // old and new file are equal, keep MF
          }
        } catch (Exception e) {
          LOGGER.error(
              "error moving video file " + mf.getFilename() + " to " + newFile.getPath(), e);
          MessageManager.instance.pushMessage(
              new Message(
                  MessageLevel.ERROR,
                  mf.getFilename(),
                  "message.renamer.failedrename",
                  new String[] {":", e.getLocalizedMessage()}));
        }
      }
    }
  }