Jacob Friedmann

Turning an Old MacBook Pro Into a Home Media Powerhouse [Part II]

In the last post I began the story of my journey from having to get up and plug my MacBook into the TV in order to watch a movie to the perfect home media bliss of not having to ever get off of the couch. Today I will pick up the saga in its second chapter, implementation. The story can further be broken into two steps: distribution and organization.

Getting Distributed with Plex

Like I mentioned in the last post, Plex is a multi-platform client/server software that allows you to stream your media from a central, organized location. On top of this, it has a wonderful interface, can transcode almost any video or audio format and also fetches metadata from several internet databases to provide movie covers, etc. There is also another benefit that I’ll come back to in a bit: it has an API (Application programming interface) which allows other applications to talk to it.

The application is broken up into two different pieces of software. The server is installed on the computer that has your media; the client on any device that you wish to stream on.

Setting up the Server

The Plex server is the part of the software that digs through your media, analyzes it and downloads metadata, and finally serves up a stream when a client asks for it. To install, simply download the software from the Plex website. After you complete the installation, you’ll have to set up your “libraries”. Each “library” is a folder on your computer that has only one type of media in it (i.e. movies). In order to properly display and download metadata, each library must be organized in a specific (and intuitive) way. I mentioned this organization scheme in the last post, but it is also mentioned on the Plex site.

Setting up the Client(s)

The client application is the part of the app that sends a request to the server for a stream and displays it on your device, be it a computer, smartphone, or smart TV. There are two different types of client applications. The first is the web application that simply works in a web browser like Google Chrome or Firefox. This is the first client you will encounter because when you launch the “Media Manager” from the server, you are opening an instance of the web client.

You can also access the web client from any other computer (even if not on the same network!) by going to my.plexapp.com and logging in. The way Plex achieves this is pretty neat. As long as your server is on, it is telling the main Plex servers what its IP address is and what port it is listening on, which allows the my.plexapp.com website to forward requests to your home server. In order to access it from outside your network, you may need to configure port forwarding on your router. This essentially takes a request to your router and forwards it to an individual computer on its network (in this case your Plex server computer). This process is different on every router, but there are websites that aggregate guides that can be helpful. Here is a very comprehensive (maybe slightly out-of-date) guide to this process if you have trouble.

From here you can watch any media, add your libraries and also configure your preferred settings for how often the server will update the media libraries (among other things). I’ve found the defaults do a pretty good job, but as you become more aware of what your computer and network can handle it does help to fine tune. For example, if your music library is gigantic and housed on an external drive, like mine is, you probably don’t want it updating every hour.

The second type of client application is a native client app. In my apartment, I have the iOS app on my iPhone and iPad, and the Samsung Smart TV app on the TV in my bedroom. The iOS app does cost $2, but not only can you stream media to these devices, you can also push the stream to an Apple TV via AirPlay. This is especially nice because there is no Apple TV app for Plex yet and in my estimation there won’t be for some time. There used to be a hack of the “Trailers” app that allowed it, but that hole has since been closed through software updates. These apps simply require you to log into your myplex account and then instantly connect to your server.

Getting Organized with Hazel

I was introduced to Hazel a while ago and fell in love almost immediately. Hazel is a Mac App that allows you to “create rules to automatically keep your files organized”. Essentially, the app watches folders, matches files within those folders to rules you define and then performs any number of actions on those files. For instance, if Hazel is watching my downloads folder, I can have it throw away any file that has been sitting in the folder for more than a week. I hate the tedious task of keeping my computer organized, but I loath having an unorganized computer, and thus, it was a match made in heaven.

Naturally, when I started thinking about a way to (mostly) programmatically organize media files my mind turned to Hazel. In this case I want Hazel to watch a single media dump folder and then organize anything that gets dumped into my Plex library folders while maintaining the proper directory structure. While this may sound fairly simple from 100 miles above, the actual implementation becomes quite non-trivial, so I’ll build it up little by little starting with the easiest media to organize and ending with the one that nearly killed me.

Photos

As a refresher, I want any photo moved into the dump folder to then be moved into a “Photos” folder on my external hard drive and further organized into folders based on date created. With Hazel, this turns out to be fairly simple, but first a little bit about how Hazel works. For each Hazel “rule” there are two parts: matching and action. The matching portion helps Hazel identify what files and folders the rule applies to. The action portion tells Hazel what to do with a file that has been matched.

For the matching portion, we will tell Hazel to look for files whose “kind” is “image”. This should work for most (if not all) photo types. If it doesn’t, we can add additional conditions (logically OR’d with the “match any” option) to match certain file extensions (i.e. png, jpeg, etc).

Screen Shot 2014-01-07 at 9.20.18 PM

Now for the action, we’ll tell Hazel to move the photo and then organize it into a subfolder.

Screen Shot 2014-01-07 at 9.21.16 PM

We can then tell it what the subfolder should be called using information about the file itself, in this case the month and year the file was created.

