Plex Auto-Delete Script

Hi! Thanks for this very useful script. I would like to do a small request:

Would it be possible to remove external subtitles as well? I find that if I have only one video left in a folder, accompanied by multiple subtitles from older episodes, plex assumes they all apply to this video, so I get subtiles from the wrong episode. This is in PHT.

They usuallay have the format [filename-without-extentsion][.languagecode(not always)].[extension], so maybe it would be as simple as changing the remove to 'remove [filename without extension].* ?

0.9.11.4 seems to have broken this. Getting 401 unauthorized now. 

Mains is a little different but it’s still working. I will share it soon. It’s modify to delete special show whit special characters in the name.

I did a quick and dirty fix for the script to deal with tokens. 

http://gmc.00100.fi/AutoDelete/AutoDelete.script

Make sure to update the new TOKEN setting with your personal authentication token.

DISCLAIMER - I HAVE NO CLUE WHAT I AM DOING, THIS IS SCRIPTING BY SEARCH & REPLACE!

I got a error running this today.  Any ideas?  This is on linux 0.9.11.1

Traceback (most recent call last):
  File "./AutoDelete.script", line 346, in 
    blah = urllib2.urlopen(URL)
  File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
    return _opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 410, in open
    response = meth(req, response)
  File "/usr/lib/python2.7/urllib2.py", line 523, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib/python2.7/urllib2.py", line 448, in error
    return self._call_chain(*args)
  File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 531, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request

I updated the script to use either a token or login to plex.tv. It will also use Headers which should keep the token out of logs and stuff. It's kind of hacked together, but it works for now.

I derived the inspiration for the Headers from dane22 from here: https://forums.plex.tv/topic/129340-getting-a-token-from-plextv/

Hope it works for you.

https://www.dropbox.com/s/ebl1x3ffyhmdgt8/PlexAutoDeleteWatched.py?dl=0

After you do a test run and the printout looks ok, change Delete to 1 to actually remove files, or setup moving or copying the files.

Mains is working fine no matter the updates i only had someones change the command to delete show whit special characters. this version is without the two last change yall help whit. Also to keep in mind the program you edit you code is important sometimes it just spoil everything because they don't use the same character settings. 

#!/usr/bin/python
# -*- coding: utf-8 -*-
####################################################################################
##                                  INFORMATION
####################################################################################
##   Developed by: Steven Johnson  (minor modifications by egretsareherons)
##                                 (a minor modifications by aross01)
##                                 (a minor modification by STEEVo)
##
##   Last Updated: 02/26/2014 5:05PM MST
##
##   Description: Auto-Delete Watched Items In Plex
##
##   Required Configurations:
##      - PC       (Blank = AutoDetect  |  W = Windows  |  L = Linux/Unix/MAC)
##      - Host     (Hostname or IP  | Blank = 127.0.0.1)
##      - Port     (Port  |  Blank = 32400)
##      - SectionList - A list of sections to operate on.  If this is blank, the script
##              will get a list of all sections from the server & operate on all, except
##              for the sections listed in IgnoreSections
##      - IgnoreSections - Only used if SectionList is blank. When SectionList is blank,
##              the  script will operate on all available sections of your install
##              except for the ones listed here.
##      - Delete   (1 = Delete  |  Blank = 0 (For Testing or Review))
##      - Move     (1 = move files to DestinationDir, 0 = don't move)
##      - Copy     (1 = copy files to DestinationDir, 0 = don't copy)
##      - Shows    ( ["Show1","Show2"]; = Kept Shows OR [""]; = DEL ALL SHOWS )
##      - OnDeck   ( 1 = Keep If On Deck  |  Blank = Delete Regardless if onDeck  )
##      - DestinationDir  - the place to move or copy your watched files for later manual deletion
##      - DestinationSection - if your destination dir is also a configured section, set the
##                number here so TriggerRescan knows to rescan that section when changes are made.
##                only used if TriggerRescan is True;
##      - TriggerRescan - automatically trigger a deep rescan of changed sections. True or False.
##
##      - KeepMode (Blank = Disabled | W = "Watched" | A = "Added")
##                Method used to keep items for X number of days (specified by DaysToKeep below.)
##                You may choose to base the keep on the last "watched" or "added" date.
##      - DaysToKeep ("30" = Keep items for 30 days following the KeepMode selected)
##
####################################################################################
####################################################################################

