Exemple #1
0
                print "Processing episode " + inputfile
                converter.process(inputfile)

#NzbDrone
# SABnzbd
if len(sys.argv) == 8:
    # SABnzbd argv:
    # 1 The final directory of the job (full path)
    # 2 The original name of the NZB file
    # 3 Clean version of the job name (no path info and ".nzb" removed)
    # 4 Indexer's report number (if supported)
    # 5 User-defined category
    # 6 Group that the NZB was posted in e.g. alt.binaries.x
    # 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
    print "Script triggered from SABnzbd, starting nzbDroneFactory..."
    nzbDroneFactory.scan(sys.argv[1], sys.argv[7])

# NZBGet
elif len(sys.argv) == 4:
    # NZBGet argv:
    # 1  The final directory of the job (full path)
    # 2  The original name of the NZB file
    # 3  The status of the download: 0 == successful
    print "Script triggered from NZBGet, starting nzbDroneFactory..."
    nzbDroneFactory.scan(sys.argv[1], sys.argv[3])

else:
    print "Invalid number of arguments received from client."
    print "Running autoProcessMovie as a manual run..."
    autoProcessMovie.process('Manual Run', 'Manual Run', 0)
def main(inputDirectory, inputName, inputCategory, inputHash):

    status = int(1)  # 1 = failed | 0 = success
    root = int(0)
    video = int(0)
    video2 = int(0)
    foundFile = int(0)
    deleteOriginal = int(0)
    numCompressed = int(0)
    extractionSuccess = False

    Logger.debug("MAIN: Received Directory: %s | Name: %s | Category: %s", inputDirectory, inputName, inputCategory)

    inputDirectory, inputName, inputCategory, root = category_search(inputDirectory, inputName, inputCategory, root, categories)  # Confirm the category by parsing directory structure

    for category in categories:
        if category == inputCategory:
            outputDestination = os.path.normpath(os.path.join(outputDirectory, category, safeName(inputName)))
            Logger.info("MAIN: Output directory set to: %s", outputDestination)
            break
        else:
            continue

    Logger.debug("MAIN: Scanning files in directory: %s", inputDirectory)      

    now = datetime.datetime.now()
    for dirpath, dirnames, filenames in os.walk(inputDirectory):
        for file in filenames:

            filePath = os.path.join(dirpath, file)
            fileName, fileExtension = os.path.splitext(file)
            targetDirectory = os.path.join(outputDestination, file)

            if root == 1:
                if not foundFile: 
                    Logger.debug("MAIN: Looking for %s in: %s", inputName, file)
                if (safeName(inputName) in safeName(file)) or (safeName(os.path.splitext(file)[0]) in safeName(inputName)) and foundFile == 0:
                    pass  # This file does match the Torrent name
                    foundFile = 1
                    Logger.debug("MAIN: Found file %s that matches Torrent Name %s", file, inputName)
                else:
                    continue  # This file does not match the Torrent name, skip it

            if root == 2:
                Logger.debug("MAIN: Looking for files with modified/created dates less than 5 minutes old.")
                mtime_lapse = now - datetime.datetime.fromtimestamp(os.path.getmtime(os.path.join(dirpath, file)))
                ctime_lapse = now - datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(dirpath, file)))
                if (mtime_lapse < datetime.timedelta(minutes=5)) or (ctime_lapse < datetime.timedelta(minutes=5)) and foundFile == 0:
                    pass  # This file does match the date time criteria
                    foundFile = 1
                    Logger.debug("MAIN: Found file %s with date modifed/created less than 5 minutes ago.", file)
                else:
                    continue  # This file has not been recently moved or created, skip it

            if not (inputCategory == cpsCategory or inputCategory == sbCategory): #process all for non-video categories.
                Logger.info("MAIN: Found file %s for category %s", filepath, inputCategory)
                copy_link(filePath, targetDirectory, useLink, outputDestination)
            elif fileExtension in mediaContainer:  # If the file is a video file
                if is_sample(filePath, inputName, minSampleSize):  # Ignore samples
                    Logger.info("MAIN: Ignoring sample file: %s  ", filePath)
                    continue
                else:
                    video = video + 1
                    Logger.info("MAIN: Found video file %s in %s", fileExtension, filePath)
                    try:
                        copy_link(filePath, targetDirectory, useLink, outputDestination)
                    except Exception as e:
                        Logger.error("MAIN: Failed to link file: %s", file)
                        Logger.debug(e)
            elif fileExtension in metaContainer:
                Logger.info("MAIN: Found metadata file %s for file %s", fileExtension, filePath)
                try:
                    copy_link(filePath, targetDirectory, useLink, outputDestination)
                except Exception as e:
                    Logger.error("MAIN: Failed to link file: %s", file)
                    Logger.debug(e)
            elif fileExtension in compressedContainer:
                numCompressed = numCompressed + 1
                if re.search(r'\d+', os.path.splitext(fileName)[1]) and numCompressed > 1: # find part numbers in second "extension" from right, if we have more than 1 compressed file.
                    part = int(re.search(r'\d+', os.path.splitext(fileName)[1]).group())
                    if part == 1: # we only want to extract the primary part.
                        Logger.debug("MAIN: Found primary part of a multi-part archive %s. Extracting", file)                       
                    else:
                        Logger.debug("MAIN: Found part %s of a multi-part archive %s. Ignoring", part, file)
                        continue
                Logger.info("MAIN: Found compressed archive %s for file %s", fileExtension, filePath)
                try:
                    extractor.extract(filePath, outputDestination)
                    extractionSuccess = True # we use this variable to determine if we need to pause a torrent or not in uTorrent (dont need to pause archived content)
                except Exception as e:
                    Logger.warn("MAIN: Extraction failed for: %s", file)
                    Logger.debug(e)
            else:
                Logger.debug("MAIN: Ignoring unknown filetype %s for file %s", fileExtension, filePath)
                continue
    flatten(outputDestination)

    # Now check if movie files exist in destination:
    for dirpath, dirnames, filenames in os.walk(outputDestination):
        for file in filenames:
            filePath = os.path.join(dirpath, file)
            fileExtension = os.path.splitext(file)[1]
            if fileExtension in mediaContainer:  # If the file is a video file
                if is_sample(filePath, inputName, minSampleSize):
                    Logger.debug("MAIN: Removing sample file: %s", filePath)
                    os.unlink(filePath)  # remove samples
                else:
                    video2 = video2 + 1
    if video2 >= video and video2 > 0:  # Check that all video files were moved
        status = 0

    # Hardlink solution for uTorrent, need to implent support for deluge, transmission
    if clientAgent == 'utorrent' and extractionSuccess == False and inputHash:
        try:
            Logger.debug("MAIN: Connecting to uTorrent: %s", uTorrentWEBui)
            utorrentClass = UTorrentClient(uTorrentWEBui, uTorrentUSR, uTorrentPWD)
        except Exception as e:
            Logger.error("MAIN: Failed to connect to uTorrent: %s", e)

        # if we are using links with uTorrent it means we need to pause it in order to access the files
        if useLink == 1:
            Logger.debug("MAIN: Stoping torrent %s in uTorrent while processing", inputName)
            utorrentClass.stop(inputHash)
            time.sleep(5)  # Give uTorrent some time to catch up with the change

        # Delete torrent and torrentdata from uTorrent
        if deleteOriginal == 1:
            Logger.debug("MAIN: Deleting torrent %s from uTorrent", inputName)
            utorrentClass.removedata(inputHash)
            utorrentClass.remove(inputHash)
            time.sleep(5)

    processCategories = {cpsCategory, sbCategory, hpCategory, mlCategory, gzCategory}

    if inputCategory and not (inputCategory in processCategories): # no extra processign to be done... yet.
        Logger.info("MAIN: No further processing to be done for category %s.", inputCategory)
        result = 1
    elif status == 0:
        Logger.debug("MAIN: Calling autoProcess script for successful download.")
    else:
        Logger.error("MAIN: Something failed! Please check logs. Exiting")
        sys.exit(-1)

    if inputCategory == cpsCategory:
        Logger.info("MAIN: Calling CouchPotatoServer to post-process: %s", inputName)
        result = autoProcessMovie.process(outputDestination, inputName, status)
    elif inputCategory == sbCategory:
        Logger.info("MAIN: Calling Sick-Beard to post-process: %s", inputName)
        result = autoProcessTV.processEpisode(outputDestination, inputName, status)
    elif inputCategory == hpCategory:
        Logger.info("MAIN: Calling HeadPhones to post-process: %s", inputName)
        result = autoProcessMusic.process(outputDestination, inputName, status)
    elif inputCategory == mlCategory:
        Logger.info("MAIN: Calling Mylar to post-process: %s", inputName)
        result = autoProcessComics.processEpisode(outputDestination, inputName, status)
    elif inputCategory == gzCategory:
        Logger.info("MAIN: Calling Gamez to post-process: %s", inputName)
        result = autoProcessGames.process(outputDestination, inputName, status)

    if result == 1:
        Logger.info("MAIN: A problem was reported in the autoProcess* script. If torrent was pasued we will resume seeding")

    # Hardlink solution for uTorrent, need to implent support for deluge, transmission
    if clientAgent == 'utorrent' and extractionSuccess == False and inputHash and useLink == 1 and deleteOriginal == 0: # we always want to resume seeding, for now manually find out what is wrong when extraction fails
        Logger.debug("MAIN: Starting torrent %s in uTorrent", inputName)
        utorrentClass.start(inputHash)

    Logger.info("MAIN: All done.")
