Navidrome, Clients and Albums With Varying Artist Tags

I’m using Navidrome as my music server now, having switched from shoehorning Jellyfin into that role, and I quite like it. One thing I discovered, however, is that it – and quite a few clients that support it – rely on the Album Artist tag to help index files. Initially I found this problematic because many of my files didn’t have Album Artist populated. I solved this by writing a PowerShell script to copy the Perfomers tag data (Performers = Artist) to the Album Artist tag. I’ll share that in another post.

But then I discovered a different problem. If an album is a compilation or has variance in the artists from file to file (for example: This Guy feat. That Other Guy), it’d index files into separate albums of the same title. Navidrome’s developers confirmed that this is a feature, not a bug, and it makes sense. The solution for compilation albums is to set the Compilation tag to 1, which I haven’t automated yet but will shortly. The solution for non-comp albums is to make sure the Album Artists tag is consistent.

To accomplish this, I threw together the below script. It’s not optimized, it doesn’t have error handling or confirmation, and it’s apt to be destructive if you’re not careful. Still, it’s something you can build on, just as I inevitably will. As with all my music file related scripts, it requires taglib-sharp.dll. Obtain this file from TagLibSharp. You don’t have to futz with nuget or anything – just download the zipped package, yank the dll out of it, and put it in a subdir you’ll reference in the script.

This script will prompt for the full path to the files you wish to update, and then the name you wish to update the Album Artist tag to.

Calibre Web in a Container, and Credentials

My Calibre Web installed via container in CasaOS decided to lose its admin account creds. To reset, access the container terminal from within CasaOS, navigate to:

and enter:

Where password is your updated passy. If you try to use ‘password’ you’ll probably be kicked back for it not meeting complexity requirements.

Cron for Dynamic DNS Updating

I use my commercial host, Ionos, for websites, including creating subdomains for ones I’m hosting at home. However, I don’t pay for a static ip. I was using a Windows Scheduled Task to curl the necessary URLs to keep the IP updated, but I’m switching everything I can over to Linux, and a Pi.

Start by creating your .sh file and making it executable. I created dnsupdate.sh in my home dir on my Pi, and populated it with my curl calls. I did this via Nano. Then make it executable:

Then I fired up cron by entering:

It’s going to ask you for an editor on first use. I say, stick with Nano.

After this it will present you with a crontab file. Scroll to the bottom and enter:

This will run your sh file every hour at the top of the hour.

Save the file.

If you want to see what’s up with Cron, enter:

If it’s running, it’ll show you the files – your sh file – in use by cron.

CasaOS and Caddy

Briefly – Caddy wouldn’t work out of the box for me on CasaOS. In this build, the default container path for the caddyfile is incorrect. Change the container path from

to

Also, don’t forget to adjust ports, our you’ll inevitably be running into CasaOS itself. I changed my HTTP ports to 85:

Then you can SSH into your CasaOS Pi, head to /DATA/AppData/caddy/caddyfile, create your Caddyfile, er, file, and populate it appropriately. If you’ve been running Caddy elsewhere, I recommend running the below to confirm your file’s format:

CasaOS and bash in Docker

I installed ArchiveBox on CasaOS and came upon a problem – I couldn’t figure out what the default credentials are, or even if there were any, and I didn’t know how to generate them from the command line. ArchiveBox told me to create a new superuser by running

From the command line, but doing so within the container terminal resulted in this:

But I found my way into the container bash thusly:

  • SSH into your pi running CasaOS
  • search for your ArchiveBox container ID:
  • You’ll see a path in your return with a long, long directory name, something like
  • This is your container ID. CD to /var/lib/docker/containers/and run the following, where containerID is the above ID:
  • This will invoke a bash within that container. From within it, run:
  • And follow the prompts to create a superuser for ArchiveBox. To drop out of bash, just input exit.

 

CasaOS – Tinkering