PC = “”
Host = “”
Port = “”
SectionList = []
IgnoreSections = []
Delete = “”
Move = “0”
Copy = “0”
Shows = []
OnDeck = “”
DestinationDir = “”
DestinationSection = “”
TriggerRescan = True
KeepMode = “”
DaysToKeep = 30
####################################################################################

NO NEED TO EDIT BELOW THIS LINE

####################################################################################
import os
import xml.dom.minidom
import platform
import re
import shutil
import datetime
import glob
try:
import urllib.request as urllib2
except:
import urllib2

####################################################################################

Check On Deck

####################################################################################
def CheckOnDeck( CheckDeckFile ):
InTheDeck = 0
for DeckVideoNode in deck.getElementsByTagName(“Video”):
DeckMediaNode = DeckVideoNode.getElementsByTagName(“Media”)
for DeckMedia in DeckMediaNode:
DeckPartNode = DeckMedia.getElementsByTagName(“Part”)
for DeckPart in DeckPartNode:
Deckfile = DeckPart.getAttribute(“file”)
if CheckDeckFile==Deckfile:
InTheDeck += 1
else:
InTheDeck += 0
return InTheDeck

####################################################################################

Check Shows And Delete If Configured

####################################################################################
def CheckShows( CheckFile ):
global FileCount
global DeleteCount
global MoveCount
global CopyCount
global FlaggedCount
global OnDeckCount
global ShowsCount
global DestinationDir
FileCount += 1
CantDelete = 0
ShowFound = “”
changed = 0

– CHECK SHOWS –

for Show in Shows:
Show = re.sub(’[^A-Za-z0-9 ]+’, ‘’, Show).strip()
if Show=="":
CantDelete = 0
else:
if (’ ’ in Show) == True:
if all(str(Word) in CheckFile for Word in Show.split()):
CantDelete += 1
ShowFound = “[” + Show + “]”
ShowsCount += 1
else:
CantDelete += 0
else:
if Show in CheckFile:
CantDelete += 1
ShowFound = “[” + Show + “]”
ShowsCount += 1
else:
CantDelete += 0

– Check OnDeck –

if OnDeck==“1”:
IsOnDeck = CheckOnDeck(CheckFile);
if IsOnDeck==0:
CantDelete += 0
else:
CantDelete += 1
ShowFound = “[OnDeck]” + ShowFound
OnDeckCount += 1

– Check if video older than days to keep [Viewed method] –

if KeepMode==“W”:
if DaysToKeep > DaysSinceVideoLastViewed:
CantDelete += 1
ShowsCount += 1

ShowFound = " | Days Since Viewed:" + str(DaysSinceVideoLastViewed) +" |" + ShowFound

– Check if video older than days to keep [Added method] –

if KeepMode==“A”:
if DaysToKeep > DaysSinceVideoAdded:
CantDelete += 1
ShowsCount += 1

ShowFound = " | Days Since Added:" + str(DaysSinceVideoAdded) +" |" + ShowFound

– DELETE SHOWS –

