Compare commits

...

10 Commits

11 changed files with 1094 additions and 1 deletions

1
.gitignore vendored
View File

@ -152,3 +152,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
Settings.cfg

25
MediaManager.py Normal file
View File

@ -0,0 +1,25 @@
import time
from Video import videoservice
from Universal import loggingservice
tmp_RunScript = True
tmp_Initialized = False
tmp_Debug = True
tmp_OutputFile = False
tmp_Library = 1
tmp_Resume = False
logger = loggingservice.Output(tmp_Debug, tmp_OutputFile)
while tmp_RunScript:
if not tmp_Initialized:
video_process = videoservice.VideoService()
tmp_Initialized = True
if tmp_Resume:
#TODO Implement Resume to VideoService
pass
else:
video_process.startNewProcess(tmp_Library)
tmp_Library += 1
print("Goodbye")

View File

@ -1,3 +1,3 @@
# Media-Manager
Sorts video content and if allowed uploads to server. For (Plex/Jellyfin)
Sorts video content and if allowed uploads to server. Designed Plex/Jellyfin servers in mind. But can be used for personal servers for movies and tv shows.

View File

@ -0,0 +1,140 @@
import time
class SFTPService:
def __init__(self, logger):
self.logger = logger
self.report(2, "Initializing")
try:
import paramiko
self.enabled = True
self.paramiko = paramiko
self.report(2, "API Found")
except ImportError:
self.enabled = False
self.report(3, "Module 'paramiko' was not found")
self.report(3, "Disabling SFTP features")
self.connected = False
def isEnabled(self):
self.report(2, "isEnabled")
return self.enabled
def isConnected(self):
self.report(2, "isConnected")
return self.connected
def connectSFTPServer(self, host, port, username, password, key_file, key_type):
self.report(2, "connectSFTPServer")
self.host = host
self.port = int(port)
self.username = username
self.password = password
self.key_file = key_file
self.key_type = key_type
tmp_key = None
if key_file != None:
if key_file.lower().find("dsa") != -1:
tmp_key = self.paramiko.DSAKey.from_private_key_file(key_file)
elif key_file.lower().find("rsa") != -1:
tmp_key = self.paramiko.RSAKey.from_private_key_file(key_file)
try:
tmp = self.paramiko.Transport((host, int(port)))
tmp.connect(None, username, password, tmp_key)
except:
self.connected = False
try:
self.sftp_socket = self.paramiko.SFTPClient.from_transport(tmp)
self.connected = True
return True
except:
self.connected = False
return False
def attemptReconnect(self, retries=1):
self.report(2, "attemptReconnect")
self.connected = False
if retries == 1:
self.report(1, "Lost connect to SFTP Server...")
if retries <= 5:
self.report(1, "Attempt #" + str(retries) + "... Attemping to reconnect in 15 seconds...")
self.closeConnection()
time.sleep(15)
if self.connectSFTPServer(self.host, self.port, self.username, self.password, self.key_file, self.key_type):
self.logger(0, "Successfully reconnected to SFTP Server")
return True
else:
return self.attemptReconnect(retries + 1)
else:
self.report(3, "Could not reconnect to SFTP Server...")
return False
def closeConnection(self):
self.report(2, "closeConnection")
if self.sftp_socket is not None:
self.sftp_socket.close()
self.connected = False
def getFileSize(self, path, retried=0):
self.report(2, "getFileSize")
try:
return int(str(self.sftp_socket.lstat(path)).split()[4])
except IOError:
return None
except:
if retried == 0:
if self.attemptReconnect():
return self.getFileSize(path, 1)
else:
return None
def doesFileExists(self, path):
self.report(2, "doesFileExists")
if self.getFileSize(path):
return True
else:
return False
def createFolder(self, path):
self.report(2, "createFolder")
tmp_full_path = path.split("/")
tmp_path = tmp_full_path[0]
for current_path in tmp_full_path:
if tmp_path != current_path:
tmp_path = tmp_path + "/" + current_path
try:
self.sftp_socket.mkdir(tmp_path)
except:
pass
def deleteFile(self, path):
self.report(2, "deleteFile")
try:
self.sftp_socket.remove(path)
return True
except:
self.report(3, "Failed to delete file")
def uploadFile(self, input_path, output_path, retried=0):
self.report(2, "uploadFile")
try:
self.createFolder(output_path.replace(output_path.split("/")[-1], ""))
if self.doesFileExists(output_path):
self.deleteFile(output_path)
return self.sftp_socket.put(input_path, output_path)
except:
if retried == 0:
if self.attemptReconnect():
return self.uploadFile(input_path, output_path, 1)
else:
return False
def report(self, level, message):
match level:
case 0:
self.logger.infoReport("[SFTP] " + message)
case 1:
self.logger.warningReport("[SFTP] " + message)
case 2:
self.logger.debugReport("[SFTP] " + message)
case 3:
self.logger.errorReport("[SFTP] " + message)

182
Universal/ioservice.py Normal file
View File

