Quantcast
Channel: Recent Discussions — Plex Forums
Viewing all articles
Browse latest Browse all 151235

Auto-updating/refreshing a remote Plex Media Server when new content is added on NAS (script)

$
0
0

EDIT/UPDATE 1.1: I changed the path mapping to map the fileserver's local paths to the Plex section name, which should be easier than before.

EDIT/UPDATE 1.2: Added option to connect with secure/HTTPS connection by default. Note that the script does not validate the SSL certificate, since it won't match the local hostname/IP address. This should be fine for LAN connections.

EDIT/UPDATE 1.3: Added option to provide a Plex account token, which is only required if the primary PMS account has a PIN enabled, or if you have multiple users set up. This seems to be required even if you have the guest account enabled.

My media center setup consists of a Synology NAS and a Mac Mini. I run PMS on the Mac Mini and just use the NAS as a storage device, with the content being pulled from the NAS via an NFS mount. One problem, though, is that PMS doesn't auto-update the library when there are changes on the network shares, so I either have to trigger an update on PMS manually when I add new content, or wait for the library update interval to kick in.

So, I wrote a script to automate 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. The script uses inotify to monitor 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.

  1. Make sure you have "Python3" installed from the Synology Package Manager first.

  2. 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

  3. Save this script somewhere on your NAS as plex_notify.py:

# PLEX NOTIFIER SCRIPT v1.3
# 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/

# CHANGELOG
# ---------
# v1.3
# - Added option to provide a Plex Token which is required if the primary PMS account has a PIN,
#   or if you have multiple user accounts
# v1.2
# - Use secure connections by default, with option to disable
# v1.1
# - Changed path mapping to map to PMS library names
# v1.0
# - Initial release

###################################################
# MODIFY VARIABLES HERE
###################################################

# Plex Server IP or hostname
plex_server_host = '192.168.0.11'

# Plex Server port
plex_server_port = 32400

# Plex account token; only required if your primary account has a PIN enabled,
# or if you have multiple users.  Instructions how to get your token:
# https://support.plex.tv/hc/en-us/articles/204059436-Finding-your-account-token-X-Plex-Token
plex_account_token = False

# Map the fileserver's local paths to their associated Plex Media Server library names
path_maps = {
    '/volume1/video/TV Shows': 'TV Shows',
    '/volume1/video/Movies': 'Movies',
    '/volume1/music': '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 (run in the background) or not
daemonize = False

# connect to PMS using HTTPS instead of HTTP
# NOTE: The HTTPS connection does not validate the SSL certificate!!
secure_connection = True

###################################################
# 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 ssl
import xml.etree.ElementTree as ET
import json

###################################################
# CLASSES / FUNCTIONS
###################################################

class EventHandler(pyinotify.ProcessEvent):

    def __init__(self, host, port, protocol, token, libraries, allowed_exts):
        self.modified_files = set()
        self.plex_host = host
        self.plex_port = port
        self.plex_account_token = token
        self.protocol = protocol
        self.libraries = libraries
        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.libraries.keys()):
                if fnmatch.fnmatch(event.pathname, path + "/*"):
                    log("Found match: %s matches Plex section ID: %d" % (
                        event.pathname,
                        self.libraries[path]
                    ))
                    self.update_section(self.libraries[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))
        response = url_open("%s://%s:%d/library/sections/%d/refresh" % (
            self.protocol,
            self.plex_host,
            self.plex_port,
            section
        ), self.plex_account_token)

    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)

# custom urlopen() function to bypass SSL certificate validation
def url_open(url, token):
    if token:
        req = urllib.request.Request(url + '?X-Plex-Token=' + token)
    else:
        req = urllib.request.Request(url)
    if url.startswith('https'):
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        return urllib.request.urlopen(req, context=ctx)
    else:
        return urllib.request.urlopen(req)

###################################################
# 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)

if secure_connection:
    protocol = 'https'
else:
    protocol = 'http'

libraries = {}
response = url_open(
    "%s://%s:%d/library/sections" % (
        protocol,
        plex_server_host,
        plex_server_port
    ),
    plex_account_token
)
tree = ET.fromstring(response.read().decode("utf-8"))
for directory in tree:
    for path, name in path_maps.items():
        if directory.attrib['title'] == name:
            libraries[path] = int(directory.attrib['key'])
log("Got Plex libraries: " + json.dumps(libraries))

handler = EventHandler(
    plex_server_host,
    plex_server_port,
    protocol,
    plex_account_token,
    libraries,
    allowed_exts
)
wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm, handler)

log('Adding directories to inotify watch')

wdd = wm.add_watch(
    list(libraries.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

  1. 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.

  2. Edit the path_maps variable to map the local paths of the media shares on your NAS to their corresponding library names in your Plex Media Server.

  3. You should change the daemonize variable to False for testing purposes until you're sure that everything is working properly.

  4. Try running the script with python3 plex_notify.py, and if all goes well, it will load up without errors :)

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.

TROUBLESHOOTING:

  • 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 a local path to your media on the NAS and then the corresponding library/section name on your PMS.

  • If you're getting an error that says urllib.error.HTTPError: HTTP Error 401: Unauthorized, then you need to set the plex_account_token variable. Follow this link for instructions on how to get your account token. Make sure that when you're setting the variable, you wrap the token in quotes, like this: plex_account_token = 'A2ekcFXjzPqmefBpv8da'

Let me know if you find this useful!


Viewing all articles
Browse latest Browse all 151235

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>