This commit is contained in:
2022-03-07 23:52:01 +01:00
commit 24144bb550
25 changed files with 2323 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Derailleur is a collection of tools to manage the Transmission client
and process the downloads once complete"""
from .about import __version__
from . import args
from . import config
from . import loggers
from . import client
from . import postproc
from . import history
from . import manager

16
src/derailleur/about.py Normal file
View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Basic info"""
__version__ = '0.31'
VERSION = __version__
MAINTAINER = "Mads Michelsen <mail@brokkr.net>"
DESCRIPTION = "A set of tools for the Transmission client"
URL = "https://github.com/brokkr/derailleur"

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Designation of basic arguments for the derailleur scripts"""
import sys
import argparse
from derailleur import __version__
def start_parser():
'''Start a parser, leave open for custom arguments in script'''
about = 'Derailleur ' + __version__ + '\n' \
+ 'See wiki for help: https://github.com/brokkr/derailleur/wiki'
parser = argparse.ArgumentParser(description=about, formatter_class=\
argparse.RawTextHelpFormatter)
parser.add_argument("-V", "--version", help="print version number",
action="store_true")
return parser
def end_parser(parser):
'''Close up parser and return the user input'''
parsed_args = parser.parse_args()
if parsed_args.version:
print(str(__version__))
sys.exit()
return parsed_args

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Establishing a connection to the Transmission client"""
import urllib.request
import urllib.error
from os import path
from tempfile import TemporaryDirectory
import transmissionrpc
class TransmissionClient(transmissionrpc.Client):
'''TransmissionClient with added methods for downloading .torrents'''
def download(self, torrent_url, pause_when_added):
'''Validates and starts magnets, local and remote torrent files'''
if torrent_url[:6] == 'magnet':
pass
elif torrent_url[-8:] == '.torrent' and path.isfile(torrent_url):
torrent_url = "file://" + torrent_url
elif torrent_url[-8:] == '.torrent' and torrent_url[:4] == 'http':
tmp_dir = TemporaryDirectory()
file_path = path.join(tmp_dir.name, 'd.torrent')
try:
urllib.request.urlretrieve(torrent_url, file_path)
except urllib.error.HTTPError:
return False
torrent_url = "file://" + file_path
else:
return False
try:
torrent = self.add_torrent(torrent_url, paused=pause_when_added)
return torrent.name
except (transmissionrpc.error.TransmissionError, AttributeError):
return False
def get_client(derailleurconfig):
'''Uses configobj to create a client'''
try:
transmissionclient = TransmissionClient(
derailleurconfig['Connection']['host'],
derailleurconfig['Connection']['port'],
derailleurconfig['Connection']['user'],
derailleurconfig['Connection']['password']
)
except transmissionrpc.error.TransmissionError:
transmissionclient = None
return transmissionclient

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Parses the configuration and validates it against an external schema"""
import sys
import fileinput
from os import path, makedirs
from configobj import ConfigObj
from validate import Validator, ValidateError
from derailleur import __file__ as module_file
def nestring_check(value):
'''Requires string to not be empty or contain whitespace'''
if not isinstance(value, (str, int)):
raise ValidateError
if not value or ' ' in value:
raise ValidateError
return str(value)
def path_check(value):
'''Very basic requirements for path string'''
if not isinstance(value, str):
raise ValidateError
if not value[0] == '/':
raise ValidateError
return str(value)
def get_config():
'''Create a configobj instance plus validation of user config'''
spec_file = path.join(path.dirname(module_file), 'config',
'configspec.ini')
config_dir = path.join(path.expanduser('~'), '.derailleur')
config_path = path.join(config_dir, 'derailleur.conf')
if not path.isdir(config_dir):
makedirs(config_dir)
if not path.isfile(config_path):
config = ConfigObj(config_path, configspec=spec_file, stringify=False,
write_empty_values=True)
else:
config = ConfigObj(config_path, configspec=spec_file, stringify=True)
validator = Validator({'nestring': nestring_check, 'path': path_check})
validation = config.validate(validator, copy=True)
if not path.isfile(config_path):
config.write()
print('New config file: ' + config_path)
sys.exit()
config.dir = config_dir
config.path = config_path
config.db = path.join(config_dir, 'derailleur.db')
config.log = {
'Add': path.join(config_dir, 'derailleur-add.log'),
'Feed': path.join(config_dir, 'derailleur-feed.log'),
'Manager': path.join(config_dir, 'derailleur-manager.log'),
'Postproc': path.join(config_dir, 'derailleur-postproc.log')
}
if validation is True:
validation = {
'Connection': True,
'Add': True,
'Feed': True,
'Manager': True,
'Postproc': True,
'Audio': True,
'Smtp': True,
'Kodi': True
}
return (config, validation)

View File

@@ -0,0 +1,56 @@
[Connection]
host = nestring(default=127.0.0.1)
port = integer(default=9091)
user = nestring(default=None)
password = nestring(default=None)
[Add]
pause_when_added = boolean(default=false)
file_log = boolean(default=true)
mail_log = boolean(default=false)
[Feed]
url = nestring(default=None)
pause_when_added = boolean(default=false)
file_log = boolean(default=true)
mail_log = boolean(default=false)
[Manager]
hours_before_removal = integer(default=72)
delete_data = boolean(default=false)
seed_stops = list(default=None)
file_log = boolean(default=true)
mail_log = boolean(default=false)
[Postproc]
tmp_path = path(default='/tmp')
base_path_tv = path(default='/tmp/derailleur/tv')
base_path_film = path(default='/tmp/derailleur/film')
base_path_music = path(default='/tmp/derailleur/music')
base_path_audiobook = path(default='/tmp/derailleur/audiobook')
base_path = path(default='/tmp/derailleur')
keywords = list(default=None)
file_log = boolean(default=true)
mail_log = boolean(default=false)
[Audio]
retain_id3v1 = boolean(default=false)
retain_genre = boolean(default=false)
retain_cover_image = boolean(default=true)
rename_cover_image = nestring(default=cover)
[Smtp]
mailhost = nestring(default=None)
mailport = integer(default=587)
fromaddr = nestring(default=None)
toaddr = nestring(default=None)
login = nestring(default=None)
password = nestring(default=None)
[Kodi]
connect_to_kodi = boolean(default=false)
host = ip_addr(default=127.0.0.1)
port = integer(default=8080)
user = nestring(default=None)
password = nestring(default=None)

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Poca is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Jar exists only to save any object handed to it (although it defaults to
a dictionary). Likewise, once configured it will return said object when
called upon by the load method."""
import os
import pickle
class Jar:
'''Class for retrieving and saving feed/manager entry info'''
def __init__(self, config, db_filename):
self.db_filepath = os.path.join(config.dir, db_filename)
def load(self):
'''Retrieve contents of pickle'''
if not os.path.isfile(self.db_filepath):
pickle_contents = {}
success = self.save(pickle_contents)
if not success:
return False
try:
with open(self.db_filepath, mode='rb') as f:
pickle_contents = pickle.load(f)
except (PermissionError, pickle.UnpicklingError, EOFError):
return False
return pickle_contents
def save(self, pickle_contents):
'''Saves pickle_contents to file using pickle'''
try:
with open(self.db_filepath, 'wb') as f:
pickle.dump(pickle_contents, f)
return True
except (PermissionError, pickle.PicklingError, EOFError):
return False

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""A module implmenting a subclassed version of the Logger class. Our version
pins some preferences to the instance and auto-adds any needed/requested
handlers, e.g. null, file and smtp."""
import logging
from logging import handlers
class Logger(logging.Logger):
'''Custom Logger class with auto-added handlers'''
def __init__(self, name, config, validation, level=logging.DEBUG, verbose=False):
super(self.__class__, self).__init__(name, level)
self.config = config
self.add_null_handler()
if self.config[self.name]['file_log']:
self.add_file_handler()
if self.config[self.name]['mail_log'] and validation['Smtp'] is True:
self.add_smtp_handler()
if verbose:
self.add_stream_handler()
def add_null_handler(self):
'''Adding a basic null_handler'''
null_handler = logging.NullHandler()
self.addHandler(null_handler)
def add_stream_handler(self):
'''Adding a basic stream_handler'''
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.DEBUG)
self.addHandler(stream_handler)
def add_file_handler(self):
'''Adding a filehandler'''
file_handler = logging.FileHandler(self.config.log[self.name])
file_handler.setLevel(logging.INFO)
file_formatter = logging.Formatter("%(asctime)s %(message)s",
datefmt='%Y-%m-%d %H:%M')
file_handler.setFormatter(file_formatter)
self.addHandler(file_handler)
def add_smtp_handler(self):
'''Adding an SMTP_handler'''
subject = 'derailleur-' + self.name.lower()
smtp_handler = handlers.SMTPHandler((self.config['Smtp']['mailhost'],
self.config['Smtp']['mailport']),
self.config['Smtp']['fromaddr'],
[self.config['Smtp']['toaddr']],
subject,
credentials=\
(self.config['Smtp']['login'],
self.config['Smtp']['password']),
secure=())
smtp_handler.setLevel(logging.INFO)
mail_formatter = logging.Formatter("%(message)s")
smtp_handler.setFormatter(mail_formatter)
self.addHandler(smtp_handler)

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""A library for managing active and completed torrents"""
from datetime import datetime
from collections import namedtuple
Incomplete = namedtuple('non_completed', 'all downloading paused')
SubSeeded = namedtuple('sub_ratio_seeded', 'all seeding paused')
SuperSeeded = namedtuple('ratio_seeded', 'all seeding paused')
class Manager():
'''Transmission session manager that sorts torrents according to status'''
def __init__(self, config, client, logger, jar):
self.client = client
self.config = config
self.logger = logger
self.jar = jar
torrents = client.get_torrents()
self.all = torrents
for torrent in torrents:
torrent.update()
torrent.private = 'Private' if torrent._fields['isPrivate'].value \
else 'Public'
incomplete = [x for x in torrents if x.progress < 100]
incomplete_dl = [x for x in incomplete if x.status == 'downloading']
incomplete_paused = [x for x in incomplete if x.status == 'stopped']
self.incomplete = Incomplete(incomplete,
incomplete_dl,
incomplete_paused)
completed = [x for x in torrents if x.progress == 100]
sub_seeded = [x for x in completed if x.ratio < x.seed_ratio_limit]
sub_seeding = [x for x in sub_seeded if x.status == 'seeding']
sub_paused = [x for x in sub_seeded if x.status == 'stopped']
self.sub_seeded = SubSeeded(sub_seeded,
sub_seeding,
sub_paused)
super_seeded = [x for x in completed if x.ratio >= x.seed_ratio_limit]
super_seeding = [x for x in super_seeded if x.status == 'seeding']
super_paused = [x for x in super_seeded if x.status == 'stopped']
self.super_seeded = SuperSeeded(super_seeded,
super_seeding,
super_paused)
def check_torrents(self):
'''Loop through various collections and check on them'''
self.logger.debug("=== DOWNLOADING ===")
self.logger.debug('{: <32.32s} type current'.format(''))
for torrent in self.incomplete.all:
self.print_downloading(torrent)
self.logger.debug('')
self.logger.debug("=== BELOW RATIO ===")
self.logger.debug('{: <32.32s} type current'.format(''))
for torrent in self.sub_seeded.all:
self.print_sub_seeded(torrent)
self.logger.debug('')
self.logger.debug("=== ABOVE RATIO: SEEDING ===")
self.logger.debug('{: <32.32s} type current last'.format(''))
for torrent in self.super_seeded.seeding:
self.check_super_seeding(torrent)
self.logger.debug('')
self.logger.debug("=== ABOVE RATIO: PAUSED ===")
self.logger.debug('{: <32.32s} type current hour ttl'.format(''))
for torrent in self.super_seeded.paused:
self.check_super_paused(torrent)
self.cleanup()
def print_downloading(self, torrent):
'''Print info on dl torrents'''
stream = '{: <32.32s} {: <7.7s} {:3.2f}'
stream = stream.format(torrent.name, torrent.private, torrent.ratio)
self.logger.debug(stream)
def print_sub_seeded(self, torrent):
'''Print info on seed torrents not yet at ratio'''
stream = '{: <32.32s} {: <7.7s} {:3.2f}'
stream = stream.format(torrent.name, torrent.private, torrent.ratio)
self.logger.debug(stream)
def check_super_seeding(self, torrent):
'''Check up on torrents still seeding after reaching ratio'''
jar_dic = self.jar.load()
torrent_hash = torrent.hashString
if torrent_hash in jar_dic:
last_seed_stop = jar_dic[torrent_hash][1]
else:
last_seed_stop = torrent.seed_ratio_limit
# /stream logger
stream = '{: <32.32s} {: <7.7s} {:3.2f} {:3.2f}'
stream = stream.format(torrent.name, str(torrent.private),
torrent.ratio, last_seed_stop)
self.logger.debug(stream)
# stream logger/
stop_tests = {seed_stop: last_seed_stop < float(seed_stop) \
<= torrent.ratio for seed_stop in \
self.config['Manager']['seed_stops']}
true_seed_stops = [x for x in stop_tests if stop_tests[x] is True]
if not true_seed_stops:
jar_dic[torrent_hash] = (None, last_seed_stop, torrent.name)
self.jar.save(jar_dic)
return
torrent.stop()
self.logger.info("We have stopped seeding %s @ ratio %0.2f." %
(torrent.name, torrent.ratio))
jar_dic[torrent_hash] = (datetime.now(), torrent.ratio, torrent.name)
self.jar.save(jar_dic)
def check_super_paused(self, torrent):
'''Check up on torrents that have paused after reaching ratio
(or have been paused because they've reached a seed stop)'''
jar_dic = self.jar.load()
torrent_hash = torrent.hashString
if not torrent_hash in jar_dic:
self.logger.info("%s has stopped seeding after reaching seed "
"limit %0.2f." % (torrent.name, torrent.ratio))
jar_dic[torrent_hash] = (datetime.now(), torrent.ratio, torrent.name)
_success = self.jar.save(jar_dic)
return
paused_datetime = jar_dic[torrent_hash][0]
if paused_datetime:
time_stopped = datetime.now() - jar_dic[torrent_hash][0]
hours_paused = int(time_stopped.total_seconds() / 3600)
else:
hours_paused = 0
jar_dic[torrent_hash] = (datetime.now(), torrent.ratio, torrent.name)
_success = self.jar.save(jar_dic)
wait_hours = int(self.config['Manager']['hours_before_removal'])
ttl = wait_hours - hours_paused
# /stream logger
stream = '{: <32.32s} {: <7.7s} {:3.2f} {:3.0f} {:3.0f}'
stream = stream.format(torrent.name, torrent.private, torrent.ratio,
hours_paused, ttl)
self.logger.debug(stream)
# stream logger/
if hours_paused >= wait_hours:
self.client.remove_torrent(torrent.id, delete_data=\
self.config['Manager']['delete_data'])
self.logger.info("%s has been removed after pausing for %i hours. "
"It seeded to %0.2f."
% (torrent.name, hours_paused, torrent.ratio))
del(jar_dic[torrent_hash])
_success = self.jar.save(jar_dic)
def cleanup(self):
'''Cleanup routine that removes leftover records (e.g. for
entries that have been manually removed)'''
active_hashes = set([torrent.hashString for torrent in self.all])
jar_dic = self.jar.load()
jar_hashes = set(jar_dic.keys())
difference = jar_hashes.difference(active_hashes)
for torrent_hash in difference:
del(jar_dic[torrent_hash])
_success = self.jar.save(jar_dic)

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Library for Transmission post-download sorting and filtering"""
from . import audio
from . import film
from . import keywords
from . import stats
from . import tv

View File

@@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Submodule for processing audio files and their metadata"""
import os
import re
import shutil
from collections import defaultdict
from mutagen.easyid3 import EasyID3
from mutagen.flac import FLAC
from mutagen.id3._util import ID3NoHeaderError
from . import generic
from .functions import char_wash
META_FRAMES = ['artist', 'album', 'title', 'tracknumber', 'discnumber',
'date', 'genre']
EXT_CLASS_DIC = {'.mp3': EasyID3, '.flac': FLAC}
class Torrent(generic.Torrent):
"""Custom Torrent class for mp3 and flac files. Edits metadata and renames
according to scheme."""
## 1: READING
def read_tag(self, TagClass, tag_dic):
"""run once per file, extracts metadata from tag using mutagen's
id3/flac common interface."""
try:
tag = TagClass(tag_dic['filepath'])
tag_frames = list(set(META_FRAMES).intersection(list(tag.keys())))
tag_metadata = {frame:tag[frame][0] for frame in tag_frames}
except ID3NoHeaderError:
tag_frames = []
tag_metadata = {}
# remove '/' from track and disc numbers
fix_number = lambda x: int(str(x).split('/')[0])
tag_numbers = {number: fix_number(tag_metadata[number]) for number
in ['tracknumber', 'discnumber'] if number
in tag_frames}
# merge all the metadata back into tag_dic
tag_metadata.update(tag_numbers)
tag_dic.update(tag_metadata)
tag_dic['frames'] = tag_frames
return tag_dic
def variance(self, tag_name):
"""Establish variance of tag values or missing values"""
try:
tag_set = set(tag_dic[tag_name] for tag_dic in self.tag_lst)
except KeyError:
tag_set = set()
return tag_set
def read_tags(self):
"""Creates a list of all music files with metadata information"""
self.tag_lst = [self.read_tag(EXT_CLASS_DIC[file_dic['ext']], file_dic)
for file_dic in self.stats.file_lst
if file_dic['ext'] in EXT_CLASS_DIC]
self.artist_no = len(self.variance('artist'))
self.album_no = len(self.variance('album'))
self.tracknumber_no = len(self.variance('tracknumber'))
self.title_no = len(self.variance('title'))
self.discnumber_no = len(self.variance('discnumber'))
self.track_width = len(str(max(self.variance('tracknumber'))))
## 2: NAMING SCHEME
def rename_scheme(self, tag_dic):
"""Work out the rename scheme based on number of artists and albums
in the total file mass"""
# level one
if self.artist_no == 0:
level_one = "z_missing_artist"
elif self.album_no == 0:
level_one = "z_missing_album"
elif self.artist_no == 1:
level_one = char_wash(tag_dic['artist'])
elif self.artist_no > 1:
level_one = "z_various artists"
# level two
if self.album_no == 0:
level_two = char_wash(tag_dic['parentdir'])
elif self.album_no > 0 and self.discnumber_no <= 1:
level_two = char_wash(tag_dic['album'])
elif self.album_no > 0 and self.discnumber_no > 1:
level_two = char_wash(tag_dic['album']) + '_disc_' + \
str(tag_dic['discnumber'])
# file name
if self.tracknumber_no == 0 or self.title_no == 0:
filename = char_wash(tag_dic['basename']) + tag_dic['ext']
elif self.album_no == 0:
filename = char_wash(tag_dic['basename']) + tag_dic['ext']
elif self.artist_no <= 1:
filename = str(tag_dic['tracknumber']).zfill(self.track_width) + \
'_' + char_wash(tag_dic['title']) + tag_dic['ext']
elif self.artist_no > 1:
filename = str(tag_dic['tracknumber']).zfill(self.track_width) + \
'_' + char_wash(tag_dic['artist']) + '_-_' + \
char_wash(tag_dic['title']) + tag_dic['ext']
# putting it all together
return os.path.join(self.type_path, level_one, level_two, filename)
def rename(self):
'''Run rename scheme for each file'''
for tag_dic in self.tag_lst:
tag_dic['new_path'] = self.rename_scheme(tag_dic)
## 3: WRITING
def new_tags(self):
"""Create temp files, rewrite their tags, prepare for move_files"""
for tag_dic in self.tag_lst:
tmp_file_path = os.path.join(self.tmp_folder.name,
char_wash(tag_dic['filename']))
shutil.copy(tag_dic['filepath'], tmp_file_path)
# run appropriate tag function on file to access and delete it
if tag_dic['frames']:
tag = EXT_CLASS_DIC[tag_dic['ext']](tmp_file_path)
tag.delete()
elif tag_dic['ext'] == '.mp3':
tag = EasyID3()
tag.save(tmp_file_path)
elif tag_dic['ext'] == '.flac':
tag = FLAC(tmp_file_path)
tag.save(tmp_file_path)
# why are we turning ints to strs? is it a requirement of mutagen?
for frame in tag_dic['frames']:
if frame == 'discnumber' and self.discnumber_no <= 1:
continue
if frame == 'genre' and \
not self.config['Audio']['retain_genre']:
continue
if type(tag_dic[frame]) == int:
tag[frame] = [str(tag_dic[frame])]
else:
tag[frame] = [tag_dic[frame]]
tag.save(tmp_file_path)
if type(tag).__name__ == 'EasyID3' and \
self.config['Audio']['retain_id3v1']:
tag.save(tmp_file_path, v1=2)
self.move_lst.append((tmp_file_path, tag_dic['new_path']))
self.move_type_lst.append(tag_dic['filetype'])
## 4: COVERART
def coverart(self):
"""For every destination folder, choose an image if any. Available
images are scored for suitability by various factors, including
name, placement and size."""
if not self.stats.file_by_type_dic['image']:
return
# for every destination, list origins
dest_dic = defaultdict(set)
for tag_dic in self.tag_lst:
ppath = os.path.dirname(tag_dic['new_path'])
dest_dic[ppath].add(tag_dic['folderpath'])
image_lst = self.stats.file_by_type_dic['image']
max_image_size = max([image['bytesize'] for image in image_lst])
# for every destination, list candidates
for dest_dir in list(dest_dic.keys()):
image_scores_dic = {}
for image in image_lst:
image_score = 0
# image shares origin_folder with tags with this destination
if image['folderpath'] in dest_dic[dest_dir]:
if len(dest_dic[dest_dir]) == 1:
image_score += 1000
else:
image_score += 500
# basename is cover etc. or it's part of the basename
candidate_basenames = ['cover', 'front', 'folder', 'art']
if image['basename'].lower() in candidate_basenames:
image_score += 200
else:
regex = False
for basename in candidate_basenames:
re_test = re.compile('.*%s.*'%basename)
if re_test.match(image['basename'].lower()):
regex = True
if regex:
image_score += 100
# give a bonus to the largest image (in bytesize)
size_score = float(image['bytesize']) / max_image_size * 99
image_score += size_score
# enter the image and the image score into the dictionary
image_path = os.path.join(image['folderpath'],
image['filename'])
image_scores_dic[image_path] = image_score
# find the image with the highest score for that destination
highscorer = max(iter(image_scores_dic.keys()),
key=(lambda key: image_scores_dic[key]))
# Add winner to move_lst
new_image_name = self.config['Audio']['rename_cover_image'] + \
os.path.splitext(highscorer)[1]
new_file_path = os.path.join(self.type_path, dest_dir,
new_image_name)
self.move_lst.append((highscorer, new_file_path))
self.move_type_lst.append('image')
def manipulate(self):
"""Overall music manipulation function, encompassing read_tags,
categorize, rename, new_tags, and coverart."""
self.read_tags()
self.rename()
self.new_tags()
if self.config['Audio']['retain_cover_image']:
self.coverart()

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Submodule for processing movie files"""
import os
import re
from . import generic
from .functions import char_rinse
SOURCES = ['webdl', 'web-dl', 'brrip', 'bluray', 'hdtv', 'dvdrip']
RESOLUTIONS = ['1080p', '720p']
VIDEO = ['h264', 'x264', 'xvid', 'divx']
AUDIO = ['dts', 'aac2', 'ac3', 'mp3', 'aac']
CHANNELS = ['5 1', '6ch']
class Torrent(generic.Torrent):
"""Film content specific Torrent. Filters files and creates containing
folder according to naming scheme."""
def manipulate(self):
"""Use regex to extract information from name, then reconstruct
according to scheme"""
# lowercase, replace points by spaces, remove multiple spaces & parantheses
if os.path.isfile(self.torrent_path):
tname = os.path.splitext(self.torrent_name)[0]
else:
tname = self.torrent_name
tname = re.sub('[\[\]\.()]', ' ', tname)
tname = re.sub(' +', ' ', tname.lower())
# regex searches for year and resolution info
re_year = re.search(r'(19[3-9]\d|20[0-1]\d)', tname)
re_resolution = re.search(r'1080p|720p', tname)
# construct folder name according to scheme
if re_year:
dir_name = tname[0:re_year.start()] + '(' + re_year.group() + ')'
else:
dir_name = tname
if re_resolution:
dir_name = dir_name + ' [' + re_resolution.group() + ']'
dir_name = char_rinse(dir_name)
# get video files and subtitles, ignore the rest, apply filters
keep_types = ['video', 'subtitle']
keepers = [f for f in self.stats.file_lst if f['filetype'] in keep_types]
for file_dic in keepers:
if re.search(r'sample', file_dic['filename'].lower()):
continue
if file_dic['filetype'] == 'video' and file_dic['bytesize'] < 100000000:
continue
old_file_path = file_dic['filepath']
new_file_path = os.path.join(self.type_path,
dir_name, file_dic['filename'])
self.move_lst.append((old_file_path, new_file_path))
self.move_type_lst.append(file_dic['filetype'])

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""A collection of string manipulation functions, mostly transliteration"""
import string
from unidecode import unidecode
## Filter rules
WHITESTR = string.ascii_lowercase + string.digits + '_' + '-'
DANISH = [('Æ', 'Ae'), ('Ø', 'Oe'), ('Å', 'Aa'), ('æ', 'ae'), ('ø', 'oe'),
('å', 'aa')]
REPLACEMENTS = [('&', ' and '), ('=', ' equal '), ('+', ' plus '),
('/', ' or '), ('@', ' at ')]
## Functions
def asciify(_str):
'''Transliterates unicode into ascii'''
ustr = str(_str)
for swap in DANISH:
ustr = ustr.replace(swap[0], swap[1])
astr = unidecode(ustr)
return astr
def char_rinse(orgstring):
'''Custom replacements'''
rstr = asciify(orgstring)
# replace spaces with underscores + special cases:
# lowercase
for swap in REPLACEMENTS:
rstr = orgstring.replace(swap[0], swap[1])
rstr = rstr.lower()
return rstr
def char_wash(orgstring):
'''Replace whitespace'''
wstr = char_rinse(orgstring)
# underscores and filter anything not asciiish remaining
wstr = wstr.replace(' ', '_')
whitefilter = lambda x: x in WHITESTR
wstr = ''.join(list(filter(whitefilter, wstr)))
# remove multiple underscores
while '__' in wstr:
wstr = wstr.replace('__', '_')
# remove underscores from end of filename
if len(wstr) == 0:
wstr = '_empty'
if len(wstr) > 1:
if wstr[-1] == '_':
wstr = wstr[0:-1]
return wstr

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Defining the standard torrent class"""
import os
import shutil
from tempfile import TemporaryDirectory
class Torrent:
"""Basic torrent class with all required values and functions. Exists as
template for more customized content specific Torrent classes."""
def __init__(self, config, stats, type_path):
"""Creates instance of class. Universal, never overridden."""
self.config = config
self.stats = stats
self.type_path = type_path
self.torrent_name = stats.torrent_name
self.torrent_path = stats.torrent_path
self.torrent_ppath = stats.torrent_ppath
self.move_lst, self.move_type_lst = [], []
self.tmp_folder = config['Postproc']['tmp_path']
def manipulate(self):
"""Changes files and variables before selecting the relevant files for
move_files. Overridden by all but default content types."""
for file_dic in self.stats.file_lst:
rel_file_path = os.path.relpath(file_dic['filepath'],
self.torrent_ppath)
new_file_path = os.path.join(self.type_path, rel_file_path)
self.move_lst.append((file_dic['filepath'], new_file_path))
self.move_type_lst.append(file_dic['filetype'])
def move_files(self):
"""Copies each individual file (no folders) selected from origin
to chosen destination. Universal, never overridden."""
errors = False
for file_tuple in self.move_lst:
new_ppath = os.path.dirname(file_tuple[1])
if not os.path.isdir(new_ppath):
try:
os.makedirs(new_ppath)
except (PermissionError, OSError, IOError):
errors = True
continue
try:
shutil.copy(file_tuple[0], file_tuple[1])
except IOError:
errors = True
self.tmp_folder.cleanup()
return errors

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Defining the keywords specific torrent class"""
import os
from . import generic
class Torrent(generic.Torrent):
"""Keyword content type"""
def manipulate(self):
"""Changes files and variables before selecting the relevant files for
move_files. Keyword special that removes keyword from file name."""
for file_dic in self.stats.file_lst:
keyword = self.stats.identity
rel_file_path = os.path.relpath(file_dic['filepath'],
self.torrent_ppath)
rel_file_path = rel_file_path[len(keyword)+1:]
new_file_path = os.path.join(self.type_path, rel_file_path)
self.move_lst.append((file_dic['filepath'], new_file_path))
self.move_type_lst.append(file_dic['filetype'])

