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) ## 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. ## 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,"Source information
" 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,"Title information
" 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, , 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 "", and values are the parsed values of those elements. Example ------- >>> from docopt import docopt >>> doc = ''' ... Usage: ... my_program tcp [--timeout=] ... my_program serial [--baud=] [--timeout=] ... my_program (-h | --help | --version) ... ... Options: ... -h, --help Show this screen and exit. ... --baud= Baudrate [default: 9600] ... ''' >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] >>> docopt(doc, argv) {'--baud': '9600', '--help': False, '--timeout': '30', '--version': False, '': '127.0.0.1', '': '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