Implemented testing actionbar state (#382)

* Added JSON datafiles for TV shows and music videos
   * Implemented instrumentations tests for MoviesActivity, MusicActivity,
     TVShowActivity, and AddonsActivity.
   * Moved RestoreSearchQueryViewPagerTest to music package as it uses
     the MusicActivity
   * Moved RestoreSearchQueryListFragmentTest to movies packages as it
     uses the MoviesActivity
   * Added scripts to get JSON data for music videos, addons, and TV shows
   * Added sequence diagram for BaseMediaActivity to clarify new setup
   * Refactored BaseMediaActivity to comply with diagram
   * Refactored SyncMusicVideos and SyncTVShows so we can use the same code
     for adding test data as we use for adding real data.
   * Removed unused StringBuffer and synchronize block in MockTcpServer
This commit is contained in:
Martijn Brekhof 2017-04-29 17:08:15 +02:00 committed by Synced Synapse
parent 46bb4a4bfc
commit 1cb77876be
28 changed files with 54966 additions and 164 deletions

View File

@ -21,15 +21,23 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;
import org.hamcrest.Matcher;
import org.xbmc.kore.R;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.clearText;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.action.ViewActions.swipeRight;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
@ -37,10 +45,12 @@ import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFro
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.xbmc.kore.testhelpers.action.ViewActions.clearFocus;
public class EspressoTestUtils {
@ -164,4 +174,110 @@ public class EspressoTestUtils {
public static void checkSearchMenuCollapsed() {
onView(isAssignableFrom(AutoCompleteTextView.class)).check(doesNotExist());
}
/**
* Returns the current active activity. Use this when the originally started activity
* started a new activity and you need the reference to the new activity.
* @return reference to the current active activity
*/
public static Activity getActivity() {
final Activity[] activity = new Activity[1];
onView(allOf(withId(android.R.id.content), isDisplayed())).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(View.class);
}
@Override
public String getDescription() {
return "getting current activity";
}
@Override
public void perform(UiController uiController, View view) {
if (view.getContext() instanceof Activity) {
activity[0] = ((Activity)view.getContext());
}
}
});
return activity[0];
}
/**
* Clicks the album tab in the music activity
*/
public static void clickAlbumsTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeLeft());
onView(withText(R.string.albums)).perform(click());
}
/**
* Clicks the artists tab in the music activity
*/
public static void clickArtistsTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeRight());
onView(withText(R.string.artists)).perform(click());
}
/**
* Clicks the genres tab in the music activity
*/
public static void clickGenresTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeLeft());
onView(withText(R.string.genres)).perform(click());
}
/**
* Clicks the music videos tab in the music activity
*/
public static void clickMusicVideosTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeLeft());
onView(withText(R.string.videos)).perform(click());
}
/**
* Selects an item in the list, then presses back and checks the action bar title
* @param item number (0 is first item) of the item that should be pressed
* @param listResourceId Resource identifier of the AdapterView
* @param actionbarTitle title that should be displayed in the action bar after pressing back
*/
public static void selectListItemPressBackAndCheckActionbarTitle(int item,
int listResourceId,
String actionbarTitle) {
EspressoTestUtils.clickAdapterViewItem(item, listResourceId);
pressBack();
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(actionbarTitle)));
}
/**
* Selects an item in the list, then rotates the device and checks the action bar title
* @param item number (0 is first item) of the item that should be pressed
* @param listResourceId Resource identifier of the AdapterView
* @param actionbarTitle title that should be displayed in the action bar after rotating
*/
public static void selectListItemRotateDeviceAndCheckActionbarTitle(int item,
int listResourceId,
String actionbarTitle,
Activity activity) {
EspressoTestUtils.clickAdapterViewItem(item, listResourceId);
EspressoTestUtils.rotateDevice(activity);
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(actionbarTitle)));
}
/**
* Selects an item in the list and then checks the action bar title
* @param item number (0 is first item) of the item that should be pressed
* @param listResourceId Resource identifier of the AdapterView
* @param actionbarTitle title that should be displayed in the action bar after selecting item
*/
public static void selectListItemAndCheckActionbarTitle(int item,
int listResourceId,
String actionbarTitle) {
EspressoTestUtils.clickAdapterViewItem(item, listResourceId);
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(actionbarTitle)));
}
}

View File

@ -22,6 +22,7 @@ import android.os.IBinder;
import android.support.test.rule.ActivityTestRule;
import android.support.v4.widget.DrawerLayout;
import android.util.Log;
import android.view.Gravity;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostInfo;
@ -54,10 +55,21 @@ public class Utils {
});
}
public static void initialize(ActivityTestRule<?> activityTestRule) throws Throwable {
public static void openDrawer(final ActivityTestRule<?> activityTestRule) throws Throwable {
activityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
DrawerLayout drawerLayout = (DrawerLayout) activityTestRule.getActivity().findViewById(R.id.drawer_layout);
drawerLayout.openDrawer(Gravity.LEFT);
}
});
}
public static void initialize(ActivityTestRule<?> activityTestRule, HostInfo info) throws Throwable {
if (isInitialized)
return;
hostInfo = info;
context = activityTestRule.getActivity();
disableAnimations();
@ -66,7 +78,7 @@ public class Utils {
mediaProvider.setContext(context);
mediaProvider.onCreate();
hostInfo = Database.fill(context, context.getContentResolver());
Database.fill(hostInfo, context, context.getContentResolver());
HostManager.getInstance(context).switchHost(hostInfo);
Utils.closeDrawer(activityTestRule);

View File

@ -0,0 +1,106 @@
/*
* Copyright 2017 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.tests.ui;
import android.content.Intent;
import android.support.test.espresso.Espresso;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.app.AppCompatActivity;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.testhelpers.LoaderIdlingResource;
import org.xbmc.kore.testhelpers.Utils;
import org.xbmc.kore.testutils.Database;
import org.xbmc.kore.testutils.tcpserver.MockTcpServer;
import org.xbmc.kore.testutils.tcpserver.handlers.AddonsHandler;
import org.xbmc.kore.testutils.tcpserver.handlers.JSONConnectionHandlerManager;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@Ignore
abstract public class AbstractTestClass<T extends AppCompatActivity> {
abstract protected ActivityTestRule<T> getActivityTestRule();
private LoaderIdlingResource loaderIdlingResource;
private ActivityTestRule<T> activityTestRule;
private static MockTcpServer server;
private static JSONConnectionHandlerManager manager;
private AddonsHandler addonsHandler;
@BeforeClass
public static void setupMockTCPServer() throws Throwable {
manager = new JSONConnectionHandlerManager();
server = new MockTcpServer(manager);
server.start();
}
@Before
public void setUp() throws Throwable {
activityTestRule = getActivityTestRule();
//Note: as the activity is not yet available in @BeforeClass we need
// to add the handler here
if (addonsHandler == null) {
addonsHandler = new AddonsHandler(activityTestRule.getActivity());
manager.addHandler(addonsHandler);
}
HostInfo hostInfo = Database.addHost(activityTestRule.getActivity(), server.getHostName(),
HostConnection.PROTOCOL_TCP, HostInfo.DEFAULT_HTTP_PORT,
server.getPort());
Utils.initialize(activityTestRule, hostInfo);
loaderIdlingResource = new LoaderIdlingResource(activityTestRule.getActivity().getSupportLoaderManager());
Espresso.registerIdlingResources(loaderIdlingResource);
activityTestRule.launchActivity(new Intent());
Utils.closeDrawer(activityTestRule);
}
@After
public void tearDown() throws Exception {
if ( loaderIdlingResource != null )
Espresso.unregisterIdlingResources(loaderIdlingResource);
Utils.cleanup();
}
@AfterClass
public static void cleanup() throws IOException {
server.shutdown();
}
protected T getActivity() {
if (activityTestRule != null) {
return activityTestRule.getActivity();
}
return null;
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2017 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.tests.ui;
import android.support.test.espresso.Espresso;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.ui.BaseMediaActivity;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Contains generic tests for all activities extending BaseMediaActivity
* @param <T>
*/
@Ignore
abstract public class BaseMediaActivityTests<T extends BaseMediaActivity> extends AbstractTestClass<T> {
/**
* Test if the initial state shows the hamburger icon
*/
@Test
public void showHamburgerInInitialState() {
assertFalse(getActivity().getDrawerIndicatorIsArrow());
}
/**
* Test if navigation icon is changed to an arrow when selecting a list item
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Result: navigation icon should be an arrow
*/
@Test
public void showArrowWhenSelectingListItem() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
assertTrue(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
}
/**
* Test if navigation icon is changed to an arrow when selecting a list item
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: navigation icon should be a hamburger
*/
@Test
public void showHamburgerWhenSelectingListItemAndReturn() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
assertFalse(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
}
/**
* Test if navigation icon is restored to an arrow when selecting a list item
* and rotating the device
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Rotate device
* 3. Result: navigation icon should be an arrow
*/
@Test
public void restoreArrowOnConfigurationChange() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
EspressoTestUtils.rotateDevice(getActivity());
assertTrue(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
}
/**
* Test if navigation icon is restored to an hamburger when selecting a list item
* and rotating the device and returning to the list
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Rotate device
* 3. Press back
* 4. Result: navigation icon should be a hamburger
*/
@Test
public void restoreHamburgerOnConfigurationChangeOnReturn() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
EspressoTestUtils.rotateDevice(getActivity());
Espresso.pressBack();
assertFalse(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright 2017 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.tests.ui.addons;
import android.support.test.rule.ActivityTestRule;
import android.widget.TextView;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.testhelpers.Utils;
import org.xbmc.kore.tests.ui.AbstractTestClass;
import org.xbmc.kore.tests.ui.BaseMediaActivityTests;
import org.xbmc.kore.ui.sections.video.MoviesActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBackAndCheckActionbarTitle;
/**
* Note: we use MoviesActivity here instead of AddonsActivity. The reason is that we use @Rule
* to start the activity which is done prior to executing @Before. This results in a deadlock
* situation.
*
* Normal startup procedure would be as follows:
*
* 1. Start MockTCPServer {@link AbstractTestClass#setupMockTCPServer()}
* 2. Start activity {mActivityRule}
* 3. Espresso waits for activity to become idle before calling {@link AbstractTestClass#setUp()}
* 4. Add AddonsHandler {@link AbstractTestClass#setUp()}
*
* At step 2 the AddonsActivity displays an animated progress indicator while it waits for the
* MockTCPServer to send the list of addons.
* This is never send as the {@link org.xbmc.kore.testutils.tcpserver.handlers.AddonsHandler} is
* added in {@link super#setUp()} which is never started by Espresso as it waits for
* {@link org.xbmc.kore.ui.sections.addon.AddonsActivity} to become idle.
*/
public class AddonsActivityTests extends BaseMediaActivityTests<MoviesActivity> {
@Rule
public ActivityTestRule<MoviesActivity> mActivityRule = new ActivityTestRule<>(
MoviesActivity.class);
@Override
protected ActivityTestRule<MoviesActivity> getActivityTestRule() {
return mActivityRule;
}
@Before
@Override
public void setUp() throws Throwable {
super.setUp();
//Start the AddonsActivity from the MoviesActivity
Utils.openDrawer(getActivityTestRule());
EspressoTestUtils.clickAdapterViewItem(7, R.id.navigation_drawer);
}
/**
* Test if action bar title initially displays Addons
*/
@Test
public void setActionBarTitleMain() {
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(R.string.addons)));
}
/**
* Test if action bar title is correctly set after selecting a list item
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitle() {
EspressoTestUtils.selectListItemAndCheckActionbarTitle(0, R.id.list,
"Dumpert");
}
/**
* Test if action bar title is correctly restored after a configuration change
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Rotate device
* 3. Result: action bar title should show list item title
*/
@Test
public void restoreActionBarTitleOnConfigurationStateChanged() {
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"Dumpert",
getActivity());
}
/**
* Test if action bar title is correctly restored after returning from a movie selection
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: action bar title should show main title
*/
@Test
public void restoreActionBarTitleOnReturningFromMovie() {
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
getActivity().getString(R.string.addons));
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2017 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.tests.ui.movies;
import android.support.test.rule.ActivityTestRule;
import android.widget.TextView;
import org.junit.Rule;
import org.junit.Test;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.tests.ui.BaseMediaActivityTests;
import org.xbmc.kore.ui.sections.video.MoviesActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBackAndCheckActionbarTitle;
public class MoviesActivityTests extends BaseMediaActivityTests<MoviesActivity> {
@Rule
public ActivityTestRule<MoviesActivity> mActivityRule = new ActivityTestRule<>(
MoviesActivity.class);
@Override
protected ActivityTestRule<MoviesActivity> getActivityTestRule() {
return mActivityRule;
}
/**
* Test if action bar title initially displays Movies
*/
@Test
public void setActionBarTitleMain() {
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(R.string.movies)));
}
/**
* Test if action bar title is correctly set after selecting a list item
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitle() {
EspressoTestUtils.selectListItemAndCheckActionbarTitle(0, R.id.list,
"#Rookie93 Marc Marquez: Beyond the Smile");
}
/**
* Test if action bar title is correctly restored after a configuration change
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Rotate device
* 3. Result: action bar title should show list item title
*/
@Test
public void restoreActionBarTitleOnConfigurationStateChanged() {
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"#Rookie93 Marc Marquez: Beyond the Smile",
mActivityRule.getActivity());
}
/**
* Test if action bar title is correctly restored after returning from a movie selection
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: action bar title should show main title
*/
@Test
public void restoreActionBarTitleOnReturningFromMovie() {
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
mActivityRule.getActivity().getString(R.string.movies));
}
}