import sys
import autoProcessMovie 

# SABnzbd
if len(sys.argv) == 8:
# SABnzbd argv:
# 1 The final directory of the job (full path)
# 2 The original name of the NZB file
# 3 Clean version of the job name (no path info and ".nzb" removed)
# 4 Indexer's report number (if supported)
# 5 User-defined category
# 6 Group that the NZB was posted in e.g. alt.binaries.x
# 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
	print "Script triggered from SABnzbd, starting autoProcessMovie..."
	autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[7])

# NZBGet
elif len(sys.argv) == 3:
# NZBGet argv: 
# 1  The final directory of the job (full path) 
# 2  The original name of the NZB file 
# From NZBGet only successful downloads are triggered so status is set to "0"
	print "Script triggered from NZBGet, starting autoProcessMovie..."
	
	autoProcessMovie.process(sys.argv[1], sys.argv[2], 0)

else:
	print "Invalid number of arguments received from client." 
	print "Running autoProcessMovie as a manual run..."
	autoProcessMovie.process('Manual Run', 'Manual Run', 0)
                print "Processing movie " + inputfile
                converter.process(inputfile)


# SABnzbd
if len(sys.argv) == 8:
    # SABnzbd argv:
    # 1 The final directory of the job (full path)
    # 2 The original name of the NZB file
    # 3 Clean version of the job name (no path info and ".nzb" removed)
    # 4 Indexer's report number (if supported)
    # 5 User-defined category
    # 6 Group that the NZB was posted in e.g. alt.binaries.x
    # 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
    print "Script triggered from SABnzbd, starting autoProcessMovie..."
    autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[7])

