iOS: Get file's metadata - ios

I have an mp3 file on a server. I want to get this file's information like what's the size of this file, what's the artists name, what's the album name, when was the file created, when was it modified, etc. I want all this information.
Is it possible to get this information without actually downloading the whole file? Using NSURLConnection or otherwise?
EDIT:
The following code doesn't give me the required information, i.e. file created by, artist name, etc
NSError *rerror = nil;
NSURLResponse *response = nil;
NSURL *url = [NSURL URLWithString:#"http://link.to.mp3"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"HEAD"];
NSData *result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&rerror];
NSString *resultString = [[[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding] autorelease];
NSLog(#"URL: %#", url);
NSLog(#"Request: %#", request);
NSLog(#"Result (NSData): %#", result);
NSLog(#"Result (NSString): %#", resultString);
NSLog(#"Response: %#", response);
NSLog(#"Error: %#", rerror);
if ([response isMemberOfClass:[NSHTTPURLResponse class]]) {
NSLog(#"AllHeaderFields: %#", [((NSHTTPURLResponse *)response) allHeaderFields]);
}
The "AllHeaderFields" is:
AllHeaderFields: {
"Cache-Control" = "max-age=0";
Connection = "keep-alive";
"Content-Encoding" = gzip;
"Content-Type" = "text/plain; charset=ascii";
Date = "Fri, 17 Feb 2012 12:44:59 GMT";
Etag = 19202n;
Pragma = public;
Server = dbws;
"x-robots-tag" = "noindex,nofollow";
}

It is quite possible to get the ID3 information embedded in an MP3 file (artist name, track title) without downloading the whole file or using low-level APIs. The functionality is part of the AVFoundation framework.
The class to look at is AVAsset and specifically it's network friendly subclass AVURLAsset. AVAsset has an NSArray property named commonMetadata. This commonMetadata property will contain instances of AVMetadataItem, assuming of course that the reference URL contains metadata. You will usually use the AVMetadataItem's commonKey property to reference the item. I find this method of iterating through an array checking commonKeys irritating so I create an NSDictionary using the commonKey property as the key and the value property as the object. Like so:
-(NSDictionary *)dictionaryOfMetadataFromAsset:(AVAsset *)asset{
NSMutableDictionary *metaData = [[NSMutableDictionary alloc] init];
for (AVMetadataItem *item in asset.commonMetadata) {
if (item.value && item.commonKey){
[metaData setObject:item.value forKey:item.commonKey];
}
}
return [metaData copy];
}
With the addition of this simple method the AVAsset's metadata becomes quite easy to use. Here is an example of getting an MP3's metadata through a URL:
NSURL *mp3URL = [NSURL URLWithString:#"http://'AddressOfMP3File'"];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:mp3URL options:nil];
NSDictionary *metaDict = [self dictionaryOfMetadataFromAsset:asset];
NSLog(#"Available Metadata :%#",metaDict.allKeys);
NSLog(#"title:%#",[metaDict objectForKey:#"title"]);
I have found that this code seems to load just the first few seconds of your MP3 file. Also note that this code is synchronous; So use with caution. But AVURLAsset does have some async functionality described in the docs.
Once you have the AVAsset you can create a AVPlayerItem with it and feed that to an AVPlayer and play it, or not.

Yes and no. Things like the file size and modification date often come as part of the HEAD response. But not always: with a lot of dynamic URLs, you won't get all of the information.
As for the artist and album name, they're part of the MP3's ID3, which is contained inside the file, and so you won't be able to get them with a HEAD request. Since the ID3 tag is typically at the beginning of a file, you could try to grab just that part and then read the ID3 tag. But you won't be able to do it with NSURLConnection since it doesn't support just fetching part of a file, so you'll need to find a more low-level way of getting data by HTTP.

Yep, you're right on target with NSURLConnection.
I think you want to send a HEAD request for the resource you want information about and then check the information you receive in connection:didReceiveResponse: and connection:didReceiveData:
Edit
Admittedly I didn't read your question in its entirety. It won't be possible to get ID3 information, but you should be able to get size of file and maybe creation date etc.
This answer does give some good information about how to get the ID3 information. You'd need to set up a php page to examine the mp3 file server-side and return just that information you require instead of the entire mp3.

Related

NSString from NSData returns nil

So I am using Parse (which is pretty sweet) and I'm in the process of downloading files (short video files - no more then 1mb) from the parse server to my application to play. Now the way it works is (via documentation)..
PFFile* videoFile = [[tempArray objectAtIndex:i] objectForKey:#"track"];
[videoFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
NSString* dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSURL* videoURL = [NSURL URLWithString:dataString];
// now do something with this videoURL (i.e. play it!)
[data writeToFile:#"trackFile" atomically:YES];
NSURL *filePath = [NSURL fileURLWithPath:#"trackFile"];
NSLog(#"File Path: %#",filePath);
AVAsset* asset = [AVAsset assetWithURL:filePath];
AVPlayerItem* playerItem = [[AVPlayerItem alloc] initWithAsset:asset];
}
}
On download completion you are suppossed to create a string from the data and then a url from the string. Only problem is - the dataString always returns NULL/nil. I have confirmed that the data property is not empty and does in fact hold the video data. Why is this happening? Any help would be greatly appreciated. Thanks in advance!
I have confirmed that the data property is not empty and does in fact hold the video data.
Video data is not a UTF-8 string. It's definitely not a UTF-8 string representation of an URL. So when you say "interpret this video data as UTF-8," Cocoa rightly responds that it is not UTF-8 (because it's video data).
The simplest solution is to write this to disk, and then play the file.
Couple things that need to be determined first:
does the NSData object actually have any data, i.e., data.length > 0?
does the NSData object hold video data OR the url of the video data (not exactly clear)?
Making the assumption that the NSData object is holding the url and it's length is greater than 0, then you might want to try:
NSString* dataString = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];

Updating Plist file via download iOS

I've got a quiz app on the Apple app store. The questions are stored in a Plist file. What I'm looking to do is find a way to update the Plist file via downloading a new version and not having to submit an update every time I have new questions to add
Does anyone know of a decent tutorial which may help me?
Many thanks.
I'm not sure about tutorials, but the steps to achieve what you describe are pretty simple:
Create a url request to your remote data
Parse the returned data
Write the parsed data to a new local plist
Eg:
// Create a NSURL to your data
NSString *dataPath = #"www.mydata.com/path";
NSURL *dataURL = [NSURL URLWithString:dataPath];
// Create an asycnhronous request along with a block to be executed when complete
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:dataURL]
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (error) {
NSLog(#"%#", error.localizedDescription);
// Handle the request error
}
else {
// We have some data, now we need to serialize it so that it's usable
NSError *serializationError;
NSDictionary *serializedDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError];
if (serializationError) {
NSLog(#"%#", serializationError.localizedDescription);
// Handle the serialization error
}
else {
// We have a serialized NSDictionary, now we just want to write it
// First we create the path to write it to, eg. uers documents directory
NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
// Then write it
BOOL success = [serializedDictionary writeToFile:documentsDirectory.path atomically:YES];
if (!success) {
NSLog(#"Error writing file");
}
}
}
}];
Note: You may want to think about storing your data in a remote database like Parse. This way you can query for new questions and only download those so you're not using unnecessary bandwidth. You may also want to consider using Core Data to maintain your local data rather than writing it to a plist. The main advantage being that you don't have to serialise the entire plist into memory to use it - you can just query for the particular questions you need.
Hope this helps.

Using AFNetworking to upload an Audio File

I'm trying to use the AFNetworking library to upload a file to Clypit. I've looked at their documentation here: https://github.com/AFNetworking/AFNetworking
And have configured my code to upload an audio file like so:
NSIndexPath *cellIndexPath = [self.tableView indexPathForCell:cell];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:cellIndexPath];
ICIRecordingCell *c = (ICIRecordingCell *)cell;
NSString *fileName = c.title.text;
NSURL *filePath = [NSURL fileURLWithPath:fileName];
NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:#"POST" URLString:#"http://upload.clyp.it/upload"
parameters:nil
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:filePath name:#"audioFile" fileName:#"audio.m4a" mimeType:#"audio/m4a" error:nil];
} error:nil];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSProgress *progress = nil;
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithStreamedRequest:request progress:&progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
NSLog(#"Error: %#", error);
} else {
NSLog(#"%# %#", response, responseObject);
}
} ];
[uploadTask resume];
But neither responseObject or error are ever firing. I'm not even certain if this is the type of request I should be sending. Clypit Api says they accept request as follows:
Upload an audio file. Accepted file types are mp3, ogg, m4a, wav,
aiff, aif and 3gpp. Regardless of file type when uploading, a
resulting mp3 and ogg will be created and made available. The audio
file can be added to a playlist by providing a playlistId and the
playlistUploadToken. Otherwise, a new playlist will automatically be
created. A playlist can contain a maximum of 20 audio files. The title
of the audio file will be set to that of the name of the uploaded
file.
Parameters: audioFile - The binaries of the audio file. playlistId -
Optional. The playlist that the audio file will be added to. If this
value is specified, the correct playlistUploadToken must also be
included in the request. If this value is not specified, a playlist
will be automatically created. playlistUploadToken - Optional. Given
to you after you create a playlist. When adding an audio file to an
already existing playlist, this value must be provided. order -
Optional. The position in which you would like this audio file to
appear in the playlist. description - Optional. The description of the
audio file. Maximum allowed length is 420 characters. longitude -
Optional. The longitude of where the audio file was recorded. If
passed in, latitude becomes required. Value must be between -15069 and
15069 degrees. latitude - Optional. The latitude of where the audio
file was recorded. If passed in, longitude becomes required. Value
must be between -90 and 90 degrees.
Uploads are done via a multipart/form-data POST. Consider the
following form:
It will create a request that looks like this: POST
http://upload.clyp.it/upload HTTP/1.1 Host: upload.clyp.it
Connection: keep-alive Content-Type: multipart/form-data;
boundary=---------------------------21632794128452 Content-Length:
5005
-----------------------------21632794128452 Content-Disposition: form-data; name="audioFile"; filename="MyAudioFile.mp3" Content-Type:
audio/mpeg (Audio file data goes here)
Am I using the right approach? Thanks in advance

iOS how to insert a local file URL into NSURLConnection response data?

I'm trying to modify a behavior of a webpage within my iOS app and make the in-page media player play a file from the local caches folder instead of fetching it from a web server.
Below is my code that replaces the http:// video path with a local file path. The code does not work, giving me "Resource Temporary not available. Please try again" error message popup. Is it possible to have a web-based media player play file from a local disk using file URL?
I tried substituting these for the instanceURL, but they don't seem to work.
[fileURL path]
[fileURL absolutePath]
I'm intercepting the request for the file and am parsing it to find out that the page is asking for a video file:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
// An NSURLConnection delegate callback. We pass this on to the client.
{
NSDictionary* decisionDictionary = [[RequestListener sharedInstance] shouldContinue:connection processRequestData:data];
BOOL shouldContinue = [decisionDictionary[#"shouldContinue"] boolValue];
if(shouldContinue == NO)
{
return;
}else
{
NSData* d = data;
//substitute fake data
if(decisionDictionary[#"data"])
{
d = decisionDictionary[#"data"];
}
[[self client] URLProtocol:self didLoadData:d];
}
}
Within my shouldContinue method, I check if the video is present locally and modify the response data to create a path to a local video.
NSString* path = [VideoDownloader localVideoPathForVideoID:videoID];
NSURL* fileURL = [NSURL fileURLWithPath:path];
DLog(#"url:%#",[fileURL absoluteString]);
NSString* replacement = [NSString stringWithFormat:#"\"instanceUrl\":\"%#\"",[[fileURL absoluteURL] absoluteString]];
DLog(#"replacement:%#",replacement);
NSString* forgedResponse = [parts componentsJoinedByString:#","];
NSData* forgedData = [forgedResponse dataUsingEncoding:NSUTF8StringEncoding];
return #{#"shouldContinue":#(YES),#"data":forgedData};
Have a look at NSURLProtocol. You can intercept http requests before they are sent to a host to decide what to do about it: Continue to server, redirect to local cache.
There's a decent tutorial by our beloved Ray Wenderlich.
Apple has a programming guide as well.

iOS NSURL queuing mechansim for multiple requests from file

I am very new to iOS development, but I would like to make an app that has two table view controllers (columns): both are a row of images that act as links. The first would be a column of YouTube videos and the second a column of websites. I would like to have all these listed in a file file.txt listed like so: V, http://youtube.com/example W, http://example.com
There would be a long list of those, the V meaning its a video (for the video column) and W for the websites. Now, I understand how to being the single file in, but what happens afterwards is my concern. Can I read each line into some sort of queue and then fire the NSURL request for each one consecutively? How can that be done with NSURL? Is there perhaps a better approach?
There are two questions for me:
Is a text file really the best format?
I might suggest a plist or archive (if the file is only going to exist only in your app's bundle and/or documents folder) or JSON (if it's going to live on a server before delivering it to the user) instead of a text file. It will make it easier to parse this file than a text file. For example, consider the following dictionary:
NSDictionary *dictionary = #{#"videos" : #[#"http://youtube.com/abc", #"http://vimeo.com/xyz"],
#"websites": #[#"http://apple.com", #"http://microsoft.com"]};
You can save that to a plist with:
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *plistPath = [documentsPath stringByAppendingPathComponent:#"files.plist"];
[dictionary writeToFile:plistPath atomically:YES];
You can add that file to your bundle or whatever, and then read it at a future date with:
dictionary = [NSDictionary dictionaryWithContentsOfFile:plistPath];
You can, alternatively, write that to a JSON file with:
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:&error];
NSString *jsonPath = [documentsPath stringByAppendingPathComponent:#"files.json"];
[data writeToFile:jsonPath atomically:YES];
You can read that JSON file with:
data = [NSData dataWithContentsOfFile:jsonPath];
dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
Either way, you can get the list of videos or web sites like so:
NSArray *videos = dictionary[#"videos"];
NSArray *websites = dictionary[#"websites"];
Now that you have your arrays of videos and websites, the question then is how you then use those URLs.
You could do something like:
for (NSString *urlString in videos) {
NSURL *url = [NSURL URLWithString: urlString];
// now do something with the URL
}
The big question is what is the "do something" logic. Because you're dealing with a lot of URLs, you would want to use a NSOperation based solution, not a GCD solution, because NSOperationQueue lets you control the degree of concurrency. I'd suggest a NSOperation-based networking library like AFNetworking. For example, to download the HTML for your websites:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSString *urlString in websites)
{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// convert the `NSData` responseObject to a string, if you want
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
// now do something with it, like saving it in a cache or persistent storage
// I'll just log it
NSLog(#"responseObject string = %#", string);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error = %#", error);
}];
[queue addOperation:operation];
}
Having said that, I'm not sure it makes sense to kick off a ton of network requests. Wouldn't you really prefer to wait until the user taps on one of those cells before retrieving it (and for example, then just open that URL in a UIWebView)? You don't want an app that unnecessarily chews up the user's data plan and battery retrieving stuff that they might not want to retrieve. (Apple has rejected apps that request too much data from a cellular connection.) Or, at the very least, if you want to retrieve stuff up front, only retrieve stuff as you need it (e.g. in cellForRowAtIndexPath), which will retrieve the visible rows, rather than the hundreds of rows that might be in your text/plist/json file.
Frankly, we need a clearer articulation of what you're trying to do, and we might be able to help you with more concise counsel.

Resources