Plex Auto-Delete Script

OK!  So it is working, it's just the print command that's failing. That's good!

So that means that we just have to make the file thingy print properly.

Try this:  

print "Viewed:" + str(view) + "x | Days Since Viewed: " + str(DaysSinceVideoLastViewed) + " Added: " + str(DaysSincevideoAdded) + " | " + file.encode('utf-8').decode('utf-8')

Shoot. Same thing:

Traceback (most recent call last):
  File "/volume1/scripts/plexautodelete.py", line 377, in 
    print "Viewed:" + str(view) + "x | Days Since Viewed: " + str(DaysSinceVideoLastViewed) + " Added: " + str(DaysSincevideoAdded) + " | " + file.encode('utf-8').decode('utf-8')
UnicodeEncodeError: 'ascii' codec can't encode character u'\u0101' in position 112: ordinal not in range(128)

Yeah. Python's like that.  See my edit on my previous comment.  If you wrap the print statement in a try/except block, at least it won't fail.  You just won't get the output line for unicode files.  I'm sure _someone_ knows how to fix that problem for reals, but that someone is not me.  :)

Yeah. Python's like that.  See my edit on my previous comment.  If you wrap the print statement in a try/except block, at least it won't fail.  You just won't get the output line for unicode files.  I'm sure _someone_ knows how to fix that problem for reals, but that someone is not me.   :)

Haha, thanks for the help! It's not that big of a deal and I am sure someone might stumble across this and know the solution. When that happens, I will update it.  :D

@egretsareherons, I fooled around a bit more and got it to work thanks to your help!  :D

I changed the print lines from this:

print "Viewed:" + str(view) + "x | Days Since Viewed: " + str(DaysSinceVideoLastViewed) + " Added: " + str(DaysSincevideoAdded) + " | " + file.encode('utf-8').decode('utf-8')

to this (removed the decode portion:)

print "Viewed:" + str(view) + "x | Days Since Viewed: " + str(DaysSinceVideoLastViewed) + " Added: " + str(DaysSincevideoAdded) + " | " + file.encode('utf-8')

It now works wonderfully! Thanks again for the help  :D

Here is the updated code in case anyone wants it:

#!/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 = “0”
Move = “0”
Copy = “0”
Shows = []
OnDeck = “”
DestinationDir = “”
DestinationSection = “”
TriggerRescan = False
KeepMode = “”
DaysToKeep = 30
####################################################################################

NO NEED TO EDIT BELOW THIS LINE

####################################################################################
import os
import xml.dom.minidom
import platform
import re
import shutil
import datetime

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

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)
try:
os.remove(file)
changed = 1
except Exception, e:
print “error deleting file: %s” % e
DeleteCount += 1
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, 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, 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:
    if PC == 'L':
      file = urllib2.unquote(file.encode('utf-8')).decode('utf-8')
    else:
      import urllib
      file = urllib.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.encode('utf-8')
        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.encode('utf-8')
        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.encode('utf-8')
        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("----------------------------------------------------------------------------")

Nice, STEEVo!  :D

Wow, you guys are quick! Thanks Steve4x4 for the suggestion, I will try it out.

And thanks everyone for all the tweaking and work on this.

Nate

Someting that i would recommend is deletion of .srt file and asocieted files to the movie or tv show files.

Thanks again for all that contributed.

I'm having a little trouble with the two variables "SectionList" and "IgnoreSections".

If I specify SectionList I get http errors. If I specify IgnoredSections, it says it is going to ignore them but takes action on them regardless.

I want to run the script on ONLY the section "Current TV". Here is what I tried:

====Attempt 1====

==Options==

PC = ""
Host = ""
Port = ""
SectionList = ["Current TV"]
IgnoredSections = []
Delete = "0"
Move = "1"
Copy = "0"
Shows = [""]
OnDeck = ""
DestinationDir = "/t/_watched"
DestinationSection = "Watched TV"
TriggerRescan = False
KeepMode = ""
DaysToKeep = 0
 
==Result==
Host: 127.0.0.1
Port: 32400
Keep Mode: DISABLED
Show Count: 1
Number of Shows Detected For Keeping: 0
Shows to Keep: | (None Listed) | 
Delete: Disabled - Flagging Only
Delete OnDeck: Yes
Operating System: Linux (Auto Detected)
Section List Mode: User-defined
Operating on user-defined sections: Current TV
Traceback (most recent call last):
  File "./plex_move_watched.py", line 310, in
    doc = xml.dom.minidom.parse(urllib2.urlopen(URL))
  File "/usr/lib/python2.7/urllib2.py", line 126, in urlopen
    return _opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 406, in open
    response = meth(req, response)
  File "/usr/lib/python2.7/urllib2.py", line 519, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib/python2.7/urllib2.py", line 444, in error
    return self._call_chain(*args)
  File "/usr/lib/python2.7/urllib2.py", line 378, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 527, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request
 