# NZBGet
elif len(sys.argv) == 4:
    # NZBGet argv:
    # 1  The final directory of the job (full path)
    # 2  The original name of the NZB file
    # 3  The status of the download: 0 == successful
    print "Script triggered from NZBGet, starting autoProcessMovie..."
    autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[3])

else:
    print "Invalid number of arguments received from client."
    print "Running autoProcessMovie as a manual run..."
    autoProcessMovie.process("Manual Run", "Manual Run", 0)
Exemple #5
0
#!/usr/bin/env python

import sys
import autoProcessMovie

if len(sys.argv) < 8:
    print "Not enough arguments received from SABnzbd."
    print "Running autoProcessMovie as a manual run"
    autoProcessMovie.process('Manual Run', 'Manual Run', 0)
else:
    status = int(sys.argv[7])
    autoProcessMovie.process(sys.argv[1], sys.argv[2], status)

# SABnzbd argv:
# 1  The final directory of the job (full path)
# 2  The original name of the NZB file
# 3  Clean version of the job name (no path info and ".nzb" removed)
# 4  Indexer's report number (if supported)
# 5  User-defined category
# 6  Group that the NZB was posted in e.g. alt.binaries.x
# 7  Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
def nzbToMedia(nzbStatus):
    script = ""
    result = ""
    if Debug == "yes":
        Logger.debug("Post-Process: Executing external postprocessing with argument %s", nzbStatus) 
    PostProcessStatus = nzbStatus
    # 200 MB in bytes
    SIZE_CUTOFF = 200 * 1024 * 1024
    # Ignore 'sample' in files unless 'sample' in Torrent Name
    for dirpath, dirnames, filenames in os.walk(NZBPP_DIRECTORY):
                for file in filenames:
                    filePath = os.path.join(dirpath, file)
                    if ('sample' in filePath.lower()) and (not 'sample' in NZBPP_NZBNAME) and (os.path.getsize(filePath) < SIZE_CUTOFF):
                        Logger.info("Post-Process: Deleting sample file %s", filePath)
                        os.unlink(filePath)

    if NZBPP_CATEGORY == CouchPotatoCategory:
        if CouchPotato == "yes":
            script = "autoProcessMovie"
            # Call Couchpotato's postprocessing script
            Logger.info("Post-Process: Running CouchPotato's postprocessing script")

            if Debug == "yes":
                Logger.debug("Post-Process: CouchPotato-Script-ARGV1= %s", NZBPP_DIRECTORY)
                Logger.debug("Post-Process: CouchPotato-Script-ARGV2= %s", NZBPP_NZBFILENAME)
                Logger.debug("Post-Process: CouchPotato-Script-ARGV3= %s", PostProcessStatus)
            result = autoProcessMovie.process(NZBPP_DIRECTORY, NZBPP_NZBFILENAME, PostProcessStatus)
        else:
            Logger.debug("Post-Process: Ignored to run CouchPotato's postprocessing script as it is disabled by user")

    if NZBPP_CATEGORY == SickBeardCategory:
        if SickBeard == "yes":
            script = "autoProcessTv"
            # Call SickBeard's postprocessing script
            Logger.info("Post-Process: Running SickBeard's postprocessing script")

            if Debug == "yes":
                Logger.debug("Post-Process: SickBeard-Script-ARGV1= %s", NZBPP_DIRECTORY)
                Logger.debug("Post-Process: SickBeard-Script-ARGV2= %s", NZBPP_NZBFILENAME)
                Logger.debug("Post-Process: SickBeard-Script-ARGV3= %s", PostProcessStatus)
            result = autoProcessTV.processEpisode(NZBPP_DIRECTORY, NZBPP_NZBFILENAME, PostProcessStatus)
        else:
            Logger.debug("Post-Process: Ignored to run SickBeard's postprocessing script as it is disabled by user")

    if NZBPP_CATEGORY == HeadPhonesCategory:
        if HeadPhones == "yes":
            script = "autoProcessMusic"
            # Call HeadPhones' postprocessing script
            Logger.info("Post-Process: Running HeadPhones' postprocessing script")

            if Debug == "yes":
                Logger.debug("Post-Process: HeadPhones-Script-ARGV1= %s", NZBPP_DIRECTORY)
                Logger.debug("Post-Process: HeadPhones-Script-ARGV2= %s", NZBPP_NZBFILENAME)
                Logger.debug("Post-Process: HeadPhones-Script-ARGV3= %s", PostProcessStatus)
            result = autoProcessMusic.process(NZBPP_DIRECTORY, NZBPP_NZBFILENAME, PostProcessStatus)
        else:
            Logger.debug("Post-Process: Ignored to run HeadPhones' postprocessing script as it is disabled by user")

    if NZBPP_CATEGORY == MylarCategory:
        if Mylar == "yes":
            script = "autoProcessComics"
            # Call Mylar's postprocessing script
            Logger.info("Post-Process: Running Mylar's postprocessing script")

            if Debug == "yes":
                Logger.debug("Post-Process: Mylar-Script-ARGV1= %s", NZBPP_DIRECTORY)
                Logger.debug("Post-Process: Mylar-Script-ARGV2= %s", NZBPP_NZBFILENAME)
                Logger.debug("Post-Process: Mylar-Script-ARGV3= %s", PostProcessStatus)
            result = autoProcessComics.processEpisode(NZBPP_DIRECTORY, NZBPP_NZBFILENAME, PostProcessStatus)
        else:
            Logger.debug("Post-Process: Ignored to run Mylar's postprocessing script as it is disabled by user")

    if NZBPP_CATEGORY == GamezCategory:
        if Gamez == "yes":
            script = "autoProcessGames"
            # Call Gamez's postprocessing script
            Logger.info("Post-Process: Running Gamez's postprocessing script")

            if Debug == "yes":
                Logger.debug("Post-Process: Gamez-Script-ARGV1= %s", NZBPP_DIRECTORY)
                Logger.debug("Post-Process: Gamez-Script-ARGV2= %s", NZBPP_NZBFILENAME)
                Logger.debug("Post-Process: Gamez-Script-ARGV3= %s", PostProcessStatus)
            result = autoProcessGames.process(NZBPP_DIRECTORY, NZBPP_NZBFILENAME, PostProcessStatus)
        else:
            Logger.debug("Post-Process: Ignored to run Gamez's postprocessing script as it is disabled by user")

    return script, result
                converter.process(inputfile)
 
 
