using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Weather; using MediaBrowser.Model.Web; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.ApiInteraction { /// /// Provides api methods centered around an HttpClient /// public class ApiClient : BaseApiClient { /// /// Gets the HTTP client. /// /// The HTTP client. protected IAsyncHttpClient HttpClient { get; private set; } /// /// Initializes a new instance of the class. /// /// The logger. /// The HTTP client. /// httpClient public ApiClient(ILogger logger, IAsyncHttpClient httpClient) : base(logger) { if (httpClient == null) { throw new ArgumentNullException("httpClient"); } HttpClient = httpClient; } /// /// Sets the authorization header. /// /// The header. protected override void SetAuthorizationHeader(string header) { HttpClient.SetAuthorizationHeader(header); } /// /// Gets an image stream based on a url /// /// The URL. /// Task{Stream}. /// url public Task GetImageStreamAsync(string url) { if (string.IsNullOrEmpty(url)) { throw new ArgumentNullException("url"); } return HttpClient.GetStreamAsync(url, Logger, CancellationToken.None); } /// /// Gets a BaseItem /// /// The id. /// The user id. /// Task{BaseItemDto}. /// id public async Task GetItemAsync(string id, Guid userId) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/Items/" + id); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the intros async. /// /// The item id. /// The user id. /// Task{System.String[]}. /// id public async Task GetIntrosAsync(string itemId, Guid userId) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/Items/" + itemId + "/Intros"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a BaseItem /// /// The user id. /// Task{BaseItemDto}. /// userId public async Task GetRootFolderAsync(Guid userId) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/Items/Root"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets all Users /// /// Task{UserDto[]}. public async Task GetAllUsersAsync() { var url = GetApiUrl("Users"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Queries for items /// /// The query. /// Task{ItemsResult}. /// query public async Task GetItemsAsync(ItemQuery query) { if (query == null) { throw new ArgumentNullException("query"); } var url = GetItemListUrl(query); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets all People /// /// The user id. /// Optional itemId, to localize the search to a specific item or folder /// Use this to limit results to specific person types /// Used to skip over a given number of items. Use if paging. /// The maximum number of items to return /// The sort order /// if set to true items will be searched recursively. /// Task{IbnItemsResult}. /// userId public async Task GetAllPeopleAsync( Guid userId, string itemId = null, IEnumerable personTypes = null, int? startIndex = null, int? limit = null, SortOrder? sortOrder = null, bool recursive = false) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var dict = new QueryStringDictionary(); dict.AddIfNotNull("startIndex", startIndex); dict.AddIfNotNull("limit", limit); dict.Add("recursive", recursive); if (sortOrder.HasValue) { dict["sortOrder"] = sortOrder.Value.ToString(); } dict.AddIfNotNull("personTypes", personTypes); var url = string.IsNullOrEmpty(itemId) ? "Users/" + userId + "/Items/Root/Persons" : "Users/" + userId + "/Items/" + itemId + "/Persons"; url = GetApiUrl(url, dict); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a studio /// /// The name. /// Task{BaseItemDto}. /// userId public async Task GetStudioAsync(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } var url = GetApiUrl("Library/Studios/" + name); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a genre /// /// The name. /// Task{BaseItemDto}. /// userId public async Task GetGenreAsync(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } var url = GetApiUrl("Library/Genres/" + name); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Restarts the kernel or the entire server if necessary /// If the server application is restarting this request will fail to return, even if /// the operation is successful. /// /// Task. public Task PerformPendingRestartAsync() { var url = GetApiUrl("System/Restart"); return PostAsync(url, new QueryStringDictionary()); } /// /// Gets the system status async. /// /// Task{SystemInfo}. public async Task GetSystemInfoAsync() { var url = GetApiUrl("System/Info"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a person /// /// The name. /// Task{BaseItemDto}. /// userId public async Task GetPersonAsync(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } var url = GetApiUrl("Library/Persons/" + name); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a year /// /// The year. /// Task{BaseItemDto}. /// userId public async Task GetYearAsync(int year) { var url = GetApiUrl("Library/Years/" + year); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a list of plugins installed on the server /// /// Task{PluginInfo[]}. public async Task GetInstalledPluginsAsync() { var url = GetApiUrl("Plugins"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets a list of plugins installed on the server /// /// The plugin. /// Task{Stream}. /// plugin public Task GetPluginAssemblyAsync(PluginInfo plugin) { if (plugin == null) { throw new ArgumentNullException("plugin"); } var url = GetApiUrl("Plugins/" + plugin.Id + "/Assembly"); return HttpClient.GetStreamAsync(url, Logger, CancellationToken.None); } /// /// Gets the current server configuration /// /// Task{ServerConfiguration}. public async Task GetServerConfigurationAsync() { var url = GetApiUrl("System/Configuration"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the scheduled tasks. /// /// Task{TaskInfo[]}. public async Task GetScheduledTasksAsync() { var url = GetApiUrl("ScheduledTasks"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the scheduled task async. /// /// The id. /// Task{TaskInfo}. /// id public async Task GetScheduledTaskAsync(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } var url = GetApiUrl("ScheduledTasks/" + id); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the plugin configuration file in plain text. /// /// The plugin id. /// Task{Stream}. /// assemblyFileName public async Task GetPluginConfigurationFileAsync(Guid pluginId) { if (pluginId == Guid.Empty) { throw new ArgumentNullException("pluginId"); } var url = GetApiUrl("Plugins/" + pluginId + "/ConfigurationFile"); return await HttpClient.GetStreamAsync(url, Logger, CancellationToken.None).ConfigureAwait(false); } /// /// Gets a user by id /// /// The id. /// Task{UserDto}. /// id public async Task GetUserAsync(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } var url = GetApiUrl("Users/" + id); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the parental ratings async. /// /// Task{List{ParentalRating}}. public async Task> GetParentalRatingsAsync() { var url = GetApiUrl("Localization/ParentalRatings"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream>(stream); } } /// /// Gets weather information for the default location as set in configuration /// /// Task{WeatherInfo}. public async Task GetWeatherInfoAsync() { var url = GetApiUrl("Weather"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets weather information for a specific location /// Location can be a US zipcode, or "city,state", "city,state,country", "city,country" /// It can also be an ip address, or "latitude,longitude" /// /// The location. /// Task{WeatherInfo}. /// location public async Task GetWeatherInfoAsync(string location) { if (string.IsNullOrEmpty(location)) { throw new ArgumentNullException("location"); } var dict = new QueryStringDictionary(); dict.Add("location", location); var url = GetApiUrl("Weather", dict); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets local trailers for an item /// /// The user id. /// The item id. /// Task{ItemsResult}. /// query public async Task GetLocalTrailersAsync(Guid userId, string itemId) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } var url = GetApiUrl("Users/" + userId + "/Items/" + itemId + "/LocalTrailers"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets special features for an item /// /// The user id. /// The item id. /// Task{BaseItemDto[]}. /// userId public async Task GetSpecialFeaturesAsync(Guid userId, string itemId) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } var url = GetApiUrl("Users/" + userId + "/Items/" + itemId + "/SpecialFeatures"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the cultures async. /// /// Task{CultureDto[]}. public async Task GetCulturesAsync() { var url = GetApiUrl("Localization/Cultures"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Gets the countries async. /// /// Task{CountryInfo[]}. public async Task GetCountriesAsync() { var url = GetApiUrl("Localization/Countries"); using (var stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Marks an item as played or unplayed. /// This should not be used to update playstate following playback. /// There are separate playstate check-in methods for that. This should be used for a /// separate option to reset playstate. /// /// The item id. /// The user id. /// if set to true [was played]. /// Task. /// itemId public Task UpdatePlayedStatusAsync(string itemId, Guid userId, bool wasPlayed) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/PlayedItems/" + itemId); if (wasPlayed) { return PostAsync(url, new Dictionary()); } return HttpClient.DeleteAsync(url, Logger, CancellationToken.None); } /// /// Updates the favorite status async. /// /// The item id. /// The user id. /// if set to true [is favorite]. /// Task. /// itemId public Task UpdateFavoriteStatusAsync(string itemId, Guid userId, bool isFavorite) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/FavoriteItems/" + itemId); if (isFavorite) { return PostAsync(url, new Dictionary()); } return HttpClient.DeleteAsync(url, Logger, CancellationToken.None); } /// /// Reports to the server that the user has begun playing an item /// /// The item id. /// The user id. /// Task{UserItemDataDto}. /// itemId public Task ReportPlaybackStartAsync(string itemId, Guid userId) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var dict = new QueryStringDictionary(); dict.Add("id", itemId); dict.Add("userId", userId); dict.Add("type", "start"); var url = GetApiUrl("playbackcheckin", dict); return PostAsync(url, new Dictionary()); } /// /// Reports playback progress to the server /// /// The item id. /// The user id. /// The position ticks. /// Task{UserItemDataDto}. /// itemId public Task ReportPlaybackProgressAsync(string itemId, Guid userId, long? positionTicks) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var dict = new QueryStringDictionary(); dict.Add("id", itemId); dict.Add("userId", userId); dict.Add("type", "progress"); dict.AddIfNotNull("positionTicks", positionTicks); var url = GetApiUrl("playbackcheckin", dict); return PostAsync(url, new Dictionary()); } /// /// Reports to the server that the user has stopped playing an item /// /// The item id. /// The user id. /// The position ticks. /// Task{UserItemDataDto}. /// itemId public Task ReportPlaybackStoppedAsync(string itemId, Guid userId, long? positionTicks) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var dict = new QueryStringDictionary(); dict.Add("id", itemId); dict.Add("userId", userId); dict.Add("type", "stopped"); dict.AddIfNotNull("positionTicks", positionTicks); var url = GetApiUrl("playbackcheckin", dict); return PostAsync(url, new Dictionary()); } /// /// Clears a user's rating for an item /// /// The item id. /// The user id. /// Task{UserItemDataDto}. /// itemId public Task ClearUserItemRatingAsync(string itemId, Guid userId) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/Items/" + itemId + "/Rating"); return HttpClient.DeleteAsync(url, Logger, CancellationToken.None); } /// /// Updates a user's rating for an item, based on likes or dislikes /// /// The item id. /// The user id. /// if set to true [likes]. /// Task{UserItemDataDto}. /// itemId public Task UpdateUserItemRatingAsync(string itemId, Guid userId, bool likes) { if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var dict = new QueryStringDictionary { }; dict.Add("likes", likes); var url = GetApiUrl("Users/" + userId + "/Items/" + itemId + "/Rating", dict); return PostAsync(url, new Dictionary()); } /// /// Authenticates a user and returns the result /// /// The user id. /// The password. /// userId public Task AuthenticateUserAsync(Guid userId, string password) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } var url = GetApiUrl("Users/" + userId + "/Authenticate"); var args = new Dictionary(); if (!string.IsNullOrEmpty(password)) { args["password"] = password; } return PostAsync(url, args); } /// /// Uploads the user image async. /// /// The user id. /// Type of the image. /// The image. /// Task{RequestResult}. /// public Task UploadUserImageAsync(Guid userId, ImageType imageType, Stream image) { // Implement when needed throw new NotImplementedException(); } /// /// Updates the server configuration async. /// /// The configuration. /// Task. /// configuration public Task UpdateServerConfigurationAsync(ServerConfiguration configuration) { if (configuration == null) { throw new ArgumentNullException("configuration"); } var url = GetApiUrl("System/Configuration"); return PostAsync(url, configuration); } /// /// Updates the scheduled task triggers. /// /// The id. /// The triggers. /// Task{RequestResult}. /// id public Task UpdateScheduledTaskTriggersAsync(Guid id, TaskTriggerInfo[] triggers) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } if (triggers == null) { throw new ArgumentNullException("triggers"); } var url = GetApiUrl("ScheduledTasks/" + id + "/Triggers"); return PostAsync(url, triggers); } /// /// Adds a virtual folder to either the default view or a user view /// /// The name. /// The user id. /// Task{RequestResult}. /// name public Task AddVirtualFolderAsync(string name, Guid? userId = null) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } var dict = new QueryStringDictionary(); dict.Add("name", name); dict.Add("action", "AddVirtualFolder"); dict.AddIfNotNull("userId", userId); var url = GetApiUrl("UpdateMediaLibrary", dict); return PostAsync(url, new Dictionary()); } /// /// Removes a virtual folder, within either the default view or a user view /// /// The name. /// The user id. /// Task. /// name public Task RemoveVirtualFolderAsync(string name, Guid? userId = null) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } var dict = new QueryStringDictionary(); dict.Add("name", name); dict.Add("action", "RemoveVirtualFolder"); dict.AddIfNotNull("userId", userId); var url = GetApiUrl("UpdateMediaLibrary", dict); return PostAsync(url, new Dictionary()); } /// /// Renames a virtual folder, within either the default view or a user view /// /// The name. /// The new name. /// The user id. /// Task. /// name public Task RenameVirtualFolderAsync(string name, string newName, Guid? userId = null) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } if (string.IsNullOrEmpty(newName)) { throw new ArgumentNullException("newName"); } var dict = new QueryStringDictionary(); dict.Add("name", name); dict.Add("newName", newName); dict.Add("action", "RenameVirtualFolder"); dict.AddIfNotNull("userId", userId); var url = GetApiUrl("UpdateMediaLibrary", dict); return PostAsync(url, new Dictionary()); } /// /// Adds a media path to a virtual folder, within either the default view or a user view /// /// Name of the virtual folder. /// The media path. /// The user id. /// Task. /// virtualFolderName public Task AddMediaPathAsync(string virtualFolderName, string mediaPath, Guid? userId = null) { if (string.IsNullOrEmpty(virtualFolderName)) { throw new ArgumentNullException("virtualFolderName"); } if (string.IsNullOrEmpty(mediaPath)) { throw new ArgumentNullException("mediaPath"); } var dict = new QueryStringDictionary(); dict.Add("virtualFolderName", virtualFolderName); dict.Add("mediaPath", mediaPath); dict.Add("action", "AddMediaPath"); dict.AddIfNotNull("userId", userId); var url = GetApiUrl("UpdateMediaLibrary", dict); return PostAsync(url, new Dictionary()); } /// /// Removes a media path from a virtual folder, within either the default view or a user view /// /// Name of the virtual folder. /// The media path. /// The user id. /// Task. /// virtualFolderName public Task RemoveMediaPathAsync(string virtualFolderName, string mediaPath, Guid? userId = null) { if (string.IsNullOrEmpty(virtualFolderName)) { throw new ArgumentNullException("virtualFolderName"); } if (string.IsNullOrEmpty(mediaPath)) { throw new ArgumentNullException("mediaPath"); } var dict = new QueryStringDictionary(); dict.Add("virtualFolderName", virtualFolderName); dict.Add("mediaPath", mediaPath); dict.Add("action", "RemoveMediaPath"); dict.AddIfNotNull("userId", userId); var url = GetApiUrl("UpdateMediaLibrary", dict); return PostAsync(url, new Dictionary()); } /// /// Updates display preferences for a user /// /// The user id. /// The item id. /// The display preferences. /// Task{DisplayPreferences}. /// userId public Task UpdateDisplayPreferencesAsync(Guid userId, string itemId, DisplayPreferences displayPreferences) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } if (displayPreferences == null) { throw new ArgumentNullException("displayPreferences"); } var url = GetApiUrl("Users/" + userId + "/Items/" + itemId + "/DisplayPreferences"); return PostAsync(url, displayPreferences); } /// /// Posts a set of data to a url, and deserializes the return stream into T /// /// /// The URL. /// The args. /// Task{``0}. private Task PostAsync(string url, Dictionary args) where T : class { return PostAsync(url, args, SerializationFormat); } /// /// Posts a set of data to a url, and deserializes the return stream into T /// /// /// The URL. /// The args. /// The serialization format. /// Task{``0}. private async Task PostAsync(string url, Dictionary args, SerializationFormats serializationFormat) where T : class { url = AddDataFormat(url, serializationFormat); // Create the post body var strings = args.Keys.Select(key => string.Format("{0}={1}", key, args[key])); var postContent = string.Join("&", strings.ToArray()); const string contentType = "application/x-www-form-urlencoded"; using (var stream = await HttpClient.PostAsync(url, contentType, postContent, Logger, CancellationToken.None).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// Posts an object of type TInputType to a given url, and deserializes the response into an object of type TOutputType /// /// The type of the T input type. /// The type of the T output type. /// The URL. /// The obj. /// Task{``1}. private Task PostAsync(string url, TInputType obj) where TOutputType : class { return PostAsync(url, obj, SerializationFormat); } /// /// Posts an object of type TInputType to a given url, and deserializes the response into an object of type TOutputType /// /// The type of the T input type. /// The type of the T output type. /// The URL. /// The obj. /// The serialization format. /// Task{``1}. private async Task PostAsync(string url, TInputType obj, SerializationFormats serializationFormat) where TOutputType : class { url = AddDataFormat(url, serializationFormat); const string contentType = "application/x-www-form-urlencoded"; var postContent = DataSerializer.SerializeToJsonString(obj); using (var stream = await HttpClient.PostAsync(url, contentType, postContent, Logger, CancellationToken.None).ConfigureAwait(false)) { return DeserializeFromStream(stream); } } /// /// This is a helper around getting a stream from the server that contains serialized data /// /// The URL. /// Task{Stream}. public Task GetSerializedStreamAsync(string url) { return GetSerializedStreamAsync(url, SerializationFormat); } /// /// This is a helper around getting a stream from the server that contains serialized data /// /// The URL. /// The serialization format. /// Task{Stream}. public Task GetSerializedStreamAsync(string url, SerializationFormats serializationFormat) { url = AddDataFormat(url, serializationFormat); return HttpClient.GetStreamAsync(url, Logger, CancellationToken.None); } /// /// Adds the data format. /// /// The URL. /// The serialization format. /// System.String. private string AddDataFormat(string url, SerializationFormats serializationFormat) { var format = serializationFormat == SerializationFormats.Protobuf ? "x-protobuf" : serializationFormat.ToString(); if (url.IndexOf('?') == -1) { url += "?format=" + format; } else { url += "&format=" + format; } return url; } } }