Writing ID3 tags via AVMetaDataItem - swift

I'm writing ID3 tags to a file using AVMetaDataItem
var soundFileMetadata = [AVMetadataItem]()
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeyArtist, "MyArtist")!)
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeySongName, "MySong")!)
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeyAlbum, "MyAlbum")!)
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeyUserGenre, "MyGenre")!)
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeyComposer, "MyComposer")!)
Here is the createMetadata convenience method:
func createMetadata(tagKey: String, _ tagValue: AnyObject?,
keySpace:String = AVMetadataKeySpaceiTunes) -> AVMutableMetadataItem? {
if let tagValue = tagValue {
let tag = AVMutableMetadataItem()
tag.keySpace = keySpace
tag.key = tagKey
tag.value = (tagValue as? String) ?? (tagValue as? Int)
return tag
}
return nil
}
I then tried to write also the year tag, with no success:
let comps = NSDateComponents()
comps.year = 2010;
let yearTag = AVMutableMetadataItem()
yearTag.keySpace = AVMetadataKeySpaceID3
yearTag.key = AVMetadataID3MetadataKeyYear
yearTag.value = NSCalendar.currentCalendar().dateFromComponents(comps)
soundFileMetadata.append(yearTag)
In this case I get this error:
FigMetadataCreateConverter signalled err=-12482 (kFigMetadataConverterError_UnsupportedFormat) (Unsupported format conversion) at /SourceCache/CoreMedia/CoreMedia-1562.238/Prototypes/Metadata/Converters/FigMetadataConverterCommon.c line 118
Note that this is a simple error printed in console, not an exception!
Also writing it as a String, as an Int o even a Float, leads me to the same error.
Same is for Track/Disc count, Track/Disc number tags.
First question is: how to write them?
I also have another question.
Currently I've an AVAudioRecorder, I found no way to write tags directly to the output file of the recorder, so I commit the recorder file, open it with AVURLAsset and re-export it with AVAssetExportSession:
self.recorder.stop()
let urlAsset = AVURLAsset(URL: srcSoundFileURL)
let assetExportSession: AVAssetExportSession! = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetPassthrough)
assetExportSession.outputFileType = AVFileTypeAppleM4A
assetExportSession.outputURL = tmpSoundFileURL
assetExportSession.metadata = soundFileMetadata
assetExportSession.exportAsynchronouslyWithCompletionHandler({
....
})
Second question is: is there any way to avoid this double-step action?

I've managed to add the year tag with your code with a few modifications:
let yearTag = AVMutableMetadataItem()
yearTag.keySpace = AVMetadataKeySpaceiTunes
yearTag.key = AVMetadataiTunesMetadataKeyReleaseDate
yearTag.value = "2123"
I couldn't make it work with the ID3 keys so I thought this could be the problem, and indeed it works with these iTunes keys. Also, the value has to be a String (or NSString), not a date object.

Related

Check existence of localized string in .strings resource file, fall back to default