View File

@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Defining the stats class"""
import os
import re
class Stats:
"""File stats useful for identification"""
def create_file_dic(self, folder_path, f):
"""Run once per file, returns all general information about file"""
file_dic = {}
file_dic['filename'] = f
file_dic['folderpath'] = folder_path
file_dic['parentdir'] = os.path.split(folder_path)[1]
file_dic['filepath'] = os.path.join(folder_path, f)
file_dic['basename'], file_dic['ext'] = os.path.splitext(f)
file_dic['bytesize'] = os.path.getsize(file_dic['filepath'])
file_type = [k for k, v in list(self.ext_dic.items()) \
if file_dic['ext'] in v]
file_dic['filetype'] = ''.join(file_type)
return file_dic
def identify(self, keywords):
"""Analyzes naming and file stats to determine category of files"""
self.identity = 'generic'
for keyword in keywords:
re_keyword = re.search('^' + keyword + ' ', self.torrent_name)
if re_keyword:
self.identity = keyword
return
if self.torrent_name[0:10] == 'audiobook ':
self.identity = 'audiobook'
elif self.type_size_pct['music'] > 80:
self.identity = 'music'
elif self.type_size_pct['video'] > 80:
# tv torrent names either contain the season or season and episode
re_tv = re.compile('(?i).*season.*|.*series.*|.*complete.*|'
'.*s\d{1,2}.*|.*s\d{1,2}e\d{1,2}.*|'
'.*(\d{1,2})x(\d\d)')
re_daily = re.compile('.*(20[0-2][0-9]\.[0-1][0-9]\.[0-3][0-9]).*')
# films torrent names (almost) always contain a year (1930-2019)
re_film = re.compile('.*(19[3-9][0-9]|20[0-2][0-9]).*')
if re_tv.match(self.torrent_name):
self.identity = 'tv'
elif re_daily.match(self.torrent_name):
self.identity = 'tv'
elif re_film.match(self.torrent_name):
self.identity = 'film'
def __init__(self, keywords, torrent_info):
"""Creates instance of Stats class"""
self.torrent_name = torrent_info.name
self.torrent_ppath = torrent_info.ppath
self.torrent_path = os.path.join(torrent_info.ppath, torrent_info.name)
# what types are various file extensions
self.ext_dic = {
'package': ['.apk', '.deb', '.rpm'],
'music': ['.mp3', '.ogg', '.flac', '.wma', '.m4a'],
'video': ['.avi', '.mkv', '.wmv', '.mp4', '.3gp'],
'text': ['.txt', '.nfo', '.sfv', '.idx', '.url', '.m3u'],
'book': ['.pdf', '.epub', '.mobi'],
'subtitle': ['.srt', '.smi', '.sub'],
'image': ['.jpg', '.jpeg', '.png', '.gif', '.bmp'],
'compressed': ['.zip', '.rar', '.gz'],
'disc': ['.iso'],
'part': ['.part']
}
for i in range(0, 100):
self.ext_dic['compressed'].append(".r%02d" % i)
# initialize containers
self.file_lst, self.file_by_type_dic = [], {}
self.type_count, self.type_size = {}, {}
self.type_count_pct, self.type_size_pct = {}, {}
# tree walking
if os.path.isfile(self.torrent_path):
file_dic = self.create_file_dic(torrent_info.ppath, torrent_info.name)
self.file_lst.append(file_dic)
else:
for (root, dirs, files) in os.walk(self.torrent_path):
dir_contents = [self.create_file_dic(root, f) for f in files]
self.file_lst.extend(dir_contents)
# discount .part files
self.file_lst = [file_dic for file_dic in self.file_lst if
file_dic['filetype'] != 'part']
# calculate overall torrent stats
self.torrent_count = len(self.file_lst)
self.torrent_size = sum([fdic['bytesize'] for fdic in self.file_lst])
if not self.torrent_count:
self.torrent_count = 1
if not self.torrent_size:
self.torrent_size = 1
# calculate stats for represented file_types
for file_type in self.ext_dic:
file_type_lst = [file_dic for file_dic in self.file_lst \
if file_dic['filetype'] == file_type]
self.file_by_type_dic[file_type] = file_type_lst
self.type_count[file_type] = len(file_type_lst)
self.type_size[file_type] = sum([file_dic['bytesize'] \
for file_dic in file_type_lst])
self.type_count_pct[file_type] = float(self.type_count[file_type]) \
/ float(self.torrent_count) * 100
self.type_size_pct[file_type] = float(self.type_size[file_type]) \
/ float(self.torrent_size) * 100
self.identify(keywords)

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Defining the TV class"""
import os
import re
from . import generic
class Torrent(generic.Torrent):
"""TV specific content type. Establishes series name/season number
for kodi friendly folder scheme (series name/season no/files).
Works for series, seasons and individual episodes alike."""
def manipulate(self):
"""Use regexes to find series name and season number, create folder
scheme, filter junk files (anything but 'real' video and subs)"""
# lowercase, replace points by spaces, remove multiple occurences
tname = self.torrent_name.lower().replace(".", " ").replace("_", " ")
tname = re.sub(' +', ' ', tname)
# find indices for common title termination indicators
re_sxex = re.search(r's(\d\d)e(\d\d)|(\d{1,2})x(\d\d)', tname)
re_season = re.search(r'season (\d+)|series (\d+)|s(\d\d)', tname)
re_complete = re.search(r'complete', tname)
re_year = re.search(r'(19[3-9]\d|20[0-1]\d)', tname)
searches = [re_sxex, re_season, re_complete, re_year]
re_indices = [result.start() for result in searches if result]
# use lowest index to get title
if not re_indices:
re_indices = [-1]
lowest_index = min(re_indices)
series_name = tname[0:lowest_index]
# clean up series name (allow closing parenthesis for '(US)' etc.)
self.series_name = re.match(r'.*[^\-\_\ \(]', series_name).group()
# get fallback data from torrent name and set series/episode destination
if re_sxex:
se = [int(result) for result in re_sxex.groups() if result]
season_no, episode_no = se[0], se[1]
type_path = os.path.join(self.type_path, 'episodes')
elif re_season:
season_no = [int(result) for result in re_season.groups() if result][0]
type_path = os.path.join(self.type_path, 'series')
else:
season_no, episode_no = (None, None)
type_path = os.path.join(self.type_path, 'series')
# get video files and subtitles, ignore the rest
keep_types = ['video', 'subtitles']
keepers = [f for f in self.stats.file_lst if f['filetype'] in keep_types]
for file_dic in keepers:
file_name = file_dic['filename'].lower()
if re.search(r'sample', file_name):
continue
old_file_path = file_dic['filepath']
new_folder_path = os.path.join(type_path, self.series_name)
# override season_no on a per-episode basis
re_f_sxex = re.search(r's(\d\d)e(\d\d)', file_name)
re_f_exe = re.search(r'(\d\d)x(\d\d)', file_name)
if re_f_exe:
season_no = int(re_f_exe.group(1))
if re_f_sxex:
season_no = int(re_f_sxex.group(1))
if season_no:
season = 'season ' + str(season_no)
new_folder_path = os.path.join(new_folder_path, season)
new_file_path = os.path.join(new_folder_path, file_dic['filename'])
self.move_lst.append((old_file_path, new_file_path))
self.move_type_lst.append(file_dic['filetype'])

