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