[ITLib] Get a tree of artists / albums (Mac OS Swift app)

Hi,


I'm looking for a way to get all the iTunes artists and their albums in a tree (like the playlists in iTunes which I get like it).


I tried an algo where I get all the media items filtered by mediaKind == .kindSong sorted by artist and after that, I "while" loop in all the media items count (about 36 000 for me (iTunes user for 15 years 😝)) to filter them by artist name and after that by album title so each time, I increase the i + the count of the results to go to the next artist and then next album for each artist.


It is working but it is very slow. I think it is because of the array filter function of the media items list which loop into all the array (right ?) to find the criteria. But as I sorted it just before to start the loop, it could be faster to stop it as soon as the artist name is not the same than the previous (or something like that...).


Here is the 2 main func I use :

static func getArtistsTree(theITTracks: [ITLibMediaItem], theLenght: Int) -> [Artist] {
    var theArtists: [Artist] = [Artist]() // just a custon obj to record the artists
    let theArtistNames = getAllArtistNames(theITTracks: theITTracks).sorted()
    for theArtistName in theArtistNames {
        let theArtistResults = theITTracks.filter({$0.artist?.name?.lowercased() == theArtistName.lowercased()}).sorted(by: {self.sortITTrack(ITTrack1: $0, ITTrack2: $1, kind: ITSortKind.album)})
        print("V&G_Project___theArtistResult.count : ", theArtistResults.count)
        var i: Int = 0
        while i < theArtistResults.count {
            let theArtistTrack = theArtistResults[i]
            if let theAlbumTitle = theArtistTrack.album.title {
                let theAlbumResults = theArtistResults.filter({$0.album.title?.lowercased() == theAlbumTitle.lowercased()})
                print("V&G_Project___name : ", theArtistTrack.artist?.name, " - ", theAlbumTitle, "theAlbumResults.count : " + String(theAlbumResults.count))
                var j: Int = 0
                while j < theAlbumResults.count {
                    let theAlbum = theAlbumResults[j]
                    let theTracksResults = theAlbumResults.filter({$0.album.title?.lowercased() == theAlbumTitle.lowercased()})
                    j += 1
                }
                i += theAlbumResults.count
            } else {
                i += 1
            }
        }
        print("V&G_Project___--------------- : ")
    }
    
    return theArtists
}

static func getAllArtistNames(theITTracks: [ITLibMediaItem]) -> [String] {
    var theArtistsList: [String] = [String]()
    for theITTrack in theITTracks {
        let theArtistName = theITTrack.artist?.name
        var theToto: String = "unknown"
        if let theArtistName = theArtistName {
            theToto = theArtistName
        }
        let theIndex = theArtistsList.index(of: theToto)
        print("V&G_Project___getAllArtistNames : ", theIndex)
        if theIndex == nil {
            theArtistsList.append(theToto)
        }
        
    }
    return theArtistsList
}

do {
    let lib = try ITLibrary(apiVersion: "1.0")
    let theITTracks = lib.allMediaItems.filter({$0.mediaKind == .kindSong})
    let theArtistsTree = iTunesModel.getArtistsTree(theITTracks: theITTracks, theLenght: theITTracks.count)    
} catch let error {
    print("V&G_Project___<#name#> : ", error)
}

I can use only one func to save one loop but even with one, everything works fine but the algorythm is very slooooww and I need to improve it.


Any idea ?


Thx.

Replies

On line 3 you build a list of artist names and then on line 4 you iterate that list and on line 5 you filter the tracks to find the tracks for that artist. But you’ve already gone through the list of tracks on line 3, so you can accumulate the info you need at that point. For example, in the code below I walk the list of tracks building a dictionary that maps the artist name to a list of their tracks.

struct Track {
    var id: Int
    var artist: String
}

let tracks = [
    Track(id: 1, artist: "a"),
    Track(id: 2, artist: "b"),
    Track(id: 3, artist: "c"),
    Track(id: 4, artist: "a"),
    Track(id: 5, artist: "a"),
    Track(id: 6, artist: "a"),
    Track(id: 7, artist: "b"),
]
let tracksByArtist = Dictionary(grouping: tracks, by: { $0.artist })
for (artist, tracks) in tracksByArtist {
    print(artist, tracks.map { $0.id })
}
// prints:
//
// a [1, 4, 5, 6]
// b [2, 7]
// c [3]

There’s probably a bunch of other things you can improve here, but let’s stack with this.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
  • Thx for that.

    I didn't know the trick with Dictionary by grouping, very useful !

    If I understand well, we can group by a parameter and then another, right ?

    So in my case, I'd like to have [artist[album[title, title, title, title]][album[title, title, title]][album[title, title, title, title, title]][album[title]]] to have a tree.

    So I need to do multiple Dictionary with maps in the for() or can I group directly in one dictionary ?

    Thx.

Add a Comment

There is something I don't understand in your code:


Line 2, you create an empty array


    var theArtists: [Artist] = [Artist]() // just a custon obj to record the artists


You return the array on line 27


    return theArtists


But it was never modified in between.


So either it is useless, or your code cannot work.

Did I miss something ?

2 years ago, I'm back on this post !

I think my approach is too complicated even if it is working...

Like Eskimo said, Dictionary seems to be the key but I'm not familiar with that to manipulate arrays...

Do I need to do multiple dictionary of can I have all my structure in only one ?

Thx.

Hi,

In fact, I'd like to reproduce what we have in the Music app :

The Eskimo's way with the Dictionary seams to be great but I need to have the Artist >>> Albums >>> Tracks schema.

Thx for your help.

Hi,

So thanks to Eskimo and his group by trick (very useful), I just finished a pretty good version of my artists - albums - tracks tree.

Here is my playground :

import Cocoa
import iTunesLibrary

do {
    let lib = try ITLibrary(apiVersion: "1.1")
    let theITTracks = lib.allMediaItems.filter({$0.mediaKind == .kindSong})
    
    let theTracksByArtist = Dictionary(grouping: theITTracks, by: { $0.artist?.name } )
    
    var theArtistsTree = [Artist]()
    for(artistKey, tracksByArtist) in theTracksByArtist {
        print("artistKey >>>", artistKey)
        print("/////////")
        let theAlbumsTracks = tracksByArtist.map{ $0 }
        let theITArtist = theAlbumsTracks.first?.artist
        let theArtist = Artist(theITArtist: theITArtist!)
        
        let theArtistAlbums = Dictionary(grouping: theAlbumsTracks, by: { $0.album.persistentID } )
        for (albumID, tracksAlbum) in theArtistAlbums {
            let theTracksAlbum = tracksAlbum.map { $0 }
            let theITAlbum = theTracksAlbum.first?.album
            let theAlbum = Album(theITAlbum: theITAlbum!)
            
            print(theTracksAlbum.first?.album.title)
            print("++++")
            
            for track in theTracksAlbum {
                print(track.title)
                let theTrack = Track(theITTrack: track)
                theAlbum.tracks.append(theTrack)
            }
            print("***************")
            
            theArtist.albums.append(theAlbum)
        }
        
        theArtistsTree.append(theArtist)
        print("---------------------------------------------------------------------------------------------------")
    }
    
} catch let error {
    print(error)
}

But I have a bunch of problems to solve like the sort of the dictionary grouped by artist.

I tried :

let theTracksByArtist = Dictionary(grouping: theITTracks, by: { ($0.artist?.name)! } ).sorted(by: { $0.key.localizedStandardCompare($1.key) == .orderedAscending })

It is working until the moment an artist nil is parsed :

Multiline error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x1006c2ad8). The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

So how to avoid this error ? Do I need to test the artist?.name before to compare it ? How to ?

Thx.

What about writing

($0.artist?.name) ?? "--"

I tried :

let theTracksByArtist = Dictionary(grouping: theITTracks, by: { $0.artist?.name ?? "--" } ).sorted(by: { $0.key.localizedStandardCompare($1.key) == .orderedAscending })

And it is working, thx !

What is "--" ? Special code or can I type "" ?

What is "--" ? Special code or can I type "" ?

Just a pseudo name, to avoid an empty string. You may filter them out after.

Did you try ? Does it solves the crash ?

  • Yes, it is working.

    Thx a lot.

Add a Comment

Hi,

Another problem here is when I get all the tracks artist's albums, the function is slow :

for track in theTracksAlbum {
                print(track.title)
                let theTrack = Track(theITTrack: track) // a NSObject to show it in a NSOutlineView
                theAlbum.tracks.append(theTrack)
            }

If I comment those lines, the tree appears instantly.

Problem : I need the albums tracks to get the artwork's album (right ?) and especially to add them in a playlist.

So each time I click on an artist, I could execute a function to retrieve all the tracks albums, it is not so difficult.

But before to do that, I'd like to know if I could enhance something to get the tracks ? Each time I do a loop to build objects, the program goes slow...

I think it is possible because when I click on the Artists / All artists in the Music app, it is fast :

Thx.

Hi,

No answer but for those like me who have this problem, I solved it.

As I don't need to show the tracks in the NSOutlineView :

I add directly the ITTracks to an array in the album :

for (albumID, tracksAlbum) in theArtistAlbums {
                let theTracksAlbum = tracksAlbum.map { $0 }.sorted(by: { $0.trackNumber < $1.trackNumber })
                let theITAlbum = theTracksAlbum.first?.album
                let theAlbum = Album(theITAlbum: theITAlbum!)
                theAlbum.ITTracks = theTracksAlbum // tracks added here, don't need to build a NSObject in the loop like before...
                
                theArtist.albums.append(theAlbum)
            }

The powerful way here is to use the map of tracksAlbum.map.

But for my info, is it possible to build objects in this kind of function instead of doing a loop like I tried to do which is very slow ?

Thx.