MacOS, especially on ARM-based Mac minis, offers a remarkably powerful and affordable platform for Jellyfin.
ARM Mac minis deliver exceptional transcoding performance thanks to Apple's M series processors and VideoToolBox hardware encoder. The output of VideoToolBox won't match the quality of software encoders, but I can't tell the difference without pausing playback and searching for fragments. This is particularly true when watching from outside my local network.
I initially sourced a base-model M1 Mac mini for just $300, easily handling eight simultaneous live transcode sessions without issue. After upgrading to an M4 Pro Mac mini, transcoding performance became essentially limitless for practical purposes.
Note: These are live transcode sessions, meaning that ffmpeg is encoding to keep up with realtime playback. A single high quality encoding session running with ffmpeg can exhaust the resources of either model. Although notably, when I did encode my library I experienced no lag when using Jellyfin to watch content simultaneously.
Moreover, macOS's native RAID capabilities add robust data security and flexibility. RAID arrays configured on macOS store volume information directly on the disks, allowing seamless migration between Macs. When I upgraded my Mac mini from an M1 to an M4, transitioning my media library was as straightforward as plugging the RAID array into the new machine—no data loss, no hassle.
Why Jellyfin over alternatives like Plex or Emby? Jellyfin is fully free and open-source, respects your privacy, does not pollute your library with content injections, and officially supports ARM builds for macOS. Crucially, Jellyfin leverages Apple's VideoToolBox hardware acceleration out-of-the-box. A capability notably absent in Emby, even on paid plans.
When I started this project I found resources for hosting on macOS limited despite the obvious benefits. Almost all existing resources are limited to getting Jellyfin started and connecting to it via the local network. It took a lot of trial and error to create a simple configuration that achieved the stability I desired. I wanted to provide a guide to others, so they could skip the hassle. The goals of this implementation are:
Note: If you already have a NAS, RAID array, or other storage solution, feel free to connect that it your Mac and skip this section.
To connect hard drives to your Mac, you will need a JBOD (Just a Bunch Of Disks) external drive enclosure. I don't recommend purchasing an enclosure with RAID capabilities built-in. In that case, if the hardware powering the raid array fails, your data will be forever lost. Using the software RAID provided by macOS, the drives simply need to be moved to a new enclosure and any Mac can rebuild the array.
The enclosures most recommended online are made by OWC and MediaSonic. These are the OWC enclosures I use for both my arrays.
If you are looking for hard drive recommendations, I use refurbished drives sold by goHardDrive, and they've lasted several years of heavy use without issue. Buying large drives at the start will be an investment, but will reduce the amount of drive enclosures you need to buy.
MacOS supports RAID 0, 1, and 10 (via building a RAID 0 on top of two RAID 1 arrays). There are only two setups I recommend, and each comes with their own advantages and disadvantages.
Combining two RAID 1 arrays into a single RAID 10 reduces the number of volumes you need to manage. This simplifies media organization, as moving files and directories within the same volume is instantaneous and requires fewer configurations in Jellyfin. If you prefer organizing media this way, larger volumes can be beneficial. However, the downside is that each RAID 10 array requires four drives upfront. At $150-300 per 10-14 TB drive, this can be a significant investment.
Use Apple's RAID array documentation to complete the next two steps.
Using RAID 1 provides the same data resiliency while requiring only two drives per array. Although you will need to update your libraries when adding a new RAID array, so too many arrays may become inconvenient. If you're placing media into a single directory and relying on Jellyfin for organization, RAID 1 might be the better option.
Use Apple's RAID array documentation to create a RAID 1 array.
MacOS's Disk Utility does not provide RAID 5/6 capabilities, and I've found no open source solutions that "Just work" on macOS. The only trustworthy solutions that run on Mac are paid, and require yearly subscription payments. I'd rather pay more upfront in terms of hard drives to have permanent access to my RAID configuration.
The folder structure I recommend for each RAID array is below. The metadata only needs to exist on one raid array,
but should be moved off the Mac Mini's storage as the metadata directory can get quite large as your library grows.
/Volumes/media0 // Root of the RAID array
- Jellyfin
- metadata // Only needed on one RAID array
- movies
- shows
Spotlight indexing is not useful for Jellyfin media directories, and it can create unnecessary disk activity on large libraries. You can exclude the media folders from Spotlight using macOS privacy settings.
System Settings app.Spotlight.
Siri & Spotlight.Search Privacy near the bottom of the page.
Spotlight Privacy.+ button and add each media directory you want Spotlight to ignore, such as /Volumes/media0/Jellyfin.Note: You can add an entire drive to Spotlight privacy, but Finder search relies on Spotlight and may stop finding files on that drive. This can be annoying if you, for example, download new media to the drive and search for
.rarfiles before extracting and sorting them. I recommend excluding top level media and metadata directories while leaving download or staging directories searchable.
In this step, we will only install Jellyfin and configure libraries. Remote access and SSL will be covered in the next section.
http://localhost:8096 in your browser.The media server won't be much good if it shuts down when not interacted with.
Settings app.Energy sections.Prevent automatic sleeping when the display is off.A media server that only serves your local network is nice, but not as nice as one that can be used while waiting in the airport. In this section we will set up everything required to allow remote connections into your server and even connect a domain.
You must forward port 80 for cert validation, and 443 to serve media over https. The instructions for this differ
based on your router model. Find your router on portforward.com, and forward ports 80 and 443 to your Mac.
It is possible to manage a ssl certificate yourself by generating it and enabling https in Jellyfin, but if you want to enable
metric collection later you should route incoming connections via a reverse proxy. Using caddy as a reverse proxy
is a great choice as it will generate and rotate your ssl certificate for you. We will use caddy with plugins later in
this guide, so we will start by building caddy from xcaddy without plugins.
Terminalgo install github.com/caddyserver/xcaddy/cmd/xcaddy@latest.caddy and enter it mkdir ~/Desktop/caddy && cd ~/Desktop/caddyxcaddy buildtouch > Caddyfile.0.0.0.0 {
@metrics {
path /metrics*
}
respond @metrics 403
reverse_proxy 127.0.0.1:8096
}
0.0.0.0 with your public IP address.caddy start --config Caddyfile in the same directory as your Caddyfile.You should now be able to connect to your server by entering your public IP address in the server field of the Jellyfin app.
Using your IP address works, but a domain name is much easier to remember. Follow this section if you've purchased a domain to link to your media server.
Only a single DNS A Record needs to be created to direct your hostname to your home IP address. To do this automatically
we will use the dynamic-dns caddy plugin. For this to work we need to also
install the complimentary DNS plugin for your DNS provider. In this guide, I will use Cloudflare and the respective
cloudflare-dns plugin. If you use a different DNS provider, search the caddy-dns GitHub group
to find the respective plugin for your provider.
cd ~/Desktop/caddyxcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-dynamicdnsCaddyfile with your domain name, and add a new global configuration block at the top.
{ # Global Configuration Block.
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN} # If not using Cloudflare, use the configuration for your DNS provider.
domains {
example.com # Place your domain here.
}
versions ipv4
}
}
example.com { # Replace your IP address with your domain.
@metrics {
path /metrics*
}
respond @metrics 403
reverse_proxy 127.0.0.1:8096
}
Run export CLOUDFLARE_API_TOKEN="TOKEN_VALUE" && caddy run --config Caddyfile in the same directory as your Caddyfile
and observe caddy creating your DNS A record. You should now be able to connect to your server via domain name.
To increase security you can add geo filtering to reject traffic from countries you don't expect.
xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-dynamicdns --with github.com/porech/caddy-maxmind-geolocationDownload Databases.Download GZIP on the GeoLite2 Country row of the database table.GeoLite2-Country.mmdb file into the caddy folder on your Desktop.cd ~/Desktop/caddy && echo "$(pwd)/GeoLite2-Country.mmdb"Caddyfile by creating a geo filer and applying it to the reverse proxy. Update the allow_countries input to your liking.
{ # If using dynamic dns
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
domains {
example.com
}
}
}
example.com {
@metrics {
path /metrics*
}
respond @metrics 403
@geofilter { # Create a new filer using a maxmind_geolocation plugin
maxmind_geolocation {
db_path "YOUR_GATHERED_MMDB_FILE_PATH" # Replace with the path to your mmdb file
allow_countries US # Update if the United States is not your location
}
}
# Update the reverse proxy to use the new geo filter!
reverse_proxy @geofilter 127.0.0.1:8096
}
Run export CLOUDFLARE_API_TOKEN="TOKEN_VALUE" && caddy run --config Caddyfile in the same directory as your Caddyfile
and make sure you can still connect to your server. You may use a VPN to test connection outside your selected countries.
Geo filtering blocks broad regions, but CrowdSec can add another layer by blocking IPs that CrowdSec has flagged from local Caddy logs or community decisions. This uses the free open-source CrowdSec engine; you do not need a paid account.
brew install podman. CrowdSec's security engine is not compiled for macOS, so it runs in a
Linux container. If you prefer Docker Desktop, replace podman with docker in the container commands below.podman machine init && podman machine startpodman infoxcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-dynamicdns --with github.com/porech/caddy-maxmind-geolocation --with github.com/hslatman/caddy-crowdsec-bouncer/httpcd ~/Desktop/caddy && touch access.log && touch crowdsec-acquis.yamlcrowdsec-acquis.yaml and enter the content below.
filenames:
- /var/log/caddy/access.log
labels:
type: caddy
access.log.
podman run -d --name crowdsec \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-e COLLECTIONS="crowdsecurity/caddy" \
-v crowdsec-config:/etc/crowdsec \
-v crowdsec-db:/var/lib/crowdsec/data \
-v ~/Desktop/caddy/crowdsec-acquis.yaml:/etc/crowdsec/acquis.yaml:ro \
-v ~/Desktop/caddy:/var/log/caddy:ro \
docker.io/crowdsecurity/crowdsec:latest
podman exec crowdsec cscli bouncers add caddy-bouncerAPI key for 'caddy-bouncer':
1234567890abcdefghijklmnopqrstuvwxyz
Please keep this key since you will not be able to retrieve it!
CROWDSEC_API_KEY used by the Caddyfile, the manual caddy run command, and the
launch agent below.cd ~/Desktop/caddy && echo "$(pwd)/access.log"Caddyfile by adding the CrowdSec global configuration, enabling the bouncer inside the site, and writing
Caddy access logs. Replace YOUR_CADDY_LOG_FILE_PATH with the path gathered in the previous step.
{
# Setup the crowdsec api and force caddy to use it first.
order crowdsec first
crowdsec {
api_url http://127.0.0.1:8080
api_key {env.CROWDSEC_API_KEY}
}
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
domains {
example.com
}
}
}
example.com {
@metrics {
path /metrics*
}
respond @metrics 403
log {
output file "YOUR_CADDY_LOG_FILE_PATH"
}
crowdsec
@geofilter {
maxmind_geolocation {
db_path "YOUR_GATHERED_MMDB_FILE_PATH"
allow_countries US
}
}
reverse_proxy @geofilter 127.0.0.1:8096
}
Run export CLOUDFLARE_API_TOKEN="TOKEN_VALUE" && export CROWDSEC_API_KEY="BOUNCER_KEY_VALUE" && caddy run --config Caddyfile
in the same directory as your Caddyfile and make sure you can still connect to your server. If you skipped Geo Filtering,
remove @geofilter from the reverse_proxy line.
By default, jellyfin and caddy won't perform a clean restart on their own. In this section we will write a LaunchScript to make sure everything starts in order.
Open the Script Editor application and create a new apple script.
Writing the log to an easy-to-reach location is convenient when something goes wrong, so I write it to a caddy folder
on my desktop that contains this start up script and my Caddyfile. To do this yourself, start the script with these two
lines and adjust the file path to your needs.
-- Update the file path to that of your choosing
set logFile to open for access POSIX file "/Users/mediaserver/Desktop/caddy/StartupLog.log" with write permission
-- An example of how to write to the log file.
write "Performing start up." & return to logFile
The log file must be closed, so at the bottom of your script add the following.
close access logFile
Jellyfin will fail to start if any of the directories used in the media libraries cant be accessed, and the RAID utility takes 10-30 seconds to make volumes available. To prevent an early launch add this function to your script that waits until a provided directory is ready before continuing.
on waitForVolume(volumePath, logFile)
repeat
set volumeExists to do shell script ("if [ -d " & quoted form of volumePath & " ]; then echo 'exists'; else echo 'no'; fi")
if volumeExists is "exists" then
exit repeat
else
write "Waiting for volume " & volumePath & " to mount." & return to logFile
delay 2
end if
end repeat
write "Volume " & volumePath & " is now mounted." & return to logFile
end waitForVolume
Then call that function as many times as you need providing the volume paths.
-- Check that the required volumes exist
waitForVolume("/Volumes/media0", logFile)
waitForVolume("/Volumes/media1", logFile)
Now Jellyfin can be launched using the code below.
-- Launch Jellyfin
tell application "Finder"
open application file "Jellyfin.app" of folder "Applications" of startup disk
end tell
write "Jellyfin Launched." & return to logFile
delay 1
Podman and the crowdsec container should be started before Caddy. This gives Caddy's CrowdSec
bouncer a running local API to connect to when Caddy starts. If you skipped CrowdSec Filtering,
don't include this block from your startup script.
-- Start Podman and CrowdSec
write "Starting Podman machine." & return to logFile
do shell script "/opt/homebrew/bin/podman machine start || true"
delay 5
write "Starting CrowdSec container." & return to logFile
do shell script "/opt/homebrew/bin/podman start crowdsec || true"
delay 2
If you have implemented Caddy as your reverse proxy, that can now be started.
-- Command to start Caddy with the specified CaddyFile
write "Starting Caddy server." & return to logFile
do shell script "/Users/mediaserver/Desktop/caddy/caddy start --config /Users/mediaserver/Desktop/caddy/CaddyFile"
Save this script in a place you will remember. I store mine along with my Caddyfile. You can view the full script here.
A launch agent is needed to execute the startup script.
Terminal.cd ~/Library/LaunchAgents to navigate to your launch agents.
LaunchAgents directory does not exist in Library, create and enter it with mkdir LaunchAgents && cd LaunchAgents.touch jellyfin.launch.plistEnvironmentVariables dictionary.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>CLOUDFLARE_API_TOKEN</key>
<string>REPLACE-WITH-YOUR-API-KEY</string>
<key>CROWDSEC_API_KEY</key>
<string>REPLACE-WITH-YOUR-BOUNCER-KEY</string>
</dict>
<key>Label</key>
<string>jellyfin.launch</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>/Users/mediaserver/Desktop/caddy/Startup.scpt</string>
</array>
</dict>
</plist>
The reference to osascript is required in the array. This is the Apple application that executes apple script files.
If you skipped CrowdSec Filtering, remove CROWDSEC_API_KEY from the environment variables.launchctl load jellyfin.launch.plist. On the first run, osascript may require
permissions, approve them.One of the negatives of hosting a media server on Mac is that volumes are not mounted and applications can't be loaded before someone logs in. The launch agent created in the previous section won't run until login. We can enable automatic login to handle restarts completely. This can't be done if FileVault is enabled.
Note: This will only log in the user locally, so remote access enabled in the next section will still require authentication. This is a guide for hosting at home. If a bad actor is in your house, then you have issues more pressing than your home movie collection.
Settings.Users & Groups.Automatically log in as and select your user.The server may now restart and have Jellyfin, CrowdSec, and Caddy elegantly resume operation.
Even with Spotlight disabled, macOS may still scan large media libraries with photoanalysisd, mediaanalysisd, and
mediaanalysisd-access. These are macOS background services that analyze photos, videos, and other media for features like
faces, objects, scenes, text, memories, and search indexing. Once enough content is loaded these processes can consume
around 20% CPU. Removing these processes outright would require disabling System Integrity Protection (SIP) and deleting
the respective process plist files, but there are warnings online about this breaking some system components, and it would
require keeping SIP disabled. Not a good solution. To solve this without requiring readers to purchase anything, I built
OpenTamer, a completely free and open source AppTamer replacement.
OpenTamer-...-macos-arm64.zip from the OpenTamer releases page.OpenTamer.app to Applications, and launch it.Preferences > General, and enable Launch at Login.All Processes, find mediaanalysisd, open Limit CPU..., and select 0.01%.mediaanalysisd-access and photoanalysisd.OpenTamer saves these rules and reapplies them as the processes restart so the scanners stay suppressed.
Remotely controlling the media server with another mac is easy by enabling screen share.
Terminal and run one of the following commands to find the servers IP address on your local network.
ipconfig getifaddr en1 - For wireless connections.ipconfig getifaddr en0 - For Ethernet connections.vnc://LOCAL_IP_ADDRESS.You will be controlling the screen of the automatically logged-in user.
You can port forward ports 5900-5902 on your router to enable VNC access outside your local network, but I don't
recommend opening services unless you absolutely require it. This can be done for a short
time if you need to enable access while you are on an extended stay outside your home.
Monitoring a personal project is fun. In this section we will set up monitoring services that will use minimal resources while providing long term metrics and dashboard displays for:
Usernames cleared out for privacy