@ -0,0 +1,182 @@
import os
import stat
import time
import shutil
import platform
import configparser
M_DEFAULTCONFIG = "./Settings.cfg"
M_CONFIGPARSER = configparser.ConfigParser()
def normalizePath(path):
return os.path.normcase(path)
def doesFileExist(path):
return os.path.isfile(path)
def getPathSeperator():
if platform.system() == 'Windows':
return "\\"
else:
return "/"
def doesFolderExist(path):
path = normalizePath(path)
if os.path.exists(path):
return not doesFileExist(path)
return False
def getFileSize(path):
path = normalizePath(path)
if doesFileExist(path):
return os.path.getsize(path)
else:
return 0
def checkSizeChange(path):
path = normalizePath(path)
if doesFileExist(path):
tmp_size = getFileSize(path)
time.sleep(10)
return (tmp_size == getFileSize(path))
return False
def createFolder(path):
path = normalizePath(path)
if doesFolderExist(path):
return True
else:
try:
os.makedirs(path)
return True
except OSError:
return False
def deleteDirectory(path, attempt=True):
path = normalizePath(path)
if doesFileExist(path):
try:
os.remove(path)
except:
if attempt:
os.chmod(path, stat.S_IWRITE)
os.unlink(path)
deleteDirectory(path, False)
else:
return False
elif doesFolderExist(path):
try:
os.rmdir(path)
except:
if attempt:
os.chmod(path, stat.S_IWRITE)
os.unlink(path)
deleteDirectory(path, False)
else:
return False
return True
def moveFile(input_path, output_path, copy=False):
input_path = normalizePath(input_path)
output_path = normalizePath(output_path)
folder_created = False
if platform.system() == 'Windows':
folder_created = createFolder(output_path.rsplit("\\", 1)[0])
else:
folder_created = createFolder(output_path.rsplit("/", 1)[0])
if folder_created:
try:
if copy:
shutil.copyfile(input_path, output_path)
else:
shutil.move(input_path, output_path)
return True
except shutil.Error:
if copy:
print("Copying failed...")
else:
print("Moving failed...")
return False
except:
if doesFileExist(output_path):
if getFileSize(input_path) == getFileSize(output_path):
if not copy:
try:
deleteDirectory(input_path)
except:
print("Successfully copied file but could not delete input file...")
else:
if copy:
print("Failed to copy file")
else:
print("Failed to move file")
return False
def listAllFiles(path):
path = normalizePath(path)
try:
tmp_paths = os.listdir(path)
tmp_files = []
for files in tmp_paths:
tmp_current = os.path.join(path, files)
if doesFolderExist(tmp_current):
tmp_files = tmp_files + listAllFiles(tmp_current)
elif doesFileExist(tmp_current):
tmp_files.append(tmp_current)
return tmp_files
except OSError:
return None
def deleteEmptyFolders(path, delroot=False):
path = normalizePath(path)
if not os.path.isdir(path):
return
tmp_files = os.listdir(path)
if tmp_files:
for files in tmp_files:
tmp_fullpath = os.path.join(path, files)
if os.path.isdir(tmp_fullpath):
deleteEmptyFolders(tmp_fullpath, True)
tmp_files = os.listdir(path)
if not tmp_files and delroot:
os.rmdir(path)
def addConfiguration(section, option, key=""):
if not doesFileExist(M_DEFAULTCONFIG):
M_CONFIGPARSER.add_section(section)
M_CONFIGPARSER.set(section, option, key)
else:
M_CONFIGPARSER.read(M_DEFAULTCONFIG)
if not M_CONFIGPARSER.has_section(section):
M_CONFIGPARSER.add_section(section)
if not M_CONFIGPARSER.has_option(section, option):
M_CONFIGPARSER.set(section, option, key)
with open(M_DEFAULTCONFIG, 'w') as tmp:
M_CONFIGPARSER.write(tmp)
def setConfiguration(section, option, key=""):
if not doesFileExist(M_DEFAULTCONFIG):
addConfiguration(section, option, key)
else:
M_CONFIGPARSER.read(M_CONFIGPARSER)
if not M_CONFIGPARSER.has_section(section):
M_CONFIGPARSER.add_section(section)
M_CONFIGPARSER.set(section, option, key)
with open(M_DEFAULTCONFIG, 'w') as tmp:
M_CONFIGPARSER.write(tmp)
def getConfigurationStr(section, option):
if doesFileExist(M_DEFAULTCONFIG):
M_CONFIGPARSER.read(M_DEFAULTCONFIG)
if M_CONFIGPARSER.has_section(section) and M_CONFIGPARSER.has_option(section, option):
tmp_key = M_CONFIGPARSER.get(section, option)
if tmp_key != "" or tmp_key.lower().find("none") != -1:
return tmp_key
return None
def getConfigurationBool(section, option):
if doesFileExist(M_DEFAULTCONFIG):
M_CONFIGPARSER.read(M_DEFAULTCONFIG)
if M_CONFIGPARSER.has_section(section) and M_CONFIGPARSER.has_option(section, option):
return M_CONFIGPARSER.getboolean(section, option)
return None

View File

@ -0,0 +1,58 @@
import time
class Debugging:
def __init__(self, state):
self.state = state
def getDebugState(self):
return self.state
def setDebugState(self, state):
self.state = state
class Logging:
def __init__(self, state, directory):
self.state = state
self.directory = directory
def setState(self, state):
self.state = state
def setDirectory(self, directory):
self.directory = directory
def outputFile(self, message):
pass
class Output:
def __init__(self, d_state, l_state, l_directory="./Output.log"):
self.debug = Debugging(d_state)
self.log = Logging(l_state, l_directory)
def infoReport(self, message, console=True):
if console:
print("[INFO] " + message)
if self.log.state:
self.log.outputFile("[INFO] " + message)
def warningReport(self, message, console=True):
if console:
print("[WARNING] " + message)
if self.log.state:
self.log.outputFile("[WARNING] " + message)
def debugReport(self, message, console=True):
if console and self.debug.getDebugState():
print("[DEBUG] " + message)
if self.log.state:
self.log.outputFile("[DEBUG] " + message)
def errorReport(self, message, console=True, quit=False):
if console:
print("[ERROR] " + message)
if self.log.state:
self.log.outputFile("[ERROR] " + message)
if quit:
time.sleep(10)
quit()