Screen Shot 2014-01-07 at 9.21.38 PM

And that’s it! You’ve written your first Hazel rule! Now onto the next one…

Music

Being on a Mac, we can make use of an iTunes watch-folder called “Automatically Add to iTunes” to do most of the work here. This means that your iTunes library music folder must also be your Plex music library folder. To match we can use once again use the “kind” is “music” condition. Then the action will simply move the file to your Automatically add to iTunes folder. From here iTunes should take care of the rest!

Screen Shot 2014-01-07 at 9.38.01 PM

Movies

Here things start to get complicated for two reasons. 1) We don’t have our iTunes friend to help us perform the directory organization bit and 2) we want to keep only unwatched and “classic” movies on the internal hard drive and the rest on an external drive. In my situation, I could not assume that all movie file names would come structured the same and therefore they are not necessarily parsabale by Hazel (or any other program for that matter). For this reason the initial organization step has to be done in two stages. First, move the movie file into a waiting folder where I will manually rename it into a set format. Then, it can be moved into the Plex Movie library folder.

Step one, match “type” is “movie” OR “extension” is “mkv” (for some reason Hazel doesn’t always recognize these as movies) and then move them to a folder called “Videos to rename”.

Screen Shot 2014-01-07 at 9.39.58 PM

Step two, I can periodically monitor this folder and manually format the filenames so that they match the pattern “Movie Name (YEAR).mov”. For example, if I had a file named “Anchorman.2004.HD.BluRay.mkv”, I would need to change the name to “Anchorman (2004).mkv”. I will then make use of OS X’s new “tags” to tag the file as a “movie” and a “classic”.

Screen Shot 2014-01-07 at 9.41.57 PM

Screen Shot 2014-01-07 at 9.41.34 PM

Step three, I need to add a Hazel rule to this new “Videos to be renamed” folder. I will match using the “movie” tag that I applied in the last step.

Screen Shot 2014-01-07 at 9.46.09 PM

The action for this rule is a bit more complicated. Since I want the movie to be moved to the “Movies” folder and then organized into a subfolder based on its name, I needed to write my own bash script to achieve the desired result.

Screen Shot 2014-01-07 at 9.47.18 PM

