[
  {
    "path": ".gitignore",
    "content": "*.py[co]\ntest.py\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\n\n#Translations\n*.mo\n\n#Mr Developer\n.mr.developer.cfg\n\n# Settings\nsettings.cfg\n\n# SQLite database\n*.sqlite\n\n# Log files\n*.log\n\n\n.idea\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "-   repo: git://github.com/pre-commit/pre-commit-hooks\n    sha: master\n    hooks:\n    -   id: trailing-whitespace\n    -   id: autopep8-wrapper\n    -   id: check-yaml\n        files: \\.(yaml|yml)$\n    -   id: fix-encoding-pragma\n    -   id: flake8\n        args:\n          - --ignore=E501,F405,F403,F812\n\n- repo: git://github.com/FalconSocial/pre-commit-python-sorter\n  sha: master\n  hooks:\n  - id: python-import-sorter\n    args:\n    - --silent-overwrite\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Autorippr\n\nSo you'd like to contribue to Autorippr? Fantastic! I love pull requests and welcome any sensible additions to this project.\n\n\n### Guideline Contents\n\nThere are many ways to contribute, this document covers;\n\n\n* [Raising issues](#issues)\n* [Working on Autorippr](#autorippr)\n\n\n<a name=\"raising-issues\"></a>\n## Raising an issue\n\nIf you're looking to raise an issue, just head on over to the [issues log](https://github.com/JasonMillward/Autorippr/issues) and create a new issue.\n\nTag the issue as a [bug] or [feature request] to help keep things neat.\n\nBut before you go and press that submit button take a look at the information you're giving, is it enough?\n\nKeys points to include are:\n\n* General computer info\n    * Operating system\n        * Name and version\n    * General hardware inside (If related to compression). eg; Dual core intel & 2 dvd drives\n\n* Python\n    * Version of python\n\n* Autorippr\n    * Version of Autorippr\n\n* Detailed description of the issue and debug logs if possible.\n    * If the issue is with Autorippr, add `--debug` to the command line options for more information.\n\nProviding this information upfront can assist with diagnosing any issues.\n\n\n<a name=\"autorippr\"></a>\n## Working on Autorippr\n\nWorking with the code should be straight forward. Try to keep the style consistant.\n\n\n\n\n\n\n\n\nPush to your fork and [submit a pull request][pr].\n\n[pr]: https://github.com/JasonMillward/Autorippr/compare/\n\n"
  },
  {
    "path": "CONTRIBUTORS.md",
    "content": "# Author\n\n* JasonMillward https://github.com/JasonMillward\n\n# Contributors\n\n* carrigan98 https://github.com/carrigan98\n* RandomNinjaAtk https://github.com/RandomNinjaAtk\n* Michael Dyrynda https://github.com/michaeldyrynda\n* Ian Bird https://github.com/IanDBird\n* Will Eddins https://github.com/ascendedguard\n* Stefan Budeanu https://github.com/stefanmb\n* Fabien Reboia https://github.com/srounet\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM ubuntu:16.04\n\nCOPY /build/* /\n\nRUN echo \"deb http://ppa.launchpad.net/stebbins/handbrake-releases/ubuntu xenial main \">/etc/apt/sources.list.d/handbreak.list && apt-get update && apt-get install --allow-unauthenticated -y python-pip handbrake-cli libssl1.0.0 libexpat1 libavcodec-ffmpeg56 libgl1-mesa-glx unzip \n#libavcodec-ffmpeg-extra56\n\n\nADD https://github.com/JasonMillward/Autorippr/archive/v1.7.0.zip autorippr-1.7.0.zip\nADD \"http://downloads.sourceforge.net/project/filebot/filebot/FileBot_4.7.2/filebot_4.7.2_amd64.deb?r=http%3A%2F%2Fwww.filebot.net%2F&ts=1473715379&use_mirror=freefr\" filebot_4.7.2_amd64.deb\nRUN pip install tendo pyyaml peewee\nRUN unzip /autorippr-1.7.0.zip\nRUN dpkg -i filebot_4.7.2_amd64.deb\n\nADD settings.example.cfg /Autorippr-1.7.0/settings.cfg\n\nENTRYPOINT [\"python\", \"/Autorippr-1.7.0/autorippr.py\"]\n\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Jason Millward\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "NOTES.md",
    "content": "# Misc notes for future me / developers\n\n## TV Shows (and maybe proper movie titles)\n\nDisc info can be obtained, it returns a lot more information about each disc instead of just the 'is disc inserted'\n```makemkvcon -r info disc:[index]```\n\n## Strings returned:\n\n### Drive scan messages\n\n\tDRV:index,visible,enabled,flags,drive name,disc name\n\tindex - drive index\n\tvisible - set to 1 if drive is present\n\tenabled - set to 1 if drive is accessible\n\tflags - media flags, see AP_DskFsFlagXXX in apdefs.h\n\tdrive name - drive name string\n\tdisc name - disc name string\n\n### Disc information output messages\n\n\tTCOUT:count\n\tcount - titles count\n\n### Disc, title and stream information\n\n\tCINFO:id,code,value\n\tTINFO:id,code,value\n\tSINFO:id,code,value\n\n\tid - attribute id, see AP_ItemAttributeId in apdefs.h\n\tcode - message code if attribute value is a constant string\n\tvalue - attribute value\n\n\n#### Example\n\nTitle count\n\n\tTCOUNT:7\n\nC = Disc info\n\n\tCINFO:1,6209,\"Blu-ray disc\"\n\tCINFO:2,0,\"Breaking Bad: Season 1: Disc 1\"\n\tCINFO:28,0,\"eng\"\n\tCINFO:29,0,\"English\"\n\tCINFO:30,0,\"Breaking Bad: Season 1: Disc 1\"\n\tCINFO:31,6119,\"<b>Source information</b><br>\"\n\tCINFO:32,0,\"BREAKINGBADS1\"\n\tCINFO:33,0,\"0\"\n\nT = Title info\n\n\tTINFO:0,2,0,\"Breaking Bad: Season 1: Disc 1\"\n\tTINFO:0,8,0,\"7\"\n\tTINFO:0,9,0,\"0:58:06\"\n\tTINFO:0,10,0,\"12.5 GB\"\n\tTINFO:0,11,0,\"13472686080\"\n\tTINFO:0,16,0,\"00763.mpls\"\n\tTINFO:0,25,0,\"1\"\n\tTINFO:0,26,0,\"262\"\n\tTINFO:0,27,0,\"Breaking_Bad_Season_1_Disc_1_t00.mkv\"\n\tTINFO:0,28,0,\"eng\"\n\tTINFO:0,29,0,\"English\"\n\tTINFO:0,30,0,\"Breaking Bad: Season 1: Disc 1 - 7 chapter(s) , 12.5 GB\"\n\tTINFO:0,31,6120,\"<b>Title information</b><br>\"\n\tTINFO:0,33,0,\"0\"\n\n\n\t2 - Disc Title\n\t9 - Length of chapters\n\t10 - File size\n\t28 - audio short code\n\t29 - audio long code\n\n## Perfered language\n\n```CINFO``` provides title language, config this\n\n\tCINFO:28,0,\"eng\"\n\tCINFO:29,0,\"English\"\n\n## Subtitles\n\n\t2014-07-21 22:29:18 - Filebot - DEBUG - Get [English] subtitles for 1 files\n\t2014-07-21 22:29:18 - Filebot - DEBUG - Looking up subtitles by hash via OpenSubtitles\n\t2014-07-21 22:29:18 - Filebot - DEBUG - Looking up subtitles by name via OpenSubtitles\n\t2014-07-21 22:29:18 - Filebot - DEBUG - Fetching [Smokin'.Aces.2006.720p.BRRiP.XViD.AC3-LEGi0N.srt]\n\t2014-07-21 22:29:18 - Filebot - DEBUG - Export [Smokin'.Aces.2006.720p.BRRiP.XViD.AC3-LEGi0N.srt] as: SubRip / UTF-8\n\t2014-07-21 22:29:18 - Filebot - DEBUG - Writing [Smokin'.Aces.2006.720p.BRRiP.XViD.AC3-LEGi0N.srt] to [Smokin' Aces (2006).eng.srt]\n"
  },
  {
    "path": "README.md",
    "content": "Autorippr\n=========\n\nAutorippr is currently unmaintained. I am lacking in free time to work on this myself but am happy to accept pull requests or make someone a maintainer of the project. \n\nThat said it should still work for most cases.\n\n\n## Copyright & License\n\nCopyright (c) 2012 Jason Millward - Released under the [MIT license](LICENSE).\n"
  },
  {
    "path": "autorippr.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nAutorippr\n\nRipping\n    Uses MakeMKV to watch for videos inserted into DVD/BD Drives\n\n    Automaticly checks for existing directory/video and will NOT overwrite existing\n    files or folders\n\n    Checks minimum length of video to ensure video is ripped not previews or other\n    junk that happens to be on the DVD\n\n    DVD goes in > MakeMKV gets a proper DVD name > MakeMKV Rips\n\nCompressing\n    An optional additional used to rename and compress videos to an acceptable standard\n    which still delivers quality audio and video but reduces the file size\n    dramatically.\n\n    Using a nice value of 15 by default, it runs HandBrake (or FFmpeg) as a background task\n    that allows other critical tasks to complete first.\n\nExtras\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\nUsage:\n    autorippr.py   ( --rip | --compress | --extra )  [options]\n    autorippr.py   ( --rip [ --compress ] )          [options]\n    autorippr.py   --all                             [options]\n    autorippr.py   --test\n\nOptions:\n    -h --help           Show this screen.\n    --version           Show version.\n    --debug             Output debug.\n    --rip               Rip disc using makeMKV.\n    --compress          Compress using HandBrake or FFmpeg.\n    --extra             Lookup, rename and/or download extras.\n    --all               Do everything.\n    --test              Tests config and requirements.\n    --silent            Silent mode.\n    --skip-compress     Skip the compression step.\n    --force_db=(tv|movie)     Force use of the TheTVDB or TheMovieDB\n\n\"\"\"\n\nimport errno\nimport os\nimport subprocess\nimport sys\n\nimport yaml\nfrom classes import *\nfrom tendo import singleton\n\n__version__ = \"1.7.0\"\n\nme = singleton.SingleInstance()\nCONFIG_FILE = \"{}/settings.cfg\".format(\n    os.path.dirname(os.path.abspath(__file__)))\n\nnotify = None\n\n\ndef eject(config, drive):\n    \"\"\"\n        Ejects the DVD drive\n        Not really worth its own class\n    \"\"\"\n    log = logger.Logger(\"Eject\", config['debug'], config['silent'])\n\n    log.debug(\"Ejecting drive: \" + drive)\n    log.debug(\"Attempting OS detection\")\n\n    try:\n        if sys.platform == 'win32':\n            log.debug(\"OS detected as Windows\")\n            import ctypes\n            ctypes.windll.winmm.mciSendStringW(\n                \"set cdaudio door open\", None, drive, None)\n\n        elif sys.platform == 'darwin':\n            log.debug(\"OS detected as OSX\")\n            p = os.popen(\"drutil eject \" + drive)\n\n            while 1:\n                line = p.readline()\n                if not line:\n                    break\n                log.debug(line.strip())\n\n        else:\n            log.debug(\"OS detected as Unix\")\n            p = os.popen(\"eject -vm \" + drive)\n\n            while 1:\n                line = p.readline()\n                if not line:\n                    break\n                log.debug(line.strip())\n\n    except Exception as ex:\n        log.error(\"Could not detect OS or eject CD tray\")\n        log.ex(\"An exception of type {} occured.\".format(type(ex).__name__))\n        log.ex(\"Args: \\r\\n {}\".format(ex.args))\n\n    finally:\n        del log\n\n\ndef rip(config):\n    \"\"\"\n        Main function for ripping\n        Does everything\n        Returns nothing\n    \"\"\"\n    log = logger.Logger(\"Rip\", config['debug'], config['silent'])\n\n    mkv_save_path = config['makemkv']['savePath']\n\n    log.debug(\"Ripping initialised\")\n    mkv_api = makemkv.MakeMKV(config)\n\n    log.debug(\"Checking for DVDs\")\n    dvds = mkv_api.find_disc()\n\n    log.debug(\"{} DVD(s) found\".format(len(dvds)))\n\n    if len(dvds) > 0:\n        # Best naming convention ever\n        for dvd in dvds:\n            mkv_api.set_title(dvd[\"discTitle\"])\n            mkv_api.set_index(dvd[\"discIndex\"])\n\n            disc_title = mkv_api.get_title()\n            \n            if not config['force_db']:\n                disc_type = mkv_api.get_type()\n            else:\n                disc_type = config['force_db']\n\n            disc_path = os.path.join(mkv_save_path, disc_title)\n            if not os.path.exists(disc_path):\n                os.makedirs(disc_path)\n\n                mkv_api.get_disc_info()\n\n                saveFiles = mkv_api.get_savefiles()\n\n                if len(saveFiles) != 0:\n                    filebot = config['filebot']['enable']\n\n                    for dvdTitle in saveFiles:\n\n                        dbvideo = database.insert_video(\n                            disc_title,\n                            disc_path,\n                            disc_type,\n                            dvdTitle['index'],\n                            filebot\n                        )\n\n                        database.insert_history(\n                            dbvideo,\n                            \"Video added to database\"\n                        )\n\n                        database.update_video(\n                            dbvideo,\n                            3,\n                            dvdTitle['title']\n                        )\n\n                        log.debug(\"Attempting to rip {} from {}\".format(\n                            dvdTitle['title'],\n                            disc_title\n                        ))\n\n                        with stopwatch.StopWatch() as t:\n                            database.insert_history(\n                                dbvideo,\n                                \"Video submitted to MakeMKV\"\n                            )\n                            status = mkv_api.rip_disc(\n                                mkv_save_path, dvdTitle['index'])\n\n                            # Master_and_Commander_De_l'autre_côté_du_monde_t00.mkv become\n                            # Master_and_Commander_De_l_autre_cote_du_monde_t00.mkv\n                            log.debug('Rename {} to {}'.format(\n                                os.path.join(dbvideo.path, dvdTitle['title']),\n                                os.path.join(dbvideo.path, dvdTitle['rename_title'])\n                            ))\n                            os.rename(\n                                os.path.join(dbvideo.path, dvdTitle['title']),\n                                os.path.join(dbvideo.path, dvdTitle['rename_title'])\n                            )\n\n                        if status:\n                            log.info(\"It took {} minute(s) to complete the ripping of {} from {}\".format(\n                                t.minutes,\n                                dvdTitle['title'],\n                                disc_title\n                            ))\n\n                            database.update_video(dbvideo, 4)\n\n                            if 'rip' in config['notification']['notify_on_state']:\n                                notify.rip_complete(dbvideo)\n\n                        else:\n                            database.update_video(dbvideo, 2)\n\n                            database.insert_history(\n                                dbvideo,\n                                \"MakeMKV failed to rip video\"\n                            )\n                            notify.rip_fail(dbvideo)\n\n                            log.info(\n                                \"MakeMKV did not did not complete successfully\")\n                            log.info(\"See log for more details\")\n\n                    if config['makemkv']['eject']:\n                        eject(config, dvd['location'])\n\n                else:\n                    log.info(\"No video titles found\")\n                    log.info(\n                        \"Try decreasing 'minLength' in the config and try again\")\n\n            else:\n                log.info(\"Video folder {} already exists\".format(disc_title))\n\n    else:\n        log.info(\"Could not find any DVDs in drive list\")\n\n\ndef skip_compress(config):\n    \"\"\"\n        Main function for skipping compression\n        Does everything\n        Returns nothing\n    \"\"\"\n    log = logger.Logger(\"Skip compress\", config['debug'], config['silent'])\n\n    log.debug(\"Looking for videos to skip compression\")\n\n    dbvideos = database.next_video_to_compress()\n    comp = compression.Compression(config)\n\n    for dbvideo in dbvideos:\n        if comp.check_exists(dbvideo) is not False:\n            database.update_video(dbvideo, 6)\n            log.info(\"Skipping compression for {} from {}\" .format(\n                dbvideo.filename, dbvideo.vidname))\n\n\ndef compress(config):\n    \"\"\"\n        Main function for compressing\n        Does everything\n        Returns nothing\n    \"\"\"\n    log = logger.Logger(\"Compress\", config['debug'], config['silent'])\n\n    comp = compression.Compression(config)\n\n    log.debug(\"Compressing initialised\")\n    log.debug(\"Looking for videos to compress\")\n\n    dbvideos = database.next_video_to_compress()\n\n    for dbvideo in dbvideos:\n        dbvideo.filename = utils.strip_accents(dbvideo.filename)\n        dbvideo.filename = utils.clean_special_chars(dbvideo.filename)\n\n        if comp.check_exists(dbvideo) is not False:\n\n            database.update_video(dbvideo, 5)\n\n            log.info(\"Compressing {} from {}\" .format(\n                dbvideo.filename, dbvideo.vidname))\n\n            with stopwatch.StopWatch() as t:\n                status = comp.compress(\n                    args=config['compress']['com'],\n                    nice=int(config['compress']['nice']),\n                    dbvideo=dbvideo\n                )\n\n            if status:\n                log.info(\"Video was compressed and encoded successfully\")\n\n                log.info(\"It took {} minutes to compress {}\".format(\n                    t.minutes, dbvideo.filename\n                )\n                )\n\n                database.insert_history(\n                    dbvideo,\n                    \"Compression Completed successfully\"\n                )\n\n                database.update_video(dbvideo, 6)\n\n                if 'compress' in config['notification']['notify_on_state']:\n                    notify.compress_complete(dbvideo)\n\n                comp.cleanup()\n\n            else:\n                database.update_video(dbvideo, 5)\n\n                database.insert_history(dbvideo, \"Compression failed\", 4)\n\n                notify.compress_fail(dbvideo)\n\n                log.info(\"Compression did not complete successfully\")\n        else:\n            database.update_video(dbvideo, 2)\n\n            database.insert_history(\n                dbvideo, \"Input file no longer exists\", 4\n            )\n\n    else:\n        log.info(\"Queue does not exist or is empty\")\n\n\ndef extras(config):\n    \"\"\"\n        Main function for filebotting and flagging forced subs\n        Does everything\n        Returns nothing\n    \"\"\"\n    log = logger.Logger(\"Extras\", config['debug'], config['silent'])\n\n    fb = filebot.FileBot(config['debug'], config['silent'])\n\n    dbvideos = database.next_video_to_filebot()\n\n    for dbvideo in dbvideos:\n        if config['ForcedSubs']['enable']:\n            forced = mediainfo.ForcedSubs(config)\n            log.info(\"Attempting to discover foreign subtitle for {}.\".format(dbvideo.vidname))\n            track = forced.discover_forcedsubs(dbvideo)\n\n            if track is not None:\n                log.info(\"Found foreign subtitle for {}: track {}\".format(dbvideo.vidname, track))\n                log.debug(\"Attempting to flag track for {}: track {}\".format(dbvideo.vidname, track))\n                flagged = forced.flag_forced(dbvideo, track)\n                if flagged:\n                    log.info(\"Flagging success.\")\n                else:\n                    log.debug(\"Flag failed\")\n            else:\n                log.debug(\"Did not find foreign subtitle for {}.\".format(dbvideo.vidname))\n                \n        log.info(\"Attempting video rename\")\n\n        database.update_video(dbvideo, 7)\n\n        movePath = dbvideo.path\n        if config['filebot']['move']:\n            if dbvideo.vidtype == \"tv\":\n                movePath = config['filebot']['tvPath']\n            else:\n                movePath = config['filebot']['moviePath']\n\n        status = fb.rename(dbvideo, movePath)\n\n        if status[0]:\n            log.info(\"Rename success\")\n\n            database.update_video(dbvideo, 6)\n\n            if config['filebot']['subtitles']:\n                log.info(\"Grabbing subtitles\")\n\n                status = fb.get_subtitles(\n                    dbvideo, config['filebot']['language'])\n\n                if status:\n                    log.info(\"Subtitles downloaded\")\n                    database.update_video(dbvideo, 8)\n\n                else:\n                    log.info(\"Subtitles not downloaded, no match\")\n                    database.update_video(dbvideo, 8)\n\n                log.info(\"Completed work on {}\".format(dbvideo.vidname))\n\n                if config['commands'] is not None and len(config['commands']) > 0:\n                    for com in config['commands']:\n                        subprocess.Popen(\n                            [com],\n                            stderr=subprocess.PIPE,\n                            stdout=subprocess.PIPE,\n                            shell=True\n                        )\n\n            else:\n                log.info(\"Not grabbing subtitles\")\n                database.update_video(dbvideo, 8)\n\n\n\n            if 'extra' in config['notification']['notify_on_state']:\n                notify.extra_complete(dbvideo)\n\n            log.debug(\"Attempting to delete %s\" % dbvideo.path)\n\n            try:\n                os.rmdir(dbvideo.path)\n            except OSError as ex:\n                if ex.errno == errno.ENOTEMPTY:\n                    log.debug(\"Directory not empty\")\n\n        else:\n            log.info(\"Rename failed\")\n\n    else:\n        log.info(\"No videos ready for filebot\")\n\n\nif __name__ == '__main__':\n    arguments = docopt.docopt(__doc__, version=__version__)\n    config = yaml.safe_load(open(CONFIG_FILE))\n\n    config['debug'] = arguments['--debug']\n\n    config['silent'] = arguments['--silent']\n    \n    if arguments['--force_db'] not in ['tv','movie', None]:\n        raise ValueError('{} is not a valid DB.'.format(arguments['--force_db']))\n    else:\n        config['force_db'] = arguments['--force_db']\n        \n    notify = notification.Notification(\n        config, config['debug'], config['silent'])\n\n    if bool(config['analytics']['enable']):\n        analytics.ping(__version__)\n\n    if arguments['--test']:\n        testing.perform_testing(config)\n\n    if arguments['--rip'] or arguments['--all']:\n        rip(config)\n\n    if (arguments['--compress'] or arguments['--all']) and not arguments['--skip-compress']:\n        compress(config)\n\n    if arguments['--skip-compress']:\n        skip_compress(config)\n\n    if arguments['--extra'] or arguments['--all']:\n        extras(config)\n"
  },
  {
    "path": "autorippr_install_script.sh",
    "content": "#!/bin/bash\n\n# Install Script Version 1.0\n# This script is designed to install Autorippr for Ubuntu 16.04 LTS\n# All required dependancies and packages will be installed\n# Packages that are installed:\n# --GIT\n# --Makemkv\n# --Python Dev Tools\n# --Python Dependancies for Autorippr\n# --PIP\n# --Handbrake-CLI\n# --Filebot\n# --Autorippr\n\n# Change to execution directory\ncd ~\n\n# Ubuntu 16.04 Error fix for installing packages\nsudo apt-get purge runit\nsudo apt-get purge git-all\nsudo apt-get purge git\nsudo apt-get autoremove\nsudo apt update\n\n# Install Git\nsudo apt install git \n\n#Install PIP\nwget https://bootstrap.pypa.io/get-pip.py\nsudo python get-pip.py\n\n#Intall MakeMKV required tools and libraries\nsudo apt-get install build-essential pkg-config libc6-dev libssl-dev libexpat1-dev libavcodec-dev libgl1-mesa-dev libqt4-dev\n\n#Install MakeMKV\nwget http://www.makemkv.com/download/makemkv-bin-1.10.2.tar.gz\nwget http://www.makemkv.com/download/makemkv-oss-1.10.2.tar.gz\ntar -zxmf makemkv-oss-1.10.2.tar.gz\ntar -zxmf makemkv-bin-1.10.2.tar.gz\ncd makemkv-oss-1.10.2\n./configure\nmake\nsudo make install\ncd ..\ncd makemkv-bin-1.10.2\nmake\nsudo make install\n\n# Install Handbrake CLI\nsudo apt-get install handbrake-cli\n\n# Python update to enable next step\nsudo apt-get install python-dev\n\n# Install Java prerequisite for Filebot\nsudo add-apt-repository -y ppa:webupd8team/java\nsudo apt-get update\nsudo apt-get install oracle-java8-installer\n\n# Install Filebot\nif [ `uname -m` = \"i686\" ]\nthen\n   wget -O filebot-i386.deb 'http://filebot.sourceforge.net/download.php?type=deb&arch=i386'\nelse\n   wget -O filebot-amd64.deb 'http://filebot.sourceforge.net/download.php?type=deb&arch=amd64'\nfi\nsudo dpkg --force-depends -i filebot-*.deb && rm filebot-*.deb\n\n# Install Python Required Packages\nsudo pip install tendo pyyaml peewee pushover\n\n# Install Autorippr\ncd ~\ngit clone https://github.com/JasonMillward/Autorippr.git\ncd Autorippr\ngit checkout\ncp settings.example.cfg settings.cfg\n\n# Verification Test\npython autorippr.py --test\n\n# Completion Message\necho \" \"\necho \"###################################################\"\necho \"##            Install Complete!                  ##\"\necho \"##      Update: ~/Autorippr/settings.cfg         ##\"\necho \"###################################################\"\n"
  },
  {
    "path": "build-docker.sh",
    "content": "#!/bin/bash -e\n#\n# Createas a container with autorippr in \n#\n# Run like\n#\n# docker run -ti -v /tmp:/tmp  --device=/dev/sr1 autorippr --all --debug\n#\n\n\ndocker build -t buildmakemkv ./build_makemkv\ndocker run --rm buildmakemkv | tar xz\ndocker build -t autorippr .\n"
  },
  {
    "path": "build_makemkv/Dockerfile",
    "content": "FROM ubuntu:16.04\n\nRUN apt-get update && apt-get install -y python-virtualenv build-essential pkg-config libc6-dev libssl-dev libexpat1-dev libavcodec-dev libgl1-mesa-dev \n\nADD http://www.makemkv.com/download/makemkv-oss-1.12.3.tar.gz /src/\nADD http://www.makemkv.com/download/makemkv-bin-1.12.3.tar.gz /src/\n\nRUN tar xf /src/makemkv-bin-1.12.3.tar.gz -C /src\n\n\nRUN mkdir /src/makemkv-bin-1.12.3/tmp/\nRUN echo 'accepted' > /src/makemkv-bin-1.12.3/tmp/eula_accepted\n\nRUN  sed -ie 's#DESTDIR=#DESTDIR=/build#g' /src/makemkv-bin-1.12.3/Makefile\n\nRUN cd /src/makemkv-bin-1.12.3 && make install\n\nRUN tar xf /src/makemkv-oss-1.12.3.tar.gz -C /src\n\nRUN cd /src/makemkv-oss-1.12.3 && ./configure --prefix /build/usr --disable-gui && make install\n\nCMD [\"tar\", \"cz\", \"/build\"]\n"
  },
  {
    "path": "classes/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n__all__ = [\n    'analytics',\n    'compression',\n    'database',\n    'docopt',\n    'ffmpeg',\n    'filebot',\n    'handbrake',\n    'logger',\n    'makemkv',\n    'mediainfo',\n    'notification',\n    'stopwatch',\n    'testing',\n    'utils'\n]\n"
  },
  {
    "path": "classes/analytics.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nBasic analytics\n\nThe purpose of this file is to simply send a 'ping' with a unique identifier\nand the script version once a day to give an indication of unique users\n\nThis has been added because Github doesn't show a download counter, I have no\nway of knowing if this script is even being used (except for people telling me\nit broke).\n\nYou are free to opt-out by disabling the config option\n\n    analytics:\n        enable:     True <- make this False\n\nIf the computer doesn't have internet access the script will continue as normal\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\n\ndef ping(version):\n    \"\"\"\n        Send a simple ping to my server\n            to see how many people are using this script\n    \"\"\"\n    try:\n        import uuid\n        import requests\n        import json\n        import os\n        import time\n\n        data = {\n            \"uuid\": uuid.getnode(),\n            \"version\": version\n        }\n\n        datefile = \"/tmp/%s\" % time.strftime(\"%Y%m%d\")\n\n        if not os.path.isfile(datefile):\n\n            with open(datefile, 'w'):\n                os.utime(datefile, None)\n\n            requests.post(\n                'http://api.jcode.me/autorippr/stats',\n                data=json.dumps(data),\n                timeout=5\n            )\n\n    except:\n        pass\n"
  },
  {
    "path": "classes/compression.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCompression Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2014, Ian Bird, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@authors    Ian Bird, Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport os\n\nimport ffmpeg\nimport handbrake\nimport logger\n\n\nclass Compression(object):\n\n    def __init__(self, config):\n        \"\"\"\n            Creates the required compression instances\n\n            Inputs:\n                config    (??): The configuration\n\n            Outputs:\n                The compression instance\n        \"\"\"\n        self.log = logger.Logger(\"Compression\", config['debug'], config['silent'])\n        self.method = self.which_method(config)\n        self.invid = \"\"\n\n    def which_method(self, config):\n        if config['compress']['type'] == \"ffmpeg\":\n            return ffmpeg.FFmpeg(\n                config['debug'],\n                config['compress']['compressionPath'],\n                config['silent'],\n                config['compress']['format']\n            )\n        else:\n            return handbrake.HandBrake(\n                config['debug'],\n                config['compress']['compressionPath'],\n                config['compress']['format'],\n                config['silent']\n            )\n\n    def compress(self, **args):\n        self.log.debug('FFmpeg args: {}'.format(args))\n        return self.method.compress(**args)\n\n    def check_exists(self, dbvideo):\n        \"\"\"\n            Checks to see if the file still exists at the path set in the\n                database\n\n            Inputs:\n                dbvideo (Obj): Video database object\n\n            Outputs:\n                Bool    Does file exist\n\n        \"\"\"\n        self.invid = \"%s/%s\" % (dbvideo.path, dbvideo.filename)\n\n        if os.path.isfile(self.invid):\n            return True\n\n        else:\n            self.log.debug(self.invid)\n            self.log.error(\"Input file no longer exists, abording\")\n            return False\n\n    def cleanup(self):\n        \"\"\"\n            Deletes files once the compression has finished with them\n\n            Inputs:\n                cFile    (Str): File path of the video to remove\n\n            Outputs:\n                None\n        \"\"\"\n        if self.invid is not \"\":\n            try:\n                os.remove(self.invid)\n            except:\n                self.log.error(\"Could not remove %s\" % self.invid)\n"
  },
  {
    "path": "classes/database.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nSQLite Database Helper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport os\nfrom datetime import datetime\n\nfrom peewee import *\nimport utils\n\nDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\ndatabase = SqliteDatabase('%s/autorippr.sqlite' % DIR, **{})\n\n\nclass BaseModel(Model):\n\n    class Meta:\n        database = database\n\n\nclass History(BaseModel):\n    historyid = PrimaryKeyField(db_column='historyID')\n    historydate = DateTimeField(db_column='historyDate')\n    historytext = CharField(db_column='historyText')\n    historytypeid = IntegerField(db_column='historyTypeID')\n    vidid = IntegerField(db_column='vidID')\n\n    class Meta:\n        db_table = 'history'\n\n\nclass Historytypes(BaseModel):\n    historytypeid = PrimaryKeyField(db_column='historyTypeID')\n    historytype = CharField(db_column='historyType')\n\n    class Meta:\n        db_table = 'historyTypes'\n\n\nclass Videos(BaseModel):\n    vidid = PrimaryKeyField(db_column='vidID')\n    vidname = CharField()\n    vidtype = CharField()\n    titleindex = CharField(db_column='titleIndex')\n    path = CharField()\n    filename = CharField(null=True)\n    filebot = BooleanField()\n    statusid = IntegerField(db_column='statusID')\n    lastupdated = DateTimeField(db_column='lastUpdated')\n\n    class Meta:\n        db_table = 'videos'\n\n\nclass Statustypes(BaseModel):\n    statusid = PrimaryKeyField(db_column='statusID')\n    statustext = CharField(db_column='statusText')\n\n    class Meta:\n        db_table = 'statusTypes'\n\n\ndef create_tables():\n    database.connect()\n\n    # Fail silently if tables exists\n    History.create_table(True)\n    Historytypes.create_table(True)\n    Videos.create_table(True)\n    Statustypes.create_table(True)\n\n\ndef create_history_types():\n    historytypes = [\n        [1, 'Info'],\n        [2, 'Error'],\n        [3, 'MakeMKV Error'],\n        [4, 'Handbrake Error']\n    ]\n\n    c = 0\n    for z in Historytypes.select():\n        c += 1\n\n    if c != len(historytypes):\n        for hID, hType in historytypes:\n            Historytypes.create(historytypeid=hID, historytype=hType)\n\n\ndef create_status_types():\n    statustypes = [\n        [1, 'Added'],\n        [2, 'Error'],\n        [3, 'Submitted to makeMKV'],\n        [4, 'Awaiting HandBrake'],\n        [5, 'Submitted to HandBrake'],\n        [6, 'Awaiting FileBot'],\n        [7, 'Submitted to FileBot'],\n        [8, 'Completed']\n    ]\n\n    c = 0\n    for z in Statustypes.select():\n        c += 1\n\n    if c != len(statustypes):\n        for sID, sType in statustypes:\n            Statustypes.create(statusid=sID, statustext=sType)\n\n\ndef next_video_to_compress():\n    videos = Videos.select().where((Videos.statusid == 4) & (\n        Videos.filename != \"None\")).order_by(Videos.vidname)\n    return videos\n\n\ndef next_video_to_filebot():\n    videos = Videos.select().where((Videos.statusid == 6) & (\n        Videos.filename != \"None\") & (Videos.filebot == 1))\n    return videos\n\n\ndef search_video_name(invid):\n    vidqty = Videos.select().where(Videos.filename.startswith(invid)).count()\n    return vidqty\n\n\ndef insert_history(dbvideo, text, typeid=1):\n    return History.create(\n        vidid=dbvideo.vidid,\n        historytext=text,\n        historydate=datetime.now(),\n        historytypeid=typeid\n    )\n\n\ndef insert_video(title, path, vidtype, index, filebot):\n    return Videos.create(\n        vidname=title,\n        vidtype=vidtype,\n        titleindex=index,\n        path=path,\n        filename=\"None\",\n        filebot=filebot,\n        statusid=1,\n        lastupdated=datetime.now()\n    )\n\n\ndef update_video(vidobj, statusid, filename=None):\n    vidobj.statusid = statusid\n    vidobj.lastupdated = datetime.now()\n\n    if filename is not None:\n        filename = utils.strip_accents(filename)\n        filename = utils.clean_special_chars(filename)\n        vidobj.filename = filename\n\n    vidobj.save()\n\n\ndef db_integrity_check():\n    # Stuff\n    create_tables()\n\n    # Things\n    create_history_types()\n    create_status_types()\n\ndb_integrity_check()\n"
  },
  {
    "path": "classes/docopt.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pythonic command-line interface parser that will make you smile.\n\n * http://docopt.org\n * Repository and issue-tracker: https://github.com/docopt/docopt\n * Licensed under terms of MIT license (see LICENSE-MIT)\n * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com\n\n\"\"\"\nimport re\nimport sys\n\n__all__ = ['docopt']\n__version__ = '0.6.1'\n\n\nclass DocoptLanguageError(Exception):\n\n    \"\"\"Error in construction of usage-message by developer.\"\"\"\n\n\nclass DocoptExit(SystemExit):\n\n    \"\"\"Exit in case user invoked program with incorrect arguments.\"\"\"\n\n    usage = ''\n\n    def __init__(self, message=''):\n        SystemExit.__init__(self, (message + '\\n' + self.usage).strip())\n\n\nclass Pattern(object):\n\n    def __eq__(self, other):\n        return repr(self) == repr(other)\n\n    def __hash__(self):\n        return hash(repr(self))\n\n    def fix(self):\n        self.fix_identities()\n        self.fix_repeating_arguments()\n        return self\n\n    def fix_identities(self, uniq=None):\n        \"\"\"Make pattern-tree tips point to same object if they are equal.\"\"\"\n        if not hasattr(self, 'children'):\n            return self\n        uniq = list(set(self.flat())) if uniq is None else uniq\n        for i, child in enumerate(self.children):\n            if not hasattr(child, 'children'):\n                assert child in uniq\n                self.children[i] = uniq[uniq.index(child)]\n            else:\n                child.fix_identities(uniq)\n\n    def fix_repeating_arguments(self):\n        \"\"\"Fix elements that should accumulate/increment values.\"\"\"\n        either = [list(child.children) for child in transform(self).children]\n        for case in either:\n            for e in [child for child in case if case.count(child) > 1]:\n                if type(e) is Argument or type(e) is Option and e.argcount:\n                    if e.value is None:\n                        e.value = []\n                    elif type(e.value) is not list:\n                        e.value = e.value.split()\n                if type(e) is Command or type(e) is Option and e.argcount == 0:\n                    e.value = 0\n        return self\n\n\ndef transform(pattern):\n    \"\"\"Expand pattern into an (almost) equivalent one, but with single Either.\n\n    Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)\n    Quirks: [-a] => (-a), (-a...) => (-a -a)\n\n    \"\"\"\n    result = []\n    groups = [[pattern]]\n    while groups:\n        children = groups.pop(0)\n        parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]\n        if any(t in map(type, children) for t in parents):\n            child = [c for c in children if type(c) in parents][0]\n            children.remove(child)\n            if type(child) is Either:\n                for c in child.children:\n                    groups.append([c] + children)\n            elif type(child) is OneOrMore:\n                groups.append(child.children * 2 + children)\n            else:\n                groups.append(child.children + children)\n        else:\n            result.append(children)\n    return Either(*[Required(*e) for e in result])\n\n\nclass LeafPattern(Pattern):\n\n    \"\"\"Leaf/terminal node of a pattern tree.\"\"\"\n\n    def __init__(self, name, value=None):\n        self.name, self.value = name, value\n\n    def __repr__(self):\n        return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)\n\n    def flat(self, *types):\n        return [self] if not types or type(self) in types else []\n\n    def match(self, left, collected=None):\n        collected = [] if collected is None else collected\n        pos, match = self.single_match(left)\n        if match is None:\n            return False, left, collected\n        left_ = left[:pos] + left[pos + 1:]\n        same_name = [a for a in collected if a.name == self.name]\n        if type(self.value) in (int, list):\n            if type(self.value) is int:\n                increment = 1\n            else:\n                increment = ([match.value] if type(match.value) is str\n                             else match.value)\n            if not same_name:\n                match.value = increment\n                return True, left_, collected + [match]\n            same_name[0].value += increment\n            return True, left_, collected\n        return True, left_, collected + [match]\n\n\nclass BranchPattern(Pattern):\n\n    \"\"\"Branch/inner node of a pattern tree.\"\"\"\n\n    def __init__(self, *children):\n        self.children = list(children)\n\n    def __repr__(self):\n        return '%s(%s)' % (self.__class__.__name__,\n                           ', '.join(repr(a) for a in self.children))\n\n    def flat(self, *types):\n        if type(self) in types:\n            return [self]\n        return sum([child.flat(*types) for child in self.children], [])\n\n\nclass Argument(LeafPattern):\n\n    def single_match(self, left):\n        for n, pattern in enumerate(left):\n            if type(pattern) is Argument:\n                return n, Argument(self.name, pattern.value)\n        return None, None\n\n    @classmethod\n    def parse(class_, source):\n        name = re.findall('(<\\S*?>)', source)[0]\n        value = re.findall('\\[default: (.*)\\]', source, flags=re.I)\n        return class_(name, value[0] if value else None)\n\n\nclass Command(Argument):\n\n    def __init__(self, name, value=False):\n        self.name, self.value = name, value\n\n    def single_match(self, left):\n        for n, pattern in enumerate(left):\n            if type(pattern) is Argument:\n                if pattern.value == self.name:\n                    return n, Command(self.name, True)\n                else:\n                    break\n        return None, None\n\n\nclass Option(LeafPattern):\n\n    def __init__(self, short=None, long=None, argcount=0, value=False):\n        assert argcount in (0, 1)\n        self.short, self.long, self.argcount = short, long, argcount\n        self.value = None if value is False and argcount else value\n\n    @classmethod\n    def parse(class_, option_description):\n        short, long, argcount, value = None, None, 0, False\n        options, _, description = option_description.strip().partition('  ')\n        options = options.replace(',', ' ').replace('=', ' ')\n        for s in options.split():\n            if s.startswith('--'):\n                long = s\n            elif s.startswith('-'):\n                short = s\n            else:\n                argcount = 1\n        if argcount:\n            matched = re.findall('\\[default: (.*)\\]', description, flags=re.I)\n            value = matched[0] if matched else None\n        return class_(short, long, argcount, value)\n\n    def single_match(self, left):\n        for n, pattern in enumerate(left):\n            if self.name == pattern.name:\n                return n, pattern\n        return None, None\n\n    @property\n    def name(self):\n        return self.long or self.short\n\n    def __repr__(self):\n        return 'Option(%r, %r, %r, %r)' % (self.short, self.long,\n                                           self.argcount, self.value)\n\n\nclass Required(BranchPattern):\n\n    def match(self, left, collected=None):\n        collected = [] if collected is None else collected\n        l = left\n        c = collected\n        for pattern in self.children:\n            matched, l, c = pattern.match(l, c)\n            if not matched:\n                return False, left, collected\n        return True, l, c\n\n\nclass Optional(BranchPattern):\n\n    def match(self, left, collected=None):\n        collected = [] if collected is None else collected\n        for pattern in self.children:\n            m, left, collected = pattern.match(left, collected)\n        return True, left, collected\n\n\nclass OptionsShortcut(Optional):\n\n    \"\"\"Marker/placeholder for [options] shortcut.\"\"\"\n\n\nclass OneOrMore(BranchPattern):\n\n    def match(self, left, collected=None):\n        assert len(self.children) == 1\n        collected = [] if collected is None else collected\n        l = left\n        c = collected\n        l_ = None\n        matched = True\n        times = 0\n        while matched:\n            # could it be that something didn't match but changed l or c?\n            matched, l, c = self.children[0].match(l, c)\n            times += 1 if matched else 0\n            if l_ == l:\n                break\n            l_ = l\n        if times >= 1:\n            return True, l, c\n        return False, left, collected\n\n\nclass Either(BranchPattern):\n\n    def match(self, left, collected=None):\n        collected = [] if collected is None else collected\n        outcomes = []\n        for pattern in self.children:\n            matched, _, _ = outcome = pattern.match(left, collected)\n            if matched:\n                outcomes.append(outcome)\n        if outcomes:\n            return min(outcomes, key=lambda outcome: len(outcome[1]))\n        return False, left, collected\n\n\nclass Tokens(list):\n\n    def __init__(self, source, error=DocoptExit):\n        self += source.split() if hasattr(source, 'split') else source\n        self.error = error\n\n    @staticmethod\n    def from_pattern(source):\n        source = re.sub(r'([\\[\\]\\(\\)\\|]|\\.\\.\\.)', r' \\1 ', source)\n        source = [s for s in re.split('\\s+|(\\S*<.*?>)', source) if s]\n        return Tokens(source, error=DocoptLanguageError)\n\n    def move(self):\n        return self.pop(0) if len(self) else None\n\n    def current(self):\n        return self[0] if len(self) else None\n\n\ndef parse_long(tokens, options):\n    \"\"\"long ::= '--' chars [ ( ' ' | '=' ) chars ] ;\"\"\"\n    long, eq, value = tokens.move().partition('=')\n    assert long.startswith('--')\n    value = None if eq == value == '' else value\n    similar = [o for o in options if o.long == long]\n    if tokens.error is DocoptExit and similar == []:  # if no exact match\n        similar = [o for o in options if o.long and o.long.startswith(long)]\n    if len(similar) > 1:  # might be simply specified ambiguously 2+ times?\n        raise tokens.error('%s is not a unique prefix: %s?' %\n                           (long, ', '.join(o.long for o in similar)))\n    elif len(similar) < 1:\n        argcount = 1 if eq == '=' else 0\n        o = Option(None, long, argcount)\n        options.append(o)\n        if tokens.error is DocoptExit:\n            o = Option(None, long, argcount, value if argcount else True)\n    else:\n        o = Option(similar[0].short, similar[0].long,\n                   similar[0].argcount, similar[0].value)\n        if o.argcount == 0:\n            if value is not None:\n                raise tokens.error('%s must not have an argument' % o.long)\n        else:\n            if value is None:\n                if tokens.current() in [None, '--']:\n                    raise tokens.error('%s requires argument' % o.long)\n                value = tokens.move()\n        if tokens.error is DocoptExit:\n            o.value = value if value is not None else True\n    return [o]\n\n\ndef parse_shorts(tokens, options):\n    \"\"\"shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;\"\"\"\n    token = tokens.move()\n    assert token.startswith('-') and not token.startswith('--')\n    left = token.lstrip('-')\n    parsed = []\n    while left != '':\n        short, left = '-' + left[0], left[1:]\n        similar = [o for o in options if o.short == short]\n        if len(similar) > 1:\n            raise tokens.error('%s is specified ambiguously %d times' %\n                               (short, len(similar)))\n        elif len(similar) < 1:\n            o = Option(short, None, 0)\n            options.append(o)\n            if tokens.error is DocoptExit:\n                o = Option(short, None, 0, True)\n        else:  # why copying is necessary here?\n            o = Option(short, similar[0].long,\n                       similar[0].argcount, similar[0].value)\n            value = None\n            if o.argcount != 0:\n                if left == '':\n                    if tokens.current() in [None, '--']:\n                        raise tokens.error('%s requires argument' % short)\n                    value = tokens.move()\n                else:\n                    value = left\n                    left = ''\n            if tokens.error is DocoptExit:\n                o.value = value if value is not None else True\n        parsed.append(o)\n    return parsed\n\n\ndef parse_pattern(source, options):\n    tokens = Tokens.from_pattern(source)\n    result = parse_expr(tokens, options)\n    if tokens.current() is not None:\n        raise tokens.error('unexpected ending: %r' % ' '.join(tokens))\n    return Required(*result)\n\n\ndef parse_expr(tokens, options):\n    \"\"\"expr ::= seq ( '|' seq )* ;\"\"\"\n    seq = parse_seq(tokens, options)\n    if tokens.current() != '|':\n        return seq\n    result = [Required(*seq)] if len(seq) > 1 else seq\n    while tokens.current() == '|':\n        tokens.move()\n        seq = parse_seq(tokens, options)\n        result += [Required(*seq)] if len(seq) > 1 else seq\n    return [Either(*result)] if len(result) > 1 else result\n\n\ndef parse_seq(tokens, options):\n    \"\"\"seq ::= ( atom [ '...' ] )* ;\"\"\"\n    result = []\n    while tokens.current() not in [None, ']', ')', '|']:\n        atom = parse_atom(tokens, options)\n        if tokens.current() == '...':\n            atom = [OneOrMore(*atom)]\n            tokens.move()\n        result += atom\n    return result\n\n\ndef parse_atom(tokens, options):\n    \"\"\"atom ::= '(' expr ')' | '[' expr ']' | 'options'\n             | long | shorts | argument | command ;\n    \"\"\"\n    token = tokens.current()\n    result = []\n    if token in '([':\n        tokens.move()\n        matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]\n        result = pattern(*parse_expr(tokens, options))\n        if tokens.move() != matching:\n            raise tokens.error(\"unmatched '%s'\" % token)\n        return [result]\n    elif token == 'options':\n        tokens.move()\n        return [OptionsShortcut()]\n    elif token.startswith('--') and token != '--':\n        return parse_long(tokens, options)\n    elif token.startswith('-') and token not in ('-', '--'):\n        return parse_shorts(tokens, options)\n    elif token.startswith('<') and token.endswith('>') or token.isupper():\n        return [Argument(tokens.move())]\n    else:\n        return [Command(tokens.move())]\n\n\ndef parse_argv(tokens, options, options_first=False):\n    \"\"\"Parse command-line argument vector.\n\n    If options_first:\n        argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;\n    else:\n        argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;\n\n    \"\"\"\n    parsed = []\n    while tokens.current() is not None:\n        if tokens.current() == '--':\n            return parsed + [Argument(None, v) for v in tokens]\n        elif tokens.current().startswith('--'):\n            parsed += parse_long(tokens, options)\n        elif tokens.current().startswith('-') and tokens.current() != '-':\n            parsed += parse_shorts(tokens, options)\n        elif options_first:\n            return parsed + [Argument(None, v) for v in tokens]\n        else:\n            parsed.append(Argument(None, tokens.move()))\n    return parsed\n\n\ndef parse_defaults(doc):\n    defaults = []\n    for s in parse_section('options:', doc):\n        # FIXME corner case \"bla: options: --foo\"\n        _, _, s = s.partition(':')  # get rid of \"options:\"\n        split = re.split('\\n[ \\t]*(-\\S+?)', '\\n' + s)[1:]\n        split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]\n        options = [Option.parse(s) for s in split if s.startswith('-')]\n        defaults += options\n    return defaults\n\n\ndef parse_section(name, source):\n    pattern = re.compile('^([^\\n]*' + name + '[^\\n]*\\n?(?:[ \\t].*?(?:\\n|$))*)',\n                         re.IGNORECASE | re.MULTILINE)\n    return [s.strip() for s in pattern.findall(source)]\n\n\ndef formal_usage(section):\n    _, _, section = section.partition(':')  # drop \"usage:\"\n    pu = section.split()\n    return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'\n\n\ndef extras(help, version, options, doc):\n    if help and any((o.name in ('-h', '--help')) and o.value for o in options):\n        print(doc.strip(\"\\n\"))\n        sys.exit()\n    if version and any(o.name == '--version' and o.value for o in options):\n        print(version)\n        sys.exit()\n\n\nclass Dict(dict):\n\n    def __repr__(self):\n        return '{%s}' % ',\\n '.join('%r: %r' % i for i in sorted(self.items()))\n\n\ndef docopt(doc, argv=None, help=True, version=None, options_first=False):\n    \"\"\"Parse `argv` based on command-line interface described in `doc`.\n\n    `docopt` creates your command-line interface based on its\n    description that you pass as `doc`. Such description can contain\n    --options, <positional-argument>, commands, which could be\n    [optional], (required), (mutually | exclusive) or repeated...\n\n    Parameters\n    ----------\n    doc : str\n        Description of your command-line interface.\n    argv : list of str, optional\n        Argument vector to be parsed. sys.argv[1:] is used if not\n        provided.\n    help : bool (default: True)\n        Set to False to disable automatic help on -h or --help\n        options.\n    version : any object\n        If passed, the object will be printed if --version is in\n        `argv`.\n    options_first : bool (default: False)\n        Set to True to require options precede positional arguments,\n        i.e. to forbid options and positional arguments intermix.\n\n    Returns\n    -------\n    args : dict\n        A dictionary, where keys are names of command-line elements\n        such as e.g. \"--verbose\" and \"<path>\", and values are the\n        parsed values of those elements.\n\n    Example\n    -------\n    >>> from docopt import docopt\n    >>> doc = '''\n    ... Usage:\n    ...     my_program tcp <host> <port> [--timeout=<seconds>]\n    ...     my_program serial <port> [--baud=<n>] [--timeout=<seconds>]\n    ...     my_program (-h | --help | --version)\n    ...\n    ... Options:\n    ...     -h, --help  Show this screen and exit.\n    ...     --baud=<n>  Baudrate [default: 9600]\n    ... '''\n    >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']\n    >>> docopt(doc, argv)\n    {'--baud': '9600',\n     '--help': False,\n     '--timeout': '30',\n     '--version': False,\n     '<host>': '127.0.0.1',\n     '<port>': '80',\n     'serial': False,\n     'tcp': True}\n\n    See also\n    --------\n    * For video introduction see http://docopt.org\n    * Full documentation is available in README.rst as well as online\n      at https://github.com/docopt/docopt#readme\n\n    \"\"\"\n    argv = sys.argv[1:] if argv is None else argv\n\n    usage_sections = parse_section('usage:', doc)\n    if len(usage_sections) == 0:\n        raise DocoptLanguageError('\"usage:\" (case-insensitive) not found.')\n    if len(usage_sections) > 1:\n        raise DocoptLanguageError('More than one \"usage:\" (case-insensitive).')\n    DocoptExit.usage = usage_sections[0]\n\n    options = parse_defaults(doc)\n    pattern = parse_pattern(formal_usage(DocoptExit.usage), options)\n    # [default] syntax for argument is disabled\n    # for a in pattern.flat(Argument):\n    #    same_name = [d for d in arguments if d.name == a.name]\n    #    if same_name:\n    #        a.value = same_name[0].value\n    argv = parse_argv(Tokens(argv), list(options), options_first)\n    pattern_options = set(pattern.flat(Option))\n    for options_shortcut in pattern.flat(OptionsShortcut):\n        doc_options = parse_defaults(doc)\n        options_shortcut.children = list(set(doc_options) - pattern_options)\n        # if any_options:\n        #    options_shortcut.children += [Option(o.short, o.long, o.argcount)\n        #                    for o in argv if type(o) is Option]\n    extras(help, version, argv, doc)\n    matched, left, collected = pattern.fix().match(argv)\n    if matched and left == []:  # better error message if left?\n        return Dict((a.name, a.value) for a in (pattern.flat() + collected))\n    raise DocoptExit()\n"
  },
  {
    "path": "classes/ffmpeg.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nFFmpeg Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2014, Ian Bird\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Ian Bird\n@license    http://opensource.org/licenses/MIT\n\"\"\"\nimport os\nimport re\nimport subprocess\n\nimport database\nimport logger\n\n\nclass FFmpeg(object):\n\n    def __init__(self, debug, compressionpath, silent, vformat):\n        self.log = logger.Logger(\"FFmpeg\", debug, silent)\n        self.compressionPath = compressionpath\n        self.vformat = vformat\n\n    def compress(self, nice, args, dbvideo):\n        \"\"\"\n            Passes the necessary parameters to FFmpeg to start an encoding\n            Assigns a nice value to allow give normal system tasks priority\n\n\n            Inputs:\n                nice    (Int): Priority to assign to task (nice value)\n                args    (Str): All of the FFmpeg arguments taken from the\n                                settings file\n                output  (Str): File to log to. Used to see if the job completed\n                                successfully\n\n            Outputs:\n                Bool    Was convertion successful\n        \"\"\"\n\n        if (dbvideo.vidtype == \"tv\"):\n            # Query the SQLite database for similar titles (TV Shows)\n            vidname = re.sub(r'D(\\d)', '', dbvideo.vidname)\n            vidqty = database.search_video_name(vidname)\n            if vidqty == 0:\n                vidname = \"%sE1.%s\" % (vidname, self.vformat)\n            else:\n                vidname = \"%sE%s.%s\" % (vidname, str(vidqty + 1), self.vformat)\n        else:\n            vidname = \"%s.%s\" % (dbvideo.vidname, self.vformat)\n\n        invid = \"%s/%s\" % (dbvideo.path, dbvideo.filename)\n        outvid = os.path.join(self.compressionPath, os.path.basename(dbvideo.path), vidname)\n        destination_folder = os.path.dirname(outvid)\n\n        if not os.path.exists(destination_folder):\n            self.log.info('Destination folder does not exists, creating: {}'.format(\n                destination_folder\n            ))\n            os.makedirs(destination_folder)\n\n        command = 'nice -n {0} ffmpeg -i \"{1}\" {2} \"{3}\"'.format(\n            nice,\n            invid,\n            ' '.join(args),\n            outvid\n        )\n\n        proc = subprocess.Popen(\n            command,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            shell=True\n        )\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"FFmpeg (compress) returned status code: %d\" % proc.returncode)\n            return False\n\n        return True\n"
  },
  {
    "path": "classes/filebot.py",
    "content": "#*- coding: utf-8 -*-\n\"\"\"\nFileBot class\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport re\nimport subprocess\nimport logger\n\n\nclass FileBot(object):\n\n    def __init__(self, debug, silent):\n        self.log = logger.Logger(\"Filebot\", debug, silent)\n\n    def rename(self, dbvideo, movePath):\n        \"\"\"\n            Renames video file upon successful database lookup\n\n            Inputs:\n                dbvideo (Obj): Video database object\n\n            Outputs:\n                Bool    Was lookup successful\n        \"\"\"\n\n        if dbvideo.vidtype == \"tv\":\n            db = \"TheTVDB\"\n        else:\n            db = \"TheMovieDB\"\n\n        vidname = re.sub(r'S(\\d)', '', dbvideo.vidname)\n        vidname = re.sub(r'D(\\d)', '', vidname)\n\n\n        proc = subprocess.Popen(\n            [\n                'filebot',\n                '-rename',\n                \"%s/%s\" % (dbvideo.path, dbvideo.filename),\n                '--q',\n                \"\\\"%s\\\"\" % vidname,\n                '-non-strict',\n                '--db',\n                '%s' % db,\n                '--output',\n                \"%s\" % movePath\n            ],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE\n        )\n\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"Filebot (rename) returned status code: %d\" % proc.returncode)\n\n        renamedvideo = \"\"\n        checks = 0\n\n        if len(results) is not 0:\n            lines = results.split(\"\\n\")\n            self.log.debug(results.split(\"\\n\"))\n            for line in lines:\n                if line:\n                    self.log.debug(line.strip())\n                if \"MOVE\" in line:\n                    renamedvideo = line.split(\"] to [\", 1)[1].rstrip(']')\n                    checks += 1\n\n                if \"Processed\" in line:\n                    checks += 1\n\n                if \"Done\" in line:\n                    checks += 1\n\n        if checks >= 3 and renamedvideo:\n            return [True, renamedvideo]\n        else:\n            return [False]\n\n    def get_subtitles(self, dbvideo, lang):\n        \"\"\"\n            Downloads subtitles of specified language\n\n            Inputs:\n                dbvideo (Obj): Video database object\n                lang    (Str): Language of subtitles to download\n\n            Outputs:\n                Bool    Was download successful\n        \"\"\"\n        proc = subprocess.Popen(\n            [\n                'filebot',\n                '-get-subtitles',\n                dbvideo.path,\n                '--q',\n                \"\\\"%s\\\"\" % dbvideo.vidname,\n                '--lang',\n                lang,\n                '--output',\n                'srt',\n                '--encoding',\n                'utf8',\n                '-non-strict'\n            ],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE\n        )\n\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"Filebot (get_subtitles) returned status code: %d\" % proc.returncode)\n\n        checks = 0\n\n        if len(results) is not 0:\n            lines = results.split(\"\\n\")\n            for line in lines:\n                self.log.debug(line.strip())\n\n                if \"Processed\" in line:\n                    checks += 1\n\n                if \"Done\" in line:\n                    checks += 1\n\n        if checks >= 2:\n            return True\n        else:\n            return False\n"
  },
  {
    "path": "classes/handbrake.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nHandBrake CLI Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport re\nimport subprocess\nimport database\nimport logger\n\n\nclass HandBrake(object):\n\n    def __init__(self, debug, compressionpath, vformat, silent):\n        self.log = logger.Logger(\"HandBrake\", debug, silent)\n        self.compressionPath = compressionpath\n        self.vformat = vformat\n\n    def compress(self, nice, args, dbvideo):\n        \"\"\"\n            Passes the necessary parameters to HandBrake to start an encoding\n            Assigns a nice value to allow give normal system tasks priority\n\n            Inputs:\n                nice    (Int): Priority to assign to task (nice value)\n                args    (Str): All of the handbrake arguments taken from the\n                                settings file\n                output  (Str): File to log to. Used to see if the job completed\n                                successfully\n\n            Outputs:\n                Bool    Was convertion successful\n        \"\"\"\n        checks = 0\n\n        if (dbvideo.vidtype == \"tv\"):\n            # Query the SQLite database for similar titles (TV Shows)\n            vidname = re.sub(r'D(\\d)', '', dbvideo.vidname)\n            vidqty = database.search_video_name(vidname)\n            if vidqty == 0:\n                vidname = \"%sE1.%s\" % (vidname, self.vformat)\n            else:\n                vidname = \"%sE%s.%s\" % (vidname, str(vidqty + 1), self.vformat)\n        else:\n            vidname = \"%s.%s\" % (dbvideo.vidname, self.vformat)\n\n        invid = \"%s/%s\" % (dbvideo.path, dbvideo.filename)\n        outvid = \"%s/%s\" % (dbvideo.path, vidname)\n        command = 'nice -n {0} {1}HandBrakeCLI --verbose -i \"{2}\" -o \"{3}\" {4}'.format(\n            nice,\n            self.compressionPath,\n            invid,\n            outvid,\n            ' '.join(args)\n        )\n\n        proc = subprocess.Popen(\n            command,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            shell=True\n        )\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"HandBrakeCLI (compress) returned status code: %d\" % proc.returncode)\n\n        if results is not None and len(results) is not 0:\n            lines = results.split(\"\\n\")\n            for line in lines:\n                if \"Encoding: task\" not in line:\n                    self.log.debug(line.strip())\n\n                if \"average encoding speed for job\" in line:\n                    checks += 1\n\n                if \"Encode done!\" in line:\n                    checks += 1\n\n                if \"ERROR\" in line and \"opening\" not in line and \"udfread\" not in line:\n                    self.log.error(\n                        \"HandBrakeCLI encountered the following error: \")\n                    self.log.error(line)\n\n                    return False\n\n        if checks >= 2:\n            self.log.debug(\"HandBrakeCLI Completed successfully\")\n\n            database.update_video(\n                dbvideo, 6, filename=\"%s\" % (\n                    vidname\n                ))\n\n            return True\n        else:\n            return False\n"
  },
  {
    "path": "classes/logger.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nSimple logging class\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport logging\nimport os\nimport sys\n\n\nclass Logger(object):\n\n    def __init__(self, name, debug, silent):\n        self.silent = silent\n\n        frmt = logging.Formatter(\n            '%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n            \"%Y-%m-%d %H:%M:%S\"\n        )\n\n        if debug:\n            loglevel = logging.DEBUG\n        else:\n            loglevel = logging.INFO\n\n        self.createhandlers(frmt, name, loglevel)\n\n    def __del__(self):\n        if not self.silent:\n            self.log.removeHandler(self.sh)\n        self.log.removeHandler(self.fh)\n        self.log = None\n\n    def createhandlers(self, frmt, name, loglevel):\n        self.log = logging.getLogger(name)\n        self.log.setLevel(loglevel)\n\n        if not self.silent:\n            self.sh = logging.StreamHandler(sys.stdout)\n            self.sh.setLevel(loglevel)\n            self.sh.setFormatter(frmt)\n            self.log.addHandler(self.sh)\n\n        DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n        self.fh = logging.FileHandler('%s/autorippr.log' % DIR)\n        self.fh.setLevel(loglevel)\n        self.fh.setFormatter(frmt)\n        self.log.addHandler(self.fh)\n\n    def debug(self, msg):\n        self.log.debug(msg)\n\n    def info(self, msg):\n        self.log.info(msg)\n\n    def warn(self, msg):\n        self.log.warn(msg)\n\n    def error(self, msg):\n        self.log.error(msg)\n\n    def critical(self, msg):\n        self.log.critical(msg)\n"
  },
  {
    "path": "classes/makemkv.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nMakeMKV CLI Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport csv\nimport datetime\nimport re\nimport subprocess\nimport time\n\nimport logger\n\nimport utils\n\n\nclass MakeMKV(object):\n\n    def __init__(self, config):\n        self.discIndex = 0\n        self.vidName = \"\"\n        self.path = \"\"\n        self.vidType = \"\"\n        self.minLength = int(config['makemkv']['minLength'])\n        self.maxLength = int(config['makemkv']['maxLength'])\n        self.cacheSize = int(config['makemkv']['cache'])\n        self.ignore_region = bool(config['makemkv']['ignore_region'])\n        self.log = logger.Logger(\"Makemkv\", config['debug'], config['silent'])\n        self.makemkvconPath = config['makemkv']['makemkvconPath']\n        self.saveFiles = []\n\n    def _clean_title(self):\n        \"\"\"\n            Removes the extra bits in the title and removes whitespace\n\n            Inputs:\n                None\n\n            Outputs:\n                None\n        \"\"\"\n        tmpname = self.vidName\n        tmpname = tmpname.title().replace(\"Extended_Edition\", \"\")\n        tmpname = tmpname.replace(\"Special_Edition\", \"\")\n        tmpname = re.sub(r\"Disc_(\\d)(.*)\", r\"D\\1\", tmpname)\n        tmpname = re.sub(r\"Disc\\s*(\\d)(.*)\", r\"D\\1\", tmpname)\n        tmpname = re.sub(r\"Season_(\\d)\", r\"S\\1\", tmpname)\n        tmpname = re.sub(r\"Season(\\d)\", r\"S\\1\", tmpname)\n        tmpname = re.sub(r\"S(\\d)_\", r\"S\\1\", tmpname)\n        tmpname = tmpname.replace(\"_t00\", \"\")\n        tmpname = tmpname.replace(\"\\\"\", \"\").replace(\"_\", \" \")\n\n        # Clean up the edges and remove whitespace\n        self.vidName = tmpname.strip()\n\n    def _remove_duplicates(self, title_list):\n        seen_titles = set()\n        new_list = []\n        for obj in title_list:\n            if obj['title'] not in seen_titles:\n                new_list.append(obj)\n                seen_titles.add(obj['title'])\n\n        return new_list\n\n    def _read_mkv_messages(self, stype, sid=None, scode=None):\n        \"\"\"\n            Returns a list of messages that match the search string\n            Parses message output.\n\n            Inputs:\n                stype   (Str): Type of message\n                sid     (Int): ID of message\n                scode   (Int): Code of message\n\n            Outputs:\n                toreturn    (List)\n        \"\"\"\n        toreturn = []\n\n        with open('/tmp/makemkvMessages', 'r') as messages:\n            for line in messages:\n                if line[:len(stype)] == stype:\n                    values = line.replace(\"%s:\" % stype, \"\").strip()\n\n                    cr = csv.reader([values])\n\n                    if sid is not None:\n                        for row in cr:\n                            if int(row[0]) == int(sid):\n                                if scode is not None:\n                                    if int(row[1]) == int(scode):\n                                        toreturn.append(row[3])\n                                else:\n                                    toreturn.append(row[2])\n\n                    else:\n                        for row in cr:\n                            toreturn.append(row[0])\n\n        return toreturn\n\n    def set_title(self, vidname):\n        \"\"\"\n            Sets local video name\n\n            Inputs:\n                vidName   (Str): Name of video\n\n            Outputs:\n                None\n        \"\"\"\n        self.vidName = vidname\n\n    def set_index(self, index):\n        \"\"\"\n            Sets local disc index\n\n            Inputs:\n                index   (Int): Disc index\n\n            Outputs:\n                None\n        \"\"\"\n        self.discIndex = int(index)\n\n    def rip_disc(self, path, titleIndex):\n        \"\"\"\n            Passes in all of the arguments to makemkvcon to start the ripping\n                of the currently inserted DVD or BD\n\n            Inputs:\n                path    (Str):  Where the video will be saved to\n                output  (Str):  Temp file to save output to\n\n            Outputs:\n                Success (Bool)\n        \"\"\"\n        self.path = path\n\n        fullpath = '%s/%s' % (self.path, self.vidName)\n\n        proc = subprocess.Popen(\n            [\n                '%smakemkvcon' % self.makemkvconPath,\n                'mkv',\n                'disc:%d' % self.discIndex,\n                titleIndex,\n                fullpath,\n                '--cache=%d' % self.cacheSize,\n                '--noscan',\n                '--decrypt',\n                '--minlength=%d' % self.minLength\n            ],\n            stderr=subprocess.PIPE,\n            stdout=subprocess.PIPE\n        )\n\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"MakeMKV (rip_disc) returned status code: %d\" % proc.returncode)\n\n        if errors is not None:\n            if len(errors) is not 0:\n                self.log.error(\"MakeMKV encountered the following error: \")\n                self.log.error(errors)\n                return False\n\n        checks = 0\n\n        lines = results.split(\"\\n\")\n        for line in lines:\n            if \"skipped\" in line:\n                continue\n\n            badstrings = [\n                \"failed\",\n                \"fail\",\n                \"error\"\n            ]\n\n            if any(x in line.lower() for x in badstrings):\n                if self.ignore_region and \"RPC protection\" in line:\n                    self.log.warn(line)\n                elif \"Failed to add angle\" in line:\n                    self.log.warn(line)\n                else:\n                    self.log.error(line)\n                    return False\n\n            if \"Copy complete\" in line:\n                checks += 1\n\n            if \"titles saved\" in line:\n                checks += 1\n\n        if checks >= 2:\n            return True\n        else:\n            return False\n\n    def find_disc(self):\n        \"\"\"\n            Use makemkvcon to list all DVDs or BDs inserted\n            If more then one disc is inserted, use the first result\n\n            Inputs:\n                output  (Str): Temp file to save output to\n\n            Outputs:\n                Success (Bool)\n        \"\"\"\n        drives = []\n        proc = subprocess.Popen(\n            ['%smakemkvcon' % self.makemkvconPath, '-r', 'info', 'disc:-1'],\n            stderr=subprocess.PIPE,\n            stdout=subprocess.PIPE\n        )\n\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"MakeMKV (find_disc) returned status code: %d\" % proc.returncode)\n\n        if errors is not None:\n            if len(errors) is not 0:\n                self.log.error(\"MakeMKV encountered the following error: \")\n                self.log.error(errors)\n                return []\n\n        if \"This application version is too old.\" in results:\n            self.log.error(\"Your MakeMKV version is too old.\"\n                           \"Please download the latest version at http://www.makemkv.com\"\n                           \" or enter a registration key to continue using MakeMKV.\")\n\n            return []\n\n        # Passed the simple tests, now check for disk drives\n        lines = results.split(\"\\n\")\n        for line in lines:\n            if line[:4] == \"DRV:\":\n                if \"/dev/\" in line:\n                    out = line.split(',')\n\n                    if len(str(out[5])) > 3:\n\n                        drives.append(\n                            {\n                                \"discIndex\": out[0].replace(\"DRV:\", \"\"),\n                                \"discTitle\": out[5],\n                                \"location\": out[6]\n                            }\n                        )\n\n        return drives\n\n    def get_disc_info(self):\n        \"\"\"\n            Returns information about the selected disc\n\n            Inputs:\n                None\n\n            Outputs:\n                None\n        \"\"\"\n\n        proc = subprocess.Popen(\n            [\n                '%smakemkvcon' % self.makemkvconPath,\n                '-r',\n                'info',\n                'disc:%d' % self.discIndex,\n                '--decrypt',\n                '--minlength=%d' % self.minLength,\n                '--messages=/tmp/makemkvMessages'\n            ],\n            stderr=subprocess.PIPE\n        )\n\n        (results, errors) = proc.communicate()\n\n        if proc.returncode is not 0:\n            self.log.error(\n                \"MakeMKV (get_disc_info) returned status code: %d\" % proc.returncode)\n\n        if errors is not None:\n            if len(errors) is not 0:\n                self.log.error(\"MakeMKV encountered the following error: \")\n                self.log.error(errors)\n                return False\n\n        foundtitles = int(self._read_mkv_messages(\"TCOUNT\")[0])\n\n        self.log.debug(\"MakeMKV found {} titles\".format(foundtitles))\n\n        if foundtitles > 0:\n            for titleNo in set(self._read_mkv_messages(\"TINFO\")):\n                durTemp = self._read_mkv_messages(\"TINFO\", titleNo, 9)[0]\n                x = time.strptime(durTemp, '%H:%M:%S')\n                titleDur = datetime.timedelta(\n                    hours=x.tm_hour,\n                    minutes=x.tm_min,\n                    seconds=x.tm_sec\n                ).total_seconds()\n\n                if self.vidType == \"tv\" and titleDur > self.maxLength:\n                    self.log.debug(\"Excluding Title No.: {}, Title: {}. Exceeds maxLength\".format(\n                        titleNo,\n                        self._read_mkv_messages(\"TINFO\", titleNo, 27)\n                    ))\n                    continue\n\n                if self.vidType == \"movie\" and not re.search('00', self._read_mkv_messages(\"TINFO\", titleNo, 27)[0]):\n                    self.log.debug(\"Excluding Title No.: {}, Title: {}. Only want first title\".format(\n                        titleNo,\n                        self._read_mkv_messages(\"TINFO\", titleNo, 27)\n                    ))\n                    continue\n\n                self.log.debug(\"MakeMKV title info: Disc Title: {}, Title No.: {}, Title: {}, \".format(\n                    self._read_mkv_messages(\"CINFO\", 2),\n                    titleNo,\n                    self._read_mkv_messages(\"TINFO\", titleNo, 27)\n                ))\n\n                title = self._read_mkv_messages(\"TINFO\", titleNo, 27)[0]\n                rename_title = utils.strip_accents(title)\n                rename_title = utils.clean_special_chars(rename_title)\n\n                self.saveFiles.append({\n                    'index': titleNo,\n                    'title': title,\n                    'rename_title': rename_title,\n                })\n\n    def get_type(self):\n        \"\"\"\n            Returns the type of video (tv/movie)\n\n            Inputs:\n                None\n\n            Outputs:\n                vidType   (Str)\n        \"\"\"\n        titlePattern = re.compile(\n            r'(DISC_(\\d))|(DISC(\\d))|(D(\\d))|(SEASON_(\\d))|(SEASON(\\d))|(S(\\d))'\n        )\n\n        if titlePattern.search(self.vidName):\n            self.log.debug(\"Detected TV {}\".format(self.vidName))\n            self.vidType = \"tv\"\n        else:\n            self.log.debug(\"Detected movie {}\".format(self.vidName))\n            self.vidType = \"movie\"\n        return self.vidType\n\n    def get_title(self):\n        \"\"\"\n            Returns the current videos title\n\n            Inputs:\n                None\n\n            Outputs:\n                vidName   (Str)\n        \"\"\"\n        self._clean_title()\n        return self.vidName\n\n    def get_savefiles(self):\n        \"\"\"\n            Returns the current videos title\n\n            Inputs:\n                None\n\n            Outputs:\n                vidName   (Str)\n        \"\"\"\n        return self._remove_duplicates(self.saveFiles)\n"
  },
  {
    "path": "classes/mediainfo.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCreated on Mon Jan 09 11:20:23 2017\n\nDependencies:\n   System:\n       mediainfo\n       mkvtoolnix\n\n   Python (nonstandard library):\n       pymediainfo\n\nFor Windows, if mediainfo or mkvpropedit aren't in PATH, must give path to .dll (mediainfo)\nor .exe (mkvpropedit) file\nFor *nixs, use path to binary (although it's likly in PATH)\n\nTakes an mkv file and analyzes it for foreign subtitle track. Assumes that foreign subtitle\ntrack files are smaller in bit size but the same length as the main language track\n\n\n@author: brodi\n\"\"\"\n\nimport os\nfrom pymediainfo import MediaInfo\nfrom pipes import quote\nimport logger\nimport shlex\nimport subprocess\n\n# main class that initializes settings for discovering/flagging a forced subtitle track\n# edits python's os.environ in favor of putting full string when calling executables\nclass ForcedSubs(object):\n    def __init__(self, config):\n        self.log = logger.Logger('ForcedSubs', config['debug'], config['silent'])\n        self.lang = config['ForcedSubs']['language']\n        self.secsub_ratio = float(config['ForcedSubs']['ratio'])\n        self.mediainfoPath = config['ForcedSubs']['mediainfoPath']\n        self.mkvpropeditPath = config['ForcedSubs']['mkvpropeditPath']\n        if (self.mediainfoPath and\n            os.path.dirname(self.mediainfoPath) not in os.environ['PATH']):\n            os.environ['PATH'] = (os.path.dirname(config['ForcedSubs']['mediainfoPath']) + ';' +\n                                  os.environ['PATH'])\n        if (self.mkvpropeditPath and\n            os.path.dirname(self.mkvpropeditPath) not in os.environ['PATH']):\n            os.environ['PATH'] = (os.path.dirname(config['ForcedSubs']['mkvpropeditPath']) + ';' +\n                                  os.environ['PATH'])\n\n    def discover_forcedsubs(self, dbvideo):\n        \"\"\"\n            Attempts to find foreign subtitle track\n\n            Input:\n                dbvideo (Obj): Video database object\n\n            Output:\n                If successful, track number of forced subtitle\n                Else, None\n        \"\"\"\n        MEDIADIR = os.path.join(dbvideo.path, dbvideo.filename)\n#        wrapper class for mediainfo tool\n        media_info = MediaInfo.parse(MEDIADIR.encode('unicode-escape'))\n        subs = []\n#       Iterates though tracks and finds subtitles in preferred language, creates\n#       list of dictionaries\n        for track in media_info.tracks:\n            data = track.to_data()\n            if data['track_type'] == 'Text' and data['language']==self.lang:\n                subs.append(data)\n        if len(subs) is 0:\n            self.log.info(\"No subtitle found, cannot determine foreign language track.\")\n            return None\n        if len(subs) is 1:\n            self.log.info(\"Only one {} subtitle found, cannot determine foreign language track.\"\n                          .format(self.lang))\n            return None\n\n#   Sort list by size of track file\n        subs.sort(key=lambda sub: sub['stream_size'], reverse = True)\n\n#   Main language subtitle assumed to be largest\n        main_sub = subs[0]\n        main_subsize = main_sub['stream_size']\n        main_sublen = float(main_sub['duration'])\n#   Checks other subs for size, duration, and if forced flag is set\n        for sub in subs[1:]:\n            if (\n                sub['stream_size'] <= main_subsize*self.secsub_ratio\n                and main_sublen*.9 <= float(sub['duration']) <= main_sublen*1.1\n                and sub['forced']=='No'\n                ):\n                secondary_sub = sub\n            else:\n                self.log.info(\"No foreign language subtitle found, try adjusting ratio.\")\n                return None\n        return secondary_sub['track_id']\n\n    def flag_forced(self, dbvideo, track):\n        \"\"\"\n            Uses mkvpropedit to edit mkv header and flag the detected track as 'forced'\n\n            Input:\n                dbvideo (Obj): Video database object\n                track (int): Track number of foreign track to be flagged as 'forced'\n\n            Output:\n                Bool: Returns True of successful, returns False if not\n        \"\"\"\n\n        MEDIADIR = os.path.join(dbvideo.path, dbvideo.filename)\n        cmd_raw = 'mkvpropedit {} --edit track:{} --set flag-forced=1'.format(quote(MEDIADIR), track)\n        cmd = shlex.split(cmd_raw)\n        self.log.debug(\"mkpropedit cmd: {}\".format(cmd))\n\n        proc = subprocess.Popen(\n                                cmd,\n                                stdout=subprocess.PIPE,\n                                stderr=subprocess.PIPE\n                                )\n        \n        (results, error) = proc.communicate()\n\n\n        if proc.returncode is not 0:\n            self.log.error(\n                           \"mkvpropedit (forced subtitles) returned status code {}\".format(proc.returncode)\n                           )\n            return False\n\n        if len(results) is not 0:\n            lines = results.split('\\n')\n            for line in lines:\n                self.log.debug(line.strip())\n\n        return True\n\n"
  },
  {
    "path": "classes/notification.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nNotification Class\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport logger\n\n\nclass Notification(object):\n\n    def __init__(self, config, debug, silent):\n        self.config = config['notification']\n        self.debug = debug\n        self.silent = silent\n        self.log = logger.Logger(\"Notification\", debug, silent)\n\n    def import_from(self, module, name, config):\n        module = __import__(module, fromlist=[name])\n        class_ = getattr(module, name)\n        return class_(config, self.debug, self.silent)\n\n    def _send(self, status):\n        for method in self.config['methods']:\n            if bool(self.config['methods'][method]['enable']):\n                try:\n                    method_class = self.import_from('classes.{}'.format(\n                        method), method.capitalize(), self.config['methods'][method])\n                    method_class.send_notification(status)\n                    del method_class\n                except ImportError:\n                    self.log.error(\n                        \"Error loading notification class: {}\".format(method))\n\n    def rip_complete(self, dbvideo):\n\n        status = 'Rip of %s complete' % dbvideo.vidname\n        self._send(status)\n\n    def rip_fail(self, dbvideo):\n\n        status = 'Rip of %s failed' % dbvideo.vidname\n        self._send(status)\n\n    def compress_complete(self, dbvideo):\n\n        status = 'Compress of %s complete' % dbvideo.vidname\n        self._send(status)\n\n    def compress_fail(self, dbvideo):\n\n        status = 'Compress of %s failed' % dbvideo.vidname\n        self._send(status)\n\n    def extra_complete(self, dbvideo):\n\n        status = 'Extra of %s complete' % dbvideo.vidname\n        self._send(status)\n"
  },
  {
    "path": "classes/pushover.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nPushover Class\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\nimport logger\nfrom chump import Application\n\n\nclass Pushover(object):\n\n    def __init__(self, config, debug, silent):\n        self.log = logger.Logger(\"Pushover\", debug, silent)\n        self.config = config\n\n    def send_notification(self, notification_message):\n        app = Application(self.config['app_key'])\n        user = app.get_user(self.config['user_key'])\n        message = user.send_message(notification_message)\n\n        if message.is_sent:\n            self.log.info(\"Pushover message sent successfully\")\n        else:\n            self.log.error(\"Pushover message not sent\")\n"
  },
  {
    "path": "classes/smtp.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nSMTP Class\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jacob Carrigan\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jacob Carrigan\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport smtplib\nfrom email.MIMEMultipart import MIMEMultipart\nfrom email.MIMEText import MIMEText\n\nimport logger\n\n\nclass Smtp(object):\n\n    def __init__(self, config, debug, silent):\n        self.server = config['smtp_server']\n        self.username = config['smtp_username']\n        self.password = config['smtp_password']\n        self.port = config['smtp_port']\n        self.to_address = config['destination_email']\n        self.from_address = config['source_email']\n        self.log = logger.Logger(\"SMTP\", debug, silent)\n\n    def send_notification(self, notification_message):\n\n        if self.from_address == 'username@gmail.com':\n            self.logging.error(\n                'Email address has not been set correctly, ignoring send request from: {}'.format(self.from_address))\n            return\n\n        msg = MIMEMultipart()\n        msg['From'] = self.from_address\n        msg['To'] = self.to_address\n        msg['Subject'] = \"Autorippr\"\n\n        msg.attach(MIMEText(notification_message, 'plain'))\n\n        server = smtplib.SMTP(self.server, self.port)\n        server.starttls()\n        server.login(self.from_address, self.password)\n\n        text = msg.as_string()\n        server.sendmail(self.from_address, self.to_address, text)\n        server.quit()\n"
  },
  {
    "path": "classes/stopwatch.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nSimple StopWatch\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport datetime\n\n\nclass StopWatch(object):\n\n    def __enter__(self):\n        self.startTime = datetime.datetime.now()\n        return self\n\n    def __exit__(self, *args):\n        endtime = datetime.datetime.now()\n        totaltime = endtime - self.startTime\n        self.minutes = totaltime.seconds / 60\n"
  },
  {
    "path": "classes/testing.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nConfiguration and requirements testing\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport os\nimport subprocess\nimport sys\n\n\ndef perform_testing(config):\n\n    requirements = {\n        \"MakeMKV\": \"makemkvcon\",\n        \"Filebot\": \"filebot\",\n        \"HandBrake\": \"HandBrakeCLI\",\n        \"FFmpeg (optional)\": \"ffmpeg\"\n    }\n\n    print \"= Checking directory permissions\"\n    print canwrite(config['makemkv']['savePath']), \"MakeMKV savePath\"\n\n    print \"\"\n    print \"= Checking requirements\"\n    for req in requirements:\n        print checkcommand(requirements[req]), req\n\n    sys.exit(0)\n\n\ndef canwrite(path):\n    try:\n        ret = booltostatus(os.access(path, os.W_OK | os.X_OK))\n    except:\n        ret = False\n    finally:\n        return ret\n\n\ndef booltostatus(inbool):\n    if inbool:\n        return \"[  OK  ]\"\n    else:\n        return \"[ FAIL ]\"\n\n\ndef checkcommand(com):\n    proc = subprocess.Popen(\n        [\n            'which',\n            str(com)\n        ],\n        stderr=subprocess.PIPE,\n        stdout=subprocess.PIPE\n    )\n    return booltostatus(len(proc.stdout.read()) > 0)\n"
  },
  {
    "path": "classes/utils.py",
    "content": "\"\"\"\nHandBrake CLI Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;\n@author     Jason Millward\n@license    http://opensource.org/licenses/MIT\n\"\"\"\n\nimport re\nimport unicodedata\n\n\ndef strip_accents(s):\n    \"\"\"\n        Remove accents from an input string\n\n        Inputs:\n            s       (Str): A string to remove accents from:\n\n        Outputs:\n            Str     a string without accents\n    \"\"\"\n    s = s.decode('utf-8')\n    return ''.join(c for c in unicodedata.normalize('NFD', s)\n                  if unicodedata.category(c) != 'Mn')\n\n\ndef clean_special_chars(s):\n    \"\"\"\n        Remove any special chars from a string.\n\n        Inputs:\n            s       (Str): A string to remove special chars from:\n\n        Outputs:\n            Str     a string without special chars\n    \"\"\"\n    s = s.replace('\\'', '_')\n    s = s.replace('\"', '_')\n    return re.sub('\\W+\\.',' ', s)\n"
  },
  {
    "path": "settings.example.cfg",
    "content": "makemkv:\n    # Path to makemkvcon (with trailing slash) in case it is unavailable in your $PATH\n    makemkvconPath: \"\"\n\n    # This is where the ripped movies go\n    savePath:   /tmp/\n\n    # Minimum length of the main title (Mostly just a precaution)\n    minLength:  4000\n\n    # Maximum length of the title (For TV Series)\n    maxLength:  7200\n\n    # MakeMKV Cache size in MB, default 1GB is fine for most circumstances\n    cache:      1024\n\n    # Eject the disk\n    eject:      True\n\n    # Ignore region warnings\n    ignore_region: True\n\ncompress:\n    # Path to compression app (with trailing slash) in case it is unavailable in your $PATH\n    compressionPath: \"\"\n\n    # File format of compressed video (mkv, mp4, avi)\n    format:     \"mkv\"\n\n    # The compression application to use.\n    #   handbrake: Compress the video using Handbrake\n    #   ffmpeg:    Compress the video using FFmpeg\n    type:       handbrake\n\n    # The scheduling priority of the HandBrake program\n    #   -20 is the highest (The task gets top priority)\n    #    19 is the lowest  (The task get no priority and runs on spare CPU cycles)\n    nice:       15\n\n    # The HandBrake command line options and arguments\n    # Configure this to change output quality\n    # each line should start with -\n    com:\n        -   --x264-preset=\"medium\"\n        -   --two-pass\n        -   --turbo\n        -   -q 20\n        -   --markers\n        -   --x264-tune=\"film\"\n        -   --encoder=\"x264\"\n        -   -s 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20\n\n    # Example FFmpeg command line\n    #com:\n    #    -   -map 0\n    #    -   -c copy\n    #    -   -c:v libx264\n    #    -   -crf 20\n    #    -   -preset medium\n\nfilebot:\n    # Enable filebot renaming\n    enable:     True\n\n    # Download Subtitles?\n    subtitles:  True\n\n    # Language of subtitles if enabled\n    language:   en\n\n    # Move to folder after renaming\n    move:       False\n\n    # Movie Folder\n    moviePath:  /tmp/movies\n\n    # TV Folder\n    tvPath:     /tmp/tvshows\n\nanalytics:\n    enable:     True\n\ncommands:\n    # A list of commands to run after filebot completes\n    # each line should start with -\n    # eg:\n    # - mythutil --scanvideos\n\nnotification:\n    # Enable\n    enable:     True\n\n    # Notify on these events\n    notify_on_state:         rip, compress, extra\n\n    methods:\n        smtp:\n            # Enable email notification\n            enable:             False\n\n            # Outgoing Mail Server (smtp.live.com, smtp.mail.yahoo.com)\n            smtp_server:        smtp.gmail.com\n\n            # Outgoing Mail Port (Hotmail 587, Yahoo 995)\n            smtp_port:          587\n\n            # Email Username\n            smtp_username:      username@gmail.com\n\n            # Email Username's Password\n            smtp_password:      my_password\n\n            # Source email, usually smtp_username\n            source_email:       username@gmail.com\n\n            # Destination Email\n            destination_email:  to_address@gmail.com\n\n        pushover:\n            # https://pushover.net/\n\n            # Enable pushover notifications\n            enable:             False\n\n            user_key:\n\n            app_key:\n\nForcedSubs:\n    # Enable foreign subtitle detection and flagging\n    enable:          True\n\n    # Path to mediainfo in case it is not in $PATH\n    mediainfoPath:   \"\"\n\n    # Path to mkvpropedit in case it is not in $PATH\n    mkvpropeditPath: \"\"\n\n    # Langauge of main subtitle\n    language:        en\n\n    # Ratio of secondary subtitle file size to main subtitle size. ie, says will look for subtitle tracks <= 10% of main track.\n    ratio:           .1\n"
  }
]