The /metrics endpoint can't be enabled in the Jellyfin UI. The system.xml file must be edited manually.
~/Library/Application Support/jellyfin/config/system.xml with the text editor of your choice.<EnableMetrics>false</EnableMetrics> line and change false to true.The current metrics provided directly by Jellyfin are poor and some are bugged, so only a handful will be kept to be used in the Dashboard. There is an issue open for Jellyfin to improve metrics, but there has been no activity in some time. To get around this, I built a minimal api to get the metrics required from the Jellyfin API. Other solutions for this exist, but many of them have excessive cardinality in metric labels which will balloon the size of your Prometheus data directory.
If you want your dashboard to show media library size, connected clients, and active streams then install my jelly-metrics api.
brew install go.
brew upgrade go if your go version is under 1.23.6.go install github.com/Digital-Shane/jelly-metrics@latestAPI Keys+ icon, enter metrics for the app name, and click ok.EnvironmentVariables key and dict like below. Make sure
to replace the string REPLACE-WITH-YOUR-JELLYFIN-API-KEY with the API key you just generated. Keep your DNS API key!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>CLOUDFLARE_API_TOKEN</key>
<string>REPLACE-WITH-YOUR-API-KEY</string>
<key>JELLYFIN_TOKEN</key>
<string>REPLACE-WITH-YOUR-JELLYFIN-API-KEY</string>
</dict>
<key>Label</key>
<string>jellyfin.launch</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>/Users/mediaserver/Desktop/caddy/Startup.scpt</string>
</array>
</dict>
</plist>
macOS does not use /proc so the standard linux process-exporter does not work. I built a custom exporter that supports the
macOS by using Shirou's gopsutil library. If you want detailed CPU and Memory metrics for your server install it with:
go install github.com/Digital-Shane/macos-process-exporter@latest
The remaining tools may be installed via brew. Run the following in Terminal.
# Exports CPU, Memory, and Network Interface metrics
brew install node_exporter
# Consumes metrics from node_exporter, Jellyfin, and jelly-metrics and stores them in a database
brew install prometheus
# Reads the metrics database to create data visualizations
brew install grafana
Two files must be edited to configure Prometheus.
/opt/homebrew/etc/prometheus.args with the text editor of your choice.--storage.tsdb.path to configure the data directory. Offload this directory to a raid array if
you want to store extended data.
/Volumes/media0/prometheus.--storage.tsdb.retention.time to configure the length of time to store metrics.
5y for five years of data./opt/homebrew/etc/prometheus.yml next.global:
scrape_interval: 30s
scrape_configs:
- job_name: "node_exporter"
static_configs:
- targets: ["localhost:9100"]
metric_relabel_configs:
- source_labels: [__name__]
# Keep only these metrics
regex: "(node_boot_time_seconds|node_cpu_seconds_total|node_disk_read_bytes_total|node_disk_read_time_seconds_total|node_disk_reads_completed_total|node_disk_write_time_seconds_total|node_disk_writes_completed_total|node_disk_written_bytes_total|node_filesystem_avail_bytes|node_filesystem_size_bytes|node_load1|node_load5|node_load15|node_memory_active_bytes|node_memory_free_bytes|node_memory_inactive_bytes|node_memory_purgeable_bytes|node_memory_swap_total_bytes|node_memory_wired_bytes|node_network_receive_bytes_total|node_network_receive_errs_total|node_network_receive_packets_total|node_network_transmit_bytes_total|node_network_transmit_errs_total|node_network_transmit_packets_total|node_scrape_collector_duration_seconds|node_time_seconds)"
action: keep
- job_name: "jellyfin"
static_configs:
- targets: ["localhost:8096"]
metric_relabel_configs:
- source_labels: [__name__]
# Keep only these metrics
regex: "(http_requests_in_progress)"
action: keep
- job_name: "jellyfin-metrics" # Remove this job if not using jelly-metrics!
static_configs:
- targets: ["localhost:8097"]
- job_name: "macos-process-metrics" # Remove this job if not using macos-process-exporter!
static_configs:
- targets: ["localhost:9105"]
brew services restart prometheus.This configuration will keep only metrics that are used in the later Grafana dashboard to minimize the Prometheus data directory.
Next connect Grafana to the Prometheus data store.
http://localhost:3000/admin | Password: adminConnections > Data sources.Add new data source.Prometheus.http://localhost:9090 as the Prometheus server URL.Save and test.http://localhost:3000/ and login.Dashboards.New > Import.Upload dashboard JSON file.grafana-dashboard.json file.Import.A few updates are required in the startup script to support our monitoring tools.
ScriptEditor.jelly-metrics. In this block of code I launch a terminal
to nohup launch jelly-metrics. This is required for the process to be properly disconnected from the script's ownership.
For some odd reason, do shell script is not sufficient and will result in jelly-metrics exiting when the startup
script terminates. Launching a terminal to run the nohup command gets around this issue. Skip this step
If you opted to not use jelly metrics.
-- Launch the jelly-metrics metrics exporter
write "Launching Jelly Metrics" & return to logFile
tell application "Terminal"
do script "nohup /Users/mediaserver/go/bin/jelly-metrics > /dev/null 2>&1 &"
delay 3
quit
end tell
-- Launch Prometheus Now that Volumes are mounted and metric sources have started
write "Restarting Prometheus" & return to logFile
do shell script "/opt/homebrew/bin/brew services restart prometheus"
delay 1
You can view the full script with metric handling here.
Shout out to ErsatzTV an IPTV server that lets you create live TV channels from your Jellyfin content. They release a build for ARM Macs. I highly recommend it once you have built up some content to add a new way to enjoy your content. Great for when you want to sit down and watch television for 20 minutes, but don't want to commit to a show you are actively engaged in.
You can update your startup script to support ErsatzTV by launching the application after Jellyfin
-- Launch ErsatzTV
tell application "Finder"
open application file "ErsatzTV.app" of folder "Applications" of startup disk
end tell
write "ErsatzTV Launched." & return to logFile
delay 1
You can view the full script with metric handling and ErsatzTV here.
I recommend backing up Jellyfin's database before major version updates. Each backups require several GBs of space. Backing up my database alone consumes 8 GB. Including metadata, subtitles, and trickplay images increases the size to 17 GB. I choose to backup only my database. The remaining items can be rebuild by Jellyfin with time. Jellyfin added a method of backing up the database right in the UI:
Backups under the advanced settings.+ Create Backup, and Create.ErsatzTV has no method to create backups in its UI. To get around this I created an applescript that backups ErsatzTV's critical data directories. Make sure to update the storage path for backups. Save the script to your machine and run it before updating ErsatzTV.
-- ErsatzTV Backup
-- TODO Update backup storage location
set backupRootDir to "/Volumes/media1/Backups/ErsatzTV"
set homeDir to POSIX path of (path to home folder)
set timeStamp to do shell script "date +%Y-%m-%d"
-- Kill the running instance
try
do shell script "killall 'ErsatzTV' 'ErsatzTV-macOS'"
end try
-- Build backup directory path
set ersBackupDir to backupRootDir & "/" & timeStamp & "_" & version
do shell script "mkdir -p " & quoted form of ersBackupDir
-- Copy ErsatzTV application support directory
set ersDataDir to homeDir & "Library/Application Support/ersatztv"
do shell script "cp -a " & quoted form of ersDataDir & " " & quoted form of ersBackupDir
-- Launch ErsatzTV
tell application "Finder"
open application file "ErsatzTV.app" of folder "Applications" of startup disk
end tell
19 commits
MacOS, especially on ARM-based Mac minis, offers a remarkably powerful and affordable platform for Jellyfin.
ARM Mac minis deliver exceptional transcoding performance thanks to Apple's M series processors and VideoToolBox hardware encoder. The output of VideoToolBox won't match the quality of software encoders, but I can't tell the difference without pausing playback and searching for fragments. This is particularly true when watching from outside my local network.
I initially sourced a base-model M1 Mac mini for just $300, easily handling eight simultaneous live transcode sessions without issue. After upgrading to an M4 Pro Mac mini, transcoding performance became essentially limitless for practical purposes.
Note: These are live transcode sessions, meaning that ffmpeg is encoding to keep up with realtime playback. A single high quality encoding session running with ffmpeg can exhaust the resources of either model. Although notably, when I did encode my library I experienced no lag when using Jellyfin to watch content simultaneously.
Moreover, macOS's native RAID capabilities add robust data security and flexibility. RAID arrays configured on macOS store volume information directly on the disks, allowing seamless migration between Macs. When I upgraded my Mac mini from an M1 to an M4, transitioning my media library was as straightforward as plugging the RAID array into the new machine—no data loss, no hassle.
Why Jellyfin over alternatives like Plex or Emby? Jellyfin is fully free and open-source, respects your privacy, does not pollute your library with content injections, and officially supports ARM builds for macOS. Crucially, Jellyfin leverages Apple's VideoToolBox hardware acceleration out-of-the-box. A capability notably absent in Emby, even on paid plans.
When I started this project I found resources for hosting on macOS limited despite the obvious benefits. Almost all existing resources are limited to getting Jellyfin started and connecting to it via the local network. It took a lot of trial and error to create a simple configuration that achieved the stability I desired. I wanted to provide a guide to others, so they could skip the hassle. The goals of this implementation are:
Note: If you already have a NAS, RAID array, or other storage solution, feel free to connect that it your Mac and skip this section.
To connect hard drives to your Mac, you will need a JBOD (Just a Bunch Of Disks) external drive enclosure. I don't recommend purchasing an enclosure with RAID capabilities built-in. In that case, if the hardware powering the raid array fails, your data will be forever lost. Using the software RAID provided by macOS, the drives simply need to be moved to a new enclosure and any Mac can rebuild the array.
The enclosures most recommended online are made by OWC and MediaSonic. These are the OWC enclosures I use for both my arrays.
If you are looking for hard drive recommendations, I use refurbished drives sold by goHardDrive, and they've lasted several years of heavy use without issue. Buying large drives at the start will be an investment, but will reduce the amount of drive enclosures you need to buy.
MacOS supports RAID 0, 1, and 10 (via building a RAID 0 on top of two RAID 1 arrays). There are only two setups I recommend, and each comes with their own advantages and disadvantages.
Combining two RAID 1 arrays into a single RAID 10 reduces the number of volumes you need to manage. This simplifies media organization, as moving files and directories within the same volume is instantaneous and requires fewer configurations in Jellyfin. If you prefer organizing media this way, larger volumes can be beneficial. However, the downside is that each RAID 10 array requires four drives upfront. At $150-300 per 10-14 TB drive, this can be a significant investment.
Use Apple's RAID array documentation to complete the next two steps.
Using RAID 1 provides the same data resiliency while requiring only two drives per array. Although you will need to update your libraries when adding a new RAID array, so too many arrays may become inconvenient. If you're placing media into a single directory and relying on Jellyfin for organization, RAID 1 might be the better option.
Use Apple's RAID array documentation to create a RAID 1 array.
MacOS's Disk Utility does not provide RAID 5/6 capabilities, and I've found no open source solutions that "Just work" on macOS. The only trustworthy solutions that run on Mac are paid, and require yearly subscription payments. I'd rather pay more upfront in terms of hard drives to have permanent access to my RAID configuration.
The folder structure I recommend for each RAID array is below. The metadata only needs to exist on one raid array,
but should be moved off the Mac Mini's storage as the metadata directory can get quite large as your library grows.
/Volumes/media0 // Root of the RAID array
- Jellyfin
- metadata // Only needed on one RAID array
- movies
- shows
Spotlight indexing is not useful for Jellyfin media directories, and it can create unnecessary disk activity on large libraries. You can exclude the media folders from Spotlight using macOS privacy settings.
System Settings app.Spotlight.
Siri & Spotlight.Search Privacy near the bottom of the page.
Spotlight Privacy.+ button and add each media directory you want Spotlight to ignore, such as /Volumes/media0/Jellyfin.Note: You can add an entire drive to Spotlight privacy, but Finder search relies on Spotlight and may stop finding files on that drive. This can be annoying if you, for example, download new media to the drive and search for
.rarfiles before extracting and sorting them. I recommend excluding top level media and metadata directories while leaving download or staging directories searchable.
In this step, we will only install Jellyfin and configure libraries. Remote access and SSL will be covered in the next section.
http://localhost:8096 in your browser.The media server won't be much good if it shuts down when not interacted with.
Settings app.Energy sections.Prevent automatic sleeping when the display is off.A media server that only serves your local network is nice, but not as nice as one that can be used while waiting in the airport. In this section we will set up everything required to allow remote connections into your server and even connect a domain.
You must forward port 80 for cert validation, and 443 to serve media over https. The instructions for this differ
based on your router model. Find your router on portforward.com, and forward ports 80 and 443 to your Mac.
It is possible to manage a ssl certificate yourself by generating it and enabling https in Jellyfin, but if you want to enable
metric collection later you should route incoming connections via a reverse proxy. Using caddy as a reverse proxy
is a great choice as it will generate and rotate your ssl certificate for you. We will use caddy with plugins later in
this guide, so we will start by building caddy from xcaddy without plugins.
Terminalgo install github.com/caddyserver/xcaddy/cmd/xcaddy@latest.caddy and enter it mkdir ~/Desktop/caddy && cd ~/Desktop/caddyxcaddy buildtouch > Caddyfile.0.0.0.0 {
@metrics {
path /metrics*
}
respond @metrics 403
reverse_proxy 127.0.0.1:8096
}
0.0.0.0 with your public IP address.caddy start --config Caddyfile in the same directory as your Caddyfile.You should now be able to connect to your server by entering your public IP address in the server field of the Jellyfin app.
Using your IP address works, but a domain name is much easier to remember. Follow this section if you've purchased a domain to link to your media server.
Only a single DNS A Record needs to be created to direct your hostname to your home IP address. To do this automatically
we will use the dynamic-dns caddy plugin. For this to work we need to also
install the complimentary DNS plugin for your DNS provider. In this guide, I will use Cloudflare and the respective
cloudflare-dns plugin. If you use a different DNS provider, search the caddy-dns GitHub group
to find the respective plugin for your provider.
cd ~/Desktop/caddyxcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-dynamicdnsCaddyfile with your domain name, and add a new global configuration block at the top.
{ # Global Configuration Block.
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN} # If not using Cloudflare, use the configuration for your DNS provider.
domains {
example.com # Place your domain here.
}
versions ipv4
}
}
example.com { # Replace your IP address with your domain.
@metrics {
path /metrics*
}
respond @metrics 403
reverse_proxy 127.0.0.1:8096
}
Run export CLOUDFLARE_API_TOKEN="TOKEN_VALUE" && caddy run --config Caddyfile in the same directory as your Caddyfile
and observe caddy creating your DNS A record. You should now be able to connect to your server via domain name.
To increase security you can add geo filtering to reject traffic from countries you don't expect.
xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-dynamicdns --with github.com/porech/caddy-maxmind-geolocationDownload Databases.Download GZIP on the GeoLite2 Country row of the database table.GeoLite2-Country.mmdb file into the caddy folder on your Desktop.cd ~/Desktop/caddy && echo "$(pwd)/GeoLite2-Country.mmdb"Caddyfile by creating a geo filer and applying it to the reverse proxy. Update the allow_countries input to your liking.
{ # If using dynamic dns
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
domains {
example.com
}
}
}
example.com {
@metrics {
path /metrics*
}
respond @metrics 403
@geofilter { # Create a new filer using a maxmind_geolocation plugin
maxmind_geolocation {
db_path "YOUR_GATHERED_MMDB_FILE_PATH" # Replace with the path to your mmdb file
allow_countries US # Update if the United States is not your location
}
}
# Update the reverse proxy to use the new geo filter!
reverse_proxy @geofilter 127.0.0.1:8096
}
Run export CLOUDFLARE_API_TOKEN="TOKEN_VALUE" && caddy run --config Caddyfile in the same directory as your Caddyfile
and make sure you can still connect to your server. You may use a VPN to test connection outside your selected countries.
Geo filtering blocks broad regions, but CrowdSec can add another layer by blocking IPs that CrowdSec has flagged from local Caddy logs or community decisions. This uses the free open-source CrowdSec engine; you do not need a paid account.
brew install podman. CrowdSec's security engine is not compiled for macOS, so it runs in a
Linux container. If you prefer Docker Desktop, replace podman with docker in the container commands below.podman machine init && podman machine startpodman infoxcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-dynamicdns --with github.com/porech/caddy-maxmind-geolocation --with github.com/hslatman/caddy-crowdsec-bouncer/httpcd ~/Desktop/caddy && touch access.log && touch crowdsec-acquis.yamlcrowdsec-acquis.yaml and enter the content below.
filenames:
- /var/log/caddy/access.log
labels:
type: caddy
access.log.
podman run -d --name crowdsec \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-e COLLECTIONS="crowdsecurity/caddy" \
-v crowdsec-config:/etc/crowdsec \
-v crowdsec-db:/var/lib/crowdsec/data \
-v ~/Desktop/caddy/crowdsec-acquis.yaml:/etc/crowdsec/acquis.yaml:ro \
-v ~/Desktop/caddy:/var/log/caddy:ro \
docker.io/crowdsecurity/crowdsec:latest
podman exec crowdsec cscli bouncers add caddy-bouncerAPI key for 'caddy-bouncer':
1234567890abcdefghijklmnopqrstuvwxyz
Please keep this key since you will not be able to retrieve it!
CROWDSEC_API_KEY used by the Caddyfile, the manual caddy run command, and the
launch agent below.cd ~/Desktop/caddy && echo "$(pwd)/access.log"Caddyfile by adding the CrowdSec global configuration, enabling the bouncer inside the site, and writing
Caddy access logs. Replace YOUR_CADDY_LOG_FILE_PATH with the path gathered in the previous step.
{
# Setup the crowdsec api and force caddy to use it first.
order crowdsec first
crowdsec {
api_url http://127.0.0.1:8080
api_key {env.CROWDSEC_API_KEY}
}
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
domains {
example.com
}
}
}
example.com {
@metrics {
path /metrics*
}
respond @metrics 403
log {
output file "YOUR_CADDY_LOG_FILE_PATH"
}
crowdsec
@geofilter {
maxmind_geolocation {
db_path "YOUR_GATHERED_MMDB_FILE_PATH"
allow_countries US
}
}
reverse_proxy @geofilter 127.0.0.1:8096
}
Run export CLOUDFLARE_API_TOKEN="TOKEN_VALUE" && export CROWDSEC_API_KEY="BOUNCER_KEY_VALUE" && caddy run --config Caddyfile
in the same directory as your Caddyfile and make sure you can still connect to your server. If you skipped Geo Filtering,
remove @geofilter from the reverse_proxy line.
By default, jellyfin and caddy won't perform a clean restart on their own. In this section we will write a LaunchScript to make sure everything starts in order.
Open the Script Editor application and create a new apple script.
Writing the log to an easy-to-reach location is convenient when something goes wrong, so I write it to a caddy folder
on my desktop that contains this start up script and my Caddyfile. To do this yourself, start the script with these two
lines and adjust the file path to your needs.
-- Update the file path to that of your choosing
set logFile to open for access POSIX file "/Users/mediaserver/Desktop/caddy/StartupLog.log" with write permission
-- An example of how to write to the log file.
write "Performing start up." & return to logFile
The log file must be closed, so at the bottom of your script add the following.
close access logFile
Jellyfin will fail to start if any of the directories used in the media libraries cant be accessed, and the RAID utility takes 10-30 seconds to make volumes available. To prevent an early launch add this function to your script that waits until a provided directory is ready before continuing.
on waitForVolume(volumePath, logFile)
repeat
set volumeExists to do shell script ("if [ -d " & quoted form of volumePath & " ]; then echo 'exists'; else echo 'no'; fi")
if volumeExists is "exists" then
exit repeat
else
write "Waiting for volume " & volumePath & " to mount." & return to logFile
delay 2
end if
end repeat
write "Volume " & volumePath & " is now mounted." & return to logFile
end waitForVolume
Then call that function as many times as you need providing the volume paths.
-- Check that the required volumes exist
waitForVolume("/Volumes/media0", logFile)
waitForVolume("/Volumes/media1", logFile)
Now Jellyfin can be launched using the code below.
-- Launch Jellyfin
tell application "Finder"
open application file "Jellyfin.app" of folder "Applications" of startup disk
end tell
write "Jellyfin Launched." & return to logFile
delay 1
Podman and the crowdsec container should be started before Caddy. This gives Caddy's CrowdSec
bouncer a running local API to connect to when Caddy starts. If you skipped CrowdSec Filtering,
don't include this block from your startup script.
-- Start Podman and CrowdSec
write "Starting Podman machine." & return to logFile
do shell script "/opt/homebrew/bin/podman machine start || true"
delay 5
write "Starting CrowdSec container." & return to logFile
do shell script "/opt/homebrew/bin/podman start crowdsec || true"
delay 2
If you have implemented Caddy as your reverse proxy, that can now be started.
-- Command to start Caddy with the specified CaddyFile
write "Starting Caddy server." & return to logFile
do shell script "/Users/mediaserver/Desktop/caddy/caddy start --config /Users/mediaserver/Desktop/caddy/CaddyFile"
Save this script in a place you will remember. I store mine along with my Caddyfile. You can view the full script here.
A launch agent is needed to execute the startup script.
Terminal.cd ~/Library/LaunchAgents to navigate to your launch agents.
LaunchAgents directory does not exist in Library, create and enter it with mkdir LaunchAgents && cd LaunchAgents.touch jellyfin.launch.plistEnvironmentVariables dictionary.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>CLOUDFLARE_API_TOKEN</key>
<string>REPLACE-WITH-YOUR-API-KEY</string>
<key>CROWDSEC_API_KEY</key>
<string>REPLACE-WITH-YOUR-BOUNCER-KEY</string>
</dict>
<key>Label</key>
<string>jellyfin.launch</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>/Users/mediaserver/Desktop/caddy/Startup.scpt</string>
</array>
</dict>
</plist>
The reference to osascript is required in the array. This is the Apple application that executes apple script files.
If you skipped CrowdSec Filtering, remove CROWDSEC_API_KEY from the environment variables.launchctl load jellyfin.launch.plist. On the first run, osascript may require
permissions, approve them.One of the negatives of hosting a media server on Mac is that volumes are not mounted and applications can't be loaded before someone logs in. The launch agent created in the previous section won't run until login. We can enable automatic login to handle restarts completely. This can't be done if FileVault is enabled.
Note: This will only log in the user locally, so remote access enabled in the next section will still require authentication. This is a guide for hosting at home. If a bad actor is in your house, then you have issues more pressing than your home movie collection.
Settings.Users & Groups.Automatically log in as and select your user.The server may now restart and have Jellyfin, CrowdSec, and Caddy elegantly resume operation.
Even with Spotlight disabled, macOS may still scan large media libraries with photoanalysisd, mediaanalysisd, and
mediaanalysisd-access. These are macOS background services that analyze photos, videos, and other media for features like
faces, objects, scenes, text, memories, and search indexing. Once enough content is loaded these processes can consume
around 20% CPU. Removing these processes outright would require disabling System Integrity Protection (SIP) and deleting
the respective process plist files, but there are warnings online about this breaking some system components, and it would
require keeping SIP disabled. Not a good solution. To solve this without requiring readers to purchase anything, I built
OpenTamer, a completely free and open source AppTamer replacement.
OpenTamer-...-macos-arm64.zip from the OpenTamer releases page.OpenTamer.app to Applications, and launch it.Preferences > General, and enable Launch at Login.All Processes, find mediaanalysisd, open Limit CPU..., and select 0.01%.mediaanalysisd-access and photoanalysisd.OpenTamer saves these rules and reapplies them as the processes restart so the scanners stay suppressed.
Remotely controlling the media server with another mac is easy by enabling screen share.
Terminal and run one of the following commands to find the servers IP address on your local network.
ipconfig getifaddr en1 - For wireless connections.ipconfig getifaddr en0 - For Ethernet connections.vnc://LOCAL_IP_ADDRESS.You will be controlling the screen of the automatically logged-in user.
You can port forward ports 5900-5902 on your router to enable VNC access outside your local network, but I don't
recommend opening services unless you absolutely require it. This can be done for a short
time if you need to enable access while you are on an extended stay outside your home.
Monitoring a personal project is fun. In this section we will set up monitoring services that will use minimal resources while providing long term metrics and dashboard displays for:
Usernames cleared out for privacy

