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