13
Universal/tools.py Normal file
View File

@ -0,0 +1,13 @@
def replace(input_str, search_key, replace_str, checkspaces=False):
if replace_str is None:
input_str = replace(input_str, search_key, "", checkspaces)
else:
while input_str.find(search_key) != -1:
if checkspaces:
if input_str[input_str.index(search_key) - 1] != " ":
input_str = input_str.replace(search_key, " " + replace_str)
else:
input_str = input_str.replace(search_key, replace_str)
else:
input_str = input_str.replace(search_key, replace_str)
return input_str

View File

@ -0,0 +1,110 @@
from Universal import ioservice
class TMDBService:
def __init__(self, loggingservice):
self.logger = loggingservice
self.report(2, "Initializing")
try:
import tmdbsimple
self.tmdb = tmdbsimple
self.report(2, "API Found")
self.enabled = True
self.tmdb_key = None
self.addDefaultConfiguration()
except ImportError:
self.report(3, "Module 'tmdbsimple' was not found")
self.report(3, "Disabling TMDB API")
self.enabled = False
self.connected = False
def addDefaultConfiguration(self):
ioservice.addConfiguration("TMDB Service", "TMDB API Key", "NONE")
def updateConfiguration(self):
if self.tmdb_key == None:
self.tmdb_key = ioservice.getConfigurationStr("TMDB Service", "TMDB API Key")
def report(self, level, message):
match level:
case 0:
self.logger.infoReport("[TMDB] " + message)
case 1:
self.logger.warningReport("[TMDB] " + message)
case 2:
self.logger.debugReport("[TMDB] " + message)
case 3:
self.logger.errorReport("[TMDB] " + message)
def isEnabled(self):
return self.enabled
def isConnected(self):
self.report(2, "isConnected")
return self.connected
def connectToService(self):
self.report(2, "connectToService")
if not self.connected:
try:
self.report(2, "Attempting to Login")
self.updateConfiguration()
self.tmdb.API_KEY = self.tmdb_key
self.connected = True
self.report(2, "Connected")
except self.tmdb.APIKeyError:
self.report(3, "API Key is incorrect")
self.report(3, "Disabling TMDB API")
self.enabled = False
else:
self.report(2, "Already connected")
def searchMovie(self, name):
self.report(2, "searchMovie")
tmp_search = self.tmdb.Search()
tmp_response = tmp_search.movie(query=name)
tmp_list = []
for movie in tmp_search.results:
tmp_runtime = self.tmdb.Movies(movie['id']).info()['runtime']
try:
tmp_releasedate = movie['release_date']
except:
tmp_releasedate = 9999
if not tmp_runtime or tmp_runtime is None:
tmp_runtime = 0
if not tmp_releasedate or tmp_releasedate is None:
tmp_releasedate = 9999
tmp_list.append([movie['title'], int(tmp_runtime), int(tmp_releasedate[:4])])
return tmp_list
def getBestMovie(self, name, length=None, year=None):
self.report(2, "getBestMovie")
tmp_movies = self.searchMovie(name)
if not year and not length:
self.report(0, "Does not know year or runtime... Selecting first response.")
return tmp_movies[0]
elif not year:
self.report(0, "Got runtime but no year. Selecting closest match based on runtime.")
tmp_time = []
for movies in tmp_movies:
if movies[1] == None:
movies[1] = 0
tmp_time.append(movies[1])
return tmp_movies[min(enumerate(tmp_time), key=lambda x:abs(x[1]-length))[0]]
else:
tmp_year = []
for movies in tmp_movies:
if movies[2] == None:
movies[2] = 9999
tmp_year.append(movies[2])
return tmp_movies[min(enumerate(tmp_year), key=lambda x:abs(x[1]-year))[0]]
def getShowName(self, name):
self.report(2, "getShowName")
tmp_search = self.tmdb.Search()
tmp_response = tmp_search.tv(query=name)
return tmp_search.results[0]
def getEpisodeName(self, name, season, episode):
self.report(2, "getEpisodeName")
self.tmdb.Search().tv(query=name)
return self.tmdb.TV_Episodes(self.tmdb.results[0]['id'], season, episode).info()['name']

View File

