Full Code of JasonMillward/Autorippr for AI

master 226ce079e8cf cached
30 files
90.5 KB
22.3k tokens
151 symbols
1 requests
Download .txt
Repository: JasonMillward/Autorippr
Branch: master
Commit: 226ce079e8cf
Files: 30
Total size: 90.5 KB

Directory structure:
gitextract_auirmn1c/

├── .gitignore
├── .pre-commit-config.yaml
├── CONTRIBUTING.md
├── CONTRIBUTORS.md
├── Dockerfile
├── LICENSE
├── NOTES.md
├── README.md
├── autorippr.py
├── autorippr_install_script.sh
├── build-docker.sh
├── build_makemkv/
│   └── Dockerfile
├── classes/
│   ├── __init__.py
│   ├── analytics.py
│   ├── compression.py
│   ├── database.py
│   ├── docopt.py
│   ├── ffmpeg.py
│   ├── filebot.py
│   ├── handbrake.py
│   ├── logger.py
│   ├── makemkv.py
│   ├── mediainfo.py
│   ├── notification.py
│   ├── pushover.py
│   ├── smtp.py
│   ├── stopwatch.py
│   ├── testing.py
│   └── utils.py
└── settings.example.cfg

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.py[co]
test.py

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox

#Translations
*.mo

#Mr Developer
.mr.developer.cfg

# Settings
settings.cfg

# SQLite database
*.sqlite

# Log files
*.log


.idea


================================================
FILE: .pre-commit-config.yaml
================================================
-   repo: git://github.com/pre-commit/pre-commit-hooks
    sha: master
    hooks:
    -   id: trailing-whitespace
    -   id: autopep8-wrapper
    -   id: check-yaml
        files: \.(yaml|yml)$
    -   id: fix-encoding-pragma
    -   id: flake8
        args:
          - --ignore=E501,F405,F403,F812

- repo: git://github.com/FalconSocial/pre-commit-python-sorter
  sha: master
  hooks:
  - id: python-import-sorter
    args:
    - --silent-overwrite



================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Autorippr

So you'd like to contribue to Autorippr? Fantastic! I love pull requests and welcome any sensible additions to this project.


### Guideline Contents

There are many ways to contribute, this document covers;


