I did some rejiggering of how XBMC accesses my NAS which required re-importing all my media. The video library sets the ‘date added’ field to the file modification date of a video, which means the ‘Recently Added’ view of my movies is correct, even though everything only just got re-imported. But the music library takes the literal approach and sorts by the order in which albums are scanned. After re-importing my recently added list was just a reverse alphabetical sort by artist. The video library behaviour seems to me to be the intuitively correct behaviour, and I care because when I’m at home I primarily use the recently added view to listen to music.
After a bit of digging around I determined that the music library sets an idAlbum field for each album which is an integer that increments upwards as albums are scanned, and this is how the recently added view sorts itself. So the solution was to blow the music library away and re-import (again), but this time forcing the order in which XBMC scans the music collection.
Step 1) Get an album list sorted by modification date
My music collection is well organized (I use beets) and all of my albums are in directories that have the release year in square brackets, so the following command gave my a list of all my albums sorted by modification date:
$ find . -type d -name *[* -printf '%T@ %p\n' | sort -k 1n
1407992390.0000000000 ./Albums/T/Talking Heads/[1987] More Songs About Buildings and Food
1407992431.0000000000 ./Albums/T/Talking Heads/[1983] Remain in Light
1407992516.0000000000 ./Albums/T/Talking Heads/[1984] Stop Making Sense
1407992594.0000000000 ./Albums/T/Talking Heads/[1986] True Stories
Except that when I reviewed the output I remembered that I’d recently done some updates to the tags on a bunch of albums and so the modification dates were way too recent on a lot of them. I was almost ready to start getting bummed out.
The Real Step 1) Get an album list sorted by creation date
POSIX doesn’t require that a filesystem keep track of file creation date and the XFS filesystem my music resides on dutifully doesn’t bother to track it. However, my music collection is synced with rsync every night from a server I colo downtown, and that host uses an ext4 filesystem which does in fact track the creation date. It’s not obvious, because the standard stat command does not return a creation date on linux but debugfs can.
Now, here’s where things get complicated, mostly because by this time it was late and while I’m sure there’s a better way to do everything that follows my brain was starting to get sleepy. Please feel free to send me more efficient regexes, or versions of scripts that cut out extra steps, or whatever. I’ll update and give credit.
debugfs is easier to work with if you’re just working with inodes so first generate a list of directories and their inodes:
$ find . -type d -name *[* -exec ls -di {} \; > inodelist
$ head -n 5 inodelist
26763273 ./Albums/A/Andy Stott/[2012] Luxury Problems
26763279 ./Albums/A/Arca/[2013] &&&&&
27189257 ./Albums/A/Arcade Fire/[2010] The Suburbs
27189258 ./Albums/A/Arcade Fire/[2004] Funeral
27189256 ./Albums/A/Arcade Fire/[2007] Neon Bible
I found it easiest to generate a separate list of timestamps and then merge the two files (again, I’m sure there’s a better way to do this but I didn’t bother for a one-time use process). First we need a short script that can determine the crtime of the directory:
#!/bin/bash
inode=$1
fs="/dev/md0"
crtime=$(sudo debugfs -R 'stat <'"${inode}"'>' "${fs}" 2>/dev/null | grep crtime | cut -d ' ' -f 2 | cut -d ':' -f 1)
printf "%d\n" "${crtime}"
And use that to get yourself a nice sorted list:
$ for i in `cat inodelist | cut -d ' ' -f 1`; do ./get_crtime.sh $i >> crtimelist; done
$ paste -d " " crtimelist inodelist | sort | cut -f2- -d '/' > albumlist
$ head -n 5 albumlist
Albums/P/Prince/[1987] Dirty Mind
Albums/P/Public Enemy/[1988] It Takes a Nation of Millions to Hold Us Back
Albums/M/mum/[2002] Finally We Are No One
Albums/A/Amon Tobin/[1997] Bricolage
Albums/B/Blonde Redhead/[2000] Melody of Certain Damaged Lemons
Congratulations. You now have a list of albums sorted by their creation date. I generated the file with paths relative to the root of my music directory, because it’s easier to work with that way. You’ll clearly need to play with the cut options to get something that works for your particular library of music.
Step 2) Scan your directories in the correct order
XBMC has a JSON-RPC API, which includes the helpful AudioLibrary.Scan method, and even a wiki page on how to use it to trigger scans, though we need to pass the optional directory parameter:
$ curl --data-binary '{ "jsonrpc": "2.0", "method": "AudioLibrary.Scan", "params": { "directory": "/mnt/music/Albums/F/FKA twigs/[2013] EP2" }, "id": "1"}' -H 'content-type: application/json;' http://localhost:8080/jsonrpc
{"id":"1","jsonrpc":"2.0","result":"OK"}
In testing I found that a lot of the jsonrpc calls were silently failing – I’d get the OK result for all of them, but watching the logs I could see that not all of the requests were actually being executed. In my first test I used a while loop and fired over 700 requests in a couple seconds and saw that only about 30% of them executed (I didn’t even bother to check if they were in the correct order). I watched the import notification on screen when I imported a single album and saw it took roughly ten seconds to import the album. With that in mind for the second test I waited 20 seconds between each request and I still saw only 80-90% of them executed. I doubt it’s because the previous request was still running because then I’d expect the first test to have only resulted in a single (maybe two) successfully executed requests.
By this time it’s really late and I didn’t care enough to troubleshoot further – I decided to just brute force the matter:
$ while read album; do
echo $album
curl -s --data-binary '{ "jsonrpc": "2.0", "method": "AudioLibrary.Scan", "params": { "directory": "/mnt/music/'"$album"'" }, "id": "1"}' -H 'content-type: application/json;' http://localhost:8080/jsonrpc > /dev/null
sleep 20
curl -s --data-binary '{ "jsonrpc": "2.0", "method": "AudioLibrary.Scan", "params": { "directory": "/mnt/music/'"$album"'" }, "id": "1"}' -H 'content-type: application/json;' http://localhost:8080/jsonrpc > /dev/null
sleep 20
curl -s --data-binary '{ "jsonrpc": "2.0", "method": "AudioLibrary.Scan", "params": { "directory": "/mnt/music/'"$album"'" }, "id": "1"}' -H 'content-type: application/json;' http://localhost:8080/jsonrpc > /dev/null
sleep 20
done < albumlist
It’s about as elegant as a sledgehammer, but it works. The extra calls to the RPC method are redundant at worst since it does no harm to scan the directory repeatedly but at least you can be reasonably sure the album will get scanned successfully. Run it in screen overnight and when you return in the morning you should have all of your albums imported, in the order in which you acquired them.
UPDATE: Even the sledgehammer wasn’t enough for three albums. Evidently it’s easy for the AudioLibrary.Scan method call to be skipped. So I blew away the music library again and this time used a script to scan each album, this time checking the XBMC logs. You need to enable debug logging for this script to work, but I’ll leave enabling that as an exercise for the user since there’s a few ways to do it. Anyway, here’s a better solution for importing:
$ cat import_albums.sh
#!/bin/bash
while read album; do
while true; do
IMPORTED=`grep -F "$album" ~/.xbmc/temp/xbmc.log`
if [ $? == 0 ]; then
break
fi
echo $album
curl -s --data-binary '{ "jsonrpc": "2.0", "method": "AudioLibrary.Scan", "params": { "directory": "/mnt/music/'"$album"'" }, "id": "1"}' -H 'content-type: application/json;' http://localhost:8080/jsonrpc > /dev/null
sleep 15
done
done < $1
$ import_albums.sh albumlist
Areas for improvement:
1) That crap with get_crtime.sh and the steps around it, in particular generating a separate crtimelist file and merging it back in. Maybe something that can be called directly from find -exec?
2) The sledgehammer import. Perhaps check the log after making a request and seeing if the DoScan event shows up there before moving on?