@ -0,0 +1,112 @@
from Universal import ioservice
class TMDBService:
def __init__(self, loggingservice):
self.logger = loggingservice
self.report(2, "Initializing")
try:
import tvdb_api
from tvdb_api import tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_error
from tvdb_api import tvdb_notauthorized, tvdb_shownotfound
self.tvdb = tvdb_api
self.report(2, "API Found")
self.enabled = True
self.tvdb_key = None
self.addDefaultConfiguration()
except ImportError:
self.report(3, "Module 'tvdb_api' was not found")
self.report(3, "Disabling TVDB API")
self.enabled = False
self.connected = False
def addDefaultConfiguration(self):
ioservice.addConfiguration("TVDB Service", "TVDB API Key", "NONE")
def updateConfiguration(self):
if self.tvdb_key == None:
self.tvdb_key = ioservice.getConfigurationStr("TVDB Service", "TVDB API Key")
def report(self, level, message):
match level:
case 0:
self.logger.infoReport("[TVDB] " + message)
case 1:
self.logger.warningReport("[TVDB] " + message)
case 2:
self.logger.debugReport("[TVDB] " + message)
case 3:
self.logger.errorReport("[TVDB] " + message)
def isEnabled(self):
return self.enabled
def isConnected(self):
self.report(2, "isConnected")
return self.connected
def connectToService(self):
self.report(2, "connectToService")
if not self.connected:
try:
self.report(2, "Attempting to Login")
self.updateConfiguration()
self.tvdb = tvdb_api.Tvdb(apikey=self.tvdb_key)
self.connected = True
self.report(2, "Connected")
except self.tmdb.APIKeyError:
self.report(3, "API Key is incorrect")
self.report(3, "Disabling TMDB API")
self.enabled = False
else:
self.report(2, "Already connected")
def searchMovie(self, name):
self.report(2, "searchMovie")
tmp_search = self.tmdb.Search()
tmp_response = tmp_search.movie(query=name)
tmp_list = []
for movie in tmp_search.results:
tmp_runtime = self.tmdb.Movies(movie['id']).info()['runtime']
try:
tmp_releasedate = movie['release_date']
except:
tmp_releasedate = 9999
if not tmp_runtime or tmp_runtime is None:
tmp_runtime = 0
if not tmp_releasedate or tmp_releasedate is None:
tmp_releasedate = 9999
tmp_list.append([movie['title'], int(tmp_runtime), int(tmp_releasedate[:4])])
return tmp_list
def getBestMovie(self, name, length=None, year=None):
self.report(2, "getBestMovie")
tmp_movies = self.searchMovie(name)
if not year and not length:
self.report(0, "Does not know year or runtime... Selecting first response.")
return tmp_movies[0]
elif not year:
self.report(0, "Got runtime but no year. Selecting closest match based on runtime.")
tmp_time = []
for movies in tmp_movies:
if movies[1] == None:
movies[1] = 0
tmp_time.append(movies[1])
return tmp_movies[min(enumerate(tmp_time), key=lambda x:abs(x[1]-length))[0]]
else:
tmp_year = []
for movies in tmp_movies:
if movies[2] == None:
movies[2] = 9999
tmp_year.append(movies[2])
return tmp_movies[min(enumerate(tmp_year), key=lambda x:abs(x[1]-year))[0]]
def getShowName(self, name):
self.report(2, "getShowName")
tmp_search = self.tmdb.Search()
tmp_response = tmp_search.tv(query=name)
return tmp_search.results[0]
def getEpisodeName(self, name, season, episode):
self.report(2, "getEpisodeName")
self.tmdb.Search().tv(query=name)
return self.tmdb.TV_Episodes(self.tmdb.results[0]['id'], season, episode).info()['name']

87
Video/default.py Normal file
View File

