package com.miz.loader;/* * Copyright (C) 2014 Michell Bak * * 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. */ import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; import android.text.TextUtils; import com.google.common.collect.Lists; import com.miz.db.DbAdapterTvShowEpisodes; import com.miz.db.DbAdapterTvShows; import com.miz.functions.ColumnIndexCache; import com.miz.functions.FileSource; import com.miz.functions.Filepath; import com.miz.functions.LibrarySectionAsyncTask; import com.miz.functions.MizLib; import com.miz.functions.PreferenceKeys; import com.miz.mizuu.MizuuApplication; import com.miz.mizuu.R; import com.miz.mizuu.TvShow; import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashSet; import java.util.Locale; import java.util.TreeMap; import java.util.regex.Pattern; import jcifs.smb.SmbFile; public class TvShowLoader { // For TvShowLibraryType public static final int ALL_SHOWS = 1, FAVORITES = 2, RECENTLY_AIRED = 3, WATCHED = 4, UNWATCHED = 5; // For TvShowSortType public static final int TITLE = 1, FIRST_AIR_DATE = 2, NEWEST_EPISODE = 3, DURATION = 4, RATING = 5, WEIGHTED_RATING = 6; // For saving the sorting type preference public static final String SORT_TITLE = "sortTitle", SORT_RELEASE = "sortRelease", SORT_RATING = "sortRating", SORT_WEIGHTED_RATING = "sortWeightedRating", SORT_NEWEST_EPISODE = "sortNewestEpisode", SORT_DURATION = "sortDuration"; private final Context mContext; private final TvShowLibraryType mLibraryType; private final OnLoadCompletedCallback mCallback; private final DbAdapterTvShows mTvShowDatabase; private final DbAdapterTvShowEpisodes mTvShowEpisodeDatabase; private TvShowSortType mSortType; private ArrayList<TvShow> mResults = new ArrayList<>(); private HashSet<TvShowFilter> mFilters = new HashSet<>(); private TvShowLoaderAsyncTask mAsyncTask; private boolean mShowingSearchResults = false; public TvShowLoader(Context context, TvShowLibraryType libraryType, OnLoadCompletedCallback callback) { mContext = context; mLibraryType = libraryType; mCallback = callback; mTvShowDatabase = MizuuApplication.getTvDbAdapter(); mTvShowEpisodeDatabase = MizuuApplication.getTvEpisodeDbAdapter(); setupSortType(); } /** * Get TV show library type. Can be either <code>ALL_SHOWS</code>, * <code>FAVORITES</code>, <code>NEWLY_AIRED</code>, <code>WATCHED</code> * or <code>UNWATCHED</code>. * @return TV show library type */ public TvShowLibraryType getType() { return mLibraryType; } /** * Add a TV show filter. Filters are unique and only one * can be present at a time. It is, however, possible to * have multiple filters for different filter types, i.e. * two filters for genres. * @param filter */ public void addFilter(TvShowFilter filter) { mFilters.remove(filter); mFilters.add(filter); } /** * Clears all filters. */ public void clearFilters() { mFilters.clear(); } /** * Get all TV show filters. * @return Set of all currently added TV show filters. */ public HashSet<TvShowFilter> getFilters() { return mFilters; } /** * Set the TV show sort type. * @param type */ public void setSortType(TvShowSortType type) { if (getSortType() == type) { getSortType().toggleSortOrder(); } else { mSortType = type; // If we're setting a sort type for the "All shows" // section, we want to save the sort type as the // default way of sorting that section. if (getType() == TvShowLibraryType.ALL_SHOWS) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit(); switch (getSortType()) { case TITLE: editor.putString(PreferenceKeys.SORTING_TVSHOWS, SORT_TITLE); break; case FIRST_AIR_DATE: editor.putString(PreferenceKeys.SORTING_TVSHOWS, SORT_RELEASE); break; case NEWEST_EPISODE: editor.putString(PreferenceKeys.SORTING_TVSHOWS, SORT_NEWEST_EPISODE); break; case DURATION: editor.putString(PreferenceKeys.SORTING_TVSHOWS, SORT_DURATION); break; case RATING: editor.putString(PreferenceKeys.SORTING_TVSHOWS, SORT_RATING); break; case WEIGHTED_RATING: editor.putString(PreferenceKeys.SORTING_TVSHOWS, SORT_WEIGHTED_RATING); break; } editor.apply(); } } } /** * Get the TV show sort type. Can be either <code>TITLE</code>, * <code>FIRST_AIR_DATE</code>, <code>NEWEST_EPISODE</code>, <code>DURATION</code>, * <code>RATING</code>, or <code>WEIGHTED_RATING</code>. * @return TV show sort type */ public TvShowSortType getSortType() { return mSortType; } /** * Starts loading TV shows using any active filters, * sorting types and settings, i.e. prefix ignoring. */ public void load() { load(""); } /** * Similar to <code>load()</code>, but filters the results * based on the search query. * @param query */ public void search(String query) { load(query); } /** * Starts loading TV shows. * @param query */ private void load(String query) { if (mAsyncTask != null) { mAsyncTask.cancel(true); } mShowingSearchResults = !TextUtils.isEmpty(query); mAsyncTask = new TvShowLoaderAsyncTask(query); mAsyncTask.execute(); } /** * Used to know if the TvShowLoader is currently * showing search results. * @return True if showing search results, false otherwise. */ public boolean isShowingSearchResults() { return mShowingSearchResults; } /** * Get the results of the most recently loaded TV shows. * @return List of TV show objects. */ public ArrayList<TvShow> getResults() { return mResults; } /** * Creates TV show objects from a Cursor and adds them to a list. * @param cursor * @return List of TV show objects from the supplied Cursor. */ private ArrayList<TvShow> listFromCursor(Cursor cursor) { ArrayList<TvShow> list = new ArrayList<>(); if (cursor != null) { ColumnIndexCache cache = new ColumnIndexCache(); try { while (cursor.moveToNext()) { list.add(new TvShow( mContext, cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_ID)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_TITLE)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_PLOT)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_RATING)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_GENRES)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_ACTORS)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_CERTIFICATION)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_FIRST_AIRDATE)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_RUNTIME)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_FAVOURITE)), mTvShowEpisodeDatabase.getLatestEpisodeAirdate(cursor.getString(cache.getColumnIndex(cursor, DbAdapterTvShows.KEY_SHOW_ID))) )); } } catch (Exception e) { } finally { cursor.close(); cache.clear(); } } mResults = list; return list; } /** * Handles everything related to loading, filtering, sorting * and delivering the callback when everything is finished. */ private class TvShowLoaderAsyncTask extends LibrarySectionAsyncTask<Void, Void, Void> { private final ArrayList<TvShow> mTvShowList; private final String mSearchQuery; public TvShowLoaderAsyncTask(String searchQuery) { // Lowercase in order to search more efficiently mSearchQuery = searchQuery.toLowerCase(Locale.getDefault()); mTvShowList = new ArrayList<TvShow>(); } @Override protected Void doInBackground(Void... params) { switch (mLibraryType) { case ALL_SHOWS: mTvShowList.addAll(listFromCursor(mTvShowDatabase.getAllShows())); break; case FAVORITES: mTvShowList.addAll(listFromCursor(mTvShowDatabase.getAllFavorites())); break; case RECENTLY_AIRED: mTvShowList.addAll(listFromCursor(mTvShowDatabase.getAllShows())); int listSize = mTvShowList.size(); long now = System.currentTimeMillis(); Calendar cal = Calendar.getInstance(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); for (int i = 0; i < listSize; i++) { String latestEpisode = mTvShowEpisodeDatabase.getLatestEpisodeAirdate(mTvShowList.get(i).getId()); try { cal.setTime(sdf.parse(latestEpisode)); cal.add(Calendar.MONTH, 3); if (cal.getTimeInMillis() < now) { mTvShowList.remove(i); i--; listSize--; } } catch (ParseException e) { mTvShowList.remove(i); i--; listSize--; } } break; case UNWATCHED: mTvShowList.addAll(listFromCursor(mTvShowDatabase.getAllShows())); int size = mTvShowList.size(); for (int i = 0; i < size; i++) { if (!mTvShowEpisodeDatabase.hasUnwatchedEpisodes(mTvShowList.get(i).getId())) { mTvShowList.remove(i); i--; size--; } } break; case WATCHED: mTvShowList.addAll(listFromCursor(mTvShowDatabase.getAllShows())); int totalSize = mTvShowList.size(); for (int i = 0; i < totalSize; i++) { if (mTvShowEpisodeDatabase.hasUnwatchedEpisodes(mTvShowList.get(i).getId())) { mTvShowList.remove(i); i--; totalSize--; } } break; default: break; } int totalSize = mTvShowList.size(); for (TvShowFilter filter : getFilters()) { for (int i = 0; i < totalSize; i++) { if (isCancelled()) return null; ArrayList<Filepath> paths = new ArrayList<>(); for (String s : MizuuApplication.getTvShowEpisodeMappingsDbAdapter() .getFilepathsForShow(mTvShowList.get(i).getId())) { paths.add(new Filepath(s)); } boolean condition = false; switch (filter.getType()) { case TvShowFilter.GENRE: if (mTvShowList.get(i).getGenres().contains(filter.getFilter())) { String[] genres = mTvShowList.get(i).getGenres().split(","); for (String genre : genres) { if (genre.trim().equals(filter.getFilter())) { condition = true; break; } } } break; case TvShowFilter.CERTIFICATION: condition = mTvShowList.get(i).getCertification().trim().equals(filter.getFilter()); break; case TvShowFilter.FILE_SOURCE: for (Filepath path : paths) { condition = path.getTypeAsString(mContext).equals(filter.getFilter()); if (condition) break; } break; case TvShowFilter.RELEASE_YEAR: condition = mTvShowList.get(i).getReleaseYear().trim().contains(filter.getFilter()); break; case TvShowFilter.FOLDER: for (Filepath path : paths) { condition = path.getFilepath().trim().startsWith(filter.getFilter()); if (condition) break; } break; case TvShowFilter.OFFLINE_FILES: for (Filepath path : paths) { condition = mTvShowList.get(i).hasOfflineCopy(path); if (condition) break; } break; case TvShowFilter.AVAILABLE_FILES: ArrayList<FileSource> filesources = MizLib.getFileSources(MizLib.TYPE_SHOWS, true); if (isCancelled()) return null; for (Filepath path : paths) { if (path.isNetworkFile()) if (mTvShowList.get(i).hasOfflineCopy(path)) { condition = true; break; // break inner loop to continue to the next TV show } else { if (path.getType() == FileSource.SMB) { if (MizLib.isWifiConnected(mContext)) { FileSource source = null; for (int j = 0; j < filesources.size(); j++) if (path.getFilepath().contains(filesources.get(j).getFilepath())) { source = filesources.get(j); break; } if (source == null) continue; try { final SmbFile file = new SmbFile( MizLib.createSmbLoginString( source.getDomain(), source.getUser(), source.getPassword(), path.getFilepath(), false )); if (file.exists()) { condition = true; break; // break inner loop to continue to the next TV show } } catch (Exception e) {} // Do nothing - the file isn't available (either MalformedURLException or SmbException) } } else if (path.getType() == FileSource.UPNP) { if (MizLib.exists(path.getFilepath())) { condition = true; break; // break inner loop to continue to the next TV show } } } else { if (new File(path.getFilepath()).exists()) { condition = true; break; // break inner loop to continue to the next TV show } } } break; } if (!condition && mTvShowList.size() > i) { mTvShowList.remove(i); i--; totalSize--; } } } // If we've got a search query, we should search based on it if (!TextUtils.isEmpty(mSearchQuery)) { ArrayList<TvShow> tempCollection = Lists.newArrayList(); if (mSearchQuery.startsWith("actor:")) { for (int i = 0; i < mTvShowList.size(); i++) { if (isCancelled()) return null; if (mTvShowList.get(i).getActors().toLowerCase(Locale.ENGLISH).contains(mSearchQuery.replace("actor:", "").trim())) tempCollection.add(mTvShowList.get(i)); } } else if (mSearchQuery.equalsIgnoreCase("missing_genres")) { for (int i = 0; i < mTvShowList.size(); i++) { if (isCancelled()) return null; if (TextUtils.isEmpty(mTvShowList.get(i).getGenres())) tempCollection.add(mTvShowList.get(i)); } } else { Pattern p = Pattern.compile(MizLib.CHARACTER_REGEX); for (int i = 0; i < mTvShowList.size(); i++) { if (isCancelled()) return null; String lowerCaseTitle = mTvShowList.get(i).getTitle().toLowerCase(Locale.ENGLISH); if (lowerCaseTitle.contains(mSearchQuery) || p.matcher(lowerCaseTitle).replaceAll("").indexOf(mSearchQuery) != -1) { tempCollection.add(mTvShowList.get(i)); } } } // Clear the TV show list mTvShowList.clear(); // Add all the temporary ones after search completed mTvShowList.addAll(tempCollection); // Clear the temporary list tempCollection.clear(); } // Sort Collections.sort(mTvShowList, getSortType().getComparator()); return null; } @Override protected void onPostExecute(Void result) { if (!isCancelled()) { mResults = new ArrayList<>(mTvShowList); mCallback.onLoadCompleted(); } else mTvShowList.clear(); } } /** * Show genres filter dialog. * @param activity */ public void showGenresFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); String[] splitGenres; for (int i = 0; i < mResults.size(); i++) { if (!mResults.get(i).getGenres().isEmpty()) { splitGenres = mResults.get(i).getGenres().split(","); for (int j = 0; j < splitGenres.length; j++) { if (map.containsKey(splitGenres[j].trim())) { map.put(splitGenres[j].trim(), map.get(splitGenres[j].trim()) + 1); } else { map.put(splitGenres[j].trim(), 1); } } } } createAndShowAlertDialog(activity, setupItemArray(map, false), R.string.selectGenre, TvShowFilter.GENRE); } /** * Show certifications filter dialog. * @param activity */ public void showCertificationsFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { String certification = mResults.get(i).getCertification(); if (!TextUtils.isEmpty(certification)) { if (map.containsKey(certification.trim())) { map.put(certification.trim(), map.get(certification.trim()) + 1); } else { map.put(certification.trim(), 1); } } } createAndShowAlertDialog(activity, setupItemArray(map, false), R.string.selectCertification, TvShowFilter.CERTIFICATION); } /** * Show release year filter dialog. * @param activity */ public void showReleaseYearFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { String year = mResults.get(i).getReleaseYear().trim(); if (!TextUtils.isEmpty(year)) { if (map.containsKey(year)) { map.put(year, map.get(year) + 1); } else { map.put(year, 1); } } } createAndShowAlertDialog(activity, setupItemArray(map, false), R.string.selectReleaseYear, TvShowFilter.RELEASE_YEAR); } /** * Show file sources filter dialog. * @param activity */ public void showFileSourcesFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { ArrayList<Filepath> paths = new ArrayList<>(); for (String s : MizuuApplication.getTvShowEpisodeMappingsDbAdapter() .getFilepathsForShow(mResults.get(i).getId())) { paths.add(new Filepath(s)); } for (Filepath path : paths) { String type = path.getTypeAsString(activity); if (map.containsKey(type)) { map.put(type, map.get(type) + 1); } else { map.put(type, 1); } } } createAndShowAlertDialog(activity, setupItemArray(map, true), R.string.selectFileSource, TvShowFilter.FILE_SOURCE); } /** * Show folders filter dialog. * @param activity */ public void showFoldersFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { ArrayList<Filepath> paths = new ArrayList<>(); for (String s : MizuuApplication.getTvShowEpisodeMappingsDbAdapter() .getFilepathsForShow(mResults.get(i).getId())) { paths.add(new Filepath(s)); } for (Filepath path : paths) { String folder = path.getFolder(); if (!TextUtils.isEmpty(folder)) { if (map.containsKey(folder)) { map.put(folder, map.get(folder) + 1); } else { map.put(folder, 1); } } } } createAndShowAlertDialog(activity, setupItemArray(map, true), R.string.selectFolder, TvShowFilter.FOLDER); } /** * Used to set up an array of items for the alert dialog. * @param map * @return List of dialog options. */ private CharSequence[] setupItemArray(TreeMap<String, Integer> map, boolean addFilesPostfix) { final CharSequence[] tempArray = map.keySet().toArray(new CharSequence[map.keySet().size()]); for (int i = 0; i < tempArray.length; i++) tempArray[i] = tempArray[i] + " (" + map.get(tempArray[i]) + (addFilesPostfix ? " " + mContext.getResources().getQuantityString(R.plurals.files, map.get(tempArray[i])) + ")" : ")"); return tempArray; } /** * Shows an alert dialog and handles the user selection. * @param activity * @param temp * @param title * @param type */ private void createAndShowAlertDialog(Activity activity, final CharSequence[] temp, int title, final int type) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title) .setItems(temp, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Let's get what the user selected and remove the parenthesis at the end String selected = temp[which].toString(); selected = selected.substring(0, selected.lastIndexOf("(")).trim(); // Add filter TvShowFilter filter = new TvShowFilter(type); filter.setFilter(selected); addFilter(filter); // Re-load the library with the new filter load(); // Dismiss the dialog dialog.dismiss(); } }); builder.show(); } private void setupSortType() { if (getType() == TvShowLibraryType.ALL_SHOWS) { // Load the saved sort type and set it String savedSortType = PreferenceManager.getDefaultSharedPreferences(mContext).getString(PreferenceKeys.SORTING_TVSHOWS, SORT_TITLE); switch (savedSortType) { case SORT_TITLE: setSortType(TvShowSortType.TITLE); break; case SORT_RELEASE: setSortType(TvShowSortType.FIRST_AIR_DATE); break; case SORT_RATING: setSortType(TvShowSortType.RATING); break; case SORT_WEIGHTED_RATING: setSortType(TvShowSortType.WEIGHTED_RATING); break; case SORT_NEWEST_EPISODE: setSortType(TvShowSortType.NEWEST_EPISODE); break; case SORT_DURATION: setSortType(TvShowSortType.DURATION); break; } } else if (getType() == TvShowLibraryType.RECENTLY_AIRED) { setSortType(TvShowSortType.NEWEST_EPISODE); } else { setSortType(TvShowSortType.TITLE); } } }