file=$1
filename=${file##*/}
path=${file%/*}
movie=${filename%% (*}
filepath="$path/$movie/$filename"
mkdir "$path/$movie"
mv "$file" "$filepath"

The ability of Hazel to do this makes it immensely powerful. I can essentially perform any action on the file that I could in the terminal by using this feature.

Now comes the really fun part. I want to also watch the Plex Movie library folder and somehow determine if a movie has been watched, so I will know when to move it to the external hard drive. This is where the aforementioned Plex API comes in. Essentially, I can programmatically query the server and receive an XML response that gives me metadata about each movie, including how many times it has been watched. To make use of this, I will have to write a custom script that can query the server and then parse the results. This script is a little more involved than the previous bash script, so this time I will write it in Python. While I could write it in shell script, python makes it much easier and has some really good tools for XML parsing.

This time is also a little different because now we want to match using a script that tells us whether a movie has been watched or not. First we will write a little shell script to call our python script and analyze the result (you can’t call python directly). Notice that Hazel requires us to return an exit status of 0 if the file is a “match”.

Screen Shot 2014-01-07 at 9.57.40 PM

plays=$(python /Users/jacobfriedmann/Hazel/has_been_watched.py "movie" "$1")
exit $plays

Now, we must build our python script. First we need capture our argument (the file name) using the sys module and then make an http request to our server, which we can do using the urllib2 python library.

import sys
import urllib2

the_file = sys.argv[2]

url = "http://127.0.0.1:32400/library/sections/1/all"

section = urllib2.urlopen(url)

This url comes from the Plex API, which you can read about here. Next, we want to parse the XML response using the xml.etree.ElementTree module.

import xml.etree.ElementTree as et

xml = section.read()
movies = et.fromstring(xml)

Now let’s make a decision based on our analysis. First, if the server is off, don’t do anything (return 1). We know the server is off if we can’t open the url, so we’ll surround that line in a try, except block.

try:
    section = urllib2.urlopen(url)
except ValueError, ex:
    print 1
except urllib2.URLError, ex:
    print 1 

Next, we’ll iterate through the library and determine if the movie is in it. If the file is not in the library yet, no match (return 1).

found = False
for movie in movies:
    if (movie[0][0].attrib['file'] == the_file):
        found = True
		
if found == False:
    print 1

Finally we’ll see if the movie has a view count. If it doesn’t, no match, but if it does – it’s a match! Return 0!

if "viewCount" in movie.attrib:
    print 0
else:
    print 1

Here is the complete file (I’ve added exit statements so that the program terminates when it has made a decision):

import sys
import urllib2
import xml.etree.ElementTree as et

the_file = sys.argv[2]

url = "http://127.0.0.1:32400/library/sections/1/all"

try:
    section = urllib2.urlopen(url)
except ValueError, ex:
    print 1
    sys.exit()
except urllib2.URLError, ex:
    print 1 
    sys.exit()

xml = section.read()
movies = et.fromstring(xml)

found = False
for movie in movies:
    if (movie[0][0].attrib['file'] == the_file):
         found = True
         if "viewCount" in movie.attrib:
              print 0
              sys.exit()
         else:
              print 1
              sys.exit()
		
if found == False:
    print 1
    sys.exit()

Great, so now that we have a match, we simply move it to the external hard drive, taking it’s parent folder with it.

Screen Shot 2014-01-07 at 10.22.19 PM

But wait! We don’t want it to be a match if it is labeled a “classic”, so we will change our match option to “all” and add “tags do not contain classic”.

Screen Shot 2014-01-07 at 10.22.50 PM

Shew! Now we’re done. And even though we still have to move on to TV Shows, most of the work has already been done for us here.

TV Shows

The TV Shows flow is very similar to that of movies, with a few small differences. 1) There are two subfolders to organize into (show and season) 2) I eliminate the use of the “classic” tag and 3) we must access the Plex API in a slightly different manner.

So, we recycle the rule that moves videos into the “Videos to be renamed” folder. From here we put the file into the format “Show Name.SXXEXX.mov”. For example, the 13th episode of the 4th season of It’s Always Sunny in Philadelphia would be “It’s Always Sunny in Philadelphia.S04E13.avi”. I’ll then add a “tag” of “tvshow”.

Screen Shot 2014-01-07 at 10.25.50 PM

Next, we tell Hazel to match on that tag in the “Videos to be renamed” folder and move the file to the Plex TV Shows library folder. We also need to run a bash script to parse to name and create the proper file structure.

Screen Shot 2014-01-07 at 10.26.37 PM

file=$1
filename=${file##*/}
path=${file%/*}
show=${filename%%.*}
step1=${filename#*.}
season1=${step1%%E*}
season2=${season1#S}
season="Season ${season2#0}"
filepath="$path/$show/$season/$filename"
mkdir "$path/$show"
mkdir "$path/$show/$season"
mv "$file" "$filepath"

Finally, we need to watch the TV Shows folder for episodes that have been watched in order to move them off onto the external hard drive. We’ll match using a modified version of the same python script we used before. Here is our shell script:

Screen Shot 2014-01-07 at 10.28.57 PM

plays=$(python /Users/jacobfriedmann/Hazel/has_been_watched.py "show" "$1")
exit $plays

And here is the modified python script (only thing that changes is the url that we query):

import sys
import urllib2
import xml.etree.ElementTree as et

the_file = sys.argv[2]

if (sys.argv[1] == "movie"):
    url = "http://127.0.0.1:32400/library/sections/1/all"
elif (sys.argv[1] == "show"):
    url = "http://127.0.0.1:32400/library/sections/2/search?type=4"

try:
    section = urllib2.urlopen(url)
except ValueError, ex:
    print 1
    sys.exit()
except urllib2.URLError, ex:
    print 1 
    sys.exit()

xml = section.read()
movies = et.fromstring(xml)

found = False
for movie in movies:
    if (movie[0][0].attrib['file'] == the_file):
         found = True
         if "viewCount" in movie.attrib:
              print 0
              sys.exit()
         else:
              print 1
              sys.exit()
		
if found == False:
    print 1
    sys.exit()

Cleaning up

There are still a few use cases that we haven’t accounted for. First, by default Hazel only looks at files directly in the folder specified, not into its subfolders. Since media may get “dumped” inside another folder, we want to make Hazel look deeper. To do this we’ll make a rule called “Recursion”. It will match on “kind is folder” and then “run rules on folder contents”.

Screen Shot 2014-01-07 at 10.31.33 PM

Next, once we move files from these subfolders, we don’t want a bunch of empty folders sitting around so we’ll have Hazel delete empty folders. We’ll match on “kind is folder” and “size is less than 1 byte” (in other words, it’s empty) and then move it to trash. Another thing we need to know about Hazel: it will only match one rule to any file or folder and it does so from top to bottom. This means we need this rule to be above the recursion rule or nothing will ever get deleted.

Screen Shot 2014-01-07 at 10.32.01 PM

Screen Shot 2014-01-07 at 10.32.12 PM

Finally, some files will not match any of our rules. We’ll log these files in a text file to remind us to keep things tidy. We’ll match any remaining files and perform a bash script that writes to a text file.

Screen Shot 2014-01-07 at 10.33.04 PM

base=$(basename "$1")
echo "> The file $base is in the Downloads folder and needs to be examined." >> /Users/jacobfriedmann/Hazel/hazellog.txt

If you’ve made it this far, I applaud you – much energy must be spent in the pursuit of laziness. If you have any questions or comments about your own ultimate set up, let me know in the comments below!

Next Post

Previous Post

Leave a Reply

© 2017 Jacob Friedmann

Theme by Anders Norén