52
src/scripts/derailleur-add Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Torrent starter"""
import sys
import derailleur
def get_parsed_args():
'''Parses arguments and return them'''
parser = derailleur.args.start_parser()
parser.add_argument("torrent", nargs='*',
help="Magnet URL(s) or .torrent(s)")
parsed_args = derailleur.args.end_parser(parser)
return parsed_args
def start_torrent(config, client, logger, torrent):
'''For each torrent in nargs, hand it over'''
outcome = client.download(torrent, config['Add']['pause_when_added'])
if outcome:
logger.info(outcome)
else:
logger.error('ERROR : ' + 'Failed to add torrent')
def main():
'''Putting it all together'''
args = get_parsed_args()
config, validation = derailleur.config.get_config()
if validation['Connection'] is not True or validation['Add'] is not True:
sys.exit('ERROR : Missing configuration')
logger = derailleur.loggers.Logger('Add', config, validation)
client = derailleur.client.get_client(config)
if not client:
logger.error('ERROR : Connect to Transmission failed')
sys.exit(1)
for torrent in args.torrent:
start_torrent(config, client, logger, torrent)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass

81
src/scripts/derailleur-feed Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
import sys
import feedparser
import derailleur
def get_parsed_args():
'''Parses arguments and return them'''
parser = derailleur.args.start_parser()
parsed_args = derailleur.args.end_parser(parser)
return parsed_args
def get_feed(config):
'''Parses feed and returns dictionary of entries'''
doc = feedparser.parse(config['Feed']['url'])
get_edic = lambda x: {'title': x.title, 'url': x.link, 'done': False}
feed_dic = {exml.id: get_edic(exml) for exml in doc.entries}
return feed_dic
def get_fresh(feed_dic, jar_dic):
'''Go through feed entries'''
done_entries = [eid for eid in jar_dic if jar_dic[eid]['done'] is True]
fresh_entries = [eid for eid in feed_dic if eid not in done_entries]
return fresh_entries
def start_torrent(config, client, logger, feed_dic, eid):
'''Start an entry from the feed'''
edic = feed_dic[eid]
outcome = client.download(edic['url'], config['Feed']['pause_when_added'])
if outcome:
edic['done'] = True
logger.info(outcome)
else:
logger.error('ERROR : Failed to add %s', edic['title'])
return edic
def main():
'''Putting it all together'''
_args = get_parsed_args()
config, validation = derailleur.config.get_config()
# note that defaults should ensure always-valid configs but bad user
# input (e.g. a string for a port number) may invalidate it.
if validation['Connection'] is not True or validation['Feed'] is not True:
sys.exit('ERROR : Missing configuration')
logger = derailleur.loggers.Logger('Feed', config, validation)
client = derailleur.client.get_client(config)
if not client:
logger.error('ERROR : Connect to Transmission failed')
sys.exit(1)
feed_dic = get_feed(config)
if not feed_dic:
logger.error('ERROR : Cannot find feed or feed empty')
sys.exit(1)
jar = derailleur.history.Jar(config, 'derailleur-feed.db')
jar_dic = jar.load()
if jar_dic is False:
logger.error('ERROR : Failure loading %s', jar.db_filepath)
sys.exit(1)
fresh_entries = get_fresh(feed_dic, jar_dic)
for eid in fresh_entries:
edic = start_torrent(config, client, logger, feed_dic, eid)
jar_dic[eid] = edic
success = jar.save(jar_dic)
if success is False:
logger.error('ERROR : Failure saving %s', jar.db_filepath)
sys.exit(1)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass

47
src/scripts/derailleur-manager Executable file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
"""Torrent manager script"""
import sys
import derailleur
def get_parsed_args():
'''Parses arguments and return them'''
parser = derailleur.args.start_parser()
parser.add_argument("-v", "--verbose", help="Get info on current torrents",
action="store_true")
parsed_args = derailleur.args.end_parser(parser)
return parsed_args
def main():
'''Putting it all together'''
args = get_parsed_args()
config, validation = derailleur.config.get_config()
if validation['Connection'] is not True or \
validation['Manager'] is not True:
sys.exit('ERROR : Missing configuration')
logger = derailleur.loggers.Logger('Manager', config, validation,
verbose=args.verbose)
client = derailleur.client.get_client(config)
if not client:
logger.error('ERROR : Connect to Transmission failed')
sys.exit(1)
jar = derailleur.history.Jar(config, 'derailleur-manager.db')
manager = derailleur.manager.Manager(config, client, logger, jar)
manager.check_torrents()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass

124
src/scripts/derailleur-postproc Executable file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Mads Michelsen (mail@brokkr.net)
# This file is part of Derailleur.
# Derailleur is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
import os
import sys
from collections import namedtuple
import derailleur
try:
from xbmcjson import XBMC
import requests.packages.urllib3.exceptions
except ImportError:
XBMC = None
TorrentInfo = namedtuple('torrent_info', 'ppath name hash')
def get_parsed_args():
'''Parses arguments and return them'''
parser = derailleur.args.start_parser()
parser.add_argument("-t", "--target", help='''Set a target manually '''
'''rather than relying on environmental variables''')
parsed_args = derailleur.args.end_parser(parser)
return parsed_args
def get_torrent_info(args):
'''Get Transmission info on target'''
if args.target:
target = os.path.abspath(args.target)
if not os.path.isdir(target) and not os.path.isfile(target):
sys.exit('ERROR : Not a valid target')
get_pp = lambda x: os.path.abspath(os.path.join(x, os.path.pardir))
torrent_info = TorrentInfo(ppath=get_pp(target),
name=os.path.basename(target),
hash='abcdef123456')
else:
try:
torrent_info = TorrentInfo(ppath=os.environ['TR_TORRENT_DIR'],
name=os.environ['TR_TORRENT_NAME'],
hash=os.environ['TR_TORRENT_HASH'])
except KeyError:
torrent_info = None
return torrent_info
def get_type(config, keywords, stats):
'''Get torrent type'''
module_dic = {
'tv': derailleur.postproc.tv,
'film': derailleur.postproc.film,
'music': derailleur.postproc.audio,
'audiobook': derailleur.postproc.audio
}
if stats.identity in keywords:
type_module = derailleur.postproc.keywords
type_path = os.path.join(config['Postproc']['base_path'],
stats.identity)
elif stats.identity == 'generic':
type_module = derailleur.postproc.generic
type_path = os.path.join(config['Postproc']['base_path'], 'default')
else:
type_module = module_dic[stats.identity]
type_path = config['Postproc']['base_path_' + stats.identity]
return (type_module, type_path)
def get_log_str(torrent, stats, move_errors):
'''Logging'''
log_str = "ERRORS ENCOUNTERED IN FILE OPERATION. " if move_errors else ""
log_str += "NAME: " + stats.torrent_name + ". TYPE: " + stats.identity
log_str += ". FILES: "
type_count = [x + " " + str(torrent.move_type_lst.count(x))
for x in set(torrent.move_type_lst)]
log_str += ", ".join(type_count)
common_path = os.path.commonprefix([x[1] for x in torrent.move_lst])
log_str += ". PARENT: " + os.path.dirname(common_path)
return log_str
def update_kodi(config, stats):
'''Update kodi library'''
url = 'http://' + config['Kodi']['host'] + ':' \
+ str(config['Kodi']['port']) + '/jsonrpc'
try:
kodi = XBMC(url, config['Kodi']['user'], config['Kodi']['password'])
except requests.packages.urllib3.exceptions.NewConnectionError:
sys.exit(11)
if stats.identity in ['tv', 'film']:
kodi.VideoLibrary.Scan()
if stats.identity in ['music']:
kodi.AudioLibrary.Scan()
def main():
'''Putting the whole thing together'''
args = get_parsed_args()
config, validation = derailleur.config.get_config()
if validation['Postproc'] != True:
sys.exit('ERROR : Config not valid')
logger = derailleur.loggers.Logger('Postproc', config, validation)
torrent_info = get_torrent_info(args)
if not torrent_info:
logger.error('ERROR : Could not access Transmission variables')
sys.exit(1)
keywords = config['Postproc']['keywords']
stats = derailleur.postproc.stats.Stats(keywords, torrent_info)
type_module, type_path = get_type(config, keywords, stats)
torrent = type_module.Torrent(config, stats, type_path)
torrent.manipulate()
move_errors = torrent.move_files()
log_str = get_log_str(torrent, stats, move_errors)
logger.info(log_str)
if XBMC and config['Kodi']['connect_to_kodi']:
update_kodi(config, stats)
sys.exit(0)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass