Fixed issue with recursing music items

Using Kodi's JSON RPC, Kodi may return less items than requested, even
if there are more items available. The old method of determining if
more items are available by checking if the amount of items returned
equals the amount requested does not work in these cases. Therefore,
we now use the returned List.LimitsReturned to determine if there are
more items available. If List.LimitsReturned.end equals
List.LimitesReturned.total we assume we retrieved all items.
This commit is contained in:
Martijn Brekhof 2016-04-21 10:27:49 +02:00
parent de8e28b768
commit 7330f85241
29 changed files with 1658 additions and 1385 deletions

View File

@ -65,7 +65,7 @@
android:exported="false"/>
<!-- Services -->
<service android:name="org.xbmc.kore.service.LibrarySyncService"
<service android:name=".service.library.LibrarySyncService"
android:exported="false"/>
<service android:name="org.xbmc.kore.service.NotificationService"
android:exported="false"/>

View File

@ -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<T> {
public final List<T> items;
public final ListType.LimitsReturned limits;
public ApiList(List<T> items, ListType.LimitsReturned limits) {
this.items = items;
this.limits = limits;
}
}

View File

@ -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);

View File

@ -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<List<AudioType.DetailsArtist>> {
public static class GetArtists extends ApiMethod<ApiList<AudioType.DetailsArtist>> {
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<AudioType.DetailsArtist> resultFromJson(ObjectNode jsonObject)
throws ApiException {
public ApiList<AudioType.DetailsArtist> 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<AudioType.DetailsArtist>(0);
return new ApiList<>(new ArrayList<AudioType.DetailsArtist>(0), limits);
}
ArrayList<AudioType.DetailsArtist> result = new ArrayList<AudioType.DetailsArtist>(items.size());
ArrayList<AudioType.DetailsArtist> 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<List<AudioType.DetailsAlbum>> {
public static class GetAlbums extends ApiMethod<ApiList<AudioType.DetailsAlbum>> {
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<AudioType.DetailsAlbum> resultFromJson(ObjectNode jsonObject)
public ApiList<AudioType.DetailsAlbum> 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<AudioType.DetailsAlbum>(0);
return new ApiList<>(new ArrayList<AudioType.DetailsAlbum>(0), limits);
}
ArrayList<AudioType.DetailsAlbum> result = new ArrayList<AudioType.DetailsAlbum>(items.size());
ArrayList<AudioType.DetailsAlbum> 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<List<AudioType.DetailsSong>> {
public static class GetSongs extends ApiMethod<ApiList<AudioType.DetailsSong>> {
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<AudioType.DetailsSong> resultFromJson(ObjectNode jsonObject)
public ApiList<AudioType.DetailsSong> 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<AudioType.DetailsSong>(0);
return new ApiList<>(new ArrayList<AudioType.DetailsSong>(0), limits);
}
ArrayList<AudioType.DetailsSong> result = new ArrayList<AudioType.DetailsSong>(items.size());
ArrayList<AudioType.DetailsSong> result = new ArrayList<>(items.size());
for (JsonNode item : items) {
result.add(new AudioType.DetailsSong(item));
}
return result;
return new ApiList<>(result, limits);
}
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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<SyncOrchestrator> 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<SyncItem> getItemsSyncing(HostInfo hostInfo) {
ArrayList<SyncItem> syncItems = new ArrayList<>();
for( SyncOrchestrator orchestrator : syncOrchestrators) {
if( orchestrator.getHostInfo().getId() == hostInfo.getId() ) {
syncItems.addAll(orchestrator.getSyncItems());
return syncItems;
}
}
return null;
}
}

View File

@ -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();
}

View File

@ -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<VideoType.DetailsMovie>() {
@Override
public void onSuccess(VideoType.DetailsMovie result) {
deleteMovies(contentResolver, hostId, movieId);
List<VideoType.DetailsMovie> movies = new ArrayList<VideoType.DetailsMovie>(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<List<VideoType.DetailsMovie>>() {
@Override
public void onSuccess(List<VideoType.DetailsMovie> 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<VideoType.DetailsMovie> 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);
}
}

View File

@ -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<ApiList<AudioType.DetailsArtist>>() {
@Override
public void onSuccess(ApiList<AudioType.DetailsArtist> result) {
List<AudioType.DetailsArtist> 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<List<LibraryType.DetailsGenre>>() {
@Override
public void onSuccess(List<LibraryType.DetailsGenre> 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<ApiList<AudioType.DetailsAlbum>>() {
@Override
public void onSuccess(ApiList<AudioType.DetailsAlbum> result) {
List<AudioType.DetailsAlbum> 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<ApiList<AudioType.DetailsSong>>() {
@Override
public void onSuccess(ApiList<AudioType.DetailsSong> result) {
List<AudioType.DetailsSong> 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;
}
}

View File

@ -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<List<VideoType.DetailsMusicVideo>>() {
@Override
public void onSuccess(List<VideoType.DetailsMusicVideo> 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<VideoType.DetailsMusicVideo> 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();
}
}

View File

@ -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<SyncItem> 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<SyncItem> 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<SyncItem>();
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<SyncItem> 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);
}
}

View File

@ -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<VideoType.DetailsTVShow>());
} else {
VideoLibrary.GetTVShowDetails action =
new VideoLibrary.GetTVShowDetails(tvshowId, getTVShowsProperties);
action.execute(hostConnection, new ApiCallback<VideoType.DetailsTVShow>() {
@Override
public void onSuccess(VideoType.DetailsTVShow result) {
deleteTVShows(contentResolver, hostId, tvshowId);
List<VideoType.DetailsTVShow> 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<VideoType.DetailsTVShow> 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<List<VideoType.DetailsTVShow>>() {
@Override
public void onSuccess(List<VideoType.DetailsTVShow> 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<VideoType.DetailsTVShow> 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<VideoType.DetailsTVShow> 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<List<VideoType.DetailsSeason>>() {
@Override
public void onSuccess(List<VideoType.DetailsSeason> 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<VideoType.DetailsTVShow> 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<List<VideoType.DetailsEpisode>>() {
@Override
public void onSuccess(List<VideoType.DetailsEpisode> 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();
}
}
}

View File

@ -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<LibrarySyncService.SyncItem> itemsSyncing = service.getItemsSyncing(hostInfo);
ArrayList<SyncItem> 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;

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;