#NzbDrone
# SABnzbd
if len(sys.argv) ==8:
# SABnzbd argv:
# 1 The final directory of the job (full path)
# 2 The original name of the NZB file
# 3 Clean version of the job name (no path info and ".nzb" removed)
# 4 Indexer's report number (if supported)
# 5 User-defined category
# 6 Group that the NZB was posted in e.g. alt.binaries.x
# 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
    print "Script triggered from SABnzbd, starting nzbDroneFactory..."
    nzbDroneFactory.scan(sys.argv[1], sys.argv[7])
 
# NZBGet
elif len(sys.argv) == 4:
# NZBGet argv:
# 1  The final directory of the job (full path)
# 2  The original name of the NZB file
# 3  The status of the download: 0 == successful
    print "Script triggered from NZBGet, starting nzbDroneFactory..."
    nzbDroneFactory.scan(sys.argv[1], sys.argv[3])
 
else:
    print "Invalid number of arguments received from client."
    print "Running autoProcessMovie as a manual run..."
    autoProcessMovie.process('Manual Run', 'Manual Run', 0)
                                video2 = video2 + 1
if video2 >= video and video2 > 0: # Check that all video files were moved
        status = 0

status = int(status) #just to be safe.        
if status == 0:
        Logger.info("MAIN: Successful run")
        Logger.debug("MAIN: Calling autoProcess script for successful download.")