View File

@ -14,55 +14,36 @@
* limitations under the License.
*/
package org.xbmc.kore.tests.ui;
package org.xbmc.kore.tests.ui.movies;
import android.support.test.espresso.Espresso;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.testhelpers.LoaderIdlingResource;
import org.xbmc.kore.testhelpers.Utils;
import org.xbmc.kore.tests.ui.AbstractTestClass;
import org.xbmc.kore.ui.sections.video.MoviesActivity;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RestoreSearchQueryListFragmentTest {
public class RestoreSearchQueryListFragmentTest extends AbstractTestClass<MoviesActivity> {
private final String SEARCH_QUERY = "Room";
private final int SEARCH_QUERY_LIST_SIZE = 2;
private final int COMPLETE_LIST_SIZE = 300;
private LoaderIdlingResource loaderIdlingResource;
@Rule
public ActivityTestRule<MoviesActivity> mActivityRule = new ActivityTestRule<>(
MoviesActivity.class);
@Before
public void setUp() throws Throwable {
Utils.initialize(mActivityRule);
loaderIdlingResource = new LoaderIdlingResource(mActivityRule.getActivity().getSupportLoaderManager());
Espresso.registerIdlingResources(loaderIdlingResource);
Utils.closeDrawer(mActivityRule);
}
@After
public void tearDown() throws Exception {
Espresso.unregisterIdlingResources(loaderIdlingResource);
}
@AfterClass
public static void cleanup() {
Utils.cleanup();
@Override
protected ActivityTestRule<MoviesActivity> getActivityTestRule() {
return mActivityRule;
}
/**

View File

@ -0,0 +1,260 @@
/*
* Copyright 2017 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.tests.ui.music;
import android.support.test.rule.ActivityTestRule;
import android.widget.TextView;
import org.junit.Rule;
import org.junit.Test;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.tests.ui.BaseMediaActivityTests;
import org.xbmc.kore.ui.sections.audio.MusicActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickAlbumsTab;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickGenresTab;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickMusicVideosTab;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemAndCheckActionbarTitle;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBackAndCheckActionbarTitle;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle;
public class MusicActivityTests extends BaseMediaActivityTests<MusicActivity> {
@Rule
public ActivityTestRule<MusicActivity> musicActivityActivityTestRule =
new ActivityTestRule<>(MusicActivity.class);
@Override
protected ActivityTestRule<MusicActivity> getActivityTestRule() {
return musicActivityActivityTestRule;
}
/**
* Test if action bar title initially displays Music
*/
@Test
public void setActionBarTitleMain() {
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(R.string.music)));
}
/**
* Test if action bar title is correctly set after selecting an artist
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitleArtist() {
selectListItemAndCheckActionbarTitle(0, R.id.list, "ABC Orch");
}
/**
* Test if action bar title is correctly set after selecting an album
*
* UI interaction flow tested:
* 1. Click on albums tab
* 2. Click on list item
* 3. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitleAlbum() {
clickAlbumsTab();
selectListItemAndCheckActionbarTitle(0, R.id.list, "1958 - The Fabulous Johnny Cash");
}
/**
* Test if action bar title is correctly set after selecting a genre
*
* UI interaction flow tested:
* 1. Click on genres tab
* 2. Click on list item
* 3. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitleGenre() {
clickGenresTab();
selectListItemAndCheckActionbarTitle(0, R.id.list, "Ambient");
}
/**
* Test if action bar title is correctly set after selecting a video
*
* UI interaction flow tested:
* 1. Click on videos tab
* 2. Click on list item
* 3. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitleVideo() {
clickMusicVideosTab();
selectListItemAndCheckActionbarTitle(0, R.id.list, "(You Drive Me) Crazy");
}
/**
* Test if action bar title is correctly restored after a configuration change when artist
* is selected
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Rotate device
* 3. Result: action bar title should show list item title
*/
@Test
public void restoreActionBarTitleArtistOnConfigurationStateChanged() {
selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"ABC Orch", getActivity());
}
/**
* Test if action bar title is correctly restored after a configuration change when album
* is selected
*
* UI interaction flow tested:
* 1. Select albums tab
* 2. Click on list item
* 3. Rotate device
* 4. Result: action bar title should show list item title
*/
@Test
public void restoreActionBarTitleAlbumOnConfigurationStateChanged() {
clickAlbumsTab();
selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"1958 - The Fabulous Johnny Cash",
getActivity());
}
/**
* Test if action bar title is correctly restored after a configuration change when genre
* is selected
*
* UI interaction flow tested:
* 1. Select genres tab
* 2. Click on list item
* 3. Rotate device
* 4. Result: action bar title should show list item title
*/
@Test
public void restoreActionBarTitleGenreOnConfigurationStateChanged() {
clickGenresTab();
selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"Ambient", getActivity());
}
/**
* Test if action bar title is correctly restored after a configuration change when music video
* is selected
*
* UI interaction flow tested:
* 1. Select music videos tab
* 2. Click on list item
* 3. Rotate device
* 4. Result: action bar title should show list item title
*/
@Test
public void restoreActionBarTitleMusicVideoOnConfigurationStateChanged() {
clickMusicVideosTab();
selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"(You Drive Me) Crazy",
getActivity());
}
/**
* Test if action bar title is correctly restored after returning from artist selection
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: action bar title should show main title
*/
@Test
public void restoreActionBarTitleOnReturningFromArtist() {
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
getActivity().getString(R.string.music));
}
/**
* Test if action bar title is correctly restored after returning from an album under
* artist
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Select albums tab
* 3. Press back
* 4. Result: action bar title should show artist title
*/
@Test
public void restoreActionBarTitleOnArtistOnReturningFromAlbum() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
clickAlbumsTab();
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list, "ABC Orch");
}
/**
* Test if action bar title is correctly restored after returning from music video selection
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: action bar title should show main title
*/
@Test
public void restoreActionBarTitleOnReturningFromMusicVideo() {
clickMusicVideosTab();
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
getActivity().getString(R.string.music));
}
/**
* Test if action bar title is correctly restored after returning from genre selection
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: action bar title should show main title
*/
@Test
public void restoreActionBarTitleOnReturningFromGenre() {
clickGenresTab();
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
getActivity().getString(R.string.music));
}
/**
* Test if action bar title is correctly restored after returning from album selection
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Press back
* 3. Result: action bar title should show main title
*/
@Test
public void restoreActionBarTitleOnReturningFromAlbum() {
clickAlbumsTab();
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
getActivity().getString(R.string.music));
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.xbmc.kore.tests.ui;
package org.xbmc.kore.tests.ui.music;
import android.app.Activity;
import android.support.test.espresso.Espresso;
@ -22,28 +22,21 @@ import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.testhelpers.LoaderIdlingResource;
import org.xbmc.kore.testhelpers.Utils;
import org.xbmc.kore.tests.ui.AbstractTestClass;
import org.xbmc.kore.ui.sections.audio.MusicActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.action.ViewActions.swipeRight;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickAlbumsTab;
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickArtistsTab;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RestoreSearchQueryViewPagerTest {
public class RestoreSearchQueryViewPagerTest extends AbstractTestClass<MusicActivity> {
private final String ARTIST_SEARCH_QUERY = "Ben";
private final int ARTIST_SEARCH_QUERY_LIST_SIZE = 2;
@ -58,22 +51,9 @@ public class RestoreSearchQueryViewPagerTest {
public ActivityTestRule<MusicActivity> mActivityRule = new ActivityTestRule<>(
MusicActivity.class);
@Before
public void setUp() throws Throwable {
Utils.initialize(mActivityRule);
loaderIdlingResource = new LoaderIdlingResource(mActivityRule.getActivity().getSupportLoaderManager());
Espresso.registerIdlingResources(loaderIdlingResource);
Utils.closeDrawer(mActivityRule);
}
@After
public void tearDown() throws Exception {
Espresso.unregisterIdlingResources(loaderIdlingResource);
}
@AfterClass
public static void cleanup() {
Utils.cleanup();
@Override
protected ActivityTestRule<MusicActivity> getActivityTestRule() {
return mActivityRule;
}
/**
@ -223,7 +203,7 @@ public class RestoreSearchQueryViewPagerTest {
* UI interaction flow tested:
* 1. Enter search query
* 2. Switch to Albums tab
* 3. Rotated device
* 3. Rotate device
* 4. Switch to Artists tab
* 5. Result: search query entered at 1. should show in search field and list should match search query
*/
@ -325,14 +305,4 @@ public class RestoreSearchQueryViewPagerTest {
EspressoTestUtils.checkTextInSearchQuery(ALBUMS_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ALBUMS_SEARCH_QUERY, ALBUM_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
private void clickAlbumsTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeLeft());
onView(withText(R.string.albums)).perform(click());
}
private void clickArtistsTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeRight());
onView(withText(R.string.artists)).perform(click());
}
}

View File

@ -0,0 +1,193 @@
/*
* Copyright 2017 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.tests.ui.tvshows;
import android.support.test.rule.ActivityTestRule;
import android.widget.TextView;
import org.junit.Rule;
import org.junit.Test;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.tests.ui.BaseMediaActivityTests;
import org.xbmc.kore.ui.sections.video.TVShowsActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.scrollTo;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
public class TVShowsActivityTests extends BaseMediaActivityTests<TVShowsActivity> {
@Rule
public ActivityTestRule<TVShowsActivity> mActivityRule = new ActivityTestRule<>(
TVShowsActivity.class);
@Override
protected ActivityTestRule<TVShowsActivity> getActivityTestRule() {
return mActivityRule;
}
/**
* Test if action bar title initially displays TV Shows
*/
@Test
public void setActionBarTitleMain() {
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText(R.string.tv_shows)));
}
/**
* Test if action bar title is correctly set after selecting a list item
*
* UI interaction flow tested:
* 1. Click on list item
* 2. Result: action bar title should show list item title
*/
@Test
public void setActionBarTitle() {
EspressoTestUtils.selectListItemAndCheckActionbarTitle(0, R.id.list, "11.22.63");
}
/**
* Test if action bar title is correctly set after selecting a season
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Click on next episode item
* 3. Result: action bar title should show next episode title
*/
@Test
public void setActionBarTitleOnNextEpisode() {
EspressoTestUtils.clickAdapterViewItem(1, R.id.list);
onView( withId(R.id.next_episode_list)).perform( scrollTo(), click());
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText("3")));
}
/**
* Test if action bar title is correctly set after selecting a season
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Click on season item
* 3. Result: action bar title should show season title
*/
@Test
public void setActionBarTitleOnSeasonList() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
onView( withId(R.id.seasons_list)).perform( scrollTo(), click());
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText("Season 01")));
}
/**
* Test if action bar title is correctly set after selecting an episode from the season list
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Click on season item
* 3. Click on an episode
* 4. Result: action bar title should show episode title
*/
@Test
public void setActionBarTitleOnSeasonListEpisode() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
onView( withId(R.id.seasons_list)).perform( scrollTo(), click());
EspressoTestUtils.selectListItemAndCheckActionbarTitle(0, R.id.list, "11.22.63");
}
/**
* Test if action bar title is correctly restored after a configuration change
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Rotate device
* 3. Result: action bar title should show TV show item title
*/
@Test
public void restoreActionBarTitleOnConfigurationStateChanged() {
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"11.22.63",
mActivityRule.getActivity());
}
/**
* Test if action bar title is correctly restored on season list after a configuration change
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Click on season item
* 3. Rotate device
* 4. Result: action bar title should show season title
*/
@Test
public void restoreActionBarTitleSeasonListOnConfigurationStateChanged() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
onView( withId(R.id.seasons_list)).perform( scrollTo(), click());
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText("Season 01")));
}
/**
* Test if action bar title is correctly restored on episode item title after a configuration change
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Click on season item
* 3. Click on episode item
* 4. Rotate device
* 5. Result: action bar title should TV show title
*/
@Test
public void restoreActionBarTitleSeasonListEpisodeOnConfigurationStateChanged() {
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
onView( withId(R.id.seasons_list)).perform( scrollTo(), click());
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle(0, R.id.list,
"11.22.63",
mActivityRule.getActivity());
}
/**
* Test if action bar title is correctly restored on next episode item title after a configuration change
*
* UI interaction flow tested:
* 1. Click on TV Show item
* 2. Click on next episode item
* 3. Rotate device
* 4. Result: action bar title should show season title
*/
@Test
public void restoreActionBarTitleNextEpisodeOnConfigurationStateChanged() {
EspressoTestUtils.clickAdapterViewItem(1, R.id.list);
onView( withId(R.id.next_episode_list)).perform( scrollTo() );
onView( withText("You'll See the Sparkle")).perform( click() );
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
.check(matches(withText("3")));
}
}

View File

@ -86,7 +86,8 @@ public class SyncMusicVideos extends SyncItem {
@Override
public void onSuccess(List<VideoType.DetailsMusicVideo> result) {
deleteMusicVideos(contentResolver, hostId);
insertMusicVideos(orchestrator, contentResolver, result);
insertMusicVideos(result, contentResolver);
orchestrator.syncItemFinished();
}
@Override
@ -104,9 +105,7 @@ public class SyncMusicVideos extends SyncItem {
where, new String[]{String.valueOf(hostId)});
}
private void insertMusicVideos(final SyncOrchestrator orchestrator,
final ContentResolver contentResolver,
final List<VideoType.DetailsMusicVideo> musicVideos) {
public void insertMusicVideos(List<VideoType.DetailsMusicVideo> musicVideos, ContentResolver contentResolver) {
ContentValues musicVideosValuesBatch[] = new ContentValues[musicVideos.size()];
// Iterate on each music video
@ -117,6 +116,5 @@ public class SyncMusicVideos extends SyncItem {
// Insert the movies
contentResolver.bulkInsert(MediaContract.MusicVideos.CONTENT_URI, musicVideosValuesBatch);
orchestrator.syncItemFinished();
}
}

View File

@ -115,8 +115,9 @@ public class SyncTVShows extends SyncItem {
deleteTVShows(contentResolver, hostId, tvshowId);
List<VideoType.DetailsTVShow> tvShows = new ArrayList<>(1);
tvShows.add(result);
insertTVShowsAndGetDetails(orchestrator, hostConnection, callbackHandler,
contentResolver, tvShows);
insertTVShows(tvShows, contentResolver);
chainSyncSeasons(orchestrator, hostConnection, callbackHandler,
contentResolver, tvShows, 0);
// insertTVShows calls syncItemFinished
}
@ -157,8 +158,10 @@ public class SyncTVShows extends SyncItem {
// 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);
insertTVShows(allResults, contentResolver);
chainSyncSeasons(orchestrator, hostConnection, callbackHandler,
contentResolver, allResults, 0);
}
}
@ -197,42 +200,6 @@ public class SyncTVShows extends SyncItem {
}
}
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,
@ -270,27 +237,8 @@ public class SyncTVShows extends SyncItem {
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);
}
insertSeason(tvShow.tvshowid, result, contentResolver);
// Sync the next tv show
chainSyncSeasons(orchestrator, hostConnection, callbackHandler,
@ -358,13 +306,8 @@ public class SyncTVShows extends SyncItem {
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);
insertEpisodes(result, contentResolver);
chainSyncEpisodes(orchestrator, hostConnection, callbackHandler,
contentResolver, tvShows, position + 1);
@ -382,4 +325,67 @@ public class SyncTVShows extends SyncItem {
orchestrator.syncItemFinished();
}
}
public void insertTVShows(List<VideoType.DetailsTVShow> tvShows, ContentResolver contentResolver) {
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);
}
public void insertSeason(int tvshowId, List<VideoType.DetailsSeason> result, ContentResolver contentResolver) {
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, tvshowId);
ContentValues tvshowUpdate = new ContentValues(1);
tvshowUpdate.put(MediaContract.TVShowsColumns.WATCHEDEPISODES, totalWatchedEpisodes);
contentResolver.update(uri, tvshowUpdate, null, null);
}
}
public void insertEpisodes(List<VideoType.DetailsEpisode> episodes, ContentResolver contentResolver) {
ContentValues episodesValuesBatch[] = new ContentValues[episodes.size()];
for (int i = 0; i < episodes.size(); i++) {
VideoType.DetailsEpisode episode = episodes.get(i);
episodesValuesBatch[i] = SyncUtils.contentValuesFromEpisode(hostId, episode);
}
// Insert the episodes
contentResolver.bulkInsert(MediaContract.Episodes.CONTENT_URI, episodesValuesBatch);
}
}

View File

@ -79,14 +79,17 @@ public abstract class BaseMediaActivity extends AppCompatActivity {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
String actionBarTitle;
boolean naviconIsArrow = false;
if (savedInstanceState != null) {
updateActionBar(savedInstanceState.getString(ACTIONBAR_TITLE),
savedInstanceState.getBoolean(NAVICON_ISARROW));
actionBarTitle = savedInstanceState.getString(ACTIONBAR_TITLE);
naviconIsArrow = savedInstanceState.getBoolean(NAVICON_ISARROW);
} else {
updateActionBar(getActionBarTitle(), false);
actionBarTitle = getActionBarTitle();
}
actionBar.setDisplayHomeAsUpEnabled(true);
updateActionBar(actionBarTitle, naviconIsArrow);
}
String fragmentTitle = getActionBarTitle();
@ -183,7 +186,5 @@ public abstract class BaseMediaActivity extends AppCompatActivity {
fragTrans.replace(R.id.fragment_container, fragment, getActionBarTitle())
.addToBackStack(null)
.commit();
dataHolder.getBundle().putBoolean(NAVICON_ISARROW, true);
}
}

View File

@ -48,6 +48,8 @@ public class AbstractTestClass {
ShadowContentResolver.registerProvider("org.xbmc.kore.provider", provider);
provider.onCreate();
hostInfo = Database.fill(RuntimeEnvironment.application, contentResolver);
hostInfo = Database.addHost(RuntimeEnvironment.application);
Database.fill(hostInfo, RuntimeEnvironment.application, contentResolver);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,581 @@
{
"id" : "libMovies",
"jsonrpc" : "2.0",
"result" : {
"musicvideos" : [
{
"album" : "...Baby One More Time",
"director" : [
"Nigel Dick"
],
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fbaby-one-more-time-4dcff7453745a.jpg/"
},
"plot" : "\"(You Drive Me) Crazy\" is a song by American recording artist Britney Spears from her debut studio album, ...Baby One More Time (1999). Written and produced by Max Martin, Per Magnusson and David Kreuger, with additional writing by Jörgen Elofsson and remix by Martin and Rami Yacoub, it was released as the album's third single on August 23, 1999 by Jive Records. It was remixed for the soundtrack of Drive Me Crazy. \"(You Drive Me) Crazy\" is a teen pop song that draws influences from R&B and rock. The song garnered positive reviews from music critics, some of whom praised its simple formula and noted similarities to Spears' debut single, \"...Baby One More Time\".\n\n\"(You Drive Me) Crazy\" was a commercial success, and peaked inside the top ten on the singles charts of seventeen countries. In the United Kingdom, it became Spears' third consecutive single to peak inside the top five, while it reached number 10 in the United States' Hot 100, and peaked at number one in Belgium (Wallonia). An accompanying music video, directed by Nigel Dick, and portrayed Spears as a waitress of a dance club, and performed a highly choreographed dance routine with the other waitresses. The video premiered on MTV's Making the Video special, and featured cameo appearances of actors Melissa Joan Hart and Adrien Grenier. As part of promotion for the song, Spears performed the song at the 1999 MTV Europe Music Awards and 1999 Billboard Music Awards. It has also been included on five of her concert tours.",
"resume" : {
"position" : 0,
"total" : 0
},
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fbaby-one-more-time-4dcff7453745a.jpg/",
"fanart" : "",
"rating" : 0,
"year" : 1999,
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Britney Spears - (You Drive Me) Crazy (1999).mp4",
"dateadded" : "2016-12-29 16:50:28",
"userrating" : 0,
"artist" : [
"Britney Spears"
],
"lastplayed" : "2017-02-28 11:07:15",
"studio" : [],
"tag" : [],
"title" : "(You Drive Me) Crazy",
"label" : "(You Drive Me) Crazy",
"runtime" : 12,
"track" : -1,
"genre" : [
"Pop"
],
"streamdetails" : {
"audio" : [
{
"channels" : 2,
"language" : "und",
"codec" : "aac"
}
],
"video" : [
{
"width" : 480,
"language" : "und",
"height" : 360,
"duration" : 12,
"stereomode" : "",
"aspect" : 1.33333301544189,
"codec" : "h264"
}
],
"subtitle" : []
},
"musicvideoid" : 60,
"premiered" : "1999-01-01",
"playcount" : 1
},
{
"resume" : {
"total" : 0,
"position" : 0
},
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fsvpxyw1364737910.jpg/"
},
"director" : [],
"plot" : "",
"album" : "Rubber Factory",
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/The Black Keys - 10 A.M. Automatic (2004).mp4",
"rating" : 0,
"year" : 2004,
"fanart" : "",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fsvpxyw1364737910.jpg/",
"track" : -1,
"label" : "10 A.M. Automatic",
"runtime" : 12,
"lastplayed" : "",
"studio" : [],
"title" : "10 A.M. Automatic",
"tag" : [],
"dateadded" : "2016-12-29 16:50:29",
"artist" : [
"The Black Keys"
],
"userrating" : 0,
"premiered" : "2004-01-01",
"playcount" : 0,
"musicvideoid" : 370,
"streamdetails" : {
"subtitle" : [],
"audio" : [
{
"channels" : 2,
"language" : "und",
"codec" : "aac"
}
],
"video" : [
{
"language" : "",
"height" : 360,
"width" : 480,
"duration" : 12,
"stereomode" : "",
"aspect" : 1.33333301544189,
"codec" : "avc1"
}
]
},
"genre" : [
"Indie"
]
},
{
"runtime" : 12,
"label" : "99 Problems",
"track" : -1,
"userrating" : 0,
"artist" : [
"Jay-Z"
],
"dateadded" : "2016-12-29 16:50:28",
"title" : "99 Problems",
"tag" : [],
"studio" : [
"Anonymous Content"
],
"lastplayed" : "",
"musicvideoid" : 164,
"streamdetails" : {
"audio" : [
{
"codec" : "aac",
"channels" : 2,
"language" : "und"
}
],
"video" : [
{
"aspect" : 1.33333301544189,
"stereomode" : "",
"codec" : "avc1",
"duration" : 12,
"width" : 480,
"language" : "",
"height" : 360
}
],
"subtitle" : []
},
"premiered" : "2003-01-01",
"playcount" : 0,
"genre" : [
"Hip-Hop"
],
"director" : [
"Mark Romanek"
],
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fthe-black-album-4ee5475d9f478.jpg/"
},
"plot" : "\"99 Problems\" is the third single released by American rapper Jay-Z in 2004 from The Black Album. The song was originally written by rapper Ice-T in 1993. Throughout the song Jay-Z tells a story about dealing with a racist police officer who wants to illegally search his car, dealing with rap critics, and dealing with an aggressor. The song reached number 30 on the Billboard Hot 100.\nThe track was produced by Rick Rubin, his first hip hop production in many years. Rubin provided Jay-Z with a guitar riff and stripped-down beat that were once his trademarks. In creating the track Rubin used some classic 1980s sample staples such as \"The Big Beat\" by Billy Squier, \"Long Red\" by Mountain, and \"Get Me Back On Time\" by Wilson Pickett. These songs were long coveted by early hip hop producers, in particular the drum beat from Big Beat, used most famously by RunD.M.C. on \"Here We Go\" in 1985 and by British rapper Dizzee Rascal a year prior to Jay-Z on his break-through hit \"Fix Up, Look Sharp\". It also featured on the popular Ultimate Breaks and Beats series.\nWhile the song's meaning is widely debated, the chorus \"If you're having girl problems, I feel bad for you son/I've got 99 problems but a bitch ain't one\" was defined in Jay-Z's book, Decoded, as referring to a police dog. Jay-Z wrote that in 1994 he was pulled over by police while carrying cocaine in a secret compartment in his sunroof. Jay-Z refused to let the police search the car and the police called for the drug sniffing dogs. However, the dogs never showed up and the police had to let Jay-Z go. Moments after he drove away, he wrote that he saw a police car with the dogs drive by.\nThe title and chorus are taken from Ice-T's \"99 Problems\" from his 1993 album Home Invasion. The song featured Brother Marquis of 2 Live Crew. The original song was more profane and describes a wide range of sexual conquests. Portions of Ice-T's original lyrics were similarly quoted in a song by fellow rapper Trick Daddy on a track also titled \"99 Problems\" from his 2001 album Thugs Are Us. Jay-Z begins his third verse directly quoting lines from Bun B's opening verse off the track \"Touched\" from the UGK album Ridin' Dirty.",
"resume" : {
"position" : 0,
"total" : 0
},
"album" : "The Black Album",
"year" : 2003,
"rating" : 0,
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Jay-Z - 99 Problems (2004).mp4",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fthe-black-album-4ee5475d9f478.jpg/",
"fanart" : ""
},
{
"album" : "Dirty",
"resume" : {
"total" : 0,
"position" : 0
},
"plot" : "",
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fdirty-50030abf884f2.jpg/"
},
"director" : [
"Tamra Davis"
],
"fanart" : "",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fdirty-50030abf884f2.jpg/",
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Sonic Youth - 100% (1992).mp4",
"rating" : 0,
"year" : 1992,
"lastplayed" : "",
"studio" : [],
"tag" : [],
"title" : "100%",
"dateadded" : "2016-12-29 16:50:28",
"userrating" : 0,
"artist" : [
"Sonic Youth"
],
"track" : -1,
"label" : "100%",
"runtime" : 12,
"genre" : [
"Alternative Rock"
],
"premiered" : "1992-01-01",
"playcount" : 0,
"streamdetails" : {
"audio" : [
{
"language" : "und",
"channels" : 2,
"codec" : "aac"
}
],
"video" : [
{
"stereomode" : "",
"aspect" : 1.33333301544189,
"codec" : "avc1",
"width" : 480,
"language" : "",
"height" : 360,
"duration" : 12
}
],
"subtitle" : []
},
"musicvideoid" : 349
},
{
"resume" : {
"position" : 0,
"total" : 0
},
"art" : {
"thumb" : "image://video@%2fUsers%2fmartijn%2fProjects%2fdummymediafiles%2fmedia%2fmusicvideos%2fPeter%20Himmelman%20-%20245%20Days%20(1990).mp4/"
},
"director" : [],
"plot" : "",
"album" : "Synesthesia",
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Peter Himmelman - 245 Days (1990).mp4",
"year" : 1989,
"rating" : 0,
"fanart" : "",
"thumbnail" : "image://video@%2fUsers%2fmartijn%2fProjects%2fdummymediafiles%2fmedia%2fmusicvideos%2fPeter%20Himmelman%20-%20245%20Days%20(1990).mp4/",
"track" : -1,
"runtime" : 12,
"label" : "245 Days",
"tag" : [],
"title" : "245 Days",
"lastplayed" : "",
"studio" : [],
"artist" : [
"Peter Himmelman"
],
"userrating" : 0,
"dateadded" : "2016-12-29 16:50:28",
"premiered" : "1989-01-01",
"playcount" : 0,
"streamdetails" : {
"audio" : [
{
"language" : "und",
"channels" : 2,
"codec" : "aac"
}
],
"video" : [
{
"stereomode" : "",
"codec" : "avc1",
"aspect" : 1.33333301544189,
"duration" : 12,
"width" : 480,
"language" : "",
"height" : 360
}
],
"subtitle" : []
},
"musicvideoid" : 297,
"genre" : [
"..."
]
},
{
"dateadded" : "2016-12-29 16:50:28",
"artist" : [
"Public Enemy"
],
"userrating" : 0,
"studio" : [],
"lastplayed" : "2017-03-02 10:43:26",
"tag" : [],
"title" : "911 Is a Joke",
"label" : "911 Is a Joke",
"runtime" : 12,
"track" : -1,
"genre" : [
"Hip-Hop"
],
"streamdetails" : {
"subtitle" : [],
"audio" : [
{
"codec" : "aac",
"language" : "und",
"channels" : 2
}
],
"video" : [
{
"codec" : "h264",
"stereomode" : "",
"aspect" : 1.33333301544189,
"language" : "und",
"height" : 360,
"width" : 480,
"duration" : 12
}
]
},
"musicvideoid" : 306,
"premiered" : "1990-01-01",
"playcount" : 1,
"album" : "Fear of a Black Planet",
"director" : [],
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ffear-of-a-black-planet-526965153a1e4.jpg/"
},
"plot" : "\"911 Is a Joke\" is a 1990 song by American hip hop group Public Enemy, from their third album, Fear of a Black Planet. The song is solely done by Flavor Flav. It was released as a single and became a hit in June 1990, reaching number 15 on the Hot R&B/Hip-Hop Singles & Tracks chart, and number 1 on the Hot Rap Singles chart, becoming their second number-one rap chart hit after \"Fight the Power\". It also reached number one on the Bubbling Under Hot 100 Singles chart. This was due largely to its sales, which were unusually high for the level of mainstream airplay it received; Billboard reported that only one of the stations on its Top 40 panel was playing it.\n\nThe song is about the lack of response to emergency calls in a black neighborhood, but specifically references the poor response by paramedic crews and not the police, which is a common misconception regarding the track; the \"911\" in the title of the song refers to 9-1-1, the emergency telephone number used in North America.",
"resume" : {
"total" : 0,
"position" : 0
},
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ffear-of-a-black-planet-526965153a1e4.jpg/",
"fanart" : "",
"rating" : 0,
"year" : 1990,
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Public Enemy - 911 is a Joke (1990).mp4"
},
{
"label" : "A Case of You",
"runtime" : 12,
"track" : -1,
"dateadded" : "2016-12-29 16:50:28",
"userrating" : 0,
"artist" : [
"James Blake"
],
"lastplayed" : "",
"studio" : [],
"tag" : [],
"title" : "A Case of You",
"streamdetails" : {
"audio" : [
{
"channels" : 2,
"language" : "und",
"codec" : "aac"
}
],
"video" : [
{
"width" : 480,
"height" : 360,
"language" : "",
"duration" : 12,
"stereomode" : "",
"aspect" : 1.33333301544189,
"codec" : "avc1"
}
],
"subtitle" : []
},
"musicvideoid" : 160,
"premiered" : "2011-01-01",
"playcount" : 0,
"genre" : [
"Electronic"
],
"director" : [],
"plot" : "",
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ftyswrr1377556931.jpg/"
},
"resume" : {
"total" : 0,
"position" : 0
},
"album" : "Enough Thunder",
"rating" : 0,
"year" : 2011,
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/James Blake - A Case Of You (2011).mp4",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ftyswrr1377556931.jpg/",
"fanart" : ""
},
{
"track" : -1,
"label" : "A Little Respect",
"runtime" : 12,
"studio" : [],
"lastplayed" : "",
"tag" : [],
"title" : "A Little Respect",
"dateadded" : "2016-12-29 16:50:28",
"artist" : [
"Wheatus"
],
"userrating" : 0,
"premiered" : "2000-01-01",
"playcount" : 0,
"musicvideoid" : 430,
"streamdetails" : {
"subtitle" : [],
"audio" : [
{
"channels" : 2,
"language" : "und",
"codec" : "aac"
}
],
"video" : [
{
"stereomode" : "",
"codec" : "avc1",
"aspect" : 1.33333301544189,
"duration" : 12,
"language" : "",
"height" : 360,
"width" : 480
}
]
},
"genre" : [
"Rock"
],
"resume" : {
"total" : 0,
"position" : 0
},
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fwheatus-5144f1c42ea0a.jpg/"
},
"plot" : "",
"director" : [],
"album" : "Wheatus",
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Wheatus - A Little Respect (2000).mp4",
"rating" : 0,
"year" : 2000,
"fanart" : "",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fwheatus-5144f1c42ea0a.jpg/"
},
{
"rating" : 0,
"year" : 1996,
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Counting Crows - A Long December (1996).mp4",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frecovering-the-satellites-4dcfda1792ec0.jpg/",
"fanart" : "",
"plot" : "",
"director" : [],
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frecovering-the-satellites-4dcfda1792ec0.jpg/"
},
"resume" : {
"total" : 0,
"position" : 0
},
"album" : "Recovering the Satellites",
"musicvideoid" : 79,
"streamdetails" : {
"video" : [
{
"duration" : 12,
"width" : 480,
"height" : 360,
"language" : "",
"aspect" : 1.33333301544189,
"stereomode" : "",
"codec" : "avc1"
}
],
"audio" : [
{
"language" : "und",
"channels" : 2,
"codec" : "aac"
}
],
"subtitle" : []
},
"premiered" : "1996-01-01",
"playcount" : 0,
"genre" : [
"Alternative Rock"
],
"label" : "A Long December",
"runtime" : 12,
"track" : -1,
"dateadded" : "2016-12-29 16:50:28",
"artist" : [
"Counting Crows"
],
"userrating" : 0,
"studio" : [],
"lastplayed" : "",
"tag" : [],
"title" : "A Long December"
},
{
"genre" : [
"Folk"
],
"premiered" : "1985-01-01",
"playcount" : 0,
"streamdetails" : {
"subtitle" : [],
"audio" : [
{
"channels" : 2,
"language" : "und",
"codec" : "aac"
}
],
"video" : [
{
"stereomode" : "",
"aspect" : 1.33333301544189,
"codec" : "avc1",
"duration" : 12,
"language" : "",
"height" : 360,
"width" : 480
}
]
},
"musicvideoid" : 390,
"tag" : [],
"title" : "A Pair of Brown Eyes",
"lastplayed" : "",
"studio" : [],
"artist" : [
"The Pogues"
],
"userrating" : 0,
"dateadded" : "2016-12-29 16:50:28",
"track" : -1,
"runtime" : 12,
"label" : "A Pair of Brown Eyes",
"fanart" : "",
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frum-sodomy--the-lash-52f1dda495de3.jpg/",
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/The Pogues - A Pair of Brown Eyes (1985).mp4",
"year" : 1985,
"rating" : 0,
"album" : "Rum Sodomy & the Lash",
"resume" : {
"position" : 0,
"total" : 0
},
"art" : {
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frum-sodomy--the-lash-52f1dda495de3.jpg/"
},
"director" : [],
"plot" : ""
}
],
"limits" : {
"total" : 439,
"start" : 0,
"end" : 10
}
}
}

View File

@ -0,0 +1,465 @@
{
"id" : "libTVShowSeasons",
"jsonrpc" : "2.0",
"result" : {
"limits" : {
"end" : 21,
"total" : 21,
"start" : 0
},
"seasons" : [
{
"playcount" : 0,
"episode" : 8,
"label" : "Season 1",
"userrating" : 0,
"thumbnail" : "",
"watchedepisodes" : 3,
"seasonid" : 6,
"art" : {
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f260473-1.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f260473-g.jpg/"
},
"fanart" : "",
"showtitle" : "3",
"season" : 1,
"tvshowid" : 2
},
{
"userrating" : 0,
"thumbnail" : "",
"episode" : 49,
"playcount" : 0,
"label" : "Season 1",
"seasonid" : 9,
"watchedepisodes" : 2,
"art" : {
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/"
},
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
"season" : 1,
"showtitle" : "4 Stjerners Middag",
"tvshowid" : 3
},
{
"episode" : 41,
"playcount" : 0,
"label" : "Season 2",
"userrating" : 0,
"thumbnail" : "",
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/"
},
"seasonid" : 10,
"watchedepisodes" : 0,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
"tvshowid" : 3,
"season" : 2,
"showtitle" : "4 Stjerners Middag"
},
{
"playcount" : 0,
"episode" : 20,
"label" : "Season 3",
"userrating" : 0,
"thumbnail" : "",
"art" : {
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/"
},
"watchedepisodes" : 0,
"seasonid" : 11,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
"tvshowid" : 3,
"showtitle" : "4 Stjerners Middag",
"season" : 3
},
{
"episode" : 8,
"playcount" : 1,
"label" : "Season 1",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f301824-1-4.jpg/",
"userrating" : 0,
"art" : {
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f301824-g.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f301824-1-4.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f301824-8.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f301824-1-4.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/"
},
"watchedepisodes" : 8,
"seasonid" : 3,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/",
"tvshowid" : 1,
"showtitle" : "11.22.63",
"season" : 1
},
{
"tvshowid" : 137,
"showtitle" : "The A-Team",
"season" : 1,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"art" : {
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-1.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-1.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-1.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/"
},
"watchedepisodes" : 0,
"seasonid" : 553,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-1.jpg/",
"userrating" : 0,
"label" : "Season 1",
"playcount" : 0,
"episode" : 13
},
{
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-2.jpg/",
"playcount" : 0,
"label" : "Season 2",
"episode" : 23,
"seasonid" : 554,
"watchedepisodes" : 0,
"art" : {
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-2.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-2.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-2.jpg/"
},
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"showtitle" : "The A-Team",
"season" : 2,
"tvshowid" : 137
},
{
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"showtitle" : "The A-Team",
"season" : 3,
"tvshowid" : 137,
"episode" : 25,
"playcount" : 0,
"label" : "Season 3",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-3.jpg/",
"userrating" : 0,
"seasonid" : 555,
"watchedepisodes" : 0,
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-3.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-3.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-3.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-3.jpg/"
}
},
{
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"showtitle" : "The A-Team",
"season" : 4,
"tvshowid" : 137,
"episode" : 23,
"playcount" : 0,
"label" : "Season 4",
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-4.jpg/",
"watchedepisodes" : 0,
"seasonid" : 556,
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-4.jpg/",
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-4.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-4.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-4.jpg/"
}
},
{
"label" : "Season 5",
"playcount" : 0,
"episode" : 13,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-5.jpg/",
"userrating" : 0,
"seasonid" : 557,
"watchedepisodes" : 0,
"art" : {
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-5.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-5.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/"
},
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"season" : 5,
"showtitle" : "The A-Team",
"tvshowid" : 137
},
{
"watchedepisodes" : 9,
"seasonid" : 14,
"art" : {
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-1-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-1-2.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
},
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-1-2.jpg/",
"playcount" : 0,
"episode" : 22,
"label" : "Season 1",
"season" : 1,
"showtitle" : "According to Jim",
"tvshowid" : 4,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
},
{
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-2-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-2-2.jpg/"
},
"seasonid" : 15,
"watchedepisodes" : 0,
"playcount" : 0,
"label" : "Season 2",
"episode" : 28,
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-2-2.jpg/",
"tvshowid" : 4,
"season" : 2,
"showtitle" : "According to Jim",
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
},
{
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-3-2.jpg/",
"playcount" : 0,
"label" : "Season 3",
"episode" : 29,
"watchedepisodes" : 0,
"seasonid" : 16,
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-3-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-3-2.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/"
},
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"showtitle" : "According to Jim",
"season" : 3,
"tvshowid" : 4
},
{
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"season" : 4,
"showtitle" : "According to Jim",
"tvshowid" : 4,
"episode" : 27,
"playcount" : 0,
"label" : "Season 4",
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-4-2.jpg/",
"watchedepisodes" : 0,
"seasonid" : 17,
"art" : {
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-4-2.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-4-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
}
},
{
"showtitle" : "According to Jim",
"season" : 5,
"tvshowid" : 4,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"seasonid" : 18,
"watchedepisodes" : 0,
"art" : {
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f3449-5.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f3449-5.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/"
},
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f3449-5.jpg/",
"label" : "Season 5",
"playcount" : 0,
"episode" : 22
},
{
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"season" : 6,
"showtitle" : "According to Jim",
"tvshowid" : 4,
"playcount" : 0,
"episode" : 18,
"label" : "Season 6",
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-6-2.jpg/",
"seasonid" : 19,
"watchedepisodes" : 0,
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-6-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-6-2.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/"
}
},
{
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"tvshowid" : 4,
"season" : 7,
"showtitle" : "According to Jim",
"episode" : 18,
"playcount" : 0,
"label" : "Season 7",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-7-2.jpg/",
"userrating" : 0,
"art" : {
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-7-2.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-7-2.jpg/"
},
"watchedepisodes" : 0,
"seasonid" : 20
},
{
"watchedepisodes" : 0,
"seasonid" : 21,
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-8-2.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-8-2.jpg/"
},
"playcount" : 0,
"episode" : 18,
"label" : "Season 8",
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-8-2.jpg/",
"season" : 8,
"showtitle" : "According to Jim",
"tvshowid" : 4,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
},
{
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
"tvshowid" : 138,
"season" : 1,
"showtitle" : "The Adventures of Abney & Teal",
"userrating" : 0,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-1.jpg/",
"episode" : 26,
"playcount" : 0,
"label" : "Season 1",
"art" : {
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-1.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-1.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-1.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-1.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f252308-g3.jpg/"
},
"watchedepisodes" : 0,
"seasonid" : 560
},
{
"season" : 2,
"showtitle" : "The Adventures of Abney & Teal",
"tvshowid" : 138,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
"seasonid" : 561,
"watchedepisodes" : 0,
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-2.jpg/",
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-2.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-2.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f252308-g3.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-2.jpg/"
},
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-2.jpg/",
"userrating" : 0,
"playcount" : 0,
"label" : "Season 2",
"episode" : 26
},
{
"episode" : 1,
"playcount" : 1,
"label" : "Season 3",
"userrating" : 0,
"thumbnail" : "",
"watchedepisodes" : 1,
"seasonid" : 24,
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f104171-1.jpg/",
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f104171-g.jpg/"
},
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
"season" : 3,
"showtitle" : "Air Ways",
"tvshowid" : 5
},
{
"art" : {
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f259064-g.jpg/",
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f259064-1.jpg/",
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/"
},
"watchedepisodes" : 0,
"seasonid" : 27,
"thumbnail" : "",
"userrating" : 0,
"episode" : 10,
"playcount" : 0,
"label" : "Season 1",
"tvshowid" : 6,
"showtitle" : "American Colony Meet the Hutterites",
"season" : 1,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/"
},
{
"tvshowid" : 7,
"showtitle" : "Amish: Out Of Order",
"season" : 1,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/",
"art" : {
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/"
},
"seasonid" : 30,
"watchedepisodes" : 0,
"userrating" : 0,
"thumbnail" : "",
"episode" : 9,
"playcount" : 0,
"label" : "Season 1"
}
]
}
}

View File

@ -0,0 +1,691 @@
{
"id" : "libTVShows",
"jsonrpc" : "2.0",
"result" : {
"tvshows" : [
{
"tvshowid" : 2,
"sorttitle" : "",
"season" : 1,
"lastplayed" : "2017-02-06 16:56:12",
"runtime" : 3600,
"uniqueid" : {
"unknown" : "260473"
},
"imdbnumber" : "260473",
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f260473-1.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f260473-g.jpg/"
},
"originaltitle" : "",
"year" : 2012,
"title" : "3",
"mpaa" : "",
"userrating" : 0,
"studio" : [
"CBS"
],
"rating" : 10,
"fanart" : "",
"tag" : [],
"premiered" : "2012-07-26",
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/3 (2012)/",
"cast" : [],
"genre" : [
"Reality"
],
"episodeguide" : "<episodeguide><url cache=\"260473-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/260473/all/en.zip</url></episodeguide>",
"watchedepisodes" : 3,
"dateadded" : "2016-08-26 09:16:59",
"label" : "3",
"ratings" : {
"default" : {
"votes" : 0,
"rating" : 10,
"default" : true
}
},
"episode" : 8,
"plot" : "Instead of competing against each other, the women searching for love in this relationship series are there to share the experience with one another, offering emotional support during the dating and decision-making process as they whittle down the group of nearly 100 men they start out with and each tries to find a good match. The women -- 29-year-old entrepreneur April Francis, 34-year-old pharmaceutical sales rep Rachel Harley and 24-year-old model Libby Lopez -- bring different backgrounds and experiences to the table, but their common goal unites them as they embark on their journey. Alex Miranda hosts.",
"playcount" : 0,
"votes" : "0",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f260473-1.jpg/"
},
{
"cast" : [],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/4-stjerners middag (2010)/",
"premiered" : "2010-01-25",
"watchedepisodes" : 2,
"episodeguide" : "<episodeguide><url cache=\"146391-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/146391/all/en.zip</url></episodeguide>",
"genre" : [
"Reality"
],
"dateadded" : "2016-08-26 09:16:59",
"votes" : "1",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
"ratings" : {
"default" : {
"votes" : 1,
"default" : true,
"rating" : 10
}
},
"episode" : 110,
"playcount" : 0,
"label" : "4 Stjerners Middag",
"plot" : "Danish version of the British \"Come Dine With Me\". Every week four celebrities invites each other home for dinner, one by one. The goal is to make a perfect evening for the three guests, and collect as many points as possible, to be the host of the week. The host picks out the three course dinner and is responsible for buying groceries and preparing the meal.",
"studio" : [
"TVNorge"
],
"rating" : 10,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
"tag" : [],
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/"
},
"imdbnumber" : "146391",
"title" : "4 Stjerners Middag",
"year" : 2010,
"originaltitle" : "",
"userrating" : 0,
"mpaa" : "",
"tvshowid" : 3,
"season" : 3,
"sorttitle" : "",
"lastplayed" : "2017-02-06 17:02:53",
"uniqueid" : {
"unknown" : "146391"
},
"runtime" : 3600
},
{
"title" : "11.22.63",
"year" : 2016,
"originaltitle" : "",
"mpaa" : "TV-MA",
"userrating" : 0,
"imdbnumber" : "301824",
"art" : {
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f301824-8.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f301824-g.jpg/"
},
"lastplayed" : "2017-02-28 11:14:40",
"runtime" : 3000,
"uniqueid" : {
"unknown" : "301824"
},
"tvshowid" : 1,
"sorttitle" : "",
"season" : 1,
"dateadded" : "2016-08-26 09:17:01",
"ratings" : {
"default" : {
"votes" : 31,
"rating" : 7.69999980926514,
"default" : true
}
},
"episode" : 8,
"label" : "11.22.63",
"playcount" : 1,
"plot" : "A teacher discovers a time portal that leads to October 21st, 1960 and goes on a quest to try and prevent the assassination of John F. Kennedy, which is complicated by the presence of Lee Harvey Oswald and the fact that he's falling in love with the past itself.",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f301824-8.jpg/",
"votes" : "31",
"premiered" : "2016-02-15",
"cast" : [
{
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fromance%2f.actors%2fJames_Franco.jpg/",
"order" : 0,
"name" : "James Franco",
"role" : "Jake Epping"
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358603.jpg/",
"order" : 1,
"role" : "Sadie Dunhill",
"name" : "Sarah Gadon"
},
{
"order" : 2,
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fdrama%2f.actors%2fChris_Cooper.jpg/",
"role" : "Al Templeton",
"name" : "Chris Cooper"
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f380762.jpg/",
"order" : 3,
"role" : "Harry Dunning",
"name" : "Leon Rippy"
},
{
"name" : "Kevin J. O'Connor",
"role" : "Yellow Card Man",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f370399.jpg/",
"order" : 4
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358610.jpg/",
"order" : 5,
"role" : "Bill Turcotte",
"name" : "George MacKay"
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358609.jpg/",
"order" : 6,
"role" : "Lee Harvey Oswald",
"name" : "Daniel Webber"
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358608.jpg/",
"order" : 7,
"role" : "Johnny Clayton",
"name" : "T.R. Knight"
},
{
"order" : 8,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358607.jpg/",
"role" : "Marquerite Oswald",
"name" : "Cherry Jones"
},
{
"order" : 9,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358606.jpg/",
"name" : "Lucy Fry",
"role" : "Marina Oswald"
},
{
"role" : "Frank Dunning",
"name" : "Josh Duhamel",
"order" : 10,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358602.jpg/"
},
{
"order" : 22,
"role" : "George de Mohrenschildt",
"name" : "Johny Coyne"
},
{
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fdrama%2f.actors%2fNick_Searcy.jpg/",
"order" : 23,
"role" : "Deke Simmons",
"name" : "Nick Searcy"
}
],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/11_22_63 (2016)/",
"genre" : [
"Drama",
"Mini-Series",
"Science-Fiction"
],
"episodeguide" : "<episodeguide><url cache=\"301824-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/301824/all/en.zip</url></episodeguide>",
"watchedepisodes" : 8,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/",
"tag" : [],
"studio" : [
"Hulu"
],
"rating" : 7.69999980926514
},
{
"tvshowid" : 137,
"sorttitle" : "",
"season" : 5,
"lastplayed" : "",
"runtime" : 2700,
"uniqueid" : {
"unknown" : "77904"
},
"imdbnumber" : "77904",
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/"
},
"year" : 1983,
"originaltitle" : "",
"title" : "The A-Team",
"mpaa" : "TV-PG",
"userrating" : 0,
"studio" : [
"NBC"
],
"rating" : 7.80000019073486,
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
"tag" : [],
"premiered" : "1983-01-23",
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/The A-Team (1983)/",
"cast" : [
{
"name" : "George Peppard",
"role" : "Col. John \"Hannibal\" Smith",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f82360.jpg/",
"order" : 0
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59826.jpg/",
"order" : 1,
"name" : "Dirk Benedict",
"role" : "Lt. Templeton \"Faceman\" Peck"
},
{
"name" : "Mr. T",
"role" : "Sgt. Bosco Albert \"B.A.\" Baracus",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59825.jpg/",
"order" : 2
},
{
"order" : 3,
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fscifi%2f.actors%2fDwight_Schultz.jpg/",
"role" : "Capt. H.M. \"Howling Mad\" Murdock",
"name" : "Dwight Schultz"
},
{
"order" : 4,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59831.jpg/",
"role" : "Tawnia Baker",
"name" : "Marla Heasley"
},
{
"name" : "Melinda Culea",
"role" : "Amy Amanda 'Triple A' Allen",
"order" : 5,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59827.jpg/"
},
{
"name" : "William Lucking",
"role" : "Col. Lynch",
"order" : 6,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59836.jpg/"
},
{
"role" : "Col. Roderick Decker",
"name" : "Lance LeGault",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59828.jpg/",
"order" : 7
},
{
"name" : "Eddie Velez",
"role" : "Frankie \"Dishpan Man\" Santana",
"order" : 8,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59838.jpg/"
},
{
"role" : "Col. Briggs",
"name" : "Charles Napier",
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fcomedy%2f.actors%2fCharles_Napier.jpg/",
"order" : 9
},
{
"role" : "Capt. Crane",
"name" : "Carl Franklin",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59830.jpg/",
"order" : 10
},
{
"role" : "Gen. Hunt Stockwell",
"name" : "Robert Vaughn",
"order" : 19
}
],
"genre" : [
"Action",
"Adventure"
],
"watchedepisodes" : 0,
"episodeguide" : "<episodeguide><url cache=\"77904-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/77904/all/en.zip</url></episodeguide>",
"dateadded" : "2016-08-26 09:16:58",
"episode" : 97,
"ratings" : {
"default" : {
"votes" : 45,
"rating" : 7.80000019073486,
"default" : true
}
},
"label" : "The A-Team",
"playcount" : 0,
"plot" : "The A-Team is about a group of ex-United States Army Special Forces personnel who work as soldiers of fortune, while on the run from the Army after being branded as war criminals for a crime they didn't commit.",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
"votes" : "45"
},
{
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/"
},
"imdbnumber" : "75926",
"userrating" : 0,
"mpaa" : "TV-PG",
"year" : 2001,
"title" : "According to Jim",
"originaltitle" : "",
"sorttitle" : "",
"season" : 8,
"tvshowid" : 4,
"uniqueid" : {
"unknown" : "75926"
},
"runtime" : 1800,
"lastplayed" : "2017-02-28 12:18:10",
"watchedepisodes" : 9,
"episodeguide" : "<episodeguide><url cache=\"75926-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/75926/all/en.zip</url></episodeguide>",
"genre" : [
"Comedy"
],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/According to Jim (2001)/",
"cast" : [
{
"name" : "James Belushi",
"role" : "Jim",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41995.jpg/",
"order" : 0
},
{
"order" : 1,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41994.jpg/",
"role" : "Cheryl",
"name" : "Courtney Thorne-Smith"
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41992.jpg/",
"order" : 2,
"name" : "Kimberly Williams-Paisley",
"role" : "Dana"
},
{
"role" : "Andy",
"name" : "Larry Joe Campbell",
"order" : 3,
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41993.jpg/"
},
{
"role" : "Ruby",
"name" : "Taylor Atelian",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41991.jpg/",
"order" : 4
},
{
"name" : "Conner Rayburn",
"role" : "Kyle",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41990.jpg/",
"order" : 5
},
{
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41989.jpg/",
"order" : 6,
"role" : "Gracie",
"name" : "Billi Bruno"
}
],
"premiered" : "2001-10-03",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
"votes" : "34",
"label" : "According to Jim",
"ratings" : {
"default" : {
"votes" : 34,
"default" : true,
"rating" : 8
}
},
"playcount" : 0,
"episode" : 182,
"plot" : "Jim is an abrasive but lovable suburban father. Much like his real life counterpart, Jim's character is noted as a fan of Blues music, as well as the Chicago Blackhawks, Chicago Bulls, Chicago Bears, and the Chicago Cubs. He's married to a gorgeous woman, Cheryl, and raises his five children Ruby, Gracie, Kyle, and twins, Gordan and Jonathan in a big house. Everything is perfect for Jim, if it wasn't for the messy situations he gets himself into and his laziness, which often makes him search for alternative ways of doing things with less effort. Of course, having his wife's siblings hanging out at his house all the time is no help. While Andy might be one of his best friends, Dana often teams up with Cheryl against Jim.",
"dateadded" : "2016-08-26 09:16:59",
"rating" : 8,
"studio" : [
"ABC (US)"
],
"tag" : [],
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
},
{
"watchedepisodes" : 0,
"episodeguide" : "<episodeguide><url cache=\"252308-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/252308/all/en.zip</url></episodeguide>",
"genre" : [
"Animation",
"Children"
],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/The Adventures of Abney & Teal (2011)/",
"cast" : [],
"premiered" : "2011-09-26",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
"votes" : "1",
"episode" : 52,
"ratings" : {
"default" : {
"votes" : 1,
"rating" : 9,
"default" : true
}
},
"plot" : "Animated adventures of two friends who live on an island in the middle of a lake, in the middle of a park, in the middle of the big city.",
"playcount" : 0,
"label" : "The Adventures of Abney & Teal",
"dateadded" : "2016-08-26 09:16:57",
"rating" : 9,
"studio" : [
"CBeebies"
],
"tag" : [],
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f252308-g3.jpg/"
},
"imdbnumber" : "252308",
"userrating" : 0,
"mpaa" : "TV-Y",
"year" : 2011,
"title" : "The Adventures of Abney & Teal",
"originaltitle" : "",
"season" : 2,
"sorttitle" : "",
"tvshowid" : 138,
"uniqueid" : {
"unknown" : "252308"
},
"runtime" : 660,
"lastplayed" : ""
},
{
"tag" : [],
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
"rating" : 3,
"studio" : [
"Seven Network"
],
"episode" : 1,
"ratings" : {
"default" : {
"votes" : 1,
"default" : true,
"rating" : 3
}
},
"label" : "Air Ways",
"plot" : "Follow the ups and downs of travel on Tiger Airways as viewers get an unprecedented look into the day-to-day running of a budget airline in Australia. A cancelled flight causes chaos, staff witness an unexpected proposal, a baggage problem riles a mum's temper, and a sleep-deprived teenager awakens to a rude shock.\r\nNarrated by Corinne Grant",
"playcount" : 1,
"votes" : "1",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f104171-1.jpg/",
"dateadded" : "2016-08-26 09:16:57",
"genre" : [
"Reality"
],
"episodeguide" : "<episodeguide><url cache=\"104171-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/104171/all/en.zip</url></episodeguide>",
"watchedepisodes" : 1,
"premiered" : "2009-07-21",
"cast" : [],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/Airways (2009)/",
"runtime" : 1800,
"uniqueid" : {
"unknown" : "104171"
},
"lastplayed" : "2017-02-06 16:41:56",
"sorttitle" : "",
"season" : 1,
"tvshowid" : 5,
"mpaa" : "TV-PG",
"userrating" : 0,
"originaltitle" : "",
"year" : 2009,
"title" : "Air Ways",
"imdbnumber" : "104171",
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f104171-1.jpg/",
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f104171-g.jpg/"
}
},
{
"sorttitle" : "",
"season" : 0,
"tvshowid" : 127,
"runtime" : 0,
"uniqueid" : {
"unknown" : "278782"
},
"lastplayed" : "",
"imdbnumber" : "278782",
"art" : {
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f278782-1.jpg/"
},
"mpaa" : "",
"userrating" : 0,
"year" : 1969,
"originaltitle" : "",
"title" : "Al Jazeera Special Series",
"rating" : 10,
"studio" : [],
"tag" : [],
"fanart" : "",
"genre" : [],
"episodeguide" : "<episodeguide><url cache=\"278782-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/278782/all/en.zip</url></episodeguide>",
"watchedepisodes" : 0,
"premiered" : "1969-12-31",
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/Special Series (2016)/",
"cast" : [],
"ratings" : {
"default" : {
"votes" : 1,
"rating" : 10,
"default" : true
}
},
"episode" : 0,
"label" : "Al Jazeera Special Series",
"plot" : "",
"playcount" : 1,
"votes" : "1",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f278782-1.jpg/",
"dateadded" : ""
},
{
"originaltitle" : "",
"year" : 2012,
"title" : "American Colony Meet the Hutterites",
"userrating" : 0,
"mpaa" : "",
"art" : {
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/",
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f259064-1.jpg/",
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f259064-g.jpg/"
},
"imdbnumber" : "259064",
"lastplayed" : "",
"uniqueid" : {
"unknown" : "259064"
},
"runtime" : 1800,
"tvshowid" : 6,
"sorttitle" : "",
"season" : 1,
"dateadded" : "2016-08-26 09:16:57",
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f259064-1.jpg/",
"votes" : "1",
"plot" : "Meet the Hutterites—a small religious colony in rural Montana who holds desperately to their sacred traditions while fighting the modern temptations of the outside world. King Colony is made up of 59 people and they are almost all related. This family lives together, works together, and worships God together, 7 days a week, 365 days a year, for their entire lives. And, like any family, this one doesnt always agree. Most of the colony is holding tight to the age-old traditions of their ancestors, while others are flirting with modern society. Some feel that bringing modern technology, education, and ideas into the colony will only help it, while others fear that this modern way of thinking threatens their very existence. We follow the men, the women, the young, and the old, as they strive to live as proper Hutterites. Some will succeed, some will fail, and everyone will have a choice to make. This is the very first glimpse into the world of the Hutterites.",
"ratings" : {
"default" : {
"votes" : 1,
"default" : true,
"rating" : 1
}
},
"label" : "American Colony Meet the Hutterites",
"episode" : 10,
"playcount" : 0,
"cast" : [],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/American Colony: Meet the Hutterites (2012)/",
"premiered" : "2012-05-29",
"episodeguide" : "<episodeguide><url cache=\"259064-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/259064/all/en.zip</url></episodeguide>",
"watchedepisodes" : 0,
"genre" : [
"Documentary"
],
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/",
"tag" : [],
"studio" : [
"National Geographic"
],
"rating" : 1
},
{
"runtime" : 3600,
"uniqueid" : {
"unknown" : "258525"
},
"lastplayed" : "",
"season" : 1,
"sorttitle" : "",
"tvshowid" : 7,
"mpaa" : "TV-PG",
"userrating" : 0,
"year" : 2012,
"originaltitle" : "",
"title" : "Amish: Out Of Order",
"imdbnumber" : "258525",
"art" : {
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/"
},
"tag" : [],
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/",
"rating" : 8.5,
"studio" : [
"National Geographic"
],
"label" : "Amish: Out Of Order",
"ratings" : {
"default" : {
"rating" : 8.5,
"default" : true,
"votes" : 0
}
},
"plot" : "It takes a lot to leave the only life youve ever known—for one youve been told will lead you straight to hell. And with little possibility of normal contact with your family ever again, turning your back on the Amish order is an immense undertaking, and a choice thats not made without tremendous consideration. In the new ten-part series Amish: Out of Order, follow the trials and tribulations of individuals who have made the decision to leave the Amish community behind. Due to their religious beliefs, most Amish refuse to be photographed or videotaped—even ex-Amish risk permanent shunning by their family and community for appearing on camera. The ex-Amish in this program accept that risk.",
"playcount" : 0,
"episode" : 9,
"thumbnail" : "",
"votes" : "0",
"dateadded" : "2016-08-26 09:16:59",
"genre" : [
"Documentary",
"Reality"
],
"watchedepisodes" : 0,
"episodeguide" : "<episodeguide><url cache=\"258525-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/258525/all/en.zip</url></episodeguide>",
"premiered" : "2012-04-24",
"cast" : [],
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/Amish: Out of Order (2012)/"
}
],
"limits" : {
"start" : 0,
"total" : 177,
"end" : 10
}
}
}

View File

@ -24,6 +24,7 @@ import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiException;
import org.xbmc.kore.jsonrpc.ApiList;
import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.jsonrpc.method.AudioLibrary;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.AudioType;
@ -31,6 +32,8 @@ import org.xbmc.kore.jsonrpc.type.LibraryType;
import org.xbmc.kore.jsonrpc.type.VideoType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.SyncMusic;
import org.xbmc.kore.service.library.SyncMusicVideos;
import org.xbmc.kore.service.library.SyncTVShows;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.utils.LogUtils;
@ -40,17 +43,20 @@ import java.util.ArrayList;
public class Database {
public static final String TAG = LogUtils.makeLogTag(Database.class);
public static HostInfo fill(Context context, ContentResolver contentResolver) throws ApiException, IOException {
HostInfo hostInfo = addHost(context);
public static HostInfo fill(HostInfo hostInfo, Context context, ContentResolver contentResolver) throws ApiException, IOException {
SyncMusic syncMusic = new SyncMusic(hostInfo.getId(), null);
insertMovies(context, contentResolver, hostInfo.getId());
insertArtists(context, contentResolver, syncMusic);
insertGenres(context, contentResolver, syncMusic);
insertAlbums(context, contentResolver, syncMusic);
insertSongs(context, contentResolver, syncMusic);
SyncTVShows syncTVShows = new SyncTVShows(hostInfo.getId(), null);
insertTVShows(context, contentResolver, syncTVShows);
SyncMusicVideos syncMusicVideos = new SyncMusicVideos(hostInfo.getId(), null);
insertMusicVideos(context, contentResolver, syncMusicVideos);
return hostInfo;
}
@ -58,15 +64,24 @@ public class Database {
contentResolver.delete(MediaContract.Hosts.buildHostUri(hostInfo.getId()), null, null);
}
private static HostInfo addHost(Context context) {
return HostManager.getInstance(context).addHost("TestHost", "127.0.0.1", 1, 80, 9090, null,
null, "52:54:00:12:35:02", 9, false, 9777,
HostInfo.DEFAULT_KODI_VERSION_MAJOR, HostInfo.DEFAULT_KODI_VERSION_MINOR,
HostInfo.DEFAULT_KODI_VERSION_REVISION, HostInfo.DEFAULT_KODI_VERSION_TAG,
public static HostInfo addHost(Context context) {
return addHost(context, "127.0.0.1", HostConnection.PROTOCOL_TCP,
HostInfo.DEFAULT_HTTP_PORT, HostInfo.DEFAULT_TCP_PORT);
}
public static HostInfo addHost(Context context, String hostname, int protocol, int httpPort, int tcpPort) {
return HostManager.getInstance(context).addHost("TestHost", hostname, protocol, httpPort,
tcpPort, null, null, "52:54:00:12:35:02", 9,
false, HostInfo.DEFAULT_EVENT_SERVER_PORT,
HostInfo.DEFAULT_KODI_VERSION_MAJOR,
HostInfo.DEFAULT_KODI_VERSION_MINOR,
HostInfo.DEFAULT_KODI_VERSION_REVISION,
HostInfo.DEFAULT_KODI_VERSION_TAG,
false);
}
public static void insertMovies(Context context, ContentResolver contentResolver, int hostId)
private static void insertMovies(Context context, ContentResolver contentResolver, int hostId)
throws ApiException, IOException {
VideoLibrary.GetMovies getMovies = new VideoLibrary.GetMovies();
String result = FileUtils.readFile(context, "Video.Details.Movie.json");
@ -131,4 +146,34 @@ public class Database {
syncMusic.insertSongsItems(songList, contentResolver);
}
private static void insertTVShows(Context context, ContentResolver contentResolver, SyncTVShows syncTVShows)
throws ApiException, IOException {
VideoLibrary.GetTVShows getTVShows = new VideoLibrary.GetTVShows();
String result = FileUtils.readFile(context, "VideoLibrary.GetTVShows.json");
ArrayList<VideoType.DetailsTVShow> tvShowList = (ArrayList) getTVShows.resultFromJson(result).items;
syncTVShows.insertTVShows(tvShowList, contentResolver);
for ( VideoType.DetailsTVShow tvShow : tvShowList ) {
VideoLibrary.GetSeasons getSeasons = new VideoLibrary.GetSeasons(tvShow.tvshowid);
result = FileUtils.readFile(context, "VideoLibrary.GetSeasons.json");
ArrayList<VideoType.DetailsSeason> detailsSeasons = (ArrayList) getSeasons.resultFromJson(result);
syncTVShows.insertSeason(tvShow.tvshowid, detailsSeasons, contentResolver);
}
VideoLibrary.GetEpisodes getEpisodes = new VideoLibrary.GetEpisodes(0);
result = FileUtils.readFile(context, "VideoLibrary.GetEpisodes.json");
ArrayList<VideoType.DetailsEpisode> detailsEpisodes = (ArrayList) getEpisodes.resultFromJson(result);
syncTVShows.insertEpisodes(detailsEpisodes, contentResolver);
}
private static void insertMusicVideos(Context context, ContentResolver contentResolver, SyncMusicVideos syncMusicVideos)
throws ApiException, IOException {
VideoLibrary.GetMusicVideos getMusicVideos = new VideoLibrary.GetMusicVideos();
String result = FileUtils.readFile(context, "VideoLibrary.GetMusicVideos.json");
ArrayList<VideoType.DetailsMusicVideo> musicVideoList = (ArrayList) getMusicVideos.resultFromJson(result);
syncMusicVideos.insertMusicVideos(musicVideoList, contentResolver);
}
}

View File

@ -48,7 +48,6 @@ public class MockTcpServer {
private boolean started;
private ExecutorService executor;
private int port = -1;
private StringBuffer request;
private InetSocketAddress inetSocketAddress;
private final Set<Socket> openClientSockets =
@ -56,6 +55,10 @@ public class MockTcpServer {
private final TcpServerConnectionHandler connectionHandler;
// TODO
// Enhance handler to handle multiple connections simultaneously. It can now handle one
// connection at a time, which makes the current setup of the MockTcpServer (with threading)
// overkill.
public interface TcpServerConnectionHandler {
/**
* Processes received input
@ -131,7 +134,6 @@ public class MockTcpServer {
//Socket closed
return;
}
openClientSockets.add(socket);
serveConnection(socket);
}
@ -185,15 +187,10 @@ public class MockTcpServer {
private void handleInput() throws IOException {
InputStreamReader in = new InputStreamReader(socket.getInputStream());
request = new StringBuffer();
int i;
while ((i = in.read()) != -1) {
request.append((char) i);
synchronized (connectionHandler) {
connectionHandler.processInput((char) i);
}
}
socket.close();
openClientSockets.remove(socket);

View File

@ -0,0 +1,80 @@
/*
* 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.testutils.tcpserver.handlers;
import android.content.Context;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.testutils.FileUtils;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Addons;
import org.xbmc.kore.utils.LogUtils;
import java.io.IOException;
import java.util.ArrayList;
/**
* Simulates Addons JSON-RPC API
*/
public class AddonsHandler implements JSONConnectionHandlerManager.ConnectionHandler {
private static final String TAG = LogUtils.makeLogTag(AddonsHandler.class);
private static final String ID_NODE = "id";
private Context context;
public AddonsHandler(Context context) {
this.context = context;
}
@Override
public ArrayList<JsonResponse> getNotification() {
return null;
}
@Override
public void reset() {
}
@Override
public String[] getType() {
return new String[]{Addons.GetAddons.METHOD_NAME};
}
@Override
public ArrayList<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
int methodId = jsonRequest.get(ID_NODE).asInt(-1);
switch (method) {
case Addons.GetAddons.METHOD_NAME:
try {
String result = FileUtils.readFile(context, "Addons.GetAddons.json");
Addons.GetAddons getAddons = new Addons.GetAddons(methodId, result);
jsonResponses.add(getAddons);
} catch (IOException e) {
LogUtils.LOGW(TAG, "Error creating GetAddons response: " + e.getMessage());
}
break;
default:
LogUtils.LOGD(TAG, "method: " + method + ", not implemented");
}
return jsonResponses;
}
}

View File

@ -23,6 +23,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.utils.LogUtils;
import java.io.IOException;
public abstract class JsonResponse {
private final String TAG = LogUtils.makeLogTag(JsonResponse.class);
@ -52,6 +54,12 @@ public abstract class JsonResponse {
jsonResponse.put(ID_NODE, id);
}
public JsonResponse(int id, String jsonString) throws IOException {
jsonResponse = (ObjectNode) objectMapper.readTree(jsonString);
jsonResponse.put(JSONRPC_NODE, "2.0");
jsonResponse.put(ID_NODE, id);
}
protected ObjectNode createObjectNode() {
return objectMapper.createObjectNode();
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2017 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.testutils.tcpserver.handlers.jsonrpc.response.methods;
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
import java.io.IOException;
/**
* Serverside JSON RPC responses in Addons.*
*/
public class Addons {
/**
* JSON response for Addons.GetAddons request
*
* @return JSON string
*/
public static class GetAddons extends JsonResponse {
public final static String METHOD_NAME = "Addons.GetAddons";
public GetAddons(int id, String jsonString) throws IOException {
super(id, jsonString);
}
}
}

View File

@ -0,0 +1,48 @@
@startuml
Actor Activity
participant "BaseMediaActivity" as A
participant "MediaActivity" as B
Activity -> A: onCreate
activate A
A -> A: set theme
A -> A: setup navigation drawer
group setup action bar
alt new activity
A -> B: getActionBarTitle()
activate B
B -> A:
deactivate B
note over A: set home icon to hamburger icon
else restore action bar
note over A: get home icon and action bar title\nfrom saved instance
end
A -> A: updateActionBar
alt no fragment in layout (new activity)
A -> B: createFragment()
activate B
B -> A:
deactivate B
A -> A: add fragment
end
A -> Activity
deactivate A
...
Activity -> A: onCreateOptionsMenu
activate A
note over A: inflate generic menu items
A -> Activity:
deactivate A
...
note over B: user selects media item
B -> A: showFragment()
activate B
activate A
note over A: sets up the shared element transition\nand updates the dataholder
A -> A: replace current fragment\nwith new fragment
A -> B
deactivate A
deactivate B
@enduml

53
tools/json/getaddons.pl Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/perl
#
# Copyright 2017 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.
#
use strict;
use warnings;
use Types::Serialiser;
use JsonTools qw(sendJsonRequest writeJsonFile);
sub getAddons() {
my $jsonrequest = {
"jsonrpc" => "2.0",
"method" => "Addons.GetAddons",
"params" => {
"properties" => [
"name",
"version",
"summary",
"description",
"path",
"author",
"thumbnail",
"disclaimer",
"fanart",
"dependencies",
"broken",
"extrainfo",
"rating",
"enabled",
"installed"
],
},
"id" => "libAddons"
};
return sendJsonRequest("http://127.0.0.1:8080/jsonrpc", $jsonrequest);
}
writeJsonFile("Addons.GetAddons.json", getAddons);

63
tools/json/getmusicvideos.pl Executable file
View File

@ -0,0 +1,63 @@
#!/usr/bin/perl
#
# Copyright 2017 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.
#
use strict;
use warnings;
use Types::Serialiser;
use JsonTools qw(sendJsonRequest writeJsonFile);
sub getMusicVideos() {
my $jsonrequest = {
"jsonrpc" => "2.0",
"method" => "VideoLibrary.GetMusicVideos",
"params" => {
"limits" => { "start" => 0, "end" => 10 },
"properties" => [
"title",
"playcount",
"runtime",
"director",
"studio",
"year",
"plot",
"album",
"artist",
"genre",
"track",
"streamdetails",
"lastplayed",
"fanart",
"thumbnail",
"file",
"resume",
"dateadded",
"tag",
"art",
"rating",
"userrating",
"premiered"
],
"sort" => { "order" => "ascending", "method" => "label", "ignorearticle" => Types::Serialiser::true }
},
"id" => "libMovies"
};
return sendJsonRequest("http://127.0.0.1:8080/jsonrpc", $jsonrequest);
}
writeJsonFile("VideoLibrary.GetMusicVideos.json", getMusicVideos);

181
tools/json/gettvshows.pl Executable file
View File

@ -0,0 +1,181 @@
#!/usr/bin/perl
#
# Copyright 2017 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.
#
use strict;
use warnings;
use Types::Serialiser;
use JsonTools qw(sendJsonRequest writeJsonFile);
sub getTVShows() {
my $jsonrequest = {
"jsonrpc" => "2.0",
"method" => "VideoLibrary.GetTVShows",
"params" => {
"limits" => { "start" => 0, "end" => 10 },
"properties" => [
"title",
"genre",
"year",
"rating",
"plot",
"studio",
"mpaa",
"cast",
"playcount",
"episode",
"imdbnumber",
"premiered",
"votes",
"lastplayed",
"fanart",
"thumbnail",
"file",
"originaltitle",
"sorttitle",
"episodeguide",
"season",
"watchedepisodes",
"dateadded",
"tag",
"art",
"userrating",
"ratings",
"runtime",
"uniqueid"
],
"sort" => { "order" => "ascending", "method" => "label", "ignorearticle" => Types::Serialiser::true }
},
"id" => "libTVShows"
};
return sendJsonRequest("http://127.0.0.1:8080/jsonrpc", $jsonrequest);
}
sub getSeasons($) {
my $tvshowid = shift;
my $jsonrequest = {
"jsonrpc" => "2.0",
"method" => "VideoLibrary.GetSeasons",
"params" => {
"tvshowid" => $tvshowid,
"properties" => [
"season",
"showtitle",
"playcount",
"episode",
"fanart",
"thumbnail",
"tvshowid",
"watchedepisodes",
"art",
"userrating"
],
"sort" => { "order" => "ascending", "method" => "label", "ignorearticle" => Types::Serialiser::true }
},
"id" => "libTVShowSeasons"
};
return sendJsonRequest("http://127.0.0.1:8080/jsonrpc", $jsonrequest);
}
sub getEpisodes($) {
my $tvshowid = shift;
my $jsonrequest = {
"jsonrpc" => "2.0",
"method" => "VideoLibrary.GetEpisodes",
"params" => {
"tvshowid" => $tvshowid,
"properties" => [
"title",
"plot",
"votes",
"rating",
"writer",
"firstaired",
"playcount",
"runtime",
"director",
"productioncode",
"season",
"episode",
"originaltitle",
"showtitle",
"cast",
"streamdetails",
"lastplayed",
"fanart",
"thumbnail",
"file",
"resume",
"tvshowid",
"dateadded",
"uniqueid",
"art",
"specialsortseason",
"specialsortepisode",
"userrating",
"seasonid",
"ratings"
],
"sort" => { "order" => "ascending", "method" => "label", "ignorearticle" => Types::Serialiser::true }
},
"id" => "libTVShowEpisodes"
};
return sendJsonRequest("http://127.0.0.1:8080/jsonrpc", $jsonrequest);
}
my $tvshows_list = getTVShows();
my $json_seasons;
my $json_episodes;
for my $tvshow (@{$tvshows_list->{"result"}->{"tvshows"}}) {
my $seasons_list = getSeasons($tvshow->{"tvshowid"});
if (! defined $json_seasons) {
$json_seasons = $seasons_list;
} else {
for my $season ( @{$seasons_list->{"result"}->{"seasons"}} ) {
push $json_seasons->{"result"}->{"seasons"}, $season;
}
}
my $episodes_list = getEpisodes($tvshow->{"tvshowid"});
if (! defined $json_episodes) {
$json_episodes = $episodes_list;
} else {
for my $episode ( @{$episodes_list->{"result"}->{"episodes"}} ) {
push $json_episodes->{"result"}->{"episodes"}, $episode;
}
}
}
writeJsonFile("VideoLibrary.GetTVShows.json", $tvshows_list);
my $count = length($json_seasons->{"result"}->{"seasons"});
$json_seasons->{"result"}{"limits"} = {"end" => $count, "start" => 0, "total" => $count};
writeJsonFile("VideoLibrary.GetSeasons.json", $json_seasons);
$count = length($json_episodes->{"result"}->{"episodes"});
$json_seasons->{"result"}{"limits"} = {"end" => $count, "start" => 0, "total" => $count};
writeJsonFile("VideoLibrary.GetEpisodes.json", $json_episodes);