@ -0,0 +1,87 @@
from Universal import ioservice
def generateConfigs():
generateProcessorConfigs()
generateServerConfig()
generateVideoConfig()
def generateProcessorConfigs():
ioservice.addConfiguration("Video Configuration", "FFProbe Path")
ioservice.addConfiguration("Video Configuration", "Encoder Path")
ioservice.addConfiguration("Video Configuration", "Sleep Timer (Seconds)", "100")
ioservice.addConfiguration("Video Configuration", "Blacklist Words", "1080p, 720p, 480p, 360p, x265, x264")
def generateServerConfig():
ioservice.addConfiguration("Server 1", "Type", "NONE")
ioservice.addConfiguration("Server 1", "Host", "127.0.0.1")
ioservice.addConfiguration("Server 1", "Port", "22")
ioservice.addConfiguration("Server 1", "Username", "NONE")
ioservice.addConfiguration("Server 1", "Password", "NONE")
ioservice.addConfiguration("Server 1", "Key File", "NONE")
ioservice.addConfiguration("Server 1", "Key Type (RSA/DSA)", "RSA")
ioservice.addConfiguration("Server 1", "Media Root", "/Server/Drive/Media/")
def generateVideoConfig():
ioservice.addConfiguration("Video Library 1", "Name", "TV Shows")
ioservice.addConfiguration("Video Library 1", "Input", "./Input/TV-Shows/")
ioservice.addConfiguration("Video Library 1", "Output", "./Output/TV-Shows/")
ioservice.addConfiguration("Video Library 1", "Database Service", "TMDB")
ioservice.addConfiguration("Video Library 1", "Media Type", "SHOWS")
ioservice.addConfiguration("Video Library 1", "Compress Video", "False")
ioservice.addConfiguration("Video Library 1", "Output Format", "mkv")
ioservice.addConfiguration("Video Library 1", "Directory Format", "/${NAME}/Season ${SEASON}/${NAME} S${SEASON}E${EPISODE}.${FORMAT}")
ioservice.addConfiguration("Video Library 1", "Command", "${COMPRESSOR} -i ${INPUT} -o ${OUTPUT}")
ioservice.addConfiguration("Video Library 1", "Subtitle Command", "--srt-file ${SRT_FILE} --srt-lang ${SRT_LANGUAGE}")
ioservice.addConfiguration("Video Library 1", "Delete Input", "False")
ioservice.addConfiguration("Video Library 1", "Server", "None")
ioservice.addConfiguration("Video Library 1", "Server Path", "/TV Shows/${NAME}/Season ${SEASON}/${NAME} S${SEASON}E${EPISODE}.${FORMAT}")
ioservice.addConfiguration("Video Library 1", "Server Overwrite", "False")
class VideoSettings:
def __init__(self):
generateConfigs()
self.settings = {}
self.library = {}
self.server = {}
def updateSettings(self):
tmp_ffprobe = ioservice.getConfigurationStr("Video Configuration", "FFProbe Path")
tmp_encoder = ioservice.getConfigurationStr("Video Configuration", "Encoder Path")
tmp_timer = ioservice.getConfigurationStr("Video Configuration", "Sleep Timer (Seconds)")
tmp_blacklist = ioservice.getConfigurationStr("Video Configuration", "Blacklist Words")
self.settings = {"FFProbe": tmp_ffprobe, "Encoder": tmp_encoder, "Timer": tmp_timer,
"Blacklist": tmp_blacklist}
def updateLibrary(self, num):
tmp_section = ("Video Library " + str(num))
tmp_name = ioservice.getConfigurationStr(tmp_section, "Name")
tmp_input = ioservice.getConfigurationStr(tmp_section, "Input")
tmp_output = ioservice.getConfigurationStr(tmp_section, "Output")
tmp_database = ioservice.getConfigurationStr(tmp_section, "Database Service")
tmp_type = ioservice.getConfigurationStr(tmp_section, "Media Type")
tmp_compress = ioservice.getConfigurationBool(tmp_section, "Compress Video")
tmp_out_format = ioservice.getConfigurationStr(tmp_section, "Output Format")
tmp_dir_format = ioservice.getConfigurationStr(tmp_section, "Directory Format")
tmp_command = ioservice.getConfigurationStr(tmp_section, "Command")
tmp_srt_command = ioservice.getConfigurationStr(tmp_section, "Subtitle Command")
tmp_del_input = ioservice.getConfigurationBool(tmp_section, "Delete Input")
tmp_server = ioservice.getConfigurationStr(tmp_section, "Server")
tmp_server_path = ioservice.getConfigurationStr(tmp_section, "Server Path")
tmp_server_overwrite = ioservice.getConfigurationBool(tmp_section, "Server Overwrite")
self.library = {"Name": tmp_name, "Input": tmp_input, "Output": tmp_output, "Database": tmp_database,
"Type": tmp_type, "Compress": tmp_compress, "Format": tmp_out_format, "Directory": tmp_dir_format,
"Command": tmp_command, "SRT Command": tmp_srt_command, "Delete Input": tmp_del_input,
"Server": tmp_server, "Server Path": tmp_server_path, "Server Overwrite": tmp_server_overwrite}
def updateServer(self, num):
tmp_section = ("Server " + str(num))
tmp_type = ioservice.getConfigurationStr(tmp_section, "Type")
tmp_host = ioservice.getConfigurationStr(tmp_section, "Host")
tmp_port = ioservice.getConfigurationStr(tmp_section, "Port")
tmp_username = ioservice.getConfigurationStr(tmp_section, "Username")
tmp_password = ioservice.getConfigurationStr(tmp_section, "Password")
tmp_keyfile = ioservice.getConfigurationStr(tmp_section, "Key File")
tmp_keytype = ioservice.getConfigurationStr(tmp_section, "Key Type (RSA/DSA)")
tmp_root = ioservice.getConfigurationStr(tmp_section, "Media Root")
self.server = {"Type": tmp_type, "Host": tmp_host, "Port": tmp_port, "Username": tmp_username,
"Password": tmp_password, "Key File": tmp_keyfile, "Key Type": tmp_keytype, "Root": tmp_root}

365
Video/videoservice.py Normal file
View File