elif failed_extract == 1 and failed_link == 0: #failed to extract files only.
        Logger.info("MAIN: Failed to extract a packed file.")
        Logger.debug("MAIN: Assume this to be password protected file.")
        Logger.debug("MAIN: Calling autoProcess script for failed download.")
else:
        Logger.info("MAIN: Something failed! Please check logs. Exiting")
        sys.exit(-1)
       
# Now we pass off to CouchPotato or Sick-Beard
# Log this output
old_stdout = sys.stdout  # Still crude, but we wat to capture this for now
logFile = os.path.join(os.path.dirname(sys.argv[0]), "postprocess.log")
log_file = open(logFile,"a+")
sys.stdout = log_file
if inputCategory == movieCategory:  
        Logger.info("MAIN: Calling postprocessing script for CouchPotatoServer")
        autoProcessMovie.process(outputDestination, inputName, status)
elif inputCategory == tvCategory:
        Logger.info("MAIN: Calling postprocessing script for Sick-Beard")
        autoProcessTV.processEpisode(outputDestination, inputName, status)
sys.stdout = old_stdout
log_file.close()
Exemple #9
0
elif len(sys.argv) == NZBGET_NO_OF_ARGUMENTS:
    # NZBGet argv:
    # 1  The final directory of the job (full path)
    # 2  The original name of the NZB file
    # 3  The status of the download: 0 == successful
    # 4  User-defined category
    Logger.info("MAIN: Script triggered from NZBGet")
    nzbDir, inputName, status, inputCategory = (sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
else: # only CPS supports this manual run for now.
    Logger.warn("MAIN: Invalid number of arguments received from client.")
    Logger.info("MAIN: Running autoProcessMovie as a manual run...")
    nzbDir, inputName, status, inputCategory = ('Manual Run', 'Manual Run', 0, cpsCategory)

if inputCategory == cpsCategory:
    Logger.info("MAIN: Calling CouchPotatoServer to post-process: %s", inputName)
    result = autoProcessMovie.process(nzbDir, inputName, status)
elif inputCategory == sbCategory:
    Logger.info("MAIN: Calling Sick-Beard to post-process: %s", inputName)
    result = autoProcessTV.processEpisode(nzbDir, inputName, status)
elif inputCategory == hpCategory:
    Logger.info("MAIN: Calling HeadPhones to post-process: %s", inputName)
    result = autoProcessMusic.process(nzbDir, inputName, status)
elif inputCategory == mlCategory:
    Logger.info("MAIN: Calling Mylar to post-process: %s", inputName)
    result = autoProcessComics.processEpisode(nzbDir, inputName, status)
elif inputCategory == gzCategory:
    Logger.info("MAIN: Calling Gamez to post-process: %s", inputName)
    result = autoProcessGames.process(nzbDir, inputName, status)

if result == 0:
    Logger.info("MAIN: The autoProcess* script completed successfully.")
                    imdbmp4.writeTags(convert.output)
                except AttributeError:
                    print "Unable to tag file, Couch Potato probably screwed up passing the IMDB ID"

# SABnzbd
if len(sys.argv) == 8:
    # SABnzbd argv:
    # 1 The final directory of the job (full path)
    # 2 The original name of the NZB file
    # 3 Clean version of the job name (no path info and ".nzb" removed)
    # 4 Indexer's report number (if supported)
    # 5 User-defined category
    # 6 Group that the NZB was posted in e.g. alt.binaries.x
    # 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
    print "Script triggered from SABnzbd, starting autoProcessMovie..."
    autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[7])