I want to find the appropriate localized string based on some runtime variable and fall back to a default string:
// localizable.strings
"com.myapp.text1" = "The default text 1";
"com.myapp.text1#SPECIAL" = "The special text";
"com.myapp.text2" = "The default text 2";
// my code
let key1 = "com.myapp.text1"
let key2 = "com.myapp.text2"
let modifier = "#SPECIAL"
print( NSLocalizedString(key1 + modifier
, value: NSLocalizedString(key1, comment: "")
, comment: "") )
// > "The special text 1"
print( NSLocalizedString(key2 + modifier
, value: NSLocalizedString(key2, comment: "") # the default to fall back to
, comment: "") )
// > "The default text 2"
Nice, that's what I want, try a special variant, fall back to the default.
However, if the option NSShowNonLocalizedStrings in the user defaults is set to true, it fails:
For non-localised strings, an upper-case version of the key will be returned, ignoring the default value. Also an error message is printed in the console (documentation).
So it appears that my solution is working against intended way of using NSLocalizedString.
UserDefaults.standard.set(true, forKey: "NSShowNonLocalizedStrings") # could also be passed as launch option or set via terminal
print( NSLocalizedString(key2 + modifier
, value: NSLocalizedString(key2, comment: "")
, comment: "") )
// > ERROR: com.myapp.text2#MODIFIER not found [...]
// > "COM.MYAPP.TEXT2"
I could work around this by testing for the uppercased version etc. but this would just be a hack that masks the actual issue.
What I would probably need is a test if (bundle.hasLocalizationFor(key2 + modifier)... but to implement such a method I would have to re-implement processing of the strings files including parsing and caching. And that feels wrong in itself.
Question:
Is there some method I am missing to achieve what I am looking for?
Or is the whole idea of special/fallback localization just wrong for the platform?
Thanks to comments by Martin R, I was able to get a reasonably working solution:
static let cache = NSCache<NSString, NSDictionary>()
private func strings(for tableName: String) -> [String: String] {
guard let strings = cache.object(forKey: tableName as NSString) else {
guard let path = Bundle.main.path(forResource: tableName, ofType: "strings")
, let dict = NSDictionary(contentsOfFile: path) as? [String: String] else {
return [:]
}
cache.setObject(dict as NSDictionary, forKey: tableName as NSString)
return dict
}
return strings as! [String: String]
}
func localizedString(_ key: String) -> String {
let specificKey = key + "#SPECIAL"
let tableName = "TableName"
let bundle = Bundle.main
return self.strings(for: tableName)[specificKey]
?? NSLocalizedString(key
, tableName: tableName
, bundle: bundle
, comment: "")
}

Swift Get Next Page from header of NSHTTPURLResponse

I am consuming an API that gives me the next page in the Header inside a field called Link. (For example Github does the same, so it isn't weird.Github Doc)
The service that I am consuming retrieve me the pagination data in the following way:
As we can see in the "Link" gives me the next page,
With $0.response?.allHeaderFields["Link"]: I get </api/games?page=1&size=20>; rel="next",</api/games?page=25&size=20>; rel="last",</api/games?page=0&size=20>; rel="first".
I have found the following code to read the page, but it is very dirty... And I would like if anyone has dealt with the same problem or if there is a standard way of face with it. (I have also searched if alamofire supports any kind of feature for this but I haven't found it)
// MARK: - Pagination
private func getNextPageFromHeaders(response: NSHTTPURLResponse?) -> String? {
if let linkHeader = response?.allHeaderFields["Link"] as? String {
/* looks like:
<https://api.github.com/user/20267/gists?page=2>; rel="next", <https://api.github.com/user/20267/gists?page=6>; rel="last"
*/
// so split on "," the on ";"
let components = linkHeader.characters.split {$0 == ","}.map { String($0) }
// now we have 2 lines like '<https://api.github.com/user/20267/gists?page=2>; rel="next"'
// So let's get the URL out of there:
for item in components {
// see if it's "next"
let rangeOfNext = item.rangeOfString("rel=\"next\"", options: [])
if rangeOfNext != nil {
let rangeOfPaddedURL = item.rangeOfString("<(.*)>;", options: .RegularExpressionSearch)
if let range = rangeOfPaddedURL {
let nextURL = item.substringWithRange(range)
// strip off the < and >;
let startIndex = nextURL.startIndex.advancedBy(1) //advance as much as you like
let endIndex = nextURL.endIndex.advancedBy(-2)
let urlRange = startIndex..<endIndex
return nextURL.substringWithRange(urlRange)
}
}
}
}
return nil
}
I think that the forEach() could have a better solution, but here is what I got:
let linkHeader = "</api/games?page=1&size=20>; rel=\"next\",</api/games?page=25&size=20>; rel=\"last\",</api/games?page=0&size=20>; rel=\"first\""
let links = linkHeader.components(separatedBy: ",")
var dictionary: [String: String] = [:]
links.forEach({
let components = $0.components(separatedBy:"; ")
let cleanPath = components[0].trimmingCharacters(in: CharacterSet(charactersIn: "<>"))
dictionary[components[1]] = cleanPath
})
if let nextPagePath = dictionary["rel=\"next\""] {
print("nextPagePath: \(nextPagePath)")
}
//Bonus
if let lastPagePath = dictionary["rel=\"last\""] {
print("lastPagePath: \(lastPagePath)")
}
if let firstPagePath = dictionary["rel=\"first\""] {
print("firstPagePath: \(firstPagePath)")
}
Console output:
$> nextPagePath: /api/games?page=1&size=20
$> lastPagePath: /api/games?page=25&size=20
$> firstPagePath: /api/games?page=0&size=20
I used components(separatedBy:) instead of split() to avoid the String() conversion at the end.
I created a Dictionary for the values to hold and removed the < and > with a trim.

How to get modified date of GTLDriveFile in Google Drive in iOS in swift?

I am integrating google drive in my project of swift.Integrating via google drive integration guide line, but My projects needed to access all the details like modified date,created date etc.But unable to find in the
all these details.
Using following code :
if let modifiedDate = file.modifiedByMeTime{
print_debug(modifiedDate)
}
if let modifiedDate = file.sharedWithMeTime{
print_debug(modifiedDate.milliseconds)
}
How to get modified date of GTLDriveFile in Google Drive in iOS in swift?
Swift 3
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
query.q = path
query.fields = "files(id,name,mimeType,modifiedTime,createdTime,fileExtension,size),nextPageToken"
// u want to specify what all details want as response.//
service.executeQuery(query, completionHandler: {(ticket, files, error) in
if let filesList : GTLRDrive_FileList = files as? GTLRDrive_FileList {
if let filesShow : [GTLRDrive_File] = filesList.files {
print("files \(filesShow)")
for ArrayList in filesShow {
let name = ArrayList.name
let mimeType = ArrayList.mimeType
let id = ArrayList.identifier
let folder = (mimeType as NSString?)?.pathExtension
let creationTime = ArrayList.createdTime?.date // modified time
let modifiedTime = ArrayList.modifiedTime?.date //createdTime
}
}
})

Getting bundle identifier in Swift

I'm trying to get bundleID using app's directory and I'm getting an error:
EXC_BAD_ACCESS(code=1, address=0xd8)
application.directory! is a String
let startCString = (application.directory! as NSString).UTF8String //Type: UnsafePointer<Int8>
let convertedCString = UnsafePointer<UInt8>(startCString) //CFURLCreateFromFileRepresentation needs <UInt8> pointer
let length = application.directory!.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
let dir = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, convertedCString, length, false)
let bundle = CFBundleCreate(kCFAllocatorDefault, dir)
let result = CFBundleGetIdentifier(bundle)
and I get this error on the result line.
What am I doing wrong here?
If you are trying to get it programmatically , you can use below line of code :
Objective-C:
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
Swift 3.0:
let bundleIdentifier = Bundle.main.bundleIdentifier
(Updated for latest swift It will work for both iOS and Mac apps.)
For More Info, Check here :
Apple Docs:
https://developer.apple.com/documentation/foundation/bundle#//apple_ref/occ/instm/NSBundle/bundleIdentifier
One potential problem with your code is that the pointer obtained in
let startCString = (application.directory! as NSString).UTF8String //Type: UnsafePointer<Int8>
is valid only as long as the temporary NSString exists. But that
conversion to a C string can be done "automatically" by the compiler
(compare String value to UnsafePointer<UInt8> function parameter behavior), so a working version
should be
let dir = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, path, Int(strlen(path)), false)
let bundle = CFBundleCreate(kCFAllocatorDefault, dir)
let result = CFBundleGetIdentifier(bundle)
But you can simply create a NSBundle
from a given path and obtain its identifier:
let ident = NSBundle(path: path)!.bundleIdentifier!
Full example with added error checking:
let path = "/Applications/TextEdit.app"
if let bundle = NSBundle(path: path) {
if let ident = bundle.bundleIdentifier {
print(ident) // com.apple.TextEdit
} else {
print("bundle has no identifier")
}
} else {
print("bundle not found")
}

How to get the video ID of a YouTube string

(XCode 6.3.2, Swift 1.2)
After researching the internet for the whole evening I already know that this can't be done that easily.
I simply want to get the video ID of a YouTube link. So the "ABCDE" in "https://www.youtube.com/watch?v=ABCDE"
What I got so far:
var url = "https://www.youtube.com/watch?v=ABCDE"
let characterToFind: Character = "="
let characterIndex = find(url, characterToFind)
println(characterIndex)
url.substringFromIndex(advance(url.startIndex, characterIndex))
Prinln outputs: Optional(31)
That's right but I can't use this because it's no index.
XCode also states for the last line: Missing argument for parameter #3 in call
I also don't know what the 3rd parameter of substringFromIndex should be.
Many thanks in advance!
In your case there is no need to create an NSURL as other answers do. Just use:
var url = "https://www.youtube.com/watch?v=ABCDE"
if let videoID = url.componentsSeparatedByString("=").last {
print(videoID)
}
Swift 3+ version:
var url = "https://www.youtube.com/watch?v=ABCDE"
if let videoID = url.components(separatedBy: "=").last {
print(videoID)
}
You can use NSURL query property as follow:
Xcode 8.3.1 • Swift 3.1
let link = "https://www.youtube.com/watch?v=ABCDE"
if let videoID = URL(string: link)?.query?.components(separatedBy: "=").last {
print(videoID) // "ABCDE"
}
Another option is to use URLComponents:
let link = "https://www.youtube.com/watch?v=ABCDE"
if let videoID = URLComponents(string: link)?.queryItems?.filter({$0.name == "v"}).first?.value {
print(videoID) // "ABCDE"
}
Swift 1.x
let link = "https://www.youtube.com/watch?v=ABCDE"
if let videoID = NSURL(string: link)?.query?.componentsSeparatedByString("=").last {
println(videoID) // "ABCDE"
}
You need to unwrap the optional to use the index:
var url = "https://www.youtube.com/watch?v=ABCDE"
let characterToFind: Character = "="
if let index = find(url, characterToFind) { // Unwrap the optional
url.substringFromIndex(advance(index, 1)) // => "ABCDE"
}
find returns an optional – because the character might not be found, in which case it will be nil. You need to unwrap it to check it isn’t and to get the actual value out.
You can also use a range-based subscript on the index directly, rather than using advance to turn it into an integer index:
let url = "https://www.youtube.com/watch?v=ABCDE"
if let let characterIndex = find(url, "=") {
let value = url[characterIndex.successor()..<url.endIndex] // ABCDE
}
else {
// error handling, if you want it, here
}
You have more options if there is a reasonable default in the case of “not found” For example, if you just want an empty string in that case, this will work:
let idx = find(url, "=")?.successor() ?? url.endIndex
let value = url[idx..<url.endIndex]
Or, maybe you don’t even need to deal with the optionality right now, so you’re happy to leave the result optional as well:
// value will be Optional("ABCD")
let value = find(url, "=").map { url[$0.successor()..<url.endIndex] }
For a rundown on optionals, take a look here and here.
Also, rather than hand-parsing URL strings at all, you might find the info here useful.
With your url format, you can get the 'v' parameter's value by converting to NSURL then get parameters and separate it by '='.
var url: NSURL = NSURL(string: "https://www.youtube.com/watch?v=RAzOOfVA2_8")!
url.query?.componentsSeparatedByString("=").last
url.query returns v=RAzOOfVA2_8
If the link has more than 1 parameter, you can get all parameters then do a loop to verify each parameter:
var url: NSURL = NSURL(string: "https://www.youtube.com/watch?v=RAzOOfVA2_8")!
var params = url.query?.componentsSeparatedByString("&")
if let _params = params { //have parameters in URL
for param in _params { //verify each pair of key & value in your parameters
var _paramArray = param.componentsSeparatedByString("=") //separate by '=' to get key & value
if (_paramArray.first?.compare("v", options: nil, range: nil, locale: nil) == NSComparisonResult.OrderedSame) {
println(_paramArray.last)
}
}
} else {
//url does not have any parameter
}