Somewhere in my intertoobs wanderings I came across CasaOS and, given I had an idle Pi 4 laying around, I decided to poke at it. After all, if it works worth a damn it could help consolidate all the disparate systems I have running locally.

CasaOS is basically a slick Docker manager that rides atop another OS. Getting it up and running on the Pi with a fresh Raspbian install was painless. Simple configs, like mounting network shares, is a breeze. So far I have the following running in it:

  • PiHole – Installed without a hitch. If this runs well it’ll replace the standalone PiHole Pi 3 I have running.
  • Calibre Web – Also installed without a hitch. Love this, because I hate running the Calibre thick client full time on my server simply for its web interface. Not a lot of instructions on this one – just make sure you have your library mounted and defined as a volume in the container settings.
  • Jellyfin – Another painless install (make sure to mount your files volume as described in the above Calibre Web settings). I installed this as a replacement for my current Jellyfin music server, which runs standalone in a fully fledged VM, gobbling unnecessary resources. But I stopped it midway through indexing my gajillion files because I discovered:
  • Navidrome – Will it be better than Jellyfin for audio? I don’t know, but I’m giving it a shot.  Currently still indexing – on directory number 12406. Heh. Now, Navidrome wouldn’t run initially, and CasaOS kept screaming that it was unhealthy. Its log filled with “exec /app/navidrome: exec format.” I discovered that the image I was pulling isn’t the most recent. I exported the ComposeFile from within the settings of the failed install, uninstalled it, and updated the image source in the ComposeFile to point to “ghcr.io/navidrome/navidrome:develop.” I then imported the file (Custom Install > Import from within the App Store) and it installed fine
  • Memos – Completely painless install. Came across this randomly and figured maybe it’d replace my TiddlyWiki instance running on my server. We’ll see.
  • Mealie – I ran across this neat app via some Youtubers Apps of The Year vid and was intrigued. An app that’ll scrape a online recipe page, pull the pertinent details of the recipe and ditch all the ads and fluff? Yes please! However, Mealie wasn’t in the default app store for CasaOS. I found CoolStore for Casa and added it as a source. From there Mealie installed no problem, and now I have a slew of other apps to sift through as well. Perhaps Casa can even replace my standalone VPN Pi. We shall see.

Powershell – Monitoring a log file

I had a situation at work where I needed to monitor a log file for a particular entry. I needed to be notified when that entry appeared. This log file gets created anew, with the current date, every day. Here’s what I came up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
while($true)
{
# get the date
$now = get-date
# create the log name from said date eg :20231223Err.txt
$fileName = '{0}{1}{2}Err.txt' -f $now.Year, $now.Month, $now.Day
# network path to said log
$fullPath = "\\path\e$\LogFiles\app\$($fileName)"

#I don't need this to run in perpetuity - it's a temp situation - so I'm just
#invoking this in a Powershell window and letting it rip.
Write-Host "[$(Get-Date)] Starting job for file $fullPath"
$latest = Start-Job -Arg $fullPath -ScriptBlock {
param($file)

# wait until the file exists, just in case
while(-not (Test-Path $file)){ sleep -sec 10 }
# matching the phrase Queue Count. could make all these variables
Get-Content $file -wait | where {$_ -match 'Queue Count'} |
foreach {
Send-MailMessage -SmtpServer SMTPSERVER -From me@email.com -To me@email.com -Subject 'Queue Count' -Body $_
write-host $_
}
}

# wait until date change
while($now.Date -eq (Get-Date).Date){ sleep -Sec 10 }

# kill the job and start anew
Write-Host "[$(Get-Date)] Stopping job for file $fullPath"
$latest | Stop-Job
}

Jellyfin as a Music Server – Missing Artist Tag

See previous post about Jellyfin for details on Jellyfin database access.

1
2
3
4
5
select artists,album,IndexNumber,name,path
from TypedBaseItems where artists is NULL
and IsFolder = 0
and path not like '%Metadata%'
ORDER BY Path

 

The Magic of Logparser