@ -0,0 +1,365 @@
import os
import re
import time
import subprocess
from Video import default
from Universal import tools
from Universal import ioservice
from Universal import loggingservice
from Video.Services import tmdbservice
from Universal.network import sftpservice
class VideoService:
def __init__(self):
self.logger = loggingservice.Output(True, True)
self.video_settings = default.VideoSettings()
# Movie/TV Show Database
self.tmdbservice = tmdbservice.TMDBService(self.logger)
# Network Service
self.sftpservice = sftpservice.SFTPService(self.logger)
self.failedFiles = []
self.video_settings.updateSettings()
def startNewProcess(self, num=1):
self.logger.debugReport("[Video] startNewProcess")
self.current_library = num
self.total_processed = 0
self.is_processing = True
self.video_settings.updateLibrary(self.current_library)
if self.video_settings.library["Server"].lower().find("none") == -1:
self.video_settings.updateServer(self.video_settings.library["Server"])
while self.is_processing:
self.processLibraries()
self.startNewProcess(self.current_library)
def processLibraries(self, refreshLib=True):
self.logger.debugReport("[Video] processLibraries")
tmp_library_time = time.time()
if self.video_settings.library is not None:
if self.video_settings.library["Input"] is not None:
if refreshLib:
self.total_processed = 0
time.sleep(2)
self.logger.infoReport("[Video] Current Library: " + self.video_settings.library["Name"])
self.current_library_list = ioservice.listAllFiles(self.video_settings.library["Input"])
if self.current_library_list is not None:
self.current_library_list = sorted(self.current_library_list)
if self.current_library_list is not None:
if refreshLib:
self.srt_list = []
for item_search in self.current_library_list:
if item_search.lower().find(".srt") != -1:
self.srt_list.append(item_search)
if self.srt_list is not None:
for item_search in self.srt_list:
self.current_library_list.remove(item_search)
if len(self.current_library_list) == 0:
self.is_processing = False
else:
self.logger.infoReport("[Video] Found a total of " + str(len(self.current_library_list)) + " files")
for self.current_file in self.current_library_list:
if self.total_processed != 0:
self.logger.infoReport("[Video] Total file remaining: " + str(len(self.current_library_list) - self.total_processed))
self.processVideo()
else:
self.logger.infoReport("[Video] Folder is empty")
ioservice.deleteEmptyFolders(self.video_settings.library["Input"])
else:
self.is_processing = False
else:
self.is_processing = False
self.logger.infoReport("[Video] Current Library: " + self.video_settings.library["Name"] + " Time Elapsed: " + time.strftime("%H:%M:%S", time.gmtime(time.time() - tmp_library_time)))
self.current_library += 1
def processVideo(self):
self.logger.debugReport("[Video] processVideo")
self.logger.infoReport("[Video] Current File: " + self.current_file)
tmp_isDeleted = False
tmp_isSuccess = True
tmp_serverPath = None
if not ioservice.checkSizeChange(self.current_file):
if ioservice.getFileSize(self.current_file) == 0:
self.logger.infoReport("[Video] Assuming file has been deleted or moved... Skipping...")
tmp_isDeleted = True
else:
self.logger.infoReport("[Video] File size has changed")
self.processVideo()
else:
if not tmp_isDeleted:
tmp_currentFileDetails = self.extractVideoInformation(self.current_file)
self.constructOutputPath(tmp_currentFileDetails)
if tmp_currentFileDetails["Episode"] is None and tmp_currentFileDetails["Season"] is None:
self.logger.infoReport("[Video] " + tmp_currentFileDetails["Name"] + " " + str(tmp_currentFileDetails["Year"]))
else:
self.logger.infoReport("[Video] " + tmp_currentFileDetails["Name"] + " Season: " + str(tmp_currentFileDetails["Season"]) + " Episode: " + str(tmp_currentFileDetails["Episode"]))
self.findSRTs()
tmp_compress = False
if not self.video_settings.library["Server"].lower().find("none") != -1:
self.video_settings.updateServer(self.video_settings.library["Server"])
tmp_serverPath = self.constructServerPath()
if self.video_settings.server["Type"].lower().find("sftp") != -1:
if self.sftpservice.isEnabled():
if not self.sftpservice.isConnected():
self.sftpservice.connectSFTPServer(self.video_settings.server["Host"], self.video_settings.server["Port"], self.video_settings.server["Username"],
self.video_settings.server["Password"], self.video_settings.server["Key File"], self.video_settings.server["Key Type"])
if self.sftpservice.isConnected():
if not ioservice.doesFileExist(self.currentFileOutputPath):
if self.video_settings.library["Server Overwrite"]:
tmp_compress = True
else:
if not self.sftpservice.doesFileExists(tmp_serverPath):
tmp_compress = True
else:
if not ioservice.doesFileExist(self.currentFileOutputPath):
tmp_compress = True
if tmp_compress and not self.video_settings.library["Compress"]:
if self.video_settings.library["Delete Input"]:
self.logger.infoReport("[Video] Moving file")
self.is_processing = True
ioservice.moveFile(self.current_file, self.currentFileOutputPath)
i = 0
while i < len(self.srt_to_include):
ioservice.moveFile(self.srt_to_include[i], self.constructOutputPath(tmp_currentFileDetails, True, i))
i += 1
self.is_processing = False
tmp_isSuccess = True
self.logger.infoReport("[Video] Move complete")
else:
self.logger.infoReport("[Video] Copying file")
self.is_processing = True
ioservice.moveFile(self.current_file, self.currentFileOutputPath, True)
i = 0
while i < len(self.srt_to_include):
ioservice.moveFile(self.srt_to_include[i], self.constructOutputPath(tmp_currentFileDetails, True, i), True)
i += 1
self.is_processing = False
tmp_isSuccess = True
self.logger.infoReport("[Video] Copying complete")
elif tmp_compress and self.video_settings.library["Compress"]:
self.logger.infoReport("[Video] Compressing video")
self.startCompressing(self.current_file, self.currentFileOutputPath, self.srt_to_include)
tmp_isSuccess = True
self.logger.infoReport("[Video] Compressing completed")
if not self.video_settings.library["Server"].lower().find("none") != -1:
self.video_settings.updateServer(self.video_settings.library["Server"])
if self.video_settings.server["Type"].lower().find("sftp") != -1:
if self.sftpservice.isEnabled():
if not self.sftpservice.isConnected():
self.sftpservice.connectSFTPServer(self.video_settings.server["Host"], self.video_settings.server["Port"], self.video_settings.server["Username"],
self.video_settings.server["Password"], self.video_settings.server["Key File"], self.video_settings.server["Key Type"])
if self.sftpservice.isConnected():
if self.sftpservice.doesFileExists(tmp_serverPath):
if self.video_settings.library["Server Overwrite"]:
if self.sftpservice.getFileSize(tmp_serverPath) != ioservice.getFileSize(self.currentFileOutputPath):
if self.sftpservice.doesFileExist(tmp_serverPath):
self.sftpservice.deleteFile(tmp_serverPath)
if self.sftpservice.uploadFile(self.currentFileOutputPath, tmp_serverPath):
tmp_isSuccess = True
self.logger.infoReport("[Video] Upload Complete")
else:
tmp_isSuccess = False
else:
if self.sftpservice.uploadFile(self.currentFileOutputPath, tmp_serverPath):
tmp_isSuccess = True
self.logger.infoReport("[Video] Upload Complete")
else:
tmp_isSuccess = False
else:
if self.sftpservice.uploadFile(self.currentFileOutputPath, tmp_serverPath):
tmp_isSuccess = True
else:
tmp_isSuccess = False
if not tmp_isSuccess:
self.failedFiles = self.failedFiles + [self.current_file]
else:
self.total_processed += 1
def extractVideoInformation(self, file):
self.logger.debugReport("[Video] extractVideoInformation")
file = file.replace(self.video_settings.library["Input"], "").lstrip().replace("_", "")
for blacklist_item in self.video_settings.settings["Blacklist"].split(", "):
file = file.replace(blacklist_item, "")
tmp_input_format = file.rsplit(".", 1)[-1]
tmp_name = None
tmp_season = None
tmp_episode = None
tmp_episode_title = None
tmp_year = None
tmp_episode_year = None
tmp_length = self.getVideoLength()
tmp_folders, tmp_file = os.path.split(file)
for possible_years in re.findall(r'\d+', file):
if len(possible_years) == 4:
tmp_year = int(possible_years)
match len(tmp_folders.split(ioservice.getPathSeperator())):
case 1:
tmp_name = tmp_folders.split(ioservice.getPathSeperator())[0]
if not tmp_name:
tmp_name = tmp_file.replace(str(tmp_year), "").replace("." + tmp_input_format, "").strip()
case 2:
tmp_name = tmp_folders.split(ioservice.getPathSeperator())[0]
case 3:
tmp_name = tmp_folders.split(ioservice.getPathSeperator())[0]
case _:
tmp_name = tmp_file.replace("." + tmp_input_format, "")
if self.video_settings.library["Type"].lower().find("show") != -1:
if tmp_season is None or tmp_episode is None:
match len(tmp_folders.split(ioservice.getPathSeperator())):
case 2:
tmp_season = re.findall(r'\d+', tmp_folders.replace(tmp_name, ""))[0]
tmp_episode = re.findall(r'([0-9]*[0-9])', tmp_file.replace("." + tmp_input_format, "").replace(tmp_name, ""))[-1]
if tmp_file.lower().find(tmp_episode + "a") != -1:
tmp_episode = tmp_episode + " - Part 1"
elif tmp_file.lower().find(tmp_episode + "b") != -1:
tmp_episode = tmp_episode + " - Part 2"
case 3:
tmp_season = re.findall(r'\d+', tmp_folders.replace(tmp_name, ""))[0]
tmp_episode = re.findall(r'([0-9]*[0-9])', tmp_folders.replace("." + tmp_input_format, "").replace(tmp_name, ""))[-1]
if tmp_file.lower().find(tmp_episode + "a") != -1:
tmp_episode = tmp_episode + " - Part 1"
elif tmp_file.lower().find(tmp_episode + "b") != -1:
tmp_episode = tmp_episode + " - Part 2"
case _:
tmp_episode = re.findall(r'\d+', tmp_file.replace("." + tmp_input_format, "").replace(tmp_name, ""))[-1]
try:
tmp_season = re.findall(r'\d+', tmp_file.replace("." + tmp_input_format, "").replace(tmp_name, ""))[0]
except:
tmp_episode = re.findall(r'\d+', tmp_file.replace("." + tmp_input_format, ""))[-1]
if tmp_season != "0" and tmp_season:
tmp_season = tmp_season.lstrip("0")
if tmp_episode != "0" and tmp_episode:
tmp_episode = tmp_episode.lstrip("0")
match self.video_settings.library["Database"].lower():
case "tmdb":
if self.tmdbservice.isEnabled():
self.tmdbservice.connectToTMDB()
if self.video_settings.library["Type"].lower().find("movie") != -1:
tmp_name = tmp_name.replace(str(tmp_year), "").replace("(", "").replace(")", "")
if tmp_length is not None or tmp_year is not None:
tmp_data = self.tmdbservice.getBestMovie(tmp_name, tmp_length, tmp_year)
if tmp_data:
tmp_name = tmp_data[0]
tmp_year = tmp_data[2]
else:
tmp_data = self.tmdbservice.searchMovies(tmp_name)
tmp_name = tmp_data[0][0]
tmp_year = tmp_data[0][1]
elif self.video_settings.library["Type"].lower().find("show") != -1:
tmp_data = self.tmdbservice.getShowName(tmp_name)
tmp_name = tmp_data["name"]
tmp_year = tmp_data["first_air_date"][:4]
case "tvdb":
#TODO: Implement
pass
case _:
#TODO: Implement
pass
tmp_name = tmp_name.replace(":", "")
if tmp_episode_title:
tmp_episode_title = tmp_episode_title.replace(":", "")
return {"Name": tmp_name, "Year": tmp_year, "Season": tmp_season, "Episode": tmp_episode, "Episode Title": tmp_episode_title, "Episode Year": tmp_episode_year, "Format": tmp_input_format}
def constructOutputPath(self, file_info, subtitles=False, num=0):
self.logger.debugReport("[Video] constructOutputPath")
self.currentFileOutputPath = self.video_settings.library["Output"] + self.video_settings.library["Directory"]
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${NAME}", file_info["Name"])
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${YEAR}", file_info["Year"])
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${SEASON}", file_info["Season"])
if isinstance(file_info["Episode"], list):
tmp_episodes = None
for eps in file_info["Episode"]:
if eps != file_info["Episode"][-1]:
if not tmp_episodes:
tmp_episodes = str(eps) + "-"
else:
tmp_episodes = tmp_episodes + str(eps) + "-"
else:
if not tmp_episodes:
tmp_episodes = str(eps)
else:
tmp_episodes = tmp_episodes + str(eps)
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${EPISODE}", tmp_episodes)
else:
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${EPISODE}", file_info["Episode"])
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${EPISODE_YEAR}", file_info["Episode Year"])
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${EPISODE_NAME}", file_info["Episode Title"])
if not subtitles:
if self.currentFileOutputPath[self.currentFileOutputPath.index("${FORMAT}") - 1] != ".":
if file_info["Format"].startswith("."):
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${FORMAT}", file_info["Format"])
else:
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${FORMAT}", "." + file_info["Format"])
else:
if file_info["Format"].startswith("."):
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, ".${FORMAT}", file_info["Format"])
else:
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, ".${FORMAT}", "." + file_info["Format"])
else:
if self.currentFileOutputPath[self.currentFileOutputPath.index("${FORMAT}") - 1] != ".":
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, "${FORMAT}", " - " + self.srt_language[num] + ".srt")
elif self.currentFileOutputPath[self.currentFileOutputPath.index("${FORMAT}" - 1)] == ".":
self.currentFileOutputPath = tools.replace(self.currentFileOutputPath, ".${FORMAT}", " - " + self.srt_language[num] + ".srt")
self.logger.debugReport("[Video] Output Path: " + self.currentFileOutputPath)
return self.currentFileOutputPath
def startCompressing(input_path, output_path, srt_list):
#TODO: Implement
pass
def constructServerPath(self):
#TODO: Implement
pass
def findSRTs(self):
self.logger.debugReport("[Video] findSRTs")
tmp_srt_lang = ["aar", "abk", "afr", "aka", "alb", "amh", "ara", "arg", "arm", "asm", "ava", "ave", "aym", "aze", "bak", "bam", "baq",
"bel", "ben", "bih", "bis", "bos", "bre", "bul", "bur", "cat", "cha", "che", "chi", "chu", "chv", "cor", "cos", "cre",
"cze", "dan", "div", "dut", "dzo", "eng", "epo", "est", "ewe", "fao", "fij", "fin", "fre", "fry", "ful", "geo", "ger",
"gla", "gle", "glg", "glv", "gre", "grn", "guj", "hat", "hau", "heb", "her", "hin", "hmo", "hrv", "hun", "ibo", "ice",
"ido", "iii", "iku", "ile", "ina", "ind", "ipk", "ita", "jav", "jpn", "kal", "kan", "kas", "kau", "kaz", "khm", "kik",
"kin", "kir", "kom", "kon", "kor", "kua", "kur", "lao", "lat", "lav", "lim", "lin", "lit", "ltz", "lub", "lug", "mac",
"mah", "mal", "mao", "mar", "may", "mlg", "mlt", "mon", "nau", "nav", "nbl", "nde", "ndo", "nep", "nno", "nob", "nor",
"nya", "oci", "oji", "ori", "orm", "oss", "pan", "per", "pli", "pol", "por", "pus", "que", "roh", "rum", "run", "rus",
"sag", "san", "sin", "slo", "slv", "sme", "smo", "sna", "snd", "som", "sot", "spa", "srd", "srp", "ssw", "sun", "swa",
"swe", "tah", "tam", "tat", "tel", "tgk", "tgl", "tha", "tib", "tir", "ton", "tsn", "tso", "tuk", "tur", "twi", "uig",
"ukr", "urd", "uzb", "ven", "vie", "vol", "wel", "wln", "wol", "xho", "yid", "yor", "zha", "zul"]
self.srt_to_include = []
self.srt_language = []
tmp_srt = self.current_file.replace(self.current_file.rsplit(".")[-1], "srt").lower()
if (ioservice.doesFileExist(tmp_srt)):
self.srt_to_include.append(tmp_srt)
self.srt_language.append("eng")
else:
for possible_srt in tmp_srt_lang:
tmp_srt = self.current_file.replace(self.current_file.rsplit(".")[-1], "")[:-1].lower()
tmp_srt = tmp_srt + "-" + possible_srt + ".srt"
if (ioservice.doesFileExist(tmp_srt)):
self.srt_to_include.append(tmp_srt)
self.srt_language.append(possible_srt)
def getVideoLength(self):
self.logger.debugReport("[Video] getVideoLength")
tmp_time = None
try:
tmp_time = subprocess.check_output([self.video_settings.settings["FFProbe"], '-i', self.current_file, '-show_entries', 'format=duration', '-v', 'quiet', '-of', 'csv=%s' % ("p=0")])
tmp_time = str(tmp_time)
if not tmp_time.find("N/A") != -1:
tmp_hours = time.strftime("%H", time.gmtime(round(float(re.findall('\d+', tmp_time)[0])))).lstrip("0")
tmp_minutes = time.strftime("%M", time.gmtime(round(float(re.findall('\d+', tmp_time)[0])))).lstrip("0")
try:
tmp_hours = int(tmp_hours) * 60
except:
tmp_hours = 0
try:
tmp_length = tmp_hours + int(tmp_minutes)
except:
tmp_length = tmp_hours
else:
self.logger.errorReport("[Video] FFProbe could not get video content length")
tmp_length = None
return tmp_length
except:
self.logger.errorReport("[Video] There was a problem getting the length of video content")
return None