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;