The /metrics endpoint can't be enabled in the Jellyfin UI. The system.xml file must be edited manually.
~/Library/Application Support/jellyfin/config/system.xml with the text editor of your choice.<EnableMetrics>false</EnableMetrics> line and change false to true.The current metrics provided directly by Jellyfin are poor and some are bugged, so only a handful will be kept to be used in the Dashboard. There is an issue open for Jellyfin to improve metrics, but there has been no activity in some time. To get around this, I built a minimal api to get the metrics required from the Jellyfin API. Other solutions for this exist, but many of them have excessive cardinality in metric labels which will balloon the size of your Prometheus data directory.
If you want your dashboard to show media library size, connected clients, and active streams then install my jelly-metrics api.
brew install go.
brew upgrade go if your go version is under 1.23.6.go install github.com/Digital-Shane/jelly-metrics@latestAPI Keys+ icon, enter metrics for the app name, and click ok.EnvironmentVariables key and dict like below. Make sure
to replace the string REPLACE-WITH-YOUR-JELLYFIN-API-KEY with the API key you just generated. Keep your DNS API key!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>CLOUDFLARE_API_TOKEN</key>
<string>REPLACE-WITH-YOUR-API-KEY</string>
<key>JELLYFIN_TOKEN</key>
<string>REPLACE-WITH-YOUR-JELLYFIN-API-KEY</string>
</dict>
<key>Label</key>
<string>jellyfin.launch</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>/Users/mediaserver/Desktop/caddy/Startup.scpt</string>
</array>
</dict>
</plist>
macOS does not use /proc so the standard linux process-exporter does not work. I built a custom exporter that supports the
macOS by using Shirou's gopsutil library. If you want detailed CPU and Memory metrics for your server install it with:
go install github.com/Digital-Shane/macos-process-exporter@latest
The remaining tools may be installed via brew. Run the following in Terminal.
# Exports CPU, Memory, and Network Interface metrics
brew install node_exporter
# Consumes metrics from node_exporter, Jellyfin, and jelly-metrics and stores them in a database
brew install prometheus
# Reads the metrics database to create data visualizations
brew install grafana
Two files must be edited to configure Prometheus.
/opt/homebrew/etc/prometheus.args with the text editor of your choice.--storage.tsdb.path to configure the data directory. Offload this directory to a raid array if
you want to store extended data.
/Volumes/media0/prometheus.--storage.tsdb.retention.time to configure the length of time to store metrics.
5y for five years of data./opt/homebrew/etc/prometheus.yml next.global:
scrape_interval: 30s
scrape_configs:
- job_name: "node_exporter"
static_configs:
- targets: ["localhost:9100"]
metric_relabel_configs:
- source_labels: [__name__]
# Keep only these metrics
regex: "(node_boot_time_seconds|node_cpu_seconds_total|node_disk_read_bytes_total|node_disk_read_time_seconds_total|node_disk_reads_completed_total|node_disk_write_time_seconds_total|node_disk_writes_completed_total|node_disk_written_bytes_total|node_filesystem_avail_bytes|node_filesystem_size_bytes|node_load1|node_load5|node_load15|node_memory_active_bytes|node_memory_free_bytes|node_memory_inactive_bytes|node_memory_purgeable_bytes|node_memory_swap_total_bytes|node_memory_wired_bytes|node_network_receive_bytes_total|node_network_receive_errs_total|node_network_receive_packets_total|node_network_transmit_bytes_total|node_network_transmit_errs_total|node_network_transmit_packets_total|node_scrape_collector_duration_seconds|node_time_seconds)"
action: keep
- job_name: "jellyfin"
static_configs:
- targets: ["localhost:8096"]
metric_relabel_configs:
- source_labels: [__name__]
# Keep only these metrics
regex: "(http_requests_in_progress)"
action: keep
- job_name: "jellyfin-metrics" # Remove this job if not using jelly-metrics!
static_configs:
- targets: ["localhost:8097"]
- job_name: "macos-process-metrics" # Remove this job if not using macos-process-exporter!
static_configs:
- targets: ["localhost:9105"]
brew services restart prometheus.This configuration will keep only metrics that are used in the later Grafana dashboard to minimize the Prometheus data directory.
Next connect Grafana to the Prometheus data store.
http://localhost:3000/admin | Password: adminConnections > Data sources.Add new data source.Prometheus.http://localhost:9090 as the Prometheus server URL.Save and test.http://localhost:3000/ and login.Dashboards.New > Import.Upload dashboard JSON file.grafana-dashboard.json file.Import.A few updates are required in the startup script to support our monitoring tools.
ScriptEditor.jelly-metrics. In this block of code I launch a terminal
to nohup launch jelly-metrics. This is required for the process to be properly disconnected from the script's ownership.
For some odd reason, do shell script is not sufficient and will result in jelly-metrics exiting when the startup
script terminates. Launching a terminal to run the nohup command gets around this issue. Skip this step
If you opted to not use jelly metrics.
-- Launch the jelly-metrics metrics exporter
write "Launching Jelly Metrics" & return to logFile
tell application "Terminal"
do script "nohup /Users/mediaserver/go/bin/jelly-metrics > /dev/null 2>&1 &"
delay 3
quit
end tell
-- Launch Prometheus Now that Volumes are mounted and metric sources have started
write "Restarting Prometheus" & return to logFile
do shell script "/opt/homebrew/bin/brew services restart prometheus"
delay 1
You can view the full script with metric handling here.
Shout out to ErsatzTV an IPTV server that lets you create live TV channels from your Jellyfin content. They release a build for ARM Macs. I highly recommend it once you have built up some content to add a new way to enjoy your content. Great for when you want to sit down and watch television for 20 minutes, but don't want to commit to a show you are actively engaged in.
You can update your startup script to support ErsatzTV by launching the application after Jellyfin
-- Launch ErsatzTV
tell application "Finder"
open application file "ErsatzTV.app" of folder "Applications" of startup disk
end tell
write "ErsatzTV Launched." & return to logFile
delay 1
You can view the full script with metric handling and ErsatzTV here.
I recommend backing up Jellyfin's database before major version updates. Each backups require several GBs of space. Backing up my database alone consumes 8 GB. Including metadata, subtitles, and trickplay images increases the size to 17 GB. I choose to backup only my database. The remaining items can be rebuild by Jellyfin with time. Jellyfin added a method of backing up the database right in the UI:
Backups under the advanced settings.+ Create Backup, and Create.ErsatzTV has no method to create backups in its UI. To get around this I created an applescript that backups ErsatzTV's critical data directories. Make sure to update the storage path for backups. Save the script to your machine and run it before updating ErsatzTV.
-- ErsatzTV Backup
-- TODO Update backup storage location
set backupRootDir to "/Volumes/media1/Backups/ErsatzTV"
set homeDir to POSIX path of (path to home folder)
set timeStamp to do shell script "date +%Y-%m-%d"
-- Kill the running instance
try
do shell script "killall 'ErsatzTV' 'ErsatzTV-macOS'"
end try
-- Build backup directory path
set ersBackupDir to backupRootDir & "/" & timeStamp & "_" & version
do shell script "mkdir -p " & quoted form of ersBackupDir
-- Copy ErsatzTV application support directory
set ersDataDir to homeDir & "Library/Application Support/ersatztv"
do shell script "cp -a " & quoted form of ersDataDir & " " & quoted form of ersBackupDir
-- Launch ErsatzTV
tell application "Finder"
open application file "ErsatzTV.app" of folder "Applications" of startup disk
end tell
19 commits