Plex Auto-Delete Script

I tried the version Last Updated: 02/26/2014 5:05PM MST on Windows Python v3.3.2 and I get this:

C:\TOOLS>c:\Python33\python.exe autodel.py
  File "autodel.py", line 154
    except Exception, e:
                    ^
SyntaxError: invalid syntax

Anyway for this to delete files once they have been CloudSync'ed?

I tried the version Last Updated: 02/26/2014 5:05PM MST on Windows Python v3.3.2 and I get this:

C:\TOOLS>c:\Python33\python.exe autodel.py
  File "autodel.py", line 154
    except Exception, e:
                    ^
SyntaxError: invalid syntax

I had the same issue but using the original code on page 1 solved it for me.

Thats a syntax problem meaning he is not reading a character the right way because its rung. 

I see, the original script was tested on windows with python 3.32. The latest looks like is not compatible with 3.3.

I tried to convert it with 2to3 and fixed some errors. It runs through but I doubt it is all working.

Which python version should be used on windows to run the version from february?

Brotbuexe,

I originally coded this for both Linux and Windows.

It was odd to find something didn't work in Linux when it workedin windows and vice versa... Turned out it was a Python Version issue and not an OS issue at all.

With the original code, I wrote it for 2.7.3 Linux. I tried to port it over to 3.3.2 for windows as that was the latest version, but certian functions didn't work.

So I ended up coding a detection tool to detect linux vs windows. Well I ended up on a windows box, but it was running some python scripts with 2.7.3 and I tried to run my script on it and ran into errors.

Ended up porting all the code I wrote for linux section into the windows and worked just fine. So it really turned out to be Python version issue.

Primarlly with these lines of codes listed below... So if auto detect OS doesn't work use L in the OS settings. Remember to NOT set delete... It will create you a log file so you can see everything the script is doing if it executes successfully or everthing its going to do (log only).

Just follow the instructions in the first thread.

P.S. Your windows machine is fully capable of running 2 different versions of python side by side (2.7.3 and 3.3.2), just point the path to the correct python executable you want to use. Hope this helps.

To Everyone:

Sorry guys, haven't had anytime to even look at this or work on it.

I run a gaming organization and barely have time for that anymore either.

Thank you everyone who contributed to this thread and to coding.

I'll stay touch and help when I can.

Anyways the lines of code in question are here... the urllib2 exists in one but not the other where you'd then use the urllib.

Again, originally thought it was a Linux vs Windows issue. Turned out to be a 2.7.3 vs 3.3.2 issue.

if PC=="L":
  print("Operating System: Linux " + AD)
  import urllib2
  doc = xml.dom.minidom.parse(urllib2.urlopen(URL))
  deck = xml.dom.minidom.parse(urllib2.urlopen(OnDeckURL))
elif PC=="W":
  print("Operating System: Windows " + AD)
  import urllib.request
  doc = xml.dom.minidom.parse(urllib.request.urlopen(URL))
  deck = xml.dom.minidom.parse(urllib.request.urlopen(OnDeckURL))
else:

The better way to have done it is either detect the Python Version vs the OS. Or detect whether the module exists OR try to import it and if it fails import the other... ETC..

This my alterd code
- working on windows
- deleting of similair files
- working with python 3.3
 
 

#!/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 = [3]
IgnoreSections = []
Delete = "1"
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 = 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("----------------------------------------------------------------------------")

Works perfect for me thanks a lot @mordamus.

Would be so cool if they include this in the PHT or PMS in a advance mode. like use under your own risk.

Same for me  with changed script from @mordamus.

Thanks guys.

Just come across this thread and am wondering how difficult it would be to get this script to delete high-quality watched episodes? I tend to download both SD and 720 copies, but i like to delete the 720 ones after i watch them and keep the HD ones.

@mordamus, looks like there is an encoding issue:

Looking for D:\vids v\Switched at Birth\Season 3\Switched at Birth - S03E09 - The Past (Forgotten-Swallowed) [WEBDL-1080p]*
Traceback (most recent call last):
  File "c:\TOOLS\autodel.py", line 387, in
    changed += CheckShows(file);
  File "c:\TOOLS\autodel.py", line 158, in CheckShows
    for deleteFile in glob.glob(similairFiles):
  File "C:\Python33\lib\glob.py", line 18, in glob
    return list(iglob(pathname))
  File "C:\Python33\lib\glob.py", line 50, in iglob
    for name in glob_in_dir(dirname, basename):
  File "C:\Python33\lib\glob.py", line 69, in glob1
    return fnmatch.filter(names, pattern)
  File "C:\Python33\lib\fnmatch.py", line 52, in filter
    match = _compile_pattern(pat)
  File "C:\Python33\lib\functools.py", line 275, in wrapper
    result = user_function(*args, **kwds)
  File "C:\Python33\lib\fnmatch.py", line 46, in _compile_pattern
    return re.compile(res).match
  File "C:\Python33\lib\re.py", line 214, in compile
    return _compile(pattern, flags)
  File "C:\Python33\lib\re.py", line 281, in _compile
    p = sre_compile.compile(pattern, flags)
  File "C:\Python33\lib\sre_compile.py", line 494, in compile
    p = sre_parse.parse(p, flags)
  File "C:\Python33\lib\sre_parse.py", line 748, in parse
    p = _parse_sub(source, pattern, 0)
  File "C:\Python33\lib\sre_parse.py", line 360, in _parse_sub
    itemsappend(_parse(source, state))
  File "C:\Python33\lib\sre_parse.py", line 506, in _parse
    raise error("bad character range")