# NZBGet
elif len(sys.argv) == 4:
    # NZBGet argv:
    # 1  The final directory of the job (full path)
    # 2  The original name of the NZB file
    # 3  The status of the download: 0 == successful
    print "Script triggered from NZBGet, starting autoProcessMovie..."
    autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[3])

else:
    print "Invalid number of arguments received from client."
    print "Running autoProcessMovie as a manual run..."
    autoProcessMovie.process('Manual Run', 'Manual Run', 0)
nzbtomedia_configure_logging(os.path.dirname(sys.argv[0]))
Logger = logging.getLogger(__name__)

Logger.info("====================") # Seperate old from new log
Logger.info("nzbToCouchPotato %s", VERSION)

# SABnzbd
if len(sys.argv) == SABNZB_NO_OF_ARGUMENTS:
    # SABnzbd argv:
    # 1 The final directory of the job (full path)
    # 2 The original name of the NZB file
    # 3 Clean version of the job name (no path info and ".nzb" removed)
    # 4 Indexer's report number (if supported)
    # 5 User-defined category
    # 6 Group that the NZB was posted in e.g. alt.binaries.x
    # 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
    Logger.info("Script triggered from SABnzbd, starting autoProcessMovie...")
    result = autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[7])
# NZBGet
elif len(sys.argv) == NZBGET_NO_OF_ARGUMENTS:
    # NZBGet argv:
    # 1  The final directory of the job (full path)
    # 2  The original name of the NZB file
    # 3  The status of the download: 0 == successful
    Logger.info("Script triggered from NZBGet, starting autoProcessMovie...")
    result = autoProcessMovie.process(sys.argv[1], sys.argv[2], sys.argv[3])
else:
    Logger.warn("Invalid number of arguments received from client.")
    Logger.info("Running autoProcessMovie as a manual run...")
    result = autoProcessMovie.process('Manual Run', 'Manual Run', 0)