if CantDelete == 0:
if Delete==“1”:
#print("[DELETED] " + CheckFile)
similairFiles = re.sub(’[’, ‘[[]’, os.path.splitext(file)[0]) + “*”
print(“Looking for " + similairFiles)
for deleteFile in glob.glob(similairFiles):
try:
os.remove(deleteFile)
print(”
[DELETED] " + deleteFile)
changed = 1
DeleteCount += 1
except Exception as e:
print (“error deleting file: %s” % e)
elif Move == “1”:
try:
os.utime(os.path.realpath(CheckFile), None)
shutil.move(os.path.realpath(CheckFile), DestinationDir)
print("[MOVED] " + CheckFile)
changed = 1
except Exception as e:
print (“error moving file: %s” % e)
if os.path.islink(CheckFile):
os.unlink(CheckFile)
MoveCount += 1
elif Copy == “1”:
try:
shutil.copy(os.path.realpath(CheckFile), DestinationDir)
changed = 1
except Exception as e:
print (“error copying file: %s” % e)
print("
[COPIED] " + CheckFile)
CopyCount += 1

else:
  print("**[FLAGGED] " + CheckFile)
  FlaggedCount += 1

else:
print("[KEEPING]")
return changed

####################################################################################

Checking URL

####################################################################################
if Host=="":
Host=“127.0.0.1”
if Port=="":
Port=“32400”

print("----------------------------------------------------------------------------")
print(" Detected Settings")
print("----------------------------------------------------------------------------")
print("Host: " + Host)
print("Port: " + Port)
#print("Section: " + Section)
#print("URL: " + URL)
#print("OnDeck URL: " + OnDeckURL)

if KeepMode==“W”:
print(“Keep Mode: Keeping for " + str(DaysToKeep) + " days following the date last viewed”)
elif KeepMode==“A”:
print(“Keep Mode: Keeping viewed items that were added to the library less than " + str(DaysToKeep) + " days ago”)
else:
print(“Keep Mode: DISABLED”)

####################################################################################

Checking Shows

####################################################################################
NoDelete = " | "
ShowCount = len(Shows)
print("Show Count: " + str(ShowCount))

for Show in Shows:
Show = re.sub(’[^A-Za-z0-9 ]+’, ‘’, Show).strip()
if Show=="":
NoDelete += "(None Listed) | "
ShowCount -= 1
else:
NoDelete += Show + " | "

print("Number of Shows Detected For Keeping: " + str(ShowCount))
print (“Shows to Keep:” + NoDelete)

###################################################################################

Checking Delete

####################################################################################
if Delete==“1”:
print(“Delete: Enabled”)
else:
print(“Delete: Disabled - Flagging Only”)

if OnDeck==“1”:
print(“Delete OnDeck: No”)
else:
print(“Delete OnDeck: Yes”)

####################################################################################

Checking OS

####################################################################################
AD = “”
if PC=="":
AD = “(Auto Detected)”
if platform.system()==“Windows”:
PC = “W”
elif platform.system()==“Linux”:
PC = “L”
elif platform.system()==“Darwin”:
PC = “L”

FileCount = 0
DeleteCount = 0
MoveCount = 0
CopyCount = 0
FlaggedCount = 0
OnDeckCount = 0
ShowsCount = 0
RescannedSections = []

####################################################################################

Get list of sections

####################################################################################

if PC==“L”:
print("Operating System: Linux " + AD)
import urllib2
elif PC==“W”:
print("Operating System: Windows " + AD)
import urllib.request
else:
print(“Operating System: ** Not Configured ** (” + platform.system() + “) is not recognized.”)
exit()

if not SectionList:
URL = (“http://” + Host + “:” + Port + “/library/sections/”)
doc = xml.dom.minidom.parse(urllib2.urlopen(URL))
for Section in doc.getElementsByTagName(“Directory”):
if Section.getAttribute(“key”) not in IgnoreSections:
SectionList.append(Section.getAttribute(“key”))

SectionList.sort(key=int)
print (“Section List Mode: Auto”)
print ("Operating on sections: " + ‘,’.join(str(x) for x in SectionList))
print ("Skipping Sections: " + ‘,’.join(str(x) for x in IgnoreSections))

else:
print (“Section List Mode: User-defined”)
print ("Operating on user-defined sections: " + ‘,’.join(str(x) for x in SectionList))

####################################################################################

Loop on sections

####################################################################################
rescan_destination = False

for Section in SectionList:
Section = str(Section)
URL = (“http://” + Host + “:” + Port + “/library/sections/” + Section + “/recentlyViewed”)
OnDeckURL = (“http://” + Host + “:” + Port + “/library/sections/” + Section + “/onDeck”)
####################################################################################

Setting OS Based Variables

####################################################################################
if PC==“L”:
doc = xml.dom.minidom.parse(urllib2.urlopen(URL))
deck = xml.dom.minidom.parse(urllib2.urlopen(OnDeckURL))
elif PC==“W”:
doc = xml.dom.minidom.parse(urllib.request.urlopen(URL))
deck = xml.dom.minidom.parse(urllib.request.urlopen(OnDeckURL))
SectionName = doc.getElementsByTagName(“MediaContainer”)[0].getAttribute(“title1”)
print("")
print("--------- Section “+ Section +”: " + SectionName + " -----------------------------------")

####################################################################################

Get Files for Watched Shows

####################################################################################
changed = 0
for VideoNode in doc.getElementsByTagName(“Video”):
view = VideoNode.getAttribute(“viewCount”)
if view == ‘’:
view = 0
view = int(view)
################################################################
###Find number of days between date video was viewed and today
lastViewedAt = VideoNode.getAttribute(“lastViewedAt”)
d1 = datetime.datetime.today()
d2 = datetime.datetime.fromtimestamp(float(lastViewedAt))
DaysSinceVideoLastViewed = (d1 - d2).days
################################################################
################################################################
###Find number of days between date video was added and today
addedAt = VideoNode.getAttribute(“addedAt”)
d1 = datetime.datetime.today()
da2 = datetime.datetime.fromtimestamp(float(addedAt))
DaysSinceVideoAdded = (d1 - da2).days
################################################################
MediaNode = VideoNode.getElementsByTagName(“Media”)
for Media in MediaNode:
PartNode = Media.getElementsByTagName(“Part”)
for Part in PartNode:
file = Part.getAttribute(“file”)
#NEW (Removed by Mustang):
#if PC == ‘L’:
#file = urllib2.unquote(file.encode(‘utf-8’)).decode(‘utf-8’)
#else:
#import urllib
#file = urllib.parse.unquote(file.encode(‘utf-8’)).decode(‘utf-8’)

    if str(view)!="0":
      print(" ")
    if KeepMode=="W":
      if str(view)!="0":
        print ("Viewed:" + str(view) + "x | Days Since Viewed: " + str(DaysSinceVideoLastViewed) + " | " + file)
        if view > 0:
          if os.path.isfile(file):
            changed += CheckShows(file);
          else:
            print("##[NOT FOUND] " + file)
    if KeepMode=="A":
      if str(view)!="0":
        print ("Viewed:" + str(view) + "x | Days Since Added: " + str(DaysSinceVideoAdded) + " | " + file)
        if view > 0:
          if os.path.isfile(file):
            changed += CheckShows(file);
          else:
            print("##[NOT FOUND] " + file)
    if KeepMode=="":
      if str(view)!="0":
        print ("Viewed:" + str(view) + "x | Days Since Viewed: " + str(DaysSinceVideoLastViewed) + " Added: " + str(DaysSinceVideoAdded) + " | " + file)
        if view > 0:
          if os.path.isfile(file):
            changed += CheckShows(file);
          else:
            print("##[NOT FOUND] " + file)

rescan this section if necessary

if TriggerRescan and changed:
URL = (“http://” + Host + “:” + Port + “/library/sections/” + Section + “/refresh?deep=1”)
blah = urllib2.urlopen(URL)
RescannedSections.append(SectionName + ‘(’ + Section + ‘)’)
rescan_destination = True

rescan destination section if necessary

if TriggerRescan and DestinationSection and rescan_destination:
URL = (“http://” + Host + “:” + Port + “/library/sections/” + str(DestinationSection) + “/refresh?deep=1”)
blah = urllib2.urlopen(URL)
RescannedSections.append(’[destination dir]’ + ‘(’ + str(DestinationSection) + ‘)’)

print("")
print("----------------------------------------------------------------------------")
print("----------------------------------------------------------------------------")
print(" Summary – Script Completed Successfully")
print("----------------------------------------------------------------------------")
print("")
print(" Total File Count " + str(FileCount))
print(" Kept Show Files " + str(ShowsCount))
print(" On Deck Files " + str(OnDeckCount))
print(" Deleted Files " + str(DeleteCount))
print(" Moved Files " + str(MoveCount))
print(" Copied Files " + str(CopyCount))
print(" Flagged Files " + str(FlaggedCount))
print(" Rescanned Sections " + ', '.join(str(x) for x in RescannedSections) )
print("")
print("----------------------------------------------------------------------------")
print("----------------------------------------------------------------------------")

Is it possible that the script doesn't work anymore with the Home function of plex?

I get an unauthorized error. Using the script as in the post above.

DS1512P> python AutoDelete.py 
----------------------------------------------------------------------------
                           Detected Settings
----------------------------------------------------------------------------
Host: 127.0.0.1
Port: 32400
Keep Mode: DISABLED
Show Count: 0
Number of Shows Detected For Keeping: 0
Shows to Keep: | 
Delete: ***Enabled***
Delete OnDeck: Yes
Operating System: Linux (Auto Detected)
Traceback (most recent call last):
  File "AutoDelete.py", line 290, in 
    doc = xml.dom.minidom.parse(urllib2.urlopen(URL))
  File "/usr/lib/python2.7/urllib2.py", line 154, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 437, in open
    response = meth(req, response)
  File "/usr/lib/python2.7/urllib2.py", line 550, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib/python2.7/urllib2.py", line 475, in error
    return self._call_chain(*args)
  File "/usr/lib/python2.7/urllib2.py", line 409, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 558, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 401: Unauthorized
DS1512P> 

Looks like that URL doesn't work anymore - something in one of the updates I guess?

Mains work on Mac.

hi there,

thanks for the great script - i tried using the new script with my token but I always get an Intendation Error:

  File "plexautodelete.py", line 362
    req = urllib2.Request(URL,None,{"X-Plex-Token":Token})
    ^
IndentationError: unexpected indent

Any idea how to fix this? there is no indent in line 362 as far as I could tell...

cheers

alex

Sorry about that, it looks like there was a typo on the line just above. There was an S at the end of line 361 that shouldn't have been there.

Here's a fixed version:

https://www.dropbox.com/s/zlvt9gyp4vlkm43/PlexAutoDelete.py?dl=0

Sorry about that, it looks like there was a typo on the line just above. There was an S at the end of line 361 that shouldn't have been there.

Here's a fixed version:

https://www.dropbox.com/s/zlvt9gyp4vlkm43/PlexAutoDelete.py?dl=0

thanks for the new version of the script!

I think there is another bug on line 155 because it's commented and basically it always fails to delete a file unless the "DaysToKepp" variable is set to 0

  #if KeepMode=="A":

  if DaysToKeep > DaysSinceVideoAdded:
    CantDelete += 1
    ShowsCount += 1

I actually had to modify the delete part as well as it was not deleting files if looking for similar names, I've commented the similar name part and changed slightly the delete command, I'm on Ubuntu...

   if Delete=="1":

      #print("**[DELETED] " + CheckFile)
      #similairFiles = os.path.splitext(file)[0] + "*"
      #print("Looking for " + similairFiles)
      #for deleteFile in glob.glob(similairFiles):
      try:
       os.remove(CheckFile)
       print("**[DELETED] " + CheckFile)
       changed = 1
       DeleteCount += 1
      except Exception as e:
        print ("error deleting file: %s" % e)

Sorry about that, I had modified the script for my usage, which changed the effect of some of the settings. I'm not sure why deleting similar files isn't working for you. It works for me on Ubuntu 12.04 and 14.04, but I added in a setting to delete similar files or just delete the media file.

Update the file:

https://www.dropbox.com/s/zlvt9gyp4vlkm43/PlexAutoDelete.py?dl=0

Hi, I'm trying to get this script running on Windows, but am running into trouble.

I have installed the recommended Python 3.3.2 and am using the latest script from above.

Traceback (most recent call last):

  File "C:\Python33\PlexAutoDelete.py", line 435, in
    file = urllib.parse.unquote(file.encode('utf-8'))
  File "C:\Python33\lib\urllib\parse.py", line 514, i
    if '%' not in string:
TypeError: Type str doesn't support the buffer API
 
Any help would be greatly appreciated.

Actually I messed arround a bit, and simply removing the line 

 #file = urllib.parse.unquote(file.encode('utf-8')).decode('utf-8')

It runs fine. (I don't have nay special char folders - and if I do I can live with a couple not getting downloaded..

Found out I had to re-run it quite a few times though since it only takes about 100 files at a time :-)

Ya it turns out keeping python2 and python3 compatibility together is a pain.

I put a check for the python version on that line, it should work now, but I don't know if its perfect. The 100 files at a time is a limitation of the Plex API, that is used I believe.

I updated the download link:

https://www.dropbox.com/s/zlvt9gyp4vlkm43/PlexAutoDelete.py?dl=0

Anyone else had a problem where the Recently Viewed Episodes doesn't at all reflect the reality.

My Recently Viewed Episodes claims that I have watched 15 episodes the last 263 days... Not quite right...

(I also posted this question on the General discussion forum).

i see some of you are having problem whit the scrip. The last update that i post it working for me on MAC just fine.