From 7330f85241920e0ae9a5bec7719c497998fb070c Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Thu, 21 Apr 2016 10:27:49 +0200 Subject: [PATCH] Fixed issue with recursing music items Using Kodi's JSON RPC, Kodi may return less items than requested, even if there are more items available. The old method of determining if more items are available by checking if the amount of items returned equals the amount requested does not work in these cases. Therefore, we now use the returned List.LimitsReturned to determine if there are more items available. If List.LimitsReturned.end equals List.LimitesReturned.total we assume we retrieved all items. --- app/src/main/AndroidManifest.xml | 2 +- .../java/org/xbmc/kore/jsonrpc/ApiList.java | 31 + .../kore/jsonrpc/event/MediaSyncEvent.java | 4 +- .../kore/jsonrpc/method/AudioLibrary.java | 42 +- .../org/xbmc/kore/jsonrpc/type/ListType.java | 26 + .../xbmc/kore/service/LibrarySyncService.java | 1340 ----------------- .../service/library/LibrarySyncService.java | 196 +++ .../xbmc/kore/service/library/SyncItem.java | 59 + .../xbmc/kore/service/library/SyncMovies.java | 239 +++ .../xbmc/kore/service/library/SyncMusic.java | 383 +++++ .../kore/service/library/SyncMusicVideos.java | 122 ++ .../service/library/SyncOrchestrator.java | 165 ++ .../kore/service/library/SyncTVShows.java | 384 +++++ .../kore/service/{ => library}/SyncUtils.java | 7 +- .../kore/ui/AbstractCursorListFragment.java | 5 +- .../xbmc/kore/ui/AbstractDetailsFragment.java | 12 +- .../org/xbmc/kore/ui/AlbumListFragment.java | 2 +- .../org/xbmc/kore/ui/ArtistListFragment.java | 2 +- .../xbmc/kore/ui/AudioGenresListFragment.java | 2 +- .../xbmc/kore/ui/MovieDetailsFragment.java | 2 +- .../org/xbmc/kore/ui/MovieListFragment.java | 2 +- .../kore/ui/MusicVideoDetailsFragment.java | 2 +- .../xbmc/kore/ui/MusicVideoListFragment.java | 2 +- .../org/xbmc/kore/ui/SongsListFragment.java | 2 +- .../kore/ui/TVShowEpisodeDetailsFragment.java | 2 +- .../kore/ui/TVShowEpisodeListFragment.java | 2 +- .../org/xbmc/kore/ui/TVShowListFragment.java | 2 +- .../xbmc/kore/ui/TVShowOverviewFragment.java | 2 +- .../kore/ui/hosts/AddHostFragmentFinish.java | 2 +- 29 files changed, 1658 insertions(+), 1385 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/jsonrpc/ApiList.java delete mode 100644 app/src/main/java/org/xbmc/kore/service/LibrarySyncService.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/LibrarySyncService.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/SyncItem.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/SyncMovies.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/SyncMusic.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/SyncMusicVideos.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/SyncOrchestrator.java create mode 100644 app/src/main/java/org/xbmc/kore/service/library/SyncTVShows.java rename app/src/main/java/org/xbmc/kore/service/{ => library}/SyncUtils.java (99%) 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;