diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3370f5..2b61071 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -65,7 +65,7 @@ android:exported="false"/> - diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/ApiList.java b/app/src/main/java/org/xbmc/kore/jsonrpc/ApiList.java new file mode 100644 index 0000000..86d42bd --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/ApiList.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 Martijn Brekhof. All rights reserved. + * + * 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.xbmc.kore.jsonrpc; + +import org.xbmc.kore.jsonrpc.type.ListType; + +import java.util.List; + +public class ApiList { + public final List items; + public final ListType.LimitsReturned limits; + + public ApiList(List items, ListType.LimitsReturned limits) { + this.items = items; + this.limits = limits; + } +} diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/event/MediaSyncEvent.java b/app/src/main/java/org/xbmc/kore/jsonrpc/event/MediaSyncEvent.java index fbfedb3..77dca37 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/event/MediaSyncEvent.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/event/MediaSyncEvent.java @@ -17,6 +17,8 @@ package org.xbmc.kore.jsonrpc.event; import android.os.Bundle; +import org.xbmc.kore.service.library.LibrarySyncService; + /** * Event to post on {@link de.greenrobot.event.EventBus} that notifies of a sync */ @@ -33,7 +35,7 @@ public class MediaSyncEvent { /** * Creates a new sync event * - * @param syncType One of the constants in {@link org.xbmc.kore.service.LibrarySyncService} + * @param syncType One of the constants in {@link LibrarySyncService} */ public MediaSyncEvent(String syncType, Bundle syncExtras, int status) { this(syncType, syncExtras, status, -1, null); diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/method/AudioLibrary.java b/app/src/main/java/org/xbmc/kore/jsonrpc/method/AudioLibrary.java index b2edb4f..a56c119 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/method/AudioLibrary.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/method/AudioLibrary.java @@ -18,7 +18,9 @@ package org.xbmc.kore.jsonrpc.method; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; + import org.xbmc.kore.jsonrpc.ApiException; +import org.xbmc.kore.jsonrpc.ApiList; import org.xbmc.kore.jsonrpc.ApiMethod; import org.xbmc.kore.jsonrpc.type.AudioType; import org.xbmc.kore.jsonrpc.type.LibraryType; @@ -83,7 +85,7 @@ public class AudioLibrary { /** * Retrieve all artists */ - public static class GetArtists extends ApiMethod> { + public static class GetArtists extends ApiMethod> { public final static String METHOD_NAME = "AudioLibrary.GetArtists"; private final static String LIST_NODE = "artists"; @@ -126,28 +128,29 @@ public class AudioLibrary { } @Override - public List resultFromJson(ObjectNode jsonObject) - throws ApiException { + public ApiList resultFromJson(ObjectNode jsonObject) throws ApiException { + ListType.LimitsReturned limits = new ListType.LimitsReturned(jsonObject); + JsonNode resultNode = jsonObject.get(RESULT_NODE); ArrayNode items = resultNode.has(LIST_NODE) ? - (ArrayNode)resultNode.get(LIST_NODE) : null; + (ArrayNode)resultNode.get(LIST_NODE) : null; if (items == null) { - return new ArrayList(0); + return new ApiList<>(new ArrayList(0), limits); } - ArrayList result = new ArrayList(items.size()); + ArrayList result = new ArrayList<>(items.size()); for (JsonNode item : items) { result.add(new AudioType.DetailsArtist(item)); } - return result; + return new ApiList<>(result, limits); } } /** * Retrieve all albums from specified artist or genre */ - public static class GetAlbums extends ApiMethod> { + public static class GetAlbums extends ApiMethod> { public final static String METHOD_NAME = "AudioLibrary.GetAlbums"; private final static String LIST_NODE = "albums"; @@ -182,20 +185,22 @@ public class AudioLibrary { } @Override - public List resultFromJson(ObjectNode jsonObject) + public ApiList resultFromJson(ObjectNode jsonObject) throws ApiException { + ListType.LimitsReturned limits = new ListType.LimitsReturned(jsonObject); + JsonNode resultNode = jsonObject.get(RESULT_NODE); ArrayNode items = resultNode.has(LIST_NODE) ? (ArrayNode)resultNode.get(LIST_NODE) : null; if (items == null) { - return new ArrayList(0); + return new ApiList<>(new ArrayList(0), limits); } - ArrayList result = new ArrayList(items.size()); + ArrayList result = new ArrayList<>(items.size()); for (JsonNode item : items) { result.add(new AudioType.DetailsAlbum(item)); } - return result; + return new ApiList<>(result, limits); } } @@ -244,7 +249,7 @@ public class AudioLibrary { /** * Retrieve all songs from specified album, artist or genre */ - public static class GetSongs extends ApiMethod> { + public static class GetSongs extends ApiMethod> { public final static String METHOD_NAME = "AudioLibrary.GetSongs"; private final static String LIST_NODE = "songs"; @@ -279,21 +284,22 @@ public class AudioLibrary { } @Override - public List resultFromJson(ObjectNode jsonObject) + public ApiList resultFromJson(ObjectNode jsonObject) throws ApiException { - JsonNode resultNode = jsonObject.get(RESULT_NODE); + ListType.LimitsReturned limits = new ListType.LimitsReturned(jsonObject); + JsonNode resultNode = jsonObject.get(RESULT_NODE); ArrayNode items = resultNode.has(LIST_NODE) ? (ArrayNode)resultNode.get(LIST_NODE) : null; if (items == null) { - return new ArrayList(0); + return new ApiList<>(new ArrayList(0), limits); } - ArrayList result = new ArrayList(items.size()); + ArrayList result = new ArrayList<>(items.size()); for (JsonNode item : items) { result.add(new AudioType.DetailsSong(item)); } - return result; + return new ApiList<>(result, limits); } } diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/type/ListType.java b/app/src/main/java/org/xbmc/kore/jsonrpc/type/ListType.java index edc207f..c245a95 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/type/ListType.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/type/ListType.java @@ -18,6 +18,7 @@ package org.xbmc.kore.jsonrpc.type; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; + import org.xbmc.kore.utils.JsonUtils; import java.util.List; @@ -342,6 +343,31 @@ public class ListType { } } + public static class LimitsReturned { + public int start = -1; + public int end = -1; + public int total = -1; + + public LimitsReturned(ObjectNode jsonNode) { + JsonNode resultNode = jsonNode.get("result"); + JsonNode item = resultNode.has("limits") ? resultNode.get("limits") : null; + if (item == null) { + return; + } + + start = JsonUtils.intFromJsonNode(item, "start"); + end = JsonUtils.intFromJsonNode(item, "end"); + total = JsonUtils.intFromJsonNode(item, "total"); + } + + @Override + public String toString() { + return super.toString() + + ", start="+start+ + ", end="+end+ + ", total="+total; + } + } /** * Enums for List.Fields.All diff --git a/app/src/main/java/org/xbmc/kore/service/LibrarySyncService.java b/app/src/main/java/org/xbmc/kore/service/LibrarySyncService.java deleted file mode 100644 index d2d1d2b..0000000 --- a/app/src/main/java/org/xbmc/kore/service/LibrarySyncService.java +++ /dev/null @@ -1,1340 +0,0 @@ -/* - * Copyright 2015 Synced Synapse. All rights reserved. - * - * 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.xbmc.kore.service; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Service; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.net.Uri; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Process; -import android.support.v4.app.Fragment; -import android.util.Log; - -import org.xbmc.kore.host.HostInfo; -import org.xbmc.kore.host.HostManager; -import org.xbmc.kore.jsonrpc.ApiCallback; -import org.xbmc.kore.jsonrpc.HostConnection; -import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; -import org.xbmc.kore.jsonrpc.method.*; -import org.xbmc.kore.jsonrpc.type.AudioType; -import org.xbmc.kore.jsonrpc.type.LibraryType; -import org.xbmc.kore.jsonrpc.type.ListType; -import org.xbmc.kore.jsonrpc.type.VideoType; -import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.Utils; - -import java.lang.System; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import de.greenrobot.event.EventBus; - -/** - * Service that syncs the XBMC local database with the remote library - */ -public class LibrarySyncService extends Service { - public static final String TAG = LogUtils.makeLogTag(LibrarySyncService.class); - - private static final int LIMIT_SYNC_MOVIES = 300; - private static final int LIMIT_SYNC_TVSHOWS = 200; - private static final int LIMIT_SYNC_ARTISTS = 300; - private static final int LIMIT_SYNC_ALBUMS = 300; - private static final int LIMIT_SYNC_SONGS = 600; - - /** - * Possible requests to sync - */ - public static final String SYNC_ALL_MOVIES = "sync_all_movies"; - public static final String SYNC_SINGLE_MOVIE = "sync_single_movie"; - public static final String SYNC_ALL_TVSHOWS = "sync_all_tvshows"; - public static final String SYNC_SINGLE_TVSHOW = "sync_single_tvshow"; - public static final String SYNC_ALL_MUSIC = "sync_all_music"; - public static final String SYNC_ALL_MUSIC_VIDEOS = "sync_all_music_videos"; - - public static final String SYNC_MOVIEID = "sync_movieid"; - public static final String SYNC_TVSHOWID = "sync_tvshowid"; - - /** - * Extra used to pass parameters that will be sent back to the caller - */ - public static final String SYNC_EXTRAS = "sync_extras"; - - /** - * Constant for UI to use to signal a silent sync (pass these in SYNC_EXTRAS) - */ - public static final String SILENT_SYNC = "silent_sync"; - - /** - * Our handler to post callbacks from {@link HostConnection} calls - */ - private Handler callbackHandler; - private HandlerThread handlerThread; - - private ArrayList syncOrchestrators; - - private final IBinder serviceBinder = new LocalBinder(); - - @Override - public void onCreate() { - // Create a Handler Thread to process callback calls after the Xbmc method call - handlerThread = new HandlerThread("LibrarySyncService", Process.THREAD_PRIORITY_BACKGROUND); - handlerThread.start(); - - // Get the HandlerThread's Looper and use it for our Handler - callbackHandler = new Handler(handlerThread.getLooper()); - // Check which libraries to update and call the corresponding methods on Xbmc - - syncOrchestrators = new ArrayList<>(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // Get the connection here, not on create because we can be called for different hosts - // We'll use a specific connection through HTTP, not the singleton one, - // to not interfere with the normal application usage of it (namely calls to disconnect - // and usage of the socket). - HostInfo hostInfo = HostManager.getInstance(this).getHostInfo(); - - SyncOrchestrator syncOrchestrator = new SyncOrchestrator(this, startId, hostInfo, - callbackHandler, getContentResolver()); - - syncOrchestrators.add(syncOrchestrator); - - // Get the request parameters that we should pass when calling back the caller - Bundle syncExtras = intent.getBundleExtra(SYNC_EXTRAS); - - // Sync all movies - boolean syncAllMovies = intent.getBooleanExtra(SYNC_ALL_MOVIES, false); - if (syncAllMovies) { - syncOrchestrator.addSyncItem(new SyncMovies(hostInfo.getId(), syncExtras)); - } - - // Sync a single movie - boolean syncSingleMovie = intent.getBooleanExtra(SYNC_SINGLE_MOVIE, false); - if (syncSingleMovie) { - int movieId = intent.getIntExtra(SYNC_MOVIEID, -1); - if (movieId != -1) { - syncOrchestrator.addSyncItem(new SyncMovies(hostInfo.getId(), movieId, syncExtras)); - } - } - - // Sync all tvshows - boolean syncAllTVShows = intent.getBooleanExtra(SYNC_ALL_TVSHOWS, false); - if (syncAllTVShows) { - syncOrchestrator.addSyncItem(new SyncTVShows(hostInfo.getId(), syncExtras)); - } - - // Sync a single tvshow - boolean syncSingleTVShow = intent.getBooleanExtra(SYNC_SINGLE_TVSHOW, false); - if (syncSingleTVShow) { - int tvshowId = intent.getIntExtra(SYNC_TVSHOWID, -1); - if (tvshowId != -1) { - syncOrchestrator.addSyncItem(new SyncTVShows(hostInfo.getId(), tvshowId, syncExtras)); - } - } - - // Sync all music - boolean syncAllMusic = intent.getBooleanExtra(SYNC_ALL_MUSIC, false); - if (syncAllMusic) { - syncOrchestrator.addSyncItem(new SyncMusic(hostInfo.getId(), syncExtras)); - } - - // Sync all music videos - boolean syncAllMusicVideos = intent.getBooleanExtra(SYNC_ALL_MUSIC_VIDEOS, false); - if (syncAllMusicVideos) { - syncOrchestrator.addSyncItem(new SyncMusicVideos(hostInfo.getId(), syncExtras)); - } - - // Start syncing - syncOrchestrator.startSync(); - - // If we get killed, after returning from here, don't restart - return START_NOT_STICKY; - } - - @Override - public IBinder onBind(Intent intent) { - return serviceBinder; - } - - @SuppressLint("NewApi") - @Override - public void onDestroy() { - LogUtils.LOGD(TAG, "Destroying the service."); - if (Utils.isJellybeanMR2OrLater()) { - handlerThread.quitSafely(); - } else { - handlerThread.quit(); - } - } - - public class LocalBinder extends Binder { - public LibrarySyncService getService() { - return LibrarySyncService.this; - } - } - - /** - * - * @param hostInfo host information for which to get items currently syncing - * @return currently syncing syncitems for given hostInfo - */ - public ArrayList getItemsSyncing(HostInfo hostInfo) { - ArrayList syncItems = new ArrayList<>(); - for( SyncOrchestrator orchestrator : syncOrchestrators) { - if( orchestrator.getHostInfo().getId() == hostInfo.getId() ) { - syncItems.addAll(orchestrator.getSyncItems()); - return syncItems; - } - } - return null; - } - - /** - * Orchestrator for a list os SyncItems - * Keeps a list of SyncItems to sync, and calls each one in order - * When finishes cleans up and stops the service by calling stopSelf - */ - private class SyncOrchestrator { - private ArrayDeque syncItems; - private Service syncService; - private final int serviceStartId; - private HostConnection hostConnection; - private final HostInfo hostInfo; - private final Handler callbackHandler; - private final ContentResolver contentResolver; - - private SyncItem currentSyncItem; - - private Iterator syncItemIterator; - - /** - * Constructor - * @param syncService Service on which to call {@link #stopSelf()} when finished - * @param startId Service startid to use when calling {@link #stopSelf()} - * @param hostInfo Host from which to sync - * @param callbackHandler Handler on which to post callbacks - * @param contentResolver Content resolver - */ - public SyncOrchestrator(Service syncService, final int startId, - final HostInfo hostInfo, - final Handler callbackHandler, - final ContentResolver contentResolver) { - this.syncService = syncService; - this.syncItems = new ArrayDeque(); - this.serviceStartId = startId; - this.hostInfo = hostInfo; - this.callbackHandler = callbackHandler; - this.contentResolver = contentResolver; - } - - public HostInfo getHostInfo() { - return hostInfo; - } - - /** - * Add this item to the sync list - * @param syncItem Sync item - */ - public void addSyncItem(SyncItem syncItem) { - syncItems.add(syncItem); - } - - public ArrayDeque getSyncItems() { - return syncItems; - } - - private long startTime = -1; - private long partialStartTime; - - /** - * Starts the syncing process - */ - public void startSync() { - startTime = System.currentTimeMillis(); - hostConnection = new HostConnection(hostInfo); - hostConnection.setProtocol(HostConnection.PROTOCOL_HTTP); - syncItemIterator = syncItems.iterator(); - nextSync(); - } - - /** - * Processes the next item on the sync list, or cleans up if it is finished. - */ - private void nextSync() { - if (syncItemIterator.hasNext()) { - partialStartTime = System.currentTimeMillis(); - currentSyncItem = syncItemIterator.next(); - currentSyncItem.sync(this, hostConnection, callbackHandler, contentResolver); - } else { - LogUtils.LOGD(TAG, "Sync finished for all items. Total time: " + - (System.currentTimeMillis() - startTime)); - // No more syncs, cleanup. - // No need to disconnect, as this is HTTP - //hostConnection.disconnect(); - - syncOrchestrators.remove(this); - syncService.stopSelf(serviceStartId); - } - } - - /** - * One of the syync items finish syncing - */ - private void syncItemFinished() { - LogUtils.LOGD(TAG, "Sync finished for item: " + currentSyncItem.getDescription() + - ". Total time: " + (System.currentTimeMillis() - partialStartTime)); - - EventBus.getDefault() - .post(new MediaSyncEvent(currentSyncItem.getSyncType(), - currentSyncItem.getSyncExtras(), - MediaSyncEvent.STATUS_SUCCESS)); - - syncItems.remove(currentSyncItem); - - nextSync(); - } - - /** - * One of the sync items failed, stop and clean up - * @param errorCode Error code - * @param description Description - */ - private void syncItemFailed(int errorCode, String description) { - LogUtils.LOGD(TAG, "A Sync item has got an error. Sync item: " + - currentSyncItem.getDescription() + - ". Error description: " + description); - // No need to disconnect, as this is HTTP - //hostConnection.disconnect(); - EventBus.getDefault() - .post(new MediaSyncEvent(currentSyncItem.getSyncType(), - currentSyncItem.getSyncExtras(), - MediaSyncEvent.STATUS_FAIL, errorCode, description)); - // Keep syncing till the end - nextSync(); - //syncService.stopSelf(serviceStartId); - } - } - - /** - * Represent an item that can be synced - */ - public interface SyncItem { - /** - * Syncs an item from the XBMC host to the local database - * @param orchestrator Orchestrator to call when finished - * @param hostConnection Host connection to use - * @param callbackHandler Handler on which to post callbacks - * @param contentResolver Content resolver - */ - public void sync(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver); - - /** - * Friendly description of this sync item - * @return Description - */ - public String getDescription(); - - /** - * Returns the sync event that should be posted after completion - * @return Sync type, one of the constants in {@link org.xbmc.kore.service.LibrarySyncService} - */ - public String getSyncType(); - - /** - * Returns the extras that were passed during creation. - * Allows the caller to pass parameters that will be sent back to him - * @return Sync extras passed during construction - */ - public Bundle getSyncExtras(); - } - - /** - * Syncs all the movies on XBMC or a specific movie, to the local database - */ - private static class SyncMovies implements SyncItem { - private final int hostId; - private final int movieId; - private final Bundle syncExtras; - - /** - * Syncs all the movies on selected XBMC to the local database - * @param hostId XBMC host id - */ - public SyncMovies(final int hostId, Bundle syncExtras) { - this.hostId = hostId; - this.movieId = -1; - this.syncExtras = syncExtras; - } - - /** - * Syncs a specific movie on selected XBMC to the local database - * @param hostId XBMC host id - */ - public SyncMovies(final int hostId, final int movieId, Bundle syncExtras) { - this.hostId = hostId; - this.movieId = movieId; - this.syncExtras = syncExtras; - } - - /** {@inheritDoc} */ - public String getDescription() { - return (movieId != -1) ? - "Sync movies for host: " + hostId : - "Sync movie " + movieId + " for host: " + hostId; - } - - /** {@inheritDoc} */ - public String getSyncType() { - return (movieId == -1) ? SYNC_ALL_MOVIES : SYNC_SINGLE_MOVIE; - } - - /** {@inheritDoc} */ - public Bundle getSyncExtras() { - return syncExtras; - } - - /** {@inheritDoc} */ - public void sync(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver) { - String properties[] = { - VideoType.FieldsMovie.TITLE, VideoType.FieldsMovie.GENRE, - VideoType.FieldsMovie.YEAR, VideoType.FieldsMovie.RATING, - VideoType.FieldsMovie.DIRECTOR, VideoType.FieldsMovie.TRAILER, - VideoType.FieldsMovie.TAGLINE, VideoType.FieldsMovie.PLOT, - // VideoType.FieldsMovie.PLOTOUTLINE, VideoType.FieldsMovie.ORIGINALTITLE, - // VideoType.FieldsMovie.LASTPLAYED, - VideoType.FieldsMovie.PLAYCOUNT, VideoType.FieldsMovie.DATEADDED, - VideoType.FieldsMovie.WRITER, VideoType.FieldsMovie.STUDIO, - VideoType.FieldsMovie.MPAA, VideoType.FieldsMovie.CAST, - VideoType.FieldsMovie.COUNTRY, VideoType.FieldsMovie.IMDBNUMBER, - VideoType.FieldsMovie.RUNTIME, VideoType.FieldsMovie.SET, - // VideoType.FieldsMovie.SHOWLINK, - VideoType.FieldsMovie.STREAMDETAILS, VideoType.FieldsMovie.TOP250, - VideoType.FieldsMovie.VOTES, VideoType.FieldsMovie.FANART, - VideoType.FieldsMovie.THUMBNAIL, VideoType.FieldsMovie.FILE, - // VideoType.FieldsMovie.SORTTITLE, VideoType.FieldsMovie.RESUME, - VideoType.FieldsMovie.SETID, - // VideoType.FieldsMovie.DATEADDED, VideoType.FieldsMovie.TAG, - // VideoType.FieldsMovie.ART - }; - - if (movieId == -1) { - syncAllMovies(orchestrator, hostConnection, callbackHandler, contentResolver, properties, 0); - } else { - // Sync a specific movie - VideoLibrary.GetMovieDetails action = - new VideoLibrary.GetMovieDetails(movieId, properties); - action.execute(hostConnection, new ApiCallback() { - @Override - public void onSuccess(VideoType.DetailsMovie result) { - deleteMovies(contentResolver, hostId, movieId); - List movies = new ArrayList(1); - movies.add(result); - insertMovies(orchestrator, contentResolver, movies); - orchestrator.syncItemFinished(); - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - } - - /** - * Syncs all the movies, calling itself recursively - * Uses the {@link VideoLibrary.GetMovies} version with limits to make sure - * that Kodi doesn't blow up, and calls itself recursively until all the - * movies are returned - */ - private void syncAllMovies(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final String properties[], - final int startIdx) { - // Call GetMovies with the current limits set - ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_MOVIES); - VideoLibrary.GetMovies action = new VideoLibrary.GetMovies(limits, properties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (startIdx == 0) { - // First call, delete movies from DB - deleteMovies(contentResolver, hostId, -1); - } - if (result.size() > 0) { - insertMovies(orchestrator, contentResolver, result); - } - - LogUtils.LOGD(TAG, "syncAllMovies, movies gotten: " + result.size()); - if (result.size() == LIMIT_SYNC_MOVIES) { - // Max limit returned, there may be some more movies - // As we're going to recurse, these result objects can add up, so - // let's help the GC and indicate that we don't need this memory - // (hopefully this works) - result = null; - syncAllMovies(orchestrator, hostConnection, callbackHandler, contentResolver, - properties, startIdx + LIMIT_SYNC_MOVIES); - } else { - // Less than the limit was returned so we can finish - // (if it returned more there's a bug in Kodi but it - // shouldn't be a problem as they got inserted in the DB) - orchestrator.syncItemFinished(); - } - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - /** - * Deletes one or all movies from the database (pass -1 on movieId to delete all) - */ - private void deleteMovies(final ContentResolver contentResolver, - int hostId, int movieId) { - if (movieId == -1) { - // Delete all movies - String where = MediaContract.MoviesColumns.HOST_ID + "=?"; - contentResolver.delete(MediaContract.MovieCast.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.Movies.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - } else { - // Delete a movie - contentResolver.delete(MediaContract.MovieCast.buildMovieCastListUri(hostId, movieId), - null, null); - contentResolver.delete(MediaContract.Movies.buildMovieUri(hostId, movieId), - null, null); - } - } - - /** - * Inserts the given movies in the database - */ - private void insertMovies(final SyncOrchestrator orchestrator, - final ContentResolver contentResolver, - final List movies) { - ContentValues movieValuesBatch[] = new ContentValues[movies.size()]; - int castCount = 0; - - // Iterate on each movie - for (int i = 0; i < movies.size(); i++) { - VideoType.DetailsMovie movie = movies.get(i); - movieValuesBatch[i] = SyncUtils.contentValuesFromMovie(hostId, movie); - castCount += movie.cast.size(); - } - - // Insert the movies - contentResolver.bulkInsert(MediaContract.Movies.CONTENT_URI, movieValuesBatch); - - ContentValues movieCastValuesBatch[] = new ContentValues[castCount]; - int count = 0; - // Iterate on each movie/cast - for (VideoType.DetailsMovie movie : movies) { - for (VideoType.Cast cast : movie.cast) { - movieCastValuesBatch[count] = SyncUtils.contentValuesFromCast(hostId, cast); - movieCastValuesBatch[count].put(MediaContract.MovieCastColumns.MOVIEID, movie.movieid); - count++; - } - } - - // Insert the cast list for this movie - contentResolver.bulkInsert(MediaContract.MovieCast.CONTENT_URI, movieCastValuesBatch); - } - } - - /** - * Syncs all the TV shows or a specific show and its information to the local database - */ - private static class SyncTVShows implements SyncItem { - private final int hostId; - private final int tvshowId; - private final Bundle syncExtras; - - /** - * Syncs all the TVShows on selected XBMC to the local database - * @param hostId XBMC host id - */ - public SyncTVShows(final int hostId, Bundle syncExtras) { - this.hostId = hostId; - this.tvshowId = -1; - this.syncExtras = syncExtras; - } - - /** - * Syncs a specific TVShow to the local database - * @param hostId XBMC host id - * @param tvshowId Show to sync - */ - public SyncTVShows(final int hostId, final int tvshowId, Bundle syncExtras) { - this.hostId = hostId; - this.tvshowId = tvshowId; - this.syncExtras = syncExtras; - } - - /** {@inheritDoc} */ - public String getDescription() { - return (tvshowId != -1) ? - "Sync TV shows for host: " + hostId : - "Sync TV show " + tvshowId + " for host: " + hostId; - } - - /** {@inheritDoc} */ - public String getSyncType() { - return (tvshowId == -1) ? SYNC_ALL_TVSHOWS : SYNC_SINGLE_TVSHOW; - } - - /** {@inheritDoc} */ - public Bundle getSyncExtras() { - return syncExtras; - } - - private final static String getTVShowsProperties[] = { - VideoType.FieldsTVShow.TITLE, VideoType.FieldsTVShow.GENRE, - //VideoType.FieldsTVShow.YEAR, - VideoType.FieldsTVShow.RATING, VideoType.FieldsTVShow.PLOT, - VideoType.FieldsTVShow.STUDIO, VideoType.FieldsTVShow.MPAA, - VideoType.FieldsTVShow.CAST, VideoType.FieldsTVShow.PLAYCOUNT, - VideoType.FieldsTVShow.EPISODE, VideoType.FieldsTVShow.IMDBNUMBER, - VideoType.FieldsTVShow.PREMIERED, - //VideoType.FieldsTVShow.VOTES, VideoType.FieldsTVShow.LASTPLAYED, - VideoType.FieldsTVShow.FANART, VideoType.FieldsTVShow.THUMBNAIL, - VideoType.FieldsTVShow.FILE, - //VideoType.FieldsTVShow.ORIGINALTITLE, VideoType.FieldsTVShow.SORTTITLE, - // VideoType.FieldsTVShow.EPISODEGUIDE, VideoType.FieldsTVShow.SEASON, - VideoType.FieldsTVShow.WATCHEDEPISODES, VideoType.FieldsTVShow.DATEADDED, - //VideoType.FieldsTVShow.TAG, VideoType.FieldsTVShow.ART - }; - /** {@inheritDoc} */ - public void sync(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver) { - if (tvshowId == -1) { - syncAllTVShows(orchestrator, hostConnection, callbackHandler, contentResolver, - 0, new ArrayList()); - } else { - VideoLibrary.GetTVShowDetails action = - new VideoLibrary.GetTVShowDetails(tvshowId, getTVShowsProperties); - action.execute(hostConnection, new ApiCallback() { - @Override - public void onSuccess(VideoType.DetailsTVShow result) { - deleteTVShows(contentResolver, hostId, tvshowId); - List tvShows = new ArrayList<>(1); - tvShows.add(result); - insertTVShowsAndGetDetails(orchestrator, hostConnection, callbackHandler, - contentResolver, tvShows); - // insertTVShows calls syncItemFinished - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - } - - /** - * Syncs all the TV shows, calling itself recursively - * Uses the {@link VideoLibrary.GetTVShows} version with limits to make sure - * that Kodi doesn't blow up, and calls itself recursively until all the - * shows are returned - */ - private void syncAllTVShows(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final int startIdx, - final List allResults) { - // Call GetTVShows with the current limits set - ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_TVSHOWS); - VideoLibrary.GetTVShows action = new VideoLibrary.GetTVShows(limits, getTVShowsProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - allResults.addAll(result); - if (result.size() == LIMIT_SYNC_TVSHOWS) { - // Max limit returned, there may be some more movies - LogUtils.LOGD(TAG, "syncAllTVShows: More tv shows on media center, recursing."); - syncAllTVShows(orchestrator, hostConnection, callbackHandler, contentResolver, - startIdx + LIMIT_SYNC_TVSHOWS, allResults); - } else { - // Ok, we have all the shows, insert them - LogUtils.LOGD(TAG, "syncAllTVShows: Got all tv shows. Total: " + allResults.size()); - deleteTVShows(contentResolver, hostId, -1); - insertTVShowsAndGetDetails(orchestrator, hostConnection, callbackHandler, - contentResolver, allResults); - } - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - private void deleteTVShows(final ContentResolver contentResolver, - int hostId, int tvshowId) { - if (tvshowId == -1) { - LogUtils.LOGD(TAG, "Deleting all existing tv shows: "); - // Delete all tvshows - String where = MediaContract.TVShowsColumns.HOST_ID + "=?"; - contentResolver.delete(MediaContract.Episodes.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.Seasons.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.TVShowCast.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.TVShows.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - } else { - // Delete a specific tvshow - contentResolver.delete(MediaContract.Episodes.buildTVShowEpisodesListUri(hostId, tvshowId), - null, null); - contentResolver.delete(MediaContract.Seasons.buildTVShowSeasonsListUri(hostId, tvshowId), - null, null); - contentResolver.delete(MediaContract.TVShowCast.buildTVShowCastListUri(hostId, tvshowId), - null, null); - contentResolver.delete(MediaContract.TVShows.buildTVShowUri(hostId, tvshowId), - null, null); - } - } - - private void insertTVShowsAndGetDetails(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - List tvShows) { - ContentValues tvshowsValuesBatch[] = new ContentValues[tvShows.size()]; - int castCount = 0; - - // Iterate on each show - for (int i = 0; i < tvShows.size(); i++) { - VideoType.DetailsTVShow tvshow = tvShows.get(i); - tvshowsValuesBatch[i] = SyncUtils.contentValuesFromTVShow(hostId, tvshow); - castCount += tvshow.cast.size(); - } - // Insert the tvshows - contentResolver.bulkInsert(MediaContract.TVShows.CONTENT_URI, tvshowsValuesBatch); - LogUtils.LOGD(TAG, "Inserted " + tvShows.size() + " tv shows."); - - ContentValues tvshowsCastValuesBatch[] = new ContentValues[castCount]; - int count = 0; - // Iterate on each show/cast - for (VideoType.DetailsTVShow tvshow : tvShows) { - for (VideoType.Cast cast : tvshow.cast) { - tvshowsCastValuesBatch[count] = SyncUtils.contentValuesFromCast(hostId, cast); - tvshowsCastValuesBatch[count].put(MediaContract.TVShowCastColumns.TVSHOWID, tvshow.tvshowid); - count++; - } - } - // Insert the cast list for this movie - contentResolver.bulkInsert(MediaContract.TVShowCast.CONTENT_URI, tvshowsCastValuesBatch); - - // Start the sequential syncing of seasons - chainSyncSeasons(orchestrator, hostConnection, callbackHandler, - contentResolver, tvShows, 0); - } - - private final static String seasonsProperties[] = { - VideoType.FieldsSeason.SEASON, VideoType.FieldsSeason.SHOWTITLE, - //VideoType.FieldsSeason.PLAYCOUNT, - VideoType.FieldsSeason.EPISODE, - VideoType.FieldsSeason.FANART, VideoType.FieldsSeason.THUMBNAIL, - VideoType.FieldsSeason.TVSHOWID, VideoType.FieldsSeason.WATCHEDEPISODES, - //VideoType.FieldsSeason.ART - }; - - /** - * Sequentially syncs seasons for the tvshow specified, and on success recursively calls - * itself to sync the next tvshow on the list. - * This basically iterates through the tvshows list updating the seasons, - * in a sequential manner (defeating the parallel nature of host calls) - * After processing all tvshows on the list, starts the episode syncing - * - * @param orchestrator Orchestrator to call when finished - * @param hostConnection Host connection to use - * @param callbackHandler Handler on which to post callbacks - * @param contentResolver Content resolver - * @param tvShows TV shows list to get seasons to - * @param position Position of the tvshow on the list to process - */ - private void chainSyncSeasons(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final List tvShows, - final int position) { - if (position < tvShows.size()) { - // Process this tvshow - final VideoType.DetailsTVShow tvShow = tvShows.get(position); - - VideoLibrary.GetSeasons action = new VideoLibrary.GetSeasons(tvShow.tvshowid, seasonsProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - ContentValues seasonsValuesBatch[] = new ContentValues[result.size()]; - int totalWatchedEpisodes = 0; - for (int i = 0; i < result.size(); i++) { - VideoType.DetailsSeason season = result.get(i); - seasonsValuesBatch[i] = SyncUtils.contentValuesFromSeason(hostId, season); - - totalWatchedEpisodes += season.watchedepisodes; - } - // Insert the seasons - contentResolver.bulkInsert(MediaContract.Seasons.CONTENT_URI, seasonsValuesBatch); - - if (getSyncType().equals(SYNC_SINGLE_TVSHOW)) { - // HACK: Update watched episodes count for the tvshow with the sum - // of watched episodes from seasons, given that the value that we - // got from XBMC from the call to GetTVShowDetails is wrong (note - // that the value returned from GetTVShows is correct). - Uri uri = MediaContract.TVShows.buildTVShowUri(hostId, tvShow.tvshowid); - ContentValues tvshowUpdate = new ContentValues(1); - tvshowUpdate.put(MediaContract.TVShowsColumns.WATCHEDEPISODES, totalWatchedEpisodes); - contentResolver.update(uri, tvshowUpdate, null, null); - } - - // Sync the next tv show - chainSyncSeasons(orchestrator, hostConnection, callbackHandler, - contentResolver, tvShows, position + 1); - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } else { - // We've processed all tvshows, start episode syncing - chainSyncEpisodes(orchestrator, hostConnection, callbackHandler, - contentResolver, tvShows, 0); - } - } - - private final static String getEpisodesProperties[] = { - VideoType.FieldsEpisode.TITLE, VideoType.FieldsEpisode.PLOT, - //VideoType.FieldsEpisode.VOTES, - VideoType.FieldsEpisode.RATING, - VideoType.FieldsEpisode.WRITER, VideoType.FieldsEpisode.FIRSTAIRED, - VideoType.FieldsEpisode.PLAYCOUNT, VideoType.FieldsEpisode.RUNTIME, - VideoType.FieldsEpisode.DIRECTOR, - //VideoType.FieldsEpisode.PRODUCTIONCODE, - VideoType.FieldsEpisode.SEASON, - VideoType.FieldsEpisode.EPISODE, - //VideoType.FieldsEpisode.ORIGINALTITLE, - VideoType.FieldsEpisode.SHOWTITLE, - //VideoType.FieldsEpisode.CAST, - VideoType.FieldsEpisode.STREAMDETAILS, - //VideoType.FieldsEpisode.LASTPLAYED, - VideoType.FieldsEpisode.FANART, VideoType.FieldsEpisode.THUMBNAIL, - VideoType.FieldsEpisode.FILE, - //VideoType.FieldsEpisode.RESUME, - VideoType.FieldsEpisode.TVSHOWID, VideoType.FieldsEpisode.DATEADDED, - //VideoType.FieldsEpisode.UNIQUEID, VideoType.FieldsEpisode.ART - }; - - /** - * Sequentially syncs episodes for the tvshow specified, and on success recursively calls - * itself to sync the next tvshow on the list. - * This basically iterates through the tvshows list updating the episodes, - * in a sequential manner (defeating the parallel nature of host calls) - * - * @param orchestrator Orchestrator to call when finished - * @param hostConnection Host connection to use - * @param callbackHandler Handler on which to post callbacks - * @param contentResolver Content resolver - * @param tvShows TV shows list to get episodes to - * @param position Position of the tvshow on the list to process - */ - private void chainSyncEpisodes(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final List tvShows, - final int position) { - if (position < tvShows.size()) { - VideoType.DetailsTVShow tvShow = tvShows.get(position); - - VideoLibrary.GetEpisodes action = new VideoLibrary.GetEpisodes(tvShow.tvshowid, getEpisodesProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - ContentValues episodesValuesBatch[] = new ContentValues[result.size()]; - for (int i = 0; i < result.size(); i++) { - VideoType.DetailsEpisode episode = result.get(i); - episodesValuesBatch[i] = SyncUtils.contentValuesFromEpisode(hostId, episode); - } - // Insert the episodes - contentResolver.bulkInsert(MediaContract.Episodes.CONTENT_URI, episodesValuesBatch); - - chainSyncEpisodes(orchestrator, hostConnection, callbackHandler, - contentResolver, tvShows, position + 1); - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } else { - // We're finished - LogUtils.LOGD(TAG, "Sync tv shows finished successfully"); - orchestrator.syncItemFinished(); - } - } - } - - /** - * Syncs all the music on XBMC to the local database - */ - private static class SyncMusic implements SyncItem { - private final int hostId; - private final Bundle syncExtras; - - /** - * Syncs all the music on selected XBMC to the local database - * @param hostId XBMC host id - */ - public SyncMusic(final int hostId, Bundle syncExtras) { - this.hostId = hostId; - this.syncExtras = syncExtras; - } - - /** {@inheritDoc} */ - public String getDescription() { - return "Sync music for host: " + hostId; - } - - /** {@inheritDoc} */ - public String getSyncType() { return SYNC_ALL_MUSIC; } - - /** {@inheritDoc} */ - public Bundle getSyncExtras() { - return syncExtras; - } - - /** {@inheritDoc} */ - public void sync(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver) { - chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver, 0); - } - - private final static String getArtistsProperties[] = { - // AudioType.FieldsArtists.INSTRUMENT, AudioType.FieldsArtists.STYLE, - // AudioType.FieldsArtists.MOOD, AudioType.FieldsArtists.BORN, - // AudioType.FieldsArtists.FORMED, - AudioType.FieldsArtists.DESCRIPTION, - AudioType.FieldsArtists.GENRE, - // AudioType.FieldsArtists.DIED, - // AudioType.FieldsArtists.DISBANDED, AudioType.FieldsArtists.YEARSACTIVE, - //AudioType.FieldsArtists.MUSICBRAINZARTISTID, - AudioType.FieldsArtists.FANART, - AudioType.FieldsArtists.THUMBNAIL - }; - /** - * Gets all artists recursively and forwards the call to Genres - * Genres->Albums->Songs - */ - public void chainCallSyncArtists(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final int startIdx) { - // Artists->Genres->Albums->Songs - // Only gets album artists (first parameter) - ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_ARTISTS); - AudioLibrary.GetArtists action = new AudioLibrary.GetArtists(limits, true, getArtistsProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (result == null) result = new ArrayList<>(0); // Safeguard - // First delete all music info - if (startIdx == 0) deleteMusicInfo(contentResolver, hostId); - - // Insert artists - ContentValues artistValuesBatch[] = new ContentValues[result.size()]; - for (int i = 0; i < result.size(); i++) { - AudioType.DetailsArtist artist = result.get(i); - artistValuesBatch[i] = SyncUtils.contentValuesFromArtist(hostId, artist); - } - contentResolver.bulkInsert(MediaContract.Artists.CONTENT_URI, artistValuesBatch); - - if (result.size() == LIMIT_SYNC_ARTISTS) { - // Max limit returned, there may be some more - LogUtils.LOGD(TAG, "chainCallSyncArtists: More results on media center, recursing."); - result = null; // Help the GC? - chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver, - startIdx + LIMIT_SYNC_ARTISTS); - } else { - // Ok, we have all the artists, proceed - LogUtils.LOGD(TAG, "chainCallSyncArtists: Got all results, continuing"); - chainCallSyncGenres(orchestrator, hostConnection, callbackHandler, contentResolver); - } - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - private void deleteMusicInfo(final ContentResolver contentResolver, - int hostId) { - // Delete music info - String where = MediaContract.Artists.HOST_ID + "=?"; - contentResolver.delete(MediaContract.AlbumArtists.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.AlbumGenres.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.Songs.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.AudioGenres.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.Albums.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - contentResolver.delete(MediaContract.Artists.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - } - - - private final static String getGenresProperties[] = { - LibraryType.FieldsGenre.TITLE, LibraryType.FieldsGenre.THUMBNAIL - }; - /** - * Syncs Audio genres and forwards calls to sync albums: - * Genres->Albums->Songs - */ - private void chainCallSyncGenres(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver) { - // Genres->Albums->Songs - AudioLibrary.GetGenres action = new AudioLibrary.GetGenres(getGenresProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (result == null) result = new ArrayList<>(0); // Safeguard - ContentValues genresValuesBatch[] = new ContentValues[result.size()]; - - for (int i = 0; i < result.size(); i++) { - LibraryType.DetailsGenre genre = result.get(i); - genresValuesBatch[i] = SyncUtils.contentValuesFromAudioGenre(hostId, genre); - } - - // Insert the genres and proceed to albums - contentResolver.bulkInsert(MediaContract.AudioGenres.CONTENT_URI, genresValuesBatch); - chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver, 0); - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - private static final String getAlbumsProperties[] = { - AudioType.FieldsAlbum.TITLE, AudioType.FieldsAlbum.DESCRIPTION, - AudioType.FieldsAlbum.ARTIST, AudioType.FieldsAlbum.GENRE, - //AudioType.FieldsAlbum.THEME, AudioType.FieldsAlbum.MOOD, - //AudioType.FieldsAlbum.STYLE, AudioType.FieldsAlbum.TYPE, - AudioType.FieldsAlbum.ALBUMLABEL, AudioType.FieldsAlbum.RATING, - AudioType.FieldsAlbum.YEAR, - //AudioType.FieldsAlbum.MUSICBRAINZALBUMID, - //AudioType.FieldsAlbum.MUSICBRAINZALBUMARTISTID, - AudioType.FieldsAlbum.FANART, AudioType.FieldsAlbum.THUMBNAIL, - AudioType.FieldsAlbum.PLAYCOUNT, AudioType.FieldsAlbum.GENREID, - AudioType.FieldsAlbum.ARTISTID, AudioType.FieldsAlbum.DISPLAYARTIST - }; - - /** - * Syncs Albums recursively and forwards calls to sync songs: - * Albums->Songs - */ - private void chainCallSyncAlbums(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final int startIdx) { - final long albumSyncStartTime = System.currentTimeMillis(); - // Albums->Songs - ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_ALBUMS); - AudioLibrary.GetAlbums action = new AudioLibrary.GetAlbums(limits, getAlbumsProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (result == null) result = new ArrayList<>(0); // Safeguard - // Insert the partial results - ContentValues albumValuesBatch[] = new ContentValues[result.size()]; - int artistsCount = 0, genresCount = 0; - for (int i = 0; i < result.size(); i++) { - AudioType.DetailsAlbum album = result.get(i); - albumValuesBatch[i] = SyncUtils.contentValuesFromAlbum(hostId, album); - - artistsCount += album.artistid.size(); - genresCount += album.genreid.size(); - } - contentResolver.bulkInsert(MediaContract.Albums.CONTENT_URI, albumValuesBatch); - - LogUtils.LOGD(TAG, "Finished inserting albums in: " + - (System.currentTimeMillis() - albumSyncStartTime)); - - // Iterate on each album, collect the artists and the genres and insert them - ContentValues albumArtistsValuesBatch[] = new ContentValues[artistsCount]; - ContentValues albumGenresValuesBatch[] = new ContentValues[genresCount]; - int artistCount = 0, genreCount = 0; - for (AudioType.DetailsAlbum album : result) { - for (int artistId : album.artistid) { - albumArtistsValuesBatch[artistCount] = new ContentValues(); - albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.HOST_ID, hostId); - albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ALBUMID, album.albumid); - albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ARTISTID, artistId); - artistCount++; - } - - for (int genreId : album.genreid) { - albumGenresValuesBatch[genreCount] = new ContentValues(); - albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.HOST_ID, hostId); - albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.ALBUMID, album.albumid); - albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.GENREID, genreId); - genreCount++; - } - } - - contentResolver.bulkInsert(MediaContract.AlbumArtists.CONTENT_URI, albumArtistsValuesBatch); - contentResolver.bulkInsert(MediaContract.AlbumGenres.CONTENT_URI, albumGenresValuesBatch); - - LogUtils.LOGD(TAG, "Finished inserting artists and genres in: " + - (System.currentTimeMillis() - albumSyncStartTime)); - - if (result.size() == LIMIT_SYNC_ALBUMS) { - // Max limit returned, there may be some more - LogUtils.LOGD(TAG, "chainCallSyncAlbums: More results on media center, recursing."); - result = null; // Help the GC? - chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver, - startIdx + LIMIT_SYNC_ALBUMS); - } else { - // Ok, we have all the albums, proceed to songs - LogUtils.LOGD(TAG, "chainCallSyncAlbums: Got all results, continuing"); - chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver, 0); - } - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - private static final String getSongsProperties[] = { - AudioType.FieldsSong.TITLE, - //AudioType.FieldsSong.ARTIST, AudioType.FieldsSong.ALBUMARTIST, AudioType.FieldsSong.GENRE, - //AudioType.FieldsSong.YEAR, AudioType.FieldsSong.RATING, - //AudioType.FieldsSong.ALBUM, - AudioType.FieldsSong.TRACK, AudioType.FieldsSong.DURATION, - //AudioType.FieldsSong.COMMENT, AudioType.FieldsSong.LYRICS, - //AudioType.FieldsSong.MUSICBRAINZTRACKID, - //AudioType.FieldsSong.MUSICBRAINZARTISTID, - //AudioType.FieldsSong.MUSICBRAINZALBUMID, - //AudioType.FieldsSong.MUSICBRAINZALBUMARTISTID, - //AudioType.FieldsSong.PLAYCOUNT, AudioType.FieldsSong.FANART, - AudioType.FieldsSong.THUMBNAIL, AudioType.FieldsSong.FILE, - AudioType.FieldsSong.ALBUMID, - //AudioType.FieldsSong.LASTPLAYED, AudioType.FieldsSong.DISC, - //AudioType.FieldsSong.GENREID, AudioType.FieldsSong.ARTISTID, - //AudioType.FieldsSong.DISPLAYARTIST, AudioType.FieldsSong.ALBUMARTISTID - }; - - /** - * Syncs songs and stops - */ - private void chainCallSyncSongs(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver, - final int startIdx) { - // Songs - ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_SONGS); - AudioLibrary.GetSongs action = new AudioLibrary.GetSongs(limits, getSongsProperties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (result == null) result = new ArrayList<>(0); // Safeguard - // Save partial results to DB - ContentValues songValuesBatch[] = new ContentValues[result.size()]; - for (int i = 0; i < result.size(); i++) { - AudioType.DetailsSong song = result.get(i); - songValuesBatch[i] = SyncUtils.contentValuesFromSong(hostId, song); - } - contentResolver.bulkInsert(MediaContract.Songs.CONTENT_URI, songValuesBatch); - - if (result.size() == LIMIT_SYNC_SONGS) { - // Max limit returned, there may be some more - LogUtils.LOGD(TAG, "chainCallSyncSongs: More results on media center, recursing."); - result = null; // Help the GC? - chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver, - startIdx + LIMIT_SYNC_SONGS); - } else { - // Ok, we have all the songs, insert them - LogUtils.LOGD(TAG, "chainCallSyncSongs: Got all results, continuing"); - orchestrator.syncItemFinished(); - } - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - } - - /** - * Syncs all the music videos on XBMC, to the local database - */ - private static class SyncMusicVideos implements SyncItem { - private final int hostId; - private final Bundle syncExtras; - - /** - * Syncs all the music videos on XBMC, to the local database - * @param hostId XBMC host id - */ - public SyncMusicVideos(final int hostId, Bundle syncExtras) { - this.hostId = hostId; - this.syncExtras = syncExtras; - } - - /** {@inheritDoc} */ - public String getDescription() { - return "Sync music videos for host: " + hostId; - } - - /** {@inheritDoc} */ - public String getSyncType() { - return SYNC_ALL_MUSIC_VIDEOS; - } - - /** {@inheritDoc} */ - public Bundle getSyncExtras() { - return syncExtras; - } - - /** {@inheritDoc} */ - public void sync(final SyncOrchestrator orchestrator, - final HostConnection hostConnection, - final Handler callbackHandler, - final ContentResolver contentResolver) { - String properties[] = { - VideoType.FieldsMusicVideo.TITLE, VideoType.FieldsMusicVideo.PLAYCOUNT, - VideoType.FieldsMusicVideo.RUNTIME, VideoType.FieldsMusicVideo.DIRECTOR, - VideoType.FieldsMusicVideo.STUDIO, VideoType.FieldsMusicVideo.YEAR, - VideoType.FieldsMusicVideo.PLOT, VideoType.FieldsMusicVideo.ALBUM, - VideoType.FieldsMusicVideo.ARTIST, VideoType.FieldsMusicVideo.GENRE, - VideoType.FieldsMusicVideo.TRACK, VideoType.FieldsMusicVideo.STREAMDETAILS, - //VideoType.FieldsMusicVideo.LASTPLAYED, - VideoType.FieldsMusicVideo.FANART, - VideoType.FieldsMusicVideo.THUMBNAIL, VideoType.FieldsMusicVideo.FILE, - // VideoType.FieldsMusicVideo.RESUME, VideoType.FieldsMusicVideo.DATEADDED, - VideoType.FieldsMusicVideo.TAG, - //VideoType.FieldsMusicVideo.ART - }; - - // Delete and sync all music videos - VideoLibrary.GetMusicVideos action = new VideoLibrary.GetMusicVideos(properties); - action.execute(hostConnection, new ApiCallback>() { - @Override - public void onSuccess(List result) { - deleteMusicVideos(contentResolver, hostId); - insertMusicVideos(orchestrator, contentResolver, result); - } - - @Override - public void onError(int errorCode, String description) { - // Ok, something bad happend, just quit - orchestrator.syncItemFailed(errorCode, description); - } - }, callbackHandler); - } - - private void deleteMusicVideos(final ContentResolver contentResolver, int hostId) { - // Delete all music videos - String where = MediaContract.MusicVideosColumns.HOST_ID + "=?"; - contentResolver.delete(MediaContract.MusicVideos.CONTENT_URI, - where, new String[]{String.valueOf(hostId)}); - } - - private void insertMusicVideos(final SyncOrchestrator orchestrator, - final ContentResolver contentResolver, - final List musicVideos) { - ContentValues musicVideosValuesBatch[] = new ContentValues[musicVideos.size()]; - - // Iterate on each music video - for (int i = 0; i < musicVideos.size(); i++) { - VideoType.DetailsMusicVideo musicVideo = musicVideos.get(i); - musicVideosValuesBatch[i] = SyncUtils.contentValuesFromMusicVideo(hostId, musicVideo); - } - - // Insert the movies - contentResolver.bulkInsert(MediaContract.MusicVideos.CONTENT_URI, musicVideosValuesBatch); - orchestrator.syncItemFinished(); - } - } - -} diff --git a/app/src/main/java/org/xbmc/kore/service/library/LibrarySyncService.java b/app/src/main/java/org/xbmc/kore/service/library/LibrarySyncService.java new file mode 100644 index 0000000..8034285 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/LibrarySyncService.java @@ -0,0 +1,196 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Process; + +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.Utils; + +import java.util.ArrayList; + +/** + * Service that syncs the XBMC local database with the remote library + */ +public class LibrarySyncService extends Service { + public static final String TAG = LogUtils.makeLogTag(LibrarySyncService.class); + + /** + * Possible requests to sync + */ + public static final String SYNC_ALL_MOVIES = "sync_all_movies"; + public static final String SYNC_SINGLE_MOVIE = "sync_single_movie"; + public static final String SYNC_ALL_TVSHOWS = "sync_all_tvshows"; + public static final String SYNC_SINGLE_TVSHOW = "sync_single_tvshow"; + public static final String SYNC_ALL_MUSIC = "sync_all_music"; + public static final String SYNC_ALL_MUSIC_VIDEOS = "sync_all_music_videos"; + + public static final String SYNC_MOVIEID = "sync_movieid"; + public static final String SYNC_TVSHOWID = "sync_tvshowid"; + + /** + * Extra used to pass parameters that will be sent back to the caller + */ + public static final String SYNC_EXTRAS = "sync_extras"; + + /** + * Constant for UI to use to signal a silent sync (pass these in SYNC_EXTRAS) + */ + public static final String SILENT_SYNC = "silent_sync"; + + /** + * Our handler to post callbacks from {@link HostConnection} calls + */ + private Handler callbackHandler; + private HandlerThread handlerThread; + + private ArrayList syncOrchestrators; + + private final IBinder serviceBinder = new LocalBinder(); + + @Override + public void onCreate() { + // Create a Handler Thread to process callback calls after the Xbmc method call + handlerThread = new HandlerThread("LibrarySyncService", Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + + // Get the HandlerThread's Looper and use it for our Handler + callbackHandler = new Handler(handlerThread.getLooper()); + // Check which libraries to update and call the corresponding methods on Xbmc + + syncOrchestrators = new ArrayList<>(); + } + + @Override + public int onStartCommand(Intent intent, int flags, final int startId) { + // Get the connection here, not on create because we can be called for different hosts + // We'll use a specific connection through HTTP, not the singleton one, + // to not interfere with the normal application usage of it (namely calls to disconnect + // and usage of the socket). + HostInfo hostInfo = HostManager.getInstance(this).getHostInfo(); + + SyncOrchestrator syncOrchestrator = new SyncOrchestrator(this, startId, hostInfo, + callbackHandler, getContentResolver()); + syncOrchestrator.setListener(new SyncOrchestrator.OnSyncListener() { + @Override + public void onSyncFinished(SyncOrchestrator syncOrchestrator) { + stopSelf(startId); + } + }); + + syncOrchestrators.add(syncOrchestrator); + + // Get the request parameters that we should pass when calling back the caller + Bundle syncExtras = intent.getBundleExtra(SYNC_EXTRAS); + + // Sync all movies + boolean syncAllMovies = intent.getBooleanExtra(SYNC_ALL_MOVIES, false); + if (syncAllMovies) { + syncOrchestrator.addSyncItem(new SyncMovies(hostInfo.getId(), syncExtras)); + } + + // Sync a single movie + boolean syncSingleMovie = intent.getBooleanExtra(SYNC_SINGLE_MOVIE, false); + if (syncSingleMovie) { + int movieId = intent.getIntExtra(SYNC_MOVIEID, -1); + if (movieId != -1) { + syncOrchestrator.addSyncItem(new SyncMovies(hostInfo.getId(), movieId, syncExtras)); + } + } + + // Sync all tvshows + boolean syncAllTVShows = intent.getBooleanExtra(SYNC_ALL_TVSHOWS, false); + if (syncAllTVShows) { + syncOrchestrator.addSyncItem(new SyncTVShows(hostInfo.getId(), syncExtras)); + } + + // Sync a single tvshow + boolean syncSingleTVShow = intent.getBooleanExtra(SYNC_SINGLE_TVSHOW, false); + if (syncSingleTVShow) { + int tvshowId = intent.getIntExtra(SYNC_TVSHOWID, -1); + if (tvshowId != -1) { + syncOrchestrator.addSyncItem(new SyncTVShows(hostInfo.getId(), tvshowId, syncExtras)); + } + } + + // Sync all music + boolean syncAllMusic = intent.getBooleanExtra(SYNC_ALL_MUSIC, false); + if (syncAllMusic) { + syncOrchestrator.addSyncItem(new SyncMusic(hostInfo.getId(), syncExtras)); + } + + // Sync all music videos + boolean syncAllMusicVideos = intent.getBooleanExtra(SYNC_ALL_MUSIC_VIDEOS, false); + if (syncAllMusicVideos) { + syncOrchestrator.addSyncItem(new SyncMusicVideos(hostInfo.getId(), syncExtras)); + } + + // Start syncing + syncOrchestrator.startSync(); + + // If we get killed, after returning from here, don't restart + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return serviceBinder; + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + LogUtils.LOGD(TAG, "Destroying the service."); + if (Utils.isJellybeanMR2OrLater()) { + handlerThread.quitSafely(); + } else { + handlerThread.quit(); + } + } + + public class LocalBinder extends Binder { + public LibrarySyncService getService() { + return LibrarySyncService.this; + } + } + + /** + * + * @param hostInfo host information for which to get items currently syncing + * @return currently syncing syncitems for given hostInfo + */ + public ArrayList getItemsSyncing(HostInfo hostInfo) { + ArrayList syncItems = new ArrayList<>(); + for( SyncOrchestrator orchestrator : syncOrchestrators) { + if( orchestrator.getHostInfo().getId() == hostInfo.getId() ) { + syncItems.addAll(orchestrator.getSyncItems()); + return syncItems; + } + } + return null; + } +} diff --git a/app/src/main/java/org/xbmc/kore/service/library/SyncItem.java b/app/src/main/java/org/xbmc/kore/service/library/SyncItem.java new file mode 100644 index 0000000..9e120a7 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncItem.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.content.ContentResolver; +import android.os.Bundle; +import android.os.Handler; + +import org.xbmc.kore.jsonrpc.HostConnection; + +/** + * Represent an item that can be synced + */ +public abstract class SyncItem { + /** + * Syncs an item from the XBMC host to the local database + * @param orchestrator Orchestrator to call when finished + * @param hostConnection Host connection to use + * @param callbackHandler Handler on which to post callbacks + * @param contentResolver Content resolver + */ + abstract public void sync(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver); + + /** + * Friendly description of this sync item + * @return Description + */ + abstract public String getDescription(); + + /** + * Returns the sync event that should be posted after completion + * @return Sync type, one of the constants in {@link LibrarySyncService} + */ + abstract public String getSyncType(); + + /** + * Returns the extras that were passed during creation. + * Allows the caller to pass parameters that will be sent back to him + * @return Sync extras passed during construction + */ + abstract public Bundle getSyncExtras(); +} diff --git a/app/src/main/java/org/xbmc/kore/service/library/SyncMovies.java b/app/src/main/java/org/xbmc/kore/service/library/SyncMovies.java new file mode 100644 index 0000000..b83f075 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncMovies.java @@ -0,0 +1,239 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.os.Bundle; +import android.os.Handler; + +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.method.VideoLibrary; +import org.xbmc.kore.jsonrpc.type.ListType; +import org.xbmc.kore.jsonrpc.type.VideoType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +public class SyncMovies extends SyncItem { + public static final String TAG = LogUtils.makeLogTag(SyncMovies.class); + + private static final int LIMIT_SYNC_MOVIES = 300; + + private final int hostId; + private final int movieId; + private final Bundle syncExtras; + + /** + * Syncs all the movies on selected XBMC to the local database + * @param hostId XBMC host id + */ + public SyncMovies(final int hostId, Bundle syncExtras) { + this.hostId = hostId; + this.movieId = -1; + this.syncExtras = syncExtras; + } + + /** + * Syncs a specific movie on selected XBMC to the local database + * @param hostId XBMC host id + */ + public SyncMovies(final int hostId, final int movieId, Bundle syncExtras) { + this.hostId = hostId; + this.movieId = movieId; + this.syncExtras = syncExtras; + } + + /** {@inheritDoc} */ + public String getDescription() { + return (movieId != -1) ? + "Sync movies for host: " + hostId : + "Sync movie " + movieId + " for host: " + hostId; + } + + /** {@inheritDoc} */ + public String getSyncType() { + return (movieId == -1) ? LibrarySyncService.SYNC_ALL_MOVIES + : LibrarySyncService.SYNC_SINGLE_MOVIE; + } + + /** {@inheritDoc} */ + public Bundle getSyncExtras() { + return syncExtras; + } + + /** {@inheritDoc} */ + public void sync(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver) { + String properties[] = { + VideoType.FieldsMovie.TITLE, VideoType.FieldsMovie.GENRE, + VideoType.FieldsMovie.YEAR, VideoType.FieldsMovie.RATING, + VideoType.FieldsMovie.DIRECTOR, VideoType.FieldsMovie.TRAILER, + VideoType.FieldsMovie.TAGLINE, VideoType.FieldsMovie.PLOT, + // VideoType.FieldsMovie.PLOTOUTLINE, VideoType.FieldsMovie.ORIGINALTITLE, + // VideoType.FieldsMovie.LASTPLAYED, + VideoType.FieldsMovie.PLAYCOUNT, VideoType.FieldsMovie.DATEADDED, + VideoType.FieldsMovie.WRITER, VideoType.FieldsMovie.STUDIO, + VideoType.FieldsMovie.MPAA, VideoType.FieldsMovie.CAST, + VideoType.FieldsMovie.COUNTRY, VideoType.FieldsMovie.IMDBNUMBER, + VideoType.FieldsMovie.RUNTIME, VideoType.FieldsMovie.SET, + // VideoType.FieldsMovie.SHOWLINK, + VideoType.FieldsMovie.STREAMDETAILS, VideoType.FieldsMovie.TOP250, + VideoType.FieldsMovie.VOTES, VideoType.FieldsMovie.FANART, + VideoType.FieldsMovie.THUMBNAIL, VideoType.FieldsMovie.FILE, + // VideoType.FieldsMovie.SORTTITLE, VideoType.FieldsMovie.RESUME, + VideoType.FieldsMovie.SETID, + // VideoType.FieldsMovie.DATEADDED, VideoType.FieldsMovie.TAG, + // VideoType.FieldsMovie.ART + }; + + if (movieId == -1) { + syncAllMovies(orchestrator, hostConnection, callbackHandler, contentResolver, properties, 0); + } else { + // Sync a specific movie + VideoLibrary.GetMovieDetails action = + new VideoLibrary.GetMovieDetails(movieId, properties); + action.execute(hostConnection, new ApiCallback() { + @Override + public void onSuccess(VideoType.DetailsMovie result) { + deleteMovies(contentResolver, hostId, movieId); + List movies = new ArrayList(1); + movies.add(result); + insertMovies(orchestrator, contentResolver, movies); + orchestrator.syncItemFinished(); + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + } + + /** + * Syncs all the movies, calling itself recursively + * Uses the {@link VideoLibrary.GetMovies} version with limits to make sure + * that Kodi doesn't blow up, and calls itself recursively until all the + * movies are returned + */ + private void syncAllMovies(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final String properties[], + final int startIdx) { + // Call GetMovies with the current limits set + ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_MOVIES); + VideoLibrary.GetMovies action = new VideoLibrary.GetMovies(limits, properties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(List result) { + if (startIdx == 0) { + // First call, delete movies from DB + deleteMovies(contentResolver, hostId, -1); + } + if (result.size() > 0) { + insertMovies(orchestrator, contentResolver, result); + } + + LogUtils.LOGD(TAG, "syncAllMovies, movies gotten: " + result.size()); + if (result.size() == LIMIT_SYNC_MOVIES) { + // Max limit returned, there may be some more movies + // As we're going to recurse, these result objects can add up, so + // let's help the GC and indicate that we don't need this memory + // (hopefully this works) + result = null; + syncAllMovies(orchestrator, hostConnection, callbackHandler, contentResolver, + properties, startIdx + LIMIT_SYNC_MOVIES); + } else { + // Less than the limit was returned so we can finish + // (if it returned more there's a bug in Kodi but it + // shouldn't be a problem as they got inserted in the DB) + orchestrator.syncItemFinished(); + } + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + /** + * Deletes one or all movies from the database (pass -1 on movieId to delete all) + */ + private void deleteMovies(final ContentResolver contentResolver, + int hostId, int movieId) { + if (movieId == -1) { + // Delete all movies + String where = MediaContract.MoviesColumns.HOST_ID + "=?"; + contentResolver.delete(MediaContract.MovieCast.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.Movies.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + } else { + // Delete a movie + contentResolver.delete(MediaContract.MovieCast.buildMovieCastListUri(hostId, movieId), + null, null); + contentResolver.delete(MediaContract.Movies.buildMovieUri(hostId, movieId), + null, null); + } + } + + /** + * Inserts the given movies in the database + */ + private void insertMovies(final SyncOrchestrator orchestrator, + final ContentResolver contentResolver, + final List movies) { + ContentValues movieValuesBatch[] = new ContentValues[movies.size()]; + int castCount = 0; + + // Iterate on each movie + for (int i = 0; i < movies.size(); i++) { + VideoType.DetailsMovie movie = movies.get(i); + movieValuesBatch[i] = SyncUtils.contentValuesFromMovie(hostId, movie); + castCount += movie.cast.size(); + } + + // Insert the movies + contentResolver.bulkInsert(MediaContract.Movies.CONTENT_URI, movieValuesBatch); + + ContentValues movieCastValuesBatch[] = new ContentValues[castCount]; + int count = 0; + // Iterate on each movie/cast + for (VideoType.DetailsMovie movie : movies) { + for (VideoType.Cast cast : movie.cast) { + movieCastValuesBatch[count] = SyncUtils.contentValuesFromCast(hostId, cast); + movieCastValuesBatch[count].put(MediaContract.MovieCastColumns.MOVIEID, movie.movieid); + count++; + } + } + + // Insert the cast list for this movie + contentResolver.bulkInsert(MediaContract.MovieCast.CONTENT_URI, movieCastValuesBatch); + } +} diff --git a/app/src/main/java/org/xbmc/kore/service/library/SyncMusic.java b/app/src/main/java/org/xbmc/kore/service/library/SyncMusic.java new file mode 100644 index 0000000..9bcf37f --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncMusic.java @@ -0,0 +1,383 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.os.Bundle; +import android.os.Handler; + +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.ApiList; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.method.AudioLibrary; +import org.xbmc.kore.jsonrpc.type.AudioType; +import org.xbmc.kore.jsonrpc.type.LibraryType; +import org.xbmc.kore.jsonrpc.type.ListType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +public class SyncMusic extends SyncItem { + public static final String TAG = LogUtils.makeLogTag(SyncMusic.class); + + private static final int LIMIT_SYNC_ARTISTS = 300; + private static final int LIMIT_SYNC_ALBUMS = 300; + private static final int LIMIT_SYNC_SONGS = 600; + + private final int hostId; + private final Bundle syncExtras; + + /** + * Syncs all the music on selected XBMC to the local database + * @param hostId XBMC host id + */ + public SyncMusic(final int hostId, Bundle syncExtras) { + this.hostId = hostId; + this.syncExtras = syncExtras; + } + + /** {@inheritDoc} */ + public String getDescription() { + return "Sync music for host: " + hostId; + } + + /** {@inheritDoc} */ + public String getSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; } + + /** {@inheritDoc} */ + public Bundle getSyncExtras() { + return syncExtras; + } + + /** {@inheritDoc} */ + public void sync(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver) { + chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver, 0); + } + + private final static String getArtistsProperties[] = { + // AudioType.FieldsArtists.INSTRUMENT, AudioType.FieldsArtists.STYLE, + // AudioType.FieldsArtists.MOOD, AudioType.FieldsArtists.BORN, + // AudioType.FieldsArtists.FORMED, + AudioType.FieldsArtists.DESCRIPTION, + AudioType.FieldsArtists.GENRE, + // AudioType.FieldsArtists.DIED, + // AudioType.FieldsArtists.DISBANDED, AudioType.FieldsArtists.YEARSACTIVE, + //AudioType.FieldsArtists.MUSICBRAINZARTISTID, + AudioType.FieldsArtists.FANART, + AudioType.FieldsArtists.THUMBNAIL + }; + /** + * Gets all artists recursively and forwards the call to Genres + * Genres->Albums->Songs + */ + public void chainCallSyncArtists(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final int startIdx) { + // Artists->Genres->Albums->Songs + // Only gets album artists (first parameter) + ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_ARTISTS); + AudioLibrary.GetArtists action = new AudioLibrary.GetArtists(limits, true, getArtistsProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(ApiList result) { + List items; + ListType.LimitsReturned limitsReturned; + if (result == null) { // Safeguard + items = new ArrayList<>(0); + limitsReturned = null; + } else { + items = result.items; + limitsReturned = result.limits; + } + + // First delete all music info + if (startIdx == 0) deleteMusicInfo(contentResolver, hostId); + + // Insert artists + ContentValues artistValuesBatch[] = new ContentValues[items.size()]; + for (int i = 0; i < items.size(); i++) { + AudioType.DetailsArtist artist = items.get(i); + artistValuesBatch[i] = SyncUtils.contentValuesFromArtist(hostId, artist); + } + contentResolver.bulkInsert(MediaContract.Artists.CONTENT_URI, artistValuesBatch); + + if (moreItemsAvailable(limitsReturned)) { + LogUtils.LOGD(TAG, "chainCallSyncArtists: More results on media center, recursing."); + result = null; // Help the GC? + chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver, + startIdx + LIMIT_SYNC_ARTISTS); + } else { + // Ok, we have all the artists, proceed + LogUtils.LOGD(TAG, "chainCallSyncArtists: Got all results, continuing"); + chainCallSyncGenres(orchestrator, hostConnection, callbackHandler, contentResolver); + } + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + private void deleteMusicInfo(final ContentResolver contentResolver, + int hostId) { + // Delete music info + String where = MediaContract.Artists.HOST_ID + "=?"; + contentResolver.delete(MediaContract.AlbumArtists.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.AlbumGenres.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.Songs.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.AudioGenres.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.Albums.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.Artists.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + } + + + private final static String getGenresProperties[] = { + LibraryType.FieldsGenre.TITLE, LibraryType.FieldsGenre.THUMBNAIL + }; + /** + * Syncs Audio genres and forwards calls to sync albums: + * Genres->Albums->Songs + */ + private void chainCallSyncGenres(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver) { + // Genres->Albums->Songs + AudioLibrary.GetGenres action = new AudioLibrary.GetGenres(getGenresProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(List result) { + if (result == null) result = new ArrayList<>(0); // Safeguard + ContentValues genresValuesBatch[] = new ContentValues[result.size()]; + + for (int i = 0; i < result.size(); i++) { + LibraryType.DetailsGenre genre = result.get(i); + genresValuesBatch[i] = SyncUtils.contentValuesFromAudioGenre(hostId, genre); + } + + // Insert the genres and proceed to albums + contentResolver.bulkInsert(MediaContract.AudioGenres.CONTENT_URI, genresValuesBatch); + chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver, 0); + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + private static final String getAlbumsProperties[] = { + AudioType.FieldsAlbum.TITLE, AudioType.FieldsAlbum.DESCRIPTION, + AudioType.FieldsAlbum.ARTIST, AudioType.FieldsAlbum.GENRE, + //AudioType.FieldsAlbum.THEME, AudioType.FieldsAlbum.MOOD, + //AudioType.FieldsAlbum.STYLE, AudioType.FieldsAlbum.TYPE, + AudioType.FieldsAlbum.ALBUMLABEL, AudioType.FieldsAlbum.RATING, + AudioType.FieldsAlbum.YEAR, + //AudioType.FieldsAlbum.MUSICBRAINZALBUMID, + //AudioType.FieldsAlbum.MUSICBRAINZALBUMARTISTID, + AudioType.FieldsAlbum.FANART, AudioType.FieldsAlbum.THUMBNAIL, + AudioType.FieldsAlbum.PLAYCOUNT, AudioType.FieldsAlbum.GENREID, + AudioType.FieldsAlbum.ARTISTID, AudioType.FieldsAlbum.DISPLAYARTIST + }; + + /** + * Syncs Albums recursively and forwards calls to sync songs: + * Albums->Songs + */ + private void chainCallSyncAlbums(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final int startIdx) { + final long albumSyncStartTime = System.currentTimeMillis(); + // Albums->Songs + ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_ALBUMS); + AudioLibrary.GetAlbums action = new AudioLibrary.GetAlbums(limits, getAlbumsProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(ApiList result) { + List items; + ListType.LimitsReturned limitsReturned; + if (result == null) { // Safeguard + items = new ArrayList<>(0); + limitsReturned = null; + } else { + items = result.items; + limitsReturned = result.limits; + } + + // Insert the partial results + ContentValues albumValuesBatch[] = new ContentValues[items.size()]; + int artistsCount = 0, genresCount = 0; + for (int i = 0; i < items.size(); i++) { + AudioType.DetailsAlbum album = items.get(i); + albumValuesBatch[i] = SyncUtils.contentValuesFromAlbum(hostId, album); + + artistsCount += album.artistid.size(); + genresCount += album.genreid.size(); + } + contentResolver.bulkInsert(MediaContract.Albums.CONTENT_URI, albumValuesBatch); + + LogUtils.LOGD(TAG, "Finished inserting albums in: " + + (System.currentTimeMillis() - albumSyncStartTime)); + + // Iterate on each album, collect the artists and the genres and insert them + ContentValues albumArtistsValuesBatch[] = new ContentValues[artistsCount]; + ContentValues albumGenresValuesBatch[] = new ContentValues[genresCount]; + int artistCount = 0, genreCount = 0; + for (AudioType.DetailsAlbum album : items) { + for (int artistId : album.artistid) { + albumArtistsValuesBatch[artistCount] = new ContentValues(); + albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.HOST_ID, hostId); + albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ALBUMID, album.albumid); + albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ARTISTID, artistId); + artistCount++; + } + + for (int genreId : album.genreid) { + albumGenresValuesBatch[genreCount] = new ContentValues(); + albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.HOST_ID, hostId); + albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.ALBUMID, album.albumid); + albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.GENREID, genreId); + genreCount++; + } + } + + contentResolver.bulkInsert(MediaContract.AlbumArtists.CONTENT_URI, albumArtistsValuesBatch); + contentResolver.bulkInsert(MediaContract.AlbumGenres.CONTENT_URI, albumGenresValuesBatch); + + LogUtils.LOGD(TAG, "Finished inserting artists and genres in: " + + (System.currentTimeMillis() - albumSyncStartTime)); + + if (moreItemsAvailable(limitsReturned)) { + LogUtils.LOGD(TAG, "chainCallSyncAlbums: More results on media center, recursing."); + result = null; // Help the GC? + chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver, + startIdx + LIMIT_SYNC_ALBUMS); + } else { + // Ok, we have all the albums, proceed to songs + LogUtils.LOGD(TAG, "chainCallSyncAlbums: Got all results, continuing"); + chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver, 0); + } + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + private static final String getSongsProperties[] = { + AudioType.FieldsSong.TITLE, + //AudioType.FieldsSong.ARTIST, AudioType.FieldsSong.ALBUMARTIST, AudioType.FieldsSong.GENRE, + //AudioType.FieldsSong.YEAR, AudioType.FieldsSong.RATING, + //AudioType.FieldsSong.ALBUM, + AudioType.FieldsSong.TRACK, AudioType.FieldsSong.DURATION, + //AudioType.FieldsSong.COMMENT, AudioType.FieldsSong.LYRICS, + //AudioType.FieldsSong.MUSICBRAINZTRACKID, + //AudioType.FieldsSong.MUSICBRAINZARTISTID, + //AudioType.FieldsSong.MUSICBRAINZALBUMID, + //AudioType.FieldsSong.MUSICBRAINZALBUMARTISTID, + //AudioType.FieldsSong.PLAYCOUNT, AudioType.FieldsSong.FANART, + AudioType.FieldsSong.THUMBNAIL, AudioType.FieldsSong.FILE, + AudioType.FieldsSong.ALBUMID, + //AudioType.FieldsSong.LASTPLAYED, AudioType.FieldsSong.DISC, + //AudioType.FieldsSong.GENREID, AudioType.FieldsSong.ARTISTID, + //AudioType.FieldsSong.DISPLAYARTIST, AudioType.FieldsSong.ALBUMARTISTID + }; + + /** + * Syncs songs and stops + */ + private void chainCallSyncSongs(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final int startIdx) { + // Songs + ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_SONGS); + AudioLibrary.GetSongs action = new AudioLibrary.GetSongs(limits, getSongsProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(ApiList result) { + List items; + ListType.LimitsReturned limitsReturned; + if (result == null) { // Safeguard + items = new ArrayList<>(0); + limitsReturned = null; + } else { + items = result.items; + limitsReturned = result.limits; + } + + // Save partial results to DB + ContentValues songValuesBatch[] = new ContentValues[items.size()]; + for (int i = 0; i < items.size(); i++) { + AudioType.DetailsSong song = items.get(i); + songValuesBatch[i] = SyncUtils.contentValuesFromSong(hostId, song); + } + contentResolver.bulkInsert(MediaContract.Songs.CONTENT_URI, songValuesBatch); + + if (moreItemsAvailable(limitsReturned)) { + LogUtils.LOGD(TAG, "chainCallSyncSongs: More results on media center, recursing."); + result = null; // Help the GC? + chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver, + startIdx + LIMIT_SYNC_SONGS); + } else { + // Ok, we have all the songs, insert them + LogUtils.LOGD(TAG, "chainCallSyncSongs: Got all results, continuing"); + orchestrator.syncItemFinished(); + } + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + private boolean moreItemsAvailable(ListType.LimitsReturned limitsReturned) { + boolean moreItemsAvailable = false; + if (limitsReturned != null) { + moreItemsAvailable = ( limitsReturned.total - limitsReturned.end ) > 0; + } + return moreItemsAvailable; + } +} diff --git a/app/src/main/java/org/xbmc/kore/service/library/SyncMusicVideos.java b/app/src/main/java/org/xbmc/kore/service/library/SyncMusicVideos.java new file mode 100644 index 0000000..10ce298 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncMusicVideos.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.os.Bundle; +import android.os.Handler; + +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.method.VideoLibrary; +import org.xbmc.kore.jsonrpc.type.VideoType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.utils.LogUtils; + +import java.util.List; + +public class SyncMusicVideos extends SyncItem { + public static final String TAG = LogUtils.makeLogTag(SyncMusicVideos.class); + + private final int hostId; + private final Bundle syncExtras; + + /** + * Syncs all the music videos on XBMC, to the local database + * @param hostId XBMC host id + */ + public SyncMusicVideos(final int hostId, Bundle syncExtras) { + this.hostId = hostId; + this.syncExtras = syncExtras; + } + + /** {@inheritDoc} */ + public String getDescription() { + return "Sync music videos for host: " + hostId; + } + + /** {@inheritDoc} */ + public String getSyncType() { + return LibrarySyncService.SYNC_ALL_MUSIC_VIDEOS; + } + + /** {@inheritDoc} */ + public Bundle getSyncExtras() { + return syncExtras; + } + + /** {@inheritDoc} */ + public void sync(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver) { + String properties[] = { + VideoType.FieldsMusicVideo.TITLE, VideoType.FieldsMusicVideo.PLAYCOUNT, + VideoType.FieldsMusicVideo.RUNTIME, VideoType.FieldsMusicVideo.DIRECTOR, + VideoType.FieldsMusicVideo.STUDIO, VideoType.FieldsMusicVideo.YEAR, + VideoType.FieldsMusicVideo.PLOT, VideoType.FieldsMusicVideo.ALBUM, + VideoType.FieldsMusicVideo.ARTIST, VideoType.FieldsMusicVideo.GENRE, + VideoType.FieldsMusicVideo.TRACK, VideoType.FieldsMusicVideo.STREAMDETAILS, + //VideoType.FieldsMusicVideo.LASTPLAYED, + VideoType.FieldsMusicVideo.FANART, + VideoType.FieldsMusicVideo.THUMBNAIL, VideoType.FieldsMusicVideo.FILE, + // VideoType.FieldsMusicVideo.RESUME, VideoType.FieldsMusicVideo.DATEADDED, + VideoType.FieldsMusicVideo.TAG, + //VideoType.FieldsMusicVideo.ART + }; + + // Delete and sync all music videos + VideoLibrary.GetMusicVideos action = new VideoLibrary.GetMusicVideos(properties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(List result) { + deleteMusicVideos(contentResolver, hostId); + insertMusicVideos(orchestrator, contentResolver, result); + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + private void deleteMusicVideos(final ContentResolver contentResolver, int hostId) { + // Delete all music videos + String where = MediaContract.MusicVideosColumns.HOST_ID + "=?"; + contentResolver.delete(MediaContract.MusicVideos.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + } + + private void insertMusicVideos(final SyncOrchestrator orchestrator, + final ContentResolver contentResolver, + final List musicVideos) { + ContentValues musicVideosValuesBatch[] = new ContentValues[musicVideos.size()]; + + // Iterate on each music video + for (int i = 0; i < musicVideos.size(); i++) { + VideoType.DetailsMusicVideo musicVideo = musicVideos.get(i); + musicVideosValuesBatch[i] = SyncUtils.contentValuesFromMusicVideo(hostId, musicVideo); + } + + // Insert the movies + contentResolver.bulkInsert(MediaContract.MusicVideos.CONTENT_URI, musicVideosValuesBatch); + orchestrator.syncItemFinished(); + } +} diff --git a/app/src/main/java/org/xbmc/kore/service/library/SyncOrchestrator.java b/app/src/main/java/org/xbmc/kore/service/library/SyncOrchestrator.java new file mode 100644 index 0000000..0d82503 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncOrchestrator.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.app.Service; +import android.content.ContentResolver; +import android.os.Handler; + +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayDeque; +import java.util.Iterator; + +import de.greenrobot.event.EventBus; + +public class SyncOrchestrator { + public static final String TAG = LogUtils.makeLogTag(SyncOrchestrator.class); + + private ArrayDeque syncItems; + private Service syncService; + private final int serviceStartId; + private HostConnection hostConnection; + private final HostInfo hostInfo; + private final Handler callbackHandler; + private final ContentResolver contentResolver; + + private SyncItem currentSyncItem; + + private Iterator syncItemIterator; + + public interface OnSyncListener { + void onSyncFinished(SyncOrchestrator syncOrchestrator); + } + + private OnSyncListener listener; + + /** + * Constructor + * @param syncService Service on which to call {@link LibrarySyncService#stopSelf()} when finished + * @param startId Service startid to use when calling {@link LibrarySyncService#stopSelf()} + * @param hostInfo Host from which to sync + * @param callbackHandler Handler on which to post callbacks + * @param contentResolver Content resolver + */ + public SyncOrchestrator(Service syncService, final int startId, + final HostInfo hostInfo, + final Handler callbackHandler, + final ContentResolver contentResolver) { + this.syncService = syncService; + this.syncItems = new ArrayDeque(); + this.serviceStartId = startId; + this.hostInfo = hostInfo; + this.callbackHandler = callbackHandler; + this.contentResolver = contentResolver; + } + + public void setListener(OnSyncListener listener) { + this.listener = listener; + } + + public HostInfo getHostInfo() { + return hostInfo; + } + + /** + * Add this item to the sync list + * @param syncItem Sync item + */ + public void addSyncItem(SyncItem syncItem) { + syncItems.add(syncItem); + } + + public ArrayDeque getSyncItems() { + return syncItems; + } + + private long startTime = -1; + private long partialStartTime; + + /** + * Starts the syncing process + */ + public void startSync() { + startTime = System.currentTimeMillis(); + hostConnection = new HostConnection(hostInfo); + hostConnection.setProtocol(HostConnection.PROTOCOL_HTTP); + syncItemIterator = syncItems.iterator(); + nextSync(); + } + + /** + * Processes the next item on the sync list, or cleans up if it is finished. + */ + private void nextSync() { + if (syncItemIterator.hasNext()) { + partialStartTime = System.currentTimeMillis(); + currentSyncItem = syncItemIterator.next(); + currentSyncItem.sync(this, hostConnection, callbackHandler, contentResolver); + } else { + LogUtils.LOGD(TAG, "Sync finished for all items. Total time: " + + (System.currentTimeMillis() - startTime)); + // No more syncs, cleanup. + // No need to disconnect, as this is HTTP + //hostConnection.disconnect(); + if (listener != null) { + listener.onSyncFinished(this); + } + syncService.stopSelf(serviceStartId); + } + } + + /** + * One of the syync items finish syncing + */ + public void syncItemFinished() { + LogUtils.LOGD(TAG, "Sync finished for item: " + currentSyncItem.getDescription() + + ". Total time: " + (System.currentTimeMillis() - partialStartTime)); + + EventBus.getDefault() + .post(new MediaSyncEvent(currentSyncItem.getSyncType(), + currentSyncItem.getSyncExtras(), + MediaSyncEvent.STATUS_SUCCESS)); + + syncItems.remove(currentSyncItem); + + nextSync(); + } + + /** + * One of the sync items failed, stop and clean up + * @param errorCode Error code + * @param description Description + */ + public void syncItemFailed(int errorCode, String description) { + LogUtils.LOGD(TAG, "A Sync item has got an error. Sync item: " + + currentSyncItem.getDescription() + + ". Error description: " + description); + // No need to disconnect, as this is HTTP + //hostConnection.disconnect(); + EventBus.getDefault() + .post(new MediaSyncEvent(currentSyncItem.getSyncType(), + currentSyncItem.getSyncExtras(), + MediaSyncEvent.STATUS_FAIL, errorCode, description)); + // Keep syncing till the end + nextSync(); + //syncService.stopSelf(serviceStartId); + } +} diff --git a/app/src/main/java/org/xbmc/kore/service/library/SyncTVShows.java b/app/src/main/java/org/xbmc/kore/service/library/SyncTVShows.java new file mode 100644 index 0000000..4cca6db --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncTVShows.java @@ -0,0 +1,384 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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.xbmc.kore.service.library; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; + +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.method.VideoLibrary; +import org.xbmc.kore.jsonrpc.type.ListType; +import org.xbmc.kore.jsonrpc.type.VideoType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +public class SyncTVShows extends SyncItem { + public static final String TAG = LogUtils.makeLogTag(SyncTVShows.class); + + private static final int LIMIT_SYNC_TVSHOWS = 200; + + private final int hostId; + private final int tvshowId; + private final Bundle syncExtras; + + /** + * Syncs all the TVShows on selected XBMC to the local database + * @param hostId XBMC host id + */ + public SyncTVShows(final int hostId, Bundle syncExtras) { + this.hostId = hostId; + this.tvshowId = -1; + this.syncExtras = syncExtras; + } + + /** + * Syncs a specific TVShow to the local database + * @param hostId XBMC host id + * @param tvshowId Show to sync + */ + public SyncTVShows(final int hostId, final int tvshowId, Bundle syncExtras) { + this.hostId = hostId; + this.tvshowId = tvshowId; + this.syncExtras = syncExtras; + } + + /** {@inheritDoc} */ + public String getDescription() { + return (tvshowId != -1) ? + "Sync TV shows for host: " + hostId : + "Sync TV show " + tvshowId + " for host: " + hostId; + } + + /** {@inheritDoc} */ + public String getSyncType() { + return (tvshowId == -1) ? LibrarySyncService.SYNC_ALL_TVSHOWS + : LibrarySyncService.SYNC_SINGLE_TVSHOW; + } + + /** {@inheritDoc} */ + public Bundle getSyncExtras() { + return syncExtras; + } + + private final static String getTVShowsProperties[] = { + VideoType.FieldsTVShow.TITLE, VideoType.FieldsTVShow.GENRE, + //VideoType.FieldsTVShow.YEAR, + VideoType.FieldsTVShow.RATING, VideoType.FieldsTVShow.PLOT, + VideoType.FieldsTVShow.STUDIO, VideoType.FieldsTVShow.MPAA, + VideoType.FieldsTVShow.CAST, VideoType.FieldsTVShow.PLAYCOUNT, + VideoType.FieldsTVShow.EPISODE, VideoType.FieldsTVShow.IMDBNUMBER, + VideoType.FieldsTVShow.PREMIERED, + //VideoType.FieldsTVShow.VOTES, VideoType.FieldsTVShow.LASTPLAYED, + VideoType.FieldsTVShow.FANART, VideoType.FieldsTVShow.THUMBNAIL, + VideoType.FieldsTVShow.FILE, + //VideoType.FieldsTVShow.ORIGINALTITLE, VideoType.FieldsTVShow.SORTTITLE, + // VideoType.FieldsTVShow.EPISODEGUIDE, VideoType.FieldsTVShow.SEASON, + VideoType.FieldsTVShow.WATCHEDEPISODES, VideoType.FieldsTVShow.DATEADDED, + //VideoType.FieldsTVShow.TAG, VideoType.FieldsTVShow.ART + }; + /** {@inheritDoc} */ + public void sync(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver) { + if (tvshowId == -1) { + syncAllTVShows(orchestrator, hostConnection, callbackHandler, contentResolver, + 0, new ArrayList()); + } else { + VideoLibrary.GetTVShowDetails action = + new VideoLibrary.GetTVShowDetails(tvshowId, getTVShowsProperties); + action.execute(hostConnection, new ApiCallback() { + @Override + public void onSuccess(VideoType.DetailsTVShow result) { + deleteTVShows(contentResolver, hostId, tvshowId); + List tvShows = new ArrayList<>(1); + tvShows.add(result); + insertTVShowsAndGetDetails(orchestrator, hostConnection, callbackHandler, + contentResolver, tvShows); + // insertTVShows calls syncItemFinished + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + } + + /** + * Syncs all the TV shows, calling itself recursively + * Uses the {@link VideoLibrary.GetTVShows} version with limits to make sure + * that Kodi doesn't blow up, and calls itself recursively until all the + * shows are returned + */ + private void syncAllTVShows(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final int startIdx, + final List allResults) { + // Call GetTVShows with the current limits set + ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_TVSHOWS); + VideoLibrary.GetTVShows action = new VideoLibrary.GetTVShows(limits, getTVShowsProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(List result) { + allResults.addAll(result); + if (result.size() == LIMIT_SYNC_TVSHOWS) { + // Max limit returned, there may be some more movies + LogUtils.LOGD(TAG, "syncAllTVShows: More tv shows on media center, recursing."); + syncAllTVShows(orchestrator, hostConnection, callbackHandler, contentResolver, + startIdx + LIMIT_SYNC_TVSHOWS, allResults); + } else { + // Ok, we have all the shows, insert them + LogUtils.LOGD(TAG, "syncAllTVShows: Got all tv shows. Total: " + allResults.size()); + deleteTVShows(contentResolver, hostId, -1); + insertTVShowsAndGetDetails(orchestrator, hostConnection, callbackHandler, + contentResolver, allResults); + } + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } + + private void deleteTVShows(final ContentResolver contentResolver, + int hostId, int tvshowId) { + if (tvshowId == -1) { + LogUtils.LOGD(TAG, "Deleting all existing tv shows: "); + // Delete all tvshows + String where = MediaContract.TVShowsColumns.HOST_ID + "=?"; + contentResolver.delete(MediaContract.Episodes.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.Seasons.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.TVShowCast.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + contentResolver.delete(MediaContract.TVShows.CONTENT_URI, + where, new String[]{String.valueOf(hostId)}); + } else { + // Delete a specific tvshow + contentResolver.delete(MediaContract.Episodes.buildTVShowEpisodesListUri(hostId, tvshowId), + null, null); + contentResolver.delete(MediaContract.Seasons.buildTVShowSeasonsListUri(hostId, tvshowId), + null, null); + contentResolver.delete(MediaContract.TVShowCast.buildTVShowCastListUri(hostId, tvshowId), + null, null); + contentResolver.delete(MediaContract.TVShows.buildTVShowUri(hostId, tvshowId), + null, null); + } + } + + private void insertTVShowsAndGetDetails(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + List tvShows) { + ContentValues tvshowsValuesBatch[] = new ContentValues[tvShows.size()]; + int castCount = 0; + + // Iterate on each show + for (int i = 0; i < tvShows.size(); i++) { + VideoType.DetailsTVShow tvshow = tvShows.get(i); + tvshowsValuesBatch[i] = SyncUtils.contentValuesFromTVShow(hostId, tvshow); + castCount += tvshow.cast.size(); + } + // Insert the tvshows + contentResolver.bulkInsert(MediaContract.TVShows.CONTENT_URI, tvshowsValuesBatch); + LogUtils.LOGD(TAG, "Inserted " + tvShows.size() + " tv shows."); + + ContentValues tvshowsCastValuesBatch[] = new ContentValues[castCount]; + int count = 0; + // Iterate on each show/cast + for (VideoType.DetailsTVShow tvshow : tvShows) { + for (VideoType.Cast cast : tvshow.cast) { + tvshowsCastValuesBatch[count] = SyncUtils.contentValuesFromCast(hostId, cast); + tvshowsCastValuesBatch[count].put(MediaContract.TVShowCastColumns.TVSHOWID, tvshow.tvshowid); + count++; + } + } + // Insert the cast list for this movie + contentResolver.bulkInsert(MediaContract.TVShowCast.CONTENT_URI, tvshowsCastValuesBatch); + + // Start the sequential syncing of seasons + chainSyncSeasons(orchestrator, hostConnection, callbackHandler, + contentResolver, tvShows, 0); + } + + private final static String seasonsProperties[] = { + VideoType.FieldsSeason.SEASON, VideoType.FieldsSeason.SHOWTITLE, + //VideoType.FieldsSeason.PLAYCOUNT, + VideoType.FieldsSeason.EPISODE, + VideoType.FieldsSeason.FANART, VideoType.FieldsSeason.THUMBNAIL, + VideoType.FieldsSeason.TVSHOWID, VideoType.FieldsSeason.WATCHEDEPISODES, + //VideoType.FieldsSeason.ART + }; + + /** + * Sequentially syncs seasons for the tvshow specified, and on success recursively calls + * itself to sync the next tvshow on the list. + * This basically iterates through the tvshows list updating the seasons, + * in a sequential manner (defeating the parallel nature of host calls) + * After processing all tvshows on the list, starts the episode syncing + * + * @param orchestrator Orchestrator to call when finished + * @param hostConnection Host connection to use + * @param callbackHandler Handler on which to post callbacks + * @param contentResolver Content resolver + * @param tvShows TV shows list to get seasons to + * @param position Position of the tvshow on the list to process + */ + private void chainSyncSeasons(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final List tvShows, + final int position) { + if (position < tvShows.size()) { + // Process this tvshow + final VideoType.DetailsTVShow tvShow = tvShows.get(position); + + VideoLibrary.GetSeasons action = new VideoLibrary.GetSeasons(tvShow.tvshowid, seasonsProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(List result) { + ContentValues seasonsValuesBatch[] = new ContentValues[result.size()]; + int totalWatchedEpisodes = 0; + for (int i = 0; i < result.size(); i++) { + VideoType.DetailsSeason season = result.get(i); + seasonsValuesBatch[i] = SyncUtils.contentValuesFromSeason(hostId, season); + + totalWatchedEpisodes += season.watchedepisodes; + } + // Insert the seasons + contentResolver.bulkInsert(MediaContract.Seasons.CONTENT_URI, seasonsValuesBatch); + + if (getSyncType().equals(LibrarySyncService.SYNC_SINGLE_TVSHOW)) { + // HACK: Update watched episodes count for the tvshow with the sum + // of watched episodes from seasons, given that the value that we + // got from XBMC from the call to GetTVShowDetails is wrong (note + // that the value returned from GetTVShows is correct). + Uri uri = MediaContract.TVShows.buildTVShowUri(hostId, tvShow.tvshowid); + ContentValues tvshowUpdate = new ContentValues(1); + tvshowUpdate.put(MediaContract.TVShowsColumns.WATCHEDEPISODES, totalWatchedEpisodes); + contentResolver.update(uri, tvshowUpdate, null, null); + } + + // Sync the next tv show + chainSyncSeasons(orchestrator, hostConnection, callbackHandler, + contentResolver, tvShows, position + 1); + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } else { + // We've processed all tvshows, start episode syncing + chainSyncEpisodes(orchestrator, hostConnection, callbackHandler, + contentResolver, tvShows, 0); + } + } + + private final static String getEpisodesProperties[] = { + VideoType.FieldsEpisode.TITLE, VideoType.FieldsEpisode.PLOT, + //VideoType.FieldsEpisode.VOTES, + VideoType.FieldsEpisode.RATING, + VideoType.FieldsEpisode.WRITER, VideoType.FieldsEpisode.FIRSTAIRED, + VideoType.FieldsEpisode.PLAYCOUNT, VideoType.FieldsEpisode.RUNTIME, + VideoType.FieldsEpisode.DIRECTOR, + //VideoType.FieldsEpisode.PRODUCTIONCODE, + VideoType.FieldsEpisode.SEASON, + VideoType.FieldsEpisode.EPISODE, + //VideoType.FieldsEpisode.ORIGINALTITLE, + VideoType.FieldsEpisode.SHOWTITLE, + //VideoType.FieldsEpisode.CAST, + VideoType.FieldsEpisode.STREAMDETAILS, + //VideoType.FieldsEpisode.LASTPLAYED, + VideoType.FieldsEpisode.FANART, VideoType.FieldsEpisode.THUMBNAIL, + VideoType.FieldsEpisode.FILE, + //VideoType.FieldsEpisode.RESUME, + VideoType.FieldsEpisode.TVSHOWID, VideoType.FieldsEpisode.DATEADDED, + //VideoType.FieldsEpisode.UNIQUEID, VideoType.FieldsEpisode.ART + }; + + /** + * Sequentially syncs episodes for the tvshow specified, and on success recursively calls + * itself to sync the next tvshow on the list. + * This basically iterates through the tvshows list updating the episodes, + * in a sequential manner (defeating the parallel nature of host calls) + * + * @param orchestrator Orchestrator to call when finished + * @param hostConnection Host connection to use + * @param callbackHandler Handler on which to post callbacks + * @param contentResolver Content resolver + * @param tvShows TV shows list to get episodes to + * @param position Position of the tvshow on the list to process + */ + private void chainSyncEpisodes(final SyncOrchestrator orchestrator, + final HostConnection hostConnection, + final Handler callbackHandler, + final ContentResolver contentResolver, + final List tvShows, + final int position) { + if (position < tvShows.size()) { + VideoType.DetailsTVShow tvShow = tvShows.get(position); + + VideoLibrary.GetEpisodes action = new VideoLibrary.GetEpisodes(tvShow.tvshowid, getEpisodesProperties); + action.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(List result) { + ContentValues episodesValuesBatch[] = new ContentValues[result.size()]; + for (int i = 0; i < result.size(); i++) { + VideoType.DetailsEpisode episode = result.get(i); + episodesValuesBatch[i] = SyncUtils.contentValuesFromEpisode(hostId, episode); + } + // Insert the episodes + contentResolver.bulkInsert(MediaContract.Episodes.CONTENT_URI, episodesValuesBatch); + + chainSyncEpisodes(orchestrator, hostConnection, callbackHandler, + contentResolver, tvShows, position + 1); + } + + @Override + public void onError(int errorCode, String description) { + // Ok, something bad happend, just quit + orchestrator.syncItemFailed(errorCode, description); + } + }, callbackHandler); + } else { + // We're finished + LogUtils.LOGD(TAG, "Sync tv shows finished successfully"); + orchestrator.syncItemFinished(); + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/service/SyncUtils.java b/app/src/main/java/org/xbmc/kore/service/library/SyncUtils.java similarity index 99% rename from app/src/main/java/org/xbmc/kore/service/SyncUtils.java rename to app/src/main/java/org/xbmc/kore/service/library/SyncUtils.java index 129f6bf..c5fad35 100644 --- a/app/src/main/java/org/xbmc/kore/service/SyncUtils.java +++ b/app/src/main/java/org/xbmc/kore/service/library/SyncUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.xbmc.kore.service; +package org.xbmc.kore.service.library; import android.content.ComponentName; import android.content.ContentValues; @@ -27,6 +27,7 @@ import org.xbmc.kore.jsonrpc.type.VideoType; import org.xbmc.kore.jsonrpc.type.AudioType; import org.xbmc.kore.jsonrpc.type.LibraryType; import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.Utils; import java.util.ArrayList; @@ -462,11 +463,11 @@ public class SyncUtils { if (service == null || hostInfo == null || syncTypes == null) return false; - ArrayList itemsSyncing = service.getItemsSyncing(hostInfo); + ArrayList itemsSyncing = service.getItemsSyncing(hostInfo); if( itemsSyncing == null ) return false; - for (LibrarySyncService.SyncItem syncItem : itemsSyncing) { + for (SyncItem syncItem : itemsSyncing) { for( String syncType : syncTypes ) { if (syncItem.getSyncType().equals(syncType)) { return true; diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java index 2ef7cd3..3f6d69a 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java @@ -37,7 +37,6 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.BaseAdapter; import android.widget.CursorAdapter; import android.widget.EditText; import android.widget.Toast; @@ -47,8 +46,8 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.ApiException; import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; -import org.xbmc.kore.service.LibrarySyncService; -import org.xbmc.kore.service.SyncUtils; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.service.library.SyncUtils; import org.xbmc.kore.utils.LogUtils; import de.greenrobot.event.EventBus; diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java index 91959b3..f301ac0 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java @@ -36,8 +36,8 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.ApiException; import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; -import org.xbmc.kore.service.LibrarySyncService; -import org.xbmc.kore.service.SyncUtils; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.service.library.SyncUtils; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; @@ -65,16 +65,16 @@ abstract public class AbstractDetailsFragment extends Fragment abstract protected View createView(LayoutInflater inflater, ViewGroup container); /** - * Should return {@link org.xbmc.kore.service.LibrarySyncService} SyncType that + * Should return {@link LibrarySyncService} SyncType that * this fragment initiates - * @return {@link org.xbmc.kore.service.LibrarySyncService} SyncType + * @return {@link LibrarySyncService} SyncType */ abstract protected String getSyncType(); /** - * Should return the {@link org.xbmc.kore.service.LibrarySyncService} syncID if this fragment + * Should return the {@link LibrarySyncService} syncID if this fragment * synchronizes a single item. The itemId that should be synced must returned by {@link #getSyncItemID()} - * @return {@link org.xbmc.kore.service.LibrarySyncService} SyncID + * @return {@link LibrarySyncService} SyncID */ abstract protected String getSyncID(); diff --git a/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java index 5c63ec4..63dfb52 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java @@ -40,7 +40,7 @@ import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java b/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java index dec9d89..9d1d63c 100644 --- a/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java @@ -39,7 +39,7 @@ import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/AudioGenresListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AudioGenresListFragment.java index 09107ce..10a096a 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AudioGenresListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AudioGenresListFragment.java @@ -37,7 +37,7 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java index eab4fcf..388de63 100644 --- a/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java @@ -56,7 +56,7 @@ import org.xbmc.kore.jsonrpc.method.VideoLibrary; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.jsonrpc.type.VideoType; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.FileDownloadHelper; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/MovieListFragment.java b/app/src/main/java/org/xbmc/kore/ui/MovieListFragment.java index 430a69a..d0d427b 100644 --- a/app/src/main/java/org/xbmc/kore/ui/MovieListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/MovieListFragment.java @@ -42,7 +42,7 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; diff --git a/app/src/main/java/org/xbmc/kore/ui/MusicVideoDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/MusicVideoDetailsFragment.java index 5546428..27e80a6 100644 --- a/app/src/main/java/org/xbmc/kore/ui/MusicVideoDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/MusicVideoDetailsFragment.java @@ -53,7 +53,7 @@ import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.method.Playlist; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.FileDownloadHelper; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/MusicVideoListFragment.java b/app/src/main/java/org/xbmc/kore/ui/MusicVideoListFragment.java index 40996cb..043adca 100644 --- a/app/src/main/java/org/xbmc/kore/ui/MusicVideoListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/MusicVideoListFragment.java @@ -36,7 +36,7 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; diff --git a/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java b/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java index c68e1ce..119d22b 100644 --- a/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java @@ -44,7 +44,7 @@ import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeDetailsFragment.java index 946d76d..0a86282 100644 --- a/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeDetailsFragment.java @@ -52,7 +52,7 @@ import org.xbmc.kore.jsonrpc.method.Playlist; import org.xbmc.kore.jsonrpc.method.VideoLibrary; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.FileDownloadHelper; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeListFragment.java b/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeListFragment.java index 2375030..a6d9cd6 100644 --- a/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/TVShowEpisodeListFragment.java @@ -48,7 +48,7 @@ import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; diff --git a/app/src/main/java/org/xbmc/kore/ui/TVShowListFragment.java b/app/src/main/java/org/xbmc/kore/ui/TVShowListFragment.java index fa8bffa..7cdb5b4 100644 --- a/app/src/main/java/org/xbmc/kore/ui/TVShowListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/TVShowListFragment.java @@ -42,7 +42,7 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; diff --git a/app/src/main/java/org/xbmc/kore/ui/TVShowOverviewFragment.java b/app/src/main/java/org/xbmc/kore/ui/TVShowOverviewFragment.java index 99f9b83..0a2465e 100644 --- a/app/src/main/java/org/xbmc/kore/ui/TVShowOverviewFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/TVShowOverviewFragment.java @@ -40,7 +40,7 @@ import org.xbmc.kore.Settings; import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; import org.xbmc.kore.jsonrpc.type.VideoType; import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; diff --git a/app/src/main/java/org/xbmc/kore/ui/hosts/AddHostFragmentFinish.java b/app/src/main/java/org/xbmc/kore/ui/hosts/AddHostFragmentFinish.java index 4e91a92..7c58a5e 100644 --- a/app/src/main/java/org/xbmc/kore/ui/hosts/AddHostFragmentFinish.java +++ b/app/src/main/java/org/xbmc/kore/ui/hosts/AddHostFragmentFinish.java @@ -38,7 +38,7 @@ import org.xbmc.kore.Settings; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.HostConnection; -import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.NavigationDrawerFragment; import java.util.Arrays;