sre_constants.error: bad character range
 
 
I hope I fixed it myself by insert this at line 157
 
      # replace the left square bracket with [[]
      similairFiles = re.sub(r'\[', '[[]', similairFiles)
      # replace the right square bracket with []] but be careful not to replace
      # the right square brackets in the left square bracket's 'escape' sequence.
      similairFiles = re.sub(r'(?<!\[)\]', '[]]', similairFiles)
 

This script is one more reason I'm glad I switched from XBMC to Plex. I was doing some auto cleaning in XBMC but it was with a bash script and finding old files.

I'm wondering if the option for Shows to keep (Shows = ["Frozen Planet"] could have an opposite option so instead of shows to keep, you could list the shows to delete.

Great script. Thanks

hey thanks for this, what an awesome script!

one suggestion: how about, instead of having a variable in script that determines what not to delete "Shows", we rely on checking whether the file in question is part of a collection in the DB. If it is, then do not delete. This would be a lot more elegant than having to maintain a list within the script!

I create a New Section name Anime when i run the doce he dont delete de show in Anime in stead he came back and say:

Viewed:1x | Days Since Viewed: 12 Added: 13 | /Users/eskwire/Downloads/focker/Anime/Sword Art Online/Season 3/[HorribleSubs] Sword Art Online II - 07 [720p].mkv

Looking for /Users/eskwire/Downloads/focker/Anime/Sword Art Online/Season 3/[HorribleSubs] Sword Art Online II - 07 [720p]*

 

 

 

it seem he is looking for the file and the file is listed on PMS and the section to scan i have it configure for [ ] no number. help advice are welcome.

I have a question about deleting and using shared libraries. If I share my television library and one of my guests watches an episode, will it flag it for deletion? I assume it is only checking my account for viewed/unviewed status. Is that correct? I'd hate to fall behind on a show that my guests have watched and find it has been deleted when I go to watch it.

Scottwf,

From what i can see, this is not catered for in the script - largely because the data source it is querying doesn't show shared views. A different datasource would need to be queried and the script would need to be changed. I would welcome that change, but I think that it almost constitutes a rewrite.

So if I understand you correctly, a shared user watching an episode won't cause it to be flagged for deletion, right? I just wanted to make sure because I don't want a shared user causing my shows to be deleted.

Thanks

scottwf - yes, that is correct

Is there anyway to ignore a certain directory of files? My issue is I want to move my watched files to a second directory, but I want to keep that second directory in the initial library so that my "OnDeck" functionality remains. I need to move my watched files so I can sync accurately with a tablet.

Also, my shows are organized into folders by show name. When I move the show using this script, is there anyway to maintain that folder structure?

Am having a problem, my server have 3 section, 1 Movies, 2 TV Show, 3 TV Show(anime) but when i run the scrip it never run it on the section 3. any advice what should i do?

know i get this error please help:

/Downloads/focker/Anime/Hajime no Ippo/Season 1/[Utsukushii-Raws] Hajime no Ippo - The Fighting - 07 (DVD 720x480 H264 AC3 2.0 Chap Sub).mkv

Looking for /Users/hectorhenry/Downloads/focker/Anime/Hajime no Ippo/Season 1/[Utsukushii-Raws] Hajime no Ippo - The Fighting - 07 (DVD 720x480 H264 AC3 2.0 Chap Sub)*

Traceback (most recent call last):

  File "AutoDelete.py", line 387, in

    changed += CheckShows(file);

  File "AutoDelete.py", line 158, in CheckShows

    for deleteFile in glob.glob(similairFiles):

  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/glob.py", line 27, in glob

    return list(iglob(pathname))

  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/glob.py", line 59, in iglob

    for name in glob_in_dir(dirname, basename):

  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/glob.py", line 78, in glob1

    return fnmatch.filter(names, pattern)

  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/fnmatch.py", line 54, in filter

    _cache[pat] = re.compile(res)

  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/re.py", line 190, in compile

    return _compile(pattern, flags)

  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/re.py", line 242, in _compile

    raise error, v # invalid expression

sre_constants.error: bad character range