* [Raising issues](#issues)
* [Working on Autorippr](#autorippr)


<a name="raising-issues"></a>
## Raising an issue

If 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.

Tag the issue as a [bug] or [feature request] to help keep things neat.

But before you go and press that submit button take a look at the information you're giving, is it enough?

Keys points to include are:

* General computer info
    * Operating system
        * Name and version
    * General hardware inside (If related to compression). eg; Dual core intel & 2 dvd drives

* Python
    * Version of python

* Autorippr
    * Version of Autorippr

* Detailed description of the issue and debug logs if possible.
    * If the issue is with Autorippr, add `--debug` to the command line options for more information.

Providing this information upfront can assist with diagnosing any issues.


<a name="autorippr"></a>
## Working on Autorippr

Working with the code should be straight forward. Try to keep the style consistant.








Push to your fork and [submit a pull request][pr].

[pr]: https://github.com/JasonMillward/Autorippr/compare/



================================================
FILE: CONTRIBUTORS.md
================================================
# Author

* JasonMillward https://github.com/JasonMillward

# Contributors

* carrigan98 https://github.com/carrigan98
* RandomNinjaAtk https://github.com/RandomNinjaAtk
* Michael Dyrynda https://github.com/michaeldyrynda
* Ian Bird https://github.com/IanDBird
* Will Eddins https://github.com/ascendedguard
* Stefan Budeanu https://github.com/stefanmb
* Fabien Reboia https://github.com/srounet


================================================
FILE: Dockerfile
================================================
FROM ubuntu:16.04

COPY /build/* /

RUN 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 
#libavcodec-ffmpeg-extra56


ADD https://github.com/JasonMillward/Autorippr/archive/v1.7.0.zip autorippr-1.7.0.zip
ADD "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
RUN pip install tendo pyyaml peewee
RUN unzip /autorippr-1.7.0.zip
RUN dpkg -i filebot_4.7.2_amd64.deb

ADD settings.example.cfg /Autorippr-1.7.0/settings.cfg

ENTRYPOINT ["python", "/Autorippr-1.7.0/autorippr.py"]



================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014 Jason Millward

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: NOTES.md
================================================
# Misc notes for future me / developers

## TV Shows (and maybe proper movie titles)

Disc info can be obtained, it returns a lot more information about each disc instead of just the 'is disc inserted'
```makemkvcon -r info disc:[index]```

## Strings returned:

### Drive scan messages

	DRV:index,visible,enabled,flags,drive name,disc name
	index - drive index
	visible - set to 1 if drive is present
	enabled - set to 1 if drive is accessible
	flags - media flags, see AP_DskFsFlagXXX in apdefs.h
	drive name - drive name string
	disc name - disc name string

### Disc information output messages

	TCOUT:count
	count - titles count

### Disc, title and stream information

	CINFO:id,code,value
	TINFO:id,code,value
	SINFO:id,code,value

	id - attribute id, see AP_ItemAttributeId in apdefs.h
	code - message code if attribute value is a constant string
	value - attribute value


#### Example

Title count

	TCOUNT:7

C = Disc info

	CINFO:1,6209,"Blu-ray disc"
	CINFO:2,0,"Breaking Bad: Season 1: Disc 1"
	CINFO:28,0,"eng"
	CINFO:29,0,"English"
	CINFO:30,0,"Breaking Bad: Season 1: Disc 1"
	CINFO:31,6119,"<b>Source information</b><br>"
	CINFO:32,0,"BREAKINGBADS1"
	CINFO:33,0,"0"

T = Title info

	TINFO:0,2,0,"Breaking Bad: Season 1: Disc 1"
	TINFO:0,8,0,"7"
	TINFO:0,9,0,"0:58:06"
	TINFO:0,10,0,"12.5 GB"
	TINFO:0,11,0,"13472686080"
	TINFO:0,16,0,"00763.mpls"
	TINFO:0,25,0,"1"
	TINFO:0,26,0,"262"
	TINFO:0,27,0,"Breaking_Bad_Season_1_Disc_1_t00.mkv"
	TINFO:0,28,0,"eng"
	TINFO:0,29,0,"English"
	TINFO:0,30,0,"Breaking Bad: Season 1: Disc 1 - 7 chapter(s) , 12.5 GB"
	TINFO:0,31,6120,"<b>Title information</b><br>"
	TINFO:0,33,0,"0"


	2 - Disc Title
	9 - Length of chapters
	10 - File size
	28 - audio short code
	29 - audio long code

## Perfered language

```CINFO``` provides title language, config this

	CINFO:28,0,"eng"
	CINFO:29,0,"English"

## Subtitles

	2014-07-21 22:29:18 - Filebot - DEBUG - Get [English] subtitles for 1 files
	2014-07-21 22:29:18 - Filebot - DEBUG - Looking up subtitles by hash via OpenSubtitles
	2014-07-21 22:29:18 - Filebot - DEBUG - Looking up subtitles by name via OpenSubtitles
	2014-07-21 22:29:18 - Filebot - DEBUG - Fetching [Smokin'.Aces.2006.720p.BRRiP.XViD.AC3-LEGi0N.srt]
	2014-07-21 22:29:18 - Filebot - DEBUG - Export [Smokin'.Aces.2006.720p.BRRiP.XViD.AC3-LEGi0N.srt] as: SubRip / UTF-8
	2014-07-21 22:29:18 - Filebot - DEBUG - Writing [Smokin'.Aces.2006.720p.BRRiP.XViD.AC3-LEGi0N.srt] to [Smokin' Aces (2006).eng.srt]


================================================
FILE: README.md
================================================
Autorippr
=========

Autorippr 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. 

That said it should still work for most cases.


## Copyright & License

Copyright (c) 2012 Jason Millward - Released under the [MIT license](LICENSE).


================================================
FILE: autorippr.py
================================================
# -*- coding: utf-8 -*-
"""
Autorippr

Ripping
    Uses MakeMKV to watch for videos inserted into DVD/BD Drives

    Automaticly checks for existing directory/video and will NOT overwrite existing
    files or folders

    Checks minimum length of video to ensure video is ripped not previews or other
    junk that happens to be on the DVD

    DVD goes in > MakeMKV gets a proper DVD name > MakeMKV Rips

Compressing
    An optional additional used to rename and compress videos to an acceptable standard
    which still delivers quality audio and video but reduces the file size
    dramatically.

    Using a nice value of 15 by default, it runs HandBrake (or FFmpeg) as a background task
    that allows other critical tasks to complete first.

Extras


Released under the MIT license
Copyright (c) 2014, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT

Usage:
    autorippr.py   ( --rip | --compress | --extra )  [options]
    autorippr.py   ( --rip [ --compress ] )          [options]
    autorippr.py   --all                             [options]
    autorippr.py   --test

Options:
    -h --help           Show this screen.
    --version           Show version.
    --debug             Output debug.
    --rip               Rip disc using makeMKV.
    --compress          Compress using HandBrake or FFmpeg.
    --extra             Lookup, rename and/or download extras.
    --all               Do everything.
    --test              Tests config and requirements.
    --silent            Silent mode.
    --skip-compress     Skip the compression step.
    --force_db=(tv|movie)     Force use of the TheTVDB or TheMovieDB

"""

import errno
import os
import subprocess
import sys

import yaml
from classes import *
from tendo import singleton

__version__ = "1.7.0"

me = singleton.SingleInstance()
CONFIG_FILE = "{}/settings.cfg".format(
    os.path.dirname(os.path.abspath(__file__)))

notify = None


def eject(config, drive):
    """
        Ejects the DVD drive
        Not really worth its own class
    """
    log = logger.Logger("Eject", config['debug'], config['silent'])

    log.debug("Ejecting drive: " + drive)
    log.debug("Attempting OS detection")

    try:
        if sys.platform == 'win32':
            log.debug("OS detected as Windows")
            import ctypes
            ctypes.windll.winmm.mciSendStringW(
                "set cdaudio door open", None, drive, None)

        elif sys.platform == 'darwin':
            log.debug("OS detected as OSX")
            p = os.popen("drutil eject " + drive)

            while 1:
                line = p.readline()
                if not line:
                    break
                log.debug(line.strip())

        else:
            log.debug("OS detected as Unix")
            p = os.popen("eject -vm " + drive)

            while 1:
                line = p.readline()
                if not line:
                    break
                log.debug(line.strip())

    except Exception as ex:
        log.error("Could not detect OS or eject CD tray")
        log.ex("An exception of type {} occured.".format(type(ex).__name__))
        log.ex("Args: \r\n {}".format(ex.args))

    finally:
        del log


def rip(config):
    """
        Main function for ripping
        Does everything
        Returns nothing
    """
    log = logger.Logger("Rip", config['debug'], config['silent'])

    mkv_save_path = config['makemkv']['savePath']

    log.debug("Ripping initialised")
    mkv_api = makemkv.MakeMKV(config)

    log.debug("Checking for DVDs")
    dvds = mkv_api.find_disc()

    log.debug("{} DVD(s) found".format(len(dvds)))

    if len(dvds) > 0:
        # Best naming convention ever
        for dvd in dvds:
            mkv_api.set_title(dvd["discTitle"])
            mkv_api.set_index(dvd["discIndex"])

            disc_title = mkv_api.get_title()
            
            if not config['force_db']:
                disc_type = mkv_api.get_type()
            else:
                disc_type = config['force_db']

            disc_path = os.path.join(mkv_save_path, disc_title)
            if not os.path.exists(disc_path):
                os.makedirs(disc_path)

                mkv_api.get_disc_info()

                saveFiles = mkv_api.get_savefiles()

                if len(saveFiles) != 0:
                    filebot = config['filebot']['enable']

                    for dvdTitle in saveFiles:

                        dbvideo = database.insert_video(
                            disc_title,
                            disc_path,
                            disc_type,
                            dvdTitle['index'],
                            filebot
                        )

                        database.insert_history(
                            dbvideo,
                            "Video added to database"
                        )

                        database.update_video(
                            dbvideo,
                            3,
                            dvdTitle['title']
                        )

                        log.debug("Attempting to rip {} from {}".format(
                            dvdTitle['title'],
                            disc_title
                        ))

                        with stopwatch.StopWatch() as t:
                            database.insert_history(
                                dbvideo,
                                "Video submitted to MakeMKV"
                            )
                            status = mkv_api.rip_disc(
                                mkv_save_path, dvdTitle['index'])

                            # Master_and_Commander_De_l'autre_côté_du_monde_t00.mkv become
                            # Master_and_Commander_De_l_autre_cote_du_monde_t00.mkv
                            log.debug('Rename {} to {}'.format(
                                os.path.join(dbvideo.path, dvdTitle['title']),
                                os.path.join(dbvideo.path, dvdTitle['rename_title'])
                            ))
                            os.rename(
                                os.path.join(dbvideo.path, dvdTitle['title']),
                                os.path.join(dbvideo.path, dvdTitle['rename_title'])
                            )

                        if status:
                            log.info("It took {} minute(s) to complete the ripping of {} from {}".format(
                                t.minutes,
                                dvdTitle['title'],
                                disc_title
                            ))

                            database.update_video(dbvideo, 4)

                            if 'rip' in config['notification']['notify_on_state']:
                                notify.rip_complete(dbvideo)

                        else:
                            database.update_video(dbvideo, 2)

                            database.insert_history(
                                dbvideo,
                                "MakeMKV failed to rip video"
                            )
                            notify.rip_fail(dbvideo)

                            log.info(
                                "MakeMKV did not did not complete successfully")
                            log.info("See log for more details")

                    if config['makemkv']['eject']:
                        eject(config, dvd['location'])

                else:
                    log.info("No video titles found")
                    log.info(
                        "Try decreasing 'minLength' in the config and try again")

            else:
                log.info("Video folder {} already exists".format(disc_title))

    else:
        log.info("Could not find any DVDs in drive list")


def skip_compress(config):
    """
        Main function for skipping compression
        Does everything
        Returns nothing
    """
    log = logger.Logger("Skip compress", config['debug'], config['silent'])

    log.debug("Looking for videos to skip compression")

    dbvideos = database.next_video_to_compress()
    comp = compression.Compression(config)

    for dbvideo in dbvideos:
        if comp.check_exists(dbvideo) is not False:
            database.update_video(dbvideo, 6)
            log.info("Skipping compression for {} from {}" .format(
                dbvideo.filename, dbvideo.vidname))


def compress(config):
    """
        Main function for compressing
        Does everything
        Returns nothing
    """
    log = logger.Logger("Compress", config['debug'], config['silent'])

    comp = compression.Compression(config)

    log.debug("Compressing initialised")
    log.debug("Looking for videos to compress")

    dbvideos = database.next_video_to_compress()

    for dbvideo in dbvideos:
        dbvideo.filename = utils.strip_accents(dbvideo.filename)
        dbvideo.filename = utils.clean_special_chars(dbvideo.filename)

        if comp.check_exists(dbvideo) is not False:

            database.update_video(dbvideo, 5)

            log.info("Compressing {} from {}" .format(
                dbvideo.filename, dbvideo.vidname))

            with stopwatch.StopWatch() as t:
                status = comp.compress(
                    args=config['compress']['com'],
                    nice=int(config['compress']['nice']),
                    dbvideo=dbvideo
                )

            if status:
                log.info("Video was compressed and encoded successfully")

                log.info("It took {} minutes to compress {}".format(
                    t.minutes, dbvideo.filename
                )
                )

                database.insert_history(
                    dbvideo,
                    "Compression Completed successfully"
                )

                database.update_video(dbvideo, 6)

                if 'compress' in config['notification']['notify_on_state']:
                    notify.compress_complete(dbvideo)

                comp.cleanup()

            else:
                database.update_video(dbvideo, 5)

                database.insert_history(dbvideo, "Compression failed", 4)

                notify.compress_fail(dbvideo)

                log.info("Compression did not complete successfully")
        else:
            database.update_video(dbvideo, 2)

            database.insert_history(
                dbvideo, "Input file no longer exists", 4
            )

    else:
        log.info("Queue does not exist or is empty")


def extras(config):
    """
        Main function for filebotting and flagging forced subs
        Does everything
        Returns nothing
    """
    log = logger.Logger("Extras", config['debug'], config['silent'])

    fb = filebot.FileBot(config['debug'], config['silent'])

    dbvideos = database.next_video_to_filebot()

    for dbvideo in dbvideos:
        if config['ForcedSubs']['enable']:
            forced = mediainfo.ForcedSubs(config)
            log.info("Attempting to discover foreign subtitle for {}.".format(dbvideo.vidname))
            track = forced.discover_forcedsubs(dbvideo)

            if track is not None:
                log.info("Found foreign subtitle for {}: track {}".format(dbvideo.vidname, track))
                log.debug("Attempting to flag track for {}: track {}".format(dbvideo.vidname, track))
                flagged = forced.flag_forced(dbvideo, track)
                if flagged:
                    log.info("Flagging success.")
                else:
                    log.debug("Flag failed")
            else:
                log.debug("Did not find foreign subtitle for {}.".format(dbvideo.vidname))
                
        log.info("Attempting video rename")

        database.update_video(dbvideo, 7)

        movePath = dbvideo.path
        if config['filebot']['move']:
            if dbvideo.vidtype == "tv":
                movePath = config['filebot']['tvPath']
            else:
                movePath = config['filebot']['moviePath']

        status = fb.rename(dbvideo, movePath)

        if status[0]:
            log.info("Rename success")

            database.update_video(dbvideo, 6)

            if config['filebot']['subtitles']:
                log.info("Grabbing subtitles")

                status = fb.get_subtitles(
                    dbvideo, config['filebot']['language'])

                if status:
                    log.info("Subtitles downloaded")
                    database.update_video(dbvideo, 8)

                else:
                    log.info("Subtitles not downloaded, no match")
                    database.update_video(dbvideo, 8)

                log.info("Completed work on {}".format(dbvideo.vidname))

                if config['commands'] is not None and len(config['commands']) > 0:
                    for com in config['commands']:
                        subprocess.Popen(
                            [com],
                            stderr=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            shell=True
                        )

            else:
                log.info("Not grabbing subtitles")
                database.update_video(dbvideo, 8)



            if 'extra' in config['notification']['notify_on_state']:
                notify.extra_complete(dbvideo)

            log.debug("Attempting to delete %s" % dbvideo.path)

            try:
                os.rmdir(dbvideo.path)
            except OSError as ex:
                if ex.errno == errno.ENOTEMPTY:
                    log.debug("Directory not empty")

        else:
            log.info("Rename failed")

    else:
        log.info("No videos ready for filebot")


if __name__ == '__main__':
    arguments = docopt.docopt(__doc__, version=__version__)
    config = yaml.safe_load(open(CONFIG_FILE))

    config['debug'] = arguments['--debug']

    config['silent'] = arguments['--silent']
    
    if arguments['--force_db'] not in ['tv','movie', None]:
        raise ValueError('{} is not a valid DB.'.format(arguments['--force_db']))
    else:
        config['force_db'] = arguments['--force_db']
        
    notify = notification.Notification(
        config, config['debug'], config['silent'])

    if bool(config['analytics']['enable']):
        analytics.ping(__version__)

    if arguments['--test']:
        testing.perform_testing(config)

    if arguments['--rip'] or arguments['--all']:
        rip(config)

    if (arguments['--compress'] or arguments['--all']) and not arguments['--skip-compress']:
        compress(config)

    if arguments['--skip-compress']:
        skip_compress(config)

    if arguments['--extra'] or arguments['--all']:
        extras(config)


================================================
FILE: autorippr_install_script.sh
================================================
#!/bin/bash

# Install Script Version 1.0
# This script is designed to install Autorippr for Ubuntu 16.04 LTS
# All required dependancies and packages will be installed
# Packages that are installed:
# --GIT
# --Makemkv
# --Python Dev Tools
# --Python Dependancies for Autorippr
# --PIP
# --Handbrake-CLI
# --Filebot
# --Autorippr

# Change to execution directory
cd ~

# Ubuntu 16.04 Error fix for installing packages
sudo apt-get purge runit
sudo apt-get purge git-all
sudo apt-get purge git
sudo apt-get autoremove
sudo apt update

# Install Git
sudo apt install git 

#Install PIP
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

#Intall MakeMKV required tools and libraries
sudo apt-get install build-essential pkg-config libc6-dev libssl-dev libexpat1-dev libavcodec-dev libgl1-mesa-dev libqt4-dev

#Install MakeMKV
wget http://www.makemkv.com/download/makemkv-bin-1.10.2.tar.gz
wget http://www.makemkv.com/download/makemkv-oss-1.10.2.tar.gz
tar -zxmf makemkv-oss-1.10.2.tar.gz
tar -zxmf makemkv-bin-1.10.2.tar.gz
cd makemkv-oss-1.10.2
./configure
make
sudo make install
cd ..
cd makemkv-bin-1.10.2
make
sudo make install

# Install Handbrake CLI
sudo apt-get install handbrake-cli

# Python update to enable next step
sudo apt-get install python-dev

# Install Java prerequisite for Filebot
sudo add-apt-repository -y ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer

# Install Filebot
if [ `uname -m` = "i686" ]
then
   wget -O filebot-i386.deb 'http://filebot.sourceforge.net/download.php?type=deb&arch=i386'
else
   wget -O filebot-amd64.deb 'http://filebot.sourceforge.net/download.php?type=deb&arch=amd64'
fi
sudo dpkg --force-depends -i filebot-*.deb && rm filebot-*.deb

# Install Python Required Packages
sudo pip install tendo pyyaml peewee pushover

# Install Autorippr
cd ~
git clone https://github.com/JasonMillward/Autorippr.git
cd Autorippr
git checkout
cp settings.example.cfg settings.cfg

# Verification Test
python autorippr.py --test

# Completion Message
echo " "
echo "###################################################"
echo "##            Install Complete!                  ##"
echo "##      Update: ~/Autorippr/settings.cfg         ##"
echo "###################################################"


================================================
FILE: build-docker.sh
================================================
#!/bin/bash -e
#
# Createas a container with autorippr in 
#
# Run like
#
# docker run -ti -v /tmp:/tmp  --device=/dev/sr1 autorippr --all --debug
#


docker build -t buildmakemkv ./build_makemkv
docker run --rm buildmakemkv | tar xz
docker build -t autorippr .


================================================
FILE: build_makemkv/Dockerfile
================================================
FROM ubuntu:16.04

RUN apt-get update && apt-get install -y python-virtualenv build-essential pkg-config libc6-dev libssl-dev libexpat1-dev libavcodec-dev libgl1-mesa-dev 

ADD http://www.makemkv.com/download/makemkv-oss-1.12.3.tar.gz /src/
ADD http://www.makemkv.com/download/makemkv-bin-1.12.3.tar.gz /src/

RUN tar xf /src/makemkv-bin-1.12.3.tar.gz -C /src


RUN mkdir /src/makemkv-bin-1.12.3/tmp/
RUN echo 'accepted' > /src/makemkv-bin-1.12.3/tmp/eula_accepted

RUN  sed -ie 's#DESTDIR=#DESTDIR=/build#g' /src/makemkv-bin-1.12.3/Makefile

RUN cd /src/makemkv-bin-1.12.3 && make install

RUN tar xf /src/makemkv-oss-1.12.3.tar.gz -C /src

RUN cd /src/makemkv-oss-1.12.3 && ./configure --prefix /build/usr --disable-gui && make install

CMD ["tar", "cz", "/build"]


================================================
FILE: classes/__init__.py
================================================
# -*- coding: utf-8 -*-
__all__ = [
    'analytics',
    'compression',
    'database',
    'docopt',
    'ffmpeg',
    'filebot',
    'handbrake',
    'logger',
    'makemkv',
    'mediainfo',
    'notification',
    'stopwatch',
    'testing',
    'utils'
]


================================================
FILE: classes/analytics.py
================================================
# -*- coding: utf-8 -*-
"""
Basic analytics

The purpose of this file is to simply send a 'ping' with a unique identifier
and the script version once a day to give an indication of unique users

This has been added because Github doesn't show a download counter, I have no
way of knowing if this script is even being used (except for people telling me
it broke).

You are free to opt-out by disabling the config option

    analytics:
        enable:     True <- make this False

If the computer doesn't have internet access the script will continue as normal


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""


def ping(version):
    """
        Send a simple ping to my server
            to see how many people are using this script
    """
    try:
        import uuid
        import requests
        import json
        import os
        import time

        data = {
            "uuid": uuid.getnode(),
            "version": version
        }

        datefile = "/tmp/%s" % time.strftime("%Y%m%d")

        if not os.path.isfile(datefile):

            with open(datefile, 'w'):
                os.utime(datefile, None)

            requests.post(
                'http://api.jcode.me/autorippr/stats',
                data=json.dumps(data),
                timeout=5
            )

    except:
        pass


================================================
FILE: classes/compression.py
================================================
# -*- coding: utf-8 -*-
"""
Compression Wrapper


Released under the MIT license
Copyright (c) 2014, Ian Bird, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@authors    Ian Bird, Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import os

import ffmpeg
import handbrake
import logger


class Compression(object):

    def __init__(self, config):
        """
            Creates the required compression instances

            Inputs:
                config    (??): The configuration

            Outputs:
                The compression instance
        """
        self.log = logger.Logger("Compression", config['debug'], config['silent'])
        self.method = self.which_method(config)
        self.invid = ""

    def which_method(self, config):
        if config['compress']['type'] == "ffmpeg":
            return ffmpeg.FFmpeg(
                config['debug'],
                config['compress']['compressionPath'],
                config['silent'],
                config['compress']['format']
            )
        else:
            return handbrake.HandBrake(
                config['debug'],
                config['compress']['compressionPath'],
                config['compress']['format'],
                config['silent']
            )

    def compress(self, **args):
        self.log.debug('FFmpeg args: {}'.format(args))
        return self.method.compress(**args)

    def check_exists(self, dbvideo):
        """
            Checks to see if the file still exists at the path set in the
                database

            Inputs:
                dbvideo (Obj): Video database object

            Outputs:
                Bool    Does file exist

        """
        self.invid = "%s/%s" % (dbvideo.path, dbvideo.filename)

        if os.path.isfile(self.invid):
            return True

        else:
            self.log.debug(self.invid)
            self.log.error("Input file no longer exists, abording")
            return False

    def cleanup(self):
        """
            Deletes files once the compression has finished with them

            Inputs:
                cFile    (Str): File path of the video to remove

            Outputs:
                None
        """
        if self.invid is not "":
            try:
                os.remove(self.invid)
            except:
                self.log.error("Could not remove %s" % self.invid)


================================================
FILE: classes/database.py
================================================
# -*- coding: utf-8 -*-
"""
SQLite Database Helper


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import os
from datetime import datetime

from peewee import *
import utils

DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
database = SqliteDatabase('%s/autorippr.sqlite' % DIR, **{})


class BaseModel(Model):

    class Meta:
        database = database


class History(BaseModel):
    historyid = PrimaryKeyField(db_column='historyID')
    historydate = DateTimeField(db_column='historyDate')
    historytext = CharField(db_column='historyText')
    historytypeid = IntegerField(db_column='historyTypeID')
    vidid = IntegerField(db_column='vidID')

    class Meta:
        db_table = 'history'


class Historytypes(BaseModel):
    historytypeid = PrimaryKeyField(db_column='historyTypeID')
    historytype = CharField(db_column='historyType')

    class Meta:
        db_table = 'historyTypes'


class Videos(BaseModel):
    vidid = PrimaryKeyField(db_column='vidID')
    vidname = CharField()
    vidtype = CharField()
    titleindex = CharField(db_column='titleIndex')
    path = CharField()
    filename = CharField(null=True)
    filebot = BooleanField()
    statusid = IntegerField(db_column='statusID')
    lastupdated = DateTimeField(db_column='lastUpdated')

    class Meta:
        db_table = 'videos'


class Statustypes(BaseModel):
    statusid = PrimaryKeyField(db_column='statusID')
    statustext = CharField(db_column='statusText')

    class Meta:
        db_table = 'statusTypes'


def create_tables():
    database.connect()

    # Fail silently if tables exists
    History.create_table(True)
    Historytypes.create_table(True)
    Videos.create_table(True)
    Statustypes.create_table(True)


def create_history_types():
    historytypes = [
        [1, 'Info'],
        [2, 'Error'],
        [3, 'MakeMKV Error'],
        [4, 'Handbrake Error']
    ]

    c = 0
    for z in Historytypes.select():
        c += 1

    if c != len(historytypes):
        for hID, hType in historytypes:
            Historytypes.create(historytypeid=hID, historytype=hType)


def create_status_types():
    statustypes = [
        [1, 'Added'],
        [2, 'Error'],
        [3, 'Submitted to makeMKV'],
        [4, 'Awaiting HandBrake'],
        [5, 'Submitted to HandBrake'],
        [6, 'Awaiting FileBot'],
        [7, 'Submitted to FileBot'],
        [8, 'Completed']
    ]

    c = 0
    for z in Statustypes.select():
        c += 1

    if c != len(statustypes):
        for sID, sType in statustypes:
            Statustypes.create(statusid=sID, statustext=sType)


def next_video_to_compress():
    videos = Videos.select().where((Videos.statusid == 4) & (
        Videos.filename != "None")).order_by(Videos.vidname)
    return videos


def next_video_to_filebot():
    videos = Videos.select().where((Videos.statusid == 6) & (
        Videos.filename != "None") & (Videos.filebot == 1))
    return videos


def search_video_name(invid):
    vidqty = Videos.select().where(Videos.filename.startswith(invid)).count()
    return vidqty


def insert_history(dbvideo, text, typeid=1):
    return History.create(
        vidid=dbvideo.vidid,
        historytext=text,
        historydate=datetime.now(),
        historytypeid=typeid
    )


def insert_video(title, path, vidtype, index, filebot):
    return Videos.create(
        vidname=title,
        vidtype=vidtype,
        titleindex=index,
        path=path,
        filename="None",
        filebot=filebot,
        statusid=1,
        lastupdated=datetime.now()
    )


def update_video(vidobj, statusid, filename=None):
    vidobj.statusid = statusid
    vidobj.lastupdated = datetime.now()

    if filename is not None:
        filename = utils.strip_accents(filename)
        filename = utils.clean_special_chars(filename)
        vidobj.filename = filename

    vidobj.save()


def db_integrity_check():
    # Stuff
    create_tables()

    # Things
    create_history_types()
    create_status_types()

db_integrity_check()


================================================
FILE: classes/docopt.py
================================================
# -*- coding: utf-8 -*-
"""Pythonic command-line interface parser that will make you smile.

 * http://docopt.org
 * Repository and issue-tracker: https://github.com/docopt/docopt
 * Licensed under terms of MIT license (see LICENSE-MIT)
 * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com

"""
import re
import sys

__all__ = ['docopt']
__version__ = '0.6.1'


class DocoptLanguageError(Exception):

    """Error in construction of usage-message by developer."""


class DocoptExit(SystemExit):

    """Exit in case user invoked program with incorrect arguments."""

    usage = ''

    def __init__(self, message=''):
        SystemExit.__init__(self, (message + '\n' + self.usage).strip())


class Pattern(object):

    def __eq__(self, other):
        return repr(self) == repr(other)

    def __hash__(self):
        return hash(repr(self))

    def fix(self):
        self.fix_identities()
        self.fix_repeating_arguments()
        return self

    def fix_identities(self, uniq=None):
        """Make pattern-tree tips point to same object if they are equal."""
        if not hasattr(self, 'children'):
            return self
        uniq = list(set(self.flat())) if uniq is None else uniq
        for i, child in enumerate(self.children):
            if not hasattr(child, 'children'):
                assert child in uniq
                self.children[i] = uniq[uniq.index(child)]
            else:
                child.fix_identities(uniq)

    def fix_repeating_arguments(self):
        """Fix elements that should accumulate/increment values."""
        either = [list(child.children) for child in transform(self).children]
        for case in either:
            for e in [child for child in case if case.count(child) > 1]:
                if type(e) is Argument or type(e) is Option and e.argcount:
                    if e.value is None:
                        e.value = []
                    elif type(e.value) is not list:
                        e.value = e.value.split()
                if type(e) is Command or type(e) is Option and e.argcount == 0:
                    e.value = 0
        return self


def transform(pattern):
    """Expand pattern into an (almost) equivalent one, but with single Either.

    Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
    Quirks: [-a] => (-a), (-a...) => (-a -a)

    """
    result = []
    groups = [[pattern]]
    while groups:
        children = groups.pop(0)
        parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]
        if any(t in map(type, children) for t in parents):
            child = [c for c in children if type(c) in parents][0]
            children.remove(child)
            if type(child) is Either:
                for c in child.children:
                    groups.append([c] + children)
            elif type(child) is OneOrMore:
                groups.append(child.children * 2 + children)
            else:
                groups.append(child.children + children)
        else:
            result.append(children)
    return Either(*[Required(*e) for e in result])


class LeafPattern(Pattern):

    """Leaf/terminal node of a pattern tree."""

    def __init__(self, name, value=None):
        self.name, self.value = name, value

    def __repr__(self):
        return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)

    def flat(self, *types):
        return [self] if not types or type(self) in types else []

    def match(self, left, collected=None):
        collected = [] if collected is None else collected
        pos, match = self.single_match(left)
        if match is None:
            return False, left, collected
        left_ = left[:pos] + left[pos + 1:]
        same_name = [a for a in collected if a.name == self.name]
        if type(self.value) in (int, list):
            if type(self.value) is int:
                increment = 1
            else:
                increment = ([match.value] if type(match.value) is str
                             else match.value)
            if not same_name:
                match.value = increment
                return True, left_, collected + [match]
            same_name[0].value += increment
            return True, left_, collected
        return True, left_, collected + [match]


class BranchPattern(Pattern):

    """Branch/inner node of a pattern tree."""

    def __init__(self, *children):
        self.children = list(children)

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__,
                           ', '.join(repr(a) for a in self.children))

    def flat(self, *types):
        if type(self) in types:
            return [self]
        return sum([child.flat(*types) for child in self.children], [])


class Argument(LeafPattern):

    def single_match(self, left):
        for n, pattern in enumerate(left):
            if type(pattern) is Argument:
                return n, Argument(self.name, pattern.value)
        return None, None

    @classmethod
    def parse(class_, source):
        name = re.findall('(<\S*?>)', source)[0]
        value = re.findall('\[default: (.*)\]', source, flags=re.I)
        return class_(name, value[0] if value else None)


class Command(Argument):

    def __init__(self, name, value=False):
        self.name, self.value = name, value

    def single_match(self, left):
        for n, pattern in enumerate(left):
            if type(pattern) is Argument:
                if pattern.value == self.name:
                    return n, Command(self.name, True)
                else:
                    break
        return None, None


class Option(LeafPattern):

    def __init__(self, short=None, long=None, argcount=0, value=False):
        assert argcount in (0, 1)
        self.short, self.long, self.argcount = short, long, argcount
        self.value = None if value is False and argcount else value

    @classmethod
    def parse(class_, option_description):
        short, long, argcount, value = None, None, 0, False
        options, _, description = option_description.strip().partition('  ')
        options = options.replace(',', ' ').replace('=', ' ')
        for s in options.split():
            if s.startswith('--'):
                long = s
            elif s.startswith('-'):
                short = s
            else:
                argcount = 1
        if argcount:
            matched = re.findall('\[default: (.*)\]', description, flags=re.I)
            value = matched[0] if matched else None
        return class_(short, long, argcount, value)

    def single_match(self, left):
        for n, pattern in enumerate(left):
            if self.name == pattern.name:
                return n, pattern
        return None, None

    @property
    def name(self):
        return self.long or self.short

    def __repr__(self):
        return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
                                           self.argcount, self.value)


class Required(BranchPattern):

    def match(self, left, collected=None):
        collected = [] if collected is None else collected
        l = left
        c = collected
        for pattern in self.children:
            matched, l, c = pattern.match(l, c)
            if not matched:
                return False, left, collected
        return True, l, c


class Optional(BranchPattern):

    def match(self, left, collected=None):
        collected = [] if collected is None else collected
        for pattern in self.children:
            m, left, collected = pattern.match(left, collected)
        return True, left, collected


class OptionsShortcut(Optional):

    """Marker/placeholder for [options] shortcut."""


class OneOrMore(BranchPattern):

    def match(self, left, collected=None):
        assert len(self.children) == 1
        collected = [] if collected is None else collected
        l = left
        c = collected
        l_ = None
        matched = True
        times = 0
        while matched:
            # could it be that something didn't match but changed l or c?
            matched, l, c = self.children[0].match(l, c)
            times += 1 if matched else 0
            if l_ == l:
                break
            l_ = l
        if times >= 1:
            return True, l, c
        return False, left, collected


class Either(BranchPattern):

    def match(self, left, collected=None):
        collected = [] if collected is None else collected
        outcomes = []
        for pattern in self.children:
            matched, _, _ = outcome = pattern.match(left, collected)
            if matched:
                outcomes.append(outcome)
        if outcomes:
            return min(outcomes, key=lambda outcome: len(outcome[1]))
        return False, left, collected


class Tokens(list):

    def __init__(self, source, error=DocoptExit):
        self += source.split() if hasattr(source, 'split') else source
        self.error = error

    @staticmethod
    def from_pattern(source):
        source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
        source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s]
        return Tokens(source, error=DocoptLanguageError)

    def move(self):
        return self.pop(0) if len(self) else None

    def current(self):
        return self[0] if len(self) else None


def parse_long(tokens, options):
    """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
    long, eq, value = tokens.move().partition('=')
    assert long.startswith('--')
    value = None if eq == value == '' else value
    similar = [o for o in options if o.long == long]
    if tokens.error is DocoptExit and similar == []:  # if no exact match
        similar = [o for o in options if o.long and o.long.startswith(long)]
    if len(similar) > 1:  # might be simply specified ambiguously 2+ times?
        raise tokens.error('%s is not a unique prefix: %s?' %
                           (long, ', '.join(o.long for o in similar)))
    elif len(similar) < 1:
        argcount = 1 if eq == '=' else 0
        o = Option(None, long, argcount)
        options.append(o)
        if tokens.error is DocoptExit:
            o = Option(None, long, argcount, value if argcount else True)
    else:
        o = Option(similar[0].short, similar[0].long,
                   similar[0].argcount, similar[0].value)
        if o.argcount == 0:
            if value is not None:
                raise tokens.error('%s must not have an argument' % o.long)
        else:
            if value is None:
                if tokens.current() in [None, '--']:
                    raise tokens.error('%s requires argument' % o.long)
                value = tokens.move()
        if tokens.error is DocoptExit:
            o.value = value if value is not None else True
    return [o]


def parse_shorts(tokens, options):
    """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
    token = tokens.move()
    assert token.startswith('-') and not token.startswith('--')
    left = token.lstrip('-')
    parsed = []
    while left != '':
        short, left = '-' + left[0], left[1:]
        similar = [o for o in options if o.short == short]
        if len(similar) > 1:
            raise tokens.error('%s is specified ambiguously %d times' %
                               (short, len(similar)))
        elif len(similar) < 1:
            o = Option(short, None, 0)
            options.append(o)
            if tokens.error is DocoptExit:
                o = Option(short, None, 0, True)
        else:  # why copying is necessary here?
            o = Option(short, similar[0].long,
                       similar[0].argcount, similar[0].value)
            value = None
            if o.argcount != 0:
                if left == '':
                    if tokens.current() in [None, '--']:
                        raise tokens.error('%s requires argument' % short)
                    value = tokens.move()
                else:
                    value = left
                    left = ''
            if tokens.error is DocoptExit:
                o.value = value if value is not None else True
        parsed.append(o)
    return parsed


def parse_pattern(source, options):
    tokens = Tokens.from_pattern(source)
    result = parse_expr(tokens, options)
    if tokens.current() is not None:
        raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
    return Required(*result)


def parse_expr(tokens, options):
    """expr ::= seq ( '|' seq )* ;"""
    seq = parse_seq(tokens, options)
    if tokens.current() != '|':
        return seq
    result = [Required(*seq)] if len(seq) > 1 else seq
    while tokens.current() == '|':
        tokens.move()
        seq = parse_seq(tokens, options)
        result += [Required(*seq)] if len(seq) > 1 else seq
    return [Either(*result)] if len(result) > 1 else result


def parse_seq(tokens, options):
    """seq ::= ( atom [ '...' ] )* ;"""
    result = []
    while tokens.current() not in [None, ']', ')', '|']:
        atom = parse_atom(tokens, options)
        if tokens.current() == '...':
            atom = [OneOrMore(*atom)]
            tokens.move()
        result += atom
    return result


def parse_atom(tokens, options):
    """atom ::= '(' expr ')' | '[' expr ']' | 'options'
             | long | shorts | argument | command ;
    """
    token = tokens.current()
    result = []
    if token in '([':
        tokens.move()
        matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
        result = pattern(*parse_expr(tokens, options))
        if tokens.move() != matching:
            raise tokens.error("unmatched '%s'" % token)
        return [result]
    elif token == 'options':
        tokens.move()
        return [OptionsShortcut()]
    elif token.startswith('--') and token != '--':
        return parse_long(tokens, options)
    elif token.startswith('-') and token not in ('-', '--'):
        return parse_shorts(tokens, options)
    elif token.startswith('<') and token.endswith('>') or token.isupper():
        return [Argument(tokens.move())]
    else:
        return [Command(tokens.move())]


def parse_argv(tokens, options, options_first=False):
    """Parse command-line argument vector.

    If options_first:
        argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
    else:
        argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;

    """
    parsed = []
    while tokens.current() is not None:
        if tokens.current() == '--':
            return parsed + [Argument(None, v) for v in tokens]
        elif tokens.current().startswith('--'):
            parsed += parse_long(tokens, options)
        elif tokens.current().startswith('-') and tokens.current() != '-':
            parsed += parse_shorts(tokens, options)
        elif options_first:
            return parsed + [Argument(None, v) for v in tokens]
        else:
            parsed.append(Argument(None, tokens.move()))
    return parsed


def parse_defaults(doc):
    defaults = []
    for s in parse_section('options:', doc):
        # FIXME corner case "bla: options: --foo"
        _, _, s = s.partition(':')  # get rid of "options:"
        split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:]
        split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
        options = [Option.parse(s) for s in split if s.startswith('-')]
        defaults += options
    return defaults


def parse_section(name, source):
    pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
                         re.IGNORECASE | re.MULTILINE)
    return [s.strip() for s in pattern.findall(source)]


def formal_usage(section):
    _, _, section = section.partition(':')  # drop "usage:"
    pu = section.split()
    return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'


def extras(help, version, options, doc):
    if help and any((o.name in ('-h', '--help')) and o.value for o in options):
        print(doc.strip("\n"))
        sys.exit()
    if version and any(o.name == '--version' and o.value for o in options):
        print(version)
        sys.exit()


class Dict(dict):

    def __repr__(self):
        return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))


def docopt(doc, argv=None, help=True, version=None, options_first=False):
    """Parse `argv` based on command-line interface described in `doc`.

    `docopt` creates your command-line interface based on its
    description that you pass as `doc`. Such description can contain
    --options, <positional-argument>, commands, which could be
    [optional], (required), (mutually | exclusive) or repeated...

    Parameters
    ----------
    doc : str
        Description of your command-line interface.
    argv : list of str, optional
        Argument vector to be parsed. sys.argv[1:] is used if not
        provided.
    help : bool (default: True)
        Set to False to disable automatic help on -h or --help
        options.
    version : any object
        If passed, the object will be printed if --version is in
        `argv`.
    options_first : bool (default: False)
        Set to True to require options precede positional arguments,
        i.e. to forbid options and positional arguments intermix.

    Returns
    -------
    args : dict
        A dictionary, where keys are names of command-line elements
        such as e.g. "--verbose" and "<path>", and values are the
        parsed values of those elements.

    Example
    -------
    >>> from docopt import docopt
    >>> doc = '''
    ... Usage:
    ...     my_program tcp <host> <port> [--timeout=<seconds>]
    ...     my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
    ...     my_program (-h | --help | --version)
    ...
    ... Options:
    ...     -h, --help  Show this screen and exit.
    ...     --baud=<n>  Baudrate [default: 9600]
    ... '''
    >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
    >>> docopt(doc, argv)
    {'--baud': '9600',
     '--help': False,
     '--timeout': '30',
     '--version': False,
     '<host>': '127.0.0.1',
     '<port>': '80',
     'serial': False,
     'tcp': True}

    See also
    --------
    * For video introduction see http://docopt.org
    * Full documentation is available in README.rst as well as online
      at https://github.com/docopt/docopt#readme

    """
    argv = sys.argv[1:] if argv is None else argv

    usage_sections = parse_section('usage:', doc)
    if len(usage_sections) == 0:
        raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
    if len(usage_sections) > 1:
        raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
    DocoptExit.usage = usage_sections[0]

    options = parse_defaults(doc)
    pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
    # [default] syntax for argument is disabled
    # for a in pattern.flat(Argument):
    #    same_name = [d for d in arguments if d.name == a.name]
    #    if same_name:
    #        a.value = same_name[0].value
    argv = parse_argv(Tokens(argv), list(options), options_first)
    pattern_options = set(pattern.flat(Option))
    for options_shortcut in pattern.flat(OptionsShortcut):
        doc_options = parse_defaults(doc)
        options_shortcut.children = list(set(doc_options) - pattern_options)
        # if any_options:
        #    options_shortcut.children += [Option(o.short, o.long, o.argcount)
        #                    for o in argv if type(o) is Option]
    extras(help, version, argv, doc)
    matched, left, collected = pattern.fix().match(argv)
    if matched and left == []:  # better error message if left?
        return Dict((a.name, a.value) for a in (pattern.flat() + collected))
    raise DocoptExit()


================================================
FILE: classes/ffmpeg.py
================================================
# -*- coding: utf-8 -*-
"""
FFmpeg Wrapper


Released under the MIT license
Copyright (c) 2014, Ian Bird

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Ian Bird
@license    http://opensource.org/licenses/MIT
"""
import os
import re
import subprocess

import database
import logger


class FFmpeg(object):

    def __init__(self, debug, compressionpath, silent, vformat):
        self.log = logger.Logger("FFmpeg", debug, silent)
        self.compressionPath = compressionpath
        self.vformat = vformat

    def compress(self, nice, args, dbvideo):
        """
            Passes the necessary parameters to FFmpeg to start an encoding
            Assigns a nice value to allow give normal system tasks priority


            Inputs:
                nice    (Int): Priority to assign to task (nice value)
                args    (Str): All of the FFmpeg arguments taken from the
                                settings file
                output  (Str): File to log to. Used to see if the job completed
                                successfully

            Outputs:
                Bool    Was convertion successful
        """

        if (dbvideo.vidtype == "tv"):
            # Query the SQLite database for similar titles (TV Shows)
            vidname = re.sub(r'D(\d)', '', dbvideo.vidname)
            vidqty = database.search_video_name(vidname)
            if vidqty == 0:
                vidname = "%sE1.%s" % (vidname, self.vformat)
            else:
                vidname = "%sE%s.%s" % (vidname, str(vidqty + 1), self.vformat)
        else:
            vidname = "%s.%s" % (dbvideo.vidname, self.vformat)

        invid = "%s/%s" % (dbvideo.path, dbvideo.filename)
        outvid = os.path.join(self.compressionPath, os.path.basename(dbvideo.path), vidname)
        destination_folder = os.path.dirname(outvid)

        if not os.path.exists(destination_folder):
            self.log.info('Destination folder does not exists, creating: {}'.format(
                destination_folder
            ))
            os.makedirs(destination_folder)

        command = 'nice -n {0} ffmpeg -i "{1}" {2} "{3}"'.format(
            nice,
            invid,
            ' '.join(args),
            outvid
        )

        proc = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            shell=True
        )
        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "FFmpeg (compress) returned status code: %d" % proc.returncode)
            return False

        return True


================================================
FILE: classes/filebot.py
================================================
#*- coding: utf-8 -*-
"""
FileBot class


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import re
import subprocess
import logger


class FileBot(object):

    def __init__(self, debug, silent):
        self.log = logger.Logger("Filebot", debug, silent)

    def rename(self, dbvideo, movePath):
        """
            Renames video file upon successful database lookup

            Inputs:
                dbvideo (Obj): Video database object

            Outputs:
                Bool    Was lookup successful
        """

        if dbvideo.vidtype == "tv":
            db = "TheTVDB"
        else:
            db = "TheMovieDB"

        vidname = re.sub(r'S(\d)', '', dbvideo.vidname)
        vidname = re.sub(r'D(\d)', '', vidname)


        proc = subprocess.Popen(
            [
                'filebot',
                '-rename',
                "%s/%s" % (dbvideo.path, dbvideo.filename),
                '--q',
                "\"%s\"" % vidname,
                '-non-strict',
                '--db',
                '%s' % db,
                '--output',
                "%s" % movePath
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )

        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "Filebot (rename) returned status code: %d" % proc.returncode)

        renamedvideo = ""
        checks = 0

        if len(results) is not 0:
            lines = results.split("\n")
            self.log.debug(results.split("\n"))
            for line in lines:
                if line:
                    self.log.debug(line.strip())
                if "MOVE" in line:
                    renamedvideo = line.split("] to [", 1)[1].rstrip(']')
                    checks += 1

                if "Processed" in line:
                    checks += 1

                if "Done" in line:
                    checks += 1

        if checks >= 3 and renamedvideo:
            return [True, renamedvideo]
        else:
            return [False]

    def get_subtitles(self, dbvideo, lang):
        """
            Downloads subtitles of specified language

            Inputs:
                dbvideo (Obj): Video database object
                lang    (Str): Language of subtitles to download

            Outputs:
                Bool    Was download successful
        """
        proc = subprocess.Popen(
            [
                'filebot',
                '-get-subtitles',
                dbvideo.path,
                '--q',
                "\"%s\"" % dbvideo.vidname,
                '--lang',
                lang,
                '--output',
                'srt',
                '--encoding',
                'utf8',
                '-non-strict'
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )

        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "Filebot (get_subtitles) returned status code: %d" % proc.returncode)

        checks = 0

        if len(results) is not 0:
            lines = results.split("\n")
            for line in lines:
                self.log.debug(line.strip())

                if "Processed" in line:
                    checks += 1

                if "Done" in line:
                    checks += 1

        if checks >= 2:
            return True
        else:
            return False


================================================
FILE: classes/handbrake.py
================================================
# -*- coding: utf-8 -*-
"""
HandBrake CLI Wrapper


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import re
import subprocess
import database
import logger


class HandBrake(object):

    def __init__(self, debug, compressionpath, vformat, silent):
        self.log = logger.Logger("HandBrake", debug, silent)
        self.compressionPath = compressionpath
        self.vformat = vformat

    def compress(self, nice, args, dbvideo):
        """
            Passes the necessary parameters to HandBrake to start an encoding
            Assigns a nice value to allow give normal system tasks priority

            Inputs:
                nice    (Int): Priority to assign to task (nice value)
                args    (Str): All of the handbrake arguments taken from the
                                settings file
                output  (Str): File to log to. Used to see if the job completed
                                successfully

            Outputs:
                Bool    Was convertion successful
        """
        checks = 0

        if (dbvideo.vidtype == "tv"):
            # Query the SQLite database for similar titles (TV Shows)
            vidname = re.sub(r'D(\d)', '', dbvideo.vidname)
            vidqty = database.search_video_name(vidname)
            if vidqty == 0:
                vidname = "%sE1.%s" % (vidname, self.vformat)
            else:
                vidname = "%sE%s.%s" % (vidname, str(vidqty + 1), self.vformat)
        else:
            vidname = "%s.%s" % (dbvideo.vidname, self.vformat)

        invid = "%s/%s" % (dbvideo.path, dbvideo.filename)
        outvid = "%s/%s" % (dbvideo.path, vidname)
        command = 'nice -n {0} {1}HandBrakeCLI --verbose -i "{2}" -o "{3}" {4}'.format(
            nice,
            self.compressionPath,
            invid,
            outvid,
            ' '.join(args)
        )

        proc = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            shell=True
        )
        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "HandBrakeCLI (compress) returned status code: %d" % proc.returncode)

        if results is not None and len(results) is not 0:
            lines = results.split("\n")
            for line in lines:
                if "Encoding: task" not in line:
                    self.log.debug(line.strip())

                if "average encoding speed for job" in line:
                    checks += 1

                if "Encode done!" in line:
                    checks += 1

                if "ERROR" in line and "opening" not in line and "udfread" not in line:
                    self.log.error(
                        "HandBrakeCLI encountered the following error: ")
                    self.log.error(line)

                    return False

        if checks >= 2:
            self.log.debug("HandBrakeCLI Completed successfully")

            database.update_video(
                dbvideo, 6, filename="%s" % (
                    vidname
                ))

            return True
        else:
            return False


================================================
FILE: classes/logger.py
================================================
# -*- coding: utf-8 -*-
"""
Simple logging class


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import logging
import os
import sys


class Logger(object):

    def __init__(self, name, debug, silent):
        self.silent = silent

        frmt = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            "%Y-%m-%d %H:%M:%S"
        )

        if debug:
            loglevel = logging.DEBUG
        else:
            loglevel = logging.INFO

        self.createhandlers(frmt, name, loglevel)

    def __del__(self):
        if not self.silent:
            self.log.removeHandler(self.sh)
        self.log.removeHandler(self.fh)
        self.log = None

    def createhandlers(self, frmt, name, loglevel):
        self.log = logging.getLogger(name)
        self.log.setLevel(loglevel)

        if not self.silent:
            self.sh = logging.StreamHandler(sys.stdout)
            self.sh.setLevel(loglevel)
            self.sh.setFormatter(frmt)
            self.log.addHandler(self.sh)

        DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        self.fh = logging.FileHandler('%s/autorippr.log' % DIR)
        self.fh.setLevel(loglevel)
        self.fh.setFormatter(frmt)
        self.log.addHandler(self.fh)

    def debug(self, msg):
        self.log.debug(msg)

    def info(self, msg):
        self.log.info(msg)

    def warn(self, msg):
        self.log.warn(msg)

    def error(self, msg):
        self.log.error(msg)

    def critical(self, msg):
        self.log.critical(msg)


================================================
FILE: classes/makemkv.py
================================================
# -*- coding: utf-8 -*-
"""
MakeMKV CLI Wrapper


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import csv
import datetime
import re
import subprocess
import time

import logger

import utils


class MakeMKV(object):

    def __init__(self, config):
        self.discIndex = 0
        self.vidName = ""
        self.path = ""
        self.vidType = ""
        self.minLength = int(config['makemkv']['minLength'])
        self.maxLength = int(config['makemkv']['maxLength'])
        self.cacheSize = int(config['makemkv']['cache'])
        self.ignore_region = bool(config['makemkv']['ignore_region'])
        self.log = logger.Logger("Makemkv", config['debug'], config['silent'])
        self.makemkvconPath = config['makemkv']['makemkvconPath']
        self.saveFiles = []

    def _clean_title(self):
        """
            Removes the extra bits in the title and removes whitespace

            Inputs:
                None

            Outputs:
                None
        """
        tmpname = self.vidName
        tmpname = tmpname.title().replace("Extended_Edition", "")
        tmpname = tmpname.replace("Special_Edition", "")
        tmpname = re.sub(r"Disc_(\d)(.*)", r"D\1", tmpname)
        tmpname = re.sub(r"Disc\s*(\d)(.*)", r"D\1", tmpname)
        tmpname = re.sub(r"Season_(\d)", r"S\1", tmpname)
        tmpname = re.sub(r"Season(\d)", r"S\1", tmpname)
        tmpname = re.sub(r"S(\d)_", r"S\1", tmpname)
        tmpname = tmpname.replace("_t00", "")
        tmpname = tmpname.replace("\"", "").replace("_", " ")

        # Clean up the edges and remove whitespace
        self.vidName = tmpname.strip()

    def _remove_duplicates(self, title_list):
        seen_titles = set()
        new_list = []
        for obj in title_list:
            if obj['title'] not in seen_titles:
                new_list.append(obj)
                seen_titles.add(obj['title'])

        return new_list

    def _read_mkv_messages(self, stype, sid=None, scode=None):
        """
            Returns a list of messages that match the search string
            Parses message output.

            Inputs:
                stype   (Str): Type of message
                sid     (Int): ID of message
                scode   (Int): Code of message

            Outputs:
                toreturn    (List)
        """
        toreturn = []

        with open('/tmp/makemkvMessages', 'r') as messages:
            for line in messages:
                if line[:len(stype)] == stype:
                    values = line.replace("%s:" % stype, "").strip()

                    cr = csv.reader([values])

                    if sid is not None:
                        for row in cr:
                            if int(row[0]) == int(sid):
                                if scode is not None:
                                    if int(row[1]) == int(scode):
                                        toreturn.append(row[3])
                                else:
                                    toreturn.append(row[2])

                    else:
                        for row in cr:
                            toreturn.append(row[0])

        return toreturn

    def set_title(self, vidname):
        """
            Sets local video name

            Inputs:
                vidName   (Str): Name of video

            Outputs:
                None
        """
        self.vidName = vidname

    def set_index(self, index):
        """
            Sets local disc index

            Inputs:
                index   (Int): Disc index

            Outputs:
                None
        """
        self.discIndex = int(index)

    def rip_disc(self, path, titleIndex):
        """
            Passes in all of the arguments to makemkvcon to start the ripping
                of the currently inserted DVD or BD

            Inputs:
                path    (Str):  Where the video will be saved to
                output  (Str):  Temp file to save output to

            Outputs:
                Success (Bool)
        """
        self.path = path

        fullpath = '%s/%s' % (self.path, self.vidName)

        proc = subprocess.Popen(
            [
                '%smakemkvcon' % self.makemkvconPath,
                'mkv',
                'disc:%d' % self.discIndex,
                titleIndex,
                fullpath,
                '--cache=%d' % self.cacheSize,
                '--noscan',
                '--decrypt',
                '--minlength=%d' % self.minLength
            ],
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE
        )

        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "MakeMKV (rip_disc) returned status code: %d" % proc.returncode)

        if errors is not None:
            if len(errors) is not 0:
                self.log.error("MakeMKV encountered the following error: ")
                self.log.error(errors)
                return False

        checks = 0

        lines = results.split("\n")
        for line in lines:
            if "skipped" in line:
                continue

            badstrings = [
                "failed",
                "fail",
                "error"
            ]

            if any(x in line.lower() for x in badstrings):
                if self.ignore_region and "RPC protection" in line:
                    self.log.warn(line)
                elif "Failed to add angle" in line:
                    self.log.warn(line)
                else:
                    self.log.error(line)
                    return False

            if "Copy complete" in line:
                checks += 1

            if "titles saved" in line:
                checks += 1

        if checks >= 2:
            return True
        else:
            return False

    def find_disc(self):
        """
            Use makemkvcon to list all DVDs or BDs inserted
            If more then one disc is inserted, use the first result

            Inputs:
                output  (Str): Temp file to save output to

            Outputs:
                Success (Bool)
        """
        drives = []
        proc = subprocess.Popen(
            ['%smakemkvcon' % self.makemkvconPath, '-r', 'info', 'disc:-1'],
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE
        )

        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "MakeMKV (find_disc) returned status code: %d" % proc.returncode)

        if errors is not None:
            if len(errors) is not 0:
                self.log.error("MakeMKV encountered the following error: ")
                self.log.error(errors)
                return []

        if "This application version is too old." in results:
            self.log.error("Your MakeMKV version is too old."
                           "Please download the latest version at http://www.makemkv.com"
                           " or enter a registration key to continue using MakeMKV.")

            return []

        # Passed the simple tests, now check for disk drives
        lines = results.split("\n")
        for line in lines:
            if line[:4] == "DRV:":
                if "/dev/" in line:
                    out = line.split(',')

                    if len(str(out[5])) > 3:

                        drives.append(
                            {
                                "discIndex": out[0].replace("DRV:", ""),
                                "discTitle": out[5],
                                "location": out[6]
                            }
                        )

        return drives

    def get_disc_info(self):
        """
            Returns information about the selected disc

            Inputs:
                None

            Outputs:
                None
        """

        proc = subprocess.Popen(
            [
                '%smakemkvcon' % self.makemkvconPath,
                '-r',
                'info',
                'disc:%d' % self.discIndex,
                '--decrypt',
                '--minlength=%d' % self.minLength,
                '--messages=/tmp/makemkvMessages'
            ],
            stderr=subprocess.PIPE
        )

        (results, errors) = proc.communicate()

        if proc.returncode is not 0:
            self.log.error(
                "MakeMKV (get_disc_info) returned status code: %d" % proc.returncode)

        if errors is not None:
            if len(errors) is not 0:
                self.log.error("MakeMKV encountered the following error: ")
                self.log.error(errors)
                return False

        foundtitles = int(self._read_mkv_messages("TCOUNT")[0])

        self.log.debug("MakeMKV found {} titles".format(foundtitles))

        if foundtitles > 0:
            for titleNo in set(self._read_mkv_messages("TINFO")):
                durTemp = self._read_mkv_messages("TINFO", titleNo, 9)[0]
                x = time.strptime(durTemp, '%H:%M:%S')
                titleDur = datetime.timedelta(
                    hours=x.tm_hour,
                    minutes=x.tm_min,
                    seconds=x.tm_sec
                ).total_seconds()

                if self.vidType == "tv" and titleDur > self.maxLength:
                    self.log.debug("Excluding Title No.: {}, Title: {}. Exceeds maxLength".format(
                        titleNo,
                        self._read_mkv_messages("TINFO", titleNo, 27)
                    ))
                    continue

                if self.vidType == "movie" and not re.search('00', self._read_mkv_messages("TINFO", titleNo, 27)[0]):
                    self.log.debug("Excluding Title No.: {}, Title: {}. Only want first title".format(
                        titleNo,
                        self._read_mkv_messages("TINFO", titleNo, 27)
                    ))
                    continue

                self.log.debug("MakeMKV title info: Disc Title: {}, Title No.: {}, Title: {}, ".format(
                    self._read_mkv_messages("CINFO", 2),
                    titleNo,
                    self._read_mkv_messages("TINFO", titleNo, 27)
                ))

                title = self._read_mkv_messages("TINFO", titleNo, 27)[0]
                rename_title = utils.strip_accents(title)
                rename_title = utils.clean_special_chars(rename_title)

                self.saveFiles.append({
                    'index': titleNo,
                    'title': title,
                    'rename_title': rename_title,
                })

    def get_type(self):
        """
            Returns the type of video (tv/movie)

            Inputs:
                None

            Outputs:
                vidType   (Str)
        """
        titlePattern = re.compile(
            r'(DISC_(\d))|(DISC(\d))|(D(\d))|(SEASON_(\d))|(SEASON(\d))|(S(\d))'
        )

        if titlePattern.search(self.vidName):
            self.log.debug("Detected TV {}".format(self.vidName))
            self.vidType = "tv"
        else:
            self.log.debug("Detected movie {}".format(self.vidName))
            self.vidType = "movie"
        return self.vidType

    def get_title(self):
        """
            Returns the current videos title

            Inputs:
                None

            Outputs:
                vidName   (Str)
        """
        self._clean_title()
        return self.vidName

    def get_savefiles(self):
        """
            Returns the current videos title

            Inputs:
                None

            Outputs:
                vidName   (Str)
        """
        return self._remove_duplicates(self.saveFiles)


================================================
FILE: classes/mediainfo.py
================================================
# -*- coding: utf-8 -*-
"""
Created on Mon Jan 09 11:20:23 2017

Dependencies:
   System:
       mediainfo
       mkvtoolnix

   Python (nonstandard library):
       pymediainfo

For Windows, if mediainfo or mkvpropedit aren't in PATH, must give path to .dll (mediainfo)
or .exe (mkvpropedit) file
For *nixs, use path to binary (although it's likly in PATH)

Takes an mkv file and analyzes it for foreign subtitle track. Assumes that foreign subtitle
track files are smaller in bit size but the same length as the main language track


@author: brodi
"""

import os
from pymediainfo import MediaInfo
from pipes import quote
import logger
import shlex
import subprocess

# main class that initializes settings for discovering/flagging a forced subtitle track
# edits python's os.environ in favor of putting full string when calling executables
class ForcedSubs(object):
    def __init__(self, config):
        self.log = logger.Logger('ForcedSubs', config['debug'], config['silent'])
        self.lang = config['ForcedSubs']['language']
        self.secsub_ratio = float(config['ForcedSubs']['ratio'])
        self.mediainfoPath = config['ForcedSubs']['mediainfoPath']
        self.mkvpropeditPath = config['ForcedSubs']['mkvpropeditPath']
        if (self.mediainfoPath and
            os.path.dirname(self.mediainfoPath) not in os.environ['PATH']):
            os.environ['PATH'] = (os.path.dirname(config['ForcedSubs']['mediainfoPath']) + ';' +
                                  os.environ['PATH'])
        if (self.mkvpropeditPath and
            os.path.dirname(self.mkvpropeditPath) not in os.environ['PATH']):
            os.environ['PATH'] = (os.path.dirname(config['ForcedSubs']['mkvpropeditPath']) + ';' +
                                  os.environ['PATH'])

    def discover_forcedsubs(self, dbvideo):
        """
            Attempts to find foreign subtitle track

            Input:
                dbvideo (Obj): Video database object

            Output:
                If successful, track number of forced subtitle
                Else, None
        """
        MEDIADIR = os.path.join(dbvideo.path, dbvideo.filename)
#        wrapper class for mediainfo tool
        media_info = MediaInfo.parse(MEDIADIR.encode('unicode-escape'))
        subs = []
#       Iterates though tracks and finds subtitles in preferred language, creates
#       list of dictionaries
        for track in media_info.tracks:
            data = track.to_data()
            if data['track_type'] == 'Text' and data['language']==self.lang:
                subs.append(data)
        if len(subs) is 0:
            self.log.info("No subtitle found, cannot determine foreign language track.")
            return None
        if len(subs) is 1:
            self.log.info("Only one {} subtitle found, cannot determine foreign language track."
                          .format(self.lang))
            return None

#   Sort list by size of track file
        subs.sort(key=lambda sub: sub['stream_size'], reverse = True)

#   Main language subtitle assumed to be largest
        main_sub = subs[0]
        main_subsize = main_sub['stream_size']
        main_sublen = float(main_sub['duration'])
#   Checks other subs for size, duration, and if forced flag is set
        for sub in subs[1:]:
            if (
                sub['stream_size'] <= main_subsize*self.secsub_ratio
                and main_sublen*.9 <= float(sub['duration']) <= main_sublen*1.1
                and sub['forced']=='No'
                ):
                secondary_sub = sub
            else:
                self.log.info("No foreign language subtitle found, try adjusting ratio.")
                return None
        return secondary_sub['track_id']

    def flag_forced(self, dbvideo, track):
        """
            Uses mkvpropedit to edit mkv header and flag the detected track as 'forced'

            Input:
                dbvideo (Obj): Video database object
                track (int): Track number of foreign track to be flagged as 'forced'

            Output:
                Bool: Returns True of successful, returns False if not
        """

        MEDIADIR = os.path.join(dbvideo.path, dbvideo.filename)
        cmd_raw = 'mkvpropedit {} --edit track:{} --set flag-forced=1'.format(quote(MEDIADIR), track)
        cmd = shlex.split(cmd_raw)
        self.log.debug("mkpropedit cmd: {}".format(cmd))

        proc = subprocess.Popen(
                                cmd,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE
                                )
        
        (results, error) = proc.communicate()


        if proc.returncode is not 0:
            self.log.error(
                           "mkvpropedit (forced subtitles) returned status code {}".format(proc.returncode)
                           )
            return False

        if len(results) is not 0:
            lines = results.split('\n')
            for line in lines:
                self.log.debug(line.strip())

        return True



================================================
FILE: classes/notification.py
================================================
# -*- coding: utf-8 -*-
"""
Notification Class


Released under the MIT license
Copyright (c) 2014, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import logger


class Notification(object):

    def __init__(self, config, debug, silent):
        self.config = config['notification']
        self.debug = debug
        self.silent = silent
        self.log = logger.Logger("Notification", debug, silent)

    def import_from(self, module, name, config):
        module = __import__(module, fromlist=[name])
        class_ = getattr(module, name)
        return class_(config, self.debug, self.silent)

    def _send(self, status):
        for method in self.config['methods']:
            if bool(self.config['methods'][method]['enable']):
                try:
                    method_class = self.import_from('classes.{}'.format(
                        method), method.capitalize(), self.config['methods'][method])
                    method_class.send_notification(status)
                    del method_class
                except ImportError:
                    self.log.error(
                        "Error loading notification class: {}".format(method))

    def rip_complete(self, dbvideo):

        status = 'Rip of %s complete' % dbvideo.vidname
        self._send(status)

    def rip_fail(self, dbvideo):

        status = 'Rip of %s failed' % dbvideo.vidname
        self._send(status)

    def compress_complete(self, dbvideo):

        status = 'Compress of %s complete' % dbvideo.vidname
        self._send(status)

    def compress_fail(self, dbvideo):

        status = 'Compress of %s failed' % dbvideo.vidname
        self._send(status)

    def extra_complete(self, dbvideo):

        status = 'Extra of %s complete' % dbvideo.vidname
        self._send(status)


================================================
FILE: classes/pushover.py
================================================
# -*- coding: utf-8 -*-
"""
Pushover Class


Released under the MIT license
Copyright (c) 2014, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""
import logger
from chump import Application


class Pushover(object):

    def __init__(self, config, debug, silent):
        self.log = logger.Logger("Pushover", debug, silent)
        self.config = config

    def send_notification(self, notification_message):
        app = Application(self.config['app_key'])
        user = app.get_user(self.config['user_key'])
        message = user.send_message(notification_message)

        if message.is_sent:
            self.log.info("Pushover message sent successfully")
        else:
            self.log.error("Pushover message not sent")


================================================
FILE: classes/smtp.py
================================================
# -*- coding: utf-8 -*-
"""
SMTP Class


Released under the MIT license
Copyright (c) 2014, Jacob Carrigan

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jacob Carrigan
@license    http://opensource.org/licenses/MIT
"""

import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText

import logger


class Smtp(object):

    def __init__(self, config, debug, silent):
        self.server = config['smtp_server']
        self.username = config['smtp_username']
        self.password = config['smtp_password']
        self.port = config['smtp_port']
        self.to_address = config['destination_email']
        self.from_address = config['source_email']
        self.log = logger.Logger("SMTP", debug, silent)

    def send_notification(self, notification_message):

        if self.from_address == 'username@gmail.com':
            self.logging.error(
                'Email address has not been set correctly, ignoring send request from: {}'.format(self.from_address))
            return

        msg = MIMEMultipart()
        msg['From'] = self.from_address
        msg['To'] = self.to_address
        msg['Subject'] = "Autorippr"

        msg.attach(MIMEText(notification_message, 'plain'))

        server = smtplib.SMTP(self.server, self.port)
        server.starttls()
        server.login(self.from_address, self.password)

        text = msg.as_string()
        server.sendmail(self.from_address, self.to_address, text)
        server.quit()


================================================
FILE: classes/stopwatch.py
================================================
# -*- coding: utf-8 -*-
"""
Simple StopWatch


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import datetime


class StopWatch(object):

    def __enter__(self):
        self.startTime = datetime.datetime.now()
        return self

    def __exit__(self, *args):
        endtime = datetime.datetime.now()
        totaltime = endtime - self.startTime
        self.minutes = totaltime.seconds / 60


================================================
FILE: classes/testing.py
================================================
# -*- coding: utf-8 -*-
"""
Configuration and requirements testing


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import os
import subprocess
import sys


def perform_testing(config):

    requirements = {
        "MakeMKV": "makemkvcon",
        "Filebot": "filebot",
        "HandBrake": "HandBrakeCLI",
        "FFmpeg (optional)": "ffmpeg"
    }

    print "= Checking directory permissions"
    print canwrite(config['makemkv']['savePath']), "MakeMKV savePath"

    print ""
    print "= Checking requirements"
    for req in requirements:
        print checkcommand(requirements[req]), req

    sys.exit(0)


def canwrite(path):
    try:
        ret = booltostatus(os.access(path, os.W_OK | os.X_OK))
    except:
        ret = False
    finally:
        return ret


def booltostatus(inbool):
    if inbool:
        return "[  OK  ]"
    else:
        return "[ FAIL ]"


def checkcommand(com):
    proc = subprocess.Popen(
        [
            'which',
            str(com)
        ],
        stderr=subprocess.PIPE,
        stdout=subprocess.PIPE
    )
    return booltostatus(len(proc.stdout.read()) > 0)


================================================
FILE: classes/utils.py
================================================
"""
HandBrake CLI Wrapper


Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.7.0, 2016-08-22 14:53:29 ACST $;
@author     Jason Millward
@license    http://opensource.org/licenses/MIT
"""

import re
import unicodedata


def strip_accents(s):
    """
        Remove accents from an input string

        Inputs:
            s       (Str): A string to remove accents from:

        Outputs:
            Str     a string without accents
    """
    s = s.decode('utf-8')
    return ''.join(c for c in unicodedata.normalize('NFD', s)
                  if unicodedata.category(c) != 'Mn')


def clean_special_chars(s):
    """
        Remove any special chars from a string.

        Inputs:
            s       (Str): A string to remove special chars from:

        Outputs:
            Str     a string without special chars
    """
    s = s.replace('\'', '_')
    s = s.replace('"', '_')
    return re.sub('\W+\.',' ', s)


================================================
FILE: settings.example.cfg
================================================
makemkv:
    # Path to makemkvcon (with trailing slash) in case it is unavailable in your $PATH
    makemkvconPath: ""

    # This is where the ripped movies go
    savePath:   /tmp/

    # Minimum length of the main title (Mostly just a precaution)
    minLength:  4000

    # Maximum length of the title (For TV Series)
    maxLength:  7200

    # MakeMKV Cache size in MB, default 1GB is fine for most circumstances
    cache:      1024

    # Eject the disk
    eject:      True

    # Ignore region warnings
    ignore_region: True

compress:
    # Path to compression app (with trailing slash) in case it is unavailable in your $PATH
    compressionPath: ""

    # File format of compressed video (mkv, mp4, avi)
    format:     "mkv"

    # The compression application to use.
    #   handbrake: Compress the video using Handbrake
    #   ffmpeg:    Compress the video using FFmpeg
    type:       handbrake

    # The scheduling priority of the HandBrake program
    #   -20 is the highest (The task gets top priority)
    #    19 is the lowest  (The task get no priority and runs on spare CPU cycles)
    nice:       15

    # The HandBrake command line options and arguments
    # Configure this to change output quality
    # each line should start with -
    com:
        -   --x264-preset="medium"
        -   --two-pass
        -   --turbo
        -   -q 20
        -   --markers
        -   --x264-tune="film"
        -   --encoder="x264"
        -   -s 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20

    # Example FFmpeg command line
    #com:
    #    -   -map 0
    #    -   -c copy
    #    -   -c:v libx264
    #    -   -crf 20
    #    -   -preset medium

filebot:
    # Enable filebot renaming
    enable:     True

    # Download Subtitles?
    subtitles:  True

    # Language of subtitles if enabled
    language:   en

    # Move to folder after renaming
    move:       False

    # Movie Folder
    moviePath:  /tmp/movies

    # TV Folder
    tvPath:     /tmp/tvshows

analytics:
    enable:     True

commands:
    # A list of commands to run after filebot completes
    # each line should start with -
    # eg:
    # - mythutil --scanvideos

notification:
    # Enable
    enable:     True

    # Notify on these events
    notify_on_state:         rip, compress, extra

    methods:
        smtp:
            # Enable email notification
            enable:             False

            # Outgoing Mail Server (smtp.live.com, smtp.mail.yahoo.com)
            smtp_server:        smtp.gmail.com

            # Outgoing Mail Port (Hotmail 587, Yahoo 995)
            smtp_port:          587

            # Email Username
            smtp_username:      username@gmail.com

            # Email Username's Password
            smtp_password:      my_password

            # Source email, usually smtp_username
            source_email:       username@gmail.com

            # Destination Email
            destination_email:  to_address@gmail.com

        pushover:
            # https://pushover.net/

            # Enable pushover notifications
            enable:             False

            user_key:

            app_key:

ForcedSubs:
    # Enable foreign subtitle detection and flagging
    enable:          True

    # Path to mediainfo in case it is not in $PATH
    mediainfoPath:   ""

    # Path to mkvpropedit in case it is not in $PATH
    mkvpropeditPath: ""

    # Langauge of main subtitle
    language:        en

    # Ratio of secondary subtitle file size to main subtitle size. ie, says will look for subtitle tracks <= 10% of main track.
    ratio:           .1
Download .txt
gitextract_auirmn1c/

├── .gitignore
├── .pre-commit-config.yaml
├── CONTRIBUTING.md
├── CONTRIBUTORS.md
├── Dockerfile
├── LICENSE
├── NOTES.md
├── README.md
├── autorippr.py
├── autorippr_install_script.sh
├── build-docker.sh
├── build_makemkv/
│   └── Dockerfile
├── classes/
│   ├── __init__.py
│   ├── analytics.py
│   ├── compression.py
│   ├── database.py
│   ├── docopt.py
│   ├── ffmpeg.py
│   ├── filebot.py
│   ├── handbrake.py
│   ├── logger.py
│   ├── makemkv.py
│   ├── mediainfo.py
│   ├── notification.py
│   ├── pushover.py
│   ├── smtp.py
│   ├── stopwatch.py
│   ├── testing.py
│   └── utils.py
└── settings.example.cfg
Download .txt
SYMBOL INDEX (151 symbols across 17 files)

FILE: autorippr.py
  function eject (line 74) | def eject(config, drive):
  function rip (line 120) | def rip(config):
  function skip_compress (line 247) | def skip_compress(config):
  function compress (line 267) | def compress(config):
  function extras (line 339) | def extras(config):

FILE: classes/analytics.py
  function ping (line 30) | def ping(version):

FILE: classes/compression.py
  class Compression (line 22) | class Compression(object):
    method __init__ (line 24) | def __init__(self, config):
    method which_method (line 38) | def which_method(self, config):
    method compress (line 54) | def compress(self, **args):
    method check_exists (line 58) | def check_exists(self, dbvideo):
    method cleanup (line 80) | def cleanup(self):

FILE: classes/database.py
  class BaseModel (line 25) | class BaseModel(Model):
    class Meta (line 27) | class Meta:
  class History (line 31) | class History(BaseModel):
    class Meta (line 38) | class Meta:
  class Historytypes (line 42) | class Historytypes(BaseModel):
    class Meta (line 46) | class Meta:
  class Videos (line 50) | class Videos(BaseModel):
    class Meta (line 61) | class Meta:
  class Statustypes (line 65) | class Statustypes(BaseModel):
    class Meta (line 69) | class Meta:
  function create_tables (line 73) | def create_tables():
  function create_history_types (line 83) | def create_history_types():
  function create_status_types (line 100) | def create_status_types():
  function next_video_to_compress (line 121) | def next_video_to_compress():
  function next_video_to_filebot (line 127) | def next_video_to_filebot():
  function search_video_name (line 133) | def search_video_name(invid):
  function insert_history (line 138) | def insert_history(dbvideo, text, typeid=1):
  function insert_video (line 147) | def insert_video(title, path, vidtype, index, filebot):
  function update_video (line 160) | def update_video(vidobj, statusid, filename=None):
  function db_integrity_check (line 172) | def db_integrity_check():

FILE: classes/docopt.py
  class DocoptLanguageError (line 17) | class DocoptLanguageError(Exception):
  class DocoptExit (line 22) | class DocoptExit(SystemExit):
    method __init__ (line 28) | def __init__(self, message=''):
  class Pattern (line 32) | class Pattern(object):
    method __eq__ (line 34) | def __eq__(self, other):
    method __hash__ (line 37) | def __hash__(self):
    method fix (line 40) | def fix(self):
    method fix_identities (line 45) | def fix_identities(self, uniq=None):
    method fix_repeating_arguments (line 57) | def fix_repeating_arguments(self):
  function transform (line 72) | def transform(pattern):
  class LeafPattern (line 99) | class LeafPattern(Pattern):
    method __init__ (line 103) | def __init__(self, name, value=None):
    method __repr__ (line 106) | def __repr__(self):
    method flat (line 109) | def flat(self, *types):
    method match (line 112) | def match(self, left, collected=None):
  class BranchPattern (line 133) | class BranchPattern(Pattern):
    method __init__ (line 137) | def __init__(self, *children):
    method __repr__ (line 140) | def __repr__(self):
    method flat (line 144) | def flat(self, *types):
  class Argument (line 150) | class Argument(LeafPattern):
    method single_match (line 152) | def single_match(self, left):
    method parse (line 159) | def parse(class_, source):
  class Command (line 165) | class Command(Argument):
    method __init__ (line 167) | def __init__(self, name, value=False):
    method single_match (line 170) | def single_match(self, left):
  class Option (line 180) | class Option(LeafPattern):
    method __init__ (line 182) | def __init__(self, short=None, long=None, argcount=0, value=False):
    method parse (line 188) | def parse(class_, option_description):
    method single_match (line 204) | def single_match(self, left):
    method name (line 211) | def name(self):
    method __repr__ (line 214) | def __repr__(self):
  class Required (line 219) | class Required(BranchPattern):
    method match (line 221) | def match(self, left, collected=None):
  class Optional (line 232) | class Optional(BranchPattern):
    method match (line 234) | def match(self, left, collected=None):
  class OptionsShortcut (line 241) | class OptionsShortcut(Optional):
  class OneOrMore (line 246) | class OneOrMore(BranchPattern):
    method match (line 248) | def match(self, left, collected=None):
  class Either (line 268) | class Either(BranchPattern):
    method match (line 270) | def match(self, left, collected=None):
  class Tokens (line 282) | class Tokens(list):
    method __init__ (line 284) | def __init__(self, source, error=DocoptExit):
    method from_pattern (line 289) | def from_pattern(source):
    method move (line 294) | def move(self):
    method current (line 297) | def current(self):
  function parse_long (line 301) | def parse_long(tokens, options):
  function parse_shorts (line 334) | def parse_shorts(tokens, options):
  function parse_pattern (line 369) | def parse_pattern(source, options):
  function parse_expr (line 377) | def parse_expr(tokens, options):
  function parse_seq (line 390) | def parse_seq(tokens, options):
  function parse_atom (line 402) | def parse_atom(tokens, options):
  function parse_argv (line 428) | def parse_argv(tokens, options, options_first=False):
  function parse_defaults (line 452) | def parse_defaults(doc):
  function parse_section (line 464) | def parse_section(name, source):
  function formal_usage (line 470) | def formal_usage(section):
  function extras (line 476) | def extras(help, version, options, doc):
  class Dict (line 485) | class Dict(dict):
    method __repr__ (line 487) | def __repr__(self):
  function docopt (line 491) | def docopt(doc, argv=None, help=True, version=None, options_first=False):

FILE: classes/ffmpeg.py
  class FFmpeg (line 22) | class FFmpeg(object):
    method __init__ (line 24) | def __init__(self, debug, compressionpath, silent, vformat):
    method compress (line 29) | def compress(self, nice, args, dbvideo):

FILE: classes/filebot.py
  class FileBot (line 20) | class FileBot(object):
    method __init__ (line 22) | def __init__(self, debug, silent):
    method rename (line 25) | def rename(self, dbvideo, movePath):
    method get_subtitles (line 92) | def get_subtitles(self, dbvideo, lang):

FILE: classes/handbrake.py
  class HandBrake (line 21) | class HandBrake(object):
    method __init__ (line 23) | def __init__(self, debug, compressionpath, vformat, silent):
    method compress (line 28) | def compress(self, nice, args, dbvideo):

FILE: classes/logger.py
  class Logger (line 20) | class Logger(object):
    method __init__ (line 22) | def __init__(self, name, debug, silent):
    method __del__ (line 37) | def __del__(self):
    method createhandlers (line 43) | def createhandlers(self, frmt, name, loglevel):
    method debug (line 59) | def debug(self, msg):
    method info (line 62) | def info(self, msg):
    method warn (line 65) | def warn(self, msg):
    method error (line 68) | def error(self, msg):
    method critical (line 71) | def critical(self, msg):

FILE: classes/makemkv.py
  class MakeMKV (line 26) | class MakeMKV(object):
    method __init__ (line 28) | def __init__(self, config):
    method _clean_title (line 41) | def _clean_title(self):
    method _remove_duplicates (line 65) | def _remove_duplicates(self, title_list):
    method _read_mkv_messages (line 75) | def _read_mkv_messages(self, stype, sid=None, scode=None):
    method set_title (line 112) | def set_title(self, vidname):
    method set_index (line 124) | def set_index(self, index):
    method rip_disc (line 136) | def rip_disc(self, path, titleIndex):
    method find_disc (line 213) | def find_disc(self):
    method get_disc_info (line 269) | def get_disc_info(self):
    method get_type (line 349) | def get_type(self):
    method get_title (line 371) | def get_title(self):
    method get_savefiles (line 384) | def get_savefiles(self):

FILE: classes/mediainfo.py
  class ForcedSubs (line 33) | class ForcedSubs(object):
    method __init__ (line 34) | def __init__(self, config):
    method discover_forcedsubs (line 49) | def discover_forcedsubs(self, dbvideo):
    method flag_forced (line 98) | def flag_forced(self, dbvideo, track):

FILE: classes/notification.py
  class Notification (line 18) | class Notification(object):
    method __init__ (line 20) | def __init__(self, config, debug, silent):
    method import_from (line 26) | def import_from(self, module, name, config):
    method _send (line 31) | def _send(self, status):
    method rip_complete (line 43) | def rip_complete(self, dbvideo):
    method rip_fail (line 48) | def rip_fail(self, dbvideo):
    method compress_complete (line 53) | def compress_complete(self, dbvideo):
    method compress_fail (line 58) | def compress_fail(self, dbvideo):
    method extra_complete (line 63) | def extra_complete(self, dbvideo):

FILE: classes/pushover.py
  class Pushover (line 18) | class Pushover(object):
    method __init__ (line 20) | def __init__(self, config, debug, silent):
    method send_notification (line 24) | def send_notification(self, notification_message):

FILE: classes/smtp.py
  class Smtp (line 22) | class Smtp(object):
    method __init__ (line 24) | def __init__(self, config, debug, silent):
    method send_notification (line 33) | def send_notification(self, notification_message):

FILE: classes/stopwatch.py
  class StopWatch (line 18) | class StopWatch(object):
    method __enter__ (line 20) | def __enter__(self):
    method __exit__ (line 24) | def __exit__(self, *args):

FILE: classes/testing.py
  function perform_testing (line 20) | def perform_testing(config):
  function canwrite (line 40) | def canwrite(path):
  function booltostatus (line 49) | def booltostatus(inbool):
  function checkcommand (line 56) | def checkcommand(com):

FILE: classes/utils.py
  function strip_accents (line 18) | def strip_accents(s):
  function clean_special_chars (line 33) | def clean_special_chars(s):
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (98K chars).
[
  {
    "path": ".gitignore",
    "chars": 320,
    "preview": "*.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# Install"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 453,
    "preview": "-   repo: git://github.com/pre-commit/pre-commit-hooks\n    sha: master\n    hooks:\n    -   id: trailing-whitespace\n    - "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1457,
    "preview": "# Contributing to Autorippr\n\nSo you'd like to contribue to Autorippr? Fantastic! I love pull requests and welcome any se"
  },
  {
    "path": "CONTRIBUTORS.md",
    "chars": 396,
    "preview": "# Author\n\n* JasonMillward https://github.com/JasonMillward\n\n# Contributors\n\n* carrigan98 https://github.com/carrigan98\n*"
  },
  {
    "path": "Dockerfile",
    "chars": 839,
    "preview": "FROM ubuntu:16.04\n\nCOPY /build/* /\n\nRUN echo \"deb http://ppa.launchpad.net/stebbins/handbrake-releases/ubuntu xenial mai"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Jason Millward\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "NOTES.md",
    "chars": 2477,
    "preview": "# Misc notes for future me / developers\n\n## TV Shows (and maybe proper movie titles)\n\nDisc info can be obtained, it retu"
  },
  {
    "path": "README.md",
    "chars": 343,
    "preview": "Autorippr\n=========\n\nAutorippr is currently unmaintained. I am lacking in free time to work on this myself but am happy "
  },
  {
    "path": "autorippr.py",
    "chars": 14814,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nAutorippr\n\nRipping\n    Uses MakeMKV to watch for videos inserted into DVD/BD Drives\n\n    Aut"
  },
  {
    "path": "autorippr_install_script.sh",
    "chars": 2280,
    "preview": "#!/bin/bash\n\n# Install Script Version 1.0\n# This script is designed to install Autorippr for Ubuntu 16.04 LTS\n# All requ"
  },
  {
    "path": "build-docker.sh",
    "chars": 262,
    "preview": "#!/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/s"
  },
  {
    "path": "build_makemkv/Dockerfile",
    "chars": 767,
    "preview": "FROM ubuntu:16.04\n\nRUN apt-get update && apt-get install -y python-virtualenv build-essential pkg-config libc6-dev libss"
  },
  {
    "path": "classes/__init__.py",
    "chars": 260,
    "preview": "# -*- coding: utf-8 -*-\n__all__ = [\n    'analytics',\n    'compression',\n    'database',\n    'docopt',\n    'ffmpeg',\n    "
  },
  {
    "path": "classes/analytics.py",
    "chars": 1482,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nBasic analytics\n\nThe purpose of this file is to simply send a 'ping' with a unique identifie"
  },
  {
    "path": "classes/compression.py",
    "chars": 2436,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nCompression Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2014, Ian Bird, Jason Mil"
  },
  {
    "path": "classes/database.py",
    "chars": 4189,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nSQLite Database Helper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n"
  },
  {
    "path": "classes/docopt.py",
    "chars": 19810,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"Pythonic command-line interface parser that will make you smile.\n\n * http://docopt.org\n * Rep"
  },
  {
    "path": "classes/ffmpeg.py",
    "chars": 2664,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nFFmpeg Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2014, Ian Bird\n\n@category   mi"
  },
  {
    "path": "classes/filebot.py",
    "chars": 3655,
    "preview": "#*- coding: utf-8 -*-\n\"\"\"\nFileBot class\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category  "
  },
  {
    "path": "classes/handbrake.py",
    "chars": 3348,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nHandBrake CLI Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@"
  },
  {
    "path": "classes/logger.py",
    "chars": 1722,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nSimple logging class\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@c"
  },
  {
    "path": "classes/makemkv.py",
    "chars": 11873,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nMakeMKV CLI Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@ca"
  },
  {
    "path": "classes/mediainfo.py",
    "chars": 5055,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nCreated on Mon Jan 09 11:20:23 2017\n\nDependencies:\n   System:\n       mediainfo\n       mkvtoo"
  },
  {
    "path": "classes/notification.py",
    "chars": 1906,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nNotification Class\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jason Millward\n\n@cat"
  },
  {
    "path": "classes/pushover.py",
    "chars": 846,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nPushover Class\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jason Millward\n\n@categor"
  },
  {
    "path": "classes/smtp.py",
    "chars": 1520,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nSMTP Class\n\n\nReleased under the MIT license\nCopyright (c) 2014, Jacob Carrigan\n\n@category   "
  },
  {
    "path": "classes/stopwatch.py",
    "chars": 565,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nSimple StopWatch\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@categ"
  },
  {
    "path": "classes/testing.py",
    "chars": 1285,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nConfiguration and requirements testing\n\n\nReleased under the MIT license\nCopyright (c) 2012, "
  },
  {
    "path": "classes/utils.py",
    "chars": 975,
    "preview": "\"\"\"\nHandBrake CLI Wrapper\n\n\nReleased under the MIT license\nCopyright (c) 2012, Jason Millward\n\n@category   misc\n@version"
  },
  {
    "path": "settings.example.cfg",
    "chars": 3621,
    "preview": "makemkv:\n    # Path to makemkvcon (with trailing slash) in case it is unavailable in your $PATH\n    makemkvconPath: \"\"\n\n"
  }
]

About this extraction

This page contains the full source code of the JasonMillward/Autorippr GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (90.5 KB), approximately 22.3k tokens, and a symbol index with 151 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!