Sure! It was rather convoluted and very specific to my setup, so keep that in mind.
Steps taken to restore my listens
To submit my listens from Jellyfin, I’m using the jellyfin-listenbrainz plugin, which also logs each listen to the system log (journald in my case). With a simple grep for Playback stopped
, I was able to get a list of listens in the following format:
Nov 02 14:50:45 iridium docker[320246]: [13:50:45] [INF] [23] Emby.Server.Implementations.Session.SessionManager: Playback stopped reported by app Finamp 0.6.19 playing Sad Girls Club. Stopped at 204164 ms
First, I needed to extract the log timestamp and title from the log lines, I used sed
for that.
sed 's/^\(.*\) iridium.*playing \(.\+\)\. Stopped at .*$/\1 \2/' filtered.log > listens.log
I did have some issues with non-ascii song titles which I had to fix manually, in that case, you can use any other RegEx engine in a text editor that actually matches all characters with the dot operator.
Nov 02 14:50:45 Sad Girls Club
Next, I parsed the dates to UNIX timestamps with awk
and date
:
awk '{
cmd ="date \"+%s\" -d \""$1" "$2" "$3"\""
cmd | getline var
printf var "|"; for (i = 4; i<=NF; i++) printf $i " "; print ""
close(cmd)
}' listens.log
This gave me a list of timestamps and track titles like this:
1698933045|Sad Girls Club
Lastly, I needed to map the track titles to internal Jellyfin GUIDs of each track. With some trial and error, I came up with this monster of an awk query that calls sqlite to directly query the Jellyfin database.
awk -F'|' '{
cmd ="sqlite3 \"file:jellyfin/config/data/library.db?immutable=1\" \"SELECT substr(hguid, 7, 2) || substr(hguid, 5, 2) || substr(hguid, 3, 2) || substr(hguid, 1, 2) || \'-\' || substr(hguid, 11, 2) || substr(hguid, 9, 2) || \'-\' || substr(hguid, 15, 2) || substr(hguid, 13, 2) || \'-\' || substr(hguid, 17, 4) || \'-\' || substr(hguid, 21, 12) AS guid FROM (select lower(hex(Guid)) as hguid, name, type from TypedBaseItems where name = \'"$2"\' and type = \'MediaBrowser.Controller.Entities.Audio.Audio\');\""
cmd | getline var
print "{\"ListenedAt\":"$1",\"Id\":\""var"\"}"
close(cmd)
}' listens.log | jq -s
A lot of the complexity comes from having to convert the binary-encoded GUIDs to UUIDs, but transforming their endianness at the same time. Thanks StackOverflow for that
The result is a neat JSON structure:
[
{
"ListenedAt": 1698838676,
"Id": "e8b03f41-6d3e-6091-87e5-d710c37456bd"
},
…
]
Inserting the contents of the array into the cache.json
of the ListenBrainz plugin, restarting the Jellyfin server, and running the resubmission task then submits all listens as expected, while resolving metadata from the Jellyfin database and MusicBrainz like usual.
The reason this works is that the ListenBrainz plugin has a resubmission cache file that’s used when the network is down or ListenBrainz is having issues. Entering my listens manually slightly abuses the feature, but it’s easier than collecting the metadata yourself (recording data, player info, etc.).
All in all, this took me around an hour, and I’m pretty sure that not scripting it would’ve been a lot quicker. But this was a nice exercise and I learned something, so I’ll have sugarcoat it now
That seems to be the case. They both depend on the mapping table, which wasn’t generated after restoring the backup. There were attempts to regenerate it, but that currently fails. lucifer is working on fixing it, though.
See the chat logs for more details.
I fear it might take a while to restore that data, and it’s more complex than previously expected.