My media center setup consists of a Synology NAS and a Mac Mini. I tried to run Plex Media Server on the NAS for a while, but eventually found it too sluggish for transcoding, so I decided to move PMS over to the Mac Mini and just use the NAS as a storage device. One thing that's bothered me, though, is that PMS doesn't auto-update the library when there are changes on the network shares, so I either have to update manually or wait for the library update interval to kick in.
So, I wrote a script to fix that (borrowed heavily from here).
The script works by tying into inotify on the NAS, which is a Linux kernel subsystem that notices changes to the filesystem and reports those changes to applications. Inotify monitors the media directories on the NAS for changes, then connects to the remote Plex Server's web API to find the appropriate media section to refresh. If it finds a matching section, it uses the web API to send an update command to that section.
There's a bit of work involved in getting this up and running. This assumes you're running DSM6, and that you have shell access with admin/root privileges.
Make sure you have "Python3" installed from the Synology Package Manager first.
You'll need to install the "pynotify" Python module. The easiest way is to install the Python EasyInstall utility; Shell into your NAS, and run:
wget https://bootstrap.pypa.io/ez_setup.py -O - | python3
then run:
/var/packages/py3k/target/usr/local/bin/easy_install pyinotify
Save this script somewhere on your NAS as plex_notify.py
:
# PLEX NOTIFIER SCRIPT v1.0
# Written by Talisto: https://forums.plex.tv/profile/talisto
# Modified heavily from https://codesourcery.wordpress.com/2012/11/29/more-on-the-synology-nas-automatically-indexing-new-files/
###################################################
# MODIFY VARIABLES HERE
###################################################
# Plex Server IP or hostname
plex_server_host = '192.168.1.100'
# Plex Server port
plex_server_port = 32400
# Map the mount points on your Plex Media Server to the fileserver's local paths
path_maps = {
'/Volumes/video/movies': '/volume1/video/movies',
'/Volumes/video/tv': '/volume1/video/TV',
'/Volumes/music': '/volume1/music'
}
# Allowed file extensions
allowed_exts = [
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff',
'mp3', 'flac', 'aac', 'wma', 'ogg', 'ogv', 'wav', 'wma', 'aiff',
'mpg', 'mp4', 'avi', 'mkv', 'm4a', 'mov', 'wmv', 'm2v', 'm4v', 'vob'
]
# Log file
log_file_path = '/var/log/plex_notify.log'
# PID file
pid_file_path = '/var/run/plex_notify.pid'
# Daemonize the process or not
daemonize = False
###################################################
# YOU SHOULDN'T NEED TO TOUCH ANYTHING BELOW HERE
###################################################
import pyinotify
import sys
import os.path
from subprocess import call
import signal
import fnmatch
import urllib.request
import xml.etree.ElementTree as ET
import json
###################################################
# CLASSES / FUNCTIONS
###################################################
class EventHandler(pyinotify.ProcessEvent):
def __init__(self, host, port, sections, allowed_exts):
self.modified_files = set()
self.plex_host = host
self.plex_port = port
self.plex_sections = sections
self.allowed_exts = allowed_exts
def process_IN_CREATE(self, event):
self.process_path(event, 'CREATE')
def process_IN_MOVED_TO(self, event):
self.process_path(event, 'MOVED TO')
def process_IN_MOVED_FROM(self, event):
self.process_path(event, 'MOVED FROM')
def process_IN_DELETE(self, event):
self.process_path(event, 'DELETE')
def process_IN_MODIFY(self, event):
if self.is_allowed_path(event.pathname, event.dir):
self.modified_files.add(event.pathname)
def process_IN_CLOSE_WRITE(self, event):
# ignore close_write unlesss the file has previously been modified.
if (event.pathname in self.modified_files):
self.process_path(event, 'WRITE')
def process_path(self, event, type):
if self.is_allowed_path(event.pathname, event.dir):
log("Notification: %s (%s)" % (event.pathname, type))
for path in list(self.plex_sections.keys()):
if fnmatch.fnmatch(event.pathname, path + "/*"):
log("Found match: %s matches Plex section ID: %d" % (
event.pathname,
self.plex_sections[path]
))
self.update_section(self.plex_sections[path])
# Remove from list of modified files.
try:
self.modified_files.remove(event.pathname)
except KeyError as err:
# Don't care.
pass
else:
log("%s is not an allowed path" % event.pathname)
def update_section(self, section):
log('Updating section ID %d' % (section))
req = urllib.request.Request("http://%s:%d/library/sections/%d/refresh" % (
self.plex_host,
self.plex_port,
section
))
response = urllib.request.urlopen(req)
def is_allowed_path(self, filename, is_dir):
# Don't check the extension for directories
if not is_dir:
ext = os.path.splitext(filename)[1][1:].lower()
if ext not in self.allowed_exts:
return False
if filename.find('@eaDir') > 0:
return False
return True
def log(text):
if not daemonize:
print(text)
log_file.write(text + "\n")
log_file.flush()
def signal_handler(signal, frame):
log("Exiting")
sys.exit(0)
def get_plex_sections(host, port, path_maps):
sections = {}
req = urllib.request.Request("http://%s:%d/library/sections" % (host, port))
response = urllib.request.urlopen(req)
tree = ET.fromstring(response.read().decode("utf-8"))
for directory in tree:
key = directory.attrib['key']
for location in directory:
sections[path_maps[location.attrib['path']]] = int(key)
log("Got Plex sections: " + json.dumps(sections))
return sections
###################################################
# MAIN PROGRAM STARTS HERE
###################################################
log_file = open(log_file_path, 'a')
watch_events = pyinotify.IN_CLOSE_WRITE \
| pyinotify.IN_DELETE \
| pyinotify.IN_CREATE \
| pyinotify.IN_MOVED_TO \
| pyinotify.IN_MOVED_FROM
signal.signal(signal.SIGTERM, signal_handler)
sections = get_plex_sections(plex_server_host, plex_server_port, path_maps)
handler = EventHandler(plex_server_host, plex_server_port, sections, allowed_exts)
wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm, handler)
log('Adding directories to inotify watch')
wdd = wm.add_watch(list(sections.keys()), watch_events, rec=True, auto_add=True)
log('Starting loop')
try:
notifier.loop(daemonize=daemonize, pid_file=pid_file_path)
except pyinotify.NotifierError as err:
print >> sys.stderr, err
Edit the plex_server_host
variable near the top of your script to match the IP address of your Plex Server. If you have local DNS resolution, you can use a hostname instead.
Edit the path_maps
variable to match the mount points on your Plex Server, as well as the corresponding paths for the media shares of your NAS.
You should change the daemonize
variable to False
for testing purposes until you're sure that everything is working properly.
Try running the script with python3 plex_notify.py
, and if all goes well, it will load up without errors :)
If you see a bunch of errors that say something like Errno=No space left on device (ENOSPC)
, then your inotify watcher limit is too low. Run sysctl -n -w fs.inotify.max_user_watches=16384
and then try again. Keep raising the number until the errors go away.
If you see an error that says Errno=No such file or directory (ENOENT)
, then you didn't configure your paths_maps
properly. Make sure each entry in the list is the PMS server's mount point first, and then the corresponding local path on the NAS.
If you set daemonize
to True
, then the script will fork itself into a background task when you run it. It will stay running even if you log out of the shell.
This script should work on other NAS's and Linux-based servers as well.
Let me know if you find this useful!