I had a slew of IIS FTP logs to dig through for a work project. I needed unique visitor details, and I needed it from a month of logs. Some of these daily logs were in excess of 40mb. I threw together a PowerShell script to do just so, kicked it off, and waited. And waited. I reconfigured the script to be more efficient in its processing, kicked it off again, and waited. And waited. And researched, because this was taking far too long. And – late to the game, I know – I found Microsoft’s Logparser. Logparser is a free tool, and it’s filled with magic. With one simple query it managed to pull the raw data I needed from the log files in *seconds*.

1
2
3
4
5
#log files in c:\temp\iis, results to same path

logparser "SELECT DISTINCT c-ip, cs-username INTO C:\temp\iis\results.txt FROM 'C:\temp\iis\*.log'" -i:W3C -o:W3C

#

In less than a minute I had a text file of unique visits gleaned from 550 MB worth of plaintext log files. From there I could use PowerShell to filter out any IP dupes (some connections do not have user details, thus producing some leftover duplication) and resolve hostnames from IP addresses to create a final masterlist. I’m not sure how Logparser does what it does so quickly.

Jellyfin as a Music Server – Duplicates

Updated query for better detail!

1
2
3
4
5
6
7
8
9
10
11
12
SELECT a.artists,a.album,a.IndexNumber,a.name,a.path
FROM TypedBaseItems a
JOIN (SELECT name, album, IndexNumber
FROM TypedBaseItems
WHERE album IS NOT NULL
AND IsFolder = 0
GROUP BY name, album, IndexNumber
HAVING COUNT(*) > 1 ) b
ON a.name = b.name
AND a.album = b.album
AND a.IndexNumber = b.IndexNumber
ORDER BY a.artists, a.album, a.IndexNumber, a.name

I run 2 Jellyfin servers – one for video, and one for music. In the world of streaming I’m old school in that I still curate a huge library of audio files. I don’t like being at the whim of streaming services, I don’t like how they treat artists, and I don’t like how I’m limited to artists on their platforms. I listen to a lot of obscure music across nearly all genres, and quite a bit of it can’t be found on *any* streaming service.

Jellyfin isn’t the perfect solution for a music server, but so far it’s the best I’ve found. Admittedly I may be biased due to my familiarity with the product, but every time I come across an alternative I give it a go only to wind up back at Jellyfin. Over the course of decades I’ve wound up with many duplicates in my library and, because often they aren’t identical in name or size, it’s not easy to identify them. The other day, as I saw yet another double listing for an album in Jellyfin, it dawned on me: Jellyfin knows these are dupes. Can I leverage it to my advantage?

Indeed I can.

First you’ll need DB Browser for SQLite. It’s free. Then you’ll want to locate your database file for Jellyfin. I’m running on Windows, and my path is

C:\Users\USERNAME\AppData\Local\Jellyfin\data\library.db

For the purpose of learning, I recommend making a copy of this file elsewhere to tinker with. If you accidentally change data in it, you could sink yourself.  Everything happening below is strictly reading data, not manipulating, but still. You’ll have to shut Jellyfin down to access this file in DB Browser. Open it up, head to the Execute SQL tab, slap this query in, and execute it.

1
2
3
4
5
6
7
8
9
10
SELECT a.artists,a.album,a.name,a.path
FROM TypedBaseItems a
JOIN (SELECT name, album
FROM TypedBaseItems
WHERE album IS NOT NULL
GROUP BY name, album
HAVING COUNT(*) > 1 ) b
ON a.name = b.name
AND a.album = b.album
ORDER BY a.name

This will return you a comma delimited list of dupes. It’s keying off of song title and album title. Mind you, it’s not perfect. It’s going to find dupes that aren’t here and there, especially on boxed sets and compilations where there might be multiple takes of the same tune. You could obviously add in track number (IndexNumber in the database) and leverage IsFolder to skip directory names (1 is yes, 0 is no), but for my purposes the above gave me accurate enough data to sort in a Google spreadsheet and start hacking away at my duplicates.