====Attempt 2====

==Options==

PC = ""

Host = ""
Port = ""
SectionList = []
IgnoreSections = ["Movies","Old TV","Watched TV","Inbound Folder","Watched Movies"]
Delete = "0"
Move = "1"
Copy = "0"
Shows = [""]
OnDeck = ""
DestinationDir = "/t/_watched"
DestinationSection = "Watched TV"
TriggerRescan = False
DaysToKeep = 0
 
==Result==
Host: 127.0.0.1
Port: 32400
Keep Mode: DISABLED
Show Count: 1
Number of Shows Detected For Keeping: 0
Shows to Keep: | (None Listed) | 
Delete: Disabled - Flagging Only
Delete OnDeck: Yes
Operating System: Linux (Auto Detected)
Section List Mode: Auto
Operating on sections: 6,8,9,10,11,13
Skipping Sections: Movies,Old TV,Watched TV,Inbound Folder,Watched Movies
 
--------- Section 6: Current TV -----------------------------------
...
--------- Section 8: Old TV -----------------------------------
...
--------- Section 9: Inbound Folder -----------------------------------
...
--------- Section 10: Movies -----------------------------------
...
--------- Section 11: Watched TV -----------------------------------
...
--------- Section 13: Watched Movies -----------------------------------
...
----------------------------------------------------------------------------
----------------------------------------------------------------------------
                Summary -- Script Completed Successfully
----------------------------------------------------------------------------
 
  Total File Count      7
  Kept Show Files       0
  On Deck Files         0
  Deleted Files         0
  Moved Files           7
  Copied Files          0
  Flagged Files         0
  Rescanned Sections    
 
----------------------------------------------------------------------------
----------------------------------------------------------------------------
 
==version==
ubuntu 0.9.9.5.411-da1d892
plex_move_watched_v3.py

@akabdog,

Your problem is you can't use names for sections. You have to put the section number(s.)

example: SectionList and IgnoredSections have to be like this: [2] or [1,2] (numbers, not names)

edit: Look at Steven 4x4's first post for instructions on finding your section numbers

**You can See what Sections are Available by going to:  http : // 192.168.X.X:32400/library/sections/

Hi Guys,

This script is great, thanks everybody for it. I got to work the first two versions. But if I try to use the code posted on 27th of February by STEEVo I get the following error:

----------------------------------------------------------------------------
                           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: Disabled - Flagging Only
Delete OnDeck: Yes
Operating System: Linux 
Section List Mode: Auto
Operating on sections: 1,2
Skipping Sections: 

--------- Section 1: Movies -----------------------------------
Traceback (most recent call last):
File “./AutoDelete.py”, line 333, in
d2 = datetime.datetime.fromtimestamp(float(lastViewedAt))
ValueError: could not convert string to float:

Any idea what I have done wrong? I didn't make any adjustments on the code.

I am working on a Mac.

GMono, I just tested the script on my mac and it worked without issue. Perhaps someone here with more expertise than I can help you figure out why you are getting that error...  :unsure:

I had a problem more or lest similar solve by changing how i edit the file. some apps don't have the right symbols.

STEEVo did you use nano as well via terminal? Or did you use another program?

esquire do you know any other programs for Mac?

Thanks for your help guys!

I use to use TextEdit and there is when i start to find error when i run the code, i have Coda to program i decide to use it to edit the file and after that it work for me. You could check the typo to see if you are leavening it exactly the same.

Eddit you could try one of this free ones http://mac.appstorm.net/roundups/web-dev/the-best-code-editors-for-your-mac-in-2013/

Thanks eskwire. I’ll try the it this weekend. Hopefully I’ll find the error

Update:

So the error is in the part where the lastViewedAt is being retrieved. But strangely the part where addedAt is being retrieved works fine. The both look exactly the same, so I can't find the problem. For the moment I just took out the code where lastViewedAt is being retrieved. 

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

The script just works fine now. I just won't be able to use the Keepmode Watch now. But for now it's fine with with. 

Is it possible to add the possibility to delete extra files, like subtitles

Criminal.Minds.S09E19.SD.TV-lol.mp4 <--- show

Criminal.Minds.S09E20.SD.TV-LOL.en.srt <--- english subtitle

Criminal.Minds.S09E19.SD.TV-lol.srt <-- main subttiel

I think the abitity to add a wildcard in the filename, {filename}*

I made a little contribution based on the first version (dont need that stuff of delayed deleting)

This contribution allows the deletion of similair files (like subtitles

{code}

import glob

{code}

{code}

## -- DELETE SHOWS --
  if CantDelete == 0:
    if Delete=="1":
      similairFiles = os.path.splitext(file)[0] + "*"
      for deleteFile in glob.glob(similairFiles):
       print("**[DELETED] " + deleteFile)
       os.remove(deleteFile)

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

{code}

I have to try it.