diff --git a/dashboard-ui/bower_components/emby-apiclient/.bower.json b/dashboard-ui/bower_components/emby-apiclient/.bower.json
index 1a3df41565..da5da08783 100644
--- a/dashboard-ui/bower_components/emby-apiclient/.bower.json
+++ b/dashboard-ui/bower_components/emby-apiclient/.bower.json
@@ -16,12 +16,12 @@
},
"devDependencies": {},
"ignore": [],
- "version": "1.1.107",
- "_release": "1.1.107",
+ "version": "1.1.125",
+ "_release": "1.1.125",
"_resolution": {
"type": "version",
- "tag": "1.1.107",
- "commit": "82a9be9ffc4359043cbbae83031491dc4d2182cc"
+ "tag": "1.1.125",
+ "commit": "de914c8db94b994ea45746626ba73bfb72a594a0"
},
"_source": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript.git",
"_target": "^1.1.51",
diff --git a/dashboard-ui/bower_components/emby-apiclient/apiclient.js b/dashboard-ui/bower_components/emby-apiclient/apiclient.js
index 95980106ca..761129730e 100644
--- a/dashboard-ui/bower_components/emby-apiclient/apiclient.js
+++ b/dashboard-ui/bower_components/emby-apiclient/apiclient.js
@@ -589,7 +589,8 @@
var url = self.getUrl("socket");
url = replaceAll(url, 'emby/socket', 'embywebsocket');
- url = replaceAll(url, 'http', 'ws');
+ url = replaceAll(url, 'https:', 'wss:');
+ url = replaceAll(url, 'http:', 'ws:');
url += "?api_key=" + accessToken;
url += "&deviceId=" + deviceId;
@@ -1407,6 +1408,26 @@
return self.getJSON(url);
};
+ self.getPlaybackInfo = function(itemId, options, deviceProfile) {
+
+ var postData = {
+ DeviceProfile: deviceProfile
+ };
+
+ return self.ajax({
+ url: self.getUrl('Items/' + itemId + '/PlaybackInfo', options),
+ type: 'POST',
+ data: JSON.stringify(postData),
+ contentType: "application/json",
+ dataType: "json"
+ });
+ };
+
+ self.getIntros = function(itemId) {
+
+ return self.getJSON(self.getUrl('Users/' + self.getCurrentUserId() + '/Items/' + itemId + '/Intros'));
+ };
+
/**
* Gets the directory contents of a path on the server
*/
@@ -2122,6 +2143,19 @@
return self.getJSON(url);
};
+ self.getItemDownloadUrl = function (itemId) {
+
+ if (!itemId) {
+ throw new Error("itemId cannot be empty");
+ }
+
+ var url = "Items/" + itemId + "/Download";
+
+ return self.getUrl(url, {
+ api_key: self.accessToken()
+ });
+ };
+
self.getSessions = function (options) {
var url = self.getUrl("Sessions", options);
@@ -2617,9 +2651,10 @@
var url = self.getUrl("Users/authenticatebyname");
- require(["cryptojs-sha1"], function () {
+ require(["cryptojs-sha1", "cryptojs-md5"], function () {
var postData = {
- password: CryptoJS.SHA1(password || "").toString(),
+ Password: CryptoJS.SHA1(password || "").toString(),
+ PasswordMd5: CryptoJS.MD5(password || "").toString(),
Username: name
};
diff --git a/dashboard-ui/bower_components/emby-apiclient/apiclientex.js b/dashboard-ui/bower_components/emby-apiclient/apiclientex.js
new file mode 100644
index 0000000000..026d32f89f
--- /dev/null
+++ b/dashboard-ui/bower_components/emby-apiclient/apiclientex.js
@@ -0,0 +1,373 @@
+define(['apiclientcore', 'localassetmanager', 'events'], function (apiclientcorefactory, localassetmanager, events) {
+ 'use strict';
+
+ var localPrefix = 'local:';
+ var localViewPrefix = 'localview:';
+
+ /**
+ * Creates a new api client instance
+ * @param {String} serverAddress
+ * @param {String} clientName s
+ * @param {String} applicationVersion
+ */
+ return function (serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio) {
+
+ var apiclientcore = new apiclientcorefactory(serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio);
+
+ var self = Object.assign(this, apiclientcore);
+
+ events.on(apiclientcore, 'websocketmessage', onWebSocketMessage);
+
+ Object.defineProperty(self, 'onAuthenticated', {
+ get: function () { return apiclientcore.onAuthenticated; },
+ set: function (newValue) { apiclientcore.onAuthenticated = newValue; }
+ });
+
+ Object.defineProperty(self, 'enableAutomaticBitrateDetection', {
+ get: function () { return apiclientcore.enableAutomaticBitrateDetection; },
+ set: function (newValue) { apiclientcore.enableAutomaticBitrateDetection = newValue; }
+ });
+
+ function getUserViews(userId) {
+
+ return apiclientcore.getUserViews(userId).then(function (result) {
+
+ var serverInfo = apiclientcore.serverInfo();
+
+ if (serverInfo) {
+
+ return getLocalView(serverInfo.Id, userId).then(function (localView) {
+
+ if (localView) {
+
+ result.Items.push(localView);
+ result.TotalRecordCount++;
+ }
+
+ return Promise.resolve(result);
+ });
+ }
+
+ return Promis.resolve(result);
+ });
+ }
+
+ function getLocalView(serverId, userId) {
+
+ return localassetmanager.getViews(serverId, userId).then(function (views) {
+
+ var localView = null;
+
+ if (views.length > 0) {
+
+ localView = {
+ Name: 'Offline Items',
+ ServerId: serverId,
+ Id: 'localview',
+ Type: 'localview'
+ };
+ }
+
+ return Promise.resolve(localView);
+ });
+ }
+
+ function getItems(userId, options) {
+
+ var serverInfo = apiclientcore.serverInfo();
+ var i;
+
+ if (serverInfo && options.ParentId === 'localview') {
+
+ return localassetmanager.getViews(serverInfo.Id, userId).then(function (items) {
+ var result = {
+ Items: items,
+ TotalRecordCount: items.length
+ };
+
+ return Promise.resolve(result);
+ });
+
+ } else if (serverInfo && options && (startsWith(options.ParentId, localViewPrefix) || startsWith(options.ParentId, localPrefix))) {
+
+ return localassetmanager.getViewItems(serverInfo.Id, userId, options.ParentId).then(function (items) {
+
+ items.forEach(function (item) {
+ item.Id = localPrefix + item.Id;
+ });
+
+ items.sort(function (a, b) { return a.SortName.toLowerCase().localeCompare(b.SortName.toLowerCase()); });
+
+ var result = {
+ Items: items,
+ TotalRecordCount: items.length
+ };
+
+ return Promise.resolve(result);
+ });
+ } else if (options && options.ExcludeItemIds && options.ExcludeItemIds.length) {
+
+ var exItems = options.ExcludeItemIds.split(',');
+
+ for (i = 0; i < exItems.length; i++) {
+ if (startsWith(exItems[i], localPrefix)) {
+ return Promise.resolve(createEmptyList());
+ }
+ }
+ } else if (options && options.Ids && options.Ids.length) {
+
+ var ids = options.Ids.split(',');
+ var hasLocal = false;
+
+ for (i = 0; i < ids.length; i++) {
+ if (startsWith(ids[i], localPrefix)) {
+ hasLocal = true;
+ }
+ }
+
+ if (hasLocal) {
+ return localassetmanager.getItemsFromIds(serverInfo.Id, ids).then(function (items) {
+
+ items.forEach(function (item) {
+ item.Id = localPrefix + item.Id;
+ });
+
+ var result = {
+ Items: items,
+ TotalRecordCount: items.length
+ };
+
+ return Promise.resolve(result);
+ });
+ }
+ }
+
+ return apiclientcore.getItems(userId, options);
+ }
+
+ function getItem(userId, itemId) {
+
+ if (itemId) {
+ itemId = itemId.toString();
+ }
+
+ var serverInfo;
+
+ if (startsWith(itemId, localViewPrefix)) {
+
+ serverInfo = apiclientcore.serverInfo();
+
+ if (serverInfo) {
+ return localassetmanager.getViews(serverInfo.Id, userId).then(function (items) {
+
+ var views = items.filter(function (item) {
+ return item.Id === itemId;
+ });
+
+ if (views.length > 0) {
+ return Promise.resolve(views[0]);
+ }
+
+ // TODO: Test consequence of this
+ return Promise.reject();
+ });
+ }
+ }
+
+ if (startsWith(itemId, localPrefix)) {
+
+ serverInfo = apiclientcore.serverInfo();
+
+ if (serverInfo) {
+ return localassetmanager.getLocalItem(serverInfo.Id, stripStart(itemId, localPrefix)).then(function (item) {
+
+ item.Item.Id = localPrefix + item.Item.Id;
+
+ return Promise.resolve(item.Item);
+ });
+ }
+ }
+
+ return apiclientcore.getItem(userId, itemId);
+ }
+
+ function getNextUpEpisodes(options) {
+
+ if (options.SeriesId) {
+ if (startsWith(options.SeriesId, localPrefix)) {
+ return Promise.resolve(createEmptyList());
+ }
+ }
+
+ return apiclientcore.getNextUpEpisodes(options);
+ }
+
+ function getSeasons(itemId, options) {
+
+ if (startsWith(itemId, localPrefix)) {
+ options.ParentId = itemId;
+ return getItems(apiclientcore.getCurrentUserId(), options);
+ }
+
+ return apiclientcore.getSeasons(itemId, options);
+ }
+
+ function getEpisodes(itemId, options) {
+
+ if (startsWith(options.SeasonId, localPrefix)) {
+ options.ParentId = options.SeasonId;
+ return getItems(apiclientcore.getCurrentUserId(), options);
+ }
+
+ return apiclientcore.getEpisodes(itemId, options);
+ }
+
+ function getThemeMedia(userId, itemId, inherit) {
+
+ if (startsWith(itemId, localViewPrefix) || startsWith(itemId, localPrefix)) {
+ return Promise.reject();
+ }
+
+ return apiclientcore.getThemeMedia(userId, itemId, inherit);
+ }
+
+ function getSimilarItems(itemId, options) {
+
+ if (startsWith(itemId, localPrefix)) {
+ return Promise.resolve(createEmptyList());
+ }
+
+ return apiclientcore.getSimilarItems(itemId, options);
+ }
+
+ function updateFavoriteStatus(userId, itemId, isFavorite) {
+
+ if (startsWith(itemId, localPrefix)) {
+ return Promise.resolve();
+ }
+
+ return apiclientcore.updateFavoriteStatus(userId, itemId, isFavorite);
+ }
+
+ function getScaledImageUrl(itemId, options) {
+
+ if (startsWith(itemId, localPrefix)) {
+
+ var serverInfo = apiclientcore.serverInfo();
+ var id = stripStart(itemId, localPrefix);
+
+ return localassetmanager.getImageUrl(serverInfo.Id, id, options.type, 0);
+ }
+
+
+ return apiclientcore.getScaledImageUrl(itemId, options);
+ }
+
+ function onWebSocketMessage(e, msg) {
+
+ events.trigger(self, 'websocketmessage', [msg]);
+ }
+
+ // **************** Helper functions
+
+ function startsWith(str, find) {
+
+ if (str && find && str.length > find.length) {
+ if (str.indexOf(find) === 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function stripStart(str, find) {
+ if (startsWith(str, find)) {
+ return str.substr(find.length);
+ }
+
+ return str;
+ }
+
+ function createEmptyList() {
+ var result = {
+ Items: [],
+ TotalRecordCount: 0
+ };
+
+ return result;
+ }
+
+ function getPlaybackInfo(itemId, options, deviceProfile) {
+
+ return localassetmanager.getLocalItem(apiclientcore.serverId(), stripStart(itemId, localPrefix)).then(function (item) {
+
+ // TODO: This was already done during the sync process, right? If so, remove it
+ var mediaSources = item.Item.MediaSources.map(function (m) {
+ m.SupportsDirectPlay = true;
+ m.SupportsDirectStream = false;
+ m.SupportsTranscoding = false;
+ return m;
+ });
+
+ return {
+ MediaSources: mediaSources
+ };
+ });
+ }
+
+ // "Override" methods
+
+ self.detectBitrate = function () {
+ return Promise.reject();
+ };
+
+ self.reportPlaybackStart = function (options) {
+
+ if (!options) {
+ throw new Error("null options");
+ }
+
+ return Promise.resolve();
+ };
+
+ self.reportPlaybackProgress = function (options) {
+
+ if (!options) {
+ throw new Error("null options");
+ }
+
+ return Promise.resolve();
+ };
+
+ self.reportPlaybackStopped = function (options) {
+
+ if (!options) {
+ throw new Error("null options");
+ }
+
+ return Promise.resolve();
+ };
+
+ self.getIntros = function (itemId) {
+
+ return Promise.resolve({
+ Items: [],
+ TotalRecordCount: 0
+ });
+ };
+
+ self.getUserViews = getUserViews;
+ self.getItems = getItems;
+ self.getItem = getItem;
+ self.getSeasons = getSeasons;
+ self.getEpisodes = getEpisodes;
+ self.getThemeMedia = getThemeMedia;
+ self.getNextUpEpisodes = getNextUpEpisodes;
+ self.getSimilarItems = getSimilarItems;
+ self.updateFavoriteStatus = updateFavoriteStatus;
+ self.getScaledImageUrl = getScaledImageUrl;
+ self.getPlaybackInfo = getPlaybackInfo;
+ };
+
+});
\ No newline at end of file
diff --git a/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js b/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js
index 66581b7e0a..6f4c1e7ba5 100644
--- a/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js
+++ b/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js
@@ -216,7 +216,7 @@
return connectUser;
};
- var minServerVersion = '3.0.7200';
+ var minServerVersion = '3.0.8500';
self.minServerVersion = function (val) {
if (val) {
@@ -251,6 +251,10 @@
return credentialProvider.credentials().ConnectAccessToken;
};
+ self.getApiClients = function () {
+ return apiClients;
+ };
+
self.getServerInfo = function (id) {
var servers = credentialProvider.credentials().Servers;
@@ -353,6 +357,8 @@
function onConnectUserSignIn(user) {
+ appStorage.removeItem('lastLocalServerId');
+
connectUser = user;
events.trigger(self, 'connectusersignedin', [user]);
}
@@ -458,6 +464,12 @@
function onLocalUserSignIn(server, connectionMode, user) {
+ if (self.connectUserId()) {
+ appStorage.removeItem('lastLocalServerId');
+ } else {
+ appStorage.setItem('lastLocalServerId', server.Id);
+ }
+
// Ensure this is created so that listeners of the event can get the apiClient instance
getOrAddApiClient(server, connectionMode);
@@ -471,29 +483,26 @@
function ensureConnectUser(credentials) {
- return new Promise(function (resolve, reject) {
+ if (connectUser && connectUser.Id === credentials.ConnectUserId) {
+ return Promise.resolve();
+ }
- if (connectUser && connectUser.Id === credentials.ConnectUserId) {
- resolve();
- }
+ else if (credentials.ConnectUserId && credentials.ConnectAccessToken) {
- else if (credentials.ConnectUserId && credentials.ConnectAccessToken) {
+ connectUser = null;
- connectUser = null;
+ return getConnectUser(credentials.ConnectUserId, credentials.ConnectAccessToken).then(function (user) {
- getConnectUser(credentials.ConnectUserId, credentials.ConnectAccessToken).then(function (user) {
+ onConnectUserSignIn(user);
+ return Promise.resolve();
- onConnectUserSignIn(user);
- resolve();
+ }, function () {
+ return Promise.resolve();
+ });
- }, function () {
- resolve();
- });
-
- } else {
- resolve();
- }
- });
+ } else {
+ return Promise.resolve();
+ }
}
function getConnectUrl(handler) {
@@ -728,6 +737,10 @@
}
}
+ if (credentials.ConnectAccessToken) {
+ appStorage.removeItem('lastLocalServerId');
+ }
+
credentials.Servers = servers;
credentials.ConnectAccessToken = null;
credentials.ConnectUserId = null;
@@ -926,9 +939,18 @@
console.log('Begin connectToServers, with ' + servers.length + ' servers');
- if (servers.length === 1) {
+ var defaultServer = servers.length === 1 ? servers[0] : null;
- return self.connectToServer(servers[0], options).then(function (result) {
+ if (!defaultServer) {
+ var lastLocalServerId = appStorage.getItem('lastLocalServerId');
+ defaultServer = servers.filter(function (s) {
+ return s.Id === lastLocalServerId;
+ })[0];
+ }
+
+ if (defaultServer) {
+
+ return self.connectToServer(defaultServer, options).then(function (result) {
if (result.State === ConnectionState.Unavailable) {
diff --git a/dashboard-ui/bower_components/emby-apiclient/localassetmanager.js b/dashboard-ui/bower_components/emby-apiclient/localassetmanager.js
index 60c1a9cf5f..c949636265 100644
--- a/dashboard-ui/bower_components/emby-apiclient/localassetmanager.js
+++ b/dashboard-ui/bower_components/emby-apiclient/localassetmanager.js
@@ -66,6 +66,26 @@
});
}
+ function getItemsFromIds(serverId, ids) {
+
+ var actions = ids.map(function (id) {
+ var strippedId = stripStart(id, 'local:');
+
+ return getLocalItem(serverId, strippedId);
+ });
+
+ return Promise.all(actions).then(function (items) {
+
+ var libItems = items.map(function (locItem) {
+
+ return locItem.Item;
+ });
+
+
+ return Promise.resolve(libItems);
+ });
+ }
+
function getViews(serverId, userId) {
return itemrepository.getServerItemTypes(serverId, userId).then(function (types) {
@@ -80,7 +100,7 @@
ServerId: serverId,
Id: 'localview:MusicView',
Type: 'MusicView',
- CollectionType: 'Music',
+ CollectionType: 'music',
IsFolder: true
};
@@ -94,7 +114,7 @@
ServerId: serverId,
Id: 'localview:PhotosView',
Type: 'PhotosView',
- CollectionType: 'Photos',
+ CollectionType: 'photos',
IsFolder: true
};
@@ -108,23 +128,49 @@
ServerId: serverId,
Id: 'localview:TVView',
Type: 'TVView',
- CollectionType: 'TvShows',
+ CollectionType: 'tvshows',
IsFolder: true
};
list.push(item);
}
- if (types.indexOf('video') > -1 ||
- types.indexOf('movie') > -1 ||
- types.indexOf('musicvideo') > -1) {
+ if (types.indexOf('movie') > -1) {
+
+ item = {
+ Name: 'Movies',
+ ServerId: serverId,
+ Id: 'localview:MoviesView',
+ Type: 'MoviesView',
+ CollectionType: 'movies',
+ IsFolder: true
+ };
+
+ list.push(item);
+ }
+
+ if (types.indexOf('video') > -1) {
item = {
Name: 'Videos',
ServerId: serverId,
Id: 'localview:VideosView',
Type: 'VideosView',
- CollectionType: 'HomeVideos',
+ CollectionType: 'videos',
+ IsFolder: true
+ };
+
+ list.push(item);
+ }
+
+ if (types.indexOf('musicvideo') > -1) {
+
+ item = {
+ Name: 'Music Videos',
+ ServerId: serverId,
+ Id: 'localview:MusicVideosView',
+ Type: 'MusicVideosView',
+ CollectionType: 'videos',
IsFolder: true
};
@@ -135,28 +181,80 @@
});
}
+ function getTypeFilterForTopLevelView(parentId) {
+
+ var typeFilter = null;
+
+ switch (parentId) {
+ case 'localview:MusicView':
+ typeFilter = 'audio';
+ break;
+ case 'localview:PhotosView':
+ typeFilter = 'photo';
+ break;
+ case 'localview:TVView':
+ typeFilter = 'episode';
+ break;
+ case 'localview:VideosView':
+ typeFilter = 'video';
+ break;
+ case 'localview:MoviesView':
+ typeFilter = 'movie';
+ break;
+ case 'localview:MusicVideosView':
+ typeFilter = 'musicvideo';
+ break;
+ }
+
+ return typeFilter;
+ }
+
function getViewItems(serverId, userId, parentId) {
+ var typeFilter = getTypeFilterForTopLevelView(parentId);
+
+ parentId = stripStart(parentId, 'localview:');
+ parentId = stripStart(parentId, 'local:');
+
return getServerItems(serverId).then(function (items) {
- var resultItems = items.filter(function (item) {
+ var resultItemIds = items.filter(function (item) {
- var type = (item.Item.Type || '').toLowerCase();
-
- switch (parentId) {
- case 'localview:MusicView':
- return type === 'audio';
- case 'localview:PhotosView':
- return type === 'photo';
- case 'localview:TVView':
- return type === 'episode';
- case 'localview:VideosView':
- return type === 'movie' || type === 'video' || type === 'musicvideo';
- default:
- return false;
+ if (item.SyncStatus && item.SyncStatus !== 'synced') {
+ return false;
}
+
+ if (typeFilter) {
+ var type = (item.Item.Type || '').toLowerCase();
+ return typeFilter === type;
+ }
+
+ return item.Item.ParentId === parentId;
+
}).map(function (item2) {
- return item2.Item;
+
+ switch (typeFilter) {
+ case 'audio':
+ case 'photo':
+ return item2.Item.AlbumId;
+ case 'episode':
+ return item2.Item.SeriesId;
+ }
+
+ return item2.Item.Id;
+
+ }).filter(filterDistinct);
+
+ var resultItems = [];
+
+ items.forEach(function (item) {
+ var found = false;
+
+ resultItemIds.forEach(function (id) {
+ if (item.Item.Id === id) {
+ resultItems.push(item.Item);
+ }
+ });
});
return Promise.resolve(resultItems);
@@ -203,7 +301,14 @@
}
function addOrUpdateLocalItem(localItem) {
- return itemrepository.set(localItem.Id, localItem);
+ console.log('addOrUpdateLocalItem Start');
+ return itemrepository.set(localItem.Id, localItem).then(function (res) {
+ console.log('addOrUpdateLocalItem Success');
+ return Promise.resolve(true);
+ }, function (error) {
+ console.log('addOrUpdateLocalItem Error');
+ return Promise.resolve(false);
+ });
}
function createLocalItem(libraryItem, serverInfo, jobItem) {
@@ -211,14 +316,19 @@
var path = getDirectoryPath(libraryItem, serverInfo);
var localFolder = filerepository.getFullLocalPath(path);
- path.push(getLocalFileName(libraryItem, jobItem.OriginalFileName));
+ var localPath;
- var localPath = filerepository.getFullLocalPath(path);
+ if (jobItem) {
+ path.push(getLocalFileName(libraryItem, jobItem.OriginalFileName));
+ localPath = filerepository.getFullLocalPath(path);
+ }
- for (var i = 0; i < libraryItem.MediaSources.length; i++) {
- var mediaSource = libraryItem.MediaSources[i];
- mediaSource.Path = localPath;
- mediaSource.Protocol = 'File';
+ if (libraryItem.MediaSources) {
+ for (var i = 0; i < libraryItem.MediaSources.length; i++) {
+ var mediaSource = libraryItem.MediaSources[i];
+ mediaSource.Path = localPath;
+ mediaSource.Protocol = 'File';
+ }
}
var item = {
@@ -228,11 +338,14 @@
ServerId: serverInfo.Id,
LocalPath: localPath,
LocalFolder: localFolder,
- AdditionalFiles: jobItem.AdditionalFiles.slice(0),
- Id: getLocalId(serverInfo.Id, libraryItem.Id),
- SyncJobItemId: jobItem.SyncJobItemId
+ Id: getLocalId(serverInfo.Id, libraryItem.Id)
};
+ if (jobItem) {
+ item.AdditionalFiles = jobItem.AdditionalFiles.slice(0);
+ item.SyncJobItemId = jobItem.SyncJobItemId;
+ }
+
return Promise.resolve(item);
}
@@ -276,12 +389,14 @@
function downloadFile(url, localItem) {
- return transfermanager.downloadFile(url, localItem);
+ var folder = filerepository.getLocalPath();
+ return transfermanager.downloadFile(url, folder, localItem);
}
function downloadSubtitles(url, fileName) {
- return transfermanager.downloadSubtitles(url, fileName);
+ var folder = filerepository.getLocalPath();
+ return transfermanager.downloadSubtitles(url, folder, fileName);
}
function getImageUrl(serverId, itemId, imageType, index) {
@@ -296,7 +411,7 @@
function hasImage(serverId, itemId, imageType, index) {
var pathArray = getImagePath(serverId, itemId, imageType, index);
- var localFilePath = filerepository.getFullLocalPath(pathArray);
+ var localFilePath = filerepository.getFullMetadataPath(pathArray);
return filerepository.fileExists(localFilePath).then(function (exists) {
// TODO: Maybe check for broken download when file size is 0 and item is not queued
@@ -316,7 +431,7 @@
function downloadImage(localItem, url, serverId, itemId, imageType, index) {
var pathArray = getImagePath(serverId, itemId, imageType, index);
- var localFilePath = filerepository.getFullLocalPath(pathArray);
+ var localFilePath = filerepository.getFullMetadataPath(pathArray);
if (!localItem.AdditionalFiles) {
localItem.AdditionalFiles = [];
@@ -331,7 +446,8 @@
localItem.AdditionalFiles.push(fileInfo);
- return transfermanager.downloadImage(url, localFilePath);
+ var folder = filerepository.getMetadataPath();
+ return transfermanager.downloadImage(url, folder, localFilePath);
}
function isDownloadFileInQueue(path) {
@@ -339,6 +455,11 @@
return transfermanager.isDownloadFileInQueue(path);
}
+ function getDownloadItemCount() {
+
+ return transfermanager.getDownloadItemCount();
+ }
+
function translateFilePath(path) {
return Promise.resolve(path);
}
@@ -409,7 +530,9 @@
parts.push('Metadata');
parts.push(serverId);
parts.push('images');
- parts.push(itemId + '_' + imageType + '_' + index.toString() + '.png');
+ // Store without extension. This allows mixed image types since the browser will
+ // detect the type from the content
+ parts.push(itemId + '_' + imageType + '_' + index.toString()); // + '.jpg');
var finalParts = [];
for (var i = 0; i < parts.length; i++) {
@@ -444,6 +567,29 @@
return uuid;
}
+ function startsWith(str, find) {
+
+ if (str && find && str.length > find.length) {
+ if (str.indexOf(find) === 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function stripStart(str, find) {
+ if (startsWith(str, find)) {
+ return str.substr(find.length);
+ }
+
+ return str;
+ }
+
+ function filterDistinct(value, index, self) {
+ return self.indexOf(value) === index;
+ }
+
return {
getLocalItem: getLocalItem,
@@ -468,8 +614,10 @@
getServerItems: getServerItems,
getItemFileSize: getItemFileSize,
isDownloadFileInQueue: isDownloadFileInQueue,
+ getDownloadItemCount: getDownloadItemCount,
getViews: getViews,
getViewItems: getViewItems,
- resyncTransfers: resyncTransfers
+ resyncTransfers: resyncTransfers,
+ getItemsFromIds: getItemsFromIds
};
});
\ No newline at end of file
diff --git a/dashboard-ui/bower_components/emby-apiclient/sync/itemrepository.js b/dashboard-ui/bower_components/emby-apiclient/sync/itemrepository.js
index c9a97831fe..bd076e35d7 100644
--- a/dashboard-ui/bower_components/emby-apiclient/sync/itemrepository.js
+++ b/dashboard-ui/bower_components/emby-apiclient/sync/itemrepository.js
@@ -90,7 +90,7 @@
function clear() {
return dbPromise.then(function (db) {
var tx = db.transaction(dbName, 'readwrite');
- tx.objectStore(dbName).clear(key);
+ tx.objectStore(dbName).clear();
return tx.complete;
});
}
diff --git a/dashboard-ui/bower_components/emby-apiclient/sync/mediasync.js b/dashboard-ui/bower_components/emby-apiclient/sync/mediasync.js
index df681c3a73..a36b369136 100644
--- a/dashboard-ui/bower_components/emby-apiclient/sync/mediasync.js
+++ b/dashboard-ui/bower_components/emby-apiclient/sync/mediasync.js
@@ -178,7 +178,7 @@
});
}
- function getNewMedia(apiClient, serverInfo, options) {
+ function getNewMedia(apiClient, serverInfo, options, downloadCount) {
console.log('[mediasync] Begin getNewMedia');
@@ -186,10 +186,15 @@
var p = Promise.resolve();
+ var maxDownloads = 10;
+ var currentCount = downloadCount;
+
jobItems.forEach(function (jobItem) {
- p = p.then(function () {
- return getNewItem(jobItem, apiClient, serverInfo, options);
- });
+ if (currentCount++ <= maxDownloads) {
+ p = p.then(function () {
+ return getNewItem(jobItem, apiClient, serverInfo, options);
+ });
+ }
});
return p.then(function () {
@@ -216,7 +221,9 @@
libraryItem.CanDelete = false;
libraryItem.CanDownload = false;
+ libraryItem.SupportsSync = false;
libraryItem.People = [];
+ libraryItem.UserData = [];
libraryItem.SpecialFeatureCount = null;
return localassetmanager.createLocalItem(libraryItem, serverInfo, jobItem).then(function (localItem) {
@@ -225,18 +232,94 @@
localItem.SyncStatus = 'queued';
- return downloadMedia(apiClient, jobItem, localItem, options).then(function () {
+ return downloadParentItems(apiClient, jobItem, localItem, serverInfo, options).then(function () {
- return getImages(apiClient, jobItem, localItem).then(function () {
+ return downloadMedia(apiClient, jobItem, localItem, options).then(function () {
- return getSubtitles(apiClient, jobItem, localItem);
+ return getImages(apiClient, jobItem, localItem).then(function () {
+ return getSubtitles(apiClient, jobItem, localItem);
+
+ });
});
});
});
});
}
+ function downloadParentItems(apiClient, jobItem, localItem, serverInfo, options) {
+
+ var p = Promise.resolve();
+
+ var libraryItem = localItem.Item;
+
+ var itemType = (libraryItem.Type || '').toLowerCase();
+ var logoImageTag = (libraryItem.ImageTags || {}).Logo;
+
+ switch (itemType) {
+ case 'episode':
+ if (libraryItem.SeriesId && libraryItem.SeriesId) {
+ p = p.then(function () {
+ return downloadItem(apiClient, libraryItem, libraryItem.SeriesId, serverInfo).then(function (seriesItem) {
+ libraryItem.SeriesLogoImageTag = (seriesItem.Item.ImageTags || {}).Logo;
+ return Promise.resolve();
+ });
+ });
+ }
+ if (libraryItem.SeasonId && libraryItem.SeasonId) {
+ p = p.then(function () {
+ return downloadItem(apiClient, libraryItem, libraryItem.SeasonId, serverInfo).then(function (seasonItem) {
+ libraryItem.SeasonPrimaryImageTag = (seasonItem.Item.ImageTags || {}).Primary;
+ return Promise.resolve();
+ });
+ });
+ }
+ break;
+
+ case 'audio':
+ case 'photo':
+ if (libraryItem.AlbumId && libraryItem.AlbumId) {
+ p = p.then(function () {
+ return downloadItem(apiClient, libraryItem, libraryItem.AlbumId, serverInfo);
+ });
+ }
+ break;
+
+ case 'video':
+ case 'movie':
+ case 'musicvideo':
+ // no parent item download for now
+ break;
+ }
+
+ return p;
+ }
+
+ function downloadItem(apiClient, libraryItem, itemId, serverInfo) {
+
+ return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (downloadedItem) {
+
+ downloadedItem.CanDelete = false;
+ downloadedItem.CanDownload = false;
+ downloadedItem.SupportsSync = false;
+ downloadedItem.People = [];
+ downloadedItem.UserData = {};
+ downloadedItem.SpecialFeatureCount = null;
+ downloadedItem.BackdropImageTags = null;
+
+ return localassetmanager.createLocalItem(downloadedItem, serverInfo, null).then(function (localItem) {
+
+ return localassetmanager.addOrUpdateLocalItem(localItem).then(function () {
+ return Promise.resolve(localItem);
+ });
+ });
+ }, function (err) {
+
+ console.error('[mediasync] downloadItem failed: ' + err.toString());
+ return Promise.resolve(null);
+ });
+ }
+
function downloadMedia(apiClient, jobItem, localItem, options) {
var url = apiClient.getUrl('Sync/JobItems/' + jobItem.SyncJobItemId + '/File', {
@@ -327,8 +410,23 @@
p = p.then(function () {
return downloadImage(localItem, apiClient, serverId, libraryItem.SeriesId, libraryItem.SeriesPrimaryImageTag, 'Primary');
});
+ }
+
+ if (libraryItem.SeriesId && libraryItem.SeriesThumbImageTag) {
p = p.then(function () {
- return downloadImage(localItem, apiClient, serverId, libraryItem.SeriesId, libraryItem.SeriesPrimaryImageTag, 'Thumb');
+ return downloadImage(localItem, apiClient, serverId, libraryItem.SeriesId, libraryItem.SeriesThumbImageTag, 'Thumb');
+ });
+ }
+
+ if (libraryItem.SeriesId && libraryItem.SeriesLogoImageTag) {
+ p = p.then(function () {
+ return downloadImage(localItem, apiClient, serverId, libraryItem.SeriesId, libraryItem.SeriesLogoImageTag, 'Logo');
+ });
+ }
+
+ if (libraryItem.SeasonId && libraryItem.SeasonPrimaryImageTag) {
+ p = p.then(function () {
+ return downloadImage(localItem, apiClient, serverId, libraryItem.SeasonId, libraryItem.SeasonPrimaryImageTag, 'Primary');
});
}
@@ -344,6 +442,7 @@
return localassetmanager.addOrUpdateLocalItem(localItem);
}, function (err) {
console.log('[mediasync] Error getImages: ' + err.toString());
+ return Promise.resolve();
});
}
@@ -359,18 +458,30 @@
return Promise.resolve();
}
- var imageUrl = apiClient.getImageUrl(itemId, {
+ var maxWidth = 400;
+
+ if (imageType === 'backdrop') {
+ maxWidth = null;
+ }
+
+ var imageUrl = apiClient.getScaledImageUrl(itemId, {
tag: imageTag,
type: imageType,
- format: 'png',
+ maxWidth: maxWidth,
api_key: apiClient.accessToken()
});
console.log('[mediasync] downloadImage ' + itemId + ' ' + imageType + '_' + index.toString());
- return localassetmanager.downloadImage(localItem, imageUrl, serverId, itemId, imageType, index);
+ return localassetmanager.downloadImage(localItem, imageUrl, serverId, itemId, imageType, index).then(function (result) {
+ return Promise.resolve();
+ }, function (err) {
+ console.log('[mediasync] Error downloadImage: ' + err.toString());
+ return Promise.resolve();
+ });
}, function (err) {
console.log('[mediasync] Error downloadImage: ' + err.toString());
+ return Promise.resolve();
});
}
@@ -450,26 +561,28 @@
return processDownloadStatus(apiClient, serverInfo, options).then(function () {
- if (options.syncCheckProgressOnly === true) {
- return Promise.resolve();
- }
+ return localassetmanager.getDownloadItemCount().then(function (downloadCount) {
- return reportOfflineActions(apiClient, serverInfo).then(function () {
+ if (options.syncCheckProgressOnly === true && downloadCount > 2) {
+ return Promise.resolve();
+ }
- //// Do the first data sync
- //return syncData(apiClient, serverInfo, false).then(function () {
+ return reportOfflineActions(apiClient, serverInfo).then(function () {
- // Download new content
- return getNewMedia(apiClient, serverInfo, options).then(function () {
+ // Download new content
+ return getNewMedia(apiClient, serverInfo, options, downloadCount).then(function () {
- // Do the second data sync
- return syncData(apiClient, serverInfo, false).then(function () {
- console.log('[mediasync]************************************* Exit sync');
- return Promise.resolve();
+ // Do the second data sync
+ return syncData(apiClient, serverInfo, false).then(function () {
+ console.log('[mediasync]************************************* Exit sync');
+ return Promise.resolve();
+ });
});
+ //});
});
- //});
});
+ }, function (err) {
+ console.error(err.toString());
});
};
};
diff --git a/dashboard-ui/bower_components/emby-apiclient/sync/transfermanager.js b/dashboard-ui/bower_components/emby-apiclient/sync/transfermanager.js
index f8bea7307b..309823e1f4 100644
--- a/dashboard-ui/bower_components/emby-apiclient/sync/transfermanager.js
+++ b/dashboard-ui/bower_components/emby-apiclient/sync/transfermanager.js
@@ -1,17 +1,17 @@
define(['filerepository'], function (filerepository) {
'use strict';
- function downloadFile(url, localPath) {
+ function downloadFile(url, folderName, localPath) {
return Promise.resolve();
}
- function downloadSubtitles(url, localItem, subtitleStreamh) {
+ function downloadSubtitles(url, folderName, localItem) {
return Promise.resolve('');
}
- function downloadImage(url, serverId, itemId, imageTag) {
+ function downloadImage(url, folderName, serverId, itemId, imageTag) {
return Promise.resolve(false);
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json
index 43c84e47cf..b5d83fb2bb 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json
+++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json
@@ -14,12 +14,12 @@
},
"devDependencies": {},
"ignore": [],
- "version": "1.4.406",
- "_release": "1.4.406",
+ "version": "1.4.490",
+ "_release": "1.4.490",
"_resolution": {
"type": "version",
- "tag": "1.4.406",
- "commit": "5ef7b315244a1804f2892269a42db94a52a86ea8"
+ "tag": "1.4.490",
+ "commit": "d0ee6da0b7661ff97d3501ea7633d113d3cb5a99"
},
"_source": "https://github.com/MediaBrowser/emby-webcomponents.git",
"_target": "^1.2.1",
diff --git a/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md b/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md
index 8f5a547ed2..4522ba0659 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md
+++ b/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md
@@ -1,22 +1,339 @@
-The MIT License
+GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
-Copyright (c) Emby https://emby.media
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+ Preamble
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {{description}}
+ Copyright (C) {{year}} {{fullname}}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/dashboard-ui/bower_components/emby-webcomponents/actionsheet/actionsheet.css b/dashboard-ui/bower_components/emby-webcomponents/actionsheet/actionsheet.css
index 5d16f7945d..97f36906f8 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/actionsheet/actionsheet.css
+++ b/dashboard-ui/bower_components/emby-webcomponents/actionsheet/actionsheet.css
@@ -97,7 +97,7 @@
}
.actionSheetTitle {
- margin: .5em 0 1em !important;
+ margin: .5em 0 !important;
padding: 0 1em;
flex-grow: 0;
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/appsettings.js b/dashboard-ui/bower_components/emby-webcomponents/appsettings.js
index 06b4a79ccd..37d0d4ed04 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/appsettings.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/appsettings.js
@@ -47,7 +47,8 @@ define(['appStorage', 'events'], function (appStorage, events) {
self.set('maxStaticMusicBitrate', val);
}
- return parseInt(self.get('maxStaticMusicBitrate') || '0') || null;
+ var defaultValue = 384000;
+ return parseInt(self.get('maxStaticMusicBitrate') || defaultValue.toString()) || defaultValue;
};
self.maxChromecastBitrate = function (val) {
diff --git a/dashboard-ui/bower_components/emby-webcomponents/backdrop/backdrop.js b/dashboard-ui/bower_components/emby-webcomponents/backdrop/backdrop.js
index 3c5121a408..fd4cb438f8 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/backdrop/backdrop.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/backdrop/backdrop.js
@@ -201,7 +201,7 @@
return item.BackdropImageTags.map(function (imgTag, index) {
- return apiClient.getScaledImageUrl(item.Id, Object.assign(imageOptions, {
+ return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
type: "Backdrop",
tag: imgTag,
maxWidth: getBackdropMaxWidth(),
@@ -294,14 +294,14 @@
currentRotationIndex = -1;
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
- rotationInterval = setInterval(onRotationInterval, 20000);
+ rotationInterval = setInterval(onRotationInterval, 24000);
}
onRotationInterval();
}
function onRotationInterval() {
- if (playbackManager.isPlayingVideo()) {
+ if (playbackManager.isPlayingLocally(['Video'])) {
return;
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/browser.js b/dashboard-ui/bower_components/emby-webcomponents/browser.js
index cacaa3a57e..b403bad12e 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/browser.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/browser.js
@@ -248,16 +248,25 @@
browser.tv = true;
}
+ if (userAgent.toLowerCase().indexOf("embytheaterpi") !== -1) {
+ browser.slow = true;
+ browser.noAnimation = true;
+ }
+
if (isMobile(userAgent)) {
browser.mobile = true;
}
browser.xboxOne = userAgent.toLowerCase().indexOf('xbox') !== -1;
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
- browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || userAgent.toLowerCase().indexOf('smarthub') !== -1;
+ browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || self.tizen != null;
browser.web0s = userAgent.toLowerCase().indexOf('Web0S'.toLowerCase()) !== -1;
browser.edgeUwp = browser.edge && userAgent.toLowerCase().indexOf('msapphost') !== -1;
+ if (!browser.tizen) {
+ browser.orsay = userAgent.toLowerCase().indexOf('smarthub') !== -1;
+ }
+
if (browser.edgeUwp) {
browser.edge = true;
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js b/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js
index c73c9071d4..7180f88f4c 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js
@@ -370,6 +370,16 @@ define(['browser'], function (browser) {
profile.TranscodingProfiles = [];
+ if (canPlayNativeHls() && options.enableHlsAudio) {
+ profile.TranscodingProfiles.push({
+ Container: 'ts',
+ Type: 'Audio',
+ AudioCodec: 'aac',
+ Context: 'Streaming',
+ Protocol: 'hls'
+ });
+ }
+
['opus', 'mp3', 'aac', 'wav'].filter(canPlayAudioFormat).forEach(function (audioFormat) {
profile.TranscodingProfiles.push({
@@ -390,13 +400,8 @@ define(['browser'], function (browser) {
});
});
- var copyTimestamps = false;
- if (browser.chrome) {
- copyTimestamps = true;
- }
-
// Can't use mkv on mobile because we have to use the native player controls and they won't be able to seek it
- if (canPlayMkv && options.supportsCustomSeeking && !browser.tizen && options.enableMkvProgressive !== false) {
+ if (canPlayMkv && !browser.tizen && options.enableMkvProgressive !== false) {
profile.TranscodingProfiles.push({
Container: 'mkv',
Type: 'Video',
@@ -404,7 +409,7 @@ define(['browser'], function (browser) {
VideoCodec: 'h264',
Context: 'Streaming',
MaxAudioChannels: physicalAudioChannels.toString(),
- CopyTimestamps: copyTimestamps
+ CopyTimestamps: true
});
}
@@ -435,7 +440,6 @@ define(['browser'], function (browser) {
}
if (canPlayWebm) {
-
profile.TranscodingProfiles.push({
Container: 'webm',
Type: 'Video',
@@ -557,6 +561,16 @@ define(['browser'], function (browser) {
}
var isTizenFhd = false;
+ if (browser.tizen) {
+ try {
+ var isTizenUhd = webapis.productinfo.isUdPanelSupported();
+ isTizenFhd = !isTizenUhd;
+ console.log("isTizenFhd = " + isTizenFhd);
+ } catch (error) {
+ console.log("isUdPanelSupported() error code = " + error.code);
+ }
+ }
+
var globalMaxVideoBitrate = browser.ps4 ? '8000000' :
(browser.xboxOne ? '10000000' :
(browser.edgeUwp ? '40000000' :
diff --git a/dashboard-ui/bower_components/emby-webcomponents/cardbuilder/cardbuilder.js b/dashboard-ui/bower_components/emby-webcomponents/cardbuilder/cardbuilder.js
index b92d051bb1..b4aee60549 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/cardbuilder/cardbuilder.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/cardbuilder/cardbuilder.js
@@ -6,15 +6,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
function getCardsHtml(items, options) {
- var apiClient = connectionManager.currentApiClient();
-
if (arguments.length === 1) {
options = arguments[0];
items = options.items;
}
- var html = buildCardsHtmlInternal(items, apiClient, options);
+ var html = buildCardsHtmlInternal(items, options);
return html;
}
@@ -249,12 +247,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
else if (options.shape === 'square') {
options.width = options.width || 243;
}
+ else if (options.shape === 'banner') {
+ options.width = options.width || 800;
+ }
}
options.width = options.width || getImageWidth(options.shape);
}
- function buildCardsHtmlInternal(items, apiClient, options) {
+ function buildCardsHtmlInternal(items, options) {
var isVertical;
@@ -269,7 +270,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
setCardData(items, options);
if (options.indexBy === 'Genres') {
- return buildCardsByGenreHtmlInternal(items, apiClient, options);
+ return buildCardsByGenreHtmlInternal(items, options);
}
var className = 'card';
@@ -290,10 +291,18 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
var hasOpenSection;
var sectionTitleTagName = options.sectionTitleTagName || 'div';
+ var apiClient;
+ var lastServerId;
for (var i = 0, length = items.length; i < length; i++) {
var item = items[i];
+ var serverId = item.ServerId || options.serverId;
+
+ if (serverId !== lastServerId) {
+ lastServerId = serverId;
+ apiClient = connectionManager.getApiClient(lastServerId);
+ }
if (options.indexBy) {
var newIndexValue = '';
@@ -404,7 +413,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
});
}
- function buildCardsByGenreHtmlInternal(items, apiClient, options) {
+ function buildCardsByGenreHtmlInternal(items, options) {
var className = 'card';
@@ -435,7 +444,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
}
var cardClass = className;
- currentItemHtml += buildCard(i, renderItem, apiClient, options, cardClass);
+ currentItemHtml += buildCard(i, renderItem, connectionManager.getApiClient(renderItem.ServerId || options.serverId), options, cardClass);
itemsInRow++;
@@ -498,6 +507,9 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
if (shape.indexOf('square') !== -1) {
return 1;
}
+ if (shape.indexOf('banner') !== -1) {
+ return (1000 / 185);
+ }
}
return null;
}
@@ -583,7 +595,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
- imgUrl = apiClient.getScaledImageUrl(item.Id || item.ItemId, {
+ imgUrl = apiClient.getScaledImageUrl(item.PrimaryImageItemId || item.Id || item.ItemId, {
type: "Primary",
maxHeight: height,
maxWidth: width,
@@ -1412,9 +1424,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
}
}
- var apiClient = connectionManager.currentApiClient();
-
- var html = buildCardsHtmlInternal(items, apiClient, options);
+ var html = buildCardsHtmlInternal(items, options);
if (html) {
diff --git a/dashboard-ui/scripts/chromecast.js b/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js
similarity index 64%
rename from dashboard-ui/scripts/chromecast.js
rename to dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js
index 3ac9a14e80..6ccc16ed51 100644
--- a/dashboard-ui/scripts/chromecast.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js
@@ -1,891 +1,1084 @@
-define(['appSettings'], function (appSettings) {
- 'use strict';
-
- // Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js
- var currentResolve;
- var currentReject;
-
- var PlayerName = 'Chromecast';
-
- function sendConnectionResult(isOk) {
-
- var resolve = currentResolve;
- var reject = currentReject;
-
- currentResolve = null;
- currentReject = null;
-
- if (isOk) {
- if (resolve) {
- resolve();
- }
- } else {
- if (reject) {
- reject();
- } else {
- MediaController.removeActivePlayer(PlayerName);
- }
- }
- }
-
- /**
- * Constants of states for Chromecast device
- **/
- var DEVICE_STATE = {
- 'IDLE': 0,
- 'ACTIVE': 1,
- 'WARNING': 2,
- 'ERROR': 3
- };
-
- /**
- * Constants of states for CastPlayer
- **/
- var PLAYER_STATE = {
- 'IDLE': 'IDLE',
- 'LOADING': 'LOADING',
- 'LOADED': 'LOADED',
- 'PLAYING': 'PLAYING',
- 'PAUSED': 'PAUSED',
- 'STOPPED': 'STOPPED',
- 'SEEKING': 'SEEKING',
- 'ERROR': 'ERROR'
- };
-
- var applicationID = "2D4B1DA3";
-
- // This is the beta version used for testing new changes
-
- //applicationID = '27C4EB5B';
-
- var messageNamespace = 'urn:x-cast:com.connectsdk';
-
- var CastPlayer = function () {
-
- /* device variables */
- // @type {DEVICE_STATE} A state for device
- this.deviceState = DEVICE_STATE.IDLE;
-
- /* Cast player variables */
- // @type {Object} a chrome.cast.media.Media object
- this.currentMediaSession = null;
-
- // @type {string} a chrome.cast.Session object
- this.session = null;
- // @type {PLAYER_STATE} A state for Cast media player
- this.castPlayerState = PLAYER_STATE.IDLE;
-
- this.hasReceivers = false;
-
- // bind once - commit 2ebffc2271da0bc5e8b13821586aee2a2e3c7753
- this.errorHandler = this.onError.bind(this);
- this.mediaStatusUpdateHandler = this.onMediaStatusUpdate.bind(this);
-
- this.initializeCastPlayer();
- };
-
- /**
- * Initialize Cast media player
- * Initializes the API. Note that either successCallback and errorCallback will be
- * invoked once the API has finished initialization. The sessionListener and
- * receiverListener may be invoked at any time afterwards, and possibly more than once.
- */
- CastPlayer.prototype.initializeCastPlayer = function () {
-
- var chrome = window.chrome;
-
- if (!chrome) {
- return;
- }
-
- if (!chrome.cast || !chrome.cast.isAvailable) {
-
- setTimeout(this.initializeCastPlayer.bind(this), 1000);
- return;
- }
-
- // request session
- var sessionRequest = new chrome.cast.SessionRequest(applicationID);
- var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
- this.sessionListener.bind(this),
- this.receiverListener.bind(this),
- "origin_scoped");
-
- console.log('chromecast.initialize');
-
- chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this), this.errorHandler);
-
- };
-
- /**
- * Callback function for init success
- */
- CastPlayer.prototype.onInitSuccess = function () {
- this.isInitialized = true;
- console.log("chromecast init success");
- };
-
- /**
- * Generic error callback function
- */
- CastPlayer.prototype.onError = function () {
- console.log("chromecast error");
- };
-
- /**
- * @param {!Object} e A new session
- * This handles auto-join when a page is reloaded
- * When active session is detected, playback will automatically
- * join existing session and occur in Cast mode and media
- * status gets synced up with current media of the session
- */
- CastPlayer.prototype.sessionListener = function (e) {
-
- this.session = e;
- if (this.session) {
-
- console.log('sessionListener ' + JSON.stringify(e));
-
- if (this.session.media[0]) {
- this.onMediaDiscovered('activeSession', this.session.media[0]);
- }
-
- this.onSessionConnected(e);
- }
- };
-
- CastPlayer.prototype.messageListener = function (namespace, message) {
-
- if (typeof (message) === 'string') {
- message = JSON.parse(message);
- }
-
- if (message.type == 'playbackerror') {
-
- var errorCode = message.data;
-
- setTimeout(function () {
- Dashboard.alert({
- message: Globalize.translate('MessagePlaybackError' + errorCode),
- title: Globalize.translate('HeaderPlaybackError')
- });
- }, 300);
-
- }
- else if (message.type == 'connectionerror') {
-
- setTimeout(function () {
- Dashboard.alert({
- message: Globalize.translate('MessageChromecastConnectionError'),
- title: Globalize.translate('HeaderError')
- });
- }, 300);
-
- }
- else if (message.type) {
- Events.trigger(this, message.type, [message.data]);
- }
- };
-
- /**
- * @param {string} e Receiver availability
- * This indicates availability of receivers but
- * does not provide a list of device IDs
- */
- CastPlayer.prototype.receiverListener = function (e) {
-
- if (e === 'available') {
- console.log("chromecast receiver found");
- this.hasReceivers = true;
- }
- else {
- console.log("chromecast receiver list empty");
- this.hasReceivers = false;
- }
- };
-
- /**
- * session update listener
- */
- CastPlayer.prototype.sessionUpdateListener = function (isAlive) {
-
- console.log('sessionUpdateListener alive: ' + isAlive);
-
- if (isAlive) {
- }
- else {
- this.session = null;
- this.deviceState = DEVICE_STATE.IDLE;
- this.castPlayerState = PLAYER_STATE.IDLE;
-
- console.log('sessionUpdateListener: setting currentMediaSession to null');
- this.currentMediaSession = null;
-
- sendConnectionResult(false);
- }
- };
-
- /**
- * Requests that a receiver application session be created or joined. By default, the SessionRequest
- * passed to the API at initialization time is used; this may be overridden by passing a different
- * session request in opt_sessionRequest.
- */
- CastPlayer.prototype.launchApp = function () {
- console.log("chromecast launching app...");
- chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), this.onLaunchError.bind(this));
- };
-
- /**
- * Callback function for request session success
- * @param {Object} e A chrome.cast.Session object
- */
- CastPlayer.prototype.onRequestSessionSuccess = function (e) {
-
- console.log("chromecast session success: " + e.sessionId);
- this.onSessionConnected(e);
- };
-
- CastPlayer.prototype.onSessionConnected = function (session) {
-
- this.session = session;
-
- this.deviceState = DEVICE_STATE.ACTIVE;
-
- this.session.addMessageListener(messageNamespace, this.messageListener.bind(this));
- this.session.addMediaListener(this.sessionMediaListener.bind(this));
- this.session.addUpdateListener(this.sessionUpdateListener.bind(this));
-
- Events.trigger(this, 'connect');
-
- this.sendMessage({
- options: {},
- command: 'Identify'
- });
- };
-
- /**
- * session update listener
- */
- CastPlayer.prototype.sessionMediaListener = function (e) {
-
- console.log('sessionMediaListener');
- this.currentMediaSession = e;
- this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler);
- };
-
- /**
- * Callback function for launch error
- */
- CastPlayer.prototype.onLaunchError = function () {
- console.log("chromecast launch error");
- this.deviceState = DEVICE_STATE.ERROR;
-
- sendConnectionResult(false);
- };
-
- /**
- * Stops the running receiver application associated with the session.
- */
- CastPlayer.prototype.stopApp = function () {
-
- if (this.session) {
- this.session.stop(this.onStopAppSuccess.bind(this, 'Session stopped'),
- this.errorHandler);
- }
-
- };
-
- /**
- * Callback function for stop app success
- */
- CastPlayer.prototype.onStopAppSuccess = function (message) {
- console.log(message);
- this.deviceState = DEVICE_STATE.IDLE;
- this.castPlayerState = PLAYER_STATE.IDLE;
-
- console.log('onStopAppSuccess: setting currentMediaSession to null');
- this.currentMediaSession = null;
- };
-
- /**
- * Loads media into a running receiver application
- * @param {Number} mediaIndex An index number to indicate current media content
- */
- CastPlayer.prototype.loadMedia = function (options, command) {
-
- if (!this.session) {
- console.log("no session");
- return Promise.reject();
- }
-
- // Convert the items to smaller stubs to send the minimal amount of information
- options.items = options.items.map(function (i) {
-
- return {
- Id: i.Id,
- Name: i.Name,
- Type: i.Type,
- MediaType: i.MediaType,
- IsFolder: i.IsFolder
- };
- });
-
- return this.sendMessage({
- options: options,
- command: command
- });
- };
-
- CastPlayer.prototype.sendMessage = function (message) {
-
- var player = this;
-
- var receiverName = null;
-
- if (castPlayer.session && castPlayer.session.receiver && castPlayer.session.receiver.friendlyName) {
- receiverName = castPlayer.session.receiver.friendlyName;
- }
-
- message = Object.assign(message, {
- userId: Dashboard.getCurrentUserId(),
- deviceId: ApiClient.deviceId(),
- accessToken: ApiClient.accessToken(),
- serverAddress: ApiClient.serverAddress(),
- receiverName: receiverName
- });
-
- var bitrateSetting = appSettings.maxChromecastBitrate();
- if (bitrateSetting) {
- message.maxBitrate = bitrateSetting;
- }
-
- return new Promise(function (resolve, reject) {
-
- require(['chromecasthelpers'], function (chromecasthelpers) {
-
- chromecasthelpers.getServerAddress(ApiClient).then(function (serverAddress) {
- message.serverAddress = serverAddress;
- player.sendMessageInternal(message).then(resolve, reject);
-
- }, reject);
- });
- });
- };
-
- CastPlayer.prototype.sendMessageInternal = function (message) {
-
- message = JSON.stringify(message);
- //console.log(message);
-
- this.session.sendMessage(messageNamespace, message, this.onPlayCommandSuccess.bind(this), this.errorHandler);
- return Promise.resolve();
- };
-
- CastPlayer.prototype.onPlayCommandSuccess = function () {
- console.log('Message was sent to receiver ok.');
- };
-
- /**
- * Callback function for loadMedia success
- * @param {Object} mediaSession A new media object.
- */
- CastPlayer.prototype.onMediaDiscovered = function (how, mediaSession) {
-
- console.log("chromecast new media session ID:" + mediaSession.mediaSessionId + ' (' + how + ')');
- this.currentMediaSession = mediaSession;
-
- if (how == 'loadMedia') {
- this.castPlayerState = PLAYER_STATE.PLAYING;
- }
-
- if (how == 'activeSession') {
- this.castPlayerState = mediaSession.playerState;
- }
-
- this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler);
- };
-
- /**
- * Callback function for media status update from receiver
- * @param {!Boolean} e true/false
- */
- CastPlayer.prototype.onMediaStatusUpdate = function (e) {
-
- if (e == false) {
- this.castPlayerState = PLAYER_STATE.IDLE;
- }
- console.log("chromecast updating media: " + e);
- };
-
- /**
- * Set media volume in Cast mode
- * @param {Boolean} mute A boolean
- */
- CastPlayer.prototype.setReceiverVolume = function (mute, vol) {
-
- if (!this.currentMediaSession) {
- console.log('this.currentMediaSession is null');
- return;
- }
-
- if (!mute) {
-
- this.session.setReceiverVolumeLevel((vol || 1),
- this.mediaCommandSuccessCallback.bind(this),
- this.errorHandler);
- }
- else {
- this.session.setReceiverMuted(true,
- this.mediaCommandSuccessCallback.bind(this),
- this.errorHandler);
- }
- };
-
- /**
- * Mute CC
- */
- CastPlayer.prototype.mute = function () {
- this.setReceiverVolume(true);
- };
-
- /**
- * Callback function for media command success
- */
- CastPlayer.prototype.mediaCommandSuccessCallback = function (info, e) {
- console.log(info);
- };
-
- // Create Cast Player
- var castPlayer;
-
- function chromecastPlayer() {
-
- var self = this;
-
- // MediaController needs this
- self.name = PlayerName;
-
- self.getItemsForPlayback = function (query) {
-
- var userId = Dashboard.getCurrentUserId();
-
- if (query.Ids && query.Ids.split(',').length == 1) {
- return ApiClient.getItem(userId, query.Ids.split(',')).then(function (item) {
- return {
- Items: [item],
- TotalRecordCount: 1
- };
- });
- }
- else {
-
- query.Limit = query.Limit || 100;
- query.ExcludeLocationTypes = "Virtual";
-
- return ApiClient.getItems(userId, query);
- }
- };
-
- Events.on(castPlayer, "connect", function (e) {
-
- if (currentResolve) {
- sendConnectionResult(true);
- } else {
- MediaController.setActivePlayer(PlayerName, self.getCurrentTargetInfo());
- }
-
- console.log('cc: connect');
- // Reset this so the next query doesn't make it appear like content is playing.
- self.lastPlayerData = {};
- });
-
- Events.on(castPlayer, "playbackstart", function (e, data) {
-
- console.log('cc: playbackstart');
-
- castPlayer.initializeCastPlayer();
-
- var state = self.getPlayerStateInternal(data);
- Events.trigger(self, "playbackstart", [state]);
- });
-
- Events.on(castPlayer, "playbackstop", function (e, data) {
-
- console.log('cc: playbackstop');
- var state = self.getPlayerStateInternal(data);
-
- Events.trigger(self, "playbackstop", [state]);
-
- // Reset this so the next query doesn't make it appear like content is playing.
- self.lastPlayerData = {};
- });
-
- Events.on(castPlayer, "playbackprogress", function (e, data) {
-
- console.log('cc: positionchange');
- var state = self.getPlayerStateInternal(data);
-
- Events.trigger(self, "positionchange", [state]);
- });
-
- Events.on(castPlayer, "volumechange", function (e, data) {
-
- console.log('cc: volumechange');
- var state = self.getPlayerStateInternal(data);
-
- Events.trigger(self, "volumechange", [state]);
- });
-
- Events.on(castPlayer, "playstatechange", function (e, data) {
-
- console.log('cc: playstatechange');
- var state = self.getPlayerStateInternal(data);
-
- Events.trigger(self, "playstatechange", [state]);
- });
-
- self.play = function (options) {
-
- return Dashboard.getCurrentUser().then(function (user) {
-
- if (options.items) {
-
- return self.playWithCommand(options, 'PlayNow');
-
- } else {
-
- return self.getItemsForPlayback({
-
- Ids: options.ids.join(',')
-
- }).then(function (result) {
-
- options.items = result.Items;
- return self.playWithCommand(options, 'PlayNow');
-
- });
- }
-
- });
-
- };
-
- self.playWithCommand = function (options, command) {
-
- if (!options.items) {
- return ApiClient.getItem(Dashboard.getCurrentUserId(), options.ids[0]).then(function (item) {
-
- options.items = [item];
- return self.playWithCommand(options, command);
- });
- }
-
- return castPlayer.loadMedia(options, command);
- };
-
- self.unpause = function () {
- castPlayer.sendMessage({
- options: {},
- command: 'Unpause'
- });
- };
-
- self.pause = function () {
- castPlayer.sendMessage({
- options: {},
- command: 'Pause'
- });
- };
-
- self.shuffle = function (id) {
-
- var userId = Dashboard.getCurrentUserId();
-
- ApiClient.getItem(userId, id).then(function (item) {
-
- self.playWithCommand({
-
- items: [item]
-
- }, 'Shuffle');
-
- });
-
- };
-
- self.instantMix = function (id) {
-
- var userId = Dashboard.getCurrentUserId();
-
- ApiClient.getItem(userId, id).then(function (item) {
-
- self.playWithCommand({
-
- items: [item]
-
- }, 'InstantMix');
-
- });
-
- };
-
- self.canQueueMediaType = function (mediaType) {
- return mediaType == "Audio";
- };
-
- self.queue = function (options) {
- self.playWithCommand(options, 'PlayLast');
- };
-
- self.queueNext = function (options) {
- self.playWithCommand(options, 'PlayNext');
- };
-
- self.stop = function () {
- castPlayer.sendMessage({
- options: {},
- command: 'Stop'
- });
- };
-
- self.displayContent = function (options) {
-
- castPlayer.sendMessage({
- options: options,
- command: 'DisplayContent'
- });
- };
-
- self.mute = function () {
- castPlayer.sendMessage({
- options: {},
- command: 'Mute'
- });
- //castPlayer.mute();
- };
-
- self.unMute = function () {
- self.setVolume(getCurrentVolume() + 2);
- };
-
- self.setRepeatMode = function (mode) {
- castPlayer.sendMessage({
- options: {
- RepeatMode: mode
- },
- command: 'SetRepeatMode'
- });
- };
-
- self.toggleMute = function () {
-
- var state = self.lastPlayerData || {};
- state = state.PlayState || {};
-
- if (state.IsMuted) {
- self.unMute();
- } else {
- self.mute();
- }
- };
-
- self.getTargets = function () {
-
- var targets = [];
-
- if (castPlayer.hasReceivers) {
- targets.push(self.getCurrentTargetInfo());
- }
-
- return Promise.resolve(targets);
- };
-
- self.getCurrentTargetInfo = function () {
-
- var appName = null;
-
- if (castPlayer.session && castPlayer.session.receiver && castPlayer.session.receiver.friendlyName) {
- appName = castPlayer.session.receiver.friendlyName;
- }
-
- return {
- name: PlayerName,
- id: PlayerName,
- playerName: PlayerName,
- playableMediaTypes: ["Audio", "Video"],
- isLocalPlayer: false,
- appName: PlayerName,
- deviceName: appName,
- supportedCommands: ["VolumeUp",
- "VolumeDown",
- "Mute",
- "Unmute",
- "ToggleMute",
- "SetVolume",
- "SetAudioStreamIndex",
- "SetSubtitleStreamIndex",
- "DisplayContent",
- "SetRepeatMode",
- "EndSession"]
- };
- };
-
- self.seek = function (position) {
-
- position = parseInt(position);
-
- position = position / 10000000;
-
- castPlayer.sendMessage({
- options: {
- position: position
- },
- command: 'Seek'
- });
- };
-
- self.setAudioStreamIndex = function (index) {
- castPlayer.sendMessage({
- options: {
- index: index
- },
- command: 'SetAudioStreamIndex'
- });
- };
-
- self.setSubtitleStreamIndex = function (index) {
- castPlayer.sendMessage({
- options: {
- index: index
- },
- command: 'SetSubtitleStreamIndex'
- });
- };
-
- self.nextTrack = function () {
- castPlayer.sendMessage({
- options: {},
- command: 'NextTrack'
- });
- };
-
- self.previousTrack = function () {
- castPlayer.sendMessage({
- options: {},
- command: 'PreviousTrack'
- });
- };
-
- self.beginPlayerUpdates = function () {
- // Setup polling here
- };
-
- self.endPlayerUpdates = function () {
- // Stop polling here
- };
-
- function getCurrentVolume() {
- var state = self.lastPlayerData || {};
- state = state.PlayState || {};
-
- return state.VolumeLevel == null ? 100 : state.VolumeLevel;
- }
-
- self.volumeDown = function () {
-
- castPlayer.sendMessage({
- options: {},
- command: 'VolumeDown'
- });
- };
-
- self.endSession = function () {
-
- self.stop();
- setTimeout(function () {
- castPlayer.stopApp();
- }, 1000);
- };
-
- self.volumeUp = function () {
-
- castPlayer.sendMessage({
- options: {},
- command: 'VolumeUp'
- });
- };
-
- self.setVolume = function (vol) {
-
- vol = Math.min(vol, 100);
- vol = Math.max(vol, 0);
-
- //castPlayer.setReceiverVolume(false, (vol / 100));
- castPlayer.sendMessage({
- options: {
- volume: vol
- },
- command: 'SetVolume'
- });
- };
-
- self.getPlayerState = function () {
-
- var result = self.getPlayerStateInternal();
- return Promise.resolve(result);
- };
-
- self.lastPlayerData = {};
-
- self.getPlayerStateInternal = function (data) {
-
- data = data || self.lastPlayerData;
- self.lastPlayerData = data;
-
- console.log(JSON.stringify(data));
- return data;
- };
-
- self.tryPair = function (target) {
-
- if (castPlayer.deviceState != DEVICE_STATE.ACTIVE && castPlayer.isInitialized) {
-
- return new Promise(function (resolve, reject) {
- currentResolve = resolve;
- currentReject = reject;
-
- castPlayer.launchApp();
- });
- } else {
-
- currentResolve = null;
- currentReject = null;
-
- return Promise.reject();
- }
- };
- }
-
- function initializeChromecast() {
-
- castPlayer = new CastPlayer();
-
- var registeredPlayer = new chromecastPlayer();
- window.CCastPlayer = registeredPlayer;
- MediaController.registerPlayer(registeredPlayer);
-
- // To allow the native android app to override
- document.dispatchEvent(new CustomEvent("chromecastloaded", {
- detail: {
- player: registeredPlayer
- }
- }));
- }
-
- var fileref = document.createElement('script');
- fileref.setAttribute("type", "text/javascript");
- fileref.onload = initializeChromecast;
- fileref.setAttribute("src", "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js");
- document.querySelector('head').appendChild(fileref);
-
+define(['appSettings', 'playbackManager', 'connectionManager', 'globalize', 'events'], function (appSettings, playbackManager, connectionManager, globalize, events) {
+ 'use strict';
+
+ // Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js
+ var currentResolve;
+ var currentReject;
+
+ var PlayerName = 'Chromecast';
+
+ function sendConnectionResult(isOk) {
+
+ var resolve = currentResolve;
+ var reject = currentReject;
+
+ currentResolve = null;
+ currentReject = null;
+
+ if (isOk) {
+ if (resolve) {
+ resolve();
+ }
+ } else {
+ if (reject) {
+ reject();
+ } else {
+ playbackManager.removeActivePlayer(PlayerName);
+ }
+ }
+ }
+
+ /**
+ * Constants of states for Chromecast device
+ **/
+ var DEVICE_STATE = {
+ 'IDLE': 0,
+ 'ACTIVE': 1,
+ 'WARNING': 2,
+ 'ERROR': 3
+ };
+
+ /**
+ * Constants of states for CastPlayer
+ **/
+ var PLAYER_STATE = {
+ 'IDLE': 'IDLE',
+ 'LOADING': 'LOADING',
+ 'LOADED': 'LOADED',
+ 'PLAYING': 'PLAYING',
+ 'PAUSED': 'PAUSED',
+ 'STOPPED': 'STOPPED',
+ 'SEEKING': 'SEEKING',
+ 'ERROR': 'ERROR'
+ };
+
+ var applicationID = "2D4B1DA3";
+
+ // This is the beta version used for testing new changes
+
+ //applicationID = '27C4EB5B';
+
+ var messageNamespace = 'urn:x-cast:com.connectsdk';
+
+ var CastPlayer = function () {
+
+ /* device variables */
+ // @type {DEVICE_STATE} A state for device
+ this.deviceState = DEVICE_STATE.IDLE;
+
+ /* Cast player variables */
+ // @type {Object} a chrome.cast.media.Media object
+ this.currentMediaSession = null;
+
+ // @type {string} a chrome.cast.Session object
+ this.session = null;
+ // @type {PLAYER_STATE} A state for Cast media player
+ this.castPlayerState = PLAYER_STATE.IDLE;
+
+ this.hasReceivers = false;
+
+ // bind once - commit 2ebffc2271da0bc5e8b13821586aee2a2e3c7753
+ this.errorHandler = this.onError.bind(this);
+ this.mediaStatusUpdateHandler = this.onMediaStatusUpdate.bind(this);
+
+ this.initializeCastPlayer();
+ };
+
+ /**
+ * Initialize Cast media player
+ * Initializes the API. Note that either successCallback and errorCallback will be
+ * invoked once the API has finished initialization. The sessionListener and
+ * receiverListener may be invoked at any time afterwards, and possibly more than once.
+ */
+ CastPlayer.prototype.initializeCastPlayer = function () {
+
+ var chrome = window.chrome;
+
+ if (!chrome) {
+ return;
+ }
+
+ if (!chrome.cast || !chrome.cast.isAvailable) {
+
+ setTimeout(this.initializeCastPlayer.bind(this), 1000);
+ return;
+ }
+
+ // request session
+ var sessionRequest = new chrome.cast.SessionRequest(applicationID);
+ var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
+ this.sessionListener.bind(this),
+ this.receiverListener.bind(this),
+ "origin_scoped");
+
+ console.log('chromecast.initialize');
+
+ chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this), this.errorHandler);
+
+ };
+
+ /**
+ * Callback function for init success
+ */
+ CastPlayer.prototype.onInitSuccess = function () {
+ this.isInitialized = true;
+ console.log("chromecast init success");
+ };
+
+ /**
+ * Generic error callback function
+ */
+ CastPlayer.prototype.onError = function () {
+ console.log("chromecast error");
+ };
+
+ /**
+ * @param {!Object} e A new session
+ * This handles auto-join when a page is reloaded
+ * When active session is detected, playback will automatically
+ * join existing session and occur in Cast mode and media
+ * status gets synced up with current media of the session
+ */
+ CastPlayer.prototype.sessionListener = function (e) {
+
+ this.session = e;
+ if (this.session) {
+
+ console.log('sessionListener ' + JSON.stringify(e));
+
+ if (this.session.media[0]) {
+ this.onMediaDiscovered('activeSession', this.session.media[0]);
+ }
+
+ this.onSessionConnected(e);
+ }
+ };
+
+ function alertText(text, title) {
+ require(['alert'], function (alert) {
+ alert({
+ text: text,
+ title: title
+ });
+ });
+ }
+
+ CastPlayer.prototype.messageListener = function (namespace, message) {
+
+ if (typeof (message) === 'string') {
+ message = JSON.parse(message);
+ }
+
+ if (message.type === 'playbackerror') {
+
+ var errorCode = message.data;
+
+ setTimeout(function () {
+ alertText(globalize.translate('MessagePlaybackError' + errorCode), globalize.translate('HeaderPlaybackError'));
+ }, 300);
+
+ }
+ else if (message.type === 'connectionerror') {
+
+ setTimeout(function () {
+ alertText(globalize.translate('MessageChromecastConnectionError'), globalize.translate('HeaderError'));
+ }, 300);
+
+ }
+ else if (message.type) {
+ events.trigger(this, message.type, [message.data]);
+ }
+ };
+
+ /**
+ * @param {string} e Receiver availability
+ * This indicates availability of receivers but
+ * does not provide a list of device IDs
+ */
+ CastPlayer.prototype.receiverListener = function (e) {
+
+ if (e === 'available') {
+ console.log("chromecast receiver found");
+ this.hasReceivers = true;
+ }
+ else {
+ console.log("chromecast receiver list empty");
+ this.hasReceivers = false;
+ }
+ };
+
+ /**
+ * session update listener
+ */
+ CastPlayer.prototype.sessionUpdateListener = function (isAlive) {
+
+ console.log('sessionUpdateListener alive: ' + isAlive);
+
+ if (isAlive) {
+ }
+ else {
+ this.session = null;
+ this.deviceState = DEVICE_STATE.IDLE;
+ this.castPlayerState = PLAYER_STATE.IDLE;
+
+ console.log('sessionUpdateListener: setting currentMediaSession to null');
+ this.currentMediaSession = null;
+
+ sendConnectionResult(false);
+ }
+ };
+
+ /**
+ * Requests that a receiver application session be created or joined. By default, the SessionRequest
+ * passed to the API at initialization time is used; this may be overridden by passing a different
+ * session request in opt_sessionRequest.
+ */
+ CastPlayer.prototype.launchApp = function () {
+ console.log("chromecast launching app...");
+ chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), this.onLaunchError.bind(this));
+ };
+
+ /**
+ * Callback function for request session success
+ * @param {Object} e A chrome.cast.Session object
+ */
+ CastPlayer.prototype.onRequestSessionSuccess = function (e) {
+
+ console.log("chromecast session success: " + e.sessionId);
+ this.onSessionConnected(e);
+ };
+
+ CastPlayer.prototype.onSessionConnected = function (session) {
+
+ this.session = session;
+
+ this.deviceState = DEVICE_STATE.ACTIVE;
+
+ this.session.addMessageListener(messageNamespace, this.messageListener.bind(this));
+ this.session.addMediaListener(this.sessionMediaListener.bind(this));
+ this.session.addUpdateListener(this.sessionUpdateListener.bind(this));
+
+ events.trigger(this, 'connect');
+
+ this.sendMessage({
+ options: {},
+ command: 'Identify'
+ });
+ };
+
+ /**
+ * session update listener
+ */
+ CastPlayer.prototype.sessionMediaListener = function (e) {
+
+ console.log('sessionMediaListener');
+ this.currentMediaSession = e;
+ this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler);
+ };
+
+ /**
+ * Callback function for launch error
+ */
+ CastPlayer.prototype.onLaunchError = function () {
+ console.log("chromecast launch error");
+ this.deviceState = DEVICE_STATE.ERROR;
+
+ sendConnectionResult(false);
+ };
+
+ /**
+ * Stops the running receiver application associated with the session.
+ */
+ CastPlayer.prototype.stopApp = function () {
+
+ if (this.session) {
+ this.session.stop(this.onStopAppSuccess.bind(this, 'Session stopped'),
+ this.errorHandler);
+ }
+
+ };
+
+ /**
+ * Callback function for stop app success
+ */
+ CastPlayer.prototype.onStopAppSuccess = function (message) {
+ console.log(message);
+ this.deviceState = DEVICE_STATE.IDLE;
+ this.castPlayerState = PLAYER_STATE.IDLE;
+
+ console.log('onStopAppSuccess: setting currentMediaSession to null');
+ this.currentMediaSession = null;
+ };
+
+ /**
+ * Loads media into a running receiver application
+ * @param {Number} mediaIndex An index number to indicate current media content
+ */
+ CastPlayer.prototype.loadMedia = function (options, command) {
+
+ if (!this.session) {
+ console.log("no session");
+ return Promise.reject();
+ }
+
+ // Convert the items to smaller stubs to send the minimal amount of information
+ options.items = options.items.map(function (i) {
+
+ return {
+ Id: i.Id,
+ Name: i.Name,
+ Type: i.Type,
+ MediaType: i.MediaType,
+ IsFolder: i.IsFolder
+ };
+ });
+
+ return this.sendMessage({
+ options: options,
+ command: command
+ });
+ };
+
+ CastPlayer.prototype.sendMessage = function (message) {
+
+ var player = this;
+
+ var receiverName = null;
+
+ var session = player.session;
+
+ if (session && session.receiver && session.receiver.friendlyName) {
+ receiverName = session.receiver.friendlyName;
+ }
+
+ message = Object.assign(message, {
+ userId: ApiClient.getCurrentUserId(),
+ deviceId: ApiClient.deviceId(),
+ accessToken: ApiClient.accessToken(),
+ serverAddress: ApiClient.serverAddress(),
+ receiverName: receiverName
+ });
+
+ var bitrateSetting = appSettings.maxChromecastBitrate();
+ if (bitrateSetting) {
+ message.maxBitrate = bitrateSetting;
+ }
+
+ return new Promise(function (resolve, reject) {
+
+ require(['chromecasthelpers'], function (chromecasthelpers) {
+
+ chromecasthelpers.getServerAddress(ApiClient).then(function (serverAddress) {
+ message.serverAddress = serverAddress;
+ player.sendMessageInternal(message).then(resolve, reject);
+
+ }, reject);
+ });
+ });
+ };
+
+ CastPlayer.prototype.sendMessageInternal = function (message) {
+
+ message = JSON.stringify(message);
+ //console.log(message);
+
+ this.session.sendMessage(messageNamespace, message, this.onPlayCommandSuccess.bind(this), this.errorHandler);
+ return Promise.resolve();
+ };
+
+ CastPlayer.prototype.onPlayCommandSuccess = function () {
+ console.log('Message was sent to receiver ok.');
+ };
+
+ /**
+ * Callback function for loadMedia success
+ * @param {Object} mediaSession A new media object.
+ */
+ CastPlayer.prototype.onMediaDiscovered = function (how, mediaSession) {
+
+ console.log("chromecast new media session ID:" + mediaSession.mediaSessionId + ' (' + how + ')');
+ this.currentMediaSession = mediaSession;
+
+ if (how === 'loadMedia') {
+ this.castPlayerState = PLAYER_STATE.PLAYING;
+ }
+
+ if (how === 'activeSession') {
+ this.castPlayerState = mediaSession.playerState;
+ }
+
+ this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler);
+ };
+
+ /**
+ * Callback function for media status update from receiver
+ * @param {!Boolean} e true/false
+ */
+ CastPlayer.prototype.onMediaStatusUpdate = function (e) {
+
+ if (e === false) {
+ this.castPlayerState = PLAYER_STATE.IDLE;
+ }
+ console.log("chromecast updating media: " + e);
+ };
+
+ /**
+ * Set media volume in Cast mode
+ * @param {Boolean} mute A boolean
+ */
+ CastPlayer.prototype.setReceiverVolume = function (mute, vol) {
+
+ if (!this.currentMediaSession) {
+ console.log('this.currentMediaSession is null');
+ return;
+ }
+
+ if (!mute) {
+
+ this.session.setReceiverVolumeLevel((vol || 1),
+ this.mediaCommandSuccessCallback.bind(this),
+ this.errorHandler);
+ }
+ else {
+ this.session.setReceiverMuted(true,
+ this.mediaCommandSuccessCallback.bind(this),
+ this.errorHandler);
+ }
+ };
+
+ /**
+ * Mute CC
+ */
+ CastPlayer.prototype.mute = function () {
+ this.setReceiverVolume(true);
+ };
+
+ /**
+ * Callback function for media command success
+ */
+ CastPlayer.prototype.mediaCommandSuccessCallback = function (info, e) {
+ console.log(info);
+ };
+
+ function chromecastPlayer() {
+
+ var self = this;
+ // Create Cast Player
+ var castPlayer;
+
+ // playbackManager needs this
+ self.name = PlayerName;
+ self.type = 'mediaplayer';
+ self.id = 'chromecast';
+ self.isLocalPlayer = false;
+
+ self.getItemsForPlayback = function (query) {
+
+ var userId = ApiClient.getCurrentUserId();
+
+ if (query.Ids && query.Ids.split(',').length === 1) {
+ return ApiClient.getItem(userId, query.Ids.split(',')).then(function (item) {
+ return {
+ Items: [item],
+ TotalRecordCount: 1
+ };
+ });
+ }
+ else {
+
+ query.Limit = query.Limit || 100;
+ query.ExcludeLocationTypes = "Virtual";
+
+ return ApiClient.getItems(userId, query);
+ }
+ };
+
+ function initializeChromecast() {
+
+ fileref.loaded = true;
+ castPlayer = new CastPlayer();
+
+ // To allow the native android app to override
+ document.dispatchEvent(new CustomEvent("chromecastloaded", {
+ detail: {
+ player: self
+ }
+ }));
+
+ events.on(castPlayer, "connect", function (e) {
+
+ if (currentResolve) {
+ sendConnectionResult(true);
+ } else {
+ playbackManager.setActivePlayer(PlayerName, self.getCurrentTargetInfo());
+ }
+
+ console.log('cc: connect');
+ // Reset this so that statechange will fire
+ self.lastPlayerData = null;
+ });
+
+ events.on(castPlayer, "playbackstart", function (e, data) {
+
+ console.log('cc: playbackstart');
+
+ castPlayer.initializeCastPlayer();
+
+ var state = self.getPlayerStateInternal(data);
+ events.trigger(self, "playbackstart", [state]);
+ });
+
+ events.on(castPlayer, "playbackstop", function (e, data) {
+
+ console.log('cc: playbackstop');
+ var state = self.getPlayerStateInternal(data);
+
+ events.trigger(self, "playbackstop", [state]);
+
+ // Reset this so the next query doesn't make it appear like content is playing.
+ self.lastPlayerData = {};
+ });
+
+ events.on(castPlayer, "playbackprogress", function (e, data) {
+
+ console.log('cc: positionchange');
+ var state = self.getPlayerStateInternal(data);
+
+ events.trigger(self, "timeupdate", [state]);
+ });
+
+ events.on(castPlayer, "volumechange", function (e, data) {
+
+ console.log('cc: volumechange');
+ var state = self.getPlayerStateInternal(data);
+ events.trigger(self, "volumechange", [state]);
+ });
+
+ events.on(castPlayer, "repeatmodechange", function (e, data) {
+
+ console.log('cc: repeatmodechange');
+ var state = self.getPlayerStateInternal(data);
+ events.trigger(self, "repeatmodechange", [state]);
+ });
+
+ events.on(castPlayer, "playstatechange", function (e, data) {
+
+ console.log('cc: playstatechange');
+ var state = self.getPlayerStateInternal(data);
+
+ events.trigger(self, "pause", [state]);
+ });
+ }
+
+ self.play = function (options) {
+
+ return ApiClient.getCurrentUser().then(function (user) {
+
+ if (options.items) {
+
+ return self.playWithCommand(options, 'PlayNow');
+
+ } else {
+
+ return self.getItemsForPlayback({
+
+ Ids: options.ids.join(',')
+
+ }).then(function (result) {
+
+ options.items = result.Items;
+ return self.playWithCommand(options, 'PlayNow');
+
+ });
+ }
+
+ });
+
+ };
+
+ self.playWithCommand = function (options, command) {
+
+ if (!options.items) {
+ var apiClient = connectionManager.getApiClient(options.serverId);
+ return apiClient.getItem(apiClient.getCurrentUserId(), options.ids[0]).then(function (item) {
+
+ options.items = [item];
+ return self.playWithCommand(options, command);
+ });
+ }
+
+ return castPlayer.loadMedia(options, command);
+ };
+
+ self.unpause = function () {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'Unpause'
+ });
+ };
+
+ self.playPause = function () {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'PlayPause'
+ });
+ };
+
+ self.pause = function () {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'Pause'
+ });
+ };
+
+ self.shuffle = function (item) {
+
+ var apiClient = connectionManager.getApiClient(item.ServerId);
+ var userId = apiClient.getCurrentUserId();
+
+ apiClient.getItem(userId, item.Id).then(function (item) {
+
+ self.playWithCommand({
+
+ items: [item]
+
+ }, 'Shuffle');
+
+ });
+
+ };
+
+ self.instantMix = function (item) {
+
+ var apiClient = connectionManager.getApiClient(item.ServerId);
+ var userId = apiClient.getCurrentUserId();
+
+ apiClient.getItem(userId, item.Id).then(function (item) {
+
+ self.playWithCommand({
+
+ items: [item]
+
+ }, 'InstantMix');
+
+ });
+
+ };
+
+ self.canPlayMediaType = function (mediaType) {
+
+ mediaType = (mediaType || '').toLowerCase();
+ return mediaType === 'audio' || mediaType === 'video';
+ };
+
+ self.canQueueMediaType = function (mediaType) {
+ return self.canPlayMediaType(mediaType);
+ };
+
+ self.queue = function (options) {
+ self.playWithCommand(options, 'PlayLast');
+ };
+
+ self.queueNext = function (options) {
+ self.playWithCommand(options, 'PlayNext');
+ };
+
+ self.stop = function () {
+ return castPlayer.sendMessage({
+ options: {},
+ command: 'Stop'
+ });
+ };
+
+ self.displayContent = function (options) {
+
+ castPlayer.sendMessage({
+ options: options,
+ command: 'DisplayContent'
+ });
+ };
+
+ self.isPlaying = function () {
+ var state = self.lastPlayerData || {};
+ return state.NowPlayingItem != null;
+ };
+
+ self.isPlayingVideo = function () {
+ var state = self.lastPlayerData || {};
+ state = state.NowPlayingItem || {};
+ return state.MediaType === 'Video';
+ };
+
+ self.isPlayingAudio = function () {
+ var state = self.lastPlayerData || {};
+ state = state.NowPlayingItem || {};
+ return state.MediaType === 'Audio';
+ };
+
+ self.currentTime = function (val) {
+
+ if (val != null) {
+ return self.seek(val);
+ }
+
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+ return state.PositionTicks;
+ };
+
+ self.duration = function () {
+ var state = self.lastPlayerData || {};
+ state = state.NowPlayingItem || {};
+ return state.RunTimeTicks;
+ };
+
+ self.paused = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+
+ return state.IsPaused;
+ };
+
+ self.isMuted = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+
+ return state.IsMuted;
+ };
+
+ self.setMute = function (isMuted) {
+
+ if (isMuted) {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'Mute'
+ });
+ } else {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'Unmute'
+ });
+ }
+ };
+
+ self.getRepeatMode = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+ return state.RepeatMode;
+ };
+
+ self.setRepeatMode = function (mode) {
+ castPlayer.sendMessage({
+ options: {
+ RepeatMode: mode
+ },
+ command: 'SetRepeatMode'
+ });
+ };
+
+ self.toggleMute = function () {
+
+ castPlayer.sendMessage({
+ options: {},
+ command: 'ToggleMute'
+ });
+ };
+
+ self.getTargets = function () {
+
+ var targets = [];
+
+ if (castPlayer.hasReceivers) {
+ targets.push(self.getCurrentTargetInfo());
+ }
+
+ return Promise.resolve(targets);
+ };
+
+ self.getCurrentTargetInfo = function () {
+
+ var appName = null;
+
+ if (castPlayer.session && castPlayer.session.receiver && castPlayer.session.receiver.friendlyName) {
+ appName = castPlayer.session.receiver.friendlyName;
+ }
+
+ return {
+ name: PlayerName,
+ id: PlayerName,
+ playerName: PlayerName,
+ playableMediaTypes: ["Audio", "Video"],
+ isLocalPlayer: false,
+ appName: PlayerName,
+ deviceName: appName,
+ supportedCommands: ["VolumeUp",
+ "VolumeDown",
+ "Mute",
+ "Unmute",
+ "ToggleMute",
+ "SetVolume",
+ "SetAudioStreamIndex",
+ "SetSubtitleStreamIndex",
+ "DisplayContent",
+ "SetRepeatMode",
+ "EndSession"]
+ };
+ };
+
+ self.seek = function (position) {
+
+ position = parseInt(position);
+
+ position = position / 10000000;
+
+ castPlayer.sendMessage({
+ options: {
+ position: position
+ },
+ command: 'Seek'
+ });
+ };
+
+ self.audioTracks = function () {
+ var state = self.lastPlayerData || {};
+ state = state.NowPlayingItem || {};
+ var streams = state.MediaStreams || [];
+ return streams.filter(function (s) {
+ return s.Type === 'Audio';
+ });
+ };
+
+ self.setAudioStreamIndex = function (index) {
+ castPlayer.sendMessage({
+ options: {
+ index: index
+ },
+ command: 'SetAudioStreamIndex'
+ });
+ };
+
+ self.getAudioStreamIndex = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+ return state.AudioStreamIndex;
+ };
+
+ self.subtitleTracks = function () {
+ var state = self.lastPlayerData || {};
+ state = state.NowPlayingItem || {};
+ var streams = state.MediaStreams || [];
+ return streams.filter(function (s) {
+ return s.Type === 'Subtitle';
+ });
+ };
+
+ self.getSubtitleStreamIndex = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+ return state.SubtitleStreamIndex;
+ };
+
+ self.setSubtitleStreamIndex = function (index) {
+ castPlayer.sendMessage({
+ options: {
+ index: index
+ },
+ command: 'SetSubtitleStreamIndex'
+ });
+ };
+
+ self.getMaxStreamingBitrate = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+ return state.MaxStreamingBitrate;
+ };
+
+ self.setMaxStreamingBitrate = function (bitrate) {
+ castPlayer.sendMessage({
+ options: {
+ bitrate: bitrate
+ },
+ command: 'SetMaxStreamingBitrate'
+ });
+ };
+
+ self.isFullscreen = function () {
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+ return state.IsFullscreen;
+ };
+
+ self.toggleFullscreen = function () {
+ // not supported
+ };
+
+ self.nextTrack = function () {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'NextTrack'
+ });
+ };
+
+ self.previousTrack = function () {
+ castPlayer.sendMessage({
+ options: {},
+ command: 'PreviousTrack'
+ });
+ };
+
+ self.beginPlayerUpdates = function () {
+ // Setup polling here
+ };
+
+ self.endPlayerUpdates = function () {
+ // Stop polling here
+ };
+
+ self.getVolume = function () {
+
+ var state = self.lastPlayerData || {};
+ state = state.PlayState || {};
+
+ return state.VolumeLevel == null ? 100 : state.VolumeLevel;
+ };
+
+ self.volumeDown = function () {
+
+ castPlayer.sendMessage({
+ options: {},
+ command: 'VolumeDown'
+ });
+ };
+
+ self.endSession = function () {
+
+ self.stop().then(function () {
+ setTimeout(function () {
+ castPlayer.stopApp();
+ }, 1000);
+ });
+ };
+
+ self.volumeUp = function () {
+
+ castPlayer.sendMessage({
+ options: {},
+ command: 'VolumeUp'
+ });
+ };
+
+ self.setVolume = function (vol) {
+
+ vol = Math.min(vol, 100);
+ vol = Math.max(vol, 0);
+
+ //castPlayer.setReceiverVolume(false, (vol / 100));
+ castPlayer.sendMessage({
+ options: {
+ volume: vol
+ },
+ command: 'SetVolume'
+ });
+ };
+
+ self.getPlaylist = function () {
+ return Promise.resolve([]);
+ };
+
+ self.getCurrentPlaylistItemId = function () {
+ };
+
+ self.setCurrentPlaylistItem = function (playlistItemId) {
+ return Promise.resolve();
+ };
+
+ self.removeFromPlaylist = function (playlistItemIds) {
+ return Promise.resolve();
+ };
+
+ self.getPlayerState = function () {
+
+ return Promise.resolve(self.getPlayerStateInternal() || {});
+ };
+
+ function normalizeImages(state) {
+
+ if (state && state.NowPlayingItem) {
+
+ var item = state.NowPlayingItem;
+
+ if (!item.ImageTags || !item.ImageTags.Primary) {
+ if (item.PrimaryImageTag) {
+ item.ImageTags = item.ImageTags || {};
+ item.ImageTags.Primary = item.PrimaryImageTag;
+ }
+ }
+ if (item.BackdropImageTag && item.BackdropItemId === item.Id) {
+ item.BackdropImageTags = [item.BackdropImageTag];
+ }
+ if (item.BackdropImageTag && item.BackdropItemId !== item.Id) {
+ item.ParentBackdropImageTags = [item.BackdropImageTag];
+ item.ParentBackdropItemId = item.BackdropItemId;
+ }
+ }
+ }
+
+ self.lastPlayerData = {};
+
+ self.getPlayerStateInternal = function (data) {
+
+ var triggerStateChange = false;
+ if (data && !self.lastPlayerData) {
+ triggerStateChange = true;
+ }
+
+ data = data || self.lastPlayerData;
+ self.lastPlayerData = data;
+
+ normalizeImages(data);
+
+ //console.log(JSON.stringify(data));
+
+ if (triggerStateChange) {
+ events.trigger(self, "statechange", [data]);
+ }
+
+ return data;
+ };
+
+ self.tryPair = function (target) {
+
+ if (castPlayer.deviceState !== DEVICE_STATE.ACTIVE && castPlayer.isInitialized) {
+
+ return new Promise(function (resolve, reject) {
+ currentResolve = resolve;
+ currentReject = reject;
+
+ castPlayer.launchApp();
+ });
+ } else {
+
+ currentResolve = null;
+ currentReject = null;
+
+ return Promise.reject();
+ }
+ };
+
+ if (fileref.loaded) {
+ initializeChromecast();
+ } else {
+ fileref.onload = initializeChromecast;
+ }
+ }
+
+ var fileref = document.createElement('script');
+ fileref.setAttribute("type", "text/javascript");
+ fileref.onload = function () {
+ fileref.loaded = true;
+ };
+ fileref.setAttribute("src", "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js");
+ document.querySelector('head').appendChild(fileref);
+
+ return chromecastPlayer;
});
\ No newline at end of file
diff --git a/dashboard-ui/bower_components/emby-webcomponents/dialoghelper/dialoghelper.js b/dashboard-ui/bower_components/emby-webcomponents/dialoghelper/dialoghelper.js
index 5a4d9c2cfe..96d0ce5319 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/dialoghelper/dialoghelper.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/dialoghelper/dialoghelper.js
@@ -10,6 +10,10 @@
return false;
}
+ if (browser.noAnimation) {
+ return false;
+ }
+
return browser.supportsCssAnimation();
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/emby-button/emby-button.css b/dashboard-ui/bower_components/emby-webcomponents/emby-button/emby-button.css
index 1627a363d3..913eaddda1 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/emby-button/emby-button.css
+++ b/dashboard-ui/bower_components/emby-webcomponents/emby-button/emby-button.css
@@ -54,8 +54,8 @@
.emby-button > i {
/* For non-fab buttons that have icons */
font-size: 1.36em;
- width: auto;
- height: auto;
+ width: 1em;
+ height: 1em;
}
.fab {
@@ -72,8 +72,8 @@
}
.fab > i {
- height: auto;
- width: auto;
+ height: 1em;
+ width: 1em;
vertical-align: middle;
font-size: 2.85em;
}
@@ -88,8 +88,8 @@
}
.fab.mini > i {
- height: auto;
- width: auto;
+ height: 1em;
+ width: 1em;
font-size: 1.72em;
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.css b/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.css
index 03a6ea757d..3d784a22b0 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.css
+++ b/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.css
@@ -127,3 +127,13 @@
opacity: .7;
margin-bottom: 0;
}
+
+@-webkit-keyframes repaintChrome {
+ from {
+ padding: 0;
+ }
+
+ to {
+ padding: 0;
+ }
+}
\ No newline at end of file
diff --git a/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.js b/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.js
index 93eb7e78ea..c1ffe36205 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/emby-checkbox/emby-checkbox.js
@@ -1,4 +1,4 @@
-define(['css!./emby-checkbox', 'registerElement'], function () {
+define(['browser', 'dom', 'css!./emby-checkbox', 'registerElement'], function (browser, dom) {
'use strict';
var EmbyCheckboxPrototype = Object.create(HTMLInputElement.prototype);
@@ -19,6 +19,22 @@
}
}
+ var enableRefreshHack = browser.tizen || browser.orsay || browser.operaTv || browser.web0s ? true : false;
+
+ function forceRefresh(loading) {
+
+ var elem = this.parentNode;
+
+ elem.style.webkitAnimationName = 'repaintChrome';
+ elem.style.webkitAnimationDelay = (loading === true ? '500ms' : '');
+ elem.style.webkitAnimationDuration = '10ms';
+ elem.style.webkitAnimationIterationCount = '1';
+
+ setTimeout(function () {
+ elem.style.webkitAnimationName = '';
+ }, (loading === true ? 520 : 20));
+ }
+
EmbyCheckboxPrototype.attachedCallback = function () {
if (this.getAttribute('data-embycheckbox') === 'true') {
@@ -47,10 +63,27 @@
labelTextElement.classList.add('checkboxLabel');
this.addEventListener('keydown', onKeyDown);
+
+ if (enableRefreshHack) {
+
+ forceRefresh.call(this, true);
+ dom.addEventListener(this, 'click', forceRefresh, {
+ passive: true
+ });
+ dom.addEventListener(this, 'blur', forceRefresh, {
+ passive: true
+ });
+ dom.addEventListener(this, 'focus', forceRefresh, {
+ passive: true
+ });
+ dom.addEventListener(this, 'change', forceRefresh, {
+ passive: true
+ });
+ }
};
document.registerElement('emby-checkbox', {
prototype: EmbyCheckboxPrototype,
extends: 'input'
});
-});
\ No newline at end of file
+});
diff --git a/dashboard-ui/bower_components/emby-webcomponents/emby-itemscontainer/emby-itemscontainer.js b/dashboard-ui/bower_components/emby-webcomponents/emby-itemscontainer/emby-itemscontainer.js
index 313d93fb7f..2a52ea67ca 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/emby-itemscontainer/emby-itemscontainer.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/emby-itemscontainer/emby-itemscontainer.js
@@ -107,20 +107,35 @@
function onDrop(evt, itemsContainer) {
-
- loading.show();
-
var el = evt.item;
var newIndex = evt.newIndex;
var itemId = el.getAttribute('data-playlistitemid');
var playlistId = el.getAttribute('data-playlistid');
+ if (!playlistId) {
+
+ var oldIndex = evt.oldIndex;
+
+ el.dispatchEvent(new CustomEvent('itemdrop', {
+ detail: {
+ oldIndex: oldIndex,
+ newIndex: newIndex,
+ playlistItemId: itemId
+ },
+ bubbles: true,
+ cancelable: false
+ }));
+ return;
+ }
+
var serverId = el.getAttribute('data-serverid');
var apiClient = connectionManager.getApiClient(serverId);
newIndex = Math.max(0, newIndex - 1);
+ loading.show();
+
apiClient.ajax({
url: apiClient.getUrl('Playlists/' + playlistId + '/Items/' + itemId + '/Move/' + newIndex),
@@ -129,7 +144,6 @@
}).then(function () {
- el.setAttribute('data-index', newIndex);
loading.hide();
}, function () {
@@ -171,7 +185,7 @@
// dragging ended
onEnd: function (/**Event*/evt) {
- onDrop(evt, self);
+ return onDrop(evt, self);
}
});
});
@@ -237,6 +251,11 @@
}
}
+ ItemsContainerProtoType.createdCallback = function () {
+
+ this.classList.add('itemsContainer');
+ };
+
ItemsContainerProtoType.attachedCallback = function () {
this.addEventListener('click', onClick);
@@ -264,6 +283,10 @@
addNotificationEvent(this, 'SeriesTimerCreated', onSeriesTimerCreated);
addNotificationEvent(this, 'TimerCancelled', onTimerCancelled);
addNotificationEvent(this, 'SeriesTimerCancelled', onSeriesTimerCancelled);
+
+ if (this.getAttribute('data-dragreorder') === 'true') {
+ this.enableDragReordering(true);
+ }
};
ItemsContainerProtoType.detachedCallback = function () {
diff --git a/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.css b/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.css
index da14f584aa..c080aef980 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.css
+++ b/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.css
@@ -16,7 +16,7 @@ _:-ms-input-placeholder, :root .mdl-slider {
-ms-user-select: none;
user-select: none;
outline: 0;
- padding: 1.5em 0;
+ padding: 1em 0;
color: #52B54B;
-webkit-align-self: center;
-ms-flex-item-align: center;
@@ -220,9 +220,9 @@ _:-ms-input-placeholder, :root .mdl-slider {
.sliderBubble {
position: absolute;
- top: -3.7em;
+ top: 0;
left: 0;
- padding: .5em 1em;
+ transform: translate3d(-48%, -120%, 0);
background: #282828;
color: #fff;
display: flex;
@@ -232,4 +232,5 @@ _:-ms-input-placeholder, :root .mdl-slider {
.sliderBubbleText {
margin: 0;
+ padding: .5em .75em;
}
diff --git a/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.js b/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.js
index 831a62d6f4..7b0625646a 100644
--- a/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.js
+++ b/dashboard-ui/bower_components/emby-webcomponents/emby-slider/emby-slider.js
@@ -38,21 +38,29 @@
function updateBubble(range, value, bubble, bubbleText) {
- bubble.style.left = (value - 1) + '%';
+ bubble.style.left = value + '%';
- if (range.getBubbleText) {
- value = range.getBubbleText(value);
+ if (range.getBubbleHtml) {
+ value = range.getBubbleHtml(value);
+ } else {
+ if (range.getBubbleText) {
+ value = range.getBubbleText(value);
+ } else {
+ value = Math.